Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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 | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.chrome.browser.customtabs; | |
| 6 | |
| 7 import android.app.Activity; | |
| 8 import android.os.AsyncTask; | |
| 9 import android.os.StrictMode; | |
| 10 import android.util.Pair; | |
| 11 import android.util.SparseBooleanArray; | |
| 12 | |
| 13 import org.chromium.base.ApplicationStatus; | |
| 14 import org.chromium.base.Callback; | |
| 15 import org.chromium.base.Log; | |
| 16 import org.chromium.base.StreamUtil; | |
| 17 import org.chromium.base.ThreadUtils; | |
| 18 import org.chromium.base.VisibleForTesting; | |
| 19 import org.chromium.chrome.browser.TabState; | |
| 20 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; | |
| 21 import org.chromium.chrome.browser.tabmodel.TabModel; | |
| 22 import org.chromium.chrome.browser.tabmodel.TabModelSelector; | |
| 23 import org.chromium.chrome.browser.tabmodel.TabPersistencePolicy; | |
| 24 import org.chromium.chrome.browser.tabmodel.TabPersistentStore; | |
| 25 | |
| 26 import java.io.BufferedInputStream; | |
| 27 import java.io.DataInputStream; | |
| 28 import java.io.File; | |
| 29 import java.io.FileInputStream; | |
| 30 import java.lang.ref.WeakReference; | |
| 31 import java.util.ArrayList; | |
| 32 import java.util.Collections; | |
| 33 import java.util.Comparator; | |
| 34 import java.util.HashMap; | |
| 35 import java.util.HashSet; | |
| 36 import java.util.List; | |
| 37 import java.util.Map; | |
| 38 import java.util.Set; | |
| 39 import java.util.concurrent.ExecutionException; | |
| 40 import java.util.concurrent.Executor; | |
| 41 | |
| 42 import javax.annotation.Nullable; | |
| 43 | |
| 44 /** | |
| 45 * Handles the Custom Tab specific behaviors of tab persistence. | |
| 46 */ | |
| 47 public class CustomTabTabPersistencePolicy implements TabPersistencePolicy { | |
| 48 | |
| 49 static final String SAVED_STATE_DIRECTORY = "custom_tabs"; | |
| 50 | |
| 51 /** Threshold where old state files should be deleted (30 days). */ | |
| 52 @VisibleForTesting | |
|
gone
2016/08/31 17:59:16
You only need this annotation to prevent ProGuard
Ted C
2016/08/31 18:37:53
Done.
| |
| 53 protected static final long STATE_EXPIRY_THRESHOLD = 30L * 24 * 60 * 60 * 10 00; | |
| 54 /** Maximum number of state files before we should start deleting old ones. */ | |
| 55 @VisibleForTesting | |
| 56 protected static final int MAXIMUM_STATE_FILES = 30; | |
| 57 | |
| 58 private static final String TAG = "tabmodel"; | |
| 59 | |
| 60 /** Prevents two state directories from getting created simultaneously. */ | |
| 61 private static final Object DIR_CREATION_LOCK = new Object(); | |
| 62 /** | |
| 63 * Prevents two clean up tasks from getting created simultaneously. Also pro tects against | |
| 64 * incorrectly interleaving create/run/cancel on the task. | |
| 65 */ | |
| 66 private static final Object CLEAN_UP_TASK_LOCK = new Object(); | |
| 67 | |
| 68 private static File sStateDirectory; | |
| 69 private static AsyncTask<Void, Void, Void> sCleanupTask; | |
| 70 | |
| 71 /** | |
| 72 * The folder where the state should be saved to. | |
| 73 * @return A file representing the directory that contains TabModelSelector states. | |
| 74 */ | |
| 75 public static File getOrCreateCustomTabModeStateDirectory() { | |
| 76 synchronized (DIR_CREATION_LOCK) { | |
| 77 if (sStateDirectory == null) { | |
| 78 sStateDirectory = new File( | |
| 79 TabPersistentStore.getOrCreateBaseStateDirectory(), SAVE D_STATE_DIRECTORY); | |
| 80 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskRe ads(); | |
| 81 StrictMode.allowThreadDiskWrites(); | |
| 82 try { | |
| 83 if (!sStateDirectory.exists() && !sStateDirectory.mkdirs()) { | |
| 84 Log.e(TAG, "Failed to create state folder: " + sStateDir ectory); | |
| 85 } | |
| 86 } finally { | |
| 87 StrictMode.setThreadPolicy(oldPolicy); | |
| 88 } | |
| 89 } | |
| 90 } | |
| 91 return sStateDirectory; | |
| 92 } | |
| 93 | |
| 94 private final int mTaskId; | |
| 95 private final boolean mShouldRestore; | |
| 96 | |
| 97 private AsyncTask<Void, Void, Void> mInitializationTask; | |
| 98 private boolean mDestroyed; | |
| 99 | |
| 100 /** | |
| 101 * Constructs a persistence policy for a given Custom Tab. | |
| 102 * | |
| 103 * @param taskId The task ID that the owning Custom Tab is in. | |
| 104 * @param shouldRestore Whether an attempt to restore tab state information should be done on | |
| 105 * startup. | |
| 106 */ | |
| 107 public CustomTabTabPersistencePolicy(int taskId, boolean shouldRestore) { | |
| 108 mTaskId = taskId; | |
| 109 mShouldRestore = shouldRestore; | |
| 110 } | |
| 111 | |
| 112 @Override | |
| 113 public File getOrCreateStateDirectory() { | |
| 114 return getOrCreateCustomTabModeStateDirectory(); | |
| 115 } | |
| 116 | |
| 117 @Override | |
| 118 public String getStateFileName() { | |
| 119 return TabPersistentStore.getStateFileName(Integer.toString(mTaskId)); | |
| 120 } | |
| 121 | |
| 122 @Override | |
| 123 @Nullable | |
| 124 public String getStateToBeMergedFileName() { | |
| 125 return null; | |
| 126 } | |
| 127 | |
| 128 @Override | |
| 129 public boolean performInitialization(Executor executor) { | |
| 130 mInitializationTask = new AsyncTask<Void, Void, Void>() { | |
| 131 @Override | |
| 132 protected Void doInBackground(Void... params) { | |
| 133 File stateDir = getOrCreateStateDirectory(); | |
| 134 File metadataFile = new File(stateDir, getStateFileName()); | |
| 135 if (metadataFile.exists()) { | |
| 136 if (!mShouldRestore) { | |
| 137 if (!metadataFile.delete()) { | |
| 138 Log.e(TAG, "Failed to delete file: " + metadataFile) ; | |
| 139 } | |
| 140 } else { | |
| 141 if (!metadataFile.setLastModified(System.currentTimeMill is())) { | |
| 142 Log.e(TAG, "Unable to update last modified time: " + metadataFile); | |
| 143 } | |
| 144 } | |
| 145 } | |
| 146 return null; | |
| 147 } | |
| 148 }.executeOnExecutor(executor); | |
| 149 | |
| 150 return true; | |
| 151 } | |
| 152 | |
| 153 @Override | |
| 154 public void waitForInitializationToFinish() { | |
| 155 if (mInitializationTask == null) return; | |
| 156 try { | |
| 157 mInitializationTask.get(); | |
| 158 } catch (InterruptedException | ExecutionException e) { | |
| 159 // Ignore and proceed. | |
| 160 } | |
| 161 } | |
| 162 | |
| 163 @Override | |
| 164 public boolean isMergeInProgress() { | |
| 165 return false; | |
| 166 } | |
| 167 | |
| 168 @Override | |
| 169 public void setMergeInProgress(boolean isStarted) { | |
| 170 assert false : "Merge not supported in Custom Tabs"; | |
| 171 } | |
| 172 | |
| 173 @Override | |
| 174 public void cancelCleanupInProgress() { | |
| 175 synchronized (CLEAN_UP_TASK_LOCK) { | |
| 176 if (sCleanupTask != null) sCleanupTask.cancel(true); | |
| 177 } | |
| 178 } | |
| 179 | |
| 180 @Override | |
| 181 public void cleanupUnusedFiles(Callback<List<String>> filesToDelete) { | |
| 182 synchronized (CLEAN_UP_TASK_LOCK) { | |
| 183 if (sCleanupTask != null) sCleanupTask.cancel(true); | |
| 184 sCleanupTask = new CleanUpTabStateDataTask(filesToDelete); | |
| 185 sCleanupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | |
| 186 } | |
| 187 } | |
| 188 | |
| 189 @Override | |
| 190 public void setTabContentManager(TabContentManager cache) { | |
| 191 } | |
| 192 | |
| 193 @Override | |
| 194 public void destroy() { | |
| 195 mDestroyed = true; | |
| 196 } | |
| 197 | |
| 198 /** | |
| 199 * Given a list of metadata files, determine which are applicable for deleti on based on the | |
| 200 * deletion strategy of Custom Tabs. | |
| 201 * | |
| 202 * @param currentTimeMillis The current time in milliseconds | |
| 203 * ({@link System#currentTimeMillis()}. | |
| 204 * @param allMetadataFiles The complete list of all metadata files to check. | |
| 205 * @return The list of metadata files that are applicable for deletion. | |
| 206 */ | |
| 207 protected static List<File> getMetadataFilesForDeletion( | |
| 208 long currentTimeMillis, List<File> allMetadataFiles) { | |
| 209 Collections.sort(allMetadataFiles, new Comparator<File>() { | |
| 210 @Override | |
| 211 public int compare(File lhs, File rhs) { | |
| 212 long lhsModifiedTime = lhs.lastModified(); | |
| 213 long rhsModifiedTime = rhs.lastModified(); | |
| 214 | |
| 215 // Sort such that older files (those with an lower timestamp num ber) are at the | |
| 216 // end of the sorted listed. | |
| 217 // | |
| 218 // Copied from Long#compare, which is only available in API 19+ | |
|
gone
2016/08/31 17:59:16
This keeps coming up... can we just add a function
Ted C
2016/08/31 18:37:53
Done.
| |
| 219 return rhsModifiedTime < lhsModifiedTime | |
| 220 ? -1 : (lhsModifiedTime == rhsModifiedTime ? 0 : 1); | |
| 221 } | |
| 222 }); | |
| 223 | |
| 224 List<File> stateFilesApplicableForDeletion = new ArrayList<File>(); | |
| 225 for (int i = 0; i < allMetadataFiles.size(); i++) { | |
| 226 File file = allMetadataFiles.get(i); | |
| 227 long fileAge = currentTimeMillis - file.lastModified(); | |
| 228 if (i >= MAXIMUM_STATE_FILES || fileAge >= STATE_EXPIRY_THRESHOLD) { | |
| 229 stateFilesApplicableForDeletion.add(file); | |
| 230 } | |
| 231 } | |
| 232 return stateFilesApplicableForDeletion; | |
| 233 } | |
| 234 | |
| 235 /** | |
| 236 * Get all current Tab IDs used by the specified activity. | |
| 237 * | |
| 238 * @param activity The activity whose tab IDs are to be collected from. | |
| 239 * @param tabIds Where the tab IDs should be added to. | |
| 240 */ | |
| 241 private static void getAllTabIdsForActivity(CustomTabActivity activity, Set< Integer> tabIds) { | |
| 242 if (activity == null) return; | |
| 243 TabModelSelector selector = activity.getTabModelSelector(); | |
| 244 if (selector == null) return; | |
| 245 List<TabModel> models = selector.getModels(); | |
| 246 for (int i = 0; i < models.size(); i++) { | |
| 247 TabModel model = models.get(i); | |
| 248 for (int j = 0; j < model.getCount(); j++) { | |
| 249 tabIds.add(model.getTabAt(j).getId()); | |
| 250 } | |
| 251 } | |
| 252 } | |
| 253 | |
| 254 /** | |
| 255 * Gathers all of the tab IDs and task IDs for all currently live Custom Tab s. | |
| 256 * | |
| 257 * @param liveTabIds Where tab IDs will be added. | |
| 258 * @param liveTaskIds Where task IDs will be added. | |
| 259 */ | |
| 260 protected static void getAllLiveTabAndTaskIds( | |
| 261 Set<Integer> liveTabIds, Set<Integer> liveTaskIds) { | |
| 262 ThreadUtils.assertOnUiThread(); | |
| 263 | |
| 264 List<WeakReference<Activity>> activities = ApplicationStatus.getRunningA ctivities(); | |
| 265 for (int i = 0; i < activities.size(); i++) { | |
| 266 Activity activity = activities.get(i).get(); | |
| 267 if (activity == null) continue; | |
| 268 if (!(activity instanceof CustomTabActivity)) continue; | |
| 269 getAllTabIdsForActivity((CustomTabActivity) activity, liveTabIds); | |
| 270 liveTaskIds.add(activity.getTaskId()); | |
| 271 } | |
| 272 } | |
| 273 | |
| 274 private class CleanUpTabStateDataTask extends AsyncTask<Void, Void, Void> { | |
| 275 private final Callback<List<String>> mFilesToDeleteCallback; | |
| 276 | |
| 277 private Set<Integer> mUnreferencedTabIds; | |
| 278 private List<File> mDeletableMetadataFiles; | |
| 279 private Map<File, SparseBooleanArray> mTabIdsByMetadataFile; | |
| 280 | |
| 281 CleanUpTabStateDataTask(Callback<List<String>> filesToDelete) { | |
| 282 mFilesToDeleteCallback = filesToDelete; | |
| 283 } | |
| 284 | |
| 285 @Override | |
| 286 protected Void doInBackground(Void... voids) { | |
| 287 if (mDestroyed) return null; | |
| 288 | |
| 289 mTabIdsByMetadataFile = new HashMap<>(); | |
| 290 mUnreferencedTabIds = new HashSet<>(); | |
| 291 | |
| 292 File[] stateFiles = getOrCreateStateDirectory().listFiles(); | |
| 293 if (stateFiles == null) return null; | |
| 294 | |
| 295 Set<Integer> allTabIds = new HashSet<>(); | |
| 296 Set<Integer> allReferencedTabIds = new HashSet<>(); | |
| 297 List<File> metadataFiles = new ArrayList<>(); | |
| 298 for (File file : stateFiles) { | |
| 299 if (TabPersistentStore.isStateFile(file.getName())) { | |
| 300 metadataFiles.add(file); | |
| 301 | |
| 302 SparseBooleanArray tabIds = new SparseBooleanArray(); | |
| 303 mTabIdsByMetadataFile.put(file, tabIds); | |
| 304 getTabsFromStateFile(tabIds, file); | |
| 305 for (int i = 0; i < tabIds.size(); i++) { | |
| 306 allReferencedTabIds.add(tabIds.keyAt(i)); | |
| 307 } | |
| 308 continue; | |
| 309 } | |
| 310 | |
| 311 Pair<Integer, Boolean> tabInfo = TabState.parseInfoFromFilename( file.getName()); | |
| 312 if (tabInfo == null) continue; | |
| 313 allTabIds.add(tabInfo.first); | |
| 314 } | |
| 315 | |
| 316 mUnreferencedTabIds.addAll(allTabIds); | |
| 317 mUnreferencedTabIds.removeAll(allReferencedTabIds); | |
| 318 | |
| 319 mDeletableMetadataFiles = getMetadataFilesForDeletion( | |
| 320 System.currentTimeMillis(), metadataFiles); | |
| 321 return null; | |
| 322 } | |
| 323 | |
| 324 @Override | |
| 325 protected void onPostExecute(Void unused) { | |
| 326 List<String> filesToDelete = new ArrayList<>(); | |
| 327 if (mDestroyed) { | |
| 328 mFilesToDeleteCallback.onResult(filesToDelete); | |
| 329 return; | |
| 330 } | |
| 331 | |
| 332 if (mUnreferencedTabIds.isEmpty() && mDeletableMetadataFiles.isEmpty ()) { | |
| 333 mFilesToDeleteCallback.onResult(filesToDelete); | |
| 334 return; | |
| 335 } | |
| 336 | |
| 337 Set<Integer> liveTabIds = new HashSet<>(); | |
| 338 Set<Integer> liveTaskIds = new HashSet<>(); | |
| 339 getAllLiveTabAndTaskIds(liveTabIds, liveTaskIds); | |
| 340 | |
| 341 for (Integer unreferencedTabId : mUnreferencedTabIds) { | |
| 342 // Ignore tabs that are referenced by live activities as they mi ght not have been | |
| 343 // able to write out their state yet. | |
| 344 if (liveTabIds.contains(unreferencedTabId)) continue; | |
| 345 | |
| 346 // The tab state is not referenced by any current activities or any metadata files, | |
| 347 // so mark it for deletion. | |
| 348 filesToDelete.add(TabState.getTabStateFilename(unreferencedTabId , false)); | |
| 349 } | |
| 350 | |
| 351 for (int i = 0; i < mDeletableMetadataFiles.size(); i++) { | |
| 352 File metadataFile = mDeletableMetadataFiles.get(i); | |
| 353 String id = TabPersistentStore.getStateFileUniqueId(metadataFile .getName()); | |
| 354 try { | |
| 355 int taskId = Integer.parseInt(id); | |
| 356 | |
| 357 // Ignore the metadata file if it belongs to a currently liv e CustomTabActivity. | |
| 358 if (liveTaskIds.contains(taskId)) continue; | |
| 359 | |
| 360 filesToDelete.add(metadataFile.getName()); | |
| 361 | |
| 362 SparseBooleanArray unusedTabIds = mTabIdsByMetadataFile.get( metadataFile); | |
| 363 if (unusedTabIds == null) continue; | |
| 364 for (int j = 0; j < unusedTabIds.size(); j++) { | |
| 365 filesToDelete.add(TabState.getTabStateFilename( | |
| 366 unusedTabIds.keyAt(j), false)); | |
| 367 } | |
| 368 } catch (NumberFormatException ex) { | |
| 369 assert false : "Unexpected tab metadata file found: " + meta dataFile.getName(); | |
| 370 continue; | |
| 371 } | |
| 372 } | |
| 373 | |
| 374 mFilesToDeleteCallback.onResult(filesToDelete); | |
| 375 } | |
| 376 | |
| 377 private void getTabsFromStateFile(SparseBooleanArray tabIds, File metada taFile) { | |
| 378 DataInputStream stream = null; | |
| 379 try { | |
| 380 stream = new DataInputStream( | |
| 381 new BufferedInputStream(new FileInputStream(metadataFile ))); | |
| 382 TabPersistentStore.readSavedStateFile(stream, null, tabIds, fals e); | |
| 383 } catch (Exception e) { | |
| 384 Log.e(TAG, "Unable to read state for " + metadataFile.getName() + ": " + e); | |
| 385 } finally { | |
| 386 StreamUtil.closeQuietly(stream); | |
| 387 } | |
| 388 } | |
| 389 } | |
| 390 } | |
| OLD | NEW |