// 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.webapps;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.ActivityManager.AppTask;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.StrictMode;
import android.os.SystemClock;
import android.text.TextUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.document.DocumentUtils;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Manages directories created to store data for web apps.
*
* Directories managed by this class are all subdirectories of the app_WebappActivity/ directory,
* which each WebappActivity using a directory named either for its Webapp's ID in Document mode,
* or the index of the WebappActivity if it is a subclass of the WebappManagedActivity class (which
* are used in pre-L devices to allow multiple WebappActivities launching).
*/
public class WebappDirectoryManager {
protected static final String WEBAPP_DIRECTORY_NAME = "WebappActivity";
private static final String TAG = "WebappDirectoryManag";
/** Whether or not the class has already started trying to clean up obsolete directories. */
private static final AtomicBoolean sMustCleanUpOldDirectories = new AtomicBoolean(true);
/** AsyncTask that is used to clean up the web app directories. */
private AsyncTask<Void, Void, Void> mCleanupTask;
/**
* Deletes web app directories with stale data.
*
* This should be called by a {@link WebappActivity} after it has restored all the data it
* needs from its directory because the directory will be deleted during the process.
*
* @param context Context to pull info and Files from.
* @param currentWebappId ID for the currently running web app.
* @return AsyncTask doing the cleaning.
*/
public AsyncTask<Void, Void, Void> cleanUpDirectories(
final Context context, final String currentWebappId) {
if (mCleanupTask != null) return mCleanupTask;
mCleanupTask = new AsyncTask<Void, Void, Void>() {
@Override
protected final Void doInBackground(Void... params) {
Set<File> directoriesToDelete = new HashSet<File>();
directoriesToDelete.add(getWebappDirectory(context, currentWebappId));
boolean shouldDeleteOldDirectories =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
if (shouldDeleteOldDirectories && sMustCleanUpOldDirectories.getAndSet(false)) {
findStaleWebappDirectories(context, directoriesToDelete);
}
for (File directory : directoriesToDelete) {
if (isCancelled()) return null;
FileUtils.recursivelyDeleteFile(directory);
}
return null;
}
};
mCleanupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return mCleanupTask;
}
/** Cancels the cleanup task, if one exists. */
public void cancelCleanup() {
if (mCleanupTask != null) mCleanupTask.cancel(true);
}
/**
* Finds all directories for web apps containing stale data.
*
* This includes all directories using the old pre-L directory structure, which used directories
* named * app_WebappActivity*, as well as directories corresponding to WebappActivities that
* are no longer listed in Android's recents, since these will be unable to restore their data.
*
* @param directoriesToDelete Set to append directory names to.
*/
private void findStaleWebappDirectories(Context context, Set<File> directoriesToDelete) {
File webappBaseDirectory = getBaseWebappDirectory(context);
// Figure out what WebappActivities are still listed in Android's recents menu.
Set<String> liveWebapps = new HashSet<String>();
Set<Intent> baseIntents = getBaseIntentsForAllTasks();
for (Intent intent : baseIntents) {
Uri data = intent.getData();
if (data != null && TextUtils.equals(WebappActivity.WEBAPP_SCHEME, data.getScheme())) {
liveWebapps.add(data.getHost());
}
// WebappManagedActivities have titles from "WebappActivity0" through "WebappActivity9".
ComponentName component = intent.getComponent();
if (component != null) {
String fullClassName = component.getClassName();
int lastPeriodIndex = fullClassName.lastIndexOf(".");
if (lastPeriodIndex != -1) {
String className = fullClassName.substring(lastPeriodIndex + 1);
if (className.startsWith(WEBAPP_DIRECTORY_NAME)
&& className.length() > WEBAPP_DIRECTORY_NAME.length()) {
String activityIndex = className.substring(WEBAPP_DIRECTORY_NAME.length());
liveWebapps.add(activityIndex);
}
}
}
}
if (webappBaseDirectory != null) {
// Delete all web app directories in the main directory, which were for pre-L web apps.
File appDirectory = new File(context.getApplicationInfo().dataDir);
String webappDirectoryAppBaseName = webappBaseDirectory.getName();
File[] files = appDirectory.listFiles();
if (files != null) {
for (File file : files) {
String filename = file.getName();
if (!filename.startsWith(webappDirectoryAppBaseName)) continue;
if (filename.length() == webappDirectoryAppBaseName.length()) continue;
directoriesToDelete.add(file);
}
}
// Clean out web app directories no longer corresponding to tasks in Recents.
if (webappBaseDirectory.exists()) {
files = webappBaseDirectory.listFiles();
if (files != null) {
for (File file : files) {
if (!liveWebapps.contains(file.getName())) directoriesToDelete.add(file);
}
}
}
}
}
/**
* Returns the directory for a web app, creating it if necessary.
* @param webappId ID for the web app. Used as a subdirectory name.
* @return File for storing information about the web app.
*/
File getWebappDirectory(Context context, String webappId) {
// Temporarily allowing disk access while fixing. TODO: http://crbug.com/525781
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
StrictMode.allowThreadDiskWrites();
try {
long time = SystemClock.elapsedRealtime();
File webappDirectory = new File(getBaseWebappDirectory(context), webappId);
if (!webappDirectory.exists() && !webappDirectory.mkdir()) {
Log.e(TAG, "Failed to create web app directory.");
}
RecordHistogram.recordTimesHistogram("Android.StrictMode.WebappDir",
SystemClock.elapsedRealtime() - time, TimeUnit.MILLISECONDS);
return webappDirectory;
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/** Returns the directory containing all of Chrome's web app data, creating it if needed. */
final File getBaseWebappDirectory(Context context) {
return context.getDir(WEBAPP_DIRECTORY_NAME, Context.MODE_PRIVATE);
}
/** Returns a Set of Intents for all Chrome tasks currently known by the ActivityManager. */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected Set<Intent> getBaseIntentsForAllTasks() {
Set<Intent> baseIntents = new HashSet<Intent>();
Context context = ContextUtils.getApplicationContext();
ActivityManager manager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (AppTask task : manager.getAppTasks()) {
Intent intent = DocumentUtils.getBaseIntentFromTask(task);
if (intent != null) baseIntents.add(intent);
}
return baseIntents;
}
}