Index: chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java |
index 1b15b9a0867eaf2d06cfcc044e444a2686a7005a..629aef6de6f64b70fc45a7eef4bedf8f0074ba74 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java |
@@ -18,24 +18,18 @@ |
import org.chromium.chrome.browser.browsing_data.UrlFilterBridge; |
import java.util.Collections; |
-import java.util.HashMap; |
-import java.util.Iterator; |
+import java.util.HashSet; |
import java.util.Set; |
import java.util.concurrent.TimeUnit; |
/** |
- * Singleton class which tracks web apps backed by a SharedPreferences file (abstracted by the |
- * WebappDataStorage class). This class is not thread-safe and must be used on the UI thread. |
+ * Keeps track of web apps which have created a SharedPreference file (through the used of the |
+ * WebappDataStorage class) which may need to be cleaned up in the future. |
* |
- * Aside from web app registration, which is asynchronous as a new SharedPreferences file must be |
- * opened, all methods in this class are synchronous. All web app SharedPreferences known to |
- * WebappRegistry are pre-warmed on browser startup when creating the singleton WebappRegistry |
- * instance, whilst registering a new web app will automatically cache the new SharedPreferences |
- * after it is created. |
- * |
- * This class is not a comprehensive list of installed web apps because it is impossible to know |
- * when the user removes a web app from the home screen. The WebappDataStorage.wasLaunchedRecently() |
- * heuristic attempts to compensate for this. |
+ * It is NOT intended to be 100% accurate nor a comprehensive list of all installed web apps |
+ * because it is impossible to track when the user removes a web app from the Home screen and it |
+ * is similarily impossible to track pre-registry era web apps (this case is not a problem anyway |
+ * as these web apps have no external data to cleanup). |
*/ |
public class WebappRegistry { |
@@ -49,49 +43,19 @@ |
/** Represents a period of 13 weeks in milliseconds */ |
static final long WEBAPP_UNOPENED_CLEANUP_DURATION = TimeUnit.DAYS.toMillis(13L * 7L); |
- private static volatile WebappRegistry sInstance; |
- |
- private HashMap<String, WebappDataStorage> mStorages; |
- private SharedPreferences mPreferences; |
- |
- /** |
- * Callback run when a WebappDataStorage object is registered for the first time. The storage |
- * parameter will never be null. |
+ /** |
+ * Called when a retrieval of the set of stored web app IDs occurs. |
+ */ |
+ public interface FetchCallback { |
+ void onWebappIdsRetrieved(Set<String> readObject); |
+ } |
+ |
+ /** |
+ * Called when a retrieval of the stored WebappDataStorage occurs. The storage parameter will |
+ * be null if the web app queried for was not in the registry. |
*/ |
public interface FetchWebappDataStorageCallback { |
void onWebappDataStorageRetrieved(WebappDataStorage storage); |
- } |
- |
- /** |
- * Returns the singleton WebappRegistry instance. Creates the instance if necessary. |
- */ |
- public static WebappRegistry getInstance() { |
- assert ThreadUtils.runningOnUiThread(); |
- if (sInstance == null) sInstance = new WebappRegistry(); |
- return sInstance; |
- } |
- |
- /** |
- * Warm up the WebappRegistry and a specific WebappDataStorage SharedPreferences. This static |
- * method can be called on any thread, so it must not initialize sInstance. |
- * @param id The web app id to warm up in addition to the WebappRegistry. |
- */ |
- public static void warmUpSharedPrefsForId(String id) { |
- sInstance.initStorages(id, false); |
- } |
- |
- /** |
- * Warm up the WebappRegistry and all WebappDataStorage SharedPreferences. This static method |
- * can be called on any thread, so it must not initialize sInstance. |
- */ |
- public static void warmUpSharedPrefs() { |
- sInstance.initStorages(null, false); |
- } |
- |
- public static void refreshSharedPrefsForTesting() { |
- assert ThreadUtils.runningOnUiThread(); |
- sInstance = new WebappRegistry(); |
- sInstance.initStorages(null, true); |
} |
/** |
@@ -101,200 +65,275 @@ |
* @param callback The callback to run with the WebappDataStorage argument. |
* @return The storage object for the web app. |
*/ |
- public void register(final String webappId, final FetchWebappDataStorageCallback callback) { |
- assert ThreadUtils.runningOnUiThread(); |
- |
+ public static void registerWebapp(final String webappId, |
+ final FetchWebappDataStorageCallback callback) { |
new AsyncTask<Void, Void, WebappDataStorage>() { |
@Override |
protected final WebappDataStorage doInBackground(Void... nothing) { |
- // Create the WebappDataStorage on the background thread, as this must create and |
- // open a new SharedPreferences. |
- return WebappDataStorage.open(webappId); |
+ SharedPreferences preferences = openSharedPreferences(); |
+ // The set returned by getRegisteredWebappIds must be treated as immutable, so we |
+ // make a copy to edit and save. |
+ Set<String> webapps = new HashSet<>(getRegisteredWebappIds(preferences)); |
+ boolean added = webapps.add(webappId); |
+ assert added; |
+ |
+ preferences.edit().putStringSet(KEY_WEBAPP_SET, webapps).apply(); |
+ |
+ // Create the WebappDataStorage and update the last used time, so we can guarantee |
+ // that a web app which appears in the registry will have a |
+ // last used time != WebappDataStorage.LAST_USED_INVALID. |
+ WebappDataStorage storage = new WebappDataStorage(webappId); |
+ storage.updateLastUsedTime(); |
+ return storage; |
} |
@Override |
protected final void onPostExecute(WebappDataStorage storage) { |
- // Guarantee that last used time != WebappDataStorage.LAST_USED_INVALID. Must be |
- // run on the main thread as SharedPreferences.Editor.apply() is called. |
- mStorages.put(webappId, storage); |
- mPreferences.edit().putStringSet(KEY_WEBAPP_SET, mStorages.keySet()).apply(); |
- storage.updateLastUsedTime(); |
if (callback != null) callback.onWebappDataStorageRetrieved(storage); |
} |
}.execute(); |
} |
/** |
- * Returns the WebappDataStorage object for webappId, or null if one cannot be found. |
- * @param webappId The id of the web app. |
+ * Runs the callback, supplying the WebappDataStorage object for webappId, or null if the web |
+ * app has not been registered. |
+ * @param webappId The id of the web app to register. |
* @return The storage object for the web app, or null if webappId is not registered. |
*/ |
- public WebappDataStorage getWebappDataStorage(String webappId) { |
- return mStorages.get(webappId); |
- } |
- |
- /** |
- * Returns the WebappDataStorage object whose scope most closely matches the provided URL, or |
- * null if a matching web app cannot be found. The most closely matching scope is the longest |
- * scope which has the same prefix as the URL to open. |
- * @param url The URL to search for. |
- * @return The storage object for the web app, or null if one cannot be found. |
- */ |
- public WebappDataStorage getWebappDataStorageForUrl(final String url) { |
- WebappDataStorage bestMatch = null; |
- int largestOverlap = 0; |
- for (HashMap.Entry<String, WebappDataStorage> entry : mStorages.entrySet()) { |
- WebappDataStorage storage = entry.getValue(); |
- String scope = storage.getScope(); |
- if (url.startsWith(scope) && scope.length() > largestOverlap) { |
- bestMatch = storage; |
- largestOverlap = scope.length(); |
- } |
- } |
- return bestMatch; |
- } |
- |
- /** |
- * Returns the list of web app IDs which are written to SharedPreferences. |
+ public static void getWebappDataStorage(final String webappId, |
+ final FetchWebappDataStorageCallback callback) { |
+ new AsyncTask<Void, Void, WebappDataStorage>() { |
+ @Override |
+ protected final WebappDataStorage doInBackground(Void... nothing) { |
+ SharedPreferences preferences = openSharedPreferences(); |
+ if (getRegisteredWebappIds(preferences).contains(webappId)) { |
+ WebappDataStorage storage = WebappDataStorage.open(webappId); |
+ return storage; |
+ } |
+ return null; |
+ } |
+ |
+ @Override |
+ protected final void onPostExecute(WebappDataStorage storage) { |
+ assert callback != null; |
+ callback.onWebappDataStorageRetrieved(storage); |
+ } |
+ }.execute(); |
+ } |
+ |
+ /** |
+ * Runs the callback, supplying the WebappDataStorage object whose scope most closely matches |
+ * the provided URL, or null if a matching web app cannot be found. The most closely matching |
+ * scope is the longest scope which has the same prefix as the URL to open. |
+ * @param url The URL to search for. |
+ * @return The storage object for the web app, or null if webappId is not registered. |
+ */ |
+ public static void getWebappDataStorageForUrl(final String url, |
+ final FetchWebappDataStorageCallback callback) { |
+ new AsyncTask<Void, Void, WebappDataStorage>() { |
+ @Override |
+ protected final WebappDataStorage doInBackground(Void... nothing) { |
+ SharedPreferences preferences = openSharedPreferences(); |
+ WebappDataStorage bestMatch = null; |
+ int largestOverlap = 0; |
+ for (String id : getRegisteredWebappIds(preferences)) { |
+ WebappDataStorage storage = WebappDataStorage.open(id); |
+ String scope = storage.getScope(); |
+ if (url.startsWith(scope) && scope.length() > largestOverlap) { |
+ bestMatch = storage; |
+ largestOverlap = scope.length(); |
+ } |
+ } |
+ return bestMatch; |
+ } |
+ |
+ protected final void onPostExecute(WebappDataStorage storage) { |
+ assert callback != null; |
+ callback.onWebappDataStorageRetrieved(storage); |
+ } |
+ }.execute(); |
+ } |
+ |
+ /** |
+ * Asynchronously retrieves the list of web app IDs which this registry is aware of. |
+ * @param callback Called when the set has been retrieved. The set may be empty. |
*/ |
@VisibleForTesting |
- public static Set<String> getRegisteredWebappIdsForTesting() { |
- // Wrap with unmodifiableSet to ensure it's never modified. See crbug.com/568369. |
- return Collections.unmodifiableSet(openSharedPreferences().getStringSet( |
- KEY_WEBAPP_SET, Collections.<String>emptySet())); |
- } |
- |
- /** |
- * Deletes the data for all "old" web apps, as well as all WebAPKs that have been uninstalled in |
- * the last month. "Old" web apps have not been opened by the user in the last 3 months, or have |
- * had their last used time set to 0 by the user clearing their history. Cleanup is run, at |
- * most, once a month. |
+ public static void getRegisteredWebappIds(final FetchCallback callback) { |
+ new AsyncTask<Void, Void, Set<String>>() { |
+ @Override |
+ protected final Set<String> doInBackground(Void... nothing) { |
+ return getRegisteredWebappIds(openSharedPreferences()); |
+ } |
+ |
+ @Override |
+ protected final void onPostExecute(Set<String> result) { |
+ assert callback != null; |
+ callback.onWebappIdsRetrieved(result); |
+ } |
+ }.execute(); |
+ } |
+ |
+ /** |
+ * 1. Deletes the data for all "old" web apps. |
+ * "Old" web apps have not been opened by the user in the last 3 months, or have had their last |
+ * used time set to 0 by the user clearing their history. Cleanup is run, at most, once a month. |
+ * 2. Deletes the data for all WebAPKs that have been uninstalled in the last month. |
+ * |
* @param currentTime The current time which will be checked to decide if the task should be run |
* and if a web app should be cleaned up. |
*/ |
- public void unregisterOldWebapps(long currentTime) { |
- assert ThreadUtils.runningOnUiThread(); |
- if ((currentTime - mPreferences.getLong(KEY_LAST_CLEANUP, 0)) < FULL_CLEANUP_DURATION) { |
- return; |
- } |
- |
- Iterator<HashMap.Entry<String, WebappDataStorage>> it = mStorages.entrySet().iterator(); |
- while (it.hasNext()) { |
- HashMap.Entry<String, WebappDataStorage> entry = it.next(); |
- WebappDataStorage storage = entry.getValue(); |
- String webApkPackage = storage.getWebApkPackageName(); |
- if (webApkPackage != null) { |
- if (isWebApkInstalled(webApkPackage)) { |
- continue; |
- } |
- } else if ((currentTime - storage.getLastUsedTime()) |
- < WEBAPP_UNOPENED_CLEANUP_DURATION) { |
- continue; |
- } |
- storage.delete(); |
- it.remove(); |
- } |
- |
- mPreferences.edit() |
- .putLong(KEY_LAST_CLEANUP, currentTime) |
- .putStringSet(KEY_WEBAPP_SET, mStorages.keySet()) |
- .apply(); |
- } |
- |
- /** |
- * Deletes the data of all web apps whose url matches |urlFilter|. |
- * @param urlFilter The filter object to check URLs. |
- */ |
- @VisibleForTesting |
- void unregisterWebappsForUrlsImpl(UrlFilter urlFilter) { |
- assert ThreadUtils.runningOnUiThread(); |
- |
- Iterator<HashMap.Entry<String, WebappDataStorage>> it = mStorages.entrySet().iterator(); |
- while (it.hasNext()) { |
- HashMap.Entry<String, WebappDataStorage> entry = it.next(); |
- WebappDataStorage storage = entry.getValue(); |
- if (urlFilter.matchesUrl(storage.getUrl())) { |
- storage.delete(); |
- it.remove(); |
- } |
- } |
- |
- if (mStorages.isEmpty()) { |
- mPreferences.edit().clear().apply(); |
- } else { |
- mPreferences.edit().putStringSet(KEY_WEBAPP_SET, mStorages.keySet()).apply(); |
- } |
- } |
- |
- @CalledByNative |
- static void unregisterWebappsForUrls(UrlFilterBridge urlFilter) { |
- WebappRegistry.getInstance().unregisterWebappsForUrlsImpl(urlFilter); |
- urlFilter.destroy(); |
- } |
- |
- /** |
- * Deletes the URL and scope, and sets the last used time to 0 for all web apps whose url |
- * matches |urlFilter|. |
- * @param urlFilter The filter object to check URLs. |
- */ |
- @VisibleForTesting |
- void clearWebappHistoryForUrlsImpl(UrlFilter urlFilter) { |
- assert ThreadUtils.runningOnUiThread(); |
- |
- for (HashMap.Entry<String, WebappDataStorage> entry : mStorages.entrySet()) { |
- WebappDataStorage storage = entry.getValue(); |
- if (urlFilter.matchesUrl(storage.getUrl())) { |
- storage.clearHistory(); |
- } |
- } |
- } |
- |
- @CalledByNative |
- static void clearWebappHistoryForUrls(UrlFilterBridge urlFilter) { |
- WebappRegistry.getInstance().clearWebappHistoryForUrlsImpl(urlFilter); |
- urlFilter.destroy(); |
- } |
- |
- /** |
- * Returns true if the given WebAPK is installed. |
- */ |
- private boolean isWebApkInstalled(String webApkPackage) { |
+ static void unregisterOldWebapps(final long currentTime) { |
+ new AsyncTask<Void, Void, Void>() { |
+ @Override |
+ protected final Void doInBackground(Void... nothing) { |
+ SharedPreferences preferences = openSharedPreferences(); |
+ long lastCleanup = preferences.getLong(KEY_LAST_CLEANUP, 0); |
+ if ((currentTime - lastCleanup) < FULL_CLEANUP_DURATION) return null; |
+ |
+ Set<String> currentWebapps = getRegisteredWebappIds(preferences); |
+ Set<String> retainedWebapps = new HashSet<>(currentWebapps); |
+ PackageManager pm = ContextUtils.getApplicationContext().getPackageManager(); |
+ for (String id : currentWebapps) { |
+ WebappDataStorage storage = new WebappDataStorage(id); |
+ String webApkPackage = storage.getWebApkPackageName(); |
+ if (webApkPackage != null) { |
+ if (isWebApkInstalled(pm, webApkPackage)) continue; |
+ } else { |
+ long lastUsed = storage.getLastUsedTime(); |
+ if ((currentTime - lastUsed) < WEBAPP_UNOPENED_CLEANUP_DURATION) continue; |
+ } |
+ WebappDataStorage.deleteDataForWebapp(id); |
+ retainedWebapps.remove(id); |
+ } |
+ |
+ preferences.edit() |
+ .putLong(KEY_LAST_CLEANUP, currentTime) |
+ .putStringSet(KEY_WEBAPP_SET, retainedWebapps) |
+ .apply(); |
+ return null; |
+ } |
+ }.execute(); |
+ } |
+ |
+ /** |
+ * Returns whether the given WebAPK is still installed. |
+ */ |
+ private static boolean isWebApkInstalled(PackageManager pm, String webApkPackage) { |
+ assert !ThreadUtils.runningOnUiThread(); |
try { |
- ContextUtils.getApplicationContext().getPackageManager().getPackageInfo( |
- webApkPackage, PackageManager.GET_ACTIVITIES); |
+ pm.getPackageInfo(webApkPackage, PackageManager.GET_ACTIVITIES); |
} catch (NameNotFoundException e) { |
return false; |
} |
return true; |
} |
+ /** |
+ * Deletes the data of all web apps whose url matches |urlFilter|, as well as the registry |
+ * tracking those web apps. |
+ */ |
+ @VisibleForTesting |
+ static void unregisterWebappsForUrls(final UrlFilter urlFilter, final Runnable callback) { |
+ new AsyncTask<Void, Void, Void>() { |
+ @Override |
+ protected final Void doInBackground(Void... nothing) { |
+ SharedPreferences preferences = openSharedPreferences(); |
+ Set<String> registeredWebapps = |
+ new HashSet<>(getRegisteredWebappIds(preferences)); |
+ Set<String> webappsToUnregister = new HashSet<>(); |
+ for (String id : registeredWebapps) { |
+ if (urlFilter.matchesUrl(WebappDataStorage.open(id).getUrl())) { |
+ WebappDataStorage.deleteDataForWebapp(id); |
+ webappsToUnregister.add(id); |
+ } |
+ } |
+ |
+ // TODO(dominickn): SharedPreferences should be accessed on the main thread, not |
+ // from an AsyncTask. Simultaneous access from two threads creates a race condition. |
+ // Update all callsites in this class. |
+ registeredWebapps.removeAll(webappsToUnregister); |
+ if (registeredWebapps.isEmpty()) { |
+ preferences.edit().clear().apply(); |
+ } else { |
+ preferences.edit().putStringSet(KEY_WEBAPP_SET, registeredWebapps).apply(); |
+ } |
+ |
+ return null; |
+ } |
+ |
+ @Override |
+ protected final void onPostExecute(Void nothing) { |
+ assert callback != null; |
+ callback.run(); |
+ } |
+ }.execute(); |
+ } |
+ |
+ @CalledByNative |
+ static void unregisterWebappsForUrls( |
+ final UrlFilterBridge urlFilter, final long callbackPointer) { |
+ unregisterWebappsForUrls(urlFilter, new Runnable() { |
+ @Override |
+ public void run() { |
+ urlFilter.destroy(); |
+ nativeOnWebappsUnregistered(callbackPointer); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Deletes the URL and scope, and sets the last used time to 0 for all web apps whose url |
+ * matches |urlFilter|. |
+ */ |
+ @VisibleForTesting |
+ static void clearWebappHistoryForUrls(final UrlFilter urlFilter, final Runnable callback) { |
+ new AsyncTask<Void, Void, Void>() { |
+ @Override |
+ protected final Void doInBackground(Void... nothing) { |
+ SharedPreferences preferences = openSharedPreferences(); |
+ for (String id : getRegisteredWebappIds(preferences)) { |
+ if (urlFilter.matchesUrl(WebappDataStorage.open(id).getUrl())) { |
+ WebappDataStorage.clearHistory(id); |
+ } |
+ } |
+ return null; |
+ } |
+ |
+ @Override |
+ protected final void onPostExecute(Void nothing) { |
+ assert callback != null; |
+ callback.run(); |
+ } |
+ }.execute(); |
+ } |
+ |
+ @CalledByNative |
+ static void clearWebappHistoryForUrls( |
+ final UrlFilterBridge urlFilter, final long callbackPointer) { |
+ clearWebappHistoryForUrls(urlFilter, new Runnable() { |
+ @Override |
+ public void run() { |
+ urlFilter.destroy(); |
+ nativeOnClearedWebappHistory(callbackPointer); |
+ } |
+ }); |
+ } |
+ |
private static SharedPreferences openSharedPreferences() { |
return ContextUtils.getApplicationContext().getSharedPreferences( |
REGISTRY_FILE_NAME, Context.MODE_PRIVATE); |
} |
+ private static Set<String> getRegisteredWebappIds(SharedPreferences preferences) { |
+ // Wrap with unmodifiableSet to ensure it's never modified. See crbug.com/568369. |
+ return Collections.unmodifiableSet( |
+ preferences.getStringSet(KEY_WEBAPP_SET, Collections.<String>emptySet())); |
+ } |
+ |
private WebappRegistry() { |
- mPreferences = openSharedPreferences(); |
- mStorages = new HashMap<String, WebappDataStorage>(); |
- } |
- |
- private void initStorages(String idToInitialize, boolean replaceExisting) { |
- Set<String> webapps = |
- mPreferences.getStringSet(KEY_WEBAPP_SET, Collections.<String>emptySet()); |
- boolean initAll = (idToInitialize == null || idToInitialize.isEmpty()); |
- |
- // Don't overwrite any entry in mStorages unless replaceExisting is set to true. |
- if (initAll) { |
- for (String id : webapps) { |
- if (replaceExisting || !mStorages.containsKey(id)) { |
- mStorages.put(id, WebappDataStorage.open(id)); |
- } |
- } |
- } else { |
- if (webapps.contains(idToInitialize) |
- && (replaceExisting || !mStorages.containsKey(idToInitialize))) { |
- mStorages.put(idToInitialize, WebappDataStorage.open(idToInitialize)); |
- } |
- } |
- } |
+ } |
+ |
+ private static native void nativeOnWebappsUnregistered(long callbackPointer); |
+ private static native void nativeOnClearedWebappHistory(long callbackPointer); |
} |