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