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