// Copyright 2015 The Chromium Authors. All rights reserved. // 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 org.chromium.base.Callback; import org.chromium.base.ObserverList; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.JNINamespace; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.content_public.browser.WebContents; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Access gate to C++ side offline pages functionalities. */ @JNINamespace("offline_pages::android") public class OfflinePageBridge { public static final String BOOKMARK_NAMESPACE = "bookmark"; public static final String SHARE_NAMESPACE = "share"; /** * Retrieves the OfflinePageBridge for the given profile, creating it the first time * getForProfile is called for a given 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(); return nativeGetOfflinePageBridgeForProfile(profile); } private long mNativeOfflinePageBridge; private boolean mIsNativeOfflinePageModelLoaded; private final ObserverList<OfflinePageModelObserver> mObservers = new ObserverList<OfflinePageModelObserver>(); /** Whether an offline sub-feature is enabled or not. */ private static Boolean sOfflineBookmarksEnabled; private static Boolean sBackgroundLoadingEnabled; private static Boolean sIsPageSharingEnabled; /** * 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() */ @CalledByNative("SavePageCallback") void onSavePageDone(int savePageResult, 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 offlinePageModelChanged() {} /** * Called when an offline page is deleted. This can be called as a result of * #checkOfflinePageMetadata(). * @param offlineId The offline ID of the deleted offline page. * @param clientId The client supplied ID of the deleted offline page. */ public void offlinePageDeleted(long offlineId, ClientId clientId) {} } /** * Creates an offline page bridge for a given profile. * Accessible by the package for testability. */ @VisibleForTesting 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 saving bookmarked pages for offline viewing is enabled. */ public static boolean isOfflineBookmarksEnabled() { ThreadUtils.assertOnUiThread(); if (sOfflineBookmarksEnabled == null) { sOfflineBookmarksEnabled = nativeIsOfflineBookmarksEnabled(); } return sOfflineBookmarksEnabled; } /** * @return True if saving offline pages in the background is enabled. */ @VisibleForTesting public static boolean isBackgroundLoadingEnabled() { ThreadUtils.assertOnUiThread(); if (sBackgroundLoadingEnabled == null) { sBackgroundLoadingEnabled = nativeIsBackgroundLoadingEnabled(); } return sBackgroundLoadingEnabled; } /** * @return True if offline pages sharing is enabled. */ @VisibleForTesting public static boolean isPageSharingEnabled() { ThreadUtils.assertOnUiThread(); if (sIsPageSharingEnabled == null) { sIsPageSharingEnabled = nativeIsPageSharingEnabled(); } return sIsPageSharingEnabled; } /** * @return True if an offline copy of the given URL can be saved. */ public static boolean canSavePage(String url) { return nativeCanSavePage(url); } /** * 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 void getAllPages(final Callback<List<OfflinePageItem>> callback) { List<OfflinePageItem> result = new ArrayList<>(); nativeGetAllPages(mNativeOfflinePageBridge, result, callback); } /** @return A list of all offline ids that match a particular (namespace, client_id) pair. */ Set<Long> getOfflineIdsForClientId(ClientId clientId) { assert mIsNativeOfflinePageModelLoaded; long[] offlineIds = nativeGetOfflineIdsForClientId( mNativeOfflinePageBridge, clientId.getNamespace(), clientId.getId()); Set<Long> result = new HashSet<>(offlineIds.length); for (long id : offlineIds) { result.add(id); } return result; } /** * Gets the offline pages associated with the provided client IDs. * * @param clientIds Client's IDs associated with offline pages. * @return 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) { runWhenLoaded(new Runnable() { @Override public void run() { List<OfflinePageItem> result = new ArrayList<>(); for (ClientId clientId : clientIds) { result.addAll(getPagesByClientIdInternal(clientId)); } callback.onResult(result); } }); } /** * Gets all the URLs in the request queue. * * @return A list of {@link SavePageRequest} representing all the queued requests. */ @VisibleForTesting public void getRequestsInQueue(Callback<SavePageRequest[]> callback) { nativeGetRequestsInQueue(mNativeOfflinePageBridge, callback); } private static class RequestsRemovedCallback { private Callback<List<RequestRemovedResult>> mCallback; public RequestsRemovedCallback(Callback<List<RequestRemovedResult>> callback) { mCallback = callback; } @CalledByNative("RequestsRemovedCallback") public void onResult(long[] resultIds, int[] resultCodes) { assert resultIds.length == resultCodes.length; List<RequestRemovedResult> results = new ArrayList<>(); for (int i = 0; i < resultIds.length; i++) { results.add(new RequestRemovedResult(resultIds[i], resultCodes[i])); } mCallback.onResult(results); } } /** * Contains a result for a remove page request. */ public static class RequestRemovedResult { private long mRequestId; private int mUpdateRequestResult; public RequestRemovedResult(long requestId, int requestResult) { mRequestId = requestId; mUpdateRequestResult = requestResult; } /** Request ID as found in the SavePageRequest. */ public long getRequestId() { return mRequestId; } /** {@see org.chromium.components.offlinepages.background.UpdateRequestResult} enum. */ public int getUpdateRequestResult() { return mUpdateRequestResult; } } /** * Removes SavePageRequests from the request queue. * * The callback will be called with |null| in the case that the queue is unavailable. This can * happen in incognito, for example. * * @param requestIds The IDs of the requests to remove. * @param callback Called when the removal is done, with the SavePageRequest objects that were * actually removed. */ public void removeRequestsFromQueue( List<Long> requestIdList, Callback<List<RequestRemovedResult>> callback) { long[] requestIds = new long[requestIdList.size()]; for (int i = 0; i < requestIdList.size(); i++) { requestIds[i] = requestIdList.get(i).longValue(); } nativeRemoveRequestsFromQueue( mNativeOfflinePageBridge, requestIds, new RequestsRemovedCallback(callback)); } private List<OfflinePageItem> getPagesByClientIdInternal(ClientId clientId) { Set<Long> ids = getOfflineIdsForClientId(clientId); List<OfflinePageItem> result = new ArrayList<>(); for (long offlineId : ids) { // TODO(dewittj): Restructure the native API to avoid this loop with a native call. OfflinePageItem item = nativeGetPageByOfflineId(mNativeOfflinePageBridge, offlineId); if (item != null) { result.add(item); } } return result; } /** * 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(String onlineUrl, int tabId, Callback<OfflinePageItem> callback) { nativeSelectPageForOnlineUrl(mNativeOfflinePageBridge, 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) { runWhenLoaded(new Runnable() { @Override public void run() { OfflinePageItem item = nativeGetPageByOfflineId(mNativeOfflinePageBridge, offlineId); callback.onResult(item); } }); } /** * Saves the web page loaded into web contents offline. * * @param webContents Contents of the page to save. * @param ClientId 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) { assert mIsNativeOfflinePageModelLoaded; assert webContents != null; nativeSavePage(mNativeOfflinePageBridge, callback, webContents, clientId.getNamespace(), clientId.getId()); } /** * Save the given URL as an offline page when the network becomes available. * * The page is marked as not having been saved by the user. Use the 3-argument form to specify * a user request. * * @param url The given URL to save for later. * @param clientId The client ID for the offline page to be saved later. */ @VisibleForTesting public void savePageLater(String url, ClientId clientId) { savePageLater(url, clientId, true); } /** * Save the given URL as an offline page when the network becomes available. * * @param url The given URL to save for later. * @param clientId The client ID for the offline page to be saved later. * @param userRequested Whether this request should be prioritized because the user explicitly * requested it. */ public void savePageLater(final String url, final ClientId clientId, boolean userRequested) { nativeSavePageLater(mNativeOfflinePageBridge, url, clientId.getNamespace(), clientId.getId(), userRequested); } /** * Save the given URL as an offline page when the network becomes available with a randomly * generated clientId in the given namespace. * * @param url The given URL to save for later. * @param namespace The namespace for the offline page to be saved later. * @param userRequested Whether this request should be prioritized because the user explicitly * requested it. */ public void savePageLater(final String url, final String namespace, boolean userRequested) { ClientId clientId = ClientId.createGuidClientIdForNamespace(namespace); savePageLater(url, clientId, true /* userRequested */); } /** * 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) { assert mIsNativeOfflinePageModelLoaded; List<Long> idList = new ArrayList<>(clientIds.size()); for (ClientId clientId : clientIds) { idList.addAll(getOfflineIdsForClientId(clientId)); } deletePages(idList, callback); } void deletePages(List<Long> offlineIds, Callback<Integer> callback) { long[] ids = new long[offlineIds.size()]; for (int i = 0; i < offlineIds.size(); i++) { ids[i] = offlineIds.get(i); } nativeDeletePages(mNativeOfflinePageBridge, callback, ids); } /** * 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 String getOfflinePageHeaderForReload(WebContents webContents) { return nativeGetOfflinePageHeaderForReload(mNativeOfflinePageBridge, webContents); } private static class CheckPagesExistOfflineCallbackInternal { private Callback<Set<String>> mCallback; CheckPagesExistOfflineCallbackInternal(Callback<Set<String>> callback) { mCallback = callback; } @CalledByNative("CheckPagesExistOfflineCallbackInternal") public void onResult(String[] results) { Set<String> resultSet = new HashSet<>(); Collections.addAll(resultSet, results); mCallback.onResult(resultSet); } } /** * Returns via callback any urls in <code>urls</code> for which there exist offline pages. * * TODO(http://crbug.com/598006): Add metrics for preventing UI jank. */ public void checkPagesExistOffline(Set<String> urls, Callback<Set<String>> callback) { String[] urlArray = urls.toArray(new String[urls.size()]); CheckPagesExistOfflineCallbackInternal callbackInternal = new CheckPagesExistOfflineCallbackInternal(callback); nativeCheckPagesExistOffline(mNativeOfflinePageBridge, urlArray, callbackInternal); } @VisibleForTesting ClientId getClientIdForOfflineId(long offlineId) { OfflinePageItem item = nativeGetPageByOfflineId(mNativeOfflinePageBridge, offlineId); if (item != null) { return item.getClientId(); } return null; } private void runWhenLoaded(final Runnable runnable) { if (isOfflinePageModelLoaded()) { ThreadUtils.postOnUiThread(runnable); return; } addObserver(new OfflinePageModelObserver() { @Override public void offlinePageModelLoaded() { removeObserver(this); runnable.run(); } }); } @VisibleForTesting static void setOfflineBookmarksEnabledForTesting(boolean enabled) { sOfflineBookmarksEnabled = enabled; } @CalledByNative void offlinePageModelLoaded() { mIsNativeOfflinePageModelLoaded = true; for (OfflinePageModelObserver observer : mObservers) { observer.offlinePageModelLoaded(); } } @CalledByNative private void offlinePageModelChanged() { for (OfflinePageModelObserver observer : mObservers) { observer.offlinePageModelChanged(); } } /** * Removes references to the native OfflinePageBridge when it is being destroyed. */ @CalledByNative private 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(long offlineId, ClientId clientId) { for (OfflinePageModelObserver observer : mObservers) { observer.offlinePageDeleted(offlineId, clientId); } } @CalledByNative private static void createOfflinePageAndAddToList(List<OfflinePageItem> offlinePagesList, String url, long offlineId, String clientNamespace, String clientId, String filePath, long fileSize, long creationTime, int accessCount, long lastAccessTimeMs) { offlinePagesList.add(createOfflinePageItem(url, offlineId, clientNamespace, clientId, filePath, fileSize, creationTime, accessCount, lastAccessTimeMs)); } @CalledByNative private static OfflinePageItem createOfflinePageItem(String url, long offlineId, String clientNamespace, String clientId, String filePath, long fileSize, long creationTime, int accessCount, long lastAccessTimeMs) { return new OfflinePageItem(url, offlineId, clientNamespace, clientId, filePath, fileSize, creationTime, accessCount, lastAccessTimeMs); } @CalledByNative private static ClientId createClientId(String clientNamespace, String id) { return new ClientId(clientNamespace, id); } private static native boolean nativeIsOfflineBookmarksEnabled(); private static native boolean nativeIsBackgroundLoadingEnabled(); private static native boolean nativeIsPageSharingEnabled(); private static native boolean nativeCanSavePage(String url); private static native OfflinePageBridge nativeGetOfflinePageBridgeForProfile(Profile profile); @VisibleForTesting native void nativeGetAllPages(long nativeOfflinePageBridge, List<OfflinePageItem> offlinePages, final Callback<List<OfflinePageItem>> callback); private native void nativeCheckPagesExistOffline(long nativeOfflinePageBridge, Object[] urls, CheckPagesExistOfflineCallbackInternal callback); @VisibleForTesting native long[] nativeGetOfflineIdsForClientId( long nativeOfflinePageBridge, String clientNamespace, String clientId); @VisibleForTesting native void nativeGetRequestsInQueue( long nativeOfflinePageBridge, Callback<SavePageRequest[]> callback); @VisibleForTesting native void nativeRemoveRequestsFromQueue( long nativeOfflinePageBridge, long[] requestIds, RequestsRemovedCallback callback); @VisibleForTesting native OfflinePageItem nativeGetPageByOfflineId(long nativeOfflinePageBridge, long offlineId); private native void nativeSelectPageForOnlineUrl( long nativeOfflinePageBridge, String onlineUrl, int tabId, Callback<OfflinePageItem> callback); private native void nativeSavePage(long nativeOfflinePageBridge, SavePageCallback callback, WebContents webContents, String clientNamespace, String clientId); private native void nativeSavePageLater(long nativeOfflinePageBridge, String url, String clientNamespace, String clientId, boolean userRequested); private native void nativeDeletePages( long nativeOfflinePageBridge, Callback<Integer> callback, long[] offlineIds); private native String nativeGetOfflinePageHeaderForReload( long nativeOfflinePageBridge, WebContents webContents); }