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