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