1 /* <lambda>null2 * 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.database.sqlite.SQLiteException 21 import android.os.Handler 22 import android.os.SystemClock 23 import android.os.UserHandle 24 import android.util.ArrayMap 25 import android.util.Log 26 import com.android.launcher3.util.ComponentKey 27 import com.android.launcher3.util.SQLiteCacheHelper 28 import java.util.ArrayDeque 29 30 /** Utility class to handle updating the Icon cache */ 31 class IconCacheUpdateHandler( 32 private val iconCache: BaseIconCache, 33 private val cacheDb: SQLiteCacheHelper, 34 private val workerHandler: Handler, 35 ) { 36 37 private val packagesToIgnore = ArrayMap<UserHandle, MutableSet<String>>() 38 // Map of packageKey to ApplicationInfo, dynamically created based on all incoming data 39 private val packageAppInfoMap = HashMap<ComponentKey, ApplicationInfo?>() 40 41 private val itemsToDelete = HashSet<UpdateRow>() 42 43 // During the first pass, we load all the items from DB and add all invalid items to 44 // mItemsToDelete. In follow up passes, we go through the items in mItemsToDelete, and if the 45 // item is valid, removes it from the list, or leave it there. 46 private var firstPass = true 47 48 /** Sets a package to ignore for processing */ 49 fun addPackagesToIgnore(userHandle: UserHandle, packageName: String) { 50 packagesToIgnore.getOrPut(userHandle) { HashSet() }.add(packageName) 51 } 52 53 /** 54 * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in 55 * the DB and are updated. 56 * 57 * @return The set of packages for which icons have updated. 58 */ 59 fun <T : Any> updateIcons( 60 apps: List<T>, 61 cachingLogic: CachingLogic<T>, 62 onUpdateCallback: OnUpdateCallback, 63 ) { 64 // Filter the list per user 65 val userComponentMap = HashMap<UserHandle, HashMap<ComponentName, T>>() 66 for (app in apps) { 67 val userHandle = cachingLogic.getUser(app) 68 val cn = cachingLogic.getComponent(app) 69 userComponentMap.getOrPut(userHandle) { HashMap() }[cn] = app 70 71 // Populate application info map 72 val packageKey = BaseIconCache.getPackageKey(cn.packageName, userHandle) 73 packageAppInfoMap.getOrPut(packageKey) { cachingLogic.getApplicationInfo(app) } 74 } 75 76 if (firstPass) { 77 userComponentMap.forEach { (user, componentMap) -> 78 updateIconsPerUserForFirstPass(user, componentMap, cachingLogic, onUpdateCallback) 79 } 80 } else { 81 userComponentMap.forEach { (user, componentMap) -> 82 updateIconsPerUserForSecondPass(user, componentMap, cachingLogic, onUpdateCallback) 83 } 84 } 85 86 // From now on, clear every valid item from the global valid map. 87 firstPass = false 88 } 89 90 /** 91 * During the first pass, all the items from the cache are verified one-by-one and any entry 92 * with no corresponding entry in {@code componentMap} is added to {@code itemsToDelete} 93 * 94 * Also starts a SerializedIconUpdateTask for all updated entries 95 */ 96 private fun <T : Any> updateIconsPerUserForFirstPass( 97 user: UserHandle, 98 componentMap: MutableMap<ComponentName, T>, 99 cachingLogic: CachingLogic<T>, 100 onUpdateCallback: OnUpdateCallback, 101 ) { 102 val appsToUpdate = ArrayDeque<T>() 103 104 val userSerial = iconCache.getSerialNumberForUser(user) 105 try { 106 cacheDb 107 .query( 108 arrayOf( 109 BaseIconCache.COLUMN_ROWID, 110 BaseIconCache.COLUMN_COMPONENT, 111 BaseIconCache.COLUMN_FRESHNESS_ID, 112 ), 113 "${BaseIconCache.COLUMN_USER} = ? ", 114 arrayOf(userSerial.toString()), 115 ) 116 .use { c -> 117 var ignorePackages = packagesToIgnore[user] ?: emptySet() 118 119 val indexComponent = c.getColumnIndex(BaseIconCache.COLUMN_COMPONENT) 120 val indexFreshnessId = c.getColumnIndex(BaseIconCache.COLUMN_FRESHNESS_ID) 121 val rowIndex = c.getColumnIndex(BaseIconCache.COLUMN_ROWID) 122 123 while (c.moveToNext()) { 124 val rowId = c.getInt(rowIndex) 125 val cn = c.getString(indexComponent) 126 val freshnessId = c.getString(indexFreshnessId) ?: "" 127 128 val component = ComponentName.unflattenFromString(cn) 129 if (component == null) { 130 // b/357725795 131 Log.e(TAG, "Invalid component name while updating icon cache: $cn") 132 itemsToDelete.add( 133 UpdateRow(rowId, ComponentName("", ""), user, freshnessId) 134 ) 135 continue 136 } 137 138 val app = componentMap.remove(component) 139 if (app == null) { 140 if (!ignorePackages.contains(component.packageName)) { 141 iconCache.remove(component, user) 142 itemsToDelete.add(UpdateRow(rowId, component, user, freshnessId)) 143 } 144 continue 145 } 146 147 if ( 148 freshnessId == 149 cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider) 150 ) { 151 // Item is up-to-date 152 continue 153 } 154 appsToUpdate.add(app) 155 } 156 } 157 } catch (e: SQLiteException) { 158 Log.d(TAG, "Error reading icon cache", e) 159 // Continue updating whatever we have read so far 160 } 161 162 // Insert remaining apps. 163 if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) { 164 val appsToAdd = ArrayDeque(componentMap.values) 165 SerializedIconUpdateTask( 166 userSerial, 167 user, 168 appsToAdd, 169 appsToUpdate, 170 cachingLogic, 171 onUpdateCallback, 172 ) 173 .scheduleNext() 174 } 175 } 176 177 /** 178 * During the second pass, we go through the items in {@code itemsToDelete}, and remove any item 179 * with corresponding entry in {@code componentMap}. 180 */ 181 private fun <T : Any> updateIconsPerUserForSecondPass( 182 user: UserHandle, 183 componentMap: MutableMap<ComponentName, T>, 184 cachingLogic: CachingLogic<T>, 185 onUpdateCallback: OnUpdateCallback, 186 ) { 187 val userSerial = iconCache.getSerialNumberForUser(user) 188 val appsToUpdate = ArrayDeque<T>() 189 190 val itr = itemsToDelete.iterator() 191 while (itr.hasNext()) { 192 val row = itr.next() 193 if (user != row.user) continue 194 val app = componentMap.remove(row.componentName) ?: continue 195 196 itr.remove() 197 if ( 198 row.freshnessId != cachingLogic.getFreshnessIdentifier(app, iconCache.iconProvider) 199 ) { 200 appsToUpdate.add(app) 201 } 202 } 203 204 // Insert remaining apps. 205 if (componentMap.isNotEmpty() || appsToUpdate.isNotEmpty()) { 206 val appsToAdd = ArrayDeque<T>() 207 appsToAdd.addAll(componentMap.values) 208 SerializedIconUpdateTask( 209 userSerial, 210 user, 211 appsToAdd, 212 appsToUpdate, 213 cachingLogic, 214 onUpdateCallback, 215 ) 216 .scheduleNext() 217 } 218 } 219 220 /** 221 * Commits all updates as part of the update handler to disk. Not more calls should be made to 222 * this class after this. 223 */ 224 fun finish() { 225 // Ignore any application info entries which are already correct 226 itemsToDelete.removeIf { row -> 227 val info = packageAppInfoMap[ComponentKey(row.componentName, row.user)] 228 info != null && row.freshnessId == iconCache.iconProvider.getStateForApp(info) 229 } 230 231 // Commit all deletes 232 if (itemsToDelete.isNotEmpty()) { 233 val r = itemsToDelete.joinToString { it.rowId.toString() } 234 cacheDb.delete("${BaseIconCache.COLUMN_ROWID} IN ($r)", null) 235 Log.d(TAG, "Deleting obsolete entries, count=" + itemsToDelete.size) 236 } 237 } 238 239 data class UpdateRow( 240 val rowId: Int, 241 val componentName: ComponentName, 242 val user: UserHandle, 243 val freshnessId: String, 244 ) 245 246 /** 247 * A runnable that updates invalid icons and adds missing icons in the DB for the provided 248 * LauncherActivityInfo list. Items are updated/added one at a time, so that the worker thread 249 * doesn't get blocked. 250 */ 251 private inner class SerializedIconUpdateTask<T : Any>( 252 private val userSerial: Long, 253 private val userHandle: UserHandle, 254 private val appsToAdd: ArrayDeque<T>, 255 private val appsToUpdate: ArrayDeque<T>, 256 private val cachingLogic: CachingLogic<T>, 257 private val onUpdateCallback: OnUpdateCallback, 258 ) : Runnable { 259 private val updatedPackages = HashSet<String>() 260 261 override fun run() { 262 if (appsToUpdate.isNotEmpty()) { 263 val app = appsToUpdate.removeLast() 264 val pkg = cachingLogic.getComponent(app).packageName 265 266 iconCache.addIconToDBAndMemCache(app, cachingLogic, userSerial) 267 updatedPackages.add(pkg) 268 269 if (appsToUpdate.isEmpty() && updatedPackages.isNotEmpty()) { 270 // No more app to update. Notify callback. 271 onUpdateCallback.onPackageIconsUpdated(updatedPackages, userHandle) 272 } 273 274 // Let it run one more time. 275 scheduleNext() 276 } else if (appsToAdd.isNotEmpty()) { 277 iconCache.addIconToDBAndMemCache(appsToAdd.removeLast(), cachingLogic, userSerial) 278 279 // Let it run one more time. 280 scheduleNext() 281 } 282 } 283 284 fun scheduleNext() { 285 workerHandler.postAtTime( 286 this, 287 iconCache.iconUpdateToken, 288 SystemClock.uptimeMillis() + 1, 289 ) 290 } 291 } 292 293 fun interface OnUpdateCallback { 294 fun onPackageIconsUpdated(updatedPackages: HashSet<String>, user: UserHandle) 295 } 296 297 companion object { 298 private const val TAG = "IconCacheUpdateHandler" 299 } 300 } 301