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

Unified Diff: chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappRegistry.java

Issue 2397623004: Revert of [Reland] Refactor WebappRegistry into a singleton instance. (Closed)
Patch Set: Created 4 years, 2 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/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 2f4d265096609eb46c811a4e1c24b2f8e99fc664..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
@@ -11,31 +11,25 @@
import android.os.AsyncTask;
import org.chromium.base.ContextUtils;
+import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.browsing_data.UrlFilter;
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 must be used on the main thread, except when warming
- * SharedPreferences.
+ * 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,47 +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() {
- 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() {
- sInstance = new WebappRegistry();
- sInstance.initStorages(null, true);
}
/**
@@ -99,193 +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) {
+ 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) {
- 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) {
- 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) {
- 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);
}
« no previous file with comments | « chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappDataStorage.java ('k') | chrome/android/java_sources.gni » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698