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 android.os.Process.THREAD_PRIORITY_BACKGROUND;
19 
20 import static com.android.launcher3.Flags.enableGridOnlyOverview;
21 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
22 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
23 
24 import android.annotation.TargetApi;
25 import android.app.ActivityManager;
26 import android.app.KeyguardManager;
27 import android.content.ComponentCallbacks;
28 import android.content.ComponentCallbacks2;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.res.Configuration;
32 import android.os.Build;
33 import android.os.Process;
34 import android.os.UserHandle;
35 
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.launcher3.icons.IconProvider;
40 import com.android.launcher3.icons.IconProvider.IconChangeListener;
41 import com.android.launcher3.util.Executors.SimpleThreadFactory;
42 import com.android.launcher3.util.MainThreadInitializedObject;
43 import com.android.launcher3.util.SafeCloseable;
44 import com.android.quickstep.recents.data.RecentTasksDataSource;
45 import com.android.quickstep.util.GroupTask;
46 import com.android.quickstep.util.TaskVisualsChangeListener;
47 import com.android.systemui.shared.recents.model.Task;
48 import com.android.systemui.shared.recents.model.ThumbnailData;
49 import com.android.systemui.shared.system.ActivityManagerWrapper;
50 import com.android.systemui.shared.system.TaskStackChangeListener;
51 import com.android.systemui.shared.system.TaskStackChangeListeners;
52 
53 import java.io.PrintWriter;
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.concurrent.Executor;
57 import java.util.concurrent.Executors;
58 import java.util.function.Consumer;
59 import java.util.function.Predicate;
60 
61 /**
62  * Singleton class to load and manage recents model.
63  */
64 @TargetApi(Build.VERSION_CODES.O)
65 public class RecentsModel implements RecentTasksDataSource, IconChangeListener,
66         TaskStackChangeListener, TaskVisualsChangeListener, SafeCloseable {
67 
68     // We do not need any synchronization for this variable as its only written on UI thread.
69     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
70             new MainThreadInitializedObject<>(RecentsModel::new);
71 
72     private static final Executor RECENTS_MODEL_EXECUTOR = Executors.newSingleThreadExecutor(
73             new SimpleThreadFactory("TaskThumbnailIconCache-", THREAD_PRIORITY_BACKGROUND));
74 
75     private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
76     private final Context mContext;
77 
78     private final RecentTasksList mTaskList;
79     private final TaskIconCache mIconCache;
80     private final TaskThumbnailCache mThumbnailCache;
81     private final ComponentCallbacks mCallbacks;
82 
83     private final TaskStackChangeListeners mTaskStackChangeListeners;
84 
RecentsModel(Context context)85     private RecentsModel(Context context) {
86         this(context, new IconProvider(context));
87     }
88 
RecentsModel(Context context, IconProvider iconProvider)89     private RecentsModel(Context context, IconProvider iconProvider) {
90         this(context,
91                 new RecentTasksList(MAIN_EXECUTOR,
92                         context.getSystemService(KeyguardManager.class),
93                         SystemUiProxy.INSTANCE.get(context),
94                         TopTaskTracker.INSTANCE.get(context)),
95                 new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider),
96                 new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
97                 iconProvider,
98                 TaskStackChangeListeners.getInstance());
99     }
100 
101     @VisibleForTesting
RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache, TaskThumbnailCache thumbnailCache, IconProvider iconProvider, TaskStackChangeListeners taskStackChangeListeners)102     RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
103             TaskThumbnailCache thumbnailCache, IconProvider iconProvider,
104             TaskStackChangeListeners taskStackChangeListeners) {
105         mContext = context;
106         mTaskList = taskList;
107         mIconCache = iconCache;
108         mIconCache.registerTaskVisualsChangeListener(this);
109         mThumbnailCache = thumbnailCache;
110         if (enableGridOnlyOverview()) {
111             mCallbacks = new ComponentCallbacks() {
112                 @Override
113                 public void onConfigurationChanged(Configuration configuration) {
114                     updateCacheSizeAndPreloadIfNeeded();
115                 }
116 
117                 @Override
118                 public void onLowMemory() {
119                 }
120             };
121             context.registerComponentCallbacks(mCallbacks);
122         } else {
123             mCallbacks = null;
124         }
125 
126         mTaskStackChangeListeners = taskStackChangeListeners;
127         mTaskStackChangeListeners.registerTaskStackListener(this);
128         iconProvider.registerIconChangeListener(this, MAIN_EXECUTOR.getHandler());
129     }
130 
getIconCache()131     public TaskIconCache getIconCache() {
132         return mIconCache;
133     }
134 
getThumbnailCache()135     public TaskThumbnailCache getThumbnailCache() {
136         return mThumbnailCache;
137     }
138 
139     /**
140      * Fetches the list of recent tasks. Tasks are ordered by recency, with the latest active tasks
141      * at the end of the list.
142      *
143      * @param callback The callback to receive the task plan once its complete or null. This is
144      *                always called on the UI thread.
145      * @return the request id associated with this call.
146      */
147     @Override
getTasks(@ullable Consumer<List<GroupTask>> callback)148     public int getTasks(@Nullable Consumer<List<GroupTask>> callback) {
149         return mTaskList.getTasks(false /* loadKeysOnly */, callback,
150                 RecentsFilterState.DEFAULT_FILTER);
151     }
152 
153     /**
154      * Fetches the list of recent tasks, based on a filter
155      *
156      * @param callback The callback to receive the task plan once its complete or null. This is
157      *                always called on the UI thread.
158      * @param filter  Returns true if a GroupTask should be included into the list passed into
159      *                callback.
160      * @return the request id associated with this call.
161      */
getTasks(@ullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter)162     public int getTasks(@Nullable Consumer<List<GroupTask>> callback, Predicate<GroupTask> filter) {
163         return mTaskList.getTasks(false /* loadKeysOnly */, callback, filter);
164     }
165 
166     /**
167      * @return Whether the provided {@param changeId} is the latest recent tasks list id.
168      */
isTaskListValid(int changeId)169     public boolean isTaskListValid(int changeId) {
170         return mTaskList.isTaskListValid(changeId);
171     }
172 
173     /**
174      * @return Whether the task list is currently updating in the background
175      */
176     @VisibleForTesting
isLoadingTasksInBackground()177     public boolean isLoadingTasksInBackground() {
178         return mTaskList.isLoadingTasksInBackground();
179     }
180 
181     /**
182      * Checks if a task has been removed or not.
183      *
184      * @param callback Receives true if task is removed, false otherwise
185      * @param filter Returns true if GroupTask should be in the list of considerations
186      */
isTaskRemoved(int taskId, Consumer<Boolean> callback, Predicate<GroupTask> filter)187     public void isTaskRemoved(int taskId, Consumer<Boolean> callback, Predicate<GroupTask> filter) {
188         // Invalidate the existing list before checking to ensure this reflects the current state in
189         // the system
190         mTaskList.onRecentTasksChanged();
191         mTaskList.getTasks(true /* loadKeysOnly */, (taskGroups) -> {
192             for (GroupTask group : taskGroups) {
193                 if (group.containsTask(taskId)) {
194                     callback.accept(false);
195                     return;
196                 }
197             }
198             callback.accept(true);
199         }, filter);
200     }
201 
202     @Override
onTaskStackChangedBackground()203     public void onTaskStackChangedBackground() {
204         if (!mThumbnailCache.isPreloadingEnabled()) {
205             // Skip if we aren't preloading
206             return;
207         }
208 
209         int currentUserId = Process.myUserHandle().getIdentifier();
210         if (!checkCurrentOrManagedUserId(currentUserId, mContext)) {
211             // Skip if we are not the current user
212             return;
213         }
214 
215         // Keep the cache up to date with the latest thumbnails
216         ActivityManager.RunningTaskInfo runningTask =
217                 ActivityManagerWrapper.getInstance().getRunningTask();
218         int runningTaskId = runningTask != null ? runningTask.id : -1;
219         mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), taskGroups -> {
220             for (GroupTask group : taskGroups) {
221                 if (group.containsTask(runningTaskId)) {
222                     // Skip the running task, it's not going to have an up-to-date snapshot by the
223                     // time the user next enters overview
224                     continue;
225                 }
226                 mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ true);
227                 mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ true);
228             }
229         });
230     }
231 
232     @Override
onTaskSnapshotChanged(int taskId, ThumbnailData snapshot)233     public boolean onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
234         mThumbnailCache.updateTaskSnapShot(taskId, snapshot);
235 
236         for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
237             Task task = mThumbnailChangeListeners.get(i).onTaskThumbnailChanged(taskId, snapshot);
238             if (task != null) {
239                 task.thumbnail = snapshot;
240             }
241         }
242         return true;
243     }
244 
245     @Override
onTaskRemoved(int taskId)246     public void onTaskRemoved(int taskId) {
247         Task.TaskKey stubKey = new Task.TaskKey(taskId, 0, new Intent(), null, 0, 0);
248         mThumbnailCache.remove(stubKey);
249         mIconCache.onTaskRemoved(stubKey);
250     }
251 
onTrimMemory(int level)252     public void onTrimMemory(int level) {
253         if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
254             mThumbnailCache.getHighResLoadingState().setVisible(false);
255         }
256         if (level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
257             // Clear everything once we reach a low-mem situation
258             mThumbnailCache.clear();
259             mIconCache.clearCache();
260         }
261     }
262 
263     @Override
onAppIconChanged(String packageName, UserHandle user)264     public void onAppIconChanged(String packageName, UserHandle user) {
265         mIconCache.invalidateCacheEntries(packageName, user);
266         for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
267             mThumbnailChangeListeners.get(i).onTaskIconChanged(packageName, user);
268         }
269     }
270 
271     @Override
onTaskIconChanged(int taskId)272     public void onTaskIconChanged(int taskId) {
273         for (TaskVisualsChangeListener listener : mThumbnailChangeListeners) {
274             listener.onTaskIconChanged(taskId);
275         }
276     }
277 
278     @Override
onSystemIconStateChanged(String iconState)279     public void onSystemIconStateChanged(String iconState) {
280         mIconCache.clearCache();
281     }
282 
283     /**
284      * Adds a listener for visuals changes
285      */
addThumbnailChangeListener(TaskVisualsChangeListener listener)286     public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
287         mThumbnailChangeListeners.add(listener);
288     }
289 
290     /**
291      * Removes a previously added listener
292      */
removeThumbnailChangeListener(TaskVisualsChangeListener listener)293     public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
294         mThumbnailChangeListeners.remove(listener);
295     }
296 
dump(String prefix, PrintWriter writer)297     public void dump(String prefix, PrintWriter writer) {
298         writer.println(prefix + "RecentsModel:");
299         mTaskList.dump("  ", writer);
300     }
301 
302     /**
303      * Registers a listener for running tasks
304      */
registerRunningTasksListener(RunningTasksListener listener)305     public void registerRunningTasksListener(RunningTasksListener listener) {
306         mTaskList.registerRunningTasksListener(listener);
307     }
308 
309     /**
310      * Removes the previously registered running tasks listener
311      */
unregisterRunningTasksListener()312     public void unregisterRunningTasksListener() {
313         mTaskList.unregisterRunningTasksListener();
314     }
315 
316     /**
317      * Gets the set of running tasks.
318      */
getRunningTasks()319     public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
320         return mTaskList.getRunningTasks();
321     }
322 
323     /**
324      * Preloads cache if enableGridOnlyOverview is true, preloading is enabled and
325      * highResLoadingState is enabled
326      */
preloadCacheIfNeeded()327     public void preloadCacheIfNeeded() {
328         if (!enableGridOnlyOverview()) {
329             return;
330         }
331 
332         if (!mThumbnailCache.isPreloadingEnabled()) {
333             // Skip if we aren't preloading.
334             return;
335         }
336 
337         if (!mThumbnailCache.getHighResLoadingState().isEnabled()) {
338             // Skip if high-res loading state is disabled.
339             return;
340         }
341 
342         mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), taskGroups -> {
343             for (GroupTask group : taskGroups) {
344                 mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ false);
345                 mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ false);
346             }
347         });
348     }
349 
350     /**
351      * Updates cache size and preloads more tasks if cache size increases
352      */
updateCacheSizeAndPreloadIfNeeded()353     public void updateCacheSizeAndPreloadIfNeeded() {
354         if (!enableGridOnlyOverview()) {
355             return;
356         }
357 
358         // If new size is larger than original size, preload more cache to fill the gap
359         if (mThumbnailCache.updateCacheSizeAndRemoveExcess()) {
360             preloadCacheIfNeeded();
361         }
362     }
363 
364     @Override
close()365     public void close() {
366         if (mCallbacks != null) {
367             mContext.unregisterComponentCallbacks(mCallbacks);
368         }
369         mIconCache.removeTaskVisualsChangeListener();
370         mTaskStackChangeListeners.unregisterTaskStackListener(this);
371     }
372 
373     /**
374      * Listener for receiving running tasks changes
375      */
376     public interface RunningTasksListener {
377         /**
378          * Called when there's a change to running tasks
379          */
onRunningTasksChanged()380         void onRunningTasksChanged();
381     }
382 }
383