Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(843)

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicy.java

Issue 2277603002: Extract tabbed mode specific logic from the TabPersistenceStore. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix findbugs Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicy.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicy.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..65c21e748a342e14f9ac0b38cfd4e81d9bcd6186
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicy.java
@@ -0,0 +1,435 @@
+// Copyright 2016 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.tabmodel;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.StrictMode;
+import android.support.annotation.WorkerThread;
+import android.util.Pair;
+import android.util.SparseBooleanArray;
+
+import org.chromium.base.Callback;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+import org.chromium.base.PathUtils;
+import org.chromium.base.StreamUtil;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.browser.TabState;
+import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Handles the Tabbed mode specific behaviors of tab persistence.
+ */
+public class TabbedModeTabPersistencePolicy implements TabPersistencePolicy {
+
+ private static final String TAG = "tabmodel";
+
+ /** The name of the file where the state is saved. */
+ @VisibleForTesting
+ public static final String SAVED_STATE_FILE = "tab_state";
+
+ @VisibleForTesting
+ static final String PREF_HAS_RUN_FILE_MIGRATION =
+ "org.chromium.chrome.browser.tabmodel.TabPersistentStore.HAS_RUN_FILE_MIGRATION";
+
+ @VisibleForTesting
+ static final String PREF_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION =
+ "org.chromium.chrome.browser.tabmodel.TabPersistentStore."
+ + "HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION";
+
+ /** The name of the directory where the state is saved. */
+ @VisibleForTesting
+ static final String SAVED_STATE_DIRECTORY = "0";
+
+ /** Prevents two copies of the Migration task from being created. */
+ private static final Object MIGRATION_LOCK = new Object();
+ /** Prevents two state directories from getting created simultaneously. */
+ private static final Object DIR_CREATION_LOCK = new Object();
+ /**
+ * Prevents two clean up tasks from getting created simultaneously. Also protects against
+ * incorrectly interleaving create/run/cancel on the task.
+ */
+ private static final Object CLEAN_UP_TASK_LOCK = new Object();
+ /** Tracks whether tabs from two TabPersistentStores tabs are being merged together. */
+ private static final AtomicBoolean MERGE_IN_PROGRESS = new AtomicBoolean();
+
+ private static AsyncTask<Void, Void, Void> sMigrationTask;
+ private static AsyncTask<Void, Void, Void> sCleanupTask;
+
+ private static File sStateDirectory;
+
+ private final SharedPreferences mPreferences;
+ private final Context mContext;
+ private final int mSelectorIndex;
+ private final int mOtherSelectorIndex;
+
+ private TabContentManager mTabContentManager;
+ private boolean mDestroyed;
+
+ /**
+ * Constructs a persistence policy that handles the Tabbed mode specific logic.
+ *
+ * @param context A Context instance.
+ * @param selectorIndex The index that represents which state file to pull and save state to.
+ * This is used when there can be more than one TabModelSelector.
+ */
+ public TabbedModeTabPersistencePolicy(Context context, int selectorIndex) {
+ mPreferences = ContextUtils.getAppSharedPreferences();
+ mContext = context;
+ mSelectorIndex = selectorIndex;
+ mOtherSelectorIndex = selectorIndex == 0 ? 1 : 0;
+ }
+
+ @Override
+ public File getOrCreateStateDirectory() {
+ return getOrCreateTabbedModeStateDirectory();
+ }
+
+ @Override
+ public String getStateFileName() {
+ return getStateFileName(mSelectorIndex);
+ }
+
+ @Override
+ public String getStateToBeMergedFileName() {
+ return getStateFileName(mOtherSelectorIndex);
+ }
+
+ /**
+ * @param selectorIndex The index that represents which state file to pull and save state to.
+ * @return The name of the state file.
+ */
+ @VisibleForTesting
+ public static String getStateFileName(int selectorIndex) {
+ return TabPersistentStore.getStateFileName(Integer.toString(selectorIndex));
+ }
+
+ /**
+ * The folder where the state should be saved to.
+ * @return A file representing the directory that contains TabModelSelector states.
+ */
+ public static File getOrCreateTabbedModeStateDirectory() {
+ synchronized (DIR_CREATION_LOCK) {
+ if (sStateDirectory == null) {
+ sStateDirectory = new File(
+ TabPersistentStore.getOrCreateBaseStateDirectory(), SAVED_STATE_DIRECTORY);
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+ StrictMode.allowThreadDiskWrites();
+ try {
+ if (!sStateDirectory.exists() && !sStateDirectory.mkdirs()) {
+ Log.e(TAG, "Failed to create state folder: " + sStateDirectory);
+ }
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+ }
+ return sStateDirectory;
+ }
+
+ @Override
+ public boolean performMigration(Executor executor) {
+ ThreadUtils.assertOnUiThread();
+
+ final boolean hasRunLegacyMigration =
+ mPreferences.getBoolean(PREF_HAS_RUN_FILE_MIGRATION, false);
+ final boolean hasRunMultiInstanceMigration =
+ mPreferences.getBoolean(PREF_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION, false);
+
+ if (hasRunLegacyMigration && hasRunMultiInstanceMigration) return false;
+
+ synchronized (MIGRATION_LOCK) {
+ if (sMigrationTask != null) return true;
+ sMigrationTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (!hasRunLegacyMigration) {
+ performLegacyMigration();
+ }
+
+ // It's possible that the legacy migration ran in the past but the preference
+ // wasn't set, because the legacy migration hasn't always set a preference upon
+ // completion. If the legacy migration has already been performed,
+ // performLecacyMigration() will exit early without renaming the metadata file,
+ // so the multi-instance migration is still necessary.
+ if (!hasRunMultiInstanceMigration) {
+ performMultiInstanceMigration();
+ }
+
+ return null;
+ }
+ }.executeOnExecutor(executor);
+ return true;
+ }
+ }
+
+ /**
+ * Upgrades users from an old version of Chrome when the state file was still in the root
+ * directory.
+ */
+ @WorkerThread
+ private void performLegacyMigration() {
+ File newFolder = getOrCreateStateDirectory();
+ File[] newFiles = newFolder.listFiles();
+ // Attempt migration if we have no tab state file in the new directory.
+ if (newFiles == null || newFiles.length == 0) {
+ File oldFolder = mContext.getFilesDir();
+ File modelFile = new File(oldFolder, SAVED_STATE_FILE);
+ if (modelFile.exists()) {
+ if (!modelFile.renameTo(new File(newFolder, getStateFileName()))) {
+ Log.e(TAG, "Failed to rename file: " + modelFile);
+ }
+ }
+
+ File[] files = oldFolder.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (TabState.parseInfoFromFilename(file.getName()) != null) {
+ if (!file.renameTo(new File(newFolder, file.getName()))) {
+ Log.e(TAG, "Failed to rename file: " + file);
+ }
+ }
+ }
+ }
+ }
+ setLegacyFileMigrationPref();
+ }
+
+ /**
+ * Upgrades users from an older version of Chrome when the state files for multi-instance
+ * were each kept in separate subdirectories.
+ */
+ @WorkerThread
+ private void performMultiInstanceMigration() {
+ // 1. Rename tab metadata file for tab directory "0".
+ File stateDir = getOrCreateStateDirectory();
+ File metadataFile = new File(stateDir, SAVED_STATE_FILE);
+ if (metadataFile.exists()) {
+ if (!metadataFile.renameTo(new File(stateDir, getStateFileName()))) {
+ Log.e(TAG, "Failed to rename file: " + metadataFile);
+ }
+ }
+
+ // 2. Move files from other state directories.
+ for (int i = TabModelSelectorImpl.CUSTOM_TABS_SELECTOR_INDEX;
+ i < TabWindowManager.MAX_SIMULTANEOUS_SELECTORS; i++) {
+ // Skip the directory we're migrating to.
+ if (i == 0) continue;
+
+ File otherStateDir = new File(
+ TabPersistentStore.getOrCreateBaseStateDirectory(), Integer.toString(i));
+ if (otherStateDir == null || !otherStateDir.exists()) continue;
+
+ // Rename tab state file.
+ metadataFile = new File(otherStateDir, SAVED_STATE_FILE);
+ if (metadataFile.exists()) {
+ if (!metadataFile.renameTo(new File(stateDir, getStateFileName(i)))) {
+ Log.e(TAG, "Failed to rename file: " + metadataFile);
+ }
+ }
+
+ // Rename tab files.
+ File[] files = otherStateDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (TabState.parseInfoFromFilename(file.getName()) != null) {
+ // Custom tabs does not currently use tab files. Delete them rather than
+ // migrating.
+ if (i == TabModelSelectorImpl.CUSTOM_TABS_SELECTOR_INDEX) {
+ if (!file.delete()) {
+ Log.e(TAG, "Failed to delete file: " + file);
+ }
+ continue;
+ }
+
+ // If the tab was moved between windows in Android N multi-window, the tab
+ // file may exist in both directories. Keep whichever was modified more
+ // recently.
+ File newFileName = new File(stateDir, file.getName());
+ if (newFileName.exists()
+ && newFileName.lastModified() > file.lastModified()) {
+ if (!file.delete()) {
+ Log.e(TAG, "Failed to delete file: " + file);
+ }
+ } else if (!file.renameTo(newFileName)) {
+ Log.e(TAG, "Failed to rename file: " + file);
+ }
+ }
+ }
+ }
+
+ // Delete other state directory.
+ if (!otherStateDir.delete()) {
+ Log.e(TAG, "Failed to delete directory: " + otherStateDir);
+ }
+ }
+
+ setMultiInstanceFileMigrationPref();
+ }
+
+ private void setLegacyFileMigrationPref() {
+ mPreferences.edit().putBoolean(PREF_HAS_RUN_FILE_MIGRATION, true).apply();
+ }
+
+ private void setMultiInstanceFileMigrationPref() {
+ mPreferences.edit().putBoolean(PREF_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION, true).apply();
+ }
+
+ @Override
+ public void waitForMigrationToFinish() {
+ if (sMigrationTask == null) return;
+ try {
+ sMigrationTask.get();
+ } catch (InterruptedException e) {
+ } catch (ExecutionException e) {
+ }
+ }
+
+ @Override
+ public boolean isMergeInProgress() {
+ return MERGE_IN_PROGRESS.get();
+ }
+
+ @Override
+ public void setMergeInProgress(boolean isStarted) {
+ MERGE_IN_PROGRESS.set(isStarted);
+ }
+
+ @Override
+ public void cancelCleanupInProgress() {
+ synchronized (CLEAN_UP_TASK_LOCK) {
+ if (sCleanupTask != null) sCleanupTask.cancel(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Creates an asynchronous task to delete persistent data. The task is run using a thread pool
+ * and may be executed in parallel with other tasks. The cleanup task use a combination of the
+ * current model and the tab state files for other models to determine which tab files should
+ * be deleted. The cleanup task should be canceled if a second tab model is created.
+ */
+ @Override
+ public void cleanupUnusedFiles(Callback<List<String>> filesToDelete) {
+ synchronized (CLEAN_UP_TASK_LOCK) {
+ if (sCleanupTask != null) sCleanupTask.cancel(true);
+ sCleanupTask = new CleanUpTabStateDataTask(filesToDelete);
+ sCleanupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ @Override
+ public void setTabContentManager(TabContentManager cache) {
+ mTabContentManager = cache;
+ }
+
+ @Override
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ private class CleanUpTabStateDataTask extends AsyncTask<Void, Void, Void> {
+ private final Callback<List<String>> mFilesToDeleteCallback;
+
+ private String[] mTabFileNames;
+ private String[] mThumbnailFileNames;
+ private SparseBooleanArray mOtherTabIds;
+
+ CleanUpTabStateDataTask(Callback<List<String>> filesToDelete) {
+ mFilesToDeleteCallback = filesToDelete;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ if (mDestroyed) return null;
+
+ mTabFileNames = getOrCreateStateDirectory().list();
+ String thumbnailDirectory = PathUtils.getThumbnailCacheDirectory(mContext);
+ mThumbnailFileNames = new File(thumbnailDirectory).list();
+
+ mOtherTabIds = new SparseBooleanArray();
+ getTabsFromOtherStateFiles(mOtherTabIds);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void unused) {
+ if (mDestroyed) return;
+ TabWindowManager tabWindowManager = TabWindowManager.getInstance();
+
+ if (mTabFileNames != null) {
+ List<String> filesToDelete = new ArrayList<>();
+ for (String fileName : mTabFileNames) {
+ Pair<Integer, Boolean> data = TabState.parseInfoFromFilename(fileName);
+ if (data != null) {
+ int tabId = data.first;
+ if (shouldDeleteTabFile(tabId, tabWindowManager)) {
+ filesToDelete.add(fileName);
+ }
+ }
+ }
+ mFilesToDeleteCallback.onResult(filesToDelete);
+ }
+ if (mTabContentManager != null && mThumbnailFileNames != null) {
+ for (String fileName : mThumbnailFileNames) {
+ try {
+ int tabId = Integer.parseInt(fileName);
+ if (shouldDeleteTabFile(tabId, tabWindowManager)) {
+ mTabContentManager.removeTabThumbnail(tabId);
+ }
+ } catch (NumberFormatException expected) {
+ // This is an unknown file name, we'll leave it there.
+ }
+ }
+ }
+ }
+
+ private boolean shouldDeleteTabFile(int tabId, TabWindowManager tabWindowManager) {
+ return !tabWindowManager.tabExistsInAnySelector(tabId) && !mOtherTabIds.get(tabId);
+ }
+
+ /**
+ * Gets the IDs of all tabs in TabModelSelectors other than the currently selected one. IDs
+ * for custom tabs are excluded.
+ * @param tabIds SparseBooleanArray to populate with TabIds.
+ */
+ private void getTabsFromOtherStateFiles(SparseBooleanArray tabIds) {
+ for (int i = 0; i < TabWindowManager.MAX_SIMULTANEOUS_SELECTORS; i++) {
+ // Although we check all selectors before deleting, we can only be sure that our own
+ // selector will not go away between now and then. So, we read from disk all other
+ // state files, even if they are already loaded by another selector.
+ if (i == mSelectorIndex) continue;
+
+ File metadataFile = new File(getOrCreateStateDirectory(), getStateFileName(i));
+ if (metadataFile.exists()) {
+ DataInputStream stream = null;
+ try {
+ stream = new DataInputStream(
+ new BufferedInputStream(new FileInputStream(metadataFile)));
+ TabPersistentStore.readSavedStateFile(stream, null, tabIds, false);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to read state for " + metadataFile.getName() + ": " + e);
+ } finally {
+ StreamUtil.closeQuietly(stream);
+ }
+ }
+ }
+ }
+ }
+}
« no previous file with comments | « chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabWindowManager.java ('k') | chrome/android/java_sources.gni » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698