1 /* 2 * Copyright (C) 2014 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.systemui.recents.model; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentCallbacks2; 21 import android.content.Context; 22 import android.content.pm.ActivityInfo; 23 import android.content.res.Resources; 24 import android.graphics.Bitmap; 25 import android.graphics.drawable.BitmapDrawable; 26 import android.graphics.drawable.Drawable; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.UserHandle; 30 import android.util.Log; 31 32 import com.android.systemui.R; 33 import com.android.systemui.recents.Constants; 34 import com.android.systemui.recents.RecentsConfiguration; 35 import com.android.systemui.recents.misc.SystemServicesProxy; 36 37 import java.util.ArrayList; 38 import java.util.Collection; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.concurrent.ConcurrentLinkedQueue; 43 44 45 /** Handle to an ActivityInfo */ 46 class ActivityInfoHandle { 47 ActivityInfo info; 48 } 49 50 /** A bitmap load queue */ 51 class TaskResourceLoadQueue { 52 ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); 53 54 /** Adds a new task to the load queue */ 55 void addTasks(Collection<Task> tasks) { 56 for (Task t : tasks) { 57 if (!mQueue.contains(t)) { 58 mQueue.add(t); 59 } 60 } 61 synchronized(this) { 62 notifyAll(); 63 } 64 } 65 66 /** Adds a new task to the load queue */ 67 void addTask(Task t) { 68 if (!mQueue.contains(t)) { 69 mQueue.add(t); 70 } 71 synchronized(this) { 72 notifyAll(); 73 } 74 } 75 76 /** 77 * Retrieves the next task from the load queue, as well as whether we want that task to be 78 * force reloaded. 79 */ 80 Task nextTask() { 81 return mQueue.poll(); 82 } 83 84 /** Removes a task from the load queue */ 85 void removeTask(Task t) { 86 mQueue.remove(t); 87 } 88 89 /** Clears all the tasks from the load queue */ 90 void clearTasks() { 91 mQueue.clear(); 92 } 93 94 /** Returns whether the load queue is empty */ 95 boolean isEmpty() { 96 return mQueue.isEmpty(); 97 } 98 } 99 100 /* Task resource loader */ 101 class TaskResourceLoader implements Runnable { 102 Context mContext; 103 HandlerThread mLoadThread; 104 Handler mLoadThreadHandler; 105 Handler mMainThreadHandler; 106 107 SystemServicesProxy mSystemServicesProxy; 108 TaskResourceLoadQueue mLoadQueue; 109 DrawableLruCache mApplicationIconCache; 110 BitmapLruCache mThumbnailCache; 111 Bitmap mDefaultThumbnail; 112 BitmapDrawable mDefaultApplicationIcon; 113 114 boolean mCancelled; 115 boolean mWaitingOnLoadQueue; 116 117 /** Constructor, creates a new loading thread that loads task resources in the background */ 118 public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache applicationIconCache, 119 BitmapLruCache thumbnailCache, Bitmap defaultThumbnail, 120 BitmapDrawable defaultApplicationIcon) { 121 mLoadQueue = loadQueue; 122 mApplicationIconCache = applicationIconCache; 123 mThumbnailCache = thumbnailCache; 124 mDefaultThumbnail = defaultThumbnail; 125 mDefaultApplicationIcon = defaultApplicationIcon; 126 mMainThreadHandler = new Handler(); 127 mLoadThread = new HandlerThread("Recents-TaskResourceLoader", 128 android.os.Process.THREAD_PRIORITY_BACKGROUND); 129 mLoadThread.start(); 130 mLoadThreadHandler = new Handler(mLoadThread.getLooper()); 131 mLoadThreadHandler.post(this); 132 } 133 134 /** Restarts the loader thread */ 135 void start(Context context) { 136 mContext = context; 137 mCancelled = false; 138 mSystemServicesProxy = new SystemServicesProxy(context); 139 // Notify the load thread to start loading 140 synchronized(mLoadThread) { 141 mLoadThread.notifyAll(); 142 } 143 } 144 145 /** Requests the loader thread to stop after the current iteration */ 146 void stop() { 147 // Mark as cancelled for the thread to pick up 148 mCancelled = true; 149 mSystemServicesProxy = null; 150 // If we are waiting for the load queue for more tasks, then we can just reset the 151 // Context now, since nothing is using it 152 if (mWaitingOnLoadQueue) { 153 mContext = null; 154 } 155 } 156 157 @Override 158 public void run() { 159 while (true) { 160 if (mCancelled) { 161 // We have to unset the context here, since the background thread may be using it 162 // when we call stop() 163 mContext = null; 164 // If we are cancelled, then wait until we are started again 165 synchronized(mLoadThread) { 166 try { 167 mLoadThread.wait(); 168 } catch (InterruptedException ie) { 169 ie.printStackTrace(); 170 } 171 } 172 } else { 173 SystemServicesProxy ssp = mSystemServicesProxy; 174 175 // Load the next item from the queue 176 final Task t = mLoadQueue.nextTask(); 177 if (t != null) { 178 Drawable cachedIcon = mApplicationIconCache.get(t.key); 179 Bitmap cachedThumbnail = mThumbnailCache.get(t.key); 180 181 // Load the application icon if it is stale or we haven't cached one yet 182 if (cachedIcon == null) { 183 cachedIcon = getTaskDescriptionIcon(t.key, t.icon, t.iconFilename, ssp, 184 mContext.getResources()); 185 186 if (cachedIcon == null) { 187 ActivityInfo info = ssp.getActivityInfo(t.key.baseIntent.getComponent(), 188 t.key.userId); 189 if (info != null) { 190 cachedIcon = ssp.getActivityIcon(info, t.key.userId); 191 } 192 } 193 194 if (cachedIcon == null) { 195 cachedIcon = mDefaultApplicationIcon; 196 } 197 198 // At this point, even if we can't load the icon, we will set the default 199 // icon. 200 mApplicationIconCache.put(t.key, cachedIcon); 201 } 202 // Load the thumbnail if it is stale or we haven't cached one yet 203 if (cachedThumbnail == null) { 204 cachedThumbnail = ssp.getTaskThumbnail(t.key.id); 205 if (cachedThumbnail != null) { 206 cachedThumbnail.setHasAlpha(false); 207 } else { 208 cachedThumbnail = mDefaultThumbnail; 209 } 210 mThumbnailCache.put(t.key, cachedThumbnail); 211 } 212 if (!mCancelled) { 213 // Notify that the task data has changed 214 final Drawable newIcon = cachedIcon; 215 final Bitmap newThumbnail = cachedThumbnail == mDefaultThumbnail 216 ? null : cachedThumbnail; 217 mMainThreadHandler.post(new Runnable() { 218 @Override 219 public void run() { 220 t.notifyTaskDataLoaded(newThumbnail, newIcon); 221 } 222 }); 223 } 224 } 225 226 // If there are no other items in the list, then just wait until something is added 227 if (!mCancelled && mLoadQueue.isEmpty()) { 228 synchronized(mLoadQueue) { 229 try { 230 mWaitingOnLoadQueue = true; 231 mLoadQueue.wait(); 232 mWaitingOnLoadQueue = false; 233 } catch (InterruptedException ie) { 234 ie.printStackTrace(); 235 } 236 } 237 } 238 } 239 } 240 } 241 242 Drawable getTaskDescriptionIcon(Task.TaskKey taskKey, Bitmap iconBitmap, String iconFilename, 243 SystemServicesProxy ssp, Resources res) { 244 Bitmap tdIcon = iconBitmap != null 245 ? iconBitmap 246 : ActivityManager.TaskDescription.loadTaskDescriptionIcon(iconFilename); 247 if (tdIcon != null) { 248 return ssp.getBadgedIcon(new BitmapDrawable(res, tdIcon), taskKey.userId); 249 } 250 return null; 251 } 252 } 253 254 /* Recents task loader 255 * NOTE: We should not hold any references to a Context from a static instance */ 256 public class RecentsTaskLoader { 257 private static final String TAG = "RecentsTaskLoader"; 258 259 static RecentsTaskLoader sInstance; 260 261 SystemServicesProxy mSystemServicesProxy; 262 DrawableLruCache mApplicationIconCache; 263 BitmapLruCache mThumbnailCache; 264 StringLruCache mActivityLabelCache; 265 TaskResourceLoadQueue mLoadQueue; 266 TaskResourceLoader mLoader; 267 268 RecentsPackageMonitor mPackageMonitor; 269 270 int mMaxThumbnailCacheSize; 271 int mMaxIconCacheSize; 272 273 BitmapDrawable mDefaultApplicationIcon; 274 Bitmap mDefaultThumbnail; 275 276 /** Private Constructor */ 277 private RecentsTaskLoader(Context context) { 278 mMaxThumbnailCacheSize = context.getResources().getInteger( 279 R.integer.config_recents_max_thumbnail_count); 280 mMaxIconCacheSize = context.getResources().getInteger( 281 R.integer.config_recents_max_icon_count); 282 int iconCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : 283 mMaxIconCacheSize; 284 int thumbnailCacheSize = Constants.DebugFlags.App.DisableBackgroundCache ? 1 : 285 mMaxThumbnailCacheSize; 286 287 // Create the default assets 288 Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); 289 icon.eraseColor(0x00000000); 290 mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); 291 mDefaultThumbnail.setHasAlpha(false); 292 mDefaultThumbnail.eraseColor(0xFFffffff); 293 mDefaultApplicationIcon = new BitmapDrawable(context.getResources(), icon); 294 295 // Initialize the proxy, cache and loaders 296 mSystemServicesProxy = new SystemServicesProxy(context); 297 mPackageMonitor = new RecentsPackageMonitor(); 298 mLoadQueue = new TaskResourceLoadQueue(); 299 mApplicationIconCache = new DrawableLruCache(iconCacheSize); 300 mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); 301 mActivityLabelCache = new StringLruCache(100); 302 mLoader = new TaskResourceLoader(mLoadQueue, mApplicationIconCache, mThumbnailCache, 303 mDefaultThumbnail, mDefaultApplicationIcon); 304 } 305 306 /** Initializes the recents task loader */ 307 public static RecentsTaskLoader initialize(Context context) { 308 if (sInstance == null) { 309 sInstance = new RecentsTaskLoader(context); 310 } 311 return sInstance; 312 } 313 314 /** Returns the current recents task loader */ 315 public static RecentsTaskLoader getInstance() { 316 return sInstance; 317 } 318 319 /** Returns the system services proxy */ 320 public SystemServicesProxy getSystemServicesProxy() { 321 return mSystemServicesProxy; 322 } 323 324 /** Gets the list of recent tasks, ordered from back to front. */ 325 private static List<ActivityManager.RecentTaskInfo> getRecentTasks(SystemServicesProxy ssp, 326 boolean isTopTaskHome) { 327 RecentsConfiguration config = RecentsConfiguration.getInstance(); 328 List<ActivityManager.RecentTaskInfo> tasks = 329 ssp.getRecentTasks(config.maxNumTasksToLoad, UserHandle.CURRENT.getIdentifier(), 330 isTopTaskHome); 331 Collections.reverse(tasks); 332 return tasks; 333 } 334 335 /** Returns the activity icon using as many cached values as we can. */ 336 public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, 337 ActivityManager.TaskDescription td, SystemServicesProxy ssp, 338 Resources res, ActivityInfoHandle infoHandle, boolean preloadTask) { 339 // Return the cached activity icon if it exists 340 Drawable icon = mApplicationIconCache.getAndInvalidateIfModified(taskKey); 341 if (icon != null) { 342 return icon; 343 } 344 345 // If we are preloading this task, continue to load the task description icon or the 346 // activity icon 347 if (preloadTask) { 348 349 // Return and cache the task description icon if it exists 350 Drawable tdDrawable = mLoader.getTaskDescriptionIcon(taskKey, td.getInMemoryIcon(), 351 td.getIconFilename(), ssp, res); 352 if (tdDrawable != null) { 353 mApplicationIconCache.put(taskKey, tdDrawable); 354 return tdDrawable; 355 } 356 357 // Load the icon from the activity info and cache it 358 if (infoHandle.info == null) { 359 infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), 360 taskKey.userId); 361 } 362 if (infoHandle.info != null) { 363 icon = ssp.getActivityIcon(infoHandle.info, taskKey.userId); 364 if (icon != null) { 365 mApplicationIconCache.put(taskKey, icon); 366 return icon; 367 } 368 } 369 } 370 // If we couldn't load any icon, return null 371 return null; 372 } 373 374 /** Returns the activity label using as many cached values as we can. */ 375 public String getAndUpdateActivityLabel(Task.TaskKey taskKey, 376 ActivityManager.TaskDescription td, SystemServicesProxy ssp, 377 ActivityInfoHandle infoHandle) { 378 // Return the task description label if it exists 379 if (td != null && td.getLabel() != null) { 380 return td.getLabel(); 381 } 382 // Return the cached activity label if it exists 383 String label = mActivityLabelCache.getAndInvalidateIfModified(taskKey); 384 if (label != null) { 385 return label; 386 } 387 // All short paths failed, load the label from the activity info and cache it 388 if (infoHandle.info == null) { 389 infoHandle.info = ssp.getActivityInfo(taskKey.baseIntent.getComponent(), 390 taskKey.userId); 391 } 392 if (infoHandle.info != null) { 393 label = ssp.getActivityLabel(infoHandle.info); 394 mActivityLabelCache.put(taskKey, label); 395 } else { 396 Log.w(TAG, "Missing ActivityInfo for " + taskKey.baseIntent.getComponent() 397 + " u=" + taskKey.userId); 398 } 399 return label; 400 } 401 402 /** Returns the activity's primary color. */ 403 public int getActivityPrimaryColor(ActivityManager.TaskDescription td, 404 RecentsConfiguration config) { 405 if (td != null && td.getPrimaryColor() != 0) { 406 return td.getPrimaryColor(); 407 } 408 return config.taskBarViewDefaultBackgroundColor; 409 } 410 411 /** Reload the set of recent tasks */ 412 public SpaceNode reload(Context context, int preloadCount, boolean isTopTaskHome) { 413 ArrayList<Task.TaskKey> taskKeys = new ArrayList<Task.TaskKey>(); 414 ArrayList<Task> tasksToLoad = new ArrayList<Task>(); 415 TaskStack stack = getTaskStack(mSystemServicesProxy, context.getResources(), 416 -1, preloadCount, true, isTopTaskHome, taskKeys, tasksToLoad); 417 SpaceNode root = new SpaceNode(); 418 root.setStack(stack); 419 420 // Start the task loader and add all the tasks we need to load 421 mLoader.start(context); 422 mLoadQueue.addTasks(tasksToLoad); 423 424 // Update the package monitor with the list of packages to listen for 425 mPackageMonitor.setTasks(taskKeys); 426 427 return root; 428 } 429 430 /** Creates a lightweight stack of the current recent tasks, without thumbnails and icons. */ 431 public TaskStack getTaskStack(SystemServicesProxy ssp, Resources res, 432 int preloadTaskId, int preloadTaskCount, 433 boolean loadTaskThumbnails, boolean isTopTaskHome, 434 List<Task.TaskKey> taskKeysOut, List<Task> tasksToLoadOut) { 435 RecentsConfiguration config = RecentsConfiguration.getInstance(); 436 List<ActivityManager.RecentTaskInfo> tasks = getRecentTasks(ssp, isTopTaskHome); 437 HashMap<Task.ComponentNameKey, ActivityInfoHandle> activityInfoCache = 438 new HashMap<Task.ComponentNameKey, ActivityInfoHandle>(); 439 ArrayList<Task> tasksToAdd = new ArrayList<Task>(); 440 TaskStack stack = new TaskStack(); 441 442 int taskCount = tasks.size(); 443 for (int i = 0; i < taskCount; i++) { 444 ActivityManager.RecentTaskInfo t = tasks.get(i); 445 446 // Compose the task key 447 Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.baseIntent, t.userId, 448 t.firstActiveTime, t.lastActiveTime); 449 450 // Get an existing activity info handle if possible 451 Task.ComponentNameKey cnKey = taskKey.getComponentNameKey(); 452 ActivityInfoHandle infoHandle; 453 boolean hasCachedActivityInfo = false; 454 if (activityInfoCache.containsKey(cnKey)) { 455 infoHandle = activityInfoCache.get(cnKey); 456 hasCachedActivityInfo = true; 457 } else { 458 infoHandle = new ActivityInfoHandle(); 459 } 460 461 // Determine whether to preload this task 462 boolean preloadTask = false; 463 if (preloadTaskId > 0) { 464 preloadTask = (t.id == preloadTaskId); 465 } else if (preloadTaskCount > 0) { 466 preloadTask = (i >= (taskCount - preloadTaskCount)); 467 } 468 469 // Load the label, icon, and color 470 String activityLabel = getAndUpdateActivityLabel(taskKey, t.taskDescription, 471 ssp, infoHandle); 472 Drawable activityIcon = getAndUpdateActivityIcon(taskKey, t.taskDescription, 473 ssp, res, infoHandle, preloadTask); 474 int activityColor = getActivityPrimaryColor(t.taskDescription, config); 475 476 // Update the activity info cache 477 if (!hasCachedActivityInfo && infoHandle.info != null) { 478 activityInfoCache.put(cnKey, infoHandle); 479 } 480 481 Bitmap icon = t.taskDescription != null 482 ? t.taskDescription.getInMemoryIcon() 483 : null; 484 String iconFilename = t.taskDescription != null 485 ? t.taskDescription.getIconFilename() 486 : null; 487 488 // Add the task to the stack 489 Task task = new Task(taskKey, (t.id > -1), t.affiliatedTaskId, t.affiliatedTaskColor, 490 activityLabel, activityIcon, activityColor, (i == (taskCount - 1)), 491 config.lockToAppEnabled, icon, iconFilename); 492 493 if (preloadTask && loadTaskThumbnails) { 494 // Load the thumbnail from the cache if possible 495 task.thumbnail = mThumbnailCache.getAndInvalidateIfModified(taskKey); 496 if (task.thumbnail == null) { 497 // Load the thumbnail from the system 498 task.thumbnail = ssp.getTaskThumbnail(taskKey.id); 499 if (task.thumbnail != null) { 500 task.thumbnail.setHasAlpha(false); 501 mThumbnailCache.put(taskKey, task.thumbnail); 502 } 503 } 504 if (task.thumbnail == null && tasksToLoadOut != null) { 505 // Either the task has changed since the last active time, or it was not 506 // previously cached, so try and load the task anew. 507 tasksToLoadOut.add(task); 508 } 509 } 510 511 // Add to the list of task keys 512 if (taskKeysOut != null) { 513 taskKeysOut.add(taskKey); 514 } 515 // Add the task to the stack 516 tasksToAdd.add(task); 517 } 518 stack.setTasks(tasksToAdd); 519 stack.createAffiliatedGroupings(config); 520 return stack; 521 } 522 523 /** Acquires the task resource data directly from the pool. */ 524 public void loadTaskData(Task t) { 525 Drawable applicationIcon = mApplicationIconCache.getAndInvalidateIfModified(t.key); 526 Bitmap thumbnail = mThumbnailCache.getAndInvalidateIfModified(t.key); 527 528 // Grab the thumbnail/icon from the cache, if either don't exist, then trigger a reload and 529 // use the default assets in their place until they load 530 boolean requiresLoad = (applicationIcon == null) || (thumbnail == null); 531 applicationIcon = applicationIcon != null ? applicationIcon : mDefaultApplicationIcon; 532 if (requiresLoad) { 533 mLoadQueue.addTask(t); 534 } 535 t.notifyTaskDataLoaded(thumbnail == mDefaultThumbnail ? null : thumbnail, applicationIcon); 536 } 537 538 /** Releases the task resource data back into the pool. */ 539 public void unloadTaskData(Task t) { 540 mLoadQueue.removeTask(t); 541 t.notifyTaskDataUnloaded(null, mDefaultApplicationIcon); 542 } 543 544 /** Completely removes the resource data from the pool. */ 545 public void deleteTaskData(Task t, boolean notifyTaskDataUnloaded) { 546 mLoadQueue.removeTask(t); 547 mThumbnailCache.remove(t.key); 548 mApplicationIconCache.remove(t.key); 549 if (notifyTaskDataUnloaded) { 550 t.notifyTaskDataUnloaded(null, mDefaultApplicationIcon); 551 } 552 } 553 554 /** Stops the task loader and clears all pending tasks */ 555 void stopLoader() { 556 mLoader.stop(); 557 mLoadQueue.clearTasks(); 558 } 559 560 /** Registers any broadcast receivers. */ 561 public void registerReceivers(Context context, RecentsPackageMonitor.PackageCallbacks cb) { 562 // Register the broadcast receiver to handle messages related to packages being added/removed 563 mPackageMonitor.register(context, cb); 564 } 565 566 /** Unregisters any broadcast receivers. */ 567 public void unregisterReceivers() { 568 mPackageMonitor.unregister(); 569 } 570 571 /** 572 * Handles signals from the system, trimming memory when requested to prevent us from running 573 * out of memory. 574 */ 575 public void onTrimMemory(int level) { 576 switch (level) { 577 case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN: 578 // Stop the loader immediately when the UI is no longer visible 579 stopLoader(); 580 mThumbnailCache.trimToSize(Math.max( 581 Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount, 582 mMaxThumbnailCacheSize / 2)); 583 mApplicationIconCache.trimToSize(Math.max( 584 Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount, 585 mMaxIconCacheSize / 2)); 586 break; 587 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE: 588 case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND: 589 // We are leaving recents, so trim the data a bit 590 mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 2); 591 mApplicationIconCache.trimToSize(mMaxIconCacheSize / 2); 592 break; 593 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW: 594 case ComponentCallbacks2.TRIM_MEMORY_MODERATE: 595 // We are going to be low on memory 596 mThumbnailCache.trimToSize(mMaxThumbnailCacheSize / 4); 597 mApplicationIconCache.trimToSize(mMaxIconCacheSize / 4); 598 break; 599 case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL: 600 case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: 601 // We are low on memory, so release everything 602 mThumbnailCache.evictAll(); 603 mApplicationIconCache.evictAll(); 604 // The cache is small, only clear the label cache when we are critical 605 mActivityLabelCache.evictAll(); 606 break; 607 default: 608 break; 609 } 610 } 611 } 612