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

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java

Issue 2296833002: Add tab persistence support for CustomTabs. (Closed)
Patch Set: Address yusufo@s comments 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/customtabs/CustomTabTabPersistencePolicy.java
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a3a04c1f92c2e9d8570bd013204c89cc1886bb8
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java
@@ -0,0 +1,390 @@
+// 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.customtabs;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.os.StrictMode;
+import android.util.Pair;
+import android.util.SparseBooleanArray;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.Callback;
+import org.chromium.base.Log;
+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 org.chromium.chrome.browser.tabmodel.TabModel;
+import org.chromium.chrome.browser.tabmodel.TabModelSelector;
+import org.chromium.chrome.browser.tabmodel.TabPersistencePolicy;
+import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+import javax.annotation.Nullable;
+
+/**
+ * Handles the Custom Tab specific behaviors of tab persistence.
+ */
+public class CustomTabTabPersistencePolicy implements TabPersistencePolicy {
+
+ static final String SAVED_STATE_DIRECTORY = "custom_tabs";
+
+ /** Threshold where old state files should be deleted (30 days). */
+ @VisibleForTesting
+ protected static final long STATE_EXPIRY_THRESHOLD = 30L * 24 * 60 * 60 * 1000;
+
+ /** Maximum number of state files before we should start deleting old ones. */
+ @VisibleForTesting
+ protected static final int MAXIMUM_STATE_FILES = 30;
+
+ private static final String TAG = "tabmodel";
+
+ /** 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();
+
+ private static File sStateDirectory;
+ private static AsyncTask<Void, Void, Void> sCleanupTask;
+
+ /**
+ * The folder where the state should be saved to.
+ * @return A file representing the directory that contains TabModelSelector states.
+ */
+ public static File getOrCreateCustomTabModeStateDirectory() {
+ 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;
+ }
+
+ private final int mTaskId;
+ private final boolean mShouldRestore;
+
+ private AsyncTask<Void, Void, Void> mInitializationTask;
+ private boolean mDestroyed;
+
+ /**
+ * Constructs a persistence policy for a given Custom Tab.
+ *
+ * @param taskId The task ID that the owning Custom Tab is in.
+ * @param shouldRestore Whether an attempt to restore tab state information should be done on
+ * startup.
+ */
+ public CustomTabTabPersistencePolicy(int taskId, boolean shouldRestore) {
+ mTaskId = taskId;
+ mShouldRestore = shouldRestore;
+ }
+
+ @Override
+ public File getOrCreateStateDirectory() {
+ return getOrCreateCustomTabModeStateDirectory();
+ }
+
+ @Override
+ public String getStateFileName() {
+ return TabPersistentStore.getStateFileName(Integer.toString(mTaskId));
+ }
+
+ @Override
+ @Nullable
+ public String getStateToBeMergedFileName() {
+ return null;
+ }
+
+ @Override
+ public boolean performInitialization(Executor executor) {
+ mInitializationTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ File stateDir = getOrCreateStateDirectory();
+ File metadataFile = new File(stateDir, getStateFileName());
+ if (metadataFile.exists()) {
gone 2016/08/31 17:59:16 Flip it so if (mShouldRestore) is on top?
Ted C 2016/08/31 18:37:53 Done.
+ if (!mShouldRestore) {
+ if (!metadataFile.delete()) {
+ Log.e(TAG, "Failed to delete file: " + metadataFile);
+ }
+ } else {
+ if (!metadataFile.setLastModified(System.currentTimeMillis())) {
+ Log.e(TAG, "Unable to update last modified time: " + metadataFile);
+ }
+ }
+ }
+ return null;
+ }
+ }.executeOnExecutor(executor);
+
+ return true;
+ }
+
+ @Override
+ public void waitForInitializationToFinish() {
+ if (mInitializationTask == null) return;
+ try {
+ mInitializationTask.get();
+ } catch (InterruptedException | ExecutionException e) {
+ // Ignore and proceed.
+ }
+ }
+
+ @Override
+ public boolean isMergeInProgress() {
+ return false;
+ }
+
+ @Override
+ public void setMergeInProgress(boolean isStarted) {
+ assert false : "Merge not supported in Custom Tabs";
+ }
+
+ @Override
+ public void cancelCleanupInProgress() {
+ synchronized (CLEAN_UP_TASK_LOCK) {
+ if (sCleanupTask != null) sCleanupTask.cancel(true);
+ }
+ }
+
+ @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) {
+ }
+
+ @Override
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ /**
+ * Given a list of metadata files, determine which are applicable for deletion based on the
+ * deletion strategy of Custom Tabs.
+ *
+ * @param currentTimeMillis The current time in milliseconds
+ * ({@link System#currentTimeMillis()}.
+ * @param allMetadataFiles The complete list of all metadata files to check.
+ * @return The list of metadata files that are applicable for deletion.
+ */
+ protected static List<File> getMetadataFilesForDeletion(
+ long currentTimeMillis, List<File> allMetadataFiles) {
+ Collections.sort(allMetadataFiles, new Comparator<File>() {
+ @Override
+ public int compare(File lhs, File rhs) {
+ long lhsModifiedTime = lhs.lastModified();
+ long rhsModifiedTime = rhs.lastModified();
+
+ // Sort such that older files (those with an lower timestamp number) are at the
+ // end of the sorted listed.
+ return ApiCompatibilityUtils.compareLong(rhsModifiedTime, lhsModifiedTime);
+ }
+ });
+
+ List<File> stateFilesApplicableForDeletion = new ArrayList<File>();
+ for (int i = 0; i < allMetadataFiles.size(); i++) {
+ File file = allMetadataFiles.get(i);
+ long fileAge = currentTimeMillis - file.lastModified();
+ if (i >= MAXIMUM_STATE_FILES || fileAge >= STATE_EXPIRY_THRESHOLD) {
+ stateFilesApplicableForDeletion.add(file);
+ }
+ }
+ return stateFilesApplicableForDeletion;
+ }
+
+ /**
+ * Get all current Tab IDs used by the specified activity.
+ *
+ * @param activity The activity whose tab IDs are to be collected from.
+ * @param tabIds Where the tab IDs should be added to.
+ */
+ private static void getAllTabIdsForActivity(CustomTabActivity activity, Set<Integer> tabIds) {
+ if (activity == null) return;
+ TabModelSelector selector = activity.getTabModelSelector();
+ if (selector == null) return;
+ List<TabModel> models = selector.getModels();
+ for (int i = 0; i < models.size(); i++) {
+ TabModel model = models.get(i);
+ for (int j = 0; j < model.getCount(); j++) {
+ tabIds.add(model.getTabAt(j).getId());
+ }
+ }
+ }
+
+ /**
+ * Gathers all of the tab IDs and task IDs for all currently live Custom Tabs.
+ *
+ * @param liveTabIds Where tab IDs will be added.
+ * @param liveTaskIds Where task IDs will be added.
+ */
+ protected static void getAllLiveTabAndTaskIds(
+ Set<Integer> liveTabIds, Set<Integer> liveTaskIds) {
+ ThreadUtils.assertOnUiThread();
+
+ List<WeakReference<Activity>> activities = ApplicationStatus.getRunningActivities();
+ for (int i = 0; i < activities.size(); i++) {
+ Activity activity = activities.get(i).get();
+ if (activity == null) continue;
+ if (!(activity instanceof CustomTabActivity)) continue;
+ getAllTabIdsForActivity((CustomTabActivity) activity, liveTabIds);
+ liveTaskIds.add(activity.getTaskId());
+ }
+ }
+
+ private class CleanUpTabStateDataTask extends AsyncTask<Void, Void, Void> {
+ private final Callback<List<String>> mFilesToDeleteCallback;
+
+ private Set<Integer> mUnreferencedTabIds;
+ private List<File> mDeletableMetadataFiles;
+ private Map<File, SparseBooleanArray> mTabIdsByMetadataFile;
+
+ CleanUpTabStateDataTask(Callback<List<String>> filesToDelete) {
+ mFilesToDeleteCallback = filesToDelete;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ if (mDestroyed) return null;
+
+ mTabIdsByMetadataFile = new HashMap<>();
+ mUnreferencedTabIds = new HashSet<>();
+
+ File[] stateFiles = getOrCreateStateDirectory().listFiles();
+ if (stateFiles == null) return null;
+
+ Set<Integer> allTabIds = new HashSet<>();
+ Set<Integer> allReferencedTabIds = new HashSet<>();
+ List<File> metadataFiles = new ArrayList<>();
+ for (File file : stateFiles) {
+ if (TabPersistentStore.isStateFile(file.getName())) {
+ metadataFiles.add(file);
+
+ SparseBooleanArray tabIds = new SparseBooleanArray();
+ mTabIdsByMetadataFile.put(file, tabIds);
+ getTabsFromStateFile(tabIds, file);
+ for (int i = 0; i < tabIds.size(); i++) {
+ allReferencedTabIds.add(tabIds.keyAt(i));
+ }
+ continue;
+ }
+
+ Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename(file.getName());
+ if (tabInfo == null) continue;
+ allTabIds.add(tabInfo.first);
+ }
+
+ mUnreferencedTabIds.addAll(allTabIds);
+ mUnreferencedTabIds.removeAll(allReferencedTabIds);
+
+ mDeletableMetadataFiles = getMetadataFilesForDeletion(
+ System.currentTimeMillis(), metadataFiles);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void unused) {
+ List<String> filesToDelete = new ArrayList<>();
+ if (mDestroyed) {
+ mFilesToDeleteCallback.onResult(filesToDelete);
+ return;
+ }
+
+ if (mUnreferencedTabIds.isEmpty() && mDeletableMetadataFiles.isEmpty()) {
+ mFilesToDeleteCallback.onResult(filesToDelete);
+ return;
+ }
+
+ Set<Integer> liveTabIds = new HashSet<>();
+ Set<Integer> liveTaskIds = new HashSet<>();
+ getAllLiveTabAndTaskIds(liveTabIds, liveTaskIds);
+
+ for (Integer unreferencedTabId : mUnreferencedTabIds) {
+ // Ignore tabs that are referenced by live activities as they might not have been
+ // able to write out their state yet.
+ if (liveTabIds.contains(unreferencedTabId)) continue;
+
+ // The tab state is not referenced by any current activities or any metadata files,
+ // so mark it for deletion.
+ filesToDelete.add(TabState.getTabStateFilename(unreferencedTabId, false));
+ }
+
+ for (int i = 0; i < mDeletableMetadataFiles.size(); i++) {
+ File metadataFile = mDeletableMetadataFiles.get(i);
+ String id = TabPersistentStore.getStateFileUniqueId(metadataFile.getName());
+ try {
+ int taskId = Integer.parseInt(id);
+
+ // Ignore the metadata file if it belongs to a currently live CustomTabActivity.
+ if (liveTaskIds.contains(taskId)) continue;
+
+ filesToDelete.add(metadataFile.getName());
+
+ SparseBooleanArray unusedTabIds = mTabIdsByMetadataFile.get(metadataFile);
+ if (unusedTabIds == null) continue;
+ for (int j = 0; j < unusedTabIds.size(); j++) {
+ filesToDelete.add(TabState.getTabStateFilename(
+ unusedTabIds.keyAt(j), false));
+ }
+ } catch (NumberFormatException ex) {
+ assert false : "Unexpected tab metadata file found: " + metadataFile.getName();
+ continue;
+ }
+ }
+
+ mFilesToDeleteCallback.onResult(filesToDelete);
+ }
+
+ private void getTabsFromStateFile(SparseBooleanArray tabIds, File metadataFile) {
+ 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);
+ }
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698