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