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.icons; 18 19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 21 import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY; 22 23 import static java.util.stream.Collectors.groupingBy; 24 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.LauncherActivityInfo; 30 import android.content.pm.LauncherApps; 31 import android.content.pm.PackageInfo; 32 import android.content.pm.PackageInstaller; 33 import android.content.pm.PackageManager; 34 import android.content.pm.PackageManager.NameNotFoundException; 35 import android.content.pm.ShortcutInfo; 36 import android.database.Cursor; 37 import android.database.sqlite.SQLiteException; 38 import android.graphics.drawable.Drawable; 39 import android.os.Process; 40 import android.os.Trace; 41 import android.os.UserHandle; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.Pair; 45 46 import androidx.annotation.NonNull; 47 48 import com.android.launcher3.InvariantDeviceProfile; 49 import com.android.launcher3.LauncherFiles; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.config.FeatureFlags; 52 import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic; 53 import com.android.launcher3.icons.cache.BaseIconCache; 54 import com.android.launcher3.icons.cache.CachingLogic; 55 import com.android.launcher3.icons.cache.HandlerRunnable; 56 import com.android.launcher3.model.data.AppInfo; 57 import com.android.launcher3.model.data.IconRequestInfo; 58 import com.android.launcher3.model.data.ItemInfoWithIcon; 59 import com.android.launcher3.model.data.PackageItemInfo; 60 import com.android.launcher3.model.data.WorkspaceItemInfo; 61 import com.android.launcher3.pm.UserCache; 62 import com.android.launcher3.shortcuts.ShortcutKey; 63 import com.android.launcher3.util.InstantAppResolver; 64 import com.android.launcher3.util.PackageUserKey; 65 import com.android.launcher3.util.Preconditions; 66 import com.android.launcher3.widget.WidgetSections; 67 import com.android.launcher3.widget.WidgetSections.WidgetSection; 68 69 import java.util.Collections; 70 import java.util.List; 71 import java.util.Map; 72 import java.util.Objects; 73 import java.util.function.Predicate; 74 import java.util.function.Supplier; 75 import java.util.stream.Stream; 76 77 /** 78 * Cache of application icons. Icons can be made from any thread. 79 */ 80 public class IconCache extends BaseIconCache { 81 82 private static final String TAG = "Launcher.IconCache"; 83 84 private final Predicate<ItemInfoWithIcon> mIsUsingFallbackOrNonDefaultIconCheck = w -> 85 w.bitmap != null && (w.bitmap.isNullOrLowRes() || !isDefaultIcon(w.bitmap, w.user)); 86 87 private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic; 88 private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic; 89 private final CachingLogic<ShortcutInfo> mShortcutCachingLogic; 90 91 private final LauncherApps mLauncherApps; 92 private final UserCache mUserManager; 93 private final InstantAppResolver mInstantAppResolver; 94 private final IconProvider mIconProvider; 95 96 private int mPendingIconRequestCount = 0; 97 IconCache(Context context, InvariantDeviceProfile idp)98 public IconCache(Context context, InvariantDeviceProfile idp) { 99 this(context, idp, LauncherFiles.APP_ICONS_DB, new IconProvider(context)); 100 } 101 IconCache(Context context, InvariantDeviceProfile idp, String dbFileName, IconProvider iconProvider)102 public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName, 103 IconProvider iconProvider) { 104 super(context, dbFileName, MODEL_EXECUTOR.getLooper(), 105 idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */); 106 mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false); 107 mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context); 108 mShortcutCachingLogic = new ShortcutCachingLogic(); 109 mLauncherApps = mContext.getSystemService(LauncherApps.class); 110 mUserManager = UserCache.INSTANCE.get(mContext); 111 mInstantAppResolver = InstantAppResolver.newInstance(mContext); 112 mIconProvider = iconProvider; 113 } 114 115 @Override getSerialNumberForUser(UserHandle user)116 protected long getSerialNumberForUser(UserHandle user) { 117 return mUserManager.getSerialNumberForUser(user); 118 } 119 120 @Override isInstantApp(ApplicationInfo info)121 protected boolean isInstantApp(ApplicationInfo info) { 122 return mInstantAppResolver.isInstantApp(info); 123 } 124 125 @Override getIconFactory()126 public BaseIconFactory getIconFactory() { 127 return LauncherIcons.obtain(mContext); 128 } 129 130 /** 131 * Updates the entries related to the given package in memory and persistent DB. 132 */ updateIconsForPkg(String packageName, UserHandle user)133 public synchronized void updateIconsForPkg(String packageName, UserHandle user) { 134 removeIconsForPkg(packageName, user); 135 try { 136 PackageInfo info = mPackageManager.getPackageInfo(packageName, 137 PackageManager.GET_UNINSTALLED_PACKAGES); 138 long userSerial = mUserManager.getSerialNumberForUser(user); 139 for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) { 140 addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial, 141 false /*replace existing*/); 142 } 143 } catch (NameNotFoundException e) { 144 Log.d(TAG, "Package not found", e); 145 } 146 } 147 148 /** 149 * Closes the cache DB. This will clear any in-memory cache. 150 */ close()151 public void close() { 152 // This will clear all pending updates 153 getUpdateHandler(); 154 155 mIconDb.close(); 156 } 157 158 /** 159 * Fetches high-res icon for the provided ItemInfo and updates the caller when done. 160 * 161 * @return a request ID that can be used to cancel the request. 162 */ updateIconInBackground(final ItemInfoUpdateReceiver caller, final ItemInfoWithIcon info)163 public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller, 164 final ItemInfoWithIcon info) { 165 Preconditions.assertUIThread(); 166 if (mPendingIconRequestCount <= 0) { 167 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); 168 } 169 mPendingIconRequestCount++; 170 171 HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler, 172 () -> { 173 if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) { 174 getTitleAndIcon(info, false); 175 } else if (info instanceof PackageItemInfo) { 176 getTitleAndIconForApp((PackageItemInfo) info, false); 177 } 178 return info; 179 }, 180 MAIN_EXECUTOR, 181 caller::reapplyItemInfo, 182 this::onIconRequestEnd); 183 Utilities.postAsyncCallback(mWorkerHandler, request); 184 return request; 185 } 186 onIconRequestEnd()187 private void onIconRequestEnd() { 188 mPendingIconRequestCount--; 189 if (mPendingIconRequestCount <= 0) { 190 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 191 } 192 } 193 194 /** 195 * Updates {@param application} only if a valid entry is found. 196 */ updateTitleAndIcon(AppInfo application)197 public synchronized void updateTitleAndIcon(AppInfo application) { 198 CacheEntry entry = cacheLocked(application.componentName, 199 application.user, () -> null, mLauncherActivityInfoCachingLogic, 200 false, application.usingLowResIcon()); 201 if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) { 202 applyCacheEntry(entry, application); 203 } 204 } 205 206 /** 207 * Fill in {@param info} with the icon and label for {@param activityInfo} 208 */ getTitleAndIcon(ItemInfoWithIcon info, LauncherActivityInfo activityInfo, boolean useLowResIcon)209 public synchronized void getTitleAndIcon(ItemInfoWithIcon info, 210 LauncherActivityInfo activityInfo, boolean useLowResIcon) { 211 // If we already have activity info, no need to use package icon 212 getTitleAndIcon(info, () -> activityInfo, false, useLowResIcon); 213 } 214 215 /** 216 * Fill in {@param info} with the icon for {@param si} 217 */ getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si)218 public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) { 219 getShortcutIcon(info, si, true, mIsUsingFallbackOrNonDefaultIconCheck); 220 } 221 222 /** 223 * Fill in {@param info} with an unbadged icon for {@param si} 224 */ getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si)225 public void getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) { 226 getShortcutIcon(info, si, false, mIsUsingFallbackOrNonDefaultIconCheck); 227 } 228 229 /** 230 * Fill in {@param info} with the icon and label for {@param si}. If the icon is not 231 * available, and fallback check returns true, it keeps the old icon. 232 */ getShortcutIcon(T info, ShortcutInfo si, @NonNull Predicate<T> fallbackIconCheck)233 public <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si, 234 @NonNull Predicate<T> fallbackIconCheck) { 235 getShortcutIcon(info, si, true /* use badged */, fallbackIconCheck); 236 } 237 getShortcutIcon(T info, ShortcutInfo si, boolean useBadged, @NonNull Predicate<T> fallbackIconCheck)238 private synchronized <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si, 239 boolean useBadged, @NonNull Predicate<T> fallbackIconCheck) { 240 BitmapInfo bitmapInfo; 241 if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) { 242 bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName, si.getUserHandle(), 243 () -> si, mShortcutCachingLogic, false, false).bitmap; 244 } else { 245 // If caching is disabled, load the full icon 246 bitmapInfo = mShortcutCachingLogic.loadIcon(mContext, si); 247 } 248 if (bitmapInfo.isNullOrLowRes()) { 249 bitmapInfo = getDefaultIcon(si.getUserHandle()); 250 } 251 252 if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) { 253 return; 254 } 255 info.bitmap = bitmapInfo; 256 if (useBadged) { 257 BitmapInfo badgeInfo = getShortcutInfoBadge(si); 258 try (LauncherIcons li = LauncherIcons.obtain(mContext)) { 259 info.bitmap = li.badgeBitmap(info.bitmap.icon, badgeInfo); 260 } 261 } 262 } 263 264 /** 265 * Returns the badging info for the shortcut 266 */ getShortcutInfoBadge(ShortcutInfo shortcutInfo)267 public BitmapInfo getShortcutInfoBadge(ShortcutInfo shortcutInfo) { 268 ComponentName cn = shortcutInfo.getActivity(); 269 if (cn != null) { 270 // Get the app info for the source activity. 271 AppInfo appInfo = new AppInfo(); 272 appInfo.user = shortcutInfo.getUserHandle(); 273 appInfo.componentName = cn; 274 appInfo.intent = new Intent(Intent.ACTION_MAIN) 275 .addCategory(Intent.CATEGORY_LAUNCHER) 276 .setComponent(cn); 277 getTitleAndIcon(appInfo, false); 278 return appInfo.bitmap; 279 } else { 280 PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage(), 281 shortcutInfo.getUserHandle()); 282 getTitleAndIconForApp(pkgInfo, false); 283 return pkgInfo.bitmap; 284 } 285 } 286 287 /** 288 * Fill in {@param info} with the icon and label. If the 289 * corresponding activity is not found, it reverts to the package icon. 290 */ getTitleAndIcon(ItemInfoWithIcon info, boolean useLowResIcon)291 public synchronized void getTitleAndIcon(ItemInfoWithIcon info, boolean useLowResIcon) { 292 // null info means not installed, but if we have a component from the intent then 293 // we should still look in the cache for restored app icons. 294 if (info.getTargetComponent() == null) { 295 info.bitmap = getDefaultIcon(info.user); 296 info.title = ""; 297 info.contentDescription = ""; 298 } else { 299 Intent intent = info.getIntent(); 300 getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user), 301 true, useLowResIcon); 302 } 303 } 304 getTitleNoCache(ComponentWithLabel info)305 public synchronized String getTitleNoCache(ComponentWithLabel info) { 306 CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info, 307 mComponentWithLabelCachingLogic, false /* usePackageIcon */, 308 true /* useLowResIcon */); 309 return Utilities.trim(entry.title); 310 } 311 312 /** 313 * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info} 314 */ getTitleAndIcon( @onNull ItemInfoWithIcon infoInOut, @NonNull Supplier<LauncherActivityInfo> activityInfoProvider, boolean usePkgIcon, boolean useLowResIcon)315 public synchronized void getTitleAndIcon( 316 @NonNull ItemInfoWithIcon infoInOut, 317 @NonNull Supplier<LauncherActivityInfo> activityInfoProvider, 318 boolean usePkgIcon, boolean useLowResIcon) { 319 CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user, 320 activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, 321 useLowResIcon); 322 applyCacheEntry(entry, infoInOut); 323 } 324 325 /** 326 * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles. 327 * 328 * @param iconRequestInfos List of IconRequestInfos representing titles and icons to query. 329 * @param user UserHandle all the given iconRequestInfos share 330 * @param useLowResIcons whether we should exclude the icon column from the sql results. 331 */ createBulkQueryCursor( List<IconRequestInfo<T>> iconRequestInfos, UserHandle user, boolean useLowResIcons)332 private <T extends ItemInfoWithIcon> Cursor createBulkQueryCursor( 333 List<IconRequestInfo<T>> iconRequestInfos, UserHandle user, boolean useLowResIcons) 334 throws SQLiteException { 335 String[] queryParams = Stream.concat( 336 iconRequestInfos.stream() 337 .map(r -> r.itemInfo.getTargetComponent()) 338 .filter(Objects::nonNull) 339 .distinct() 340 .map(ComponentName::flattenToString), 341 Stream.of(Long.toString(getSerialNumberForUser(user)))).toArray(String[]::new); 342 String componentNameQuery = TextUtils.join( 343 ",", Collections.nCopies(queryParams.length - 1, "?")); 344 345 return mIconDb.query( 346 useLowResIcons ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, 347 IconDB.COLUMN_COMPONENT 348 + " IN ( " + componentNameQuery + " )" 349 + " AND " + IconDB.COLUMN_USER + " = ?", 350 queryParams); 351 } 352 353 /** 354 * Load and fill icons requested in iconRequestInfos using a single bulk sql query. 355 */ getTitlesAndIconsInBulk( List<IconRequestInfo<T>> iconRequestInfos)356 public synchronized <T extends ItemInfoWithIcon> void getTitlesAndIconsInBulk( 357 List<IconRequestInfo<T>> iconRequestInfos) { 358 Map<Pair<UserHandle, Boolean>, List<IconRequestInfo<T>>> iconLoadSubsectionsMap = 359 iconRequestInfos.stream() 360 .collect(groupingBy(iconRequest -> 361 Pair.create(iconRequest.itemInfo.user, iconRequest.useLowResIcon))); 362 363 Trace.beginSection("loadIconsInBulk"); 364 iconLoadSubsectionsMap.forEach((sectionKey, filteredList) -> { 365 Map<ComponentName, List<IconRequestInfo<T>>> duplicateIconRequestsMap = 366 filteredList.stream() 367 .collect(groupingBy(iconRequest -> 368 iconRequest.itemInfo.getTargetComponent())); 369 370 Trace.beginSection("loadIconSubsectionInBulk"); 371 try (Cursor c = createBulkQueryCursor( 372 filteredList, 373 /* user = */ sectionKey.first, 374 /* useLowResIcons = */ sectionKey.second)) { 375 int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT); 376 while (c.moveToNext()) { 377 ComponentName cn = ComponentName.unflattenFromString( 378 c.getString(componentNameColumnIndex)); 379 List<IconRequestInfo<T>> duplicateIconRequests = 380 duplicateIconRequestsMap.get(cn); 381 382 if (cn != null) { 383 CacheEntry entry = cacheLocked( 384 cn, 385 /* user = */ sectionKey.first, 386 () -> duplicateIconRequests.get(0).launcherActivityInfo, 387 mLauncherActivityInfoCachingLogic, 388 c, 389 /* usePackageIcon= */ false, 390 /* useLowResIcons = */ sectionKey.second); 391 392 for (IconRequestInfo<T> iconRequest : duplicateIconRequests) { 393 applyCacheEntry(entry, iconRequest.itemInfo); 394 } 395 } 396 } 397 } catch (SQLiteException e) { 398 Log.d(TAG, "Error reading icon cache", e); 399 } finally { 400 Trace.endSection(); 401 } 402 }); 403 Trace.endSection(); 404 } 405 406 407 /** 408 * Fill in {@param infoInOut} with the corresponding icon and label. 409 */ getTitleAndIconForApp( PackageItemInfo infoInOut, boolean useLowResIcon)410 public synchronized void getTitleAndIconForApp( 411 PackageItemInfo infoInOut, boolean useLowResIcon) { 412 CacheEntry entry = getEntryForPackageLocked( 413 infoInOut.packageName, infoInOut.user, useLowResIcon); 414 applyCacheEntry(entry, infoInOut); 415 if (infoInOut.widgetCategory != NO_CATEGORY) { 416 WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext) 417 .get(infoInOut.widgetCategory); 418 infoInOut.title = mContext.getString(widgetSection.mSectionTitle); 419 infoInOut.contentDescription = mPackageManager.getUserBadgedLabel( 420 infoInOut.title, infoInOut.user); 421 } 422 } 423 applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info)424 protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) { 425 info.title = Utilities.trim(entry.title); 426 info.contentDescription = entry.contentDescription; 427 info.bitmap = (entry.bitmap == null) ? getDefaultIcon(info.user) : entry.bitmap; 428 } 429 getFullResIcon(LauncherActivityInfo info)430 public Drawable getFullResIcon(LauncherActivityInfo info) { 431 return mIconProvider.getIcon(info, mIconDpi); 432 } 433 updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info)434 public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) { 435 cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(), 436 info.getAppLabel()); 437 } 438 439 @Override getIconSystemState(String packageName)440 protected String getIconSystemState(String packageName) { 441 return mIconProvider.getSystemStateForPackage(mSystemState, packageName); 442 } 443 444 /** 445 * Interface for receiving itemInfo with high-res icon. 446 */ 447 public interface ItemInfoUpdateReceiver { 448 reapplyItemInfo(ItemInfoWithIcon info)449 void reapplyItemInfo(ItemInfoWithIcon info); 450 } 451 } 452