chromium/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/evaluation/OfflinePageEvaluationBridge.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.offlinepages.evaluation;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.offlinepages.ClientId;
import org.chromium.chrome.browser.offlinepages.OfflinePageItem;
import org.chromium.chrome.browser.offlinepages.SavePageRequest;
import org.chromium.chrome.browser.profiles.Profile;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/** Class used for offline page evaluation testing tools. */
@JNINamespace("offline_pages::android")
public class OfflinePageEvaluationBridge {
    /**
     * Observer class for notifications on changes to save page requests or offline page model which
     * are used for testing.
     */
    public abstract static class OfflinePageEvaluationObserver {
        /** Event fired when the offline page model is loaded. */
        public void offlinePageModelLoaded() {}

        /**
         * Event fired when a new request is added.
         * @param request The newly added save page request.
         */
        public void savePageRequestAdded(SavePageRequest request) {}

        /**
         * Event fired when a request is completed.
         * @param request The completed request.
         * @param status The status of the completion, see
         * org.chromium.components.offlinepages.BackgroundSavePageResult.
         */
        public void savePageRequestCompleted(SavePageRequest request, int status) {}

        /**
         * Event fired when a new request is changed.
         * @param request The changed request.
         */
        public void savePageRequestChanged(SavePageRequest request) {}
    }

    /**
     * Get the instance of the evaluation bridge.
     * @param profile The profile used to get bridge.
     * @param useEvaluationScheduler True if using the evaluation scheduler instead of the
     *                               GCMNetworkManager one.
     */
    public OfflinePageEvaluationBridge(Profile profile, boolean useEvaluationScheduler) {
        ThreadUtils.assertOnUiThread();
        mNativeOfflinePageEvaluationBridge =
                OfflinePageEvaluationBridgeJni.get()
                        .createBridgeForProfile(
                                OfflinePageEvaluationBridge.this, profile, useEvaluationScheduler);
    }

    private static final String TAG = "OPEvalBridge";

    private static TaskRunner sSequencedTaskRunner =
            PostTask.createSequencedTaskRunner(TaskTraits.BEST_EFFORT_MAY_BLOCK);

    private long mNativeOfflinePageEvaluationBridge;
    private boolean mIsOfflinePageModelLoaded;
    private ObserverList<OfflinePageEvaluationObserver> mObservers =
            new ObserverList<OfflinePageEvaluationObserver>();

    private OutputStreamWriter mLogOutput;

    /** Destroys the native portion of the bridge. */
    public void destroy() {
        if (mNativeOfflinePageEvaluationBridge != 0) {
            OfflinePageEvaluationBridgeJni.get()
                    .destroy(mNativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge.this);
            mNativeOfflinePageEvaluationBridge = 0;
            mIsOfflinePageModelLoaded = false;
        }
        mObservers.clear();
    }

    /** Add an observer of the evaluation events. */
    public void addObserver(OfflinePageEvaluationObserver observer) {
        mObservers.addObserver(observer);
    }

    /** Remove an observer of evaluation events. */
    public void removeObserver(OfflinePageEvaluationObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Gets all pages in offline page model.
     * @param callback The callback would be invoked after the action completes and return with a
     *                 list of pages.
     */
    public void getAllPages(final Callback<List<OfflinePageItem>> callback) {
        List<OfflinePageItem> result = new ArrayList<>();
        OfflinePageEvaluationBridgeJni.get()
                .getAllPages(
                        mNativeOfflinePageEvaluationBridge,
                        OfflinePageEvaluationBridge.this,
                        result,
                        callback);
    }

    /**
     * Saves a url as offline page async.
     * @param url The url of the web page.
     * @param namespace The namespace to which the page belongs.
     * @param userRequest True if it's user-requested page.
     */
    public void savePageLater(final String url, final String namespace, boolean userRequested) {
        ClientId clientId = ClientId.createGuidClientIdForNamespace(namespace);
        OfflinePageEvaluationBridgeJni.get()
                .savePageLater(
                        mNativeOfflinePageEvaluationBridge,
                        OfflinePageEvaluationBridge.this,
                        url,
                        namespace,
                        clientId.getId(),
                        userRequested);
    }

    /**
     * Forces request coordinator to process the requests in the queue.
     * @param callback The callback would be invoked after the operation completes.
     * @return True if processing starts successfully and callback is expected to be called, false
     * otherwise.
     */
    public boolean pushRequestProcessing(final Callback<Boolean> callback) {
        return OfflinePageEvaluationBridgeJni.get()
                .pushRequestProcessing(
                        mNativeOfflinePageEvaluationBridge,
                        OfflinePageEvaluationBridge.this,
                        callback);
    }

    /**
     * Gets all requests in the queue.
     * @param callback The callback would be invoked with a list of requests which are in the queue.
     */
    public void getRequestsInQueue(Callback<SavePageRequest[]> callback) {
        OfflinePageEvaluationBridgeJni.get()
                .getRequestsInQueue(
                        mNativeOfflinePageEvaluationBridge,
                        OfflinePageEvaluationBridge.this,
                        callback);
    }

    /**
     * Removes requests from the queue by request ids.
     * @param requestIds The list of request ids to be deleted.
     * @param callback The callback would be invoked with number of successfully deleted ids.
     */
    public void removeRequestsFromQueue(List<Long> requestIds, Callback<Integer> callback) {
        long[] ids = new long[requestIds.size()];
        for (int i = 0; i < requestIds.size(); i++) {
            ids[i] = requestIds.get(i);
        }
        OfflinePageEvaluationBridgeJni.get()
                .removeRequestsFromQueue(
                        mNativeOfflinePageEvaluationBridge,
                        OfflinePageEvaluationBridge.this,
                        ids,
                        callback);
    }

    public void setLogOutputFile(File outputFile) throws IOException {
        // This open file operation shouldn't happen on UI thread.
        assert !ThreadUtils.runningOnUiThread();
        mLogOutput = new FileWriter(outputFile);
    }

    /**
     * @return True if the offline page model has fully loaded.
     */
    public boolean isOfflinePageModelLoaded() {
        return mIsOfflinePageModelLoaded;
    }

    @CalledByNative
    public void log(String sourceTag, String message) {
        Date date = new Date(System.currentTimeMillis());
        SimpleDateFormat formatter =
                new SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.getDefault());
        String logString =
                formatter.format(date)
                        + ": "
                        + sourceTag
                        + " | "
                        + message
                        + System.getProperty("line.separator");
        Log.d(TAG, logString);
        sSequencedTaskRunner.postTask(
                () -> {
                    try {
                        mLogOutput.write(logString);
                        mLogOutput.flush();
                    } catch (IOException e) {
                        Log.e(TAG, e.getMessage(), e);
                    }
                });
    }

    public void closeLog() {
        try {
            mLogOutput.close();
        } catch (IOException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    @CalledByNative
    void savePageRequestAdded(SavePageRequest request) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestAdded(request);
        }
    }

    @CalledByNative
    void savePageRequestCompleted(SavePageRequest request, int status) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestCompleted(request, status);
        }
    }

    @CalledByNative
    void savePageRequestChanged(SavePageRequest request) {
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.savePageRequestChanged(request);
        }
    }

    @CalledByNative
    void offlinePageModelLoaded() {
        mIsOfflinePageModelLoaded = true;
        for (OfflinePageEvaluationObserver observer : mObservers) {
            observer.offlinePageModelLoaded();
        }
    }

    @CalledByNative
    private static void createOfflinePageAndAddToList(
            List<OfflinePageItem> offlinePagesList,
            String url,
            long offlineId,
            String clientNamespace,
            String clientId,
            String title,
            String filePath,
            long fileSize,
            long creationTime,
            int accessCount,
            long lastAccessTimeMs,
            String requestOrigin) {
        offlinePagesList.add(
                createOfflinePageItem(
                        url,
                        offlineId,
                        clientNamespace,
                        clientId,
                        title,
                        filePath,
                        fileSize,
                        creationTime,
                        accessCount,
                        lastAccessTimeMs,
                        requestOrigin));
    }

    // This is added as a utility method in the bridge because SavePageRequest_jni.h is supposed
    // only to be included in one bridge (OfflinePageBridge). So as a testing bridge, this will be
    // used to create SavePageRequest on the native side.
    @CalledByNative
    private static SavePageRequest createSavePageRequest(
            int state, long requestId, String url, String clientIdNamespace, String clientIdId) {
        return SavePageRequest.create(state, requestId, url, clientIdNamespace, clientIdId);
    }

    private static OfflinePageItem createOfflinePageItem(
            String url,
            long offlineId,
            String clientNamespace,
            String clientId,
            String title,
            String filePath,
            long fileSize,
            long creationTime,
            int accessCount,
            long lastAccessTimeMs,
            String requestOrigin) {
        return new OfflinePageItem(
                url,
                offlineId,
                clientNamespace,
                clientId,
                title,
                filePath,
                fileSize,
                creationTime,
                accessCount,
                lastAccessTimeMs,
                requestOrigin);
    }

    @NativeMethods
    interface Natives {
        long createBridgeForProfile(
                OfflinePageEvaluationBridge caller,
                @JniType("Profile*") Profile profile,
                boolean useEvaluationScheduler);

        void destroy(long nativeOfflinePageEvaluationBridge, OfflinePageEvaluationBridge caller);

        void getAllPages(
                long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller,
                List<OfflinePageItem> offlinePages,
                final Callback<List<OfflinePageItem>> callback);

        void savePageLater(
                long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller,
                String url,
                String clientNamespace,
                String clientId,
                boolean userRequested);

        boolean pushRequestProcessing(
                long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller,
                Callback<Boolean> callback);

        void getRequestsInQueue(
                long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller,
                final Callback<SavePageRequest[]> callback);

        void removeRequestsFromQueue(
                long nativeOfflinePageEvaluationBridge,
                OfflinePageEvaluationBridge caller,
                long[] requestIds,
                final Callback<Integer> callback);
    }
}