blob: ddc5c00d6162f6d983d5405eb26c31e1470c2fc1 [file] [log] [blame]
// 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 android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.FileProviderHelper;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareParams;
import org.chromium.chrome.browser.snackbar.Snackbar;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
import org.chromium.chrome.browser.util.ChromeFileProvider;
import org.chromium.chrome.browser.util.UrlConstants;
import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.offline_items_collection.LaunchLocation;
import org.chromium.components.offlinepages.SavePageResult;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.WebContents;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.base.PageTransition;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A class holding static util functions for offline pages.
*/
public class OfflinePageUtils {
private static final String TAG = "OfflinePageUtils";
/** Background task tag to differentiate from other task types */
public static final String TASK_TAG = "OfflinePageUtils";
private static final int DEFAULT_SNACKBAR_DURATION_MS = 6 * 1000; // 6 second
/**
* Bit flags to be OR-ed together to build the context of a tab restore to be used to identify
* the appropriate TabRestoreType in a lookup table.
*/
private static final int BIT_ONLINE = 1;
private static final int BIT_CANT_SAVE_OFFLINE = 1 << 2;
private static final int BIT_OFFLINE_PAGE = 1 << 3;
private static final int BIT_LAST_N = 1 << 4;
// Used instead of the constant so tests can override the value.
private static int sSnackbarDurationMs = DEFAULT_SNACKBAR_DURATION_MS;
/** Instance carrying actual implementation of utility methods. */
private static Internal sInstance;
/**
* Tracks the observers of ChromeActivity's TabModelSelectors. This is weak so the activity can
* be garbage collected without worrying about this map. The RecentTabTracker is held here so
* that it can be destroyed when the ChromeActivity gets a new TabModelSelector.
*/
private static Map<ChromeActivity, RecentTabTracker> sTabModelObservers = new HashMap<>();
/**
* Interface for implementation of offline page utilities, that can be implemented for testing.
* We are using an internal interface, so that instance methods can have the same names as
* static methods.
*/
@VisibleForTesting
interface Internal {
/** Returns offline page bridge for specified profile. */
OfflinePageBridge getOfflinePageBridge(Profile profile);
/** Returns whether the network is connected. */
boolean isConnected();
/**
* Checks if an offline page is shown for the tab. This page could be either trusted or
* untrusted.
* @param tab The tab to be reloaded.
* @return True if the offline page is opened.
*/
boolean isOfflinePage(Tab tab);
/**
* Returns whether the tab is showing trusted offline page.
* @param tab The current tab.
* @return True if a trusted offline page is shown in the tab.
*/
boolean isShowingTrustedOfflinePage(Tab tab);
/**
* Returns whether the tab is showing offline preview.
* @param tab The current tab.
*/
boolean isShowingOfflinePreview(Tab tab);
/**
* Shows the "reload" snackbar for the given tab.
* @param context The application context.
* @param snackbarManager Class that shows the snackbar.
* @param snackbarController Class to control the snackbar.
* @param tabId Id of a tab that the snackbar is related to.
*/
void showReloadSnackbar(Context context, SnackbarManager snackbarManager,
final SnackbarController snackbarController, int tabId);
}
private static class OfflinePageUtilsImpl implements Internal {
@Override
public OfflinePageBridge getOfflinePageBridge(Profile profile) {
return OfflinePageBridge.getForProfile(profile);
}
@Override
public boolean isConnected() {
return NetworkChangeNotifier.isOnline();
}
@Override
public boolean isOfflinePage(Tab tab) {
if (tab == null) return false;
WebContents webContents = tab.getWebContents();
if (webContents == null) return false;
OfflinePageBridge offlinePageBridge =
getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return false;
return offlinePageBridge.isOfflinePage(webContents);
}
@Override
public boolean isShowingOfflinePreview(Tab tab) {
OfflinePageBridge offlinePageBridge = getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return false;
return offlinePageBridge.isShowingOfflinePreview(tab.getWebContents());
}
@Override
public void showReloadSnackbar(Context context, SnackbarManager snackbarManager,
final SnackbarController snackbarController, int tabId) {
if (tabId == Tab.INVALID_TAB_ID) return;
Log.d(TAG, "showReloadSnackbar called with controller " + snackbarController);
Snackbar snackbar =
Snackbar.make(context.getString(R.string.offline_pages_viewing_offline_page),
snackbarController, Snackbar.TYPE_ACTION,
Snackbar.UMA_OFFLINE_PAGE_RELOAD)
.setSingleLine(false)
.setAction(context.getString(R.string.reload), tabId);
snackbar.setDuration(sSnackbarDurationMs);
snackbarManager.showSnackbar(snackbar);
}
@Override
public boolean isShowingTrustedOfflinePage(Tab tab) {
if (tab == null) return false;
WebContents webContents = tab.getWebContents();
if (webContents == null) return false;
OfflinePageBridge offlinePageBridge = getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return false;
return offlinePageBridge.isShowingTrustedOfflinePage(tab.getWebContents());
}
}
/**
* Contains values from the histogram enum OfflinePagesTabRestoreType used for reporting the
* OfflinePages.TabRestore metric.
*/
@IntDef({TabRestoreType.WHILE_ONLINE, TabRestoreType.WHILE_ONLINE_CANT_SAVE_FOR_OFFLINE_USAGE,
TabRestoreType.WHILE_ONLINE_TO_OFFLINE_PAGE,
TabRestoreType.WHILE_ONLINE_TO_OFFLINE_PAGE_FROM_LAST_N, TabRestoreType.WHILE_OFFLINE,
TabRestoreType.WHILE_OFFLINE_CANT_SAVE_FOR_OFFLINE_USAGE,
TabRestoreType.WHILE_OFFLINE_TO_OFFLINE_PAGE,
TabRestoreType.WHILE_OFFLINE_TO_OFFLINE_PAGE_FROM_LAST_N, TabRestoreType.FAILED,
TabRestoreType.CRASHED})
@Retention(RetentionPolicy.SOURCE)
private @interface TabRestoreType {
int WHILE_ONLINE = 0;
int WHILE_ONLINE_CANT_SAVE_FOR_OFFLINE_USAGE = 1;
int WHILE_ONLINE_TO_OFFLINE_PAGE = 2;
int WHILE_ONLINE_TO_OFFLINE_PAGE_FROM_LAST_N = 3;
int WHILE_OFFLINE = 4;
int WHILE_OFFLINE_CANT_SAVE_FOR_OFFLINE_USAGE = 5;
int WHILE_OFFLINE_TO_OFFLINE_PAGE = 6;
int WHILE_OFFLINE_TO_OFFLINE_PAGE_FROM_LAST_N = 7;
int FAILED = 8;
int CRASHED = 9;
// NOTE: always keep this entry at the end. Add new result types only immediately above this
// line. Make sure to update the corresponding histogram enum accordingly.
int NUM_ENTRIES = 10;
}
private static Internal getInstance() {
if (sInstance == null) {
sInstance = new OfflinePageUtilsImpl();
}
return sInstance;
}
/**
* Returns false if offline pages features should be disabled.
*/
public static boolean isEnabled() {
return org.chromium.chrome.browser.BuildConfig.ENABLE_OFFLINE_PAGES;
}
/**
* Returns the number of free bytes on the storage.
*/
public static long getFreeSpaceInBytes() {
return Environment.getDataDirectory().getUsableSpace();
}
/**
* Returns the number of total bytes on the storage.
*/
public static long getTotalSpaceInBytes() {
return Environment.getDataDirectory().getTotalSpace();
}
/** Returns whether the network is connected. */
public static boolean isConnected() {
return getInstance().isConnected();
}
/*
* Save an offline copy for the bookmarked page asynchronously.
*
* @param bookmarkId The ID of the page to save an offline copy.
* @param tab A {@link Tab} object.
* @param callback The callback to be invoked when the offline copy is saved.
*/
public static void saveBookmarkOffline(BookmarkId bookmarkId, Tab tab) {
// If bookmark ID is missing there is nothing to save here.
if (bookmarkId == null) return;
// Making sure tab is worth keeping.
if (shouldSkipSavingTabOffline(tab)) return;
OfflinePageBridge offlinePageBridge = getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return;
WebContents webContents = tab.getWebContents();
ClientId clientId = ClientId.createClientIdForBookmarkId(bookmarkId);
offlinePageBridge.savePage(webContents, clientId, new OfflinePageBridge.SavePageCallback() {
@Override
public void onSavePageDone(int savePageResult, String url, long offlineId) {
// Result of the call is ignored.
}
});
}
/**
* Indicates whether we should skip saving the given tab as an offline page.
* A tab shouldn't be saved offline if it shows an error page or a sad tab page.
*/
private static boolean shouldSkipSavingTabOffline(Tab tab) {
WebContents webContents = tab.getWebContents();
return tab.isShowingErrorPage() || SadTab.isShowing(tab) || webContents == null
|| webContents.isDestroyed() || webContents.isIncognito();
}
/**
* Shows the snackbar for the current tab to provide offline specific information if needed.
* @param tab The current tab.
*/
public static void showOfflineSnackbarIfNecessary(Tab tab) {
// Set up the tab observer to watch for the tab being shown (not hidden) and a valid
// connection. When both conditions are met a snackbar is shown.
OfflinePageTabObserver.addObserverForTab(tab);
}
protected void showReloadSnackbarInternal(Context context, SnackbarManager snackbarManager,
final SnackbarController snackbarController, int tabId) {}
/**
* Shows the "reload" snackbar for the given tab.
* @param context The application context.
* @param snackbarManager Class that shows the snackbar.
* @param snackbarController Class to control the snackbar.
* @param tabId Id of a tab that the snackbar is related to.
*/
public static void showReloadSnackbar(Context context, SnackbarManager snackbarManager,
final SnackbarController snackbarController, int tabId) {
getInstance().showReloadSnackbar(context, snackbarManager, snackbarController, tabId);
}
/**
* Records UMA data for publishing internal page during sharing.
* Most of the recording are in JNI layer, since it's a point that can be used by both ways of
* sharing a page.
* TODO(romax): See if we can merge that.
* @param result The result for publishing file.
*/
public static void recordPublishPageResult(int result) {
// TODO(https://crbug.com/894714): Find a safer way to define the boundary value when
// using MAX_VALUE.
RecordHistogram.recordEnumeratedHistogram("OfflinePages.Sharing.PublishInternalPageResult",
result, SavePageResult.MAX_VALUE + 1);
}
/**
* Save the page loaded in current tab and share the saved page.
*
* @param activity The activity used for sharing and file provider interaction.
* @param currentTab The current tab from which the page is being shared.
* @param shareCallback The callback to be used to send the ShareParams. This will only be
* called if this function call returns true.
* @return true if the sharing of the page is possible. The callback will be invoked if
* saving the page succeeds.
*/
public static boolean saveAndSharePage(
final Activity activity, Tab tab, final Callback<ShareParams> shareCallback) {
OfflinePageBridge offlinePageBridge = getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) {
Log.e(TAG, "Unable to share current tab as an offline page.");
return false;
}
WebContents webContents = tab.getWebContents();
if (webContents == null) return false;
GetPagesByNamespaceForLivePageSharingCallback callback =
new GetPagesByNamespaceForLivePageSharingCallback(
activity, tab, shareCallback, offlinePageBridge);
offlinePageBridge.getPagesByNamespace(
OfflinePageBridge.LIVE_PAGE_SHARING_NAMESPACE, callback);
return true;
}
private static void getOfflinePageUriForSharing(String tabUrl, boolean isPageTemporary,
String offlinePagePath, Callback<Uri> callback) {
// Ensure that we have a file path that is longer than just "/".
if (isPageTemporary && offlinePagePath.length() > 1) {
// We share temporary pages by content URI to prevent unanticipated side effects in the
// public directory.
// Avoid file access (getContentUriFromFile()) from UI thread.
PostTask.postTask(TaskTraits.USER_VISIBLE_MAY_BLOCK, () -> {
File file = new File(offlinePagePath);
// We might get an exception if chrome does not have sharing roots configured. If
// so, just share by URL of the original page instead of sharing the offline page.
Uri uri;
try {
uri = (new FileProviderHelper()).getContentUriFromFile(file);
} catch (Exception e) {
uri = Uri.parse(tabUrl);
}
final Uri finalUri = uri;
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> callback.onResult(finalUri));
});
} else {
callback.onResult(Uri.parse(tabUrl));
}
}
/**
* If possible, creates the ShareParams needed to share the current offline page loaded in the
* provided tab as a MHTML file.
*
* @param activity The activity used for sharing and file provider interaction.
* @param currentTab The current tab from which the page is being shared.
* @param shareCallback The callback invoked when either sharing is complete, or when sharing
* cannot be completed. If sharing cannot be done, the callback parameter
* is null. May either be invoked from within the function call, or
* afterwards via PostTask.
*/
public static void maybeShareOfflinePage(
final Activity activity, Tab tab, final Callback<ShareParams> shareCallback) {
if (tab == null) {
shareCallback.onResult(null);
return;
}
boolean isOfflinePage = OfflinePageUtils.isOfflinePage(tab);
RecordHistogram.recordBooleanHistogram("OfflinePages.SharedPageWasOffline", isOfflinePage);
// If the current tab is not showing an offline page, try to see if we should do live page
// sharing.
if (!isOfflinePage) {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.OFFLINE_PAGES_LIVE_PAGE_SHARING)) {
if (!saveAndSharePage(activity, tab, shareCallback)) {
shareCallback.onResult(null);
}
return;
} else {
shareCallback.onResult(null);
return;
}
}
OfflinePageBridge offlinePageBridge = getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) {
Log.e(TAG, "Unable to share current tab as an offline page.");
shareCallback.onResult(null);
return;
}
WebContents webContents = tab.getWebContents();
if (webContents == null) {
shareCallback.onResult(null);
return;
}
OfflinePageItem offlinePage = offlinePageBridge.getOfflinePage(webContents);
if (offlinePage == null) {
shareCallback.onResult(null);
return;
}
String offlinePath = offlinePage.getFilePath();
boolean isPageTemporary =
offlinePageBridge.isTemporaryNamespace(offlinePage.getClientId().getNamespace());
String tabTitle = tab.getTitle();
getOfflinePageUriForSharing(tab.getUrl(), isPageTemporary, offlinePath,
(Uri uri)
-> maybeShareOfflinePageWithUri(activity, tabTitle, webContents,
offlinePageBridge, offlinePage, isPageTemporary, shareCallback,
uri));
}
// A continuation of maybeShareOfflinePage, after the URI is determined in a background thread.
private static void maybeShareOfflinePageWithUri(Activity activity, String tabTitle,
WebContents webContents, OfflinePageBridge offlinePageBridge,
OfflinePageItem offlinePage, boolean isPageTemporary,
Callback<ShareParams> shareCallback, Uri uri) {
if (!isOfflinePageShareable(offlinePageBridge, offlinePage, uri)) {
shareCallback.onResult(null);
return;
}
String offlinePath = offlinePage.getFilePath();
if (isPageTemporary || !offlinePageBridge.isInPrivateDirectory(offlinePath)) {
// Share temporary pages and pages already in a public location.
final File offlinePageFile = new File(offlinePath);
sharePage(activity, uri.toString(), tabTitle, offlinePath, offlinePageFile,
shareCallback);
return;
}
// The file access permission is needed since we may need to publish the archive
// file if it resides in internal directory.
offlinePageBridge.acquireFileAccessPermission(webContents, (granted) -> {
if (!granted) {
recordPublishPageResult(SavePageResult.PERMISSION_DENIED);
return;
}
// If the page is not in a public location, we must publish it before
// sharing it.
publishThenShareInternalPage(activity, offlinePageBridge, offlinePage, shareCallback);
});
}
/**
* Check to see if the offline page is sharable.
* @param offlinePageBridge Bridge to native code for offline pages use.
* @param offlinePage Page to check for sharability.
* @param pageUri Uri of the page to check.
* @return true if this page can be shared.
*/
public static boolean isOfflinePageShareable(
OfflinePageBridge offlinePageBridge, OfflinePageItem offlinePage, Uri uri) {
// Return false if there is no offline page.
if (offlinePage == null) return false;
String offlinePath = offlinePage.getFilePath();
// If we have a content or file Uri, then we can share the page.
if (isSchemeContentOrFile(uri)) {
return true;
}
// If the scheme is not one we recognize, return false.
if (!TextUtils.equals(uri.getScheme(), UrlConstants.HTTP_SCHEME)
&& !TextUtils.equals(uri.getScheme(), UrlConstants.HTTPS_SCHEME))
return false;
// If we have a http or https page with no file path, we cannot share it.
if (offlinePath.isEmpty()) {
Log.w(TAG, "Tried to share a page with no path.");
return false;
}
return true;
}
// Returns true if the scheme of the URI is either content or file.
private static boolean isSchemeContentOrFile(Uri uri) {
boolean isContentScheme = TextUtils.equals(uri.getScheme(), UrlConstants.CONTENT_SCHEME);
boolean isFileScheme = TextUtils.equals(uri.getScheme(), UrlConstants.FILE_SCHEME);
return isContentScheme || isFileScheme;
}
/**
* For internal pages, we must publish them, then share them.
* @param offlinePageBridge Bridge to native code for offline pages use.
* @param offlinePage Page to publish and share.
* @param shareCallback The callback to be used to send the ShareParams.
*/
public static void publishThenShareInternalPage(final Activity activity,
OfflinePageBridge offlinePageBridge, OfflinePageItem offlinePage,
final Callback<ShareParams> shareCallback) {
PublishPageCallback publishPageCallback =
new PublishPageCallback(activity, offlinePage, shareCallback);
offlinePageBridge.publishInternalPageByOfflineId(
offlinePage.getOfflineId(), publishPageCallback);
}
/**
* Called when publishing is done. Continues with processing to share.
*/
public static void publishCompleted(OfflinePageItem page, final Activity activity,
final Callback<ShareParams> shareCallback) {
sharePublishedPage(page, activity, shareCallback);
}
/**
* This will take a page in a public directory, and share it.
*/
public static void sharePublishedPage(OfflinePageItem page, final Activity activity,
final Callback<ShareParams> shareCallback) {
if (page == null) {
// For errors, we don't call the shareCallback. The callback only causes the page to be
// shared, and does not report errors, and is not needed to continue processing.
return;
}
final String pageUrl = page.getUrl();
final String pageTitle = page.getTitle();
final File offlinePageFile = new File(page.getFilePath());
sharePage(activity, pageUrl, pageTitle, page.getFilePath(), offlinePageFile, shareCallback);
}
/**
* Share the page.
*/
public static void sharePage(Activity activity, String pageUrl, String pageTitle,
String offlinePath, File offlinePageFile, final Callback<ShareParams> shareCallback) {
RecordUserAction.record("OfflinePages.Sharing.SharePageFromOverflowMenu");
AsyncTask<Uri> task = new AsyncTask<Uri>() {
@Override
protected Uri doInBackground() {
// Android Q+: If we already have a content URI for the published page, return that.
if (ContentUriUtils.isContentUri(offlinePath)) {
return Uri.parse(offlinePath);
}
// If we have a content or file URI, we will not have a filename, just return the
// URI.
if (offlinePath.isEmpty()) {
Uri uri = Uri.parse(pageUrl);
assert(isSchemeContentOrFile(uri));
return uri;
}
// TODO(985699): Investigate why we sometimes aren't able to generate URIs for files
// in external storage.
Uri generatedUri;
try {
generatedUri = ChromeFileProvider.generateUri(activity, offlinePageFile);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Couldn't generate URI for sharing page: " + e);
generatedUri = Uri.parse(pageUrl);
}
return generatedUri;
}
@Override
protected void onPostExecute(Uri uri) {
ShareParams.Builder builder = new ShareParams.Builder(activity, pageTitle, pageUrl)
.setShareDirectly(false);
// Only try to share the offline page if we have a content URI making the actual
// file available.
// TODO(985699): Sharing the page's online URL is a temporary fix for crashes when
// sharing the archive's content URI. Once the root cause is addressed, the offline
// URI should always be set.
if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
builder = builder.setOfflineUri(uri);
}
shareCallback.onResult(builder.build());
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Retrieves the extra request header to reload the offline page.
* @param tab The current tab.
* @return The extra request header string.
*/
public static String getOfflinePageHeaderForReload(Tab tab) {
OfflinePageBridge offlinePageBridge = getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return "";
return offlinePageBridge.getOfflinePageHeaderForReload(tab.getWebContents());
}
/**
* A load url parameters to open offline version of the offline page. If the offline page is
* trusted, the URL (http/https) of the offline page is to be opened. Otherwise, the file URL
* pointing to the archive file is to be opened. In both cases, a custom header is passed with
* the URL to ensure loading a specific version of offline page.
* @param url The url of the offline page to open.
* @param offlineId The ID of the offline page to open.
* @param location Indicates where the offline page is launched.
* @param callback The callback to pass back the LoadUrlParams for launching an URL.
*/
public static void getLoadUrlParamsForOpeningOfflineVersion(final String url, long offlineId,
final @LaunchLocation int location, Callback<LoadUrlParams> callback) {
OfflinePageBridge offlinePageBridge =
getInstance().getOfflinePageBridge(Profile.getLastUsedProfile());
if (offlinePageBridge == null) {
callback.onResult(null);
return;
}
offlinePageBridge.getLoadUrlParamsByOfflineId(
offlineId, location, (loadUrlParams) -> { callback.onResult(loadUrlParams); });
}
/**
* A load url parameters to handle the intent for viewing MHTML file or content. If the
* trusted offline page is found, the URL (http/https) of the offline page is to be opened.
* Otherwise, the file or content URL from the intent will be launched.
* @param intentUrl URL from the intent.
* @param callback The callback to pass back the launching URL and extra headers.
*/
public static void getLoadUrlParamsForOpeningMhtmlFileOrContent(
final String intentUrl, Callback<LoadUrlParams> callback) {
OfflinePageBridge offlinePageBridge =
getInstance().getOfflinePageBridge(Profile.getLastUsedProfile());
if (offlinePageBridge == null) {
callback.onResult(new LoadUrlParams(intentUrl));
return;
}
offlinePageBridge.getLoadUrlParamsForOpeningMhtmlFileOrContent(intentUrl, callback);
}
/**
* @return True if an offline preview is being shown.
* @param tab The current tab.
*/
public static boolean isShowingOfflinePreview(Tab tab) {
return getInstance().isShowingOfflinePreview(tab);
}
/**
* Checks if an offline page is shown for the tab.
* @param tab The tab to be reloaded.
* @return True if the offline page is opened.
*/
public static boolean isOfflinePage(Tab tab) {
return getInstance().isOfflinePage(tab);
}
/**
* Retrieves the offline page that is shown for the tab.
* @param tab The tab to be reloaded.
* @return The offline page if tab currently displays it, null otherwise.
*/
public static OfflinePageItem getOfflinePage(Tab tab) {
WebContents webContents = tab.getWebContents();
if (webContents == null) return null;
OfflinePageBridge offlinePageBridge = getInstance().getOfflinePageBridge(tab.getProfile());
if (offlinePageBridge == null) return null;
return offlinePageBridge.getOfflinePage(webContents);
}
/**
* Returns whether the tab is showing a trusted offline page.
* @param tab The current tab.
* @return True if the tab is showing a trusted offline page.
*/
public static boolean isShowingTrustedOfflinePage(Tab tab) {
return getInstance().isShowingTrustedOfflinePage(tab);
}
/**
* Reloads specified tab, which should allow to open an online version of the page.
* @param tab The tab to be reloaded.
*/
public static void reload(Tab tab) {
// Only the transition type with both RELOAD and FROM_ADDRESS_BAR set will force the
// navigation to be treated as reload (see ShouldTreatNavigationAsReload()). Without this,
// reloading an URL containing a hash will be treated as same document load and thus
// no loading is triggered.
int transitionTypeForReload = PageTransition.RELOAD | PageTransition.FROM_ADDRESS_BAR;
OfflinePageItem offlinePage = getOfflinePage(tab);
if (isShowingTrustedOfflinePage(tab) || offlinePage == null) {
// If current page is an offline page, reload it with custom behavior defined in extra
// header respected.
LoadUrlParams params = new LoadUrlParams(tab.getOriginalUrl(), transitionTypeForReload);
params.setVerbatimHeaders(getOfflinePageHeaderForReload(tab));
tab.loadUrl(params);
return;
}
LoadUrlParams params = new LoadUrlParams(offlinePage.getUrl(), transitionTypeForReload);
tab.loadUrl(params);
}
/**
* Tracks tab creation and closure for the Recent Tabs feature. UI needs to stop showing
* recent offline pages as soon as the tab is closed. The TabModel is used to get profile
* information because Tab's profile is tied to the native WebContents, which may not exist at
* tab adding or tab closing time.
*/
private static class RecentTabTracker extends TabModelSelectorTabModelObserver {
/**
* The single, stateless TabRestoreTracker instance to monitor all tab restores.
*/
private static final TabRestoreTracker sTabRestoreTracker = new TabRestoreTracker();
private TabModelSelector mTabModelSelector;
public RecentTabTracker(TabModelSelector selector) {
super(selector);
mTabModelSelector = selector;
}
@Override
public void didAddTab(Tab tab, @TabLaunchType int type) {
tab.addObserver(sTabRestoreTracker);
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
Profile profile = mTabModelSelector.getModel(tab.isIncognito()).getProfile();
OfflinePageBridge bridge = OfflinePageBridge.getForProfile(profile);
if (bridge == null) return;
WebContents webContents = tab.getWebContents();
if (webContents != null) bridge.willCloseTab(webContents);
}
@Override
public void didCloseTab(int tabId, boolean incognito) {
Profile profile = mTabModelSelector.getModel(incognito).getProfile();
OfflinePageBridge bridge = OfflinePageBridge.getForProfile(profile);
if (bridge == null) return;
// Delete any "Last N" offline pages as well. This is an optimization because
// the UI will no longer show the page, and the page would also be cleaned up by GC
// given enough time.
ClientId clientId =
new ClientId(OfflinePageBridge.LAST_N_NAMESPACE, Integer.toString(tabId));
List<ClientId> clientIds = new ArrayList<>();
clientIds.add(clientId);
bridge.deletePagesByClientId(clientIds, new Callback<Integer>() {
@Override
public void onResult(Integer result) {
// Result is ignored.
}
});
}
}
/**
* Starts tracking the tab models in the given selector for tab addition and closure,
* destroying obsolete observers as necessary.
*/
public static void observeTabModelSelector(
ChromeActivity activity, TabModelSelector tabModelSelector) {
RecentTabTracker previousObserver =
sTabModelObservers.put(activity, new RecentTabTracker(tabModelSelector));
if (previousObserver != null) {
previousObserver.destroy();
} else {
// This is the 1st time we see this activity so register a state listener with it.
ApplicationStatus.registerStateListenerForActivity(
new ApplicationStatus.ActivityStateListener() {
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (newState == ActivityState.DESTROYED) {
sTabModelObservers.remove(activity).destroy();
ApplicationStatus.unregisterActivityStateListener(this);
}
}
},
activity);
}
}
private static class TabRestoreTracker extends EmptyTabObserver {
/**
* If the tab was being restored, reports that it successfully finished reloading its
* contents.
*/
@Override
public void onPageLoadFinished(Tab tab, String url) {
if (!tab.isBeingRestored()) return;
// We first compute the bitwise tab restore context.
int tabRestoreContext = 0;
if (isConnected()) tabRestoreContext |= BIT_ONLINE;
OfflinePageItem page = getOfflinePage(tab);
if (page != null) {
tabRestoreContext |= BIT_OFFLINE_PAGE;
if (page.getClientId().getNamespace().equals(OfflinePageBridge.LAST_N_NAMESPACE)) {
tabRestoreContext |= BIT_LAST_N;
}
} else if (!OfflinePageBridge.canSavePage(tab.getUrl()) || tab.isIncognito()) {
tabRestoreContext |= BIT_CANT_SAVE_OFFLINE;
}
// Now determine the correct tab restore type based on the context.
int tabRestoreType;
switch (tabRestoreContext) {
case BIT_ONLINE:
tabRestoreType = TabRestoreType.WHILE_ONLINE;
break;
case BIT_ONLINE | BIT_CANT_SAVE_OFFLINE:
tabRestoreType = TabRestoreType.WHILE_ONLINE_CANT_SAVE_FOR_OFFLINE_USAGE;
break;
case BIT_ONLINE | BIT_OFFLINE_PAGE:
tabRestoreType = TabRestoreType.WHILE_ONLINE_TO_OFFLINE_PAGE;
break;
case BIT_ONLINE | BIT_OFFLINE_PAGE | BIT_LAST_N:
tabRestoreType = TabRestoreType.WHILE_ONLINE_TO_OFFLINE_PAGE_FROM_LAST_N;
break;
case 0: // offline (not BIT_ONLINE present).
tabRestoreType = TabRestoreType.WHILE_OFFLINE;
break;
case BIT_CANT_SAVE_OFFLINE:
tabRestoreType = TabRestoreType.WHILE_OFFLINE_CANT_SAVE_FOR_OFFLINE_USAGE;
break;
case BIT_OFFLINE_PAGE:
tabRestoreType = TabRestoreType.WHILE_OFFLINE_TO_OFFLINE_PAGE;
break;
case BIT_OFFLINE_PAGE | BIT_LAST_N:
tabRestoreType = TabRestoreType.WHILE_OFFLINE_TO_OFFLINE_PAGE_FROM_LAST_N;
break;
default:
assert false;
return;
}
recordTabRestoreHistogram(tabRestoreType, tab.getUrl());
}
/**
* If the tab was being restored, reports that it failed reloading its contents.
*/
@Override
public void onPageLoadFailed(Tab tab, int errorCode) {
if (tab.isBeingRestored()) recordTabRestoreHistogram(TabRestoreType.FAILED, null);
}
/**
* If the tab was being restored, reports that it crashed while doing so.
*/
@Override
public void onCrash(Tab tab) {
if (tab.isBeingRestored()) recordTabRestoreHistogram(TabRestoreType.CRASHED, null);
}
}
private static void recordTabRestoreHistogram(int tabRestoreType, String url) {
Log.d(TAG, "Concluded tab restore: type=" + tabRestoreType + ", url=" + url);
RecordHistogram.recordEnumeratedHistogram(
"OfflinePages.TabRestore", tabRestoreType, TabRestoreType.NUM_ENTRIES);
}
@VisibleForTesting
static void setInstanceForTesting(Internal instance) {
sInstance = instance;
}
@VisibleForTesting
public static void setSnackbarDurationForTesting(int durationMs) {
sSnackbarDurationMs = durationMs;
}
}