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