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