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

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

Issue 2351113005: [Reland] Refactor WebappRegistry into a singleton instance. (Closed)
Patch Set: Comments 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 629aef6de6f64b70fc45a7eef4bedf8f0074ba74..2f4d265096609eb46c811a4e1c24b2f8e99fc664 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,25 +11,31 @@ import android.content.pm.PackageManager.NameNotFoundException;
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.HashSet;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
- * 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.
+ * 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.
*
- * 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).
+ * 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.
*/
public class WebappRegistry {
@@ -43,19 +49,47 @@ public class WebappRegistry {
/** 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;
+
/**
- * Called when a retrieval of the set of stored web app IDs occurs.
+ * Callback run when a WebappDataStorage object is registered for the first time. The storage
+ * parameter will never be null.
*/
- public interface FetchCallback {
- void onWebappIdsRetrieved(Set<String> readObject);
+ public interface FetchWebappDataStorageCallback {
+ void onWebappDataStorageRetrieved(WebappDataStorage storage);
}
/**
- * 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.
+ * Returns the singleton WebappRegistry instance. Creates the instance if necessary.
*/
- public interface FetchWebappDataStorageCallback {
- void onWebappDataStorageRetrieved(WebappDataStorage storage);
+ 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);
}
/**
@@ -65,259 +99,164 @@ public class WebappRegistry {
* @param callback The callback to run with the WebappDataStorage argument.
* @return The storage object for the web app.
*/
- public static void registerWebapp(final String webappId,
- final FetchWebappDataStorageCallback callback) {
+ public void register(final String webappId, final FetchWebappDataStorageCallback callback) {
new AsyncTask<Void, Void, WebappDataStorage>() {
@Override
protected final WebappDataStorage doInBackground(Void... nothing) {
- 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;
+ // Create the WebappDataStorage on the background thread, as this must create and
+ // open a new SharedPreferences.
+ return WebappDataStorage.open(webappId);
}
@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();
}
/**
- * 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.
+ * Returns the WebappDataStorage object for webappId, or null if one cannot be found.
+ * @param webappId The id of the web app.
* @return The storage object for the web app, or null if webappId is not registered.
*/
- 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();
+ public WebappDataStorage getWebappDataStorage(String webappId) {
+ return mStorages.get(webappId);
}
/**
- * 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.
+ * 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 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);
+ 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();
}
- }.execute();
+ }
+ return bestMatch;
}
/**
- * 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.
+ * Returns the list of web app IDs which are written to SharedPreferences.
*/
@VisibleForTesting
- 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();
+ 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()));
}
/**
- * 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.
- *
+ * 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.
* @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.
*/
- 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);
- }
+ public void unregisterOldWebapps(long currentTime) {
+ if ((currentTime - mPreferences.getLong(KEY_LAST_CLEANUP, 0)) < FULL_CLEANUP_DURATION) {
+ return;
+ }
- preferences.edit()
- .putLong(KEY_LAST_CLEANUP, currentTime)
- .putStringSet(KEY_WEBAPP_SET, retainedWebapps)
- .apply();
- return null;
+ 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;
}
- }.execute();
- }
-
- /**
- * Returns whether the given WebAPK is still installed.
- */
- private static boolean isWebApkInstalled(PackageManager pm, String webApkPackage) {
- assert !ThreadUtils.runningOnUiThread();
- try {
- pm.getPackageInfo(webApkPackage, PackageManager.GET_ACTIVITIES);
- } catch (NameNotFoundException e) {
- return false;
+ storage.delete();
+ it.remove();
}
- return true;
+
+ 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|, as well as the registry
- * tracking those web apps.
+ * Deletes the data of all web apps whose url matches |urlFilter|.
+ * @param urlFilter The filter object to check URLs.
*/
@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;
+ 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();
}
+ }
- @Override
- protected final void onPostExecute(Void nothing) {
- assert callback != null;
- callback.run();
- }
- }.execute();
+ if (mStorages.isEmpty()) {
+ mPreferences.edit().clear().apply();
+ } else {
+ mPreferences.edit().putStringSet(KEY_WEBAPP_SET, mStorages.keySet()).apply();
+ }
}
@CalledByNative
- static void unregisterWebappsForUrls(
- final UrlFilterBridge urlFilter, final long callbackPointer) {
- unregisterWebappsForUrls(urlFilter, new Runnable() {
- @Override
- public void run() {
- urlFilter.destroy();
- nativeOnWebappsUnregistered(callbackPointer);
- }
- });
+ 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
- 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;
+ void clearWebappHistoryForUrlsImpl(UrlFilter urlFilter) {
+ for (HashMap.Entry<String, WebappDataStorage> entry : mStorages.entrySet()) {
+ WebappDataStorage storage = entry.getValue();
+ if (urlFilter.matchesUrl(storage.getUrl())) {
+ storage.clearHistory();
}
-
- @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);
- }
- });
+ static void clearWebappHistoryForUrls(UrlFilterBridge urlFilter) {
+ WebappRegistry.getInstance().clearWebappHistoryForUrlsImpl(urlFilter);
+ urlFilter.destroy();
+ }
+
+ /**
+ * Returns true if the given WebAPK is installed.
+ */
+ private boolean isWebApkInstalled(String webApkPackage) {
+ try {
+ ContextUtils.getApplicationContext().getPackageManager().getPackageInfo(
+ webApkPackage, PackageManager.GET_ACTIVITIES);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ return true;
}
private static SharedPreferences openSharedPreferences() {
@@ -325,15 +264,28 @@ public class WebappRegistry {
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 static native void nativeOnWebappsUnregistered(long callbackPointer);
- private static native void nativeOnClearedWebappHistory(long callbackPointer);
+ 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));
+ }
+ }
+ }
}
« 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