chromium/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java

// Copyright 2015 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;

import android.text.TextUtils;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

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.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileKey;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorSupplier;
import org.chromium.components.offline_items_collection.LaunchLocation;
import org.chromium.components.offlinepages.DeletePageResult;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Access gate to C++ side offline pages functionalities. */
@JNINamespace("offline_pages::android")
public class OfflinePageBridge {
    // These constants must be kept in sync with the constants defined in
    // //components/offline_pages/core/client_namespace_constants.cc
    public static final String ASYNC_NAMESPACE = "async_loading";
    public static final String BOOKMARK_NAMESPACE = "bookmark";
    public static final String LAST_N_NAMESPACE = "last_n";
    public static final String SHARE_NAMESPACE = "share";
    public static final String CCT_NAMESPACE = "custom_tabs";
    public static final String DOWNLOAD_NAMESPACE = "download";
    public static final String NTP_SUGGESTIONS_NAMESPACE = "ntp_suggestions";
    public static final String SUGGESTED_ARTICLES_NAMESPACE = "suggested_articles";
    public static final String BROWSER_ACTIONS_NAMESPACE = "browser_actions";
    public static final String LIVE_PAGE_SHARING_NAMESPACE = "live_page_sharing";

    private long mNativeOfflinePageBridge;
    private boolean mIsNativeOfflinePageModelLoaded;
    private final ObserverList<OfflinePageModelObserver> mObservers = new ObserverList<>();

    /**
     * Retrieves the OfflinePageBridge for the given profile, creating it the first time
     * getForProfile or getForProfileKey is called for the profile.  Must be called on the UI
     * thread.
     *
     * @param profile The profile associated with the OfflinePageBridge to get.
     */
    public static OfflinePageBridge getForProfile(Profile profile) {
        ThreadUtils.assertOnUiThread();

        if (profile == null) {
            return null;
        }

        return getForProfileKey(profile.getProfileKey());
    }

    /**
     * Retrieves the OfflinePageBridge for the profile with the given key, creating it the first
     * time getForProfile or getForProfileKey is called for the profile.  Must be called on the UI
     * thread.
     *
     * @param profileKey Key of the profile associated with the OfflinePageBridge to get.
     */
    public static OfflinePageBridge getForProfileKey(ProfileKey profileKey) {
        ThreadUtils.assertOnUiThread();

        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getOfflinePageBridgeForProfileKey(profileKey);
    }

    /** Callback used when saving an offline page. */
    public interface SavePageCallback {
        /**
         * Delivers result of saving a page.
         *
         * @param savePageResult Result of the saving. Uses {@see
         *     org.chromium.components.offlinepages.SavePageResult} enum.
         * @param url URL of the saved page.
         * @see OfflinePageBridge#savePage(WebContents, ClientId, OfflinePageOrigin,
         *     SavePageCallback)
         */
        @CalledByNative("SavePageCallback")
        void onSavePageDone(int savePageResult, @JniType("std::string") String url, long offlineId);
    }

    /** Base observer class listeners to be notified of changes to the offline page model. */
    public abstract static class OfflinePageModelObserver {
        /** Called when the native side of offline pages is loaded and now in usable state. */
        public void offlinePageModelLoaded() {}

        /**
         * Called when the native side of offline pages is changed due to adding, removing or
         * update an offline page.
         */
        public void offlinePageAdded(OfflinePageItem addedPage) {}

        /**
         * Called when an offline page is deleted. This can be called as a result of
         * #checkOfflinePageMetadata().
         * @param deletedPage Info about the deleted offline page.
         */
        public void offlinePageDeleted(DeletedPageInfo deletedPage) {}
    }

    /** Creates an offline page bridge for a given profile. */
    @VisibleForTesting
    protected OfflinePageBridge(long nativeOfflinePageBridge) {
        mNativeOfflinePageBridge = nativeOfflinePageBridge;
    }

    /**
     * Called by the native OfflinePageBridge so that it can cache the new Java OfflinePageBridge.
     */
    @CalledByNative
    private static OfflinePageBridge create(long nativeOfflinePageBridge) {
        return new OfflinePageBridge(nativeOfflinePageBridge);
    }

    /**
     * @return True if an offline copy of the given URL can be saved.
     */
    public static boolean canSavePage(GURL url) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get().canSavePage(url);
    }

    /**
     * @return the string representing the origin of the tab.
     */
    @CalledByNative
    private static @JniType("std::string") String getEncodedOriginApp(Tab tab) {
        return new OfflinePageOrigin(ContextUtils.getApplicationContext(), tab)
                .encodeAsJsonString();
    }

    /**
     * Adds an observer to offline page model changes.
     * @param observer The observer to be added.
     */
    public void addObserver(OfflinePageModelObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * Removes an observer to offline page model changes.
     * @param observer The observer to be removed.
     */
    public void removeObserver(OfflinePageModelObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Gets all available offline pages, returning results via the provided callback.
     *
     * @param callback The callback to run when the operation completes.
     */
    @VisibleForTesting
    public void getAllPages(final Callback<List<OfflinePageItem>> callback) {
        List<OfflinePageItem> result = new ArrayList<>();
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getAllPages(mNativeOfflinePageBridge, OfflinePageBridge.this, result, callback);
    }

    /**
     * Gets the offline pages associated with the provided client IDs.
     *
     * @param clientIds Client's IDs associated with offline pages.
     * @param callback The callback to run when the response is ready. It will be passed a list of
     *         {@link OfflinePageItem} matching the provided IDs, or an empty list if none exist.
     */
    @VisibleForTesting
    public void getPagesByClientIds(
            final List<ClientId> clientIds, final Callback<List<OfflinePageItem>> callback) {
        String[] namespaces = new String[clientIds.size()];
        String[] ids = new String[clientIds.size()];

        for (int i = 0; i < clientIds.size(); i++) {
            namespaces[i] = clientIds.get(i).getNamespace();
            ids[i] = clientIds.get(i).getId();
        }

        List<OfflinePageItem> result = new ArrayList<>();
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getPagesByClientId(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        result,
                        namespaces,
                        ids,
                        callback);
    }

    /**
     * Gets the offline pages associated with the provided origin.
     * @param origin The JSON-like string of the app's package name and encrypted signature hash.
     * @param callback The callback to run when the response is ready. It will be passed a list of
     *         {@link OfflinePageItem} matching the provided origin, or an empty list if none exist.
     */
    public void getPagesByRequestOrigin(String origin, Callback<List<OfflinePageItem>> callback) {
        List<OfflinePageItem> result = new ArrayList<>();
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getPagesByRequestOrigin(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, result, origin, callback);
    }

    /**
     * Gets the offline pages associated with the provided namespace.
     *
     * @param namespace The string form of the namespace to query.
     * @param callback The callback to run when the response is ready. It will be passed a list of
     *         {@link OfflinePageItem} matching the provided namespace, or an empty list if none
     * exist.
     */
    public void getPagesByNamespace(
            final String namespace, final Callback<List<OfflinePageItem>> callback) {
        List<OfflinePageItem> result = new ArrayList<>();
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getPagesByNamespace(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        result,
                        namespace,
                        callback);
    }

    /**
     * Get the offline page associated with the provided offline URL.
     *
     * @param onlineUrl URL of the page.
     * @param tabId Android tab ID.
     * @param callback callback to pass back the matching {@link OfflinePageItem} if found. Will
     *         pass back null if not.
     */
    public void selectPageForOnlineUrl(
            GURL onlineUrl, int tabId, Callback<OfflinePageItem> callback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .selectPageForOnlineUrl(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        onlineUrl,
                        tabId,
                        callback);
    }

    /**
     * Get the offline page associated with the provided offline ID.
     *
     * @param offlineId ID of the offline page.
     * @param callback callback to pass back the matching {@link OfflinePageItem} if found. Will
     *         pass back <code>null</code> if not.
     */
    public void getPageByOfflineId(final long offlineId, final Callback<OfflinePageItem> callback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getPageByOfflineId(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, offlineId, callback);
    }

    /**
     * Saves the web page loaded into web contents offline.
     * Retrieves the origin of the page from the WebContents.
     *
     * @param webContents Contents of the page to save.
     * @param clientId Client ID of the bookmark related to the offline page.
     * @param callback Interface that contains a callback. This may be called synchronously, e.g. if
     *         the web contents is already destroyed.
     * @see SavePageCallback
     */
    public void savePage(
            final WebContents webContents,
            final ClientId clientId,
            final SavePageCallback callback) {
        OfflinePageOrigin origin;
        Tab currentTab =
                TabModelSelectorSupplier.getCurrentTabFrom(webContents.getTopLevelNativeWindow());
        if (currentTab != null) {
            origin = new OfflinePageOrigin(ContextUtils.getApplicationContext(), currentTab);
        } else {
            origin = new OfflinePageOrigin();
        }
        savePage(webContents, clientId, origin, callback);
    }

    /**
     * Saves the web page loaded into web contents offline.
     *
     * @param webContents Contents of the page to save.
     * @param clientId Client ID of the bookmark related to the offline page.
     * @param origin The app that initiated the download.
     * @param callback Interface that contains a callback. This may be called synchronously, e.g. if
     *         the web contents is already destroyed.
     * @see SavePageCallback
     */
    public void savePage(
            final WebContents webContents,
            final ClientId clientId,
            final OfflinePageOrigin origin,
            final SavePageCallback callback) {
        assert mIsNativeOfflinePageModelLoaded;
        assert webContents != null;
        assert origin != null;

        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .savePage(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        callback,
                        webContents,
                        clientId.getNamespace(),
                        clientId.getId(),
                        origin.encodeAsJsonString());
    }

    /**
     * Deletes an offline page related to a specified bookmark.
     *
     * @param clientId Client ID for which the offline copy will be deleted.
     * @param callback Interface that contains a callback.
     */
    @VisibleForTesting
    public void deletePage(final ClientId clientId, Callback<Integer> callback) {
        assert mIsNativeOfflinePageModelLoaded;
        ArrayList<ClientId> ids = new ArrayList<ClientId>();
        ids.add(clientId);

        deletePagesByClientId(ids, callback);
    }

    /**
     * Deletes offline pages based on the list of provided client IDs. Calls the callback
     * when operation is complete. Requires that the model is already loaded.
     *
     * @param clientIds A list of Client IDs for which the offline pages will be deleted.
     * @param callback A callback that will be called once operation is completed.
     */
    public void deletePagesByClientId(List<ClientId> clientIds, Callback<Integer> callback) {
        String[] namespaces = new String[clientIds.size()];
        String[] ids = new String[clientIds.size()];

        for (int i = 0; i < clientIds.size(); i++) {
            namespaces[i] = clientIds.get(i).getNamespace();
            ids[i] = clientIds.get(i).getId();
        }

        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .deletePagesByClientId(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        namespaces,
                        ids,
                        callback);
    }

    /**
     * Deletes offline pages based on the list of provided client IDs only if they originate
     * from the same origin. Calls the callback when operation is complete. Requires that the
     * model is already loaded.
     *
     * @param clientIds A list of Client IDs for which the offline pages will be deleted.
     * @param callback A callback that will be called once operation is completed.
     */
    public void deletePagesByClientIdAndOrigin(
            List<ClientId> clientIds, OfflinePageOrigin origin, Callback<Integer> callback) {
        String[] namespaces = new String[clientIds.size()];
        String[] ids = new String[clientIds.size()];

        for (int i = 0; i < clientIds.size(); i++) {
            namespaces[i] = clientIds.get(i).getNamespace();
            ids[i] = clientIds.get(i).getId();
        }

        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .deletePagesByClientIdAndOrigin(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        namespaces,
                        ids,
                        origin.encodeAsJsonString(),
                        callback);
    }

    /**
     * Deletes offline pages based on the list of offline IDs. Calls the callback
     * when operation is complete. Note that offline IDs are not intended to be saved across
     * restarts of Chrome; they should be obtained by querying the model for the appropriate client
     * ID.
     *
     * @param offlineIdList A list of offline IDs of pages that will be deleted.
     * @param callback A callback that will be called once operation is completed, called with the
     *         DeletePageResult of the operation..
     */
    public void deletePagesByOfflineId(List<Long> offlineIdList, Callback<Integer> callback) {
        if (offlineIdList == null) {
            callback.onResult(Integer.valueOf(DeletePageResult.SUCCESS));
            return;
        }

        long[] offlineIds = new long[offlineIdList.size()];
        for (int i = 0; i < offlineIdList.size(); i++) {
            offlineIds[i] = offlineIdList.get(i).longValue();
        }
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .deletePagesByOfflineId(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, offlineIds, callback);
    }

    /**
     * Ask the native code to publish the internal page asychronously.
     * @param offlineId ID of the offline page to publish.
     * @param publishedCallback Function to call when publishing is done.  This will be called with
     *         the new path of the file.
     */
    public void publishInternalPageByOfflineId(long offlineId, Callback<String> publishedCallback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .publishInternalPageByOfflineId(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        offlineId,
                        publishedCallback);
    }

    /**
     * Ask the native code to publish the internal page asychronously.
     * @param guid Client ID of the offline page to publish.
     * @param publishedCallback Function to call when publishing is done.
     */
    public void publishInternalPageByGuid(String guid, Callback<String> publishedCallback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .publishInternalPageByGuid(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, guid, publishedCallback);
    }

    /** Whether or not the underlying offline page model is loaded. */
    public boolean isOfflinePageModelLoaded() {
        return mIsNativeOfflinePageModelLoaded;
    }

    /**
     * Retrieves the extra request header to reload the offline page.
     *
     * @param webContents Contents of the page to reload.
     * @return The extra request header string.
     */
    public @Nullable String getOfflinePageHeaderForReload(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getOfflinePageHeaderForReload(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * @param webContents Contents of the page to check.
     * @return True if an offline preview is being shown.
     */
    public boolean isShowingOfflinePreview(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isShowingOfflinePreview(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * @param webContents Contents of the page to check.
     * @return True if download button is being shown in the error page.
     */
    public boolean isShowingDownloadButtonInErrorPage(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isShowingDownloadButtonInErrorPage(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /** Tells the native side that the tab of |webContents| will be closed. */
    void willCloseTab(WebContents webContents) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .willCloseTab(mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * Schedules to download a page from |url| and categorize under |nameSpace|.
     * The duplicate pages or requests will be checked.
     * Origin is presumed to be Chrome.
     *
     * @param webContents Web contents upon which the infobar is shown.
     * @param nameSpace Namespace of the page to save.
     * @param url URL of the page to save.
     * @param uiAction UI action, like showing infobar or toast on certain case.
     */
    public void scheduleDownload(
            WebContents webContents, String nameSpace, String url, int uiAction) {
        scheduleDownload(webContents, nameSpace, url, uiAction, new OfflinePageOrigin());
    }

    /**
     * Schedules to download a page from |url| and categorize under |namespace| from |origin|.
     * The duplicate pages or requests will be checked.
     *
     * @param webContents Web contents upon which the infobar is shown.
     * @param nameSpace Namespace of the page to save.
     * @param url URL of the page to save.
     * @param uiAction UI action, like showing infobar or toast on certain case.
     * @param origin Origin of the page.
     */
    public void scheduleDownload(
            WebContents webContents,
            String nameSpace,
            String url,
            int uiAction,
            OfflinePageOrigin origin) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .scheduleDownload(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        webContents,
                        nameSpace,
                        url,
                        uiAction,
                        origin.encodeAsJsonString());
    }

    /**
     * Checks if an offline page is shown for the webContents.
     * @param webContents Web contents used to find the offline page.
     * @return True if the offline page is opened.
     */
    public boolean isOfflinePage(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isOfflinePage(mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * Returns whether |nameSpace| is a temporary namespace.
     * @param nameSpace Namespace of the page in question.
     * @return true if the page is in a temporary namespace.
     */
    public boolean isTemporaryNamespace(String nameSpace) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isTemporaryNamespace(mNativeOfflinePageBridge, OfflinePageBridge.this, nameSpace);
    }

    /**
     * Checks if the supplied file path is in a private dir internal to chrome.
     * @param filePath Path of the file to check.
     * @return True if the file is in a private directory.
     */
    public boolean isInPrivateDirectory(String filePath) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isInPrivateDirectory(mNativeOfflinePageBridge, OfflinePageBridge.this, filePath);
    }

    /**
     * Retrieves the offline page that is shown for the tab.
     * @param webContents Web contents used to find the offline page.
     * @return The offline page if tab currently displays it, null otherwise.
     */
    public @Nullable OfflinePageItem getOfflinePage(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getOfflinePage(mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * Get the the url params to open the offline page associated with the provided offline ID.
     * Depending on whether it is trusted or not, either http/https or file URL will be returned in
     * the callback.
     *
     * @param offlineId ID of the offline page.
     * @param location Where the offline page is launched.
     * @param callback callback to pass back the url string if found. Will pass back
     *         <code>null</code> if not.
     */
    public void getLoadUrlParamsByOfflineId(
            long offlineId, @LaunchLocation int location, Callback<LoadUrlParams> callback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getLoadUrlParamsByOfflineId(
                        mNativeOfflinePageBridge,
                        OfflinePageBridge.this,
                        offlineId,
                        location,
                        callback);
    }

    /**
     * Get the url params to open the intent carrying MHTML file or content.
     *
     * @param url The file:// or content:// URL.
     * @param callback Callback to pass back the url params.
     */
    public void getLoadUrlParamsForOpeningMhtmlFileOrContent(
            String url, Callback<LoadUrlParams> callback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .getLoadUrlParamsForOpeningMhtmlFileOrContent(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, url, callback);
    }

    /**
     * Checks if the web contents is showing a trusted offline page.
     * @param webContents Web contents shown.
     * @return True if a trusted offline page is shown.
     */
    public boolean isShowingTrustedOfflinePage(WebContents webContents) {
        return org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .isShowingTrustedOfflinePage(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, webContents);
    }

    /**
     * Tries to acquire the storage access permssion if not yet.
     *
     * @param webContents Contents of the page to check.
     * @param callback Callback to notify the result.
     */
    public void acquireFileAccessPermission(WebContents webContents, Callback<Boolean> callback) {
        org.chromium.chrome.browser.offlinepages.OfflinePageBridgeJni.get()
                .acquireFileAccessPermission(
                        mNativeOfflinePageBridge, OfflinePageBridge.this, webContents, callback);
    }

    @CalledByNative
    protected void offlinePageModelLoaded() {
        mIsNativeOfflinePageModelLoaded = true;
        for (OfflinePageModelObserver observer : mObservers) {
            observer.offlinePageModelLoaded();
        }
    }

    @CalledByNative
    protected void offlinePageAdded(OfflinePageItem addedPage) {
        for (OfflinePageModelObserver observer : mObservers) {
            observer.offlinePageAdded(addedPage);
        }
    }

    /** Removes references to the native OfflinePageBridge when it is being destroyed. */
    @CalledByNative
    protected void offlinePageBridgeDestroyed() {
        ThreadUtils.assertOnUiThread();
        assert mNativeOfflinePageBridge != 0;

        mIsNativeOfflinePageModelLoaded = false;
        mNativeOfflinePageBridge = 0;

        // TODO(dewittj): Add a model destroyed method to the observer interface.
        mObservers.clear();
    }

    @CalledByNative
    void offlinePageDeleted(DeletedPageInfo deletedPage) {
        for (OfflinePageModelObserver observer : mObservers) {
            observer.offlinePageDeleted(deletedPage);
        }
    }

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

    @CalledByNative
    private static OfflinePageItem createOfflinePageItem(
            @JniType("std::string") String url,
            long offlineId,
            @JniType("std::string") String clientNamespace,
            @JniType("std::string") String clientId,
            @JniType("std::u16string") String title,
            @JniType("std::string") String filePath,
            long fileSize,
            long creationTime,
            int accessCount,
            long lastAccessTimeMs,
            @JniType("std::string") String requestOrigin) {
        return new OfflinePageItem(
                url,
                offlineId,
                clientNamespace,
                clientId,
                title,
                filePath,
                fileSize,
                creationTime,
                accessCount,
                lastAccessTimeMs,
                requestOrigin);
    }

    @CalledByNative
    private static ClientId createClientId(
            @JniType("std::string") String clientNamespace, @JniType("std::string") String id) {
        return new ClientId(clientNamespace, id);
    }

    @CalledByNative
    private static DeletedPageInfo createDeletedPageInfo(
            long offlineId,
            @JniType("std::string") String clientNamespace,
            @JniType("std::string") String clientId,
            @JniType("std::string") String requestOrigin) {
        return new DeletedPageInfo(offlineId, clientNamespace, clientId, requestOrigin);
    }

    @CalledByNative
    private static LoadUrlParams createLoadUrlParams(
            @JniType("std::string") String url,
            @JniType("std::string") String extraHeaderKey,
            @JniType("std::string") String extraHeaderValue) {
        LoadUrlParams loadUrlParams = new LoadUrlParams(url);
        if (!TextUtils.isEmpty(extraHeaderKey) && !TextUtils.isEmpty(extraHeaderValue)) {
            // Set both map-based and collapsed headers to support all use scenarios.
            Map<String, String> headers = new HashMap<String, String>();
            headers.put(extraHeaderKey, extraHeaderValue);
            loadUrlParams.setExtraHeaders(headers);
            loadUrlParams.setVerbatimHeaders(extraHeaderKey + ":" + extraHeaderValue);
        }
        return loadUrlParams;
    }

    @NativeMethods
    interface Natives {
        boolean canSavePage(GURL url);

        OfflinePageBridge getOfflinePageBridgeForProfileKey(ProfileKey profileKey);

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

        void willCloseTab(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        void getPageByOfflineId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                long offlineId,
                Callback<OfflinePageItem> callback);

        void getPagesByClientId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                List<OfflinePageItem> result,
                String[] namespaces,
                String[] ids,
                Callback<List<OfflinePageItem>> callback);

        void getPagesByRequestOrigin(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                List<OfflinePageItem> result,
                @JniType("std::string") String requestOrigin,
                Callback<List<OfflinePageItem>> callback);

        void getPagesByNamespace(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                List<OfflinePageItem> result,
                @JniType("std::string") String nameSpace,
                Callback<List<OfflinePageItem>> callback);

        void deletePagesByClientId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                String[] namespaces,
                String[] ids,
                Callback<Integer> callback);

        void deletePagesByClientIdAndOrigin(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                String[] namespaces,
                String[] ids,
                @JniType("std::string") String origin,
                Callback<Integer> callback);

        void deletePagesByOfflineId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                long[] offlineIds,
                Callback<Integer> callback);

        void publishInternalPageByOfflineId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                long offlineId,
                Callback<String> publishedCallback);

        void publishInternalPageByGuid(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                @JniType("std::string") String guid,
                Callback<String> publishedCallback);

        void selectPageForOnlineUrl(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                GURL onlineUrl,
                int tabId,
                Callback<OfflinePageItem> callback);

        void savePage(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                SavePageCallback callback,
                WebContents webContents,
                @JniType("std::string") String clientNamespace,
                @JniType("std::string") String clientId,
                @JniType("std::string") String origin);

        @Nullable
        String getOfflinePageHeaderForReload(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        boolean isShowingOfflinePreview(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        boolean isShowingDownloadButtonInErrorPage(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        void scheduleDownload(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                WebContents webContents,
                @JniType("std::string") String nameSpace,
                @JniType("std::string") String url,
                int uiAction,
                @JniType("std::string") String origin);

        boolean isOfflinePage(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        boolean isInPrivateDirectory(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                @JniType("std::string") String filePath);

        boolean isTemporaryNamespace(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                @JniType("std::string") String nameSpace);

        OfflinePageItem getOfflinePage(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        void getLoadUrlParamsByOfflineId(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                long offlineId,
                int location,
                Callback<LoadUrlParams> callback);

        boolean isShowingTrustedOfflinePage(
                long nativeOfflinePageBridge, OfflinePageBridge caller, WebContents webContents);

        void getLoadUrlParamsForOpeningMhtmlFileOrContent(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                @JniType("std::string") String url,
                Callback<LoadUrlParams> callback);

        void acquireFileAccessPermission(
                long nativeOfflinePageBridge,
                OfflinePageBridge caller,
                WebContents webContents,
                Callback<Boolean> callback);
    }
}