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