• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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