• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.icons.cache;
17 
18 import android.content.ComponentName;
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageInfo;
21 import android.content.pm.PackageManager;
22 import android.database.Cursor;
23 import android.database.sqlite.SQLiteException;
24 import android.os.SystemClock;
25 import android.os.UserHandle;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.util.SparseBooleanArray;
29 
30 import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
31 
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map.Entry;
37 import java.util.Set;
38 import java.util.Stack;
39 
40 /**
41  * Utility class to handle updating the Icon cache
42  */
43 public class IconCacheUpdateHandler {
44 
45     private static final String TAG = "IconCacheUpdateHandler";
46 
47     /**
48      * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
49      * This mode is used for the first run.
50      */
51     private static final boolean MODE_SET_INVALID_ITEMS = true;
52 
53     /**
54      * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
55      * subsequent runs, which essentially acts as set-union of all valid items.
56      */
57     private static final boolean MODE_CLEAR_VALID_ITEMS = false;
58 
59     private static final Object ICON_UPDATE_TOKEN = new Object();
60 
61     private final HashMap<String, PackageInfo> mPkgInfoMap;
62     private final BaseIconCache mIconCache;
63 
64     private final HashMap<UserHandle, Set<String>> mPackagesToIgnore = new HashMap<>();
65 
66     private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
67     private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
68 
IconCacheUpdateHandler(BaseIconCache cache)69     IconCacheUpdateHandler(BaseIconCache cache) {
70         mIconCache = cache;
71 
72         mPkgInfoMap = new HashMap<>();
73 
74         // Remove all active icon update tasks.
75         mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
76 
77         createPackageInfoMap();
78     }
79 
setPackagesToIgnore(UserHandle userHandle, Set<String> packages)80     public void setPackagesToIgnore(UserHandle userHandle, Set<String> packages) {
81         mPackagesToIgnore.put(userHandle, packages);
82     }
83 
createPackageInfoMap()84     private void createPackageInfoMap() {
85         PackageManager pm = mIconCache.mPackageManager;
86         for (PackageInfo info :
87                 pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
88             mPkgInfoMap.put(info.packageName, info);
89         }
90     }
91 
92     /**
93      * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
94      * the DB and are updated.
95      * @return The set of packages for which icons have updated.
96      */
updateIcons(List<T> apps, CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback)97     public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
98             OnUpdateCallback onUpdateCallback) {
99         // Filter the list per user
100         HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
101         int count = apps.size();
102         for (int i = 0; i < count; i++) {
103             T app = apps.get(i);
104             UserHandle userHandle = cachingLogic.getUser(app);
105             HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
106             if (componentMap == null) {
107                 componentMap = new HashMap<>();
108                 userComponentMap.put(userHandle, componentMap);
109             }
110             componentMap.put(cachingLogic.getComponent(app), app);
111         }
112 
113         for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
114             updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
115         }
116 
117         // From now on, clear every valid item from the global valid map.
118         mFilterMode = MODE_CLEAR_VALID_ITEMS;
119     }
120 
121     /**
122      * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
123      * the DB and are updated.
124      * @return The set of packages for which icons have updated.
125      */
126     @SuppressWarnings("unchecked")
updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap, CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback)127     private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
128             CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
129         Set<String> ignorePackages = mPackagesToIgnore.get(user);
130         if (ignorePackages == null) {
131             ignorePackages = Collections.emptySet();
132         }
133         long userSerial = mIconCache.getSerialNumberForUser(user);
134 
135         Stack<T> appsToUpdate = new Stack<>();
136 
137         try (Cursor c = mIconCache.mIconDb.query(
138                 new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
139                         IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
140                         IconDB.COLUMN_SYSTEM_STATE},
141                 IconDB.COLUMN_USER + " = ? ",
142                 new String[]{Long.toString(userSerial)})) {
143 
144             final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
145             final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
146             final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
147             final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
148             final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
149 
150             while (c.moveToNext()) {
151                 String cn = c.getString(indexComponent);
152                 ComponentName component = ComponentName.unflattenFromString(cn);
153                 PackageInfo info = mPkgInfoMap.get(component.getPackageName());
154 
155                 int rowId = c.getInt(rowIndex);
156                 if (info == null) {
157                     if (!ignorePackages.contains(component.getPackageName())) {
158 
159                         if (mFilterMode == MODE_SET_INVALID_ITEMS) {
160                             mIconCache.remove(component, user);
161                             mItemsToDelete.put(rowId, true);
162                         }
163                     }
164                     continue;
165                 }
166                 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
167                     // Application is not present
168                     continue;
169                 }
170 
171                 long updateTime = c.getLong(indexLastUpdate);
172                 int version = c.getInt(indexVersion);
173                 T app = componentMap.remove(component);
174                 if (version == info.versionCode && updateTime == info.lastUpdateTime &&
175                         TextUtils.equals(c.getString(systemStateIndex),
176                                 mIconCache.getIconSystemState(info.packageName))) {
177 
178                     if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
179                         mItemsToDelete.put(rowId, false);
180                     }
181                     continue;
182                 }
183                 if (app == null) {
184                     if (mFilterMode == MODE_SET_INVALID_ITEMS) {
185                         mIconCache.remove(component, user);
186                         mItemsToDelete.put(rowId, true);
187                     }
188                 } else {
189                     appsToUpdate.add(app);
190                 }
191             }
192         } catch (SQLiteException e) {
193             Log.d(TAG, "Error reading icon cache", e);
194             // Continue updating whatever we have read so far
195         }
196 
197         // Insert remaining apps.
198         if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
199             Stack<T> appsToAdd = new Stack<>();
200             appsToAdd.addAll(componentMap.values());
201             new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
202                     onUpdateCallback).scheduleNext();
203         }
204     }
205 
206     /**
207      * Commits all updates as part of the update handler to disk. Not more calls should be made
208      * to this class after this.
209      */
finish()210     public void finish() {
211         // Commit all deletes
212         int deleteCount = 0;
213         StringBuilder queryBuilder = new StringBuilder()
214                 .append(IconDB.COLUMN_ROWID)
215                 .append(" IN (");
216 
217         int count = mItemsToDelete.size();
218         for (int i = 0;  i < count; i++) {
219             if (mItemsToDelete.valueAt(i)) {
220                 if (deleteCount > 0) {
221                     queryBuilder.append(", ");
222                 }
223                 queryBuilder.append(mItemsToDelete.keyAt(i));
224                 deleteCount++;
225             }
226         }
227         queryBuilder.append(')');
228 
229         if (deleteCount > 0) {
230             mIconCache.mIconDb.delete(queryBuilder.toString(), null);
231         }
232     }
233 
234 
235     /**
236      * A runnable that updates invalid icons and adds missing icons in the DB for the provided
237      * LauncherActivityInfo list. Items are updated/added one at a time, so that the
238      * worker thread doesn't get blocked.
239      */
240     private class SerializedIconUpdateTask<T> implements Runnable {
241         private final long mUserSerial;
242         private final UserHandle mUserHandle;
243         private final Stack<T> mAppsToAdd;
244         private final Stack<T> mAppsToUpdate;
245         private final CachingLogic<T> mCachingLogic;
246         private final HashSet<String> mUpdatedPackages = new HashSet<>();
247         private final OnUpdateCallback mOnUpdateCallback;
248 
SerializedIconUpdateTask(long userSerial, UserHandle userHandle, Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback)249         SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
250                 Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic,
251                 OnUpdateCallback onUpdateCallback) {
252             mUserHandle = userHandle;
253             mUserSerial = userSerial;
254             mAppsToAdd = appsToAdd;
255             mAppsToUpdate = appsToUpdate;
256             mCachingLogic = cachingLogic;
257             mOnUpdateCallback = onUpdateCallback;
258         }
259 
260         @Override
run()261         public void run() {
262             if (!mAppsToUpdate.isEmpty()) {
263                 T app = mAppsToUpdate.pop();
264                 String pkg = mCachingLogic.getComponent(app).getPackageName();
265                 PackageInfo info = mPkgInfoMap.get(pkg);
266                 mIconCache.addIconToDBAndMemCache(
267                         app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
268                 mUpdatedPackages.add(pkg);
269 
270                 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
271                     // No more app to update. Notify callback.
272                     mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
273                 }
274 
275                 // Let it run one more time.
276                 scheduleNext();
277             } else if (!mAppsToAdd.isEmpty()) {
278                 T app = mAppsToAdd.pop();
279                 PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
280                 // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
281                 // app should have package info, this is not guaranteed by the api
282                 if (info != null) {
283                     mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
284                             mUserSerial, false /*replace existing*/);
285                 }
286 
287                 if (!mAppsToAdd.isEmpty()) {
288                     scheduleNext();
289                 }
290             }
291         }
292 
scheduleNext()293         public void scheduleNext() {
294             mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
295                     SystemClock.uptimeMillis() + 1);
296         }
297     }
298 
299     public interface OnUpdateCallback {
300 
onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user)301         void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user);
302     }
303 }
304