Index: chrome/android/java_staging/src/org/chromium/chrome/browser/webapps/ActivityAssigner.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/webapps/ActivityAssigner.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/webapps/ActivityAssigner.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..30bbb8e57b8c87629feda504dacb7593a87353ef |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/webapps/ActivityAssigner.java |
@@ -0,0 +1,266 @@ |
+// 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.content.Context; |
+import android.content.SharedPreferences; |
+import android.util.Log; |
+ |
+import org.chromium.base.ThreadUtils; |
+import org.chromium.base.VisibleForTesting; |
+ |
+import java.util.ArrayList; |
+import java.util.HashSet; |
+import java.util.List; |
+import java.util.Set; |
+ |
+/** |
+ * Manages a rotating LRU buffer of WebappActivities to assign webapps to. |
+ * |
+ * In order to accommodate a limited number of WebappActivities with a potentially unlimited number |
+ * of webapps, we have to rotate the available WebappActivities between the webapps we start up. |
+ * Activities are reused in order of when they were last used, with the least recently used |
+ * ones culled first. |
+ * |
+ * It is impossible to know whether Tasks have been removed from the Recent Task list without the |
+ * GET_TASKS permission. As a result, the list of Activities inside the Recent Task list will |
+ * be highly unlikely to match the list maintained in memory. Instead, we store the mapping as it |
+ * was the last time we changed it, which allows us to launch webapps in the WebappActivity they |
+ * were most recently associated with in cases where a user restarts a webapp from the Recent Tasks. |
+ * Note that in situations where the user manually clears the app data, we will again have an |
+ * incorrect mapping. |
+ * |
+ * Unless otherwise noted, all methods MUST be called on the UI thread to avoid threading issues. |
+ * |
+ * EXAMPLE: |
+ * - 3 Activities are available for assignment (0, 1, 2). |
+ * - 4 webapps exist (X, Y, Z, W). |
+ * |
+ * ACTION EFFECT ACTIVITY LIST |
+ * 0) Clean slate (0 -) (1 -) (2 -) |
+ * 1) Start X Assigned to Activity 0 and pushed back. (1 -) (2 -) (0 X) |
+ * 2) Start Y Assigned to Activity 1 and pushed back. (2 -) (0 X) (1 Y) |
+ * 3) Start Z Assigned to Activity 2 and pushed back. (0 X) (1 Y) (2 Z) |
+ * 4) Restart Y Re-assigned to Activity 1 and pushed back. (0 X) (2 Z) (1 Y) |
+ * 4) Start W Assigned to Activity 0 and pushed back. X evicted. (2 Z) (1 Y) (0 W) |
+ * 5) Restart X Assigned to Activity 2 and pushed back. Z evicted. (1 Y) (0 W) (2 X) |
+ */ |
+public class ActivityAssigner { |
+ private static final String TAG = "ActivityAssigner"; |
+ |
+ // Don't ever change this. 10 is enough for everyone. |
+ static final int NUM_WEBAPP_ACTIVITIES = 10; |
+ |
+ // A sanity check limit to ensure that we aren't reading an unreasonable number of preferences. |
+ // This number is different from above because the number of WebappActivities available may |
+ // change. |
+ static final int MAX_WEBAPP_ACTIVITIES_EVER = 100; |
+ |
+ // Don't ever change the package. Left for backwards compatibility. |
+ @VisibleForTesting |
+ static final String PREF_PACKAGE = "com.google.android.apps.chrome.webapps"; |
+ static final String PREF_NUM_SAVED_ENTRIES = "ActivityAssigner.numSavedEntries"; |
+ static final String PREF_ACTIVITY_INDEX = "ActivityAssigner.activityIndex"; |
+ static final String PREF_WEBAPP_ID = "ActivityAssigner.webappId"; |
+ |
+ static final int INVALID_ACTIVITY_INDEX = -1; |
+ |
+ private static ActivityAssigner sInstance; |
+ |
+ private final Context mContext; |
+ private final List<ActivityEntry> mActivityList; |
+ |
+ static class ActivityEntry { |
+ final int mActivityIndex; |
+ final String mWebappId; |
+ |
+ ActivityEntry(int activity, String webapp) { |
+ mActivityIndex = activity; |
+ mWebappId = webapp; |
+ } |
+ } |
+ |
+ /** |
+ * Returns the singleton instance, creating it if necessary. |
+ */ |
+ public static ActivityAssigner instance(Context context) { |
+ ThreadUtils.assertOnUiThread(); |
+ if (sInstance == null) { |
+ sInstance = new ActivityAssigner(context); |
+ } |
+ return sInstance; |
+ } |
+ |
+ private ActivityAssigner(Context context) { |
+ mContext = context.getApplicationContext(); |
+ mActivityList = new ArrayList<ActivityEntry>(); |
+ |
+ restoreActivityList(); |
+ } |
+ |
+ /** |
+ * Assigns the webapp with the given ID to one of the available WebappActivities. |
+ * If we know that the webapp was previously launched in one of the Activities, re-use it. |
+ * Otherwise, take the least recently used WebappActivity ID and use that. |
+ * @param webappId ID of the webapp. |
+ * @return Index of the Activity to use for the webapp. |
+ */ |
+ int assign(String webappId) { |
+ // Reuse a running Activity with the same ID, if it exists. |
+ int activityIndex = checkIfAssigned(webappId); |
+ |
+ // Allocate the one in the front of the list. |
+ if (activityIndex == INVALID_ACTIVITY_INDEX) { |
+ activityIndex = mActivityList.get(0).mActivityIndex; |
+ ActivityEntry newEntry = new ActivityEntry(activityIndex, webappId); |
+ mActivityList.set(0, newEntry); |
+ } |
+ |
+ markActivityUsed(activityIndex, webappId); |
+ return activityIndex; |
+ } |
+ |
+ /** |
+ * Checks if the webapp with the given ID has been assigned to an Activity already. |
+ * @param webappId ID of the webapp being displayed. |
+ * @return Index of the Activity for the webapp if assigned, INVALID_ACTIVITY_INDEX otherwise. |
+ */ |
+ int checkIfAssigned(String webappId) { |
+ if (webappId == null) { |
+ return INVALID_ACTIVITY_INDEX; |
+ } |
+ |
+ // Go backwards in the queue to catch more recent instances of any duplicated webapps. |
+ for (int i = mActivityList.size() - 1; i >= 0; i--) { |
+ if (webappId.equals(mActivityList.get(i).mWebappId)) { |
+ return mActivityList.get(i).mActivityIndex; |
+ } |
+ } |
+ return INVALID_ACTIVITY_INDEX; |
+ } |
+ |
+ /** |
+ * Moves a WebappActivity to the back of the queue, indicating that the Webapp is still in use |
+ * and shouldn't be killed. |
+ * @param activityIndex Index of the WebappActivity. |
+ * @param webappId ID of the webapp being shown in the WebappActivity. |
+ */ |
+ void markActivityUsed(int activityIndex, String webappId) { |
+ // Find the entry corresponding to the Activity. |
+ int elementIndex = findActivityElement(activityIndex); |
+ |
+ if (elementIndex == -1) { |
+ Log.e(TAG, "Failed to find WebappActivity entry: " + activityIndex + ", " + webappId); |
+ return; |
+ } |
+ |
+ // We have to reassign the webapp ID in case WebappActivities get repurposed. |
+ ActivityEntry updatedEntry = new ActivityEntry(activityIndex, webappId); |
+ mActivityList.remove(elementIndex); |
+ mActivityList.add(updatedEntry); |
+ storeActivityList(); |
+ } |
+ |
+ /** |
+ * Finds the index of the ActivityElement corresponding to the given activityIndex. |
+ * @param activityIndex Index of the activity to find. |
+ * @return The index of the ActivityElement in the activity list, or -1 if it couldn't be found. |
+ */ |
+ private int findActivityElement(int activityIndex) { |
+ for (int elementIndex = 0; elementIndex < mActivityList.size(); elementIndex++) { |
+ if (mActivityList.get(elementIndex).mActivityIndex == activityIndex) { |
+ return elementIndex; |
+ } |
+ } |
+ return -1; |
+ } |
+ |
+ /** |
+ * Returns the current mapping between Activities and webapps. |
+ */ |
+ @VisibleForTesting |
+ List<ActivityEntry> getEntries() { |
+ return mActivityList; |
+ } |
+ |
+ /** |
+ * Restores/creates the mapping between webapps and WebappActivities. |
+ * The logic is slightly complicated to future-proof against situations where the number of |
+ * WebappActivities is changed. |
+ */ |
+ private void restoreActivityList() { |
+ boolean isMapDirty = false; |
+ mActivityList.clear(); |
+ |
+ // Create a Set of indices corresponding to every possible Activity. |
+ // As ActivityEntries are read, they are and removed from this list to indicate that the |
+ // Activity has already been assigned. |
+ Set<Integer> availableWebapps = new HashSet<Integer>(); |
+ for (int i = 0; i < NUM_WEBAPP_ACTIVITIES; ++i) { |
+ availableWebapps.add(i); |
+ } |
+ |
+ // Restore any entries that were previously saved. If it seems that the preferences have |
+ // been corrupted somehow, just discard the whole map. |
+ SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE); |
+ try { |
+ final int numSavedEntries = prefs.getInt(PREF_NUM_SAVED_ENTRIES, 0); |
+ if (numSavedEntries <= NUM_WEBAPP_ACTIVITIES) { |
+ for (int i = 0; i < numSavedEntries; ++i) { |
+ String currentActivityIndexPref = PREF_ACTIVITY_INDEX + i; |
+ String currentWebappIdPref = PREF_WEBAPP_ID + i; |
+ |
+ int activityIndex = prefs.getInt(currentActivityIndexPref, i); |
+ String webappId = prefs.getString(currentWebappIdPref, null); |
+ ActivityEntry entry = new ActivityEntry(activityIndex, webappId); |
+ |
+ if (availableWebapps.remove(entry.mActivityIndex)) { |
+ mActivityList.add(entry); |
+ } else { |
+ // If the same activity was assigned to two different entries, or if the |
+ // number of activities changed, discard it and mark that it needs to be |
+ // rewritten. |
+ isMapDirty = true; |
+ } |
+ } |
+ } |
+ } catch (ClassCastException exception) { |
+ // Something went wrong reading the preferences. Nuke everything. |
+ mActivityList.clear(); |
+ availableWebapps.clear(); |
+ for (int i = 0; i < NUM_WEBAPP_ACTIVITIES; ++i) { |
+ availableWebapps.add(i); |
+ } |
+ } |
+ |
+ // Add entries for any missing WebappActivities. |
+ for (Integer availableIndex : availableWebapps) { |
+ ActivityEntry entry = new ActivityEntry(availableIndex, null); |
+ mActivityList.add(entry); |
+ isMapDirty = true; |
+ } |
+ |
+ if (isMapDirty) { |
+ storeActivityList(); |
+ } |
+ } |
+ |
+ /** |
+ * Saves the mapping between webapps and WebappActivities. |
+ */ |
+ private void storeActivityList() { |
+ SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Context.MODE_PRIVATE); |
+ SharedPreferences.Editor editor = prefs.edit(); |
+ editor.clear(); |
+ editor.putInt(PREF_NUM_SAVED_ENTRIES, mActivityList.size()); |
+ for (int i = 0; i < mActivityList.size(); ++i) { |
+ String currentActivityIndexPref = PREF_ACTIVITY_INDEX + i; |
+ String currentWebappIdPref = PREF_WEBAPP_ID + i; |
+ editor.putInt(currentActivityIndexPref, mActivityList.get(i).mActivityIndex); |
+ editor.putString(currentWebappIdPref, mActivityList.get(i).mWebappId); |
+ } |
+ editor.apply(); |
+ } |
+} |