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

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

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 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_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();
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698