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