• 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.ContentValues
20 import android.content.Context
21 import android.content.pm.ActivityInfo
22 import android.content.pm.ApplicationInfo
23 import android.content.pm.LauncherApps
24 import android.content.pm.PackageManager
25 import android.content.pm.PackageManager.NameNotFoundException
26 import android.database.Cursor
27 import android.database.sqlite.SQLiteDatabase
28 import android.database.sqlite.SQLiteException
29 import android.database.sqlite.SQLiteReadOnlyDatabaseException
30 import android.graphics.Bitmap
31 import android.graphics.Bitmap.Config.HARDWARE
32 import android.graphics.BitmapFactory
33 import android.graphics.BitmapFactory.Options
34 import android.graphics.drawable.Drawable
35 import android.os.Handler
36 import android.os.Looper
37 import android.os.Trace
38 import android.os.UserHandle
39 import android.text.TextUtils
40 import android.util.Log
41 import android.util.SparseArray
42 import androidx.annotation.VisibleForTesting
43 import androidx.annotation.WorkerThread
44 import com.android.launcher3.Flags
45 import com.android.launcher3.icons.BaseIconFactory
46 import com.android.launcher3.icons.BaseIconFactory.IconOptions
47 import com.android.launcher3.icons.BitmapInfo
48 import com.android.launcher3.icons.GraphicsUtils
49 import com.android.launcher3.icons.IconProvider
50 import com.android.launcher3.icons.SourceHint
51 import com.android.launcher3.icons.cache.CacheLookupFlag.Companion.DEFAULT_LOOKUP_FLAG
52 import com.android.launcher3.util.ComponentKey
53 import com.android.launcher3.util.FlagOp
54 import com.android.launcher3.util.SQLiteCacheHelper
55 import java.util.function.Supplier
56 import kotlin.collections.MutableMap.MutableEntry
57 
58 abstract class BaseIconCache
59 @JvmOverloads
60 constructor(
61     @JvmField protected val context: Context,
62     private val dbFileName: String?,
63     private val bgLooper: Looper,
64     private var iconDpi: Int,
65     iconPixelSize: Int,
66     inMemoryCache: Boolean,
67     val iconProvider: IconProvider = IconProvider(context),
68 ) {
69     class CacheEntry {
70         @JvmField var bitmap: BitmapInfo = BitmapInfo.LOW_RES_INFO
71         @JvmField var title: CharSequence = ""
72         @JvmField var contentDescription: CharSequence = ""
73     }
74 
75     private val packageManager: PackageManager = context.packageManager
76 
77     private val cache: MutableMap<ComponentKey, CacheEntry?> =
78         if (inMemoryCache) {
79             HashMap(INITIAL_ICON_CACHE_CAPACITY)
80         } else {
81             object : AbstractMutableMap<ComponentKey, CacheEntry?>() {
82                 override fun put(key: ComponentKey, value: CacheEntry?): CacheEntry? = value
83 
84                 override val entries: MutableSet<MutableEntry<ComponentKey, CacheEntry?>> =
85                     mutableSetOf()
86             }
87         }
88 
89     val iconUpdateToken = Any()
90 
91     @JvmField val workerHandler = Handler(bgLooper)
92 
93     @JvmField protected var iconDb = IconDB(context, dbFileName, iconPixelSize)
94 
95     private var defaultIcon: BitmapInfo? = null
96     private val userFlagOpMap = SparseArray<FlagOp>()
97     private val userFormatString = SparseArray<String?>()
98 
99     private val appInfoCachingLogic =
100         AppInfoCachingLogic(
101             pm = context.packageManager,
102             instantAppResolver = this::isInstantApp,
103             errorLogger = this::logPersistently,
104         )
105 
106     init {
107         updateSystemState()
108     }
109 
110     /**
111      * Returns the persistable serial number for {@param user}. Subclass should implement proper
112      * caching strategy to avoid making binder call every time.
113      */
114     abstract fun getSerialNumberForUser(user: UserHandle): Long
115 
116     /** Return true if the given app is an instant app and should be badged appropriately. */
117     protected abstract fun isInstantApp(info: ApplicationInfo): Boolean
118 
119     /** Opens and returns an icon factory. The factory is recycled by the caller. */
120     abstract val iconFactory: BaseIconFactory
121 
122     fun updateIconParams(iconDpi: Int, iconPixelSize: Int) =
123         workerHandler.post { updateIconParamsBg(iconDpi, iconPixelSize) }
124 
125     @Synchronized
126     private fun updateIconParamsBg(iconDpi: Int, iconPixelSize: Int) {
127         try {
128             this.iconDpi = iconDpi
129             defaultIcon = null
130             userFlagOpMap.clear()
131             iconDb.clear()
132             iconDb.close()
133             iconDb = IconDB(context, dbFileName, iconPixelSize)
134             cache.clear()
135         } catch (e: SQLiteReadOnlyDatabaseException) {
136             // This is known to happen during repeated backup and restores, if the Launcher is in
137             // restricted mode. When the launcher is loading and the backup restore is being cleared
138             // there can be a conflict where one DB is trying to delete the DB file, and the other
139             // is attempting to write to it. The effect is that launcher crashes, then the backup /
140             // restore process fails, then the user's home screen icons fail to restore. Adding this
141             // try / catch will stop the crash, and LoaderTask will sanitize any residual icon data,
142             // leading to a completed backup / restore and a better experience for our customers.
143             Log.e(TAG, "failed to clear the launcher's icon db or cache.", e)
144         }
145     }
146 
147     fun getFullResIcon(info: ActivityInfo): Drawable? = iconProvider.getIcon(info, iconDpi)
148 
149     /** Remove any records for the supplied ComponentName. */
150     @Synchronized
151     fun remove(componentName: ComponentName, user: UserHandle) =
152         cache.remove(ComponentKey(componentName, user))
153 
154     /** Remove any records for the supplied package name from memory. */
155     private fun removeFromMemCacheLocked(packageName: String, user: UserHandle) =
156         cache.keys.removeIf { it.componentName.packageName == packageName && it.user == user }
157 
158     /** Removes the entries related to the given package in memory and persistent DB. */
159     @Synchronized
160     fun removeIconsForPkg(packageName: String, user: UserHandle) {
161         removeFromMemCacheLocked(packageName, user)
162         iconDb.delete(
163             "$COLUMN_COMPONENT LIKE ? AND $COLUMN_USER = ?",
164             arrayOf("$packageName/%", getSerialNumberForUser(user).toString()),
165         )
166     }
167 
168     fun getUpdateHandler(): IconCacheUpdateHandler {
169         updateSystemState()
170         // Remove all active icon update tasks.
171         workerHandler.removeCallbacksAndMessages(iconUpdateToken)
172         return IconCacheUpdateHandler(this, iconDb, workerHandler)
173     }
174 
175     /**
176      * Refreshes the system state definition used to check the validity of the cache. It
177      * incorporates all the properties that can affect the cache like the list of enabled locale and
178      * system-version.
179      */
180     private fun updateSystemState() {
181         iconProvider.updateSystemState()
182         userFormatString.clear()
183     }
184 
185     fun getUserBadgedLabel(label: CharSequence, user: UserHandle): CharSequence {
186         val key = user.hashCode()
187         val index = userFormatString.indexOfKey(key)
188         var format: String?
189         if (index < 0) {
190             format = packageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString()
191             if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
192                 format = null
193             }
194             userFormatString.put(key, format)
195         } else {
196             format = userFormatString.valueAt(index)
197         }
198         return if (format == null) label else String.format(format, label)
199     }
200 
201     /**
202      * Adds/updates an entry into the DB and the in-memory cache. The update is skipped if the entry
203      * fails to load
204      */
205     @Synchronized
206     fun <T : Any> addIconToDBAndMemCache(obj: T, cachingLogic: CachingLogic<T>, userSerial: Long) {
207         val user = cachingLogic.getUser(obj)
208         val componentName = cachingLogic.getComponent(obj)
209         val key = ComponentKey(componentName, user)
210         val bitmapInfo = cachingLogic.loadIcon(context, this, obj)
211 
212         // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
213         // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
214         // an empty entry.
215         if (bitmapInfo.isNullOrLowRes || isDefaultIcon(bitmapInfo, user)) {
216             return
217         }
218         val entryTitle =
219             cachingLogic.getLabel(obj).let {
220                 if (it.isNullOrEmpty()) componentName.packageName else it
221             }
222 
223         // Only add an entry in memory, if there was already something previously
224         if (cache[key] != null) {
225             val entry = CacheEntry()
226             entry.bitmap = bitmapInfo
227             entry.title = entryTitle
228             entry.contentDescription = getUserBadgedLabel(entryTitle, user)
229             cache[key] = entry
230         }
231 
232         val freshnessId = cachingLogic.getFreshnessIdentifier(obj, iconProvider)
233         if (freshnessId != null) {
234             addOrUpdateCacheDbEntry(bitmapInfo, entryTitle, componentName, userSerial, freshnessId)
235         }
236     }
237 
238     @Synchronized
239     fun getDefaultIcon(user: UserHandle): BitmapInfo {
240         if (defaultIcon == null) {
241             iconFactory.use { li -> defaultIcon = li.makeDefaultIcon(iconProvider) }
242         }
243         return defaultIcon!!.withFlags(getUserFlagOpLocked(user))
244     }
245 
246     protected fun getUserFlagOpLocked(user: UserHandle): FlagOp {
247         val key = user.hashCode()
248         val index = userFlagOpMap.indexOfKey(key)
249         if (index >= 0) {
250             return userFlagOpMap.valueAt(index)
251         } else {
252             iconFactory.use { li ->
253                 val op = li.getBitmapFlagOp(IconOptions().setUser(user))
254                 userFlagOpMap.put(key, op)
255                 return op
256             }
257         }
258     }
259 
260     fun isDefaultIcon(icon: BitmapInfo, user: UserHandle) = getDefaultIcon(user).icon == icon.icon
261 
262     /**
263      * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. This
264      * method is not thread safe, it must be called from a synchronized method.
265      */
266     @JvmOverloads
267     protected fun <T : Any> cacheLocked(
268         componentName: ComponentName,
269         user: UserHandle,
270         infoProvider: Supplier<T?>,
271         cachingLogic: CachingLogic<T>,
272         lookupFlags: CacheLookupFlag,
273         cursor: Cursor? = null,
274     ): CacheEntry {
275         assertWorkerThread()
276         val cacheKey = ComponentKey(componentName, user)
277         var entry = cache[cacheKey]
278         if (entry == null || entry.bitmap.matchingLookupFlag.isVisuallyLessThan(lookupFlags)) {
279             val addToMemCache = entry != null || !lookupFlags.skipAddToMemCache()
280             entry = CacheEntry()
281             if (addToMemCache) cache[cacheKey] = entry
282             // Check the DB first.
283             val cacheEntryUpdated =
284                 if (cursor == null) getEntryFromDBLocked(cacheKey, entry, lookupFlags, cachingLogic)
285                 else updateTitleAndIconLocked(cacheKey, entry, cursor, lookupFlags, cachingLogic)
286 
287             val obj: T? by lazy { infoProvider.get() }
288             if (!cacheEntryUpdated) {
289                 loadFallbackIcon(
290                     obj,
291                     entry,
292                     cachingLogic,
293                     lookupFlags.usePackageIcon(),
294                     /* usePackageTitle= */ true,
295                     componentName,
296                     user,
297                 )
298             }
299 
300             if (TextUtils.isEmpty(entry.title)) {
301                 obj?.let { loadFallbackTitle(it, entry, cachingLogic, user) }
302             }
303         }
304         return entry
305     }
306 
307     /** Fallback method for loading an icon bitmap. */
308     protected fun <T : Any> loadFallbackIcon(
309         obj: T?,
310         entry: CacheEntry,
311         cachingLogic: CachingLogic<T>,
312         usePackageIcon: Boolean,
313         usePackageTitle: Boolean,
314         componentName: ComponentName,
315         user: UserHandle,
316     ) {
317         if (obj != null) {
318             entry.bitmap = cachingLogic.loadIcon(context, this, obj)
319         } else {
320             if (usePackageIcon) {
321                 val packageEntry = getEntryForPackageLocked(componentName.packageName, user)
322                 if (DEBUG) {
323                     Log.d(TAG, "using package default icon for " + componentName.toShortString())
324                 }
325                 entry.bitmap = packageEntry.bitmap
326                 entry.contentDescription = packageEntry.contentDescription
327 
328                 if (usePackageTitle) {
329                     entry.title = packageEntry.title
330                 }
331             }
332         }
333     }
334 
335     /** Fallback method for loading an app title. */
336     protected fun <T : Any> loadFallbackTitle(
337         obj: T,
338         entry: CacheEntry,
339         cachingLogic: CachingLogic<T>,
340         user: UserHandle,
341     ) {
342         entry.title =
343             cachingLogic.getLabel(obj).let {
344                 if (it.isNullOrEmpty()) cachingLogic.getComponent(obj).packageName else it
345             }
346         entry.contentDescription = getUserBadgedLabel(entry.title, user)
347     }
348 
349     @Synchronized
350     fun clearMemoryCache() {
351         assertWorkerThread()
352         cache.clear()
353     }
354 
355     /**
356      * Adds a default package entry in the cache. This entry is not persisted and will be removed
357      * when the cache is flushed.
358      */
359     @Synchronized
360     protected fun cachePackageInstallInfo(
361         packageName: String,
362         user: UserHandle,
363         icon: Bitmap?,
364         title: CharSequence?,
365     ) {
366         removeFromMemCacheLocked(packageName, user)
367         val cacheKey = getPackageKey(packageName, user)
368 
369         // For icon caching, do not go through DB. Just update the in-memory entry.
370         val entry = cache[cacheKey] ?: CacheEntry()
371         if (!title.isNullOrEmpty()) {
372             entry.title = title
373         }
374 
375         if (icon != null) {
376             iconFactory.use { li ->
377                 entry.bitmap =
378                     li.createBadgedIconBitmap(
379                         li.createShapedAdaptiveIcon(icon),
380                         IconOptions().setUser(user),
381                     )
382             }
383         }
384         if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
385             cache[cacheKey] = entry
386         }
387     }
388 
389     /** Returns the package entry if it has already been cached in memory, null otherwise */
390     protected fun getInMemoryPackageEntryLocked(
391         packageName: String,
392         user: UserHandle,
393     ): CacheEntry? = getInMemoryEntryLocked(getPackageKey(packageName, user))
394 
395     @VisibleForTesting
396     fun getInMemoryEntryLocked(key: ComponentKey): CacheEntry? {
397         assertWorkerThread()
398         return cache[key]
399     }
400 
401     /**
402      * Gets an entry for the package, which can be used as a fallback entry for various components.
403      * This method is not thread safe, it must be called from a synchronized method.
404      */
405     @WorkerThread
406     protected fun getEntryForPackageLocked(
407         packageName: String,
408         user: UserHandle,
409         lookupFlags: CacheLookupFlag = DEFAULT_LOOKUP_FLAG,
410     ): CacheEntry {
411         assertWorkerThread()
412         val cacheKey = getPackageKey(packageName, user)
413         var entry = cache[cacheKey]
414 
415         if (entry == null || entry.bitmap.matchingLookupFlag.isVisuallyLessThan(lookupFlags)) {
416             entry = CacheEntry()
417             var entryUpdated = true
418 
419             // Check the DB first.
420             if (!getEntryFromDBLocked(cacheKey, entry, lookupFlags, appInfoCachingLogic)) {
421                 try {
422                     val appInfo =
423                         context
424                             .getSystemService(LauncherApps::class.java)!!
425                             .getApplicationInfo(
426                                 packageName,
427                                 PackageManager.MATCH_UNINSTALLED_PACKAGES,
428                                 user,
429                             )
430                     if (appInfo == null) {
431                         throw NameNotFoundException("ApplicationInfo is null").also {
432                             logPersistently(
433                                 String.format("ApplicationInfo is null for %s", packageName),
434                                 it,
435                             )
436                         }
437                     }
438 
439                     // Load the full res icon for the application, but if useLowResIcon is set, then
440                     // only keep the low resolution icon instead of the larger full-sized icon
441                     val iconInfo = appInfoCachingLogic.loadIcon(context, this, appInfo)
442                     entry.bitmap =
443                         if (lookupFlags.useLowRes())
444                             BitmapInfo.of(BitmapInfo.LOW_RES_ICON, iconInfo.color)
445                         else iconInfo
446 
447                     loadFallbackTitle(appInfo, entry, appInfoCachingLogic, user)
448 
449                     // Add the icon in the DB here, since these do not get written during
450                     // package updates.
451                     appInfoCachingLogic.getFreshnessIdentifier(appInfo, iconProvider)?.let {
452                         freshnessId ->
453                         addOrUpdateCacheDbEntry(
454                             iconInfo,
455                             entry.title,
456                             cacheKey.componentName,
457                             getSerialNumberForUser(user),
458                             freshnessId,
459                         )
460                     }
461                 } catch (e: NameNotFoundException) {
462                     if (DEBUG) Log.d(TAG, "Application not installed $packageName")
463                     entryUpdated = false
464                 }
465             }
466 
467             val shouldAddToCache =
468                 !(lookupFlags.skipAddToMemCache() && Flags.restoreArchivedAppIconsFromDb())
469             // Only add a filled-out entry to the cache
470             if (entryUpdated && shouldAddToCache) {
471                 cache[cacheKey] = entry
472             }
473         }
474         return entry
475     }
476 
477     protected fun getEntryFromDBLocked(
478         cacheKey: ComponentKey,
479         entry: CacheEntry,
480         lookupFlags: CacheLookupFlag,
481         cachingLogic: CachingLogic<*>,
482     ): Boolean {
483         var c: Cursor? = null
484         Trace.beginSection("loadIconIndividually")
485         try {
486             c =
487                 iconDb.query(
488                     lookupFlags.toLookupColumns(),
489                     "$COLUMN_COMPONENT = ? AND $COLUMN_USER = ?",
490                     arrayOf(
491                         cacheKey.componentName.flattenToString(),
492                         getSerialNumberForUser(cacheKey.user).toString(),
493                     ),
494                 )
495             if (c.moveToNext()) {
496                 return updateTitleAndIconLocked(cacheKey, entry, c, lookupFlags, cachingLogic)
497             }
498         } catch (e: SQLiteException) {
499             Log.d(TAG, "Error reading icon cache", e)
500         } finally {
501             c?.close()
502             Trace.endSection()
503         }
504         return false
505     }
506 
507     private fun updateTitleAndIconLocked(
508         cacheKey: ComponentKey,
509         entry: CacheEntry,
510         c: Cursor,
511         lookupFlags: CacheLookupFlag,
512         logic: CachingLogic<*>,
513     ): Boolean {
514         // Set the alpha to be 255, so that we never have a wrong color
515         entry.bitmap =
516             BitmapInfo.of(
517                 BitmapInfo.LOW_RES_ICON,
518                 GraphicsUtils.setColorAlphaBound(c.getInt(INDEX_COLOR), 255),
519             )
520         c.getString(INDEX_TITLE).let {
521             if (it.isNullOrEmpty()) {
522                 entry.title = ""
523                 entry.contentDescription = ""
524             } else {
525                 entry.title = it
526                 entry.contentDescription = getUserBadgedLabel(it, cacheKey.user)
527             }
528         }
529 
530         if (!lookupFlags.useLowRes()) {
531             try {
532                 val data: ByteArray = c.getBlob(INDEX_ICON) ?: return false
533                 entry.bitmap =
534                     BitmapInfo.of(
535                         BitmapFactory.decodeByteArray(
536                             data,
537                             0,
538                             data.size,
539                             Options().apply { inPreferredConfig = HARDWARE },
540                         )!!,
541                         entry.bitmap.color,
542                     )
543             } catch (e: Exception) {
544                 return false
545             }
546 
547             iconFactory.use { factory ->
548                 val themeController = factory.themeController
549                 val monoIconData = c.getBlob(INDEX_MONO_ICON)
550                 if (themeController != null && monoIconData != null) {
551                     entry.bitmap.themedBitmap =
552                         themeController.decode(
553                             data = monoIconData,
554                             info = entry.bitmap,
555                             factory = factory,
556                             sourceHint =
557                                 SourceHint(cacheKey, logic, c.getString(INDEX_FRESHNESS_ID)),
558                         )
559                 }
560             }
561         }
562         entry.bitmap.flags = c.getInt(INDEX_FLAGS)
563         entry.bitmap = entry.bitmap.withFlags(getUserFlagOpLocked(cacheKey.user))
564         return true
565     }
566 
567     private fun addOrUpdateCacheDbEntry(
568         bitmapInfo: BitmapInfo,
569         label: CharSequence,
570         key: ComponentName,
571         userSerial: Long,
572         freshnessId: String,
573     ) {
574         val values = ContentValues()
575         if (bitmapInfo.canPersist()) {
576             values.put(COLUMN_ICON, GraphicsUtils.flattenBitmap(bitmapInfo.icon))
577             values.put(COLUMN_MONO_ICON, bitmapInfo.themedBitmap?.serialize())
578         } else {
579             values.put(COLUMN_ICON, null as ByteArray?)
580             values.put(COLUMN_MONO_ICON, null as ByteArray?)
581         }
582 
583         values.put(COLUMN_ICON_COLOR, bitmapInfo.color)
584         values.put(COLUMN_FLAGS, bitmapInfo.flags)
585         values.put(COLUMN_LABEL, label.toString())
586 
587         values.put(COLUMN_COMPONENT, key.flattenToString())
588         values.put(COLUMN_USER, userSerial)
589         values.put(COLUMN_FRESHNESS_ID, freshnessId)
590         iconDb.insertOrReplace(values)
591     }
592 
593     private fun assertWorkerThread() {
594         check(Looper.myLooper() == bgLooper) {
595             "Cache accessed on wrong thread " + Looper.myLooper()
596         }
597     }
598 
599     /** Log to Log.d. Subclasses can override this method to log persistently for debugging. */
600     protected open fun logPersistently(message: String, e: Exception?) {
601         Log.d(TAG, message, e)
602     }
603 
604     /** Cache class to store the actual entries on disk */
605     class IconDB(context: Context, dbFileName: String?, iconPixelSize: Int) :
606         SQLiteCacheHelper(
607             context,
608             dbFileName,
609             (RELEASE_VERSION shl 16) + iconPixelSize,
610             TABLE_NAME,
611         ) {
612 
613         override fun onCreateTable(db: SQLiteDatabase) {
614             db.execSQL(
615                 ("CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
616                     "$COLUMN_COMPONENT TEXT NOT NULL, " +
617                     "$COLUMN_USER INTEGER NOT NULL, " +
618                     "$COLUMN_FRESHNESS_ID TEXT, " +
619                     "$COLUMN_ICON BLOB, " +
620                     "$COLUMN_MONO_ICON BLOB, " +
621                     "$COLUMN_ICON_COLOR INTEGER NOT NULL DEFAULT 0, " +
622                     "$COLUMN_FLAGS INTEGER NOT NULL DEFAULT 0, " +
623                     "$COLUMN_LABEL TEXT, " +
624                     "PRIMARY KEY ($COLUMN_COMPONENT, $COLUMN_USER) " +
625                     ");")
626             )
627         }
628     }
629 
630     companion object {
631         protected const val TAG = "BaseIconCache"
632         private const val DEBUG = false
633 
634         private const val INITIAL_ICON_CACHE_CAPACITY = 50
635 
636         // A format string which returns the original string as is.
637         private const val IDENTITY_FORMAT_STRING = "%1\$s"
638 
639         // Empty class name is used for storing package default entry.
640         const val EMPTY_CLASS_NAME: String = "."
641 
642         fun getPackageKey(packageName: String, user: UserHandle) =
643             ComponentKey(ComponentName(packageName, packageName + EMPTY_CLASS_NAME), user)
644 
645         // Ensures themed bitmaps in the icon cache are invalidated
646         @JvmField val RELEASE_VERSION = if (Flags.forceMonochromeAppIcons()) 10 else 9
647 
648         @JvmField val TABLE_NAME = "icons"
649         @JvmField val COLUMN_ROWID = "rowid"
650         @JvmField val COLUMN_COMPONENT = "componentName"
651         @JvmField val COLUMN_USER = "profileId"
652         @JvmField val COLUMN_FRESHNESS_ID = "freshnessId"
653         @JvmField val COLUMN_ICON = "icon"
654         @JvmField val COLUMN_ICON_COLOR = "icon_color"
655         @JvmField val COLUMN_MONO_ICON = "mono_icon"
656         @JvmField val COLUMN_FLAGS = "flags"
657         @JvmField val COLUMN_LABEL = "label"
658 
659         @JvmField
660         val COLUMNS_LOW_RES =
661             arrayOf(COLUMN_COMPONENT, COLUMN_LABEL, COLUMN_ICON_COLOR, COLUMN_FLAGS)
662 
663         @JvmField
664         val COLUMNS_HIGH_RES =
665             COLUMNS_LOW_RES.copyOf(COLUMNS_LOW_RES.size + 3).apply {
666                 this[size - 3] = COLUMN_ICON
667                 this[size - 2] = COLUMN_MONO_ICON
668                 this[size - 1] = COLUMN_FRESHNESS_ID
669             }
670 
671         @JvmField val INDEX_TITLE = COLUMNS_HIGH_RES.indexOf(COLUMN_LABEL)
672         @JvmField val INDEX_COLOR = COLUMNS_HIGH_RES.indexOf(COLUMN_ICON_COLOR)
673         @JvmField val INDEX_FLAGS = COLUMNS_HIGH_RES.indexOf(COLUMN_FLAGS)
674         @JvmField val INDEX_ICON = COLUMNS_HIGH_RES.indexOf(COLUMN_ICON)
675         @JvmField val INDEX_MONO_ICON = COLUMNS_HIGH_RES.indexOf(COLUMN_MONO_ICON)
676         @JvmField val INDEX_FRESHNESS_ID = COLUMNS_HIGH_RES.indexOf(COLUMN_FRESHNESS_ID)
677 
678         @JvmStatic
679         fun CacheLookupFlag.toLookupColumns() =
680             if (useLowRes()) COLUMNS_LOW_RES else COLUMNS_HIGH_RES
681     }
682 }
683