1 /* 2 * Copyright (C) 2023 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 17 package com.android.car.carlauncher.recents; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 21 import android.content.ComponentName; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.view.View; 28 29 import androidx.annotation.ColorInt; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 33 import java.util.ArrayList; 34 import java.util.HashMap; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 40 public class RecentTasksViewModel { 41 private static RecentTasksViewModel sInstance; 42 private final RecentTasksProviderInterface mDataStore; 43 private final Set<RecentTasksChangeListener> mRecentTasksChangeListener; 44 private final Set<HiddenTaskProvider> mHiddenTaskProviders; 45 private DisabledTaskProvider mDisabledTaskProvider; 46 private final RecentTasksProviderInterface.RecentsDataChangeListener 47 mRecentsDataChangeListener = 48 new RecentTasksProviderInterface.RecentsDataChangeListener() { 49 @Override 50 public void recentTasksFetched() { 51 mapAbsoluteTasksToShownTasks(); 52 mRecentTasksChangeListener.forEach( 53 RecentTasksChangeListener::onRecentTasksFetched); 54 } 55 56 @Override 57 public void recentTaskThumbnailChange(int taskId) { 58 int index = mRecentTaskIds.indexOf(taskId); 59 if (index == -1) { 60 return; 61 } 62 mTaskIdToCroppedThumbnailMap.remove(taskId); 63 mRecentTasksChangeListener.forEach(l -> l.onTaskThumbnailChange(index)); 64 } 65 66 @Override 67 public void recentTaskIconChange(int taskId) { 68 int index = mRecentTaskIds.indexOf(taskId); 69 if (index == -1) { 70 return; 71 } 72 mRecentTasksChangeListener.forEach(l -> 73 l.onTaskIconChange(index)); 74 } 75 }; 76 private List<Integer> mRecentTaskIds; 77 private final Map<Integer, Bitmap> mTaskIdToCroppedThumbnailMap; 78 private Bitmap mDefaultThumbnail; 79 private boolean isInitialised; 80 private int mDisplayId = DEFAULT_DISPLAY; 81 private int mWindowWidth; 82 private int mWindowHeight; 83 private Rect mWindowInsets; 84 RecentTasksViewModel()85 private RecentTasksViewModel() { 86 mDataStore = RecentTasksProvider.getInstance(); 87 mRecentTasksChangeListener = new HashSet<>(); 88 mHiddenTaskProviders = new HashSet<>(); 89 mRecentTaskIds = new ArrayList<>(); 90 mTaskIdToCroppedThumbnailMap = new HashMap<>(); 91 } 92 93 /** 94 * Initialise connections and setup configs 95 * 96 * @param displayId the display on which the recents activity is displayed. 97 * @param windowWidth width of window on which recent activity is displayed. 98 * @param windowHeight height of window on which recent activity is displayed. 99 * @param windowInsets insets of window on which recent activity is displayed. 100 * @param defaultThumbnailColor color of the default recent task thumbnail to be shown when 101 * thumbnail is not loaded or not present. 102 */ init(int displayId, int windowWidth, int windowHeight, @NonNull Rect windowInsets, @ColorInt Integer defaultThumbnailColor)103 public void init(int displayId, int windowWidth, int windowHeight, @NonNull Rect windowInsets, 104 @ColorInt Integer defaultThumbnailColor) { 105 if (isInitialised) { 106 return; 107 } 108 isInitialised = true; 109 mDataStore.setRecentsDataChangeListener(mRecentsDataChangeListener); 110 mDisplayId = displayId; 111 mWindowWidth = windowWidth; 112 mWindowHeight = windowHeight; 113 mWindowInsets = windowInsets; 114 mDefaultThumbnail = createThumbnail(defaultThumbnailColor); 115 } 116 117 /** 118 * Terminates connections and removes all {@link RecentTasksChangeListener}s and 119 * {@link HiddenTaskProvider}s. 120 */ terminate()121 public void terminate() { 122 isInitialised = false; 123 mDataStore.setRecentsDataChangeListener(/* listener= */ null); 124 mRecentTasksChangeListener.clear(); 125 mHiddenTaskProviders.clear(); 126 mDisabledTaskProvider = null; 127 } 128 getInstance()129 public static RecentTasksViewModel getInstance() { 130 if (sInstance == null) { 131 sInstance = new RecentTasksViewModel(); 132 } 133 return sInstance; 134 } 135 136 /** 137 * Fetches recent task list asynchronously and communicates changes through 138 * {@link RecentTasksChangeListener}. 139 */ fetchRecentTaskList()140 public void fetchRecentTaskList() { 141 mDataStore.getRecentTasksAsync(); 142 } 143 144 /** 145 * Refreshes the UI associated with recent tasks. 146 * Does not fetch recent task list from the system. 147 */ refreshRecentTaskList()148 public void refreshRecentTaskList() { 149 mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onRecentTasksFetched); 150 } 151 152 /** 153 * @return the {@link Drawable} icon for the given {@code index} or null. 154 */ 155 @Nullable getRecentTaskIconAt(int index)156 public Drawable getRecentTaskIconAt(int index) { 157 if (!safeCheckIndex(mRecentTaskIds, index)) { 158 return null; 159 } 160 return mDataStore.getRecentTaskIcon(mRecentTaskIds.get(index)); 161 } 162 163 /** 164 * @return the {@link Bitmap} thumbnail for the given {@code index} or 165 * default thumbnail(which could be null of not initialised). 166 */ 167 @Nullable getRecentTaskThumbnailAt(int index)168 public Bitmap getRecentTaskThumbnailAt(int index) { 169 if (!safeCheckIndex(mRecentTaskIds, index)) { 170 return null; 171 } 172 if (mTaskIdToCroppedThumbnailMap.containsKey(mRecentTaskIds.get(index))) { 173 return mTaskIdToCroppedThumbnailMap.get(mRecentTaskIds.get(index)); 174 } 175 Bitmap thumbnail = mDataStore.getRecentTaskThumbnail(mRecentTaskIds.get(index)); 176 Rect insets = mDataStore.getRecentTaskInsets(mRecentTaskIds.get(index)); 177 if (thumbnail != null) { 178 Bitmap croppedThumbnail = cropInsets(thumbnail, insets); 179 mTaskIdToCroppedThumbnailMap.put(mRecentTaskIds.get(index), croppedThumbnail); 180 return croppedThumbnail; 181 } 182 return mDefaultThumbnail; 183 } 184 185 /** 186 * @return {@code true} if task for the given {@code index} is disabled. 187 */ isRecentTaskDisabled(int index)188 public boolean isRecentTaskDisabled(int index) { 189 if (mDisabledTaskProvider == null) { 190 return false; 191 } 192 ComponentName componentName = getRecentTaskComponentName(index); 193 return componentName != null && 194 mDisabledTaskProvider.isTaskDisabledFromRecents(componentName); 195 } 196 197 /** 198 * @return the {@link View.OnClickListener} for the task at the given {@code index} or null. 199 */ 200 @Nullable getDisabledTaskClickListener(int index)201 public View.OnClickListener getDisabledTaskClickListener(int index) { 202 if (mDisabledTaskProvider == null) { 203 return null; 204 } 205 ComponentName componentName = getRecentTaskComponentName(index); 206 return componentName != null 207 ? mDisabledTaskProvider.getDisabledTaskClickListener(componentName) : null; 208 } 209 210 @Nullable getRecentTaskComponentName(int index)211 private ComponentName getRecentTaskComponentName(int index) { 212 if (!safeCheckIndex(mRecentTaskIds, index)) { 213 return null; 214 } 215 return mDataStore.getRecentTaskComponentName(mRecentTaskIds.get(index)); 216 } 217 218 /** 219 * Tries to open the recent task at the given {@code index}. 220 * Communicates failure through {@link RecentTasksChangeListener}. 221 */ openRecentTask(int index)222 public void openRecentTask(int index) { 223 if (safeCheckIndex(mRecentTaskIds, index) && 224 mDataStore.openRecentTask(mRecentTaskIds.get(index))) { 225 return; 226 } 227 // failure to open recent task 228 mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onOpenRecentTaskFail); 229 } 230 231 /** 232 * Tries to open the top running task. 233 * Communicates failure through {@link RecentTasksChangeListener}. 234 */ openMostRecentTask()235 public void openMostRecentTask() { 236 if (!mDataStore.openTopRunningTask(CarRecentsActivity.class, mDisplayId)) { 237 mRecentTasksChangeListener.forEach(RecentTasksChangeListener::onOpenTopRunningTaskFail); 238 } 239 } 240 241 /** 242 * Communicates success through {@link RecentTasksChangeListener}. 243 * 244 * @param index index of the task to be removed from recents. 245 */ removeTaskFromRecents(int index)246 public void removeTaskFromRecents(int index) { 247 if (!safeCheckIndex(mRecentTaskIds, index)) { 248 return; 249 } 250 removeTaskWithId(mRecentTaskIds.get(index)); 251 mRecentTaskIds.remove(index); 252 mRecentTasksChangeListener.forEach(l -> l.onRecentTaskRemoved(index)); 253 } 254 255 /** 256 * Removes all tasks from recents and clears cached data by calling {@link #clearCache}. 257 */ removeAllRecentTasks()258 public void removeAllRecentTasks() { 259 for (int recentTaskId : mRecentTaskIds) { 260 removeTaskWithId(recentTaskId); 261 } 262 clearCache(); 263 } 264 265 /** 266 * Clears cached data. 267 * Communicates success through {@link RecentTasksChangeListener}. 268 */ clearCache()269 public void clearCache() { 270 mDataStore.clearCache(); 271 mTaskIdToCroppedThumbnailMap.clear(); 272 int countRemoved = mRecentTaskIds.size(); 273 mRecentTaskIds.clear(); 274 mRecentTasksChangeListener.forEach(l -> l.onAllRecentTasksRemoved(countRemoved)); 275 } 276 277 /** 278 * @return the length of the recent task list 279 */ getRecentTasksSize()280 public int getRecentTasksSize() { 281 return mRecentTaskIds.size(); 282 } 283 284 /** 285 * Used to map relative indexes to absolute indexes based on tasks hidden by 286 * {@link HiddenTaskProvider}. 287 */ mapAbsoluteTasksToShownTasks()288 private void mapAbsoluteTasksToShownTasks() { 289 List<Integer> recentTaskIds = mDataStore.getRecentTaskIds(); 290 mRecentTaskIds = new ArrayList<>(recentTaskIds.size()); 291 for (int taskId : recentTaskIds) { 292 ComponentName topComponent = mDataStore.getRecentTaskComponentName(taskId); 293 Intent baseIntent = mDataStore.getRecentTaskBaseIntent(taskId); 294 boolean isTaskHidden = mHiddenTaskProviders.stream() 295 .anyMatch(p -> p.isTaskHiddenFromRecents( 296 topComponent != null ? topComponent.getPackageName() : null, 297 topComponent != null ? topComponent.getClassName() : null, 298 baseIntent)); 299 if (isTaskHidden) { 300 // skip since it should be hidden 301 continue; 302 } 303 mRecentTaskIds.add(taskId); 304 } 305 } 306 307 @NonNull cropInsets(Bitmap bitmap, Rect insets)308 private Bitmap cropInsets(Bitmap bitmap, Rect insets) { 309 return Bitmap.createBitmap(bitmap, insets.left, insets.top, 310 /* width= */ bitmap.getWidth() - insets.left - insets.right, 311 /* height= */ bitmap.getHeight() - insets.top - insets.bottom); 312 } 313 314 /** 315 * @return a new {@link Bitmap} with aspect ratio of the current window and the given 316 * {@code color}. 317 */ createThumbnail(@olorInt Integer color)318 public Bitmap createThumbnail(@ColorInt Integer color) { 319 return createThumbnail(mWindowWidth, mWindowHeight, mWindowInsets, color); 320 } 321 createThumbnail(int width, int height, @NonNull Rect insets, @ColorInt Integer color)322 private Bitmap createThumbnail(int width, int height, @NonNull Rect insets, 323 @ColorInt Integer color) { 324 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 325 Canvas canvas = new Canvas(bitmap); 326 canvas.drawColor(color); 327 return cropInsets(bitmap, insets); 328 } 329 safeCheckIndex(List<?> list, int index)330 private boolean safeCheckIndex(List<?> list, int index) { 331 return index >= 0 && index < list.size(); 332 } 333 removeTaskWithId(int taskId)334 private void removeTaskWithId(int taskId) { 335 mDataStore.removeTaskFromRecents(taskId); 336 mTaskIdToCroppedThumbnailMap.remove(taskId); 337 } 338 339 /** 340 * @param listener listener to send changes in recent task list to. 341 */ addRecentTasksChangeListener(RecentTasksChangeListener listener)342 public void addRecentTasksChangeListener(RecentTasksChangeListener listener) { 343 mRecentTasksChangeListener.add(listener); 344 } 345 346 /** 347 * @param listener remove the given listener. 348 */ removeAllRecentTasksChangeListeners(RecentTasksChangeListener listener)349 public void removeAllRecentTasksChangeListeners(RecentTasksChangeListener listener) { 350 mRecentTasksChangeListener.remove(listener); 351 } 352 353 /** 354 * @param provider provider of packages to be hidden from recents. 355 */ addHiddenTaskProvider(HiddenTaskProvider provider)356 public void addHiddenTaskProvider(HiddenTaskProvider provider) { 357 mHiddenTaskProviders.add(provider); 358 } 359 360 /** 361 * @param provider remove the given provider. 362 */ removeHiddenTaskProvider(HiddenTaskProvider provider)363 public void removeHiddenTaskProvider(HiddenTaskProvider provider) { 364 mHiddenTaskProviders.remove(provider); 365 } 366 367 /** 368 * @param provider provider of packages to be disabled in recents. 369 */ setDisabledTaskProvider(DisabledTaskProvider provider)370 public void setDisabledTaskProvider(DisabledTaskProvider provider) { 371 mDisabledTaskProvider = provider; 372 } 373 374 /** 375 * Listen to changes in the recents. 376 */ 377 public interface RecentTasksChangeListener { 378 /** 379 * Called when recent tasks have been fetched from the system. 380 */ onRecentTasksFetched()381 default void onRecentTasksFetched() { 382 } 383 384 /** 385 * @param position position whose thumbnail has been changed. 386 */ onTaskThumbnailChange(int position)387 default void onTaskThumbnailChange(int position) { 388 } 389 390 /** 391 * @param position position whose icon has been changed. 392 */ onTaskIconChange(int position)393 default void onTaskIconChange(int position) { 394 } 395 396 /** 397 * Called when system fails to open a recent task. 398 */ onOpenRecentTaskFail()399 default void onOpenRecentTaskFail() { 400 } 401 402 /** 403 * Called when system fails to open the top task. 404 */ onOpenTopRunningTaskFail()405 default void onOpenTopRunningTaskFail() { 406 } 407 408 /** 409 * @param countRemoved number of recent tasks removed. 410 */ onAllRecentTasksRemoved(int countRemoved)411 default void onAllRecentTasksRemoved(int countRemoved) { 412 } 413 414 /** 415 * @param position position at which the recent task was removed. 416 */ onRecentTaskRemoved(int position)417 default void onRecentTaskRemoved(int position) { 418 } 419 } 420 421 /** 422 * Decides if a task should be hidden from recents. 423 * This is necessary to be able to get tasks to be hidden at runtime. 424 */ 425 public interface HiddenTaskProvider { 426 /** 427 * @return if the task should be hidden from recents. 428 */ isTaskHiddenFromRecents(String packageName, String className, Intent baseIntent)429 boolean isTaskHiddenFromRecents(String packageName, String className, Intent baseIntent); 430 } 431 432 /** 433 * Decides if a task is disabled in recents. 434 * This is necessary to be able to get tasks to be disabled at runtime. 435 * Note: Hidden tasks cannot be disabled. 436 */ 437 public interface DisabledTaskProvider { 438 /** 439 * @return if the task associated with {@code componentName} is disabled in recents. 440 */ isTaskDisabledFromRecents(ComponentName componentName)441 boolean isTaskDisabledFromRecents(ComponentName componentName); 442 443 /** 444 * @return {@link View.OnClickListener} to be called when user tries to click on 445 * disabled task associated with {@code componentName}. 446 */ getDisabledTaskClickListener(ComponentName componentName)447 default View.OnClickListener getDisabledTaskClickListener(ComponentName componentName) { 448 return null; 449 } 450 } 451 } 452