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