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