1 /* 2 * Copyright (C) 2008 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 17 package com.android.launcher3; 18 19 import android.content.ComponentName; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ActivityInfo; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.res.Resources; 29 import android.database.Cursor; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteOpenHelper; 32 import android.graphics.Bitmap; 33 import android.graphics.BitmapFactory; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.Paint; 37 import android.graphics.Rect; 38 import android.graphics.drawable.Drawable; 39 import android.os.Handler; 40 import android.os.SystemClock; 41 import android.text.TextUtils; 42 import android.util.Log; 43 44 import com.android.launcher3.compat.LauncherActivityInfoCompat; 45 import com.android.launcher3.compat.LauncherAppsCompat; 46 import com.android.launcher3.compat.UserHandleCompat; 47 import com.android.launcher3.compat.UserManagerCompat; 48 import com.android.launcher3.model.PackageItemInfo; 49 import com.android.launcher3.util.ComponentKey; 50 import com.android.launcher3.util.Thunk; 51 52 import java.util.Collections; 53 import java.util.HashMap; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Locale; 57 import java.util.Set; 58 import java.util.Stack; 59 60 /** 61 * Cache of application icons. Icons can be made from any thread. 62 */ 63 public class IconCache { 64 65 private static final String TAG = "Launcher.IconCache"; 66 67 private static final int INITIAL_ICON_CACHE_CAPACITY = 50; 68 69 // Empty class name is used for storing package default entry. 70 private static final String EMPTY_CLASS_NAME = "."; 71 72 private static final boolean DEBUG = false; 73 74 private static final int LOW_RES_SCALE_FACTOR = 5; 75 76 @Thunk static final Object ICON_UPDATE_TOKEN = new Object(); 77 78 @Thunk static class CacheEntry { 79 public Bitmap icon; 80 public CharSequence title = ""; 81 public CharSequence contentDescription = ""; 82 public boolean isLowResIcon; 83 } 84 85 private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>(); 86 @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); 87 88 private final Context mContext; 89 private final PackageManager mPackageManager; 90 @Thunk final UserManagerCompat mUserManager; 91 private final LauncherAppsCompat mLauncherApps; 92 private final HashMap<ComponentKey, CacheEntry> mCache = 93 new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY); 94 private final int mIconDpi; 95 @Thunk final IconDB mIconDb; 96 97 @Thunk final Handler mWorkerHandler; 98 99 // The background color used for activity icons. Since these icons are displayed in all-apps 100 // and folders, this would be same as the light quantum panel background. This color 101 // is used to convert icons to RGB_565. 102 private final int mActivityBgColor; 103 // The background color used for package icons. These are displayed in widget tray, which 104 // has a dark quantum panel background. 105 private final int mPackageBgColor; 106 private final BitmapFactory.Options mLowResOptions; 107 108 private String mSystemState; 109 private Bitmap mLowResBitmap; 110 private Canvas mLowResCanvas; 111 private Paint mLowResPaint; 112 113 public IconCache(Context context, InvariantDeviceProfile inv) { 114 mContext = context; 115 mPackageManager = context.getPackageManager(); 116 mUserManager = UserManagerCompat.getInstance(mContext); 117 mLauncherApps = LauncherAppsCompat.getInstance(mContext); 118 mIconDpi = inv.fillResIconDpi; 119 mIconDb = new IconDB(context); 120 121 mWorkerHandler = new Handler(LauncherModel.getWorkerLooper()); 122 123 mActivityBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color); 124 mPackageBgColor = context.getResources().getColor(R.color.quantum_panel_bg_color_dark); 125 mLowResOptions = new BitmapFactory.Options(); 126 // Always prefer RGB_565 config for low res. If the bitmap has transparency, it will 127 // automatically be loaded as ALPHA_8888. 128 mLowResOptions.inPreferredConfig = Bitmap.Config.RGB_565; 129 updateSystemStateString(); 130 } 131 132 private Drawable getFullResDefaultActivityIcon() { 133 return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); 134 } 135 136 private Drawable getFullResIcon(Resources resources, int iconId) { 137 Drawable d; 138 try { 139 d = resources.getDrawableForDensity(iconId, mIconDpi); 140 } catch (Resources.NotFoundException e) { 141 d = null; 142 } 143 144 return (d != null) ? d : getFullResDefaultActivityIcon(); 145 } 146 147 public Drawable getFullResIcon(String packageName, int iconId) { 148 Resources resources; 149 try { 150 resources = mPackageManager.getResourcesForApplication(packageName); 151 } catch (PackageManager.NameNotFoundException e) { 152 resources = null; 153 } 154 if (resources != null) { 155 if (iconId != 0) { 156 return getFullResIcon(resources, iconId); 157 } 158 } 159 return getFullResDefaultActivityIcon(); 160 } 161 162 public Drawable getFullResIcon(ActivityInfo info) { 163 Resources resources; 164 try { 165 resources = mPackageManager.getResourcesForApplication( 166 info.applicationInfo); 167 } catch (PackageManager.NameNotFoundException e) { 168 resources = null; 169 } 170 if (resources != null) { 171 int iconId = info.getIconResource(); 172 if (iconId != 0) { 173 return getFullResIcon(resources, iconId); 174 } 175 } 176 177 return getFullResDefaultActivityIcon(); 178 } 179 180 private Bitmap makeDefaultIcon(UserHandleCompat user) { 181 Drawable unbadged = getFullResDefaultActivityIcon(); 182 Drawable d = mUserManager.getBadgedDrawableForUser(unbadged, user); 183 Bitmap b = Bitmap.createBitmap(Math.max(d.getIntrinsicWidth(), 1), 184 Math.max(d.getIntrinsicHeight(), 1), 185 Bitmap.Config.ARGB_8888); 186 Canvas c = new Canvas(b); 187 d.setBounds(0, 0, b.getWidth(), b.getHeight()); 188 d.draw(c); 189 c.setBitmap(null); 190 return b; 191 } 192 193 /** 194 * Remove any records for the supplied ComponentName. 195 */ 196 public synchronized void remove(ComponentName componentName, UserHandleCompat user) { 197 mCache.remove(new ComponentKey(componentName, user)); 198 } 199 200 /** 201 * Remove any records for the supplied package name from memory. 202 */ 203 private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) { 204 HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>(); 205 for (ComponentKey key: mCache.keySet()) { 206 if (key.componentName.getPackageName().equals(packageName) 207 && key.user.equals(user)) { 208 forDeletion.add(key); 209 } 210 } 211 for (ComponentKey condemned: forDeletion) { 212 mCache.remove(condemned); 213 } 214 } 215 216 /** 217 * Updates the entries related to the given package in memory and persistent DB. 218 */ 219 public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) { 220 removeIconsForPkg(packageName, user); 221 try { 222 PackageInfo info = mPackageManager.getPackageInfo(packageName, 223 PackageManager.GET_UNINSTALLED_PACKAGES); 224 long userSerial = mUserManager.getSerialNumberForUser(user); 225 for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) { 226 addIconToDBAndMemCache(app, info, userSerial); 227 } 228 } catch (NameNotFoundException e) { 229 Log.d(TAG, "Package not found", e); 230 return; 231 } 232 } 233 234 /** 235 * Removes the entries related to the given package in memory and persistent DB. 236 */ 237 public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) { 238 removeFromMemCacheLocked(packageName, user); 239 long userSerial = mUserManager.getSerialNumberForUser(user); 240 mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, 241 IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?", 242 new String[] {packageName + "/%", Long.toString(userSerial)}); 243 } 244 245 public void updateDbIcons(Set<String> ignorePackagesForMainUser) { 246 // Remove all active icon update tasks. 247 mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN); 248 249 updateSystemStateString(); 250 for (UserHandleCompat user : mUserManager.getUserProfiles()) { 251 // Query for the set of apps 252 final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user); 253 // Fail if we don't have any apps 254 // TODO: Fix this. Only fail for the current user. 255 if (apps == null || apps.isEmpty()) { 256 return; 257 } 258 259 // Update icon cache. This happens in segments and {@link #onPackageIconsUpdated} 260 // is called by the icon cache when the job is complete. 261 updateDBIcons(user, apps, UserHandleCompat.myUserHandle().equals(user) 262 ? ignorePackagesForMainUser : Collections.<String>emptySet()); 263 } 264 } 265 266 /** 267 * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in 268 * the DB and are updated. 269 * @return The set of packages for which icons have updated. 270 */ 271 private void updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps, 272 Set<String> ignorePackages) { 273 long userSerial = mUserManager.getSerialNumberForUser(user); 274 PackageManager pm = mContext.getPackageManager(); 275 HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>(); 276 for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) { 277 pkgInfoMap.put(info.packageName, info); 278 } 279 280 HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>(); 281 for (LauncherActivityInfoCompat app : apps) { 282 componentMap.put(app.getComponentName(), app); 283 } 284 285 Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, 286 new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, 287 IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION, 288 IconDB.COLUMN_SYSTEM_STATE}, 289 IconDB.COLUMN_USER + " = ? ", 290 new String[] {Long.toString(userSerial)}, 291 null, null, null); 292 293 final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT); 294 final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED); 295 final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION); 296 final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID); 297 final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE); 298 299 HashSet<Integer> itemsToRemove = new HashSet<Integer>(); 300 Stack<LauncherActivityInfoCompat> appsToUpdate = new Stack<>(); 301 302 while (c.moveToNext()) { 303 String cn = c.getString(indexComponent); 304 ComponentName component = ComponentName.unflattenFromString(cn); 305 PackageInfo info = pkgInfoMap.get(component.getPackageName()); 306 if (info == null) { 307 if (!ignorePackages.contains(component.getPackageName())) { 308 remove(component, user); 309 itemsToRemove.add(c.getInt(rowIndex)); 310 } 311 continue; 312 } 313 if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) { 314 // Application is not present 315 continue; 316 } 317 318 long updateTime = c.getLong(indexLastUpdate); 319 int version = c.getInt(indexVersion); 320 LauncherActivityInfoCompat app = componentMap.remove(component); 321 if (version == info.versionCode && updateTime == info.lastUpdateTime && 322 TextUtils.equals(mSystemState, c.getString(systemStateIndex))) { 323 continue; 324 } 325 if (app == null) { 326 remove(component, user); 327 itemsToRemove.add(c.getInt(rowIndex)); 328 } else { 329 appsToUpdate.add(app); 330 } 331 } 332 c.close(); 333 if (!itemsToRemove.isEmpty()) { 334 mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME, 335 Utilities.createDbSelectionQuery(IconDB.COLUMN_ROWID, itemsToRemove), 336 null); 337 } 338 339 // Insert remaining apps. 340 if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) { 341 Stack<LauncherActivityInfoCompat> appsToAdd = new Stack<>(); 342 appsToAdd.addAll(componentMap.values()); 343 new SerializedIconUpdateTask(userSerial, pkgInfoMap, 344 appsToAdd, appsToUpdate).scheduleNext(); 345 } 346 } 347 348 @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info, 349 long userSerial) { 350 // Reuse the existing entry if it already exists in the DB. This ensures that we do not 351 // create bitmap if it was already created during loader. 352 ContentValues values = updateCacheAndGetContentValues(app, false); 353 addIconToDB(values, app.getComponentName(), info, userSerial); 354 } 355 356 /** 357 * Updates {@param values} to contain versoning information and adds it to the DB. 358 * @param values {@link ContentValues} containing icon & title 359 */ 360 private void addIconToDB(ContentValues values, ComponentName key, 361 PackageInfo info, long userSerial) { 362 values.put(IconDB.COLUMN_COMPONENT, key.flattenToString()); 363 values.put(IconDB.COLUMN_USER, userSerial); 364 values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime); 365 values.put(IconDB.COLUMN_VERSION, info.versionCode); 366 mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, 367 SQLiteDatabase.CONFLICT_REPLACE); 368 } 369 370 @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app, 371 boolean replaceExisting) { 372 final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser()); 373 CacheEntry entry = null; 374 if (!replaceExisting) { 375 entry = mCache.get(key); 376 // We can't reuse the entry if the high-res icon is not present. 377 if (entry == null || entry.isLowResIcon || entry.icon == null) { 378 entry = null; 379 } 380 } 381 if (entry == null) { 382 entry = new CacheEntry(); 383 entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext); 384 } 385 entry.title = app.getLabel(); 386 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser()); 387 mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry); 388 389 return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor); 390 } 391 392 /** 393 * Fetches high-res icon for the provided ItemInfo and updates the caller when done. 394 * @return a request ID that can be used to cancel the request. 395 */ 396 public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) { 397 Runnable request = new Runnable() { 398 399 @Override 400 public void run() { 401 if (info instanceof AppInfo) { 402 getTitleAndIcon((AppInfo) info, null, false); 403 } else if (info instanceof ShortcutInfo) { 404 ShortcutInfo st = (ShortcutInfo) info; 405 getTitleAndIcon(st, 406 st.promisedIntent != null ? st.promisedIntent : st.intent, 407 st.user, false); 408 } else if (info instanceof PackageItemInfo) { 409 PackageItemInfo pti = (PackageItemInfo) info; 410 getTitleAndIconForApp(pti.packageName, pti.user, false, pti); 411 } 412 mMainThreadExecutor.execute(new Runnable() { 413 414 @Override 415 public void run() { 416 caller.reapplyItemInfo(info); 417 } 418 }); 419 } 420 }; 421 mWorkerHandler.post(request); 422 return new IconLoadRequest(request, mWorkerHandler); 423 } 424 425 private Bitmap getNonNullIcon(CacheEntry entry, UserHandleCompat user) { 426 return entry.icon == null ? getDefaultIcon(user) : entry.icon; 427 } 428 429 /** 430 * Fill in "application" with the icon and label for "info." 431 */ 432 public synchronized void getTitleAndIcon(AppInfo application, 433 LauncherActivityInfoCompat info, boolean useLowResIcon) { 434 UserHandleCompat user = info == null ? application.user : info.getUser(); 435 CacheEntry entry = cacheLocked(application.componentName, info, user, 436 false, useLowResIcon); 437 application.title = Utilities.trim(entry.title); 438 application.iconBitmap = getNonNullIcon(entry, user); 439 application.contentDescription = entry.contentDescription; 440 application.usingLowResIcon = entry.isLowResIcon; 441 } 442 443 /** 444 * Updates {@param application} only if a valid entry is found. 445 */ 446 public synchronized void updateTitleAndIcon(AppInfo application) { 447 CacheEntry entry = cacheLocked(application.componentName, null, application.user, 448 false, application.usingLowResIcon); 449 if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) { 450 application.title = Utilities.trim(entry.title); 451 application.iconBitmap = entry.icon; 452 application.contentDescription = entry.contentDescription; 453 application.usingLowResIcon = entry.isLowResIcon; 454 } 455 } 456 457 /** 458 * Returns a high res icon for the given intent and user 459 */ 460 public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) { 461 ComponentName component = intent.getComponent(); 462 // null info means not installed, but if we have a component from the intent then 463 // we should still look in the cache for restored app icons. 464 if (component == null) { 465 return getDefaultIcon(user); 466 } 467 468 LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user); 469 CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, false /* useLowRes */); 470 return entry.icon; 471 } 472 473 /** 474 * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the 475 * corresponding activity is not found, it reverts to the package icon. 476 */ 477 public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent, 478 UserHandleCompat user, boolean useLowResIcon) { 479 ComponentName component = intent.getComponent(); 480 // null info means not installed, but if we have a component from the intent then 481 // we should still look in the cache for restored app icons. 482 if (component == null) { 483 shortcutInfo.setIcon(getDefaultIcon(user)); 484 shortcutInfo.title = ""; 485 shortcutInfo.usingFallbackIcon = true; 486 shortcutInfo.usingLowResIcon = false; 487 } else { 488 LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user); 489 getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon); 490 } 491 } 492 493 /** 494 * Fill in {@param shortcutInfo} with the icon and label for {@param info} 495 */ 496 public synchronized void getTitleAndIcon( 497 ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info, 498 UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) { 499 CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon); 500 shortcutInfo.setIcon(getNonNullIcon(entry, user)); 501 shortcutInfo.title = Utilities.trim(entry.title); 502 shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user); 503 shortcutInfo.usingLowResIcon = entry.isLowResIcon; 504 } 505 506 /** 507 * Fill in {@param appInfo} with the icon and label for {@param packageName} 508 */ 509 public synchronized void getTitleAndIconForApp( 510 String packageName, UserHandleCompat user, boolean useLowResIcon, 511 PackageItemInfo infoOut) { 512 CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon); 513 infoOut.iconBitmap = getNonNullIcon(entry, user); 514 infoOut.title = Utilities.trim(entry.title); 515 infoOut.usingLowResIcon = entry.isLowResIcon; 516 infoOut.contentDescription = entry.contentDescription; 517 } 518 519 public synchronized Bitmap getDefaultIcon(UserHandleCompat user) { 520 if (!mDefaultIcons.containsKey(user)) { 521 mDefaultIcons.put(user, makeDefaultIcon(user)); 522 } 523 return mDefaultIcons.get(user); 524 } 525 526 public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) { 527 return mDefaultIcons.get(user) == icon; 528 } 529 530 /** 531 * Retrieves the entry from the cache. If the entry is not present, it creates a new entry. 532 * This method is not thread safe, it must be called from a synchronized method. 533 */ 534 private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info, 535 UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) { 536 ComponentKey cacheKey = new ComponentKey(componentName, user); 537 CacheEntry entry = mCache.get(cacheKey); 538 if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { 539 entry = new CacheEntry(); 540 mCache.put(cacheKey, entry); 541 542 // Check the DB first. 543 if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { 544 if (info != null) { 545 entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); 546 } else { 547 if (usePackageIcon) { 548 CacheEntry packageEntry = getEntryForPackageLocked( 549 componentName.getPackageName(), user, false); 550 if (packageEntry != null) { 551 if (DEBUG) Log.d(TAG, "using package default icon for " + 552 componentName.toShortString()); 553 entry.icon = packageEntry.icon; 554 entry.title = packageEntry.title; 555 entry.contentDescription = packageEntry.contentDescription; 556 } 557 } 558 if (entry.icon == null) { 559 if (DEBUG) Log.d(TAG, "using default icon for " + 560 componentName.toShortString()); 561 entry.icon = getDefaultIcon(user); 562 } 563 } 564 } 565 566 if (TextUtils.isEmpty(entry.title) && info != null) { 567 entry.title = info.getLabel(); 568 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); 569 } 570 } 571 return entry; 572 } 573 574 /** 575 * Adds a default package entry in the cache. This entry is not persisted and will be removed 576 * when the cache is flushed. 577 */ 578 public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user, 579 Bitmap icon, CharSequence title) { 580 removeFromMemCacheLocked(packageName, user); 581 582 CacheEntry entry = getEntryForPackageLocked(packageName, user, false); 583 if (!TextUtils.isEmpty(title)) { 584 entry.title = title; 585 } 586 if (icon != null) { 587 entry.icon = Utilities.createIconBitmap(icon, mContext); 588 } 589 } 590 591 /** 592 * Gets an entry for the package, which can be used as a fallback entry for various components. 593 * This method is not thread safe, it must be called from a synchronized method. 594 * 595 */ 596 private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, 597 boolean useLowResIcon) { 598 ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); 599 ComponentKey cacheKey = new ComponentKey(cn, user); 600 CacheEntry entry = mCache.get(cacheKey); 601 602 if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { 603 entry = new CacheEntry(); 604 boolean entryUpdated = true; 605 606 // Check the DB first. 607 if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { 608 try { 609 PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); 610 ApplicationInfo appInfo = info.applicationInfo; 611 if (appInfo == null) { 612 throw new NameNotFoundException("ApplicationInfo is null"); 613 } 614 Drawable drawable = mUserManager.getBadgedDrawableForUser( 615 appInfo.loadIcon(mPackageManager), user); 616 entry.icon = Utilities.createIconBitmap(drawable, mContext); 617 entry.title = appInfo.loadLabel(mPackageManager); 618 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); 619 entry.isLowResIcon = false; 620 621 // Add the icon in the DB here, since these do not get written during 622 // package updates. 623 ContentValues values = 624 newContentValues(entry.icon, entry.title.toString(), mPackageBgColor); 625 addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); 626 627 } catch (NameNotFoundException e) { 628 if (DEBUG) Log.d(TAG, "Application not installed " + packageName); 629 entryUpdated = false; 630 } 631 } 632 633 // Only add a filled-out entry to the cache 634 if (entryUpdated) { 635 mCache.put(cacheKey, entry); 636 } 637 } 638 return entry; 639 } 640 641 /** 642 * Pre-load an icon into the persistent cache. 643 * 644 * <P>Queries for a component that does not exist in the package manager 645 * will be answered by the persistent cache. 646 * 647 * @param componentName the icon should be returned for this component 648 * @param icon the icon to be persisted 649 * @param dpi the native density of the icon 650 */ 651 public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, 652 long userSerial) { 653 // TODO rescale to the correct native DPI 654 try { 655 PackageManager packageManager = mContext.getPackageManager(); 656 packageManager.getActivityIcon(componentName); 657 // component is present on the system already, do nothing 658 return; 659 } catch (PackageManager.NameNotFoundException e) { 660 // pass 661 } 662 663 ContentValues values = newContentValues(icon, label, Color.TRANSPARENT); 664 values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); 665 values.put(IconDB.COLUMN_USER, userSerial); 666 mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, 667 SQLiteDatabase.CONFLICT_REPLACE); 668 } 669 670 private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, 671 CacheEntry entry, boolean lowRes) { 672 Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, 673 new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, 674 IconDB.COLUMN_LABEL}, 675 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 676 new String[] {component.flattenToString(), 677 Long.toString(mUserManager.getSerialNumberForUser(user))}, 678 null, null, null); 679 try { 680 if (c.moveToNext()) { 681 entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null); 682 entry.isLowResIcon = lowRes; 683 entry.title = c.getString(1); 684 if (entry.title == null) { 685 entry.title = ""; 686 entry.contentDescription = ""; 687 } else { 688 entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); 689 } 690 return true; 691 } 692 } finally { 693 c.close(); 694 } 695 return false; 696 } 697 698 public static class IconLoadRequest { 699 private final Runnable mRunnable; 700 private final Handler mHandler; 701 702 IconLoadRequest(Runnable runnable, Handler handler) { 703 mRunnable = runnable; 704 mHandler = handler; 705 } 706 707 public void cancel() { 708 mHandler.removeCallbacks(mRunnable); 709 } 710 } 711 712 /** 713 * A runnable that updates invalid icons and adds missing icons in the DB for the provided 714 * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the 715 * worker thread doesn't get blocked. 716 */ 717 @Thunk class SerializedIconUpdateTask implements Runnable { 718 private final long mUserSerial; 719 private final HashMap<String, PackageInfo> mPkgInfoMap; 720 private final Stack<LauncherActivityInfoCompat> mAppsToAdd; 721 private final Stack<LauncherActivityInfoCompat> mAppsToUpdate; 722 private final HashSet<String> mUpdatedPackages = new HashSet<String>(); 723 724 @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap, 725 Stack<LauncherActivityInfoCompat> appsToAdd, 726 Stack<LauncherActivityInfoCompat> appsToUpdate) { 727 mUserSerial = userSerial; 728 mPkgInfoMap = pkgInfoMap; 729 mAppsToAdd = appsToAdd; 730 mAppsToUpdate = appsToUpdate; 731 } 732 733 @Override 734 public void run() { 735 if (!mAppsToUpdate.isEmpty()) { 736 LauncherActivityInfoCompat app = mAppsToUpdate.pop(); 737 String cn = app.getComponentName().flattenToString(); 738 ContentValues values = updateCacheAndGetContentValues(app, true); 739 mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values, 740 IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", 741 new String[] {cn, Long.toString(mUserSerial)}); 742 mUpdatedPackages.add(app.getComponentName().getPackageName()); 743 744 if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) { 745 // No more app to update. Notify model. 746 LauncherAppState.getInstance().getModel().onPackageIconsUpdated( 747 mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial)); 748 } 749 750 // Let it run one more time. 751 scheduleNext(); 752 } else if (!mAppsToAdd.isEmpty()) { 753 LauncherActivityInfoCompat app = mAppsToAdd.pop(); 754 PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName()); 755 if (info != null) { 756 synchronized (IconCache.this) { 757 addIconToDBAndMemCache(app, info, mUserSerial); 758 } 759 } 760 761 if (!mAppsToAdd.isEmpty()) { 762 scheduleNext(); 763 } 764 } 765 } 766 767 public void scheduleNext() { 768 mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN, SystemClock.uptimeMillis() + 1); 769 } 770 } 771 772 private void updateSystemStateString() { 773 mSystemState = Locale.getDefault().toString(); 774 } 775 776 private static final class IconDB extends SQLiteOpenHelper { 777 private final static int DB_VERSION = 6; 778 779 private final static String TABLE_NAME = "icons"; 780 private final static String COLUMN_ROWID = "rowid"; 781 private final static String COLUMN_COMPONENT = "componentName"; 782 private final static String COLUMN_USER = "profileId"; 783 private final static String COLUMN_LAST_UPDATED = "lastUpdated"; 784 private final static String COLUMN_VERSION = "version"; 785 private final static String COLUMN_ICON = "icon"; 786 private final static String COLUMN_ICON_LOW_RES = "icon_low_res"; 787 private final static String COLUMN_LABEL = "label"; 788 private final static String COLUMN_SYSTEM_STATE = "system_state"; 789 790 public IconDB(Context context) { 791 super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION); 792 } 793 794 @Override 795 public void onCreate(SQLiteDatabase db) { 796 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" + 797 COLUMN_COMPONENT + " TEXT NOT NULL, " + 798 COLUMN_USER + " INTEGER NOT NULL, " + 799 COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " + 800 COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " + 801 COLUMN_ICON + " BLOB, " + 802 COLUMN_ICON_LOW_RES + " BLOB, " + 803 COLUMN_LABEL + " TEXT, " + 804 COLUMN_SYSTEM_STATE + " TEXT, " + 805 "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " + 806 ");"); 807 } 808 809 @Override 810 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 811 if (oldVersion != newVersion) { 812 clearDB(db); 813 } 814 } 815 816 @Override 817 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 818 if (oldVersion != newVersion) { 819 clearDB(db); 820 } 821 } 822 823 private void clearDB(SQLiteDatabase db) { 824 db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); 825 onCreate(db); 826 } 827 } 828 829 private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) { 830 ContentValues values = new ContentValues(); 831 values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon)); 832 833 values.put(IconDB.COLUMN_LABEL, label); 834 values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState); 835 836 if (lowResBackgroundColor == Color.TRANSPARENT) { 837 values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap( 838 Bitmap.createScaledBitmap(icon, 839 icon.getWidth() / LOW_RES_SCALE_FACTOR, 840 icon.getHeight() / LOW_RES_SCALE_FACTOR, true))); 841 } else { 842 synchronized (this) { 843 if (mLowResBitmap == null) { 844 mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR, 845 icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565); 846 mLowResCanvas = new Canvas(mLowResBitmap); 847 mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); 848 } 849 mLowResCanvas.drawColor(lowResBackgroundColor); 850 mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()), 851 new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()), 852 mLowResPaint); 853 values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap)); 854 } 855 } 856 return values; 857 } 858 859 private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) { 860 byte[] data = c.getBlob(iconIndex); 861 try { 862 return BitmapFactory.decodeByteArray(data, 0, data.length, options); 863 } catch (Exception e) { 864 return null; 865 } 866 } 867 } 868