| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.chrome.browser.tabmodel; | 5 package org.chromium.chrome.browser.tabmodel; |
| 6 | 6 |
| 7 import android.annotation.TargetApi; | 7 import android.annotation.TargetApi; |
| 8 import android.content.Context; | 8 import android.content.Context; |
| 9 import android.content.SharedPreferences; | 9 import android.content.SharedPreferences; |
| 10 import android.os.AsyncTask; | 10 import android.os.AsyncTask; |
| 11 import android.os.Build; | 11 import android.os.Build; |
| 12 import android.preference.PreferenceManager; |
| 12 import android.util.Pair; | 13 import android.util.Pair; |
| 13 | 14 |
| 14 import org.chromium.base.ApplicationStatus; | 15 import org.chromium.base.ApplicationStatus; |
| 15 import org.chromium.base.CommandLine; | 16 import org.chromium.base.CommandLine; |
| 16 import org.chromium.base.FileUtils; | 17 import org.chromium.base.FileUtils; |
| 17 import org.chromium.base.Log; | 18 import org.chromium.base.Log; |
| 18 import org.chromium.base.ObserverList; | 19 import org.chromium.base.ObserverList; |
| 19 import org.chromium.base.StreamUtil; | 20 import org.chromium.base.StreamUtil; |
| 20 import org.chromium.base.ThreadUtils; | 21 import org.chromium.base.ThreadUtils; |
| 21 import org.chromium.base.VisibleForTesting; | 22 import org.chromium.base.VisibleForTesting; |
| 22 import org.chromium.chrome.browser.ChromeApplication; | 23 import org.chromium.chrome.browser.ChromeApplication; |
| 23 import org.chromium.chrome.browser.ChromeSwitches; | 24 import org.chromium.chrome.browser.ChromeSwitches; |
| 24 import org.chromium.chrome.browser.TabState; | 25 import org.chromium.chrome.browser.TabState; |
| 25 import org.chromium.chrome.browser.document.DocumentActivity; | 26 import org.chromium.chrome.browser.document.DocumentActivity; |
| 26 import org.chromium.chrome.browser.document.DocumentUtils; | 27 import org.chromium.chrome.browser.document.DocumentUtils; |
| 27 import org.chromium.chrome.browser.document.IncognitoDocumentActivity; | 28 import org.chromium.chrome.browser.document.IncognitoDocumentActivity; |
| 29 import org.chromium.chrome.browser.document.IncognitoNotificationManager; |
| 28 import org.chromium.chrome.browser.preferences.DocumentModeManager; | 30 import org.chromium.chrome.browser.preferences.DocumentModeManager; |
| 29 import org.chromium.chrome.browser.tab.Tab; | 31 import org.chromium.chrome.browser.tab.Tab; |
| 30 import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabModelMetadata; | 32 import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabModelMetadata; |
| 31 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; | 33 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; |
| 32 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegateImpl; | 34 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegateImpl; |
| 33 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; | 35 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; |
| 34 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelImpl; | 36 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelImpl; |
| 35 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector; | |
| 36 import org.chromium.chrome.browser.tabmodel.document.StorageDelegate; | 37 import org.chromium.chrome.browser.tabmodel.document.StorageDelegate; |
| 37 import org.chromium.chrome.browser.util.FeatureUtilities; | 38 import org.chromium.chrome.browser.util.FeatureUtilities; |
| 38 | 39 |
| 39 import java.io.File; | 40 import java.io.File; |
| 40 import java.io.FileInputStream; | 41 import java.io.FileInputStream; |
| 41 import java.io.FileOutputStream; | 42 import java.io.FileOutputStream; |
| 42 import java.io.IOException; | 43 import java.io.IOException; |
| 43 import java.nio.channels.FileChannel; | 44 import java.nio.channels.FileChannel; |
| 44 import java.util.HashSet; | 45 import java.util.HashSet; |
| 45 import java.util.Set; | 46 import java.util.Set; |
| 46 | 47 |
| 47 import javax.annotation.Nullable; | |
| 48 | |
| 49 /** | 48 /** |
| 50 * Divorces Chrome's tabs from Android's Overview menu. Assumes native librarie
s are unavailable. | 49 * Divorces Chrome's tabs from Android's Overview menu. Assumes native librarie
s are unavailable. |
| 51 * | 50 * |
| 52 * Migration from document mode to tabbed mode occurs in two main phases: | 51 * Migration from document mode to tabbed mode occurs in two main phases: |
| 53 * | 52 * |
| 54 * 1) NON-DESTRUCTIVE MIGRATION: | 53 * 1) NON-DESTRUCTIVE MIGRATION: |
| 55 * TabState files for the normal DocumentTabModel are copied from the documen
t mode directories | 54 * TabState files for the normal DocumentTabModel are copied from the documen
t mode directories |
| 56 * into the tabbed mode directory. Incognito tabs are silently dropped, as w
ith the previous | 55 * into the tabbed mode directory. Incognito tabs are silently dropped, as w
ith the previous |
| 57 * migration pathway. | 56 * migration pathway. |
| 58 * | 57 * |
| 59 * TODO(dfalcantara): Check what happens on other launchers. | |
| 60 * | |
| 61 * Once all TabState files are copied, a TabModel metadata file is written ou
t for the tabbed | 58 * Once all TabState files are copied, a TabModel metadata file is written ou
t for the tabbed |
| 62 * mode {@link TabModelImpl} to read out. Because the native library is not
available, the file | 59 * mode {@link TabModelImpl} to read out. Because the native library is not
available, the file |
| 63 * will be incomplete but usable; it will be corrected by the TabModelImpl wh
en it loads it and | 60 * will be incomplete but usable; it will be corrected by the TabModelImpl wh
en it loads it and |
| 64 * all of the TabState files up. See {@link #writeTabModelMetadata} for deta
ils. | 61 * all of the TabState files up. See {@link #writeTabModelMetadata} for deta
ils. |
| 65 * | 62 * |
| 66 * 2) CLEANUP OF ALL DOCUMENT-RELATED THINGS: | 63 * 2) CLEANUP OF ALL DOCUMENT-RELATED THINGS: |
| 67 * DocumentActivity tasks in Android's Recents are removed, TabState files in
the document mode | 64 * DocumentActivity tasks in Android's Recents are removed, TabState files in
the document mode |
| 68 * directory are deleted, and document mode preferences are cleared. | 65 * directory are deleted, and document mode preferences are cleared. |
| 69 * | 66 * |
| 70 * TODO(dfalcantara): Add histograms for tracking migration progress. | 67 * TODO(dfalcantara): Add histograms for tracking migration progress? |
| 71 * | |
| 72 * TODO(dfalcantara): Potential pitfalls that need to be accounted for: | |
| 73 * - Consistently crashing during migration means you can never open Chrome un
til you clear data. | |
| 74 * - Successfully migrating, but crashing while deleting things and closing of
f tasks. | |
| 75 * - Failing to copy all the TabState files over during migration because of a
lack of space. | |
| 76 */ | 68 */ |
| 77 @TargetApi(Build.VERSION_CODES.LOLLIPOP) | 69 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 78 public class DocumentModeAssassin { | 70 public class DocumentModeAssassin { |
| 79 /** Alerted about progress along the migration pipeline. */ | 71 /** Alerted about progress along the migration pipeline. */ |
| 80 public static class DocumentModeAssassinObserver { | 72 public static class DocumentModeAssassinObserver { |
| 81 /** | 73 /** |
| 82 * Called on the UI thread when the DocumentModeAssassin has progressed
along its pipeline, | 74 * Called on the UI thread when the DocumentModeAssassin has progressed
along its pipeline, |
| 83 * and when the DocumentModeAssasssinObserver is first added to the Docu
mentModeAssassin. | 75 * and when the DocumentModeAssasssinObserver is first added to the Docu
mentModeAssassin. |
| 84 * | 76 * |
| 85 * @param newStage New stage of the pipeline. | 77 * @param newStage New stage of the pipeline. |
| (...skipping 16 matching lines...) Expand all Loading... |
| 102 static final int STAGE_INITIALIZED = 1; | 94 static final int STAGE_INITIALIZED = 1; |
| 103 static final int STAGE_COPY_TAB_STATES_STARTED = 2; | 95 static final int STAGE_COPY_TAB_STATES_STARTED = 2; |
| 104 static final int STAGE_COPY_TAB_STATES_DONE = 3; | 96 static final int STAGE_COPY_TAB_STATES_DONE = 3; |
| 105 static final int STAGE_WRITE_TABMODEL_METADATA_STARTED = 4; | 97 static final int STAGE_WRITE_TABMODEL_METADATA_STARTED = 4; |
| 106 static final int STAGE_WRITE_TABMODEL_METADATA_DONE = 5; | 98 static final int STAGE_WRITE_TABMODEL_METADATA_DONE = 5; |
| 107 static final int STAGE_CHANGE_SETTINGS_STARTED = 6; | 99 static final int STAGE_CHANGE_SETTINGS_STARTED = 6; |
| 108 static final int STAGE_CHANGE_SETTINGS_DONE = 7; | 100 static final int STAGE_CHANGE_SETTINGS_DONE = 7; |
| 109 static final int STAGE_DELETION_STARTED = 8; | 101 static final int STAGE_DELETION_STARTED = 8; |
| 110 public static final int STAGE_DONE = 9; | 102 public static final int STAGE_DONE = 9; |
| 111 | 103 |
| 104 static final String PREF_NUM_MIGRATION_ATTEMPTS = |
| 105 "org.chromium.chrome.browser.tabmodel.NUM_MIGRATION_ATTEMPTS"; |
| 106 static final int MAX_MIGRATION_ATTEMPTS_BEFORE_FAILURE = 3; |
| 112 private static final String TAG = "DocumentModeAssassin"; | 107 private static final String TAG = "DocumentModeAssassin"; |
| 113 | 108 |
| 114 /** Which TabModelSelectorImpl to copy files into during migration. */ | 109 /** Which TabModelSelectorImpl to copy files into during migration. */ |
| 115 private static final int TAB_MODEL_INDEX = 0; | 110 private static final int TAB_MODEL_INDEX = 0; |
| 116 | 111 |
| 117 /** Creates and holds the Singleton. */ | 112 /** Creates and holds the Singleton. */ |
| 118 private static class LazyHolder { | 113 private static class LazyHolder { |
| 119 private static final DocumentModeAssassin INSTANCE = new DocumentModeAss
assin(); | 114 private static final DocumentModeAssassin INSTANCE = new DocumentModeAss
assin(); |
| 120 } | 115 } |
| 121 | 116 |
| 122 /** Returns the Singleton instance. */ | 117 /** Returns the Singleton instance. */ |
| 123 public static DocumentModeAssassin getInstance() { | 118 public static DocumentModeAssassin getInstance() { |
| 124 return LazyHolder.INSTANCE; | 119 return LazyHolder.INSTANCE; |
| 125 } | 120 } |
| 126 | 121 |
| 127 /** IDs of Tabs that have had their TabState files copied between directorie
s successfully. */ | 122 /** IDs of Tabs that have had their TabState files copied between directorie
s successfully. */ |
| 128 private final Set<Integer> mMigratedTabIds = new HashSet<Integer>(); | 123 private final Set<Integer> mMigratedTabIds = new HashSet<Integer>(); |
| 129 | 124 |
| 130 /** Observers of the migration pipeline. */ | 125 /** Observers of the migration pipeline. */ |
| 131 private final ObserverList<DocumentModeAssassinObserver> mObservers = | 126 private final ObserverList<DocumentModeAssassinObserver> mObservers = |
| 132 new ObserverList<DocumentModeAssassinObserver>(); | 127 new ObserverList<DocumentModeAssassinObserver>(); |
| 133 | 128 |
| 134 /** Current stage of the migration. */ | 129 /** Current stage of the migration. */ |
| 135 private int mStage = STAGE_UNINITIALIZED; | 130 private int mStage = STAGE_UNINITIALIZED; |
| 136 | 131 |
| 137 /** Whether or not startStage is allowed to progress along the migration pip
eline. */ | 132 /** Whether or not startStage is allowed to progress along the migration pip
eline. */ |
| 138 private boolean mIsPipelineActive; | 133 private boolean mIsPipelineActive; |
| 139 | 134 |
| 140 /** Returns whether or not a migration to tabbed mode from document mode is
necessary. */ | |
| 141 public static boolean isMigrationNecessary() { | |
| 142 return CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_FORCED_
MIGRATION) | |
| 143 && FeatureUtilities.isDocumentMode(ApplicationStatus.getApplicat
ionContext()); | |
| 144 } | |
| 145 | |
| 146 /** Migrates the user from document mode to tabbed mode if necessary. */ | 135 /** Migrates the user from document mode to tabbed mode if necessary. */ |
| 147 @VisibleForTesting | 136 @VisibleForTesting |
| 148 public void migrateFromDocumentToTabbedMode() { | 137 public final void migrateFromDocumentToTabbedMode() { |
| 149 ThreadUtils.assertOnUiThread(); | 138 ThreadUtils.assertOnUiThread(); |
| 150 | 139 |
| 140 // Migration is already underway. |
| 141 if (mStage != STAGE_UNINITIALIZED) return; |
| 142 |
| 143 // If migration isn't necessary, don't do anything. |
| 151 if (!isMigrationNecessary()) { | 144 if (!isMigrationNecessary()) { |
| 152 // Don't kick off anything if we don't need to. | |
| 153 setStage(STAGE_UNINITIALIZED, STAGE_DONE); | 145 setStage(STAGE_UNINITIALIZED, STAGE_DONE); |
| 154 return; | 146 return; |
| 155 } else if (mStage != STAGE_UNINITIALIZED) { | |
| 156 // Migration is already underway. | |
| 157 return; | |
| 158 } | 147 } |
| 159 | 148 |
| 160 // TODO(dfalcantara): Add a pathway to catch repeated migration failures
. | 149 // If we've crashed or failed too many times, send them to tabbed mode w
ithout their data. |
| 150 // - Any incorrect or invalid files in the tabbed mode directory will be
wiped out by the |
| 151 // TabPersistentStore when the ChromeTabbedActivity starts. |
| 152 // |
| 153 // - If it crashes in the step after being migrated, then the user will
simply be left |
| 154 // with a bunch of inaccessible document mode data instead of being st
uck trying to |
| 155 // migrate, which is a lesser evil. This case will be caught by the c
heck above to see if |
| 156 // migration is even necessary. |
| 157 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getContext()); |
| 158 int numMigrationAttempts = prefs.getInt(PREF_NUM_MIGRATION_ATTEMPTS, 0); |
| 159 if (numMigrationAttempts >= MAX_MIGRATION_ATTEMPTS_BEFORE_FAILURE) { |
| 160 Log.e(TAG, "Too many failures. Migrating user to tabbed mode withou
t data."); |
| 161 setStage(STAGE_UNINITIALIZED, STAGE_WRITE_TABMODEL_METADATA_DONE); |
| 162 return; |
| 163 } |
| 161 | 164 |
| 165 // Kick off the migration pipeline. |
| 166 // Using apply() instead of commit() seems to save the preference just f
ine, even if Chrome |
| 167 // crashes immediately afterward. |
| 168 SharedPreferences.Editor editor = prefs.edit(); |
| 169 editor.putInt(PREF_NUM_MIGRATION_ATTEMPTS, numMigrationAttempts + 1); |
| 170 editor.apply(); |
| 162 setStage(STAGE_UNINITIALIZED, STAGE_INITIALIZED); | 171 setStage(STAGE_UNINITIALIZED, STAGE_INITIALIZED); |
| 163 } | 172 } |
| 164 | 173 |
| 165 /** | 174 /** |
| 166 * Makes copies of {@link TabState} files in the document mode directory and
places them in the | 175 * Makes copies of {@link TabState} files in the document mode directory and
places them in the |
| 167 * tabbed mode directory. Only non-Incognito tabs are transferred. | 176 * tabbed mode directory. Only non-Incognito tabs are transferred. |
| 168 * | 177 * |
| 169 * TODO(dfalcantara): Prevent migrating chrome:// pages? | 178 * If the user is out of space on their device, this plows through the migra
tion pathway. |
| 179 * TODO(dfalcantara): Should we do something about this? A user can have at
most 16 tabs in |
| 180 * Android's Recents menu. |
| 170 * | 181 * |
| 171 * @param selectedTabId ID of the last viewed non-Incognito tab. | 182 * @param selectedTabId ID of the last viewed non-Incognito tab. |
| 172 * @param context Context to use when accessing directorie
s. | |
| 173 * @param documentDirectoryOverride Overrides the default location for where
document mode's | |
| 174 * TabState files are expected to be. | |
| 175 * @param tabbedDirectoryOverride Overrides the default location for where
tabbed mode's | |
| 176 * TabState files are expected to be. | |
| 177 */ | 183 */ |
| 178 void copyTabStateFiles(final int selectedTabId, final Context context, | 184 final void copyTabStateFiles(final int selectedTabId) { |
| 179 @Nullable final File documentDirectoryOverride, | |
| 180 @Nullable final File tabbedDirectoryOverride) { | |
| 181 ThreadUtils.assertOnUiThread(); | 185 ThreadUtils.assertOnUiThread(); |
| 182 if (!setStage(STAGE_INITIALIZED, STAGE_COPY_TAB_STATES_STARTED)) return; | 186 if (!setStage(STAGE_INITIALIZED, STAGE_COPY_TAB_STATES_STARTED)) return; |
| 183 | 187 |
| 184 new AsyncTask<Void, Void, Void>() { | 188 new AsyncTask<Void, Void, Void>() { |
| 185 private DocumentTabModelImpl mNormalTabModel; | |
| 186 | |
| 187 @Override | |
| 188 protected void onPreExecute() { | |
| 189 if (documentDirectoryOverride == null) { | |
| 190 mNormalTabModel = (DocumentTabModelImpl) | |
| 191 ChromeApplication.getDocumentTabModelSelector().getM
odel(false); | |
| 192 } | |
| 193 } | |
| 194 | |
| 195 @Override | 189 @Override |
| 196 protected Void doInBackground(Void... params) { | 190 protected Void doInBackground(Void... params) { |
| 197 File documentDirectory = documentDirectoryOverride == null | 191 File documentDirectory = getDocumentDataDirectory(); |
| 198 ? mNormalTabModel.getStorageDelegate().getStateDirectory
() | 192 File tabbedDirectory = getTabbedDataDirectory(); |
| 199 : documentDirectoryOverride; | |
| 200 File tabbedDirectory = tabbedDirectoryOverride == null | |
| 201 ? TabPersistentStore.getStateDirectory(context, TAB_MODE
L_INDEX) | |
| 202 : tabbedDirectoryOverride; | |
| 203 | 193 |
| 204 Log.d(TAG, "Copying TabState files from document to tabbed mode
directory."); | 194 Log.d(TAG, "Copying TabState files from document to tabbed mode
directory."); |
| 205 assert mMigratedTabIds.size() == 0; | 195 assert mMigratedTabIds.size() == 0; |
| 206 | 196 |
| 207 File[] allTabStates = documentDirectory.listFiles(); | 197 File[] allTabStates = documentDirectory.listFiles(); |
| 208 if (allTabStates != null) { | 198 if (allTabStates != null) { |
| 209 // If we know what tab the user was last viewing, copy just
that TabState file | 199 // If we know what tab the user was last viewing, copy just
that TabState file |
| 210 // before all the other ones to mitigate storage issues for
devices with limited | 200 // before all the other ones to mitigate storage issues for
devices with limited |
| 211 // available storage. | 201 // available storage. |
| 212 if (selectedTabId != Tab.INVALID_TAB_ID) { | 202 if (selectedTabId != Tab.INVALID_TAB_ID) { |
| (...skipping 87 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 300 * | 290 * |
| 301 * 1) {@link TabPersistentStore} uses the URL to allow reusing already open
tabs for Home screen | 291 * 1) {@link TabPersistentStore} uses the URL to allow reusing already open
tabs for Home screen |
| 302 * Intents. If a Tab doesn't match the Intent's URL, a new Tab is create
d. This is already | 292 * Intents. If a Tab doesn't match the Intent's URL, a new Tab is create
d. This is already |
| 303 * the case when a cold start launches into document mode because the dat
a is unavailable at | 293 * the case when a cold start launches into document mode because the dat
a is unavailable at |
| 304 * startup. | 294 * startup. |
| 305 * | 295 * |
| 306 * 2) {@link TabModelImpl} uses the URL when it fails to load a Tab's persis
ted TabState. This | 296 * 2) {@link TabModelImpl} uses the URL when it fails to load a Tab's persis
ted TabState. This |
| 307 * means that the user loses some navigation history, but it's not a case
document mode would | 297 * means that the user loses some navigation history, but it's not a case
document mode would |
| 308 * have been able to recover from anyway because the TabState stores the
URL data. | 298 * have been able to recover from anyway because the TabState stores the
URL data. |
| 309 * | 299 * |
| 310 * @param normalTabModel DocumentTabModel containing info about n
on-Incognito tabs. | 300 * @param migratedTabIds IDs of Tabs whose TabState files were copied suc
cessfully. |
| 311 * @param migratedTabIds IDs of Tabs whose TabState files were co
pied successfully. | |
| 312 * @param context Context to access Files from. | |
| 313 * @param tabbedDirectoryOverride Overrides the default location for where
tabbed mode's | |
| 314 * TabState files are expected to be. | |
| 315 */ | 301 */ |
| 316 void writeTabModelMetadata(final DocumentTabModel normalTabModel, | 302 final void writeTabModelMetadata(final Set<Integer> migratedTabIds) { |
| 317 final Set<Integer> migratedTabIds, final Context context, | |
| 318 @Nullable final File tabbedDirectoryOverride) { | |
| 319 ThreadUtils.assertOnUiThread(); | 303 ThreadUtils.assertOnUiThread(); |
| 320 if (!setStage(STAGE_COPY_TAB_STATES_DONE, STAGE_WRITE_TABMODEL_METADATA_
STARTED)) return; | 304 if (!setStage(STAGE_COPY_TAB_STATES_DONE, STAGE_WRITE_TABMODEL_METADATA_
STARTED)) return; |
| 321 | 305 |
| 322 new AsyncTask<Void, Void, Boolean>() { | 306 new AsyncTask<Void, Void, Boolean>() { |
| 323 private byte[] mSerializedMetadata; | 307 private byte[] mSerializedMetadata; |
| 324 | 308 |
| 325 @Override | 309 @Override |
| 326 protected void onPreExecute() { | 310 protected void onPreExecute() { |
| 327 Log.d(TAG, "Beginning to write tabbed mode metadata files."); | 311 Log.d(TAG, "Beginning to write tabbed mode metadata files."); |
| 328 | 312 |
| 329 // Collect information about all the normal tabs on the UI threa
d. | 313 // Collect information about all the normal tabs on the UI threa
d. |
| 314 final DocumentTabModel normalTabModel = getNormalDocumentTabMode
l(); |
| 330 TabModelMetadata normalMetadata = new TabModelMetadata(normalTab
Model.index()); | 315 TabModelMetadata normalMetadata = new TabModelMetadata(normalTab
Model.index()); |
| 331 for (int i = 0; i < normalTabModel.getCount(); i++) { | 316 for (int i = 0; i < normalTabModel.getCount(); i++) { |
| 332 int tabId = normalTabModel.getTabAt(i).getId(); | 317 int tabId = normalTabModel.getTabAt(i).getId(); |
| 333 normalMetadata.ids.add(tabId); | 318 normalMetadata.ids.add(tabId); |
| 334 | 319 |
| 335 if (migratedTabIds.contains(tabId)) { | 320 if (migratedTabIds.contains(tabId)) { |
| 336 // Don't save a URL because it's in the TabState. | 321 // Don't save a URL because it's in the TabState. |
| 337 normalMetadata.urls.add(""); | 322 normalMetadata.urls.add(""); |
| 338 } else { | 323 } else { |
| 339 // The best that can be done is to fall back to the init
ial URL for the Tab. | 324 // The best that can be done is to fall back to the init
ial URL for the Tab. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 351 normalMetadata, incognitoMetadata, null); | 336 normalMetadata, incognitoMetadata, null); |
| 352 } catch (IOException e) { | 337 } catch (IOException e) { |
| 353 Log.e(TAG, "Failed to serialize the TabModel.", e); | 338 Log.e(TAG, "Failed to serialize the TabModel.", e); |
| 354 mSerializedMetadata = null; | 339 mSerializedMetadata = null; |
| 355 } | 340 } |
| 356 } | 341 } |
| 357 | 342 |
| 358 @Override | 343 @Override |
| 359 protected Boolean doInBackground(Void... params) { | 344 protected Boolean doInBackground(Void... params) { |
| 360 if (mSerializedMetadata != null) { | 345 if (mSerializedMetadata != null) { |
| 361 File tabbedDirectory = tabbedDirectoryOverride == null | 346 File tabbedDirectory = getTabbedDataDirectory(); |
| 362 ? TabPersistentStore.getStateDirectory(context, TAB_
MODEL_INDEX) | |
| 363 : tabbedDirectoryOverride; | |
| 364 TabPersistentStore.saveListToFile(tabbedDirectory, mSerializ
edMetadata); | 347 TabPersistentStore.saveListToFile(tabbedDirectory, mSerializ
edMetadata); |
| 365 return true; | 348 return true; |
| 366 } else { | 349 } else { |
| 367 return false; | 350 return false; |
| 368 } | 351 } |
| 369 } | 352 } |
| 370 | 353 |
| 371 @Override | 354 @Override |
| 372 protected void onPostExecute(Boolean result) { | 355 protected void onPostExecute(Boolean result) { |
| 373 // TODO(dfalcantara): What do we do if the metadata file failed
to be written out? | 356 // TODO(dfalcantara): What do we do if the metadata file failed
to be written out? |
| 374 Log.d(TAG, "Finished writing tabbed mode metadata file."); | 357 Log.d(TAG, "Finished writing tabbed mode metadata file."); |
| 375 setStage(STAGE_WRITE_TABMODEL_METADATA_STARTED, STAGE_WRITE_TABM
ODEL_METADATA_DONE); | 358 setStage(STAGE_WRITE_TABMODEL_METADATA_STARTED, STAGE_WRITE_TABM
ODEL_METADATA_DONE); |
| 376 } | 359 } |
| 377 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); | 360 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); |
| 378 } | 361 } |
| 379 | 362 |
| 380 /** | 363 /** Moves the user to tabbed mode by opting them out and removing all the ta
sks. */ |
| 381 * Moves the user to tabbed mode by opting them out. | 364 final void switchToTabbedMode() { |
| 382 * @param context Context to grab SharedPreferences from. | |
| 383 */ | |
| 384 void changePreferences(Context context) { | |
| 385 ThreadUtils.assertOnUiThread(); | 365 ThreadUtils.assertOnUiThread(); |
| 386 if (!setStage(STAGE_WRITE_TABMODEL_METADATA_DONE, STAGE_CHANGE_SETTINGS_
STARTED)) return; | 366 if (!setStage(STAGE_WRITE_TABMODEL_METADATA_DONE, STAGE_CHANGE_SETTINGS_
STARTED)) return; |
| 387 | 367 |
| 388 // Record that the user has opted-out of document mode now that their da
ta has been | 368 // Record that the user has opted-out of document mode now that their da
ta has been |
| 389 // safely copied to the other directory. | 369 // safely copied to the other directory. |
| 390 Log.d(TAG, "Setting tabbed mode preference."); | 370 Log.d(TAG, "Setting tabbed mode preference."); |
| 391 DocumentModeManager.getInstance(context).setOptedOutState( | 371 DocumentModeManager.getInstance(getContext()).setOptedOutState( |
| 392 DocumentModeManager.OPTED_OUT_OF_DOCUMENT_MODE); | 372 DocumentModeManager.OPTED_OUT_OF_DOCUMENT_MODE); |
| 393 | 373 |
| 374 // Remove all the {@link DocumentActivity} tasks from Android's Recents
list. Users |
| 375 // viewing Recents during migration will continue to see their tabs unti
l they exit. |
| 376 // Reselecting a migrated tab will kick the user to the launcher without
crashing. |
| 377 // TODO(dfalcantara): Confirm that the different Android flavors work th
e same way. |
| 378 createActivityDelegate().finishAllDocumentActivities(); |
| 379 |
| 380 // Dismiss the "Close all incognito tabs" notification. |
| 381 IncognitoNotificationManager.dismissIncognitoNotification(); |
| 382 |
| 394 setStage(STAGE_CHANGE_SETTINGS_STARTED, STAGE_CHANGE_SETTINGS_DONE); | 383 setStage(STAGE_CHANGE_SETTINGS_STARTED, STAGE_CHANGE_SETTINGS_DONE); |
| 395 } | 384 } |
| 396 | 385 |
| 397 /** TODO(dfalcantara): Add a unit test for this function. */ | 386 /** Deletes all remnants of the document mode directory and preferences. */ |
| 398 private void deleteDocumentModeData(final Context context) { | 387 final void deleteDocumentModeData() { |
| 399 ThreadUtils.assertOnUiThread(); | 388 ThreadUtils.assertOnUiThread(); |
| 400 if (!setStage(STAGE_CHANGE_SETTINGS_DONE, STAGE_DELETION_STARTED)) retur
n; | 389 if (!setStage(STAGE_CHANGE_SETTINGS_DONE, STAGE_DELETION_STARTED)) retur
n; |
| 401 | 390 |
| 402 new AsyncTask<Void, Void, Void>() { | 391 new AsyncTask<Void, Void, Void>() { |
| 403 @Override | 392 @Override |
| 404 protected Void doInBackground(Void... params) { | 393 protected Void doInBackground(Void... params) { |
| 405 Log.d(TAG, "Starting to delete document mode data."); | 394 Log.d(TAG, "Starting to delete document mode data."); |
| 406 | 395 |
| 407 // Remove all the {@link DocumentActivity} tasks from Android's
Recents list. Users | |
| 408 // viewing Recents during migration will continue to see their t
abs until they exit. | |
| 409 // Reselecting a migrated tab will kick the user to the launcher
without crashing. | |
| 410 // TODO(dfalcantara): Confirm that the different Android flavors
work the same way. | |
| 411 ActivityDelegate delegate = new ActivityDelegateImpl( | |
| 412 DocumentActivity.class, IncognitoDocumentActivity.class)
; | |
| 413 delegate.finishAllDocumentActivities(); | |
| 414 | |
| 415 // Delete the old tab state directory. | 396 // Delete the old tab state directory. |
| 416 StorageDelegate migrationStorageDelegate = new StorageDelegate()
; | 397 FileUtils.recursivelyDeleteFile(getDocumentDataDirectory()); |
| 417 FileUtils.recursivelyDeleteFile(migrationStorageDelegate.getStat
eDirectory()); | |
| 418 | 398 |
| 419 // Clean up the {@link DocumentTabModel} shared preferences. | 399 // Clean up the {@link DocumentTabModel} shared preferences. |
| 420 SharedPreferences prefs = context.getSharedPreferences( | 400 SharedPreferences prefs = getContext().getSharedPreferences( |
| 421 DocumentTabModelImpl.PREF_PACKAGE, Context.MODE_PRIVATE)
; | 401 DocumentTabModelImpl.PREF_PACKAGE, Context.MODE_PRIVATE)
; |
| 422 SharedPreferences.Editor editor = prefs.edit(); | 402 SharedPreferences.Editor editor = prefs.edit(); |
| 423 editor.clear(); | 403 editor.clear(); |
| 424 editor.apply(); | 404 editor.apply(); |
| 425 return null; | 405 return null; |
| 426 } | 406 } |
| 427 | 407 |
| 428 @Override | 408 @Override |
| 429 protected void onPostExecute(Void result) { | 409 protected void onPostExecute(Void result) { |
| 430 Log.d(TAG, "Finished deleting document mode data."); | 410 Log.d(TAG, "Finished deleting document mode data."); |
| 431 setStage(STAGE_DELETION_STARTED, STAGE_DONE); | 411 setStage(STAGE_DELETION_STARTED, STAGE_DONE); |
| 432 } | 412 } |
| 433 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); | 413 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); |
| 434 } | 414 } |
| 435 | 415 |
| 436 /** | 416 /** |
| 437 * Updates the stage of the migration. | 417 * Updates the stage of the migration. |
| 438 * @param expectedStage Stage of the pipeline that is currently expected. | 418 * @param expectedStage Stage of the pipeline that is currently expected. |
| 439 * @param newStage Stage of the pipeline that is being activated. | 419 * @param newStage Stage of the pipeline that is being activated. |
| 420 * @return Whether or not the stage was updated. |
| 440 */ | 421 */ |
| 441 private boolean setStage(int expectedStage, int newStage) { | 422 private final boolean setStage(int expectedStage, int newStage) { |
| 442 ThreadUtils.assertOnUiThread(); | 423 ThreadUtils.assertOnUiThread(); |
| 443 | 424 |
| 444 assert mStage == expectedStage; | 425 if (mStage != expectedStage) { |
| 426 Log.e(TAG, "Wrong stage encountered: expected " + expectedStage + "
but in " + mStage); |
| 427 return false; |
| 428 } |
| 445 mStage = newStage; | 429 mStage = newStage; |
| 446 | 430 |
| 447 for (DocumentModeAssassinObserver callback : mObservers) callback.onStag
eChange(newStage); | 431 for (DocumentModeAssassinObserver callback : mObservers) callback.onStag
eChange(newStage); |
| 448 startStage(newStage); | 432 startStage(newStage); |
| 449 return true; | 433 return true; |
| 450 } | 434 } |
| 451 | 435 |
| 452 /** | 436 /** |
| 453 * Kicks off tasks for the new state of the pipeline. | 437 * Kicks off tasks for the new state of the pipeline. |
| 454 * | 438 * |
| 455 * We don't wait for the DocumentTabModel to finish parsing its metadata fil
e before proceeding | 439 * We don't wait for the DocumentTabModel to finish parsing its metadata fil
e before proceeding |
| 456 * with migration because it doesn't have actionable information: | 440 * with migration because it doesn't have actionable information: |
| 457 * | 441 * |
| 458 * 1) WE DON'T NEED TO RE-POPULATE THE "RECENTLY CLOSED" LIST: | 442 * 1) WE DON'T NEED TO RE-POPULATE THE "RECENTLY CLOSED" LIST: |
| 459 * The metadata file contains a list of tabs Chrome knew about before it
died, which | 443 * The metadata file contains a list of tabs Chrome knew about before it
died, which |
| 460 * could differ from the list of tabs in Android Overview. The canonical
list of | 444 * could differ from the list of tabs in Android Overview. The canonical
list of |
| 461 * live tabs, however, has always been the ones displayed by the Android
Overview. | 445 * live tabs, however, has always been the ones displayed by the Android
Overview. |
| 462 * | 446 * |
| 463 * 2) RETARGETING MIGRATED TABS FROM THE HOME SCREEN IS A CORNER CASE: | 447 * 2) RETARGETING MIGRATED TABS FROM THE HOME SCREEN IS A CORNER CASE: |
| 464 * The only downside here is that Chrome ends up creating a new tab for a
home screen | 448 * The only downside here is that Chrome ends up creating a new tab for a
home screen |
| 465 * shortcut the first time they start Chrome after migration. This was a
lready | 449 * shortcut the first time they start Chrome after migration. This was a
lready |
| 466 * broken for document mode during cold starts, anyway. | 450 * broken for document mode during cold starts, anyway. |
| 467 */ | 451 */ |
| 468 private void startStage(int newStage) { | 452 private final void startStage(int newStage) { |
| 469 ThreadUtils.assertOnUiThread(); | 453 ThreadUtils.assertOnUiThread(); |
| 470 if (!mIsPipelineActive) return; | 454 if (!mIsPipelineActive) return; |
| 471 | 455 |
| 472 Context context = ApplicationStatus.getApplicationContext(); | |
| 473 if (newStage == STAGE_INITIALIZED) { | 456 if (newStage == STAGE_INITIALIZED) { |
| 474 Log.d(TAG, "Migrating user into tabbed mode."); | 457 Log.d(TAG, "Migrating user into tabbed mode."); |
| 475 int selectedTabId = DocumentUtils.getLastShownTabIdFromPrefs(context
, false); | 458 int selectedTabId = DocumentUtils.getLastShownTabIdFromPrefs(getCont
ext(), false); |
| 476 copyTabStateFiles(selectedTabId, context, null, null); | 459 copyTabStateFiles(selectedTabId); |
| 477 } else if (newStage == STAGE_COPY_TAB_STATES_DONE) { | 460 } else if (newStage == STAGE_COPY_TAB_STATES_DONE) { |
| 478 Log.d(TAG, "Writing tabbed mode metadata file."); | 461 Log.d(TAG, "Writing tabbed mode metadata file."); |
| 479 DocumentTabModelSelector selector = ChromeApplication.getDocumentTab
ModelSelector(); | 462 writeTabModelMetadata(mMigratedTabIds); |
| 480 DocumentTabModelImpl normalTabModel = | |
| 481 (DocumentTabModelImpl) selector.getModel(false); | |
| 482 writeTabModelMetadata(normalTabModel, mMigratedTabIds, context, null
); | |
| 483 } else if (newStage == STAGE_WRITE_TABMODEL_METADATA_DONE) { | 463 } else if (newStage == STAGE_WRITE_TABMODEL_METADATA_DONE) { |
| 484 Log.d(TAG, "Changing user preference."); | 464 Log.d(TAG, "Changing user preference."); |
| 485 changePreferences(context); | 465 switchToTabbedMode(); |
| 486 } else if (newStage == STAGE_CHANGE_SETTINGS_DONE) { | 466 } else if (newStage == STAGE_CHANGE_SETTINGS_DONE) { |
| 487 Log.d(TAG, "Cleaning up document mode data."); | 467 Log.d(TAG, "Cleaning up document mode data."); |
| 488 deleteDocumentModeData(context); | 468 deleteDocumentModeData(); |
| 489 } | 469 } |
| 490 } | 470 } |
| 491 | 471 |
| 492 | 472 /** @return the current stage of the pipeline. */ |
| 493 /** | 473 public final int getStage() { |
| 494 * Returns the current stage of the pipeline. | |
| 495 */ | |
| 496 @VisibleForTesting | |
| 497 public int getStage() { | |
| 498 ThreadUtils.assertOnUiThread(); | 474 ThreadUtils.assertOnUiThread(); |
| 499 return mStage; | 475 return mStage; |
| 500 } | 476 } |
| 501 | 477 |
| 502 | |
| 503 /** | 478 /** |
| 504 * Adds a observer that is alerted as migration progresses. | 479 * Adds a observer that is alerted as migration progresses. |
| 505 * | 480 * |
| 506 * @param observer Observer to add. | 481 * @param observer Observer to add. |
| 507 */ | 482 */ |
| 508 @VisibleForTesting | 483 public final void addObserver(final DocumentModeAssassinObserver observer) { |
| 509 public void addObserver(final DocumentModeAssassinObserver observer) { | |
| 510 ThreadUtils.assertOnUiThread(); | 484 ThreadUtils.assertOnUiThread(); |
| 511 mObservers.addObserver(observer); | 485 mObservers.addObserver(observer); |
| 512 } | 486 } |
| 513 | 487 |
| 514 /** | 488 /** |
| 515 * Removes an Observer. | 489 * Removes an Observer. |
| 516 * | 490 * |
| 517 * @param observer Observer to remove. | 491 * @param observer Observer to remove. |
| 518 */ | 492 */ |
| 519 @VisibleForTesting | 493 public final void removeObserver(final DocumentModeAssassinObserver observer
) { |
| 520 public void removeObserver(final DocumentModeAssassinObserver observer) { | |
| 521 ThreadUtils.assertOnUiThread(); | 494 ThreadUtils.assertOnUiThread(); |
| 522 mObservers.removeObserver(observer); | 495 mObservers.removeObserver(observer); |
| 523 } | 496 } |
| 524 | 497 |
| 525 /** | |
| 526 * Creates a DocumentModeAssassin that starts at the given stage and does no
t automatically | |
| 527 * move along the pipeline. | |
| 528 * | |
| 529 * @param stage Stage to start at. See the STAGE_* values above. | |
| 530 * @return DocumentModeAssassin that can be used for testing specific stages
of the pipeline. | |
| 531 */ | |
| 532 @VisibleForTesting | |
| 533 public static DocumentModeAssassin createForTesting(int stage) { | |
| 534 return new DocumentModeAssassin(stage, false); | |
| 535 } | |
| 536 | |
| 537 private DocumentModeAssassin() { | 498 private DocumentModeAssassin() { |
| 538 this(isMigrationNecessary() ? STAGE_UNINITIALIZED : STAGE_DONE, true); | 499 mStage = isMigrationNecessary() ? STAGE_UNINITIALIZED : STAGE_DONE; |
| 500 mIsPipelineActive = true; |
| 539 } | 501 } |
| 540 | 502 |
| 541 private DocumentModeAssassin(int stage, boolean isPipelineActive) { | 503 private DocumentModeAssassin(int stage, boolean isPipelineActive) { |
| 542 mStage = stage; | 504 mStage = stage; |
| 543 mIsPipelineActive = isPipelineActive; | 505 mIsPipelineActive = isPipelineActive; |
| 544 } | 506 } |
| 507 |
| 508 /** DocumentModeAssassin that can have its methods be overridden for testing
. */ |
| 509 static class DocumentModeAssassinForTesting extends DocumentModeAssassin { |
| 510 /** |
| 511 * Creates a DocumentModeAssassin that starts at the given stage. |
| 512 * |
| 513 * @param stage Stage to start at. See the STAGE_* values a
bove. |
| 514 * @param isPipelineActive Whether the pipeline should continue after a
stage finishes. |
| 515 */ |
| 516 @VisibleForTesting |
| 517 DocumentModeAssassinForTesting(int stage, boolean isPipelineActive) { |
| 518 super(stage, isPipelineActive); |
| 519 } |
| 520 } |
| 521 |
| 522 /** @return Whether or not a migration to tabbed mode from document mode is
necessary. */ |
| 523 public boolean isMigrationNecessary() { |
| 524 return CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_FORCED_
MIGRATION) |
| 525 && FeatureUtilities.isDocumentMode(getContext()); |
| 526 } |
| 527 |
| 528 /** @return Context to use when grabbing SharedPreferences, Files, and other
resources. */ |
| 529 protected Context getContext() { |
| 530 return ApplicationStatus.getApplicationContext(); |
| 531 } |
| 532 |
| 533 /** @return Interfaces with the Android ActivityManager. */ |
| 534 protected ActivityDelegate createActivityDelegate() { |
| 535 return new ActivityDelegateImpl(DocumentActivity.class, IncognitoDocumen
tActivity.class); |
| 536 } |
| 537 |
| 538 /** @return The {@link DocumentTabModelImpl} for regular tabs. */ |
| 539 protected DocumentTabModel getNormalDocumentTabModel() { |
| 540 return ChromeApplication.getDocumentTabModelSelector().getModel(false); |
| 541 } |
| 542 |
| 543 /** @return Where document mode data is stored for the normal {@link Documen
tTabModel}. */ |
| 544 protected File getDocumentDataDirectory() { |
| 545 return new StorageDelegate().getStateDirectory(); |
| 546 } |
| 547 |
| 548 /** @return Where tabbed mode data is stored for the main {@link TabModelImp
l}. */ |
| 549 protected File getTabbedDataDirectory() { |
| 550 return TabPersistentStore.getStateDirectory(getContext(), TAB_MODEL_INDE
X); |
| 551 } |
| 545 } | 552 } |
| OLD | NEW |