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