// 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.Context; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; import org.chromium.base.ActivityState; import org.chromium.base.ApplicationStatus; import org.chromium.base.Callback; import org.chromium.base.FileUtils; import org.chromium.base.Log; import org.chromium.base.StreamUtil; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.UrlConstants; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.share.ShareHelper; 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.Tab; import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver; import org.chromium.components.bookmarks.BookmarkId; import org.chromium.components.offlinepages.SavePageResult; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.WebContents; import org.chromium.net.ConnectionType; import org.chromium.net.NetworkChangeNotifier; import org.chromium.ui.base.PageTransition; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * 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"; public static final String EXTERNAL_MHTML_FILE_PATH = "offline-pages"; private static final int DEFAULT_SNACKBAR_DURATION_MS = 6 * 1000; // 6 second private static final long STORAGE_ALMOST_FULL_THRESHOLD_BYTES = 10L * (1 << 20); // 10M /** * 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; private static File sOfflineSharingDirectory; /** * 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. * @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 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) { 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); } } /** * Contains values from the histogram enum OfflinePagesTabRestoreType used for reporting the * OfflinePages.TabRestore metric. */ private static class TabRestoreType { public static final int WHILE_ONLINE = 0; public static final int WHILE_ONLINE_CANT_SAVE_FOR_OFFLINE_USAGE = 1; public static final int WHILE_ONLINE_TO_OFFLINE_PAGE = 2; public static final int WHILE_ONLINE_TO_OFFLINE_PAGE_FROM_LAST_N = 3; public static final int WHILE_OFFLINE = 4; public static final int WHILE_OFFLINE_CANT_SAVE_FOR_OFFLINE_USAGE = 5; public static final int WHILE_OFFLINE_TO_OFFLINE_PAGE = 6; public static final int WHILE_OFFLINE_TO_OFFLINE_PAGE_FROM_LAST_N = 7; public static final int FAILED = 8; public static final 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. public static final int COUNT = 10; } private static Internal getInstance() { if (sInstance == null) { sInstance = new OfflinePageUtilsImpl(); } return sInstance; } /** * 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 the feature is enabled. if (!OfflinePageBridge.isOfflineBookmarksEnabled()) 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() || tab.isShowingSadTab() || webContents == null || webContents.isDestroyed() || webContents.isIncognito(); } /** * Strips scheme from the original URL of the offline page. This is meant to be used by UI. * @param onlineUrl an online URL to from which the scheme is removed * @return onlineUrl without the scheme */ public static String stripSchemeFromOnlineUrl(String onlineUrl) { onlineUrl = onlineUrl.trim(); // Offline pages are only saved for https:// and http:// schemes. if (onlineUrl.startsWith(UrlConstants.HTTPS_URL_PREFIX)) { return onlineUrl.substring(8); } else if (onlineUrl.startsWith(UrlConstants.HTTP_URL_PREFIX)) { return onlineUrl.substring(7); } else { return onlineUrl; } } /** * 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 when the Offline Pages Background Load service awakens. * @param context android context */ public static void recordWakeupUMA(Context context, long taskScheduledTimeMillis) { DeviceConditions deviceConditions = DeviceConditions.getCurrentConditions(context); if (deviceConditions == null) return; // Report charging state. RecordHistogram.recordBooleanHistogram( "OfflinePages.Wakeup.ConnectedToPower", deviceConditions.isPowerConnected()); // Report battery percentage. RecordHistogram.recordPercentageHistogram( "OfflinePages.Wakeup.BatteryPercentage", deviceConditions.getBatteryPercentage()); // Report the default network found (or none, if we aren't connected). int connectionType = deviceConditions.getNetConnectionType(); Log.d(TAG, "Found default network of type " + connectionType); RecordHistogram.recordEnumeratedHistogram("OfflinePages.Wakeup.NetworkAvailable", connectionType, ConnectionType.CONNECTION_LAST + 1); // Collect UMA on the time since the request started. long nowMillis = System.currentTimeMillis(); long delayInMilliseconds = nowMillis - taskScheduledTimeMillis; if (delayInMilliseconds <= 0) { return; } RecordHistogram.recordLongTimesHistogram( "OfflinePages.Wakeup.DelayTime", delayInMilliseconds, TimeUnit.MILLISECONDS); } /** * Share an offline copy of the current page. * @param shareDirectly Whether it should share directly with the activity that was most * recently used to share. * @param saveLastUsed Whether to save the chosen activity for future direct sharing. * @param mainActivity Activity that is used to access package manager. * @param text Text to be shared. If both |text| and |url| are supplied, they are concatenated * with a space. * @param screenshotUri Screenshot of the page to be shared. * @param callback Optional callback to be called when user makes a choice. Will not be called * if receiving a response when the user makes a choice is not supported (see * TargetChosenReceiver#isSupported()). * @param currentTab The current tab for which sharing is being done. */ public static void shareOfflinePage(final boolean shareDirectly, final boolean saveLastUsed, final Activity mainActivity, final String text, final Uri screenshotUri, final ShareHelper.TargetChosenCallback callback, final Tab currentTab) { final String url = currentTab.getUrl(); final String title = currentTab.getTitle(); final OfflinePageBridge offlinePageBridge = OfflinePageBridge.getForProfile(currentTab.getProfile()); if (offlinePageBridge == null) { Log.e(TAG, "Unable to perform sharing on current tab."); return; } OfflinePageItem offlinePage = offlinePageBridge.getOfflinePage(currentTab.getWebContents()); if (offlinePage != null) { // If we're currently on offline page get the saved file directly. prepareFileAndShare(shareDirectly, saveLastUsed, mainActivity, title, text, url, screenshotUri, callback, offlinePage.getFilePath()); return; } // If this is an online page, share the offline copy of it. Callback<OfflinePageItem> prepareForSharing = onGotOfflinePageItemToShare(shareDirectly, saveLastUsed, mainActivity, title, text, url, screenshotUri, callback); offlinePageBridge.selectPageForOnlineUrl(url, currentTab.getId(), selectPageForOnlineUrlCallback(currentTab.getWebContents(), offlinePageBridge, prepareForSharing)); } /** * Callback for receiving the OfflinePageItem and use it to call prepareForSharing. * @param shareDirectly Whether it should share directly with the activity that was most * recently used to share. * @param mainActivity Activity that is used to access package manager * @param title Title of the page. * @param onlineUrl Online URL associated with the offline page that is used to access the * offline page file path. * @param screenshotUri Screenshot of the page to be shared. * @param mContext The application context. * @return a callback of OfflinePageItem */ private static Callback<OfflinePageItem> onGotOfflinePageItemToShare( final boolean shareDirectly, final boolean saveLastUsed, final Activity mainActivity, final String title, final String text, final String onlineUrl, final Uri screenshotUri, final ShareHelper.TargetChosenCallback callback) { return new Callback<OfflinePageItem>() { @Override public void onResult(OfflinePageItem item) { String offlineFilePath = (item == null) ? null : item.getFilePath(); prepareFileAndShare(shareDirectly, saveLastUsed, mainActivity, title, text, onlineUrl, screenshotUri, callback, offlineFilePath); } }; } /** * Takes the offline page item from selectPageForOnlineURL. If it exists, invokes * |prepareForSharing| with it. Otherwise, saves a page for the online URL and invokes * |prepareForSharing| with the result when it's ready. * @param webContents Contents of the page to save. * @param offlinePageBridge A static copy of the offlinePageBridge. * @param prepareForSharing Callback of a single OfflinePageItem that is used to call * prepareForSharing * @return a callback of OfflinePageItem */ private static Callback<OfflinePageItem> selectPageForOnlineUrlCallback( final WebContents webContents, final OfflinePageBridge offlinePageBridge, final Callback<OfflinePageItem> prepareForSharing) { return new Callback<OfflinePageItem>() { @Override public void onResult(OfflinePageItem item) { if (item == null) { // If the page has no offline copy, save the page offline. ClientId clientId = ClientId.createGuidClientIdForNamespace( OfflinePageBridge.SHARE_NAMESPACE); offlinePageBridge.savePage(webContents, clientId, savePageCallback(prepareForSharing, offlinePageBridge)); return; } // If the online page has offline copy associated with it, use the file directly. prepareForSharing.onResult(item); } }; } /** * Saves the web page loaded into web contents. If page saved successfully, get the offline * page item with the save page result and use it to invoke |prepareForSharing|. Otherwise, * invokes |prepareForSharing| with null. * @param prepareForSharing Callback of a single OfflinePageItem that is used to call * prepareForSharing * @param offlinePageBridge A static copy of the offlinePageBridge. * @return a call back of a list of OfflinePageItem */ private static OfflinePageBridge.SavePageCallback savePageCallback( final Callback<OfflinePageItem> prepareForSharing, final OfflinePageBridge offlinePageBridge) { return new OfflinePageBridge.SavePageCallback() { @Override public void onSavePageDone(int savePageResult, String url, long offlineId) { if (savePageResult != SavePageResult.SUCCESS) { Log.e(TAG, "Unable to save the page."); prepareForSharing.onResult(null); return; } offlinePageBridge.getPageByOfflineId(offlineId, prepareForSharing); } }; } /** * If file path of offline page is not null, do file operations needed for the page to be * shared. Otherwise, only share the online url. * @param shareDirectly Whether it should share directly with the activity that was most * recently used to share. * @param saveLastUsed Whether to save the chosen activity for future direct sharing. * @param activity Activity that is used to access package manager * @param title Title of the page. * @param text Text to be shared. If both |text| and |url| are supplied, they are concatenated * with a space. * @param onlineUrl Online URL associated with the offline page that is used to access the * offline page file path. * @param screenshotUri Screenshot of the page to be shared. * @param callback Optional callback to be called when user makes a choice. Will not be called * if receiving a response when the user makes a choice is not supported (on * older Android versions). * @param filePath File path of the offline page. */ private static void prepareFileAndShare(final boolean shareDirectly, final boolean saveLastUsed, final Activity activity, final String title, final String text, final String onlineUrl, final Uri screenshotUri, final ShareHelper.TargetChosenCallback callback, final String filePath) { new AsyncTask<Void, Void, File>() { @Override protected File doInBackground(Void... params) { if (filePath == null) return null; File offlinePageOriginal = new File(filePath); File shareableDir = getDirectoryForOfflineSharing(activity); if (shareableDir == null) { Log.e(TAG, "Unable to create subdirectory in shareable directory"); return null; } String fileName = rewriteOfflineFileName(offlinePageOriginal.getName()); File offlinePageShareable = new File(shareableDir, fileName); if (offlinePageShareable.exists()) { try { // Old shareable files are stored in an external directory, which may cause // problems when: // 1. Files been changed by external sources. // 2. Difference in file size that results in partial overwrite. // Thus the file is deleted before we make a new copy. offlinePageShareable.delete(); } catch (SecurityException e) { Log.e(TAG, "Failed to delete: " + offlinePageOriginal.getName(), e); return null; } } if (copyToShareableLocation(offlinePageOriginal, offlinePageShareable)) { return offlinePageShareable; } return null; } @Override protected void onPostExecute(File offlinePageShareable) { Uri offlineUri = null; if (offlinePageShareable != null) { offlineUri = Uri.fromFile(offlinePageShareable); } ShareHelper.share(shareDirectly, saveLastUsed, activity, title, text, onlineUrl, offlineUri, screenshotUri, callback); } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); } /** * Copies the file from internal storage to a sharable directory. * @param src The original file to be copied. * @param dst The destination file. */ @VisibleForTesting static boolean copyToShareableLocation(File src, File dst) { FileInputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = new FileInputStream(src); outputStream = new FileOutputStream(dst); FileChannel inChannel = inputStream.getChannel(); FileChannel outChannel = outputStream.getChannel(); inChannel.transferTo(0, inChannel.size(), outChannel); } catch (IOException e) { Log.e(TAG, "Failed to copy the file: " + src.getName(), e); return false; } finally { StreamUtil.closeQuietly(inputStream); StreamUtil.closeQuietly(outputStream); } return true; } /** * Gets the directory to use for sharing offline pages, creating it if necessary. * @param context Context that is used to access external cache directory. * @return Path to the directory where shared files are stored. */ @VisibleForTesting static File getDirectoryForOfflineSharing(Context context) { if (sOfflineSharingDirectory == null) { sOfflineSharingDirectory = new File(context.getExternalCacheDir(), EXTERNAL_MHTML_FILE_PATH); } if (!sOfflineSharingDirectory.exists() && !sOfflineSharingDirectory.mkdir()) { sOfflineSharingDirectory = null; } return sOfflineSharingDirectory; } /** * Rewrite file name so that it does not contain periods except the one to separate the file * extension. * This step is used to ensure that file name can be recognized by intent filter (.*\\.mhtml") * as Android's path pattern only matches the first dot that appears in a file path. * @pram fileName Name of the offline page file. */ @VisibleForTesting static String rewriteOfflineFileName(String fileName) { fileName = fileName.replaceAll("\\s+", ""); return fileName.replaceAll("\\.(?=.*\\.)", "_"); } /** * Clears all shared mhtml files. * @param context Context that is used to access external cache directory. */ public static void clearSharedOfflineFiles(final Context context) { if (!OfflinePageBridge.isPageSharingEnabled()) return; new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { File offlinePath = getDirectoryForOfflineSharing(context); if (offlinePath != null) { FileUtils.recursivelyDeleteFile(offlinePath); } return null; } }.executeOnExecutor(AsyncTask.SERIAL_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 (i.e. to ensure no * automatic redirection based on the connection status). * @param url The url of the offline page to open. * @param offlineId The ID of the offline page to open. * @return The LoadUrlParams with a special header. */ public static LoadUrlParams getLoadUrlParamsForOpeningOfflineVersion( String url, long offlineId) { LoadUrlParams params = new LoadUrlParams(url); Map<String, String> headers = new HashMap<String, String>(); headers.put("X-Chrome-offline", "persist=1 reason=download id=" + Long.toString(offlineId)); params.setExtraHeaders(headers); return params; } /** * @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); } /** * 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) { // If current page is an offline page, reload it with custom behavior defined in extra // header respected. LoadUrlParams params = new LoadUrlParams(tab.getOriginalUrl(), PageTransition.RELOAD); params.setVerbatimHeaders(getOfflinePageHeaderForReload(tab)); tab.loadUrl(params); } /** * Navigates the given tab to the saved local snapshot of the offline page identified by the URL * and the offline ID. No automatic redirection is happening based on the connection status. * @param url The URL of the offine page. * @param offlineId The ID of the offline page. * @param tab The tab to navigate to the page. */ public static void openInExistingTab(String url, long offlineId, Tab tab) { LoadUrlParams params = OfflinePageUtils.getLoadUrlParamsForOpeningOfflineVersion(url, offlineId); // Extra headers are not read in loadUrl, but verbatim headers are. params.setVerbatimHeaders(params.getExtraHeadersString()); 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, TabModel.TabLaunchType type) { tab.addObserver(sTabRestoreTracker); Profile profile = mTabModelSelector.getModel(tab.isIncognito()).getProfile(); OfflinePageBridge bridge = OfflinePageBridge.getForProfile(profile); if (bridge == null) return; bridge.registerRecentTab(tab.getId()); } @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; // First, unregister the tab with the UI. bridge.unregisterRecentTab(tabId); // Then, 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) { 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, boolean sadTabShown) { 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.COUNT); } @VisibleForTesting static void setInstanceForTesting(Internal instance) { sInstance = instance; } @VisibleForTesting public static void setSnackbarDurationForTesting(int durationMs) { sSnackbarDurationMs = durationMs; } }