| Index: chrome/android/java_staging/src/org/chromium/chrome/browser/document/DocumentMigrationHelper.java
|
| diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/document/DocumentMigrationHelper.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/document/DocumentMigrationHelper.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..250f8c1848b50ff617e9566f7aedcca6bd1d7a04
|
| --- /dev/null
|
| +++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/document/DocumentMigrationHelper.java
|
| @@ -0,0 +1,458 @@
|
| +// 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.document;
|
| +
|
| +import android.annotation.TargetApi;
|
| +import android.app.Activity;
|
| +import android.app.ActivityManager;
|
| +import android.app.ActivityManager.AppTask;
|
| +import android.app.ActivityManager.TaskDescription;
|
| +import android.content.Context;
|
| +import android.content.Intent;
|
| +import android.graphics.Bitmap;
|
| +import android.graphics.Bitmap.Config;
|
| +import android.graphics.Canvas;
|
| +import android.graphics.Color;
|
| +import android.os.Build;
|
| +import android.text.TextUtils;
|
| +import android.util.Log;
|
| +import android.util.Pair;
|
| +import android.util.SparseArray;
|
| +
|
| +import com.google.android.apps.chrome.R;
|
| +
|
| +import org.chromium.base.ApplicationStatus;
|
| +import org.chromium.base.ImportantFileWriterAndroid;
|
| +import org.chromium.chrome.browser.ApplicationLifetime;
|
| +import org.chromium.chrome.browser.ChromeMobileApplication;
|
| +import org.chromium.chrome.browser.IntentHandler;
|
| +import org.chromium.chrome.browser.Tab;
|
| +import org.chromium.chrome.browser.TabState;
|
| +import org.chromium.chrome.browser.UrlConstants;
|
| +import org.chromium.chrome.browser.UrlUtilities;
|
| +import org.chromium.chrome.browser.compositor.layouts.content.ContentOffsetProvider;
|
| +import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
|
| +import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager.DecompressThumbnailCallback;
|
| +import org.chromium.chrome.browser.device.DeviceClassManager;
|
| +import org.chromium.chrome.browser.favicon.FaviconHelper;
|
| +import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback;
|
| +import org.chromium.chrome.browser.ntp.NativePageFactory;
|
| +import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
|
| +import org.chromium.chrome.browser.profiles.Profile;
|
| +import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
|
| +import org.chromium.chrome.browser.tabmodel.TabPersistentStore.OnTabStateReadCallback;
|
| +import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate;
|
| +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel;
|
| +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.Entry;
|
| +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel.InitializationObserver;
|
| +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelImpl;
|
| +import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector;
|
| +import org.chromium.chrome.browser.tabmodel.document.OffTheRecordDocumentTabModel;
|
| +import org.chromium.chrome.browser.tabmodel.document.StorageDelegate;
|
| +
|
| +import java.io.File;
|
| +import java.io.IOException;
|
| +import java.util.ArrayList;
|
| +import java.util.List;
|
| +
|
| +/**
|
| + * The class that carries out migration of tab states from/to document mode.
|
| + */
|
| +@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
| +public class DocumentMigrationHelper {
|
| + private static final String TAG = "DocumentMigrationHelper";
|
| + private static final int[] ICON_TYPES = {FaviconHelper.FAVICON,
|
| + FaviconHelper.TOUCH_ICON | FaviconHelper.TOUCH_PRECOMPOSED_ICON};
|
| + private static final int DESIRED_ICON_SIZE_DP = 32;
|
| +
|
| + public static final int FINALIZE_MODE_NO_ACTION = 0;
|
| + public static final int FINALIZE_MODE_FINISH_ACTIVITY = 1;
|
| + public static final int FINALIZE_MODE_RESTART_APP = 2;
|
| +
|
| + private static class MigrationTabStateReadCallback implements OnTabStateReadCallback {
|
| + private int mSelectedTabId = Tab.INVALID_TAB_ID;
|
| +
|
| + @Override
|
| + public void onDetailsRead(int index, int id, String url, boolean isStandardActiveIndex,
|
| + boolean isIncognitoActiveIndex) {
|
| + Tab.incrementIdCounterTo(id + 1);
|
| + if (!isStandardActiveIndex) return;
|
| + // If the current tab read is the active standard tab, set the last used
|
| + // tab pref with the id, so that when document mode starts we show that
|
| + // tab first.
|
| + mSelectedTabId = id;
|
| + }
|
| +
|
| + public int getSelectedTabId() {
|
| + return mSelectedTabId;
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Stores a list of "tasks" that are meant to be returned by the ActivityManager for migration.
|
| + * The tasks are inserted manually during the migration from classic mode to document mode.
|
| + */
|
| + private static class MigrationActivityDelegate extends ActivityDelegate {
|
| + private final List<Entry> mEntries;
|
| + private final int mSelectedTabId;
|
| +
|
| + private MigrationActivityDelegate(List<Entry> entries, int selectedTabId) {
|
| + super(DocumentActivity.class, IncognitoDocumentActivity.class);
|
| + mEntries = entries;
|
| + mSelectedTabId = selectedTabId;
|
| + }
|
| +
|
| + public int getSelectedTabId() {
|
| + return mSelectedTabId;
|
| + }
|
| +
|
| + @Override
|
| + public boolean isValidActivity(boolean isIncognito, Intent intent) {
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public List<Entry> getTasksFromRecents(boolean isIncognito) {
|
| + // We need to have our own list here, since these entries have not actually been
|
| + // created in Recents yet.
|
| + return mEntries;
|
| + }
|
| + }
|
| +
|
| + private static class MigrationTabModel extends DocumentTabModelImpl {
|
| + private final SparseArray<String> mTitleList;
|
| +
|
| + /**
|
| + * Constucts a {@link DocumentTabModel} to be used for migration.
|
| + * @param activityDelegate The delegate that has the tabs to be migrated.
|
| + * @param storageDelegate Delegate that interacts with the file system.
|
| + */
|
| + MigrationTabModel(MigrationActivityDelegate activityDelegate,
|
| + StorageDelegate storageDelegate) {
|
| + super(activityDelegate, storageDelegate, new TabDelegateImpl(), false,
|
| + Tab.INVALID_TAB_ID, ApplicationStatus.getApplicationContext());
|
| + startTabStateLoad();
|
| + mTitleList = new SparseArray<String>();
|
| + setLastShownId(activityDelegate.getSelectedTabId());
|
| + }
|
| +
|
| + /**
|
| + * Returns the display title for the Document with the given ID.
|
| + * @param tabId The ID for the document to return the url for.
|
| + * @return The display title for the entry if it was found, null otherwise.
|
| + */
|
| + public String getTitleForDocument(int tabId) {
|
| + String title = mTitleList.get(tabId);
|
| + return TextUtils.isEmpty(title) ? "" : title;
|
| + }
|
| +
|
| + @Override
|
| + protected boolean shouldStartDeserialization(int currentState) {
|
| + return currentState == STATE_LOAD_TAB_STATE_BG_END;
|
| + }
|
| +
|
| + @Override
|
| + protected void updateEntryInfoFromTabState(Entry entry, TabState tabState) {
|
| + super.updateEntryInfoFromTabState(entry, tabState);
|
| + mTitleList.put(entry.tabId, tabState.getDisplayTitleFromState());
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Migrates all tab state to classic mode and creates a tab model file using the current
|
| + * {@link DocumentTabModel} instances.
|
| + * @param activity The activity to be finished after migration if necessary.
|
| + * @param finalizeMode The mode in which the migration should be finalized.
|
| + */
|
| + public static void migrateTabsFromDocumentToClassic(final Activity activity,
|
| + int finalizeMode) {
|
| + Context context = ApplicationStatus.getApplicationContext();
|
| + // Before migration we remove all incognito tabs and also remove the
|
| + // tabs that can not be reached through the {@link DocumentTabModel} instances.
|
| + List<Integer> tabIdsToRemove = new ArrayList<Integer>();
|
| +
|
| + DocumentTabModelImpl normalTabModel = (DocumentTabModelImpl)
|
| + ChromeMobileApplication.getDocumentTabModelSelector().getModel(false);
|
| + OffTheRecordDocumentTabModel incognitoTabModel = (OffTheRecordDocumentTabModel)
|
| + ChromeMobileApplication.getDocumentTabModelSelector().getModel(true);
|
| +
|
| + // TODO(yusufo): Clean up this logic.
|
| + for (int i = 0; i < incognitoTabModel.getCount(); i++) {
|
| + tabIdsToRemove.add(incognitoTabModel.getTabAt(i).getId());
|
| + }
|
| +
|
| + ActivityManager am =
|
| + (ActivityManager) context.getSystemService(Activity.ACTIVITY_SERVICE);
|
| + List<AppTask> taskList = am.getAppTasks();
|
| + for (int i = 0; i < taskList.size(); i++) {
|
| + Intent intent = DocumentUtils.getBaseIntentFromTask(taskList.get(i));
|
| + int id = ActivityDelegate.getTabIdFromIntent(intent);
|
| + if (id == Tab.INVALID_TAB_ID) continue;
|
| + if (tabIdsToRemove.contains(id)) taskList.get(i).finishAndRemoveTask();
|
| + }
|
| + incognitoTabModel.updateRecentlyClosed();
|
| +
|
| + File migratedFolder = TabPersistentStore.getStateDirectory(context, 0);
|
| + String tabStatefileName = new File(migratedFolder,
|
| + TabPersistentStore.SAVED_STATE_FILE).getAbsolutePath();
|
| +
|
| + // All the TabStates (incognito or not) live in the same directory.
|
| + File[] allTabs = normalTabModel.getStorageDelegate().getStateDirectory().listFiles();
|
| + try {
|
| + for (int i = 0; i < allTabs.length; i++) {
|
| + String fileName = allTabs[i].getName();
|
| + Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(fileName);
|
| + if (tabInfo == null) continue;
|
| + int tabId = tabInfo.first;
|
| +
|
| + // Also remove the tab state file for the closed tabs.
|
| + boolean success;
|
| + if (!tabIdsToRemove.contains(tabId)) {
|
| + success = allTabs[i].renameTo(new File(migratedFolder, fileName));
|
| + } else {
|
| + success = allTabs[i].delete();
|
| + }
|
| +
|
| + if (!success) Log.e(TAG, "Failed to move/delete file for tab ID: " + tabId);
|
| + }
|
| +
|
| + if (normalTabModel.getCount() != 0) {
|
| + byte[] listData;
|
| + listData = TabPersistentStore.serializeTabLists(incognitoTabModel, normalTabModel);
|
| + ImportantFileWriterAndroid.writeFileAtomically(tabStatefileName, listData);
|
| + }
|
| + } catch (IOException e) {
|
| + Log.e(TAG , "IO exception during tab migration, tab state might not restore correctly");
|
| + }
|
| + finalizeMigration(activity, finalizeMode);
|
| + }
|
| +
|
| + /**
|
| + * Migrates all tab state to document mode and creates tasks for each currently open tab.
|
| + * @param activity Activity to be used while launching the tasks.
|
| + * @param finalizeMode The mode in which the migration should be finalized.
|
| + */
|
| + public static void migrateTabsFromClassicToDocument(
|
| + final Activity activity, final int finalizeMode) {
|
| + StorageDelegate storageDelegate = new StorageDelegate();
|
| + MigrationActivityDelegate activityDelegate =
|
| + createActivityDelegateWithTabsToMigrate(storageDelegate, activity);
|
| + final MigrationTabModel normalTabModel =
|
| + new MigrationTabModel(activityDelegate, storageDelegate);
|
| +
|
| + InitializationObserver observer = new InitializationObserver(normalTabModel) {
|
| + @Override
|
| + protected void runImmediately() {
|
| + addAppTasksFromFiles(activity, normalTabModel, finalizeMode);
|
| + }
|
| +
|
| + @Override
|
| + public boolean isSatisfied(int currentState) {
|
| + return currentState == DocumentTabModelImpl.STATE_DESERIALIZE_END;
|
| + }
|
| +
|
| + @Override
|
| + public boolean isCanceled() {
|
| + return false;
|
| + }
|
| + };
|
| +
|
| + observer.runWhenReady();
|
| + }
|
| +
|
| + /**
|
| + * Migrate tabs saved in classic mode to document mode for an upgrade. This doesn't restart
|
| + * the app process but only finishes the {@link ChromeLauncherActivity} it was being called
|
| + * with.
|
| + * @param activity The activity to use for carrying out and finalizing the migration.
|
| + * @param finalizeMode The mode in which the migration should be finalized.
|
| + * @return Whether any tabs will be migrated.
|
| + */
|
| + public static boolean migrateTabsToDocumentForUpgrade(Activity activity,
|
| + int finalizeMode) {
|
| + ChromePreferenceManager.getInstance(activity).setAttemptedMigrationOnUpgrade();
|
| + File[] fileList = TabPersistentStore.getStateDirectory(activity, 0).listFiles();
|
| + if (fileList == null || fileList.length == 0
|
| + || (fileList.length == 1
|
| + && fileList[0].getName().equals(TabPersistentStore.SAVED_STATE_FILE))) {
|
| + return false;
|
| + }
|
| +
|
| + migrateTabsFromClassicToDocument(activity, finalizeMode);
|
| + return true;
|
| + }
|
| +
|
| + private static void finalizeMigration(Activity activity, final int mode) {
|
| + switch(mode) {
|
| + case FINALIZE_MODE_NO_ACTION:
|
| + return;
|
| + case FINALIZE_MODE_FINISH_ACTIVITY:
|
| + activity.finishAndRemoveTask();
|
| + return;
|
| + case FINALIZE_MODE_RESTART_APP:
|
| + ApplicationLifetime.terminate(true);
|
| + return;
|
| + default:
|
| + assert false;
|
| + }
|
| + }
|
| +
|
| + private static MigrationActivityDelegate createActivityDelegateWithTabsToMigrate(
|
| + StorageDelegate storageDelegate, Activity activity) {
|
| + File migratedFolder = storageDelegate.getStateDirectory();
|
| + if (!migratedFolder.exists() && !migratedFolder.mkdir()) {
|
| + Log.e(TAG, "Failed to create folder: " + migratedFolder.getAbsolutePath());
|
| + }
|
| +
|
| + // Create maps for all tabs that will be used during TabModel initialization.
|
| + final List<Entry> normalEntryMap = new ArrayList<Entry>();
|
| +
|
| + int currentSelectorIndex = 0;
|
| + File currentFolder = TabPersistentStore.getStateDirectory(activity, currentSelectorIndex);
|
| + MigrationTabStateReadCallback callback = new MigrationTabStateReadCallback();
|
| + while (currentFolder.listFiles() != null && currentFolder.listFiles().length != 0) {
|
| + File[] allTabs = TabPersistentStore
|
| + .getStateDirectory(activity, currentSelectorIndex).listFiles();
|
| + try {
|
| + TabPersistentStore.readSavedStateFile(currentFolder, callback);
|
| + } catch (IOException e) {
|
| + Log.e(TAG, "IO Exception while trying to get the last used tab id");
|
| + }
|
| +
|
| + for (int i = 0; i < allTabs.length; i++) {
|
| + // Move tab state file to the document side folder.
|
| + String fileName = allTabs[i].getName();
|
| + Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(fileName);
|
| + if (tabInfo == null) continue;
|
| +
|
| + boolean success;
|
| + if (tabInfo.second) {
|
| + success = allTabs[i].delete();
|
| + } else {
|
| + success = allTabs[i].renameTo(new File(migratedFolder, fileName));
|
| + normalEntryMap.add(new Entry(tabInfo.first, UrlConstants.NTP_URL));
|
| + }
|
| +
|
| + if (!success) Log.e(TAG, "Failed to move/delete file: " + fileName);
|
| + }
|
| + currentSelectorIndex++;
|
| + currentFolder = TabPersistentStore.getStateDirectory(activity, currentSelectorIndex);
|
| + }
|
| +
|
| + return new MigrationActivityDelegate(normalEntryMap, callback.getSelectedTabId());
|
| + }
|
| +
|
| + private static void addAppTasksFromFiles(final Activity activity,
|
| + final MigrationTabModel tabModel, final int finalizeMode) {
|
| + if (tabModel.getCount() == 0) {
|
| + finalizeMigration(activity, finalizeMode);
|
| + return;
|
| + }
|
| +
|
| + final TabContentManager contentManager =
|
| + new TabContentManager(activity, new ContentOffsetProvider() {
|
| + @Override
|
| + public int getOverlayTranslateY() {
|
| + return 0;
|
| + }
|
| + }, DeviceClassManager.enableSnapshots());
|
| + FaviconHelper faviconHelper = new FaviconHelper();
|
| + for (int i = 0; i < tabModel.getCount(); i++) {
|
| + final int tabId = tabModel.getTabAt(i).getId();
|
| + String currentUrl = tabModel.getCurrentUrlForDocument(tabId);
|
| + String currentTitle = tabModel.getTitleForDocument(tabId);
|
| + final boolean finalizeWhenDone = i == tabModel.getCount() - 1;
|
| +
|
| + // Use placeholders if we can't find anything for url and title.
|
| + if (TextUtils.isEmpty(currentUrl)) currentUrl = UrlConstants.NTP_URL;
|
| + if (TextUtils.isEmpty(currentTitle)
|
| + && !NativePageFactory.isNativePageUrl(currentUrl, false)) {
|
| + currentTitle = UrlUtilities.getDomainAndRegistry(currentUrl, false);
|
| + }
|
| + final String url = currentUrl;
|
| + final String title = currentTitle;
|
| +
|
| + faviconHelper.getLargestRawFaviconForUrl(
|
| + Profile.getLastUsedProfile().getOriginalProfile(),
|
| + url, ICON_TYPES, DESIRED_ICON_SIZE_DP,
|
| + new FaviconImageCallback() {
|
| + @Override
|
| + public void onFaviconAvailable(final Bitmap favicon, String iconUrl) {
|
| + // Even if either the favicon or the thumbnail comes back null
|
| + // add the AppTask with the return values. The framework handles a null
|
| + // favicon and addAppTask below handles null thumbnails.
|
| + DecompressThumbnailCallback thumbnailCallback =
|
| + new DecompressThumbnailCallback() {
|
| + @Override
|
| + public void onFinishGetBitmap(Bitmap bitmap) {
|
| + if (!NativePageFactory.isNativePageUrl(url, false)
|
| + && !url.startsWith(UrlConstants.CHROME_SCHEME)) {
|
| + addAppTask(activity, tabId,
|
| + tabModel.getTabStateForDocument(tabId),
|
| + url, title, favicon, bitmap);
|
| + }
|
| + // TODO(yusufo) : Have a counter here to make sure all tabs
|
| + // before this one has been added.
|
| + if (finalizeWhenDone) {
|
| + finalizeMigration(activity, finalizeMode);
|
| + }
|
| + }
|
| + };
|
| + contentManager.getThumbnailForId(tabId, thumbnailCallback);
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + private static void addAppTask(Activity activity, int tabId, TabState tabState,
|
| + String currentUrl, String title, Bitmap favicon, Bitmap bitmap) {
|
| + if (tabId == ActivityDelegate.getTabIdFromIntent(activity.getIntent())) return;
|
| + // Create intent and taskDescription.
|
| + Intent intent = new Intent(Intent.ACTION_VIEW,
|
| + DocumentTabModelSelector.createDocumentDataString(tabId, currentUrl));
|
| + intent.setClassName(activity, ChromeLauncherActivity.getDocumentClassName(false));
|
| + intent.putExtra(IntentHandler.EXTRA_PRESERVE_TASK, true);
|
| + ActivityManager am =
|
| + (ActivityManager) activity.getSystemService(Activity.ACTIVITY_SERVICE);
|
| +
|
| + Bitmap thumbnail = Bitmap.createBitmap(am.getAppTaskThumbnailSize().getWidth(),
|
| + am.getAppTaskThumbnailSize().getHeight(), Config.ARGB_8888);
|
| + Canvas canvas = new Canvas(thumbnail);
|
| + if (bitmap == null) {
|
| + canvas.drawColor(Color.WHITE);
|
| + } else {
|
| + float scale = Math.max(
|
| + (float) thumbnail.getWidth() / bitmap.getWidth(),
|
| + (float) thumbnail.getHeight() / bitmap.getHeight());
|
| + canvas.scale(scale, scale);
|
| + canvas.drawBitmap(bitmap, 0, 0, null);
|
| + }
|
| + TaskDescription taskDescription = new TaskDescription(title, favicon,
|
| + activity.getResources().getColor(R.color.default_primary_color));
|
| + am.addAppTask(activity, intent, taskDescription, thumbnail);
|
| + Entry entry = new Entry(tabId, tabState);
|
| + DocumentTabModelImpl tabModel = (DocumentTabModelImpl) ChromeMobileApplication
|
| + .getDocumentTabModelSelector().getModel(false);
|
| + tabModel.addEntryForMigration(entry);
|
| + }
|
| +
|
| +
|
| + /**
|
| + * Migrates tabs with state to and from document mode.
|
| + * @param toDocumentMode Whether the user is opting out. If true the migration is from Document
|
| + * to Classic mode.
|
| + * @param activity The activity to use for launching intent if needed.
|
| + * @param terminate Whether the application process should be terminated after the migration.
|
| + */
|
| + public static void migrateTabs(boolean toDocumentMode, final Activity activity,
|
| + boolean terminate) {
|
| + int terminateMode = terminate ? FINALIZE_MODE_RESTART_APP : FINALIZE_MODE_FINISH_ACTIVITY;
|
| + if (toDocumentMode) {
|
| + migrateTabsFromClassicToDocument(activity, terminateMode);
|
| + } else {
|
| + migrateTabsFromDocumentToClassic(activity, terminateMode);
|
| + }
|
| + }
|
| +}
|
|
|