blob: 936eeb922064756b00a44f6ed0e01e1b48ed13b9 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.icons;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
import static java.util.stream.Collectors.groupingBy;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ShortcutInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.Trace;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabel.ComponentCachingLogic;
import com.android.launcher3.icons.cache.BaseIconCache;
import com.android.launcher3.icons.cache.CachingLogic;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.IconRequestInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.InstantAppResolver;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.widget.WidgetSections;
import com.android.launcher3.widget.WidgetSections.WidgetSection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* Cache of application icons. Icons can be made from any thread.
*/
public class IconCache extends BaseIconCache {
private static final String TAG = "Launcher.IconCache";
private final Predicate<ItemInfoWithIcon> mIsUsingFallbackOrNonDefaultIconCheck = w ->
w.bitmap != null && (w.bitmap.isNullOrLowRes() || !isDefaultIcon(w.bitmap, w.user));
private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
private final CachingLogic<ShortcutInfo> mShortcutCachingLogic;
private final LauncherApps mLauncherApps;
private final UserCache mUserManager;
private final InstantAppResolver mInstantAppResolver;
private final IconProvider mIconProvider;
private int mPendingIconRequestCount = 0;
public IconCache(Context context, InvariantDeviceProfile idp) {
this(context, idp, LauncherFiles.APP_ICONS_DB, new IconProvider(context));
}
public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName,
IconProvider iconProvider) {
super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */);
mComponentWithLabelCachingLogic = new ComponentCachingLogic(context, false);
mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.newInstance(context);
mShortcutCachingLogic = new ShortcutCachingLogic();
mLauncherApps = mContext.getSystemService(LauncherApps.class);
mUserManager = UserCache.INSTANCE.get(mContext);
mInstantAppResolver = InstantAppResolver.newInstance(mContext);
mIconProvider = iconProvider;
}
@Override
protected long getSerialNumberForUser(UserHandle user) {
return mUserManager.getSerialNumberForUser(user);
}
@Override
protected boolean isInstantApp(ApplicationInfo info) {
return mInstantAppResolver.isInstantApp(info);
}
@Override
public BaseIconFactory getIconFactory() {
return LauncherIcons.obtain(mContext);
}
/**
* Updates the entries related to the given package in memory and persistent DB.
*/
public synchronized void updateIconsForPkg(String packageName, UserHandle user) {
removeIconsForPkg(packageName, user);
try {
PackageInfo info = mPackageManager.getPackageInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES);
long userSerial = mUserManager.getSerialNumberForUser(user);
for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) {
addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial,
false /*replace existing*/);
}
} catch (NameNotFoundException e) {
Log.d(TAG, "Package not found", e);
}
}
/**
* Closes the cache DB. This will clear any in-memory cache.
*/
public void close() {
// This will clear all pending updates
getUpdateHandler();
mIconDb.close();
}
/**
* Fetches high-res icon for the provided ItemInfo and updates the caller when done.
*
* @return a request ID that can be used to cancel the request.
*/
public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
final ItemInfoWithIcon info) {
Preconditions.assertUIThread();
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
}
mPendingIconRequestCount++;
HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
() -> {
if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
getTitleAndIcon(info, false);
} else if (info instanceof PackageItemInfo) {
getTitleAndIconForApp((PackageItemInfo) info, false);
}
return info;
},
MAIN_EXECUTOR,
caller::reapplyItemInfo,
this::onIconRequestEnd);
Utilities.postAsyncCallback(mWorkerHandler, request);
return request;
}
private void onIconRequestEnd() {
mPendingIconRequestCount--;
if (mPendingIconRequestCount <= 0) {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
}
/**
* Updates {@param application} only if a valid entry is found.
*/
public synchronized void updateTitleAndIcon(AppInfo application) {
CacheEntry entry = cacheLocked(application.componentName,
application.user, () -> null, mLauncherActivityInfoCachingLogic,
false, application.usingLowResIcon());
if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) {
applyCacheEntry(entry, application);
}
}
/**
* Fill in {@param info} with the icon and label for {@param activityInfo}
*/
public synchronized void getTitleAndIcon(ItemInfoWithIcon info,
LauncherActivityInfo activityInfo, boolean useLowResIcon) {
// If we already have activity info, no need to use package icon
getTitleAndIcon(info, () -> activityInfo, false, useLowResIcon);
}
/**
* Fill in {@param info} with the icon for {@param si}
*/
public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
getShortcutIcon(info, si, true, mIsUsingFallbackOrNonDefaultIconCheck);
}
/**
* Fill in {@param info} with an unbadged icon for {@param si}
*/
public void getUnbadgedShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
getShortcutIcon(info, si, false, mIsUsingFallbackOrNonDefaultIconCheck);
}
/**
* Fill in {@param info} with the icon and label for {@param si}. If the icon is not
* available, and fallback check returns true, it keeps the old icon.
*/
public <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
@NonNull Predicate<T> fallbackIconCheck) {
getShortcutIcon(info, si, true /* use badged */, fallbackIconCheck);
}
private synchronized <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
boolean useBadged, @NonNull Predicate<T> fallbackIconCheck) {
BitmapInfo bitmapInfo;
if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName, si.getUserHandle(),
() -> si, mShortcutCachingLogic, false, false).bitmap;
} else {
// If caching is disabled, load the full icon
bitmapInfo = mShortcutCachingLogic.loadIcon(mContext, si);
}
if (bitmapInfo.isNullOrLowRes()) {
bitmapInfo = getDefaultIcon(si.getUserHandle());
}
if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) {
return;
}
info.bitmap = bitmapInfo;
if (useBadged) {
BitmapInfo badgeInfo = getShortcutInfoBadge(si);
try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
info.bitmap = li.badgeBitmap(info.bitmap.icon, badgeInfo);
}
}
}
/**
* Returns the badging info for the shortcut
*/
public BitmapInfo getShortcutInfoBadge(ShortcutInfo shortcutInfo) {
ComponentName cn = shortcutInfo.getActivity();
if (cn != null) {
// Get the app info for the source activity.
AppInfo appInfo = new AppInfo();
appInfo.user = shortcutInfo.getUserHandle();
appInfo.componentName = cn;
appInfo.intent = new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(cn);
getTitleAndIcon(appInfo, false);
return appInfo.bitmap;
} else {
PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage(),
shortcutInfo.getUserHandle());
getTitleAndIconForApp(pkgInfo, false);
return pkgInfo.bitmap;
}
}
/**
* Fill in {@param info} with the icon and label. If the
* corresponding activity is not found, it reverts to the package icon.
*/
public synchronized void getTitleAndIcon(ItemInfoWithIcon info, boolean useLowResIcon) {
// null info means not installed, but if we have a component from the intent then
// we should still look in the cache for restored app icons.
if (info.getTargetComponent() == null) {
info.bitmap = getDefaultIcon(info.user);
info.title = "";
info.contentDescription = "";
} else {
Intent intent = info.getIntent();
getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user),
true, useLowResIcon);
}
}
public synchronized String getTitleNoCache(ComponentWithLabel info) {
CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info,
mComponentWithLabelCachingLogic, false /* usePackageIcon */,
true /* useLowResIcon */);
return Utilities.trim(entry.title);
}
/**
* Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info}
*/
public synchronized void getTitleAndIcon(
@NonNull ItemInfoWithIcon infoInOut,
@NonNull Supplier<LauncherActivityInfo> activityInfoProvider,
boolean usePkgIcon, boolean useLowResIcon) {
CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user,
activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon,
useLowResIcon);
applyCacheEntry(entry, infoInOut);
}
/**
* Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles.
*
* @param iconRequestInfos List of IconRequestInfos representing titles and icons to query.
* @param user UserHandle all the given iconRequestInfos share
* @param useLowResIcons whether we should exclude the icon column from the sql results.
*/
private <T extends ItemInfoWithIcon> Cursor createBulkQueryCursor(
List<IconRequestInfo<T>> iconRequestInfos, UserHandle user, boolean useLowResIcons)
throws SQLiteException {
String[] queryParams = Stream.concat(
iconRequestInfos.stream()
.map(r -> r.itemInfo.getTargetComponent())
.filter(Objects::nonNull)
.distinct()
.map(ComponentName::flattenToString),
Stream.of(Long.toString(getSerialNumberForUser(user)))).toArray(String[]::new);
String componentNameQuery = TextUtils.join(
",", Collections.nCopies(queryParams.length - 1, "?"));
return mIconDb.query(
useLowResIcons ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
IconDB.COLUMN_COMPONENT
+ " IN ( " + componentNameQuery + " )"
+ " AND " + IconDB.COLUMN_USER + " = ?",
queryParams);
}
/**
* Load and fill icons requested in iconRequestInfos using a single bulk sql query.
*/
public synchronized <T extends ItemInfoWithIcon> void getTitlesAndIconsInBulk(
List<IconRequestInfo<T>> iconRequestInfos) {
Map<Pair<UserHandle, Boolean>, List<IconRequestInfo<T>>> iconLoadSubsectionsMap =
iconRequestInfos.stream()
.collect(groupingBy(iconRequest ->
Pair.create(iconRequest.itemInfo.user, iconRequest.useLowResIcon)));
Trace.beginSection("loadIconsInBulk");
iconLoadSubsectionsMap.forEach((sectionKey, filteredList) -> {
Map<ComponentName, List<IconRequestInfo<T>>> duplicateIconRequestsMap =
filteredList.stream()
.collect(groupingBy(iconRequest ->
iconRequest.itemInfo.getTargetComponent()));
Trace.beginSection("loadIconSubsectionInBulk");
try (Cursor c = createBulkQueryCursor(
filteredList,
/* user = */ sectionKey.first,
/* useLowResIcons = */ sectionKey.second)) {
int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT);
while (c.moveToNext()) {
ComponentName cn = ComponentName.unflattenFromString(
c.getString(componentNameColumnIndex));
List<IconRequestInfo<T>> duplicateIconRequests =
duplicateIconRequestsMap.get(cn);
if (cn != null) {
CacheEntry entry = cacheLocked(
cn,
/* user = */ sectionKey.first,
() -> duplicateIconRequests.get(0).launcherActivityInfo,
mLauncherActivityInfoCachingLogic,
c,
/* usePackageIcon= */ false,
/* useLowResIcons = */ sectionKey.second);
for (IconRequestInfo<T> iconRequest : duplicateIconRequests) {
applyCacheEntry(entry, iconRequest.itemInfo);
}
}
}
} catch (SQLiteException e) {
Log.d(TAG, "Error reading icon cache", e);
} finally {
Trace.endSection();
}
});
Trace.endSection();
}
/**
* Fill in {@param infoInOut} with the corresponding icon and label.
*/
public synchronized void getTitleAndIconForApp(
PackageItemInfo infoInOut, boolean useLowResIcon) {
CacheEntry entry = getEntryForPackageLocked(
infoInOut.packageName, infoInOut.user, useLowResIcon);
applyCacheEntry(entry, infoInOut);
if (infoInOut.widgetCategory != NO_CATEGORY) {
WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext)
.get(infoInOut.widgetCategory);
infoInOut.title = mContext.getString(widgetSection.mSectionTitle);
infoInOut.contentDescription = mPackageManager.getUserBadgedLabel(
infoInOut.title, infoInOut.user);
}
}
protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
info.title = Utilities.trim(entry.title);
info.contentDescription = entry.contentDescription;
info.bitmap = (entry.bitmap == null) ? getDefaultIcon(info.user) : entry.bitmap;
}
public Drawable getFullResIcon(LauncherActivityInfo info) {
return mIconProvider.getIcon(info, mIconDpi);
}
public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
cachePackageInstallInfo(key.mPackageName, key.mUser, info.getAppIcon(),
info.getAppLabel());
}
@Override
protected String getIconSystemState(String packageName) {
return mIconProvider.getSystemStateForPackage(mSystemState, packageName);
}
/**
* Interface for receiving itemInfo with high-res icon.
*/
public interface ItemInfoUpdateReceiver {
void reapplyItemInfo(ItemInfoWithIcon info);
}
}