1 /* 2 * 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.quickstep; 17 18 import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED; 19 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY; 20 21 import android.app.ActivityManager.TaskDescription; 22 import android.content.Context; 23 import android.content.pm.ActivityInfo; 24 import android.content.pm.PackageManager; 25 import android.content.res.Resources; 26 import android.graphics.Bitmap; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.os.Build; 30 import android.os.UserHandle; 31 import android.text.TextUtils; 32 import android.util.SparseArray; 33 import android.view.accessibility.AccessibilityManager; 34 35 import androidx.annotation.WorkerThread; 36 37 import com.android.launcher3.R; 38 import com.android.launcher3.Utilities; 39 import com.android.launcher3.icons.BaseIconFactory; 40 import com.android.launcher3.icons.BitmapInfo; 41 import com.android.launcher3.icons.IconProvider; 42 import com.android.launcher3.util.DisplayController; 43 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener; 44 import com.android.launcher3.util.DisplayController.Info; 45 import com.android.launcher3.util.Preconditions; 46 import com.android.quickstep.util.CancellableTask; 47 import com.android.quickstep.util.TaskKeyLruCache; 48 import com.android.systemui.shared.recents.model.Task; 49 import com.android.systemui.shared.recents.model.Task.TaskKey; 50 import com.android.systemui.shared.system.PackageManagerWrapper; 51 import com.android.systemui.shared.system.TaskDescriptionCompat; 52 53 import java.util.concurrent.Executor; 54 import java.util.function.Consumer; 55 56 /** 57 * Manages the caching of task icons and related data. 58 */ 59 public class TaskIconCache implements DisplayInfoChangeListener { 60 61 private final Executor mBgExecutor; 62 private final AccessibilityManager mAccessibilityManager; 63 64 private final Context mContext; 65 private final TaskKeyLruCache<TaskCacheEntry> mIconCache; 66 private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>(); 67 private final IconProvider mIconProvider; 68 69 private BaseIconFactory mIconFactory; 70 TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider)71 public TaskIconCache(Context context, Executor bgExecutor, IconProvider iconProvider) { 72 mContext = context; 73 mBgExecutor = bgExecutor; 74 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 75 mIconProvider = iconProvider; 76 77 Resources res = context.getResources(); 78 int cacheSize = res.getInteger(R.integer.recentsIconCacheSize); 79 80 mIconCache = new TaskKeyLruCache<>(cacheSize); 81 82 DisplayController.INSTANCE.get(mContext).addChangeListener(this); 83 } 84 85 @Override onDisplayInfoChanged(Context context, Info info, int flags)86 public void onDisplayInfoChanged(Context context, Info info, int flags) { 87 if ((flags & CHANGE_DENSITY) != 0) { 88 clearCache(); 89 } 90 } 91 92 /** 93 * Asynchronously fetches the icon and other task data. 94 * 95 * @param task The task to fetch the data for 96 * @param callback The callback to receive the task after its data has been populated. 97 * @return A cancelable handle to the request 98 */ updateIconInBackground(Task task, Consumer<Task> callback)99 public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) { 100 Preconditions.assertUIThread(); 101 if (task.icon != null) { 102 // Nothing to load, the icon is already loaded 103 callback.accept(task); 104 return null; 105 } 106 CancellableTask<TaskCacheEntry> request = new CancellableTask<TaskCacheEntry>() { 107 @Override 108 public TaskCacheEntry getResultOnBg() { 109 return getCacheEntry(task); 110 } 111 112 @Override 113 public void handleResult(TaskCacheEntry result) { 114 task.icon = result.icon; 115 task.titleDescription = result.contentDescription; 116 callback.accept(task); 117 } 118 }; 119 mBgExecutor.execute(request); 120 return request; 121 } 122 123 /** 124 * Clears the icon cache 125 */ clearCache()126 public void clearCache() { 127 mBgExecutor.execute(this::resetFactory); 128 } 129 onTaskRemoved(TaskKey taskKey)130 void onTaskRemoved(TaskKey taskKey) { 131 mIconCache.remove(taskKey); 132 } 133 invalidateCacheEntries(String pkg, UserHandle handle)134 void invalidateCacheEntries(String pkg, UserHandle handle) { 135 mBgExecutor.execute(() -> mIconCache.removeAll(key -> 136 pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId)); 137 } 138 139 @WorkerThread getCacheEntry(Task task)140 private TaskCacheEntry getCacheEntry(Task task) { 141 TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key); 142 if (entry != null) { 143 return entry; 144 } 145 146 TaskDescription desc = task.taskDescription; 147 TaskKey key = task.key; 148 ActivityInfo activityInfo = null; 149 150 // Create new cache entry 151 entry = new TaskCacheEntry(); 152 153 // Load icon 154 // TODO: Load icon resource (b/143363444) 155 Bitmap icon = TaskDescriptionCompat.getIcon(desc, key.userId); 156 if (icon != null) { 157 /* isInstantApp */ 158 entry.icon = getBitmapInfo( 159 new BitmapDrawable(mContext.getResources(), icon), 160 key.userId, 161 desc.getPrimaryColor(), 162 false /* isInstantApp */).newIcon(mContext); 163 } else { 164 activityInfo = PackageManagerWrapper.getInstance().getActivityInfo( 165 key.getComponent(), key.userId); 166 if (activityInfo != null) { 167 BitmapInfo bitmapInfo = getBitmapInfo( 168 mIconProvider.getIcon(activityInfo), 169 key.userId, 170 desc.getPrimaryColor(), 171 activityInfo.applicationInfo.isInstantApp()); 172 entry.icon = bitmapInfo.newIcon(mContext); 173 } else { 174 entry.icon = getDefaultIcon(key.userId); 175 } 176 } 177 178 // Loading content descriptions if accessibility or low RAM recents is enabled. 179 if (GO_LOW_RAM_RECENTS_ENABLED || mAccessibilityManager.isEnabled()) { 180 // Skip loading the content description if the activity no longer exists 181 if (activityInfo == null) { 182 activityInfo = PackageManagerWrapper.getInstance().getActivityInfo( 183 key.getComponent(), key.userId); 184 } 185 if (activityInfo != null) { 186 entry.contentDescription = getBadgedContentDescription( 187 activityInfo, task.key.userId, task.taskDescription); 188 } 189 } 190 191 mIconCache.put(task.key, entry); 192 return entry; 193 } 194 getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td)195 private String getBadgedContentDescription(ActivityInfo info, int userId, TaskDescription td) { 196 PackageManager pm = mContext.getPackageManager(); 197 String taskLabel = td == null ? null : Utilities.trim(td.getLabel()); 198 if (TextUtils.isEmpty(taskLabel)) { 199 taskLabel = Utilities.trim(info.loadLabel(pm)); 200 } 201 202 String applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(pm)); 203 String badgedApplicationLabel = userId != UserHandle.myUserId() 204 ? pm.getUserBadgedLabel(applicationLabel, UserHandle.of(userId)).toString() 205 : applicationLabel; 206 return applicationLabel.equals(taskLabel) 207 ? badgedApplicationLabel : badgedApplicationLabel + " " + taskLabel; 208 } 209 210 @WorkerThread getDefaultIcon(int userId)211 private Drawable getDefaultIcon(int userId) { 212 synchronized (mDefaultIcons) { 213 BitmapInfo info = mDefaultIcons.get(userId); 214 if (info == null) { 215 try (BaseIconFactory bif = getIconFactory()) { 216 info = bif.makeDefaultIcon(UserHandle.of(userId)); 217 } 218 mDefaultIcons.put(userId, info); 219 } 220 return info.newIcon(mContext); 221 } 222 } 223 224 @WorkerThread getBitmapInfo(Drawable drawable, int userId, int primaryColor, boolean isInstantApp)225 private BitmapInfo getBitmapInfo(Drawable drawable, int userId, 226 int primaryColor, boolean isInstantApp) { 227 try (BaseIconFactory bif = getIconFactory()) { 228 bif.disableColorExtraction(); 229 bif.setWrapperBackgroundColor(primaryColor); 230 231 // User version code O, so that the icon is always wrapped in an adaptive icon container 232 return bif.createBadgedIconBitmap(drawable, UserHandle.of(userId), 233 Build.VERSION_CODES.O, isInstantApp); 234 } 235 } 236 237 @WorkerThread getIconFactory()238 private BaseIconFactory getIconFactory() { 239 if (mIconFactory == null) { 240 mIconFactory = new BaseIconFactory(mContext, 241 DisplayController.INSTANCE.get(mContext).getInfo().densityDpi, 242 mContext.getResources().getDimensionPixelSize(R.dimen.taskbar_icon_size)); 243 } 244 return mIconFactory; 245 } 246 247 @WorkerThread resetFactory()248 private void resetFactory() { 249 mIconFactory = null; 250 mIconCache.evictAll(); 251 } 252 253 private static class TaskCacheEntry { 254 public Drawable icon; 255 public String contentDescription = ""; 256 } 257 } 258