OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.webapps; |
| 6 |
| 7 import android.content.Context; |
| 8 import android.content.SharedPreferences; |
| 9 import android.util.Log; |
| 10 |
| 11 import org.chromium.base.ThreadUtils; |
| 12 import org.chromium.base.VisibleForTesting; |
| 13 |
| 14 import java.util.ArrayList; |
| 15 import java.util.HashSet; |
| 16 import java.util.List; |
| 17 import java.util.Set; |
| 18 |
| 19 /** |
| 20 * Manages a rotating LRU buffer of WebappActivities to assign webapps to. |
| 21 * |
| 22 * In order to accommodate a limited number of WebappActivities with a potential
ly unlimited number |
| 23 * of webapps, we have to rotate the available WebappActivities between the weba
pps we start up. |
| 24 * Activities are reused in order of when they were last used, with the least re
cently used |
| 25 * ones culled first. |
| 26 * |
| 27 * It is impossible to know whether Tasks have been removed from the Recent Task
list without the |
| 28 * GET_TASKS permission. As a result, the list of Activities inside the Recent
Task list will |
| 29 * be highly unlikely to match the list maintained in memory. Instead, we store
the mapping as it |
| 30 * was the last time we changed it, which allows us to launch webapps in the Web
appActivity they |
| 31 * were most recently associated with in cases where a user restarts a webapp fr
om the Recent Tasks. |
| 32 * Note that in situations where the user manually clears the app data, we will
again have an |
| 33 * incorrect mapping. |
| 34 * |
| 35 * Unless otherwise noted, all methods MUST be called on the UI thread to avoid
threading issues. |
| 36 * |
| 37 * EXAMPLE: |
| 38 * - 3 Activities are available for assignment (0, 1, 2). |
| 39 * - 4 webapps exist (X, Y, Z, W). |
| 40 * |
| 41 * ACTION EFFECT ACTIVI
TY LIST |
| 42 * 0) Clean slate (0 -)
(1 -) (2 -) |
| 43 * 1) Start X Assigned to Activity 0 and pushed back. (1 -)
(2 -) (0 X) |
| 44 * 2) Start Y Assigned to Activity 1 and pushed back. (2 -)
(0 X) (1 Y) |
| 45 * 3) Start Z Assigned to Activity 2 and pushed back. (0 X)
(1 Y) (2 Z) |
| 46 * 4) Restart Y Re-assigned to Activity 1 and pushed back. (0 X)
(2 Z) (1 Y) |
| 47 * 4) Start W Assigned to Activity 0 and pushed back. X evicted. (2 Z)
(1 Y) (0 W) |
| 48 * 5) Restart X Assigned to Activity 2 and pushed back. Z evicted. (1 Y)
(0 W) (2 X) |
| 49 */ |
| 50 public class ActivityAssigner { |
| 51 private static final String TAG = "ActivityAssigner"; |
| 52 |
| 53 // Don't ever change this. 10 is enough for everyone. |
| 54 static final int NUM_WEBAPP_ACTIVITIES = 10; |
| 55 |
| 56 // A sanity check limit to ensure that we aren't reading an unreasonable num
ber of preferences. |
| 57 // This number is different from above because the number of WebappActivitie
s available may |
| 58 // change. |
| 59 static final int MAX_WEBAPP_ACTIVITIES_EVER = 100; |
| 60 |
| 61 // Don't ever change the package. Left for backwards compatibility. |
| 62 @VisibleForTesting |
| 63 static final String PREF_PACKAGE = "com.google.android.apps.chrome.webapps"; |
| 64 static final String PREF_NUM_SAVED_ENTRIES = "ActivityAssigner.numSavedEntri
es"; |
| 65 static final String PREF_ACTIVITY_INDEX = "ActivityAssigner.activityIndex"; |
| 66 static final String PREF_WEBAPP_ID = "ActivityAssigner.webappId"; |
| 67 |
| 68 static final int INVALID_ACTIVITY_INDEX = -1; |
| 69 |
| 70 private static ActivityAssigner sInstance; |
| 71 |
| 72 private final Context mContext; |
| 73 private final List<ActivityEntry> mActivityList; |
| 74 |
| 75 static class ActivityEntry { |
| 76 final int mActivityIndex; |
| 77 final String mWebappId; |
| 78 |
| 79 ActivityEntry(int activity, String webapp) { |
| 80 mActivityIndex = activity; |
| 81 mWebappId = webapp; |
| 82 } |
| 83 } |
| 84 |
| 85 /** |
| 86 * Returns the singleton instance, creating it if necessary. |
| 87 */ |
| 88 public static ActivityAssigner instance(Context context) { |
| 89 ThreadUtils.assertOnUiThread(); |
| 90 if (sInstance == null) { |
| 91 sInstance = new ActivityAssigner(context); |
| 92 } |
| 93 return sInstance; |
| 94 } |
| 95 |
| 96 private ActivityAssigner(Context context) { |
| 97 mContext = context.getApplicationContext(); |
| 98 mActivityList = new ArrayList<ActivityEntry>(); |
| 99 |
| 100 restoreActivityList(); |
| 101 } |
| 102 |
| 103 /** |
| 104 * Assigns the webapp with the given ID to one of the available WebappActivi
ties. |
| 105 * If we know that the webapp was previously launched in one of the Activiti
es, re-use it. |
| 106 * Otherwise, take the least recently used WebappActivity ID and use that. |
| 107 * @param webappId ID of the webapp. |
| 108 * @return Index of the Activity to use for the webapp. |
| 109 */ |
| 110 int assign(String webappId) { |
| 111 // Reuse a running Activity with the same ID, if it exists. |
| 112 int activityIndex = checkIfAssigned(webappId); |
| 113 |
| 114 // Allocate the one in the front of the list. |
| 115 if (activityIndex == INVALID_ACTIVITY_INDEX) { |
| 116 activityIndex = mActivityList.get(0).mActivityIndex; |
| 117 ActivityEntry newEntry = new ActivityEntry(activityIndex, webappId); |
| 118 mActivityList.set(0, newEntry); |
| 119 } |
| 120 |
| 121 markActivityUsed(activityIndex, webappId); |
| 122 return activityIndex; |
| 123 } |
| 124 |
| 125 /** |
| 126 * Checks if the webapp with the given ID has been assigned to an Activity a
lready. |
| 127 * @param webappId ID of the webapp being displayed. |
| 128 * @return Index of the Activity for the webapp if assigned, INVALID_ACTIVIT
Y_INDEX otherwise. |
| 129 */ |
| 130 int checkIfAssigned(String webappId) { |
| 131 if (webappId == null) { |
| 132 return INVALID_ACTIVITY_INDEX; |
| 133 } |
| 134 |
| 135 // Go backwards in the queue to catch more recent instances of any dupli
cated webapps. |
| 136 for (int i = mActivityList.size() - 1; i >= 0; i--) { |
| 137 if (webappId.equals(mActivityList.get(i).mWebappId)) { |
| 138 return mActivityList.get(i).mActivityIndex; |
| 139 } |
| 140 } |
| 141 return INVALID_ACTIVITY_INDEX; |
| 142 } |
| 143 |
| 144 /** |
| 145 * Moves a WebappActivity to the back of the queue, indicating that the Weba
pp is still in use |
| 146 * and shouldn't be killed. |
| 147 * @param activityIndex Index of the WebappActivity. |
| 148 * @param webappId ID of the webapp being shown in the WebappActivity. |
| 149 */ |
| 150 void markActivityUsed(int activityIndex, String webappId) { |
| 151 // Find the entry corresponding to the Activity. |
| 152 int elementIndex = findActivityElement(activityIndex); |
| 153 |
| 154 if (elementIndex == -1) { |
| 155 Log.e(TAG, "Failed to find WebappActivity entry: " + activityIndex +
", " + webappId); |
| 156 return; |
| 157 } |
| 158 |
| 159 // We have to reassign the webapp ID in case WebappActivities get repurp
osed. |
| 160 ActivityEntry updatedEntry = new ActivityEntry(activityIndex, webappId); |
| 161 mActivityList.remove(elementIndex); |
| 162 mActivityList.add(updatedEntry); |
| 163 storeActivityList(); |
| 164 } |
| 165 |
| 166 /** |
| 167 * Finds the index of the ActivityElement corresponding to the given activit
yIndex. |
| 168 * @param activityIndex Index of the activity to find. |
| 169 * @return The index of the ActivityElement in the activity list, or -1 if i
t couldn't be found. |
| 170 */ |
| 171 private int findActivityElement(int activityIndex) { |
| 172 for (int elementIndex = 0; elementIndex < mActivityList.size(); elementI
ndex++) { |
| 173 if (mActivityList.get(elementIndex).mActivityIndex == activityIndex)
{ |
| 174 return elementIndex; |
| 175 } |
| 176 } |
| 177 return -1; |
| 178 } |
| 179 |
| 180 /** |
| 181 * Returns the current mapping between Activities and webapps. |
| 182 */ |
| 183 @VisibleForTesting |
| 184 List<ActivityEntry> getEntries() { |
| 185 return mActivityList; |
| 186 } |
| 187 |
| 188 /** |
| 189 * Restores/creates the mapping between webapps and WebappActivities. |
| 190 * The logic is slightly complicated to future-proof against situations wher
e the number of |
| 191 * WebappActivities is changed. |
| 192 */ |
| 193 private void restoreActivityList() { |
| 194 boolean isMapDirty = false; |
| 195 mActivityList.clear(); |
| 196 |
| 197 // Create a Set of indices corresponding to every possible Activity. |
| 198 // As ActivityEntries are read, they are and removed from this list to i
ndicate that the |
| 199 // Activity has already been assigned. |
| 200 Set<Integer> availableWebapps = new HashSet<Integer>(); |
| 201 for (int i = 0; i < NUM_WEBAPP_ACTIVITIES; ++i) { |
| 202 availableWebapps.add(i); |
| 203 } |
| 204 |
| 205 // Restore any entries that were previously saved. If it seems that the
preferences have |
| 206 // been corrupted somehow, just discard the whole map. |
| 207 SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Co
ntext.MODE_PRIVATE); |
| 208 try { |
| 209 final int numSavedEntries = prefs.getInt(PREF_NUM_SAVED_ENTRIES, 0); |
| 210 if (numSavedEntries <= NUM_WEBAPP_ACTIVITIES) { |
| 211 for (int i = 0; i < numSavedEntries; ++i) { |
| 212 String currentActivityIndexPref = PREF_ACTIVITY_INDEX + i; |
| 213 String currentWebappIdPref = PREF_WEBAPP_ID + i; |
| 214 |
| 215 int activityIndex = prefs.getInt(currentActivityIndexPref, i
); |
| 216 String webappId = prefs.getString(currentWebappIdPref, null)
; |
| 217 ActivityEntry entry = new ActivityEntry(activityIndex, webap
pId); |
| 218 |
| 219 if (availableWebapps.remove(entry.mActivityIndex)) { |
| 220 mActivityList.add(entry); |
| 221 } else { |
| 222 // If the same activity was assigned to two different en
tries, or if the |
| 223 // number of activities changed, discard it and mark tha
t it needs to be |
| 224 // rewritten. |
| 225 isMapDirty = true; |
| 226 } |
| 227 } |
| 228 } |
| 229 } catch (ClassCastException exception) { |
| 230 // Something went wrong reading the preferences. Nuke everything. |
| 231 mActivityList.clear(); |
| 232 availableWebapps.clear(); |
| 233 for (int i = 0; i < NUM_WEBAPP_ACTIVITIES; ++i) { |
| 234 availableWebapps.add(i); |
| 235 } |
| 236 } |
| 237 |
| 238 // Add entries for any missing WebappActivities. |
| 239 for (Integer availableIndex : availableWebapps) { |
| 240 ActivityEntry entry = new ActivityEntry(availableIndex, null); |
| 241 mActivityList.add(entry); |
| 242 isMapDirty = true; |
| 243 } |
| 244 |
| 245 if (isMapDirty) { |
| 246 storeActivityList(); |
| 247 } |
| 248 } |
| 249 |
| 250 /** |
| 251 * Saves the mapping between webapps and WebappActivities. |
| 252 */ |
| 253 private void storeActivityList() { |
| 254 SharedPreferences prefs = mContext.getSharedPreferences(PREF_PACKAGE, Co
ntext.MODE_PRIVATE); |
| 255 SharedPreferences.Editor editor = prefs.edit(); |
| 256 editor.clear(); |
| 257 editor.putInt(PREF_NUM_SAVED_ENTRIES, mActivityList.size()); |
| 258 for (int i = 0; i < mActivityList.size(); ++i) { |
| 259 String currentActivityIndexPref = PREF_ACTIVITY_INDEX + i; |
| 260 String currentWebappIdPref = PREF_WEBAPP_ID + i; |
| 261 editor.putInt(currentActivityIndexPref, mActivityList.get(i).mActivi
tyIndex); |
| 262 editor.putString(currentWebappIdPref, mActivityList.get(i).mWebappId
); |
| 263 } |
| 264 editor.apply(); |
| 265 } |
| 266 } |
OLD | NEW |