RecentsImpl.java revision d64ef3ef33c50a03b4be3b2baaa93aab7319fca8
1/* 2 * Copyright (C) 2015 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 17package com.android.systemui.recents; 18 19import static android.app.ActivityManager.FREEFORM_WORKSPACE_STACK_ID; 20 21import android.app.ActivityManager; 22import android.app.ActivityOptions; 23import android.app.ITaskStackListener; 24import android.content.ActivityNotFoundException; 25import android.content.Context; 26import android.content.Intent; 27import android.content.res.Resources; 28import android.graphics.Bitmap; 29import android.graphics.Canvas; 30import android.graphics.Rect; 31import android.graphics.RectF; 32import android.os.Handler; 33import android.os.SystemClock; 34import android.os.UserHandle; 35import android.util.MutableBoolean; 36import android.view.AppTransitionAnimationSpec; 37import android.view.LayoutInflater; 38import android.view.View; 39 40import com.android.internal.logging.MetricsLogger; 41import com.android.systemui.Prefs; 42import com.android.systemui.R; 43import com.android.systemui.SystemUIApplication; 44import com.android.systemui.recents.events.EventBus; 45import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationStartedEvent; 46import com.android.systemui.recents.events.activity.HideRecentsEvent; 47import com.android.systemui.recents.events.activity.IterateRecentsEvent; 48import com.android.systemui.recents.events.activity.ToggleRecentsEvent; 49import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent; 50import com.android.systemui.recents.events.component.ScreenPinningRequestEvent; 51import com.android.systemui.recents.misc.Console; 52import com.android.systemui.recents.misc.ForegroundThread; 53import com.android.systemui.recents.misc.SystemServicesProxy; 54import com.android.systemui.recents.model.RecentsTaskLoadPlan; 55import com.android.systemui.recents.model.RecentsTaskLoader; 56import com.android.systemui.recents.model.Task; 57import com.android.systemui.recents.model.TaskGrouping; 58import com.android.systemui.recents.model.TaskStack; 59import com.android.systemui.recents.views.TaskStackView; 60import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm; 61import com.android.systemui.recents.views.TaskViewHeader; 62import com.android.systemui.recents.views.TaskViewTransform; 63import com.android.systemui.statusbar.phone.PhoneStatusBar; 64 65import java.util.ArrayList; 66 67/** 68 * An implementation of the Recents component for the current user. For secondary users, this can 69 * be called remotely from the system user. 70 */ 71public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub 72 implements ActivityOptions.OnAnimationStartedListener { 73 74 private final static String TAG = "RecentsImpl"; 75 private final static boolean DEBUG = false; 76 77 private final static int sMinToggleDelay = 350; 78 79 public final static String RECENTS_PACKAGE = "com.android.systemui"; 80 public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity"; 81 82 83 /** 84 * An implementation of ITaskStackListener, that allows us to listen for changes to the system 85 * task stacks and update recents accordingly. 86 */ 87 class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable { 88 Handler mHandler; 89 90 public TaskStackListenerImpl(Handler handler) { 91 mHandler = handler; 92 } 93 94 @Override 95 public void onTaskStackChanged() { 96 // Debounce any task stack changes 97 mHandler.removeCallbacks(this); 98 mHandler.post(this); 99 } 100 101 /** Preloads the next task */ 102 public void run() { 103 // TODO: Temporarily skip this if multi stack is enabled 104 /* 105 RecentsConfiguration config = RecentsConfiguration.getInstance(); 106 if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) { 107 RecentsTaskLoader loader = Recents.getTaskLoader(); 108 SystemServicesProxy ssp = Recents.getSystemServices(); 109 ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask(); 110 111 // Load the next task only if we aren't svelte 112 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); 113 loader.preloadTasks(plan, true); 114 RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); 115 // This callback is made when a new activity is launched and the old one is paused 116 // so ignore the current activity and try and preload the thumbnail for the 117 // previous one. 118 if (runningTaskInfo != null) { 119 launchOpts.runningTaskId = runningTaskInfo.id; 120 } 121 launchOpts.numVisibleTasks = 2; 122 launchOpts.numVisibleTaskThumbnails = 2; 123 launchOpts.onlyLoadForCache = true; 124 launchOpts.onlyLoadPausedActivities = true; 125 loader.loadTasks(mContext, plan, launchOpts); 126 } 127 */ 128 } 129 } 130 131 private static RecentsTaskLoadPlan sInstanceLoadPlan; 132 133 Context mContext; 134 Handler mHandler; 135 TaskStackListenerImpl mTaskStackListener; 136 RecentsAppWidgetHost mAppWidgetHost; 137 boolean mBootCompleted; 138 boolean mStartAnimationTriggered; 139 boolean mCanReuseTaskStackViews = true; 140 141 // Task launching 142 RecentsConfiguration mConfig; 143 Rect mSearchBarBounds = new Rect(); 144 Rect mTaskStackBounds = new Rect(); 145 Rect mLastTaskViewBounds = new Rect(); 146 TaskViewTransform mTmpTransform = new TaskViewTransform(); 147 int mStatusBarHeight; 148 int mNavBarHeight; 149 int mNavBarWidth; 150 int mTaskBarHeight; 151 152 // Header (for transition) 153 TaskViewHeader mHeaderBar; 154 final Object mHeaderBarLock = new Object(); 155 TaskStackView mDummyStackView; 156 157 // Variables to keep track of if we need to start recents after binding 158 boolean mTriggeredFromAltTab; 159 long mLastToggleTime; 160 161 Bitmap mThumbnailTransitionBitmapCache; 162 Task mThumbnailTransitionBitmapCacheKey; 163 164 165 public RecentsImpl(Context context) { 166 mContext = context; 167 mHandler = new Handler(); 168 mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId); 169 Resources res = mContext.getResources(); 170 LayoutInflater inflater = LayoutInflater.from(mContext); 171 172 // Initialize the static foreground thread 173 ForegroundThread.get(); 174 175 // Register the task stack listener 176 mTaskStackListener = new TaskStackListenerImpl(mHandler); 177 SystemServicesProxy ssp = Recents.getSystemServices(); 178 ssp.registerTaskStackListener(mTaskStackListener); 179 180 // Initialize the static configuration resources 181 mConfig = RecentsConfiguration.initialize(mContext, ssp); 182 mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 183 mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height); 184 mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width); 185 mTaskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height); 186 mDummyStackView = new TaskStackView(mContext, new TaskStack()); 187 mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header, 188 null, false); 189 reloadHeaderBarLayout(true /* tryAndBindSearchWidget */); 190 191 // When we start, preload the data associated with the previous recent tasks. 192 // We can use a new plan since the caches will be the same. 193 RecentsTaskLoader loader = Recents.getTaskLoader(); 194 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); 195 loader.preloadTasks(plan, true /* isTopTaskHome */); 196 RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); 197 launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize(); 198 launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize(); 199 launchOpts.onlyLoadForCache = true; 200 loader.loadTasks(mContext, plan, launchOpts); 201 } 202 203 public void onBootCompleted() { 204 mBootCompleted = true; 205 reloadHeaderBarLayout(true /* tryAndBindSearchWidget */); 206 } 207 208 @Override 209 public void onConfigurationChanged() { 210 // Don't reuse task stack views if the configuration changes 211 mCanReuseTaskStackViews = false; 212 mConfig.updateOnConfigurationChange(); 213 } 214 215 /** 216 * This is only called from the system user's Recents. Secondary users will instead proxy their 217 * visibility change events through to the system user via 218 * {@link Recents#onBusEvent(RecentsVisibilityChangedEvent)}. 219 */ 220 public void onVisibilityChanged(Context context, boolean visible) { 221 SystemUIApplication app = (SystemUIApplication) context; 222 PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class); 223 if (statusBar != null) { 224 statusBar.updateRecentsVisibility(visible); 225 } 226 } 227 228 /** 229 * This is only called from the system user's Recents. Secondary users will instead proxy their 230 * visibility change events through to the system user via 231 * {@link Recents#onBusEvent(ScreenPinningRequestEvent)}. 232 */ 233 public void onStartScreenPinning(Context context) { 234 SystemUIApplication app = (SystemUIApplication) context; 235 PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class); 236 if (statusBar != null) { 237 statusBar.showScreenPinningRequest(false); 238 } 239 } 240 241 @Override 242 public void showRecents(boolean triggeredFromAltTab) { 243 mTriggeredFromAltTab = triggeredFromAltTab; 244 245 try { 246 // Check if the top task is in the home stack, and start the recents activity 247 SystemServicesProxy ssp = Recents.getSystemServices(); 248 ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask(); 249 MutableBoolean isTopTaskHome = new MutableBoolean(true); 250 if (topTask == null || !ssp.isRecentsTopMost(topTask, isTopTaskHome)) { 251 startRecentsActivity(topTask, isTopTaskHome.value); 252 } 253 } catch (ActivityNotFoundException e) { 254 Console.logRawError("Failed to launch RecentAppsIntent", e); 255 } 256 } 257 258 @Override 259 public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { 260 if (mBootCompleted) { 261 // Defer to the activity to handle hiding recents, if it handles it, then it must still 262 // be visible 263 EventBus.getDefault().post(new HideRecentsEvent(triggeredFromAltTab, 264 triggeredFromHomeKey)); 265 } 266 } 267 268 @Override 269 public void toggleRecents() { 270 mTriggeredFromAltTab = false; 271 272 try { 273 SystemServicesProxy ssp = Recents.getSystemServices(); 274 ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask(); 275 MutableBoolean isTopTaskHome = new MutableBoolean(true); 276 if (topTask != null && ssp.isRecentsTopMost(topTask, isTopTaskHome)) { 277 if (Constants.DebugFlags.App.EnableFastToggleRecents) { 278 // Notify recents to move onto the next task 279 EventBus.getDefault().post(new IterateRecentsEvent()); 280 } else { 281 // If the user has toggled it too quickly, then just eat up the event here (it's 282 // better than showing a janky screenshot). 283 // NOTE: Ideally, the screenshot mechanism would take the window transform into 284 // account 285 if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) { 286 return; 287 } 288 289 EventBus.getDefault().post(new ToggleRecentsEvent()); 290 mLastToggleTime = SystemClock.elapsedRealtime(); 291 } 292 return; 293 } else { 294 // If the user has toggled it too quickly, then just eat up the event here (it's 295 // better than showing a janky screenshot). 296 // NOTE: Ideally, the screenshot mechanism would take the window transform into 297 // account 298 if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) { 299 return; 300 } 301 302 // Otherwise, start the recents activity 303 startRecentsActivity(topTask, isTopTaskHome.value); 304 mLastToggleTime = SystemClock.elapsedRealtime(); 305 } 306 } catch (ActivityNotFoundException e) { 307 Console.logRawError("Failed to launch RecentAppsIntent", e); 308 } 309 } 310 311 @Override 312 public void preloadRecents() { 313 // Preload only the raw task list into a new load plan (which will be consumed by the 314 // RecentsActivity) only if there is a task to animate to. 315 SystemServicesProxy ssp = Recents.getSystemServices(); 316 ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask(); 317 MutableBoolean topTaskHome = new MutableBoolean(true); 318 RecentsTaskLoader loader = Recents.getTaskLoader(); 319 sInstanceLoadPlan = loader.createLoadPlan(mContext); 320 if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) { 321 sInstanceLoadPlan.preloadRawTasks(topTaskHome.value); 322 loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value); 323 TaskStack stack = sInstanceLoadPlan.getTaskStack(); 324 if (stack.getTaskCount() > 0) { 325 // We try and draw the thumbnail transition bitmap in parallel before 326 // toggle/show recents is called 327 preCacheThumbnailTransitionBitmapAsync(topTask, stack, mDummyStackView); 328 } 329 } 330 } 331 332 @Override 333 public void cancelPreloadingRecents() { 334 // Do nothing 335 } 336 337 public void showRelativeAffiliatedTask(boolean showNextTask) { 338 // Return early if there is no focused stack 339 SystemServicesProxy ssp = Recents.getSystemServices(); 340 int focusedStackId = ssp.getFocusedStack(); 341 RecentsTaskLoader loader = Recents.getTaskLoader(); 342 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); 343 loader.preloadTasks(plan, true /* isTopTaskHome */); 344 TaskStack focusedStack = plan.getTaskStack(); 345 346 // Return early if there are no tasks in the focused stack 347 if (focusedStack == null || focusedStack.getTaskCount() == 0) return; 348 349 ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask(); 350 // Return early if there is no running task (can't determine affiliated tasks in this case) 351 if (runningTask == null) return; 352 // Return early if the running task is in the home stack (optimization) 353 if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return; 354 355 // Find the task in the recents list 356 ArrayList<Task> tasks = focusedStack.getTasks(); 357 Task toTask = null; 358 ActivityOptions launchOpts = null; 359 int taskCount = tasks.size(); 360 int numAffiliatedTasks = 0; 361 for (int i = 0; i < taskCount; i++) { 362 Task task = tasks.get(i); 363 if (task.key.id == runningTask.id) { 364 TaskGrouping group = task.group; 365 Task.TaskKey toTaskKey; 366 if (showNextTask) { 367 toTaskKey = group.getNextTaskInGroup(task); 368 launchOpts = ActivityOptions.makeCustomAnimation(mContext, 369 R.anim.recents_launch_next_affiliated_task_target, 370 R.anim.recents_launch_next_affiliated_task_source); 371 } else { 372 toTaskKey = group.getPrevTaskInGroup(task); 373 launchOpts = ActivityOptions.makeCustomAnimation(mContext, 374 R.anim.recents_launch_prev_affiliated_task_target, 375 R.anim.recents_launch_prev_affiliated_task_source); 376 } 377 if (toTaskKey != null) { 378 toTask = focusedStack.findTaskWithId(toTaskKey.id); 379 } 380 numAffiliatedTasks = group.getTaskCount(); 381 break; 382 } 383 } 384 385 // Return early if there is no next task 386 if (toTask == null) { 387 if (numAffiliatedTasks > 1) { 388 if (showNextTask) { 389 ssp.startInPlaceAnimationOnFrontMostApplication( 390 ActivityOptions.makeCustomInPlaceAnimation(mContext, 391 R.anim.recents_launch_next_affiliated_task_bounce)); 392 } else { 393 ssp.startInPlaceAnimationOnFrontMostApplication( 394 ActivityOptions.makeCustomInPlaceAnimation(mContext, 395 R.anim.recents_launch_prev_affiliated_task_bounce)); 396 } 397 } 398 return; 399 } 400 401 // Keep track of actually launched affiliated tasks 402 MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1); 403 404 // Launch the task 405 if (toTask.isActive) { 406 // Bring an active task to the foreground 407 ssp.moveTaskToFront(toTask.key.id, launchOpts); 408 } else { 409 ssp.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts); 410 } 411 } 412 413 public void showNextAffiliatedTask() { 414 // Keep track of when the affiliated task is triggered 415 MetricsLogger.count(mContext, "overview_affiliated_task_next", 1); 416 showRelativeAffiliatedTask(true); 417 } 418 419 public void showPrevAffiliatedTask() { 420 // Keep track of when the affiliated task is triggered 421 MetricsLogger.count(mContext, "overview_affiliated_task_prev", 1); 422 showRelativeAffiliatedTask(false); 423 } 424 425 public void dockTopTask() { 426 SystemServicesProxy ssp = Recents.getSystemServices(); 427 ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask(); 428 if (topTask != null && !SystemServicesProxy.isHomeStack(topTask.stackId)) { 429 ssp.startTaskInDockedMode(topTask.id, 430 ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT); 431 showRecents(false /* triggeredFromAltTab */); 432 } 433 } 434 435 /** 436 * Returns the preloaded load plan and invalidates it. 437 */ 438 public static RecentsTaskLoadPlan consumeInstanceLoadPlan() { 439 RecentsTaskLoadPlan plan = sInstanceLoadPlan; 440 sInstanceLoadPlan = null; 441 return plan; 442 } 443 444 /** 445 * Prepares the header bar layout for the next transition, if the task view bounds has changed 446 * since the last call, it will attempt to re-measure and layout the header bar to the new size. 447 * 448 * @param tryAndBindSearchWidget if set, will attempt to fetch and bind the search widget if one 449 * is not already bound (can be expensive) 450 */ 451 private void reloadHeaderBarLayout(boolean tryAndBindSearchWidget) { 452 SystemServicesProxy ssp = Recents.getSystemServices(); 453 Rect windowRect = ssp.getWindowRect(); 454 455 // Update the configuration for the current state 456 mConfig.update(mContext, ssp, ssp.getWindowRect()); 457 458 if (!Constants.DebugFlags.App.DisableSearchBar && tryAndBindSearchWidget) { 459 // Try and pre-emptively bind the search widget on startup to ensure that we 460 // have the right thumbnail bounds to animate to. 461 // Note: We have to reload the widget id before we get the task stack bounds below 462 if (ssp.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) { 463 mConfig.getSearchBarBounds(windowRect, mStatusBarHeight, mSearchBarBounds); 464 } 465 } 466 Rect systemInsets = new Rect(0, mStatusBarHeight, 467 (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), 468 (mConfig.hasTransposedNavBar ? 0 : mNavBarHeight)); 469 mConfig.getTaskStackBounds(windowRect, systemInsets.top, systemInsets.right, 470 mSearchBarBounds, mTaskStackBounds); 471 472 // Rebind the header bar and draw it for the transition 473 TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm(); 474 Rect taskStackBounds = new Rect(mTaskStackBounds); 475 algo.setSystemInsets(systemInsets); 476 algo.computeRects(windowRect.width(), windowRect.height(), taskStackBounds); 477 Rect taskViewBounds = algo.getUntransformedTaskViewBounds(); 478 if (!taskViewBounds.equals(mLastTaskViewBounds)) { 479 mLastTaskViewBounds.set(taskViewBounds); 480 481 int taskViewWidth = taskViewBounds.width(); 482 synchronized (mHeaderBarLock) { 483 mHeaderBar.measure( 484 View.MeasureSpec.makeMeasureSpec(taskViewWidth, View.MeasureSpec.EXACTLY), 485 View.MeasureSpec.makeMeasureSpec(mTaskBarHeight, View.MeasureSpec.EXACTLY)); 486 mHeaderBar.layout(0, 0, taskViewWidth, mTaskBarHeight); 487 } 488 } 489 } 490 491 /** 492 * Preloads the icon of a task. 493 */ 494 private void preloadIcon(ActivityManager.RunningTaskInfo task) { 495 // Ensure that we load the running task's icon 496 RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); 497 launchOpts.runningTaskId = task.id; 498 launchOpts.loadThumbnails = false; 499 launchOpts.onlyLoadForCache = true; 500 Recents.getTaskLoader().loadTasks(mContext, sInstanceLoadPlan, launchOpts); 501 } 502 503 /** 504 * Caches the header thumbnail used for a window animation asynchronously into 505 * {@link #mThumbnailTransitionBitmapCache}. 506 */ 507 private void preCacheThumbnailTransitionBitmapAsync(ActivityManager.RunningTaskInfo topTask, 508 TaskStack stack, TaskStackView stackView) { 509 preloadIcon(topTask); 510 511 // Update the header bar if necessary 512 reloadHeaderBarLayout(false /* tryAndBindSearchWidget */); 513 514 // Update the destination rect 515 mDummyStackView.updateMinMaxScrollForStack(stack); 516 final Task toTask = new Task(); 517 final TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, 518 topTask.id, toTask); 519 ForegroundThread.getHandler().post(new Runnable() { 520 @Override 521 public void run() { 522 final Bitmap transitionBitmap = drawThumbnailTransitionBitmap(toTask, toTransform); 523 mHandler.post(new Runnable() { 524 @Override 525 public void run() { 526 mThumbnailTransitionBitmapCache = transitionBitmap; 527 mThumbnailTransitionBitmapCacheKey = toTask; 528 } 529 }); 530 } 531 }); 532 } 533 534 /** 535 * Creates the activity options for a unknown state->recents transition. 536 */ 537 private ActivityOptions getUnknownTransitionActivityOptions() { 538 return ActivityOptions.makeCustomAnimation(mContext, 539 R.anim.recents_from_unknown_enter, 540 R.anim.recents_from_unknown_exit, 541 mHandler, this); 542 } 543 544 /** 545 * Creates the activity options for a home->recents transition. 546 */ 547 private ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) { 548 if (fromSearchHome) { 549 return ActivityOptions.makeCustomAnimation(mContext, 550 R.anim.recents_from_search_launcher_enter, 551 R.anim.recents_from_search_launcher_exit, 552 mHandler, this); 553 } 554 return ActivityOptions.makeCustomAnimation(mContext, 555 R.anim.recents_from_launcher_enter, 556 R.anim.recents_from_launcher_exit, 557 mHandler, this); 558 } 559 560 /** 561 * Creates the activity options for an app->recents transition. 562 */ 563 private ActivityOptions getThumbnailTransitionActivityOptions( 564 ActivityManager.RunningTaskInfo topTask, TaskStack stack, TaskStackView stackView) { 565 if (topTask.stackId == FREEFORM_WORKSPACE_STACK_ID) { 566 ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>(); 567 stackView.getScroller().setStackScrollToInitialState(); 568 ArrayList<Task> tasks = stack.getTasks(); 569 for (int i = tasks.size() - 1; i >= 0; i--) { 570 Task task = tasks.get(i); 571 if (SystemServicesProxy.isFreeformStack(task.key.stackId)) { 572 mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task, 573 stackView.getScroller().getStackScroll(), mTmpTransform, null); 574 Rect toTaskRect = new Rect(); 575 mTmpTransform.rect.round(toTaskRect); 576 Bitmap thumbnail = getThumbnailBitmap(topTask, task, mTmpTransform); 577 specs.add(new AppTransitionAnimationSpec(task.key.id, thumbnail, toTaskRect)); 578 } 579 } 580 AppTransitionAnimationSpec[] specsArray = new AppTransitionAnimationSpec[specs.size()]; 581 specs.toArray(specsArray); 582 return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView, 583 specsArray, mHandler, this); 584 } else { 585 // Update the destination rect 586 Task toTask = new Task(); 587 TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, 588 topTask.id, toTask); 589 RectF toTaskRect = toTransform.rect; 590 Bitmap thumbnail = getThumbnailBitmap(topTask, toTask, toTransform); 591 if (thumbnail != null) { 592 return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView, 593 thumbnail, (int) toTaskRect.left, (int) toTaskRect.top, 594 (int) toTaskRect.width(), (int) toTaskRect.height(), mHandler, this); 595 } 596 // If both the screenshot and thumbnail fails, then just fall back to the default transition 597 return getUnknownTransitionActivityOptions(); 598 } 599 } 600 601 private Bitmap getThumbnailBitmap(ActivityManager.RunningTaskInfo topTask, Task toTask, 602 TaskViewTransform toTransform) { 603 Bitmap thumbnail; 604 if (mThumbnailTransitionBitmapCacheKey != null 605 && mThumbnailTransitionBitmapCacheKey.key != null 606 && mThumbnailTransitionBitmapCacheKey.key.equals(toTask.key)) { 607 thumbnail = mThumbnailTransitionBitmapCache; 608 mThumbnailTransitionBitmapCacheKey = null; 609 mThumbnailTransitionBitmapCache = null; 610 } else { 611 preloadIcon(topTask); 612 thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform); 613 } 614 return thumbnail; 615 } 616 617 /** 618 * Returns the transition rect for the given task id. 619 */ 620 private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, 621 TaskStackView stackView, int runningTaskId, Task runningTaskOut) { 622 // Find the running task in the TaskStack 623 Task task = null; 624 ArrayList<Task> tasks = stack.getTasks(); 625 if (runningTaskId != -1) { 626 // Otherwise, try and find the task with the 627 int taskCount = tasks.size(); 628 for (int i = taskCount - 1; i >= 0; i--) { 629 Task t = tasks.get(i); 630 if (t.key.id == runningTaskId) { 631 task = t; 632 runningTaskOut.copyFrom(t); 633 break; 634 } 635 } 636 } 637 if (task == null) { 638 // If no task is specified or we can not find the task just use the front most one 639 task = tasks.get(tasks.size() - 1); 640 runningTaskOut.copyFrom(task); 641 } 642 643 // Get the transform for the running task 644 stackView.getScroller().setStackScrollToInitialState(); 645 mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task, 646 stackView.getScroller().getStackScroll(), mTmpTransform, null); 647 return mTmpTransform; 648 } 649 650 /** 651 * Draws the header of a task used for the window animation into a bitmap. 652 */ 653 private Bitmap drawThumbnailTransitionBitmap(Task toTask, TaskViewTransform toTransform) { 654 if (toTransform != null && toTask.key != null) { 655 Bitmap thumbnail; 656 synchronized (mHeaderBarLock) { 657 int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale); 658 int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale); 659 thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight, 660 Bitmap.Config.ARGB_8888); 661 if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { 662 thumbnail.eraseColor(0xFFff0000); 663 } else { 664 Canvas c = new Canvas(thumbnail); 665 c.scale(toTransform.scale, toTransform.scale); 666 mHeaderBar.rebindToTask(toTask); 667 mHeaderBar.draw(c); 668 c.setBitmap(null); 669 } 670 } 671 return thumbnail.createAshmemBitmap(); 672 } 673 return null; 674 } 675 676 /** 677 * Shows the recents activity 678 */ 679 private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, 680 boolean isTopTaskHome) { 681 SystemServicesProxy ssp = Recents.getSystemServices(); 682 RecentsTaskLoader loader = Recents.getTaskLoader(); 683 684 // Update the header bar if necessary 685 reloadHeaderBarLayout(false /* tryAndBindSearchWidget */); 686 687 if (sInstanceLoadPlan == null) { 688 // Create a new load plan if onPreloadRecents() was never triggered 689 sInstanceLoadPlan = loader.createLoadPlan(mContext); 690 } 691 692 if (!sInstanceLoadPlan.hasTasks()) { 693 loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); 694 } 695 TaskStack stack = sInstanceLoadPlan.getTaskStack(); 696 697 // Prepare the dummy stack for the transition 698 mDummyStackView.updateMinMaxScrollForStack(stack); 699 TaskStackViewLayoutAlgorithm.VisibilityReport stackVr = 700 mDummyStackView.computeStackVisibilityReport(); 701 boolean hasRecentTasks = stack.getTaskCount() > 0; 702 boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks; 703 704 if (useThumbnailTransition) { 705 // Try starting with a thumbnail transition 706 ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack, 707 mDummyStackView); 708 if (opts != null) { 709 startRecentsActivity(topTask, opts, false /* fromHome */, 710 false /* fromSearchHome */, true /* fromThumbnail */, stackVr); 711 } else { 712 // Fall through below to the non-thumbnail transition 713 useThumbnailTransition = false; 714 } 715 } 716 717 if (!useThumbnailTransition) { 718 // If there is no thumbnail transition, but is launching from home into recents, then 719 // use a quick home transition and do the animation from home 720 if (!Constants.DebugFlags.App.DisableSearchBar && hasRecentTasks) { 721 String homeActivityPackage = ssp.getHomeActivityPackageName(); 722 String searchWidgetPackage = Prefs.getString(mContext, 723 Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null); 724 725 // Determine whether we are coming from a search owned home activity 726 boolean fromSearchHome = (homeActivityPackage != null) && 727 homeActivityPackage.equals(searchWidgetPackage); 728 ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome); 729 startRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome, 730 false /* fromThumbnail */, stackVr); 731 } else { 732 // Otherwise we do the normal fade from an unknown source 733 ActivityOptions opts = getUnknownTransitionActivityOptions(); 734 startRecentsActivity(topTask, opts, true /* fromHome */, 735 false /* fromSearchHome */, false /* fromThumbnail */, stackVr); 736 } 737 } 738 mLastToggleTime = SystemClock.elapsedRealtime(); 739 } 740 741 /** 742 * Starts the recents activity. 743 */ 744 private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, 745 ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail, 746 TaskStackViewLayoutAlgorithm.VisibilityReport vr) { 747 mStartAnimationTriggered = false; 748 749 // Update the configuration based on the launch options 750 RecentsActivityLaunchState launchState = mConfig.getLaunchState(); 751 launchState.launchedFromHome = fromSearchHome || fromHome; 752 launchState.launchedFromSearchHome = fromSearchHome; 753 launchState.launchedFromAppWithThumbnail = fromThumbnail; 754 launchState.launchedToTaskId = (topTask != null) ? topTask.id : -1; 755 launchState.launchedWithAltTab = mTriggeredFromAltTab; 756 launchState.launchedReuseTaskStackViews = mCanReuseTaskStackViews; 757 launchState.launchedNumVisibleTasks = vr.numVisibleTasks; 758 launchState.launchedNumVisibleThumbnails = vr.numVisibleThumbnails; 759 launchState.launchedHasConfigurationChanged = false; 760 761 Intent intent = new Intent(); 762 intent.setClassName(RECENTS_PACKAGE, RECENTS_ACTIVITY); 763 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 764 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 765 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 766 if (opts != null) { 767 mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT); 768 } else { 769 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 770 } 771 mCanReuseTaskStackViews = true; 772 } 773 774 /**** OnAnimationStartedListener Implementation ****/ 775 776 @Override 777 public void onAnimationStarted() { 778 // Notify recents to start the enter animation 779 if (!mStartAnimationTriggered) { 780 mStartAnimationTriggered = true; 781 EventBus.getDefault().post(new EnterRecentsWindowAnimationStartedEvent()); 782 } 783 } 784} 785