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)); |
+ } |
+ } |
+ } |
} |