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