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.views; 18 19 import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; 20 21 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS; 22 23 import android.animation.ValueAnimator; 24 import android.animation.ValueAnimator.AnimatorUpdateListener; 25 import android.annotation.Nullable; 26 import android.app.ActivityOptions; 27 import android.content.Context; 28 import android.content.res.ColorStateList; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Point; 32 import android.graphics.PointF; 33 import android.graphics.Rect; 34 import android.graphics.drawable.ColorDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.os.Handler; 37 import android.util.ArraySet; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.MathUtils; 41 import android.view.LayoutInflater; 42 import android.view.MotionEvent; 43 import android.view.View; 44 import android.view.ViewDebug; 45 import android.view.ViewPropertyAnimator; 46 import android.view.Window; 47 import android.view.WindowInsets; 48 import android.widget.FrameLayout; 49 import android.widget.TextView; 50 51 import com.android.internal.colorextraction.ColorExtractor; 52 import com.android.internal.colorextraction.drawable.ScrimDrawable; 53 import com.android.internal.logging.MetricsLogger; 54 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 55 import com.android.settingslib.Utils; 56 import com.android.systemui.Interpolators; 57 import com.android.systemui.R; 58 import com.android.systemui.recents.LegacyRecentsImpl; 59 import com.android.systemui.recents.RecentsActivity; 60 import com.android.systemui.recents.RecentsActivityLaunchState; 61 import com.android.systemui.recents.RecentsConfiguration; 62 import com.android.systemui.recents.events.EventBus; 63 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent; 64 import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted; 65 import com.android.systemui.recents.events.activity.DockedFirstAnimationFrameEvent; 66 import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationCompletedEvent; 67 import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent; 68 import com.android.systemui.recents.events.activity.HideStackActionButtonEvent; 69 import com.android.systemui.recents.events.activity.LaunchTaskEvent; 70 import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent; 71 import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent; 72 import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent; 73 import com.android.systemui.recents.events.activity.MultiWindowStateChangedEvent; 74 import com.android.systemui.recents.events.activity.ShowEmptyViewEvent; 75 import com.android.systemui.recents.events.activity.ShowStackActionButtonEvent; 76 import com.android.systemui.recents.events.component.ExpandPipEvent; 77 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent; 78 import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent; 79 import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent; 80 import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent; 81 import com.android.systemui.recents.events.ui.DraggingInRecentsEndedEvent; 82 import com.android.systemui.recents.events.ui.DraggingInRecentsEvent; 83 import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent; 84 import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent; 85 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; 86 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; 87 import com.android.systemui.recents.misc.ReferenceCountedTrigger; 88 import com.android.systemui.recents.misc.SystemServicesProxy; 89 import com.android.systemui.recents.model.TaskStack; 90 import com.android.systemui.recents.utilities.Utilities; 91 import com.android.systemui.shared.recents.model.Task; 92 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; 93 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; 94 import com.android.systemui.shared.recents.view.RecentsTransition; 95 import com.android.systemui.shared.system.ActivityManagerWrapper; 96 import com.android.systemui.shared.system.ActivityOptionsCompat; 97 import com.android.systemui.shared.system.WindowManagerWrapper; 98 import com.android.systemui.stackdivider.WindowManagerProxy; 99 import com.android.systemui.statusbar.FlingAnimationUtils; 100 import com.android.systemui.statusbar.phone.ScrimController; 101 102 import java.io.PrintWriter; 103 import java.util.ArrayList; 104 import java.util.List; 105 106 /** 107 * This view is the the top level layout that contains TaskStacks (which are laid out according 108 * to their SpaceNode bounds. 109 */ 110 public class RecentsView extends FrameLayout { 111 112 private static final String TAG = "RecentsView"; 113 114 private static final int DEFAULT_UPDATE_SCRIM_DURATION = 200; 115 116 private static final int SHOW_STACK_ACTION_BUTTON_DURATION = 134; 117 private static final int HIDE_STACK_ACTION_BUTTON_DURATION = 100; 118 119 private static final int BUSY_RECENTS_TASK_COUNT = 3; 120 121 private Handler mHandler; 122 private TaskStackView mTaskStackView; 123 private TextView mStackActionButton; 124 private TextView mEmptyView; 125 private final float mStackButtonShadowRadius; 126 private final PointF mStackButtonShadowDistance; 127 private final int mStackButtonShadowColor; 128 129 private boolean mAwaitingFirstLayout = true; 130 131 @ViewDebug.ExportedProperty(category="recents") 132 Rect mSystemInsets = new Rect(); 133 private int mDividerSize; 134 135 private float mBusynessFactor; 136 private ScrimDrawable mBackgroundScrim; 137 private ColorDrawable mMultiWindowBackgroundScrim; 138 private ValueAnimator mBackgroundScrimAnimator; 139 private Point mTmpDisplaySize = new Point(); 140 141 private final AnimatorUpdateListener mUpdateBackgroundScrimAlpha = (animation) -> { 142 int alpha = (Integer) animation.getAnimatedValue(); 143 mBackgroundScrim.setAlpha(alpha); 144 mMultiWindowBackgroundScrim.setAlpha(alpha); 145 }; 146 147 private RecentsTransitionComposer mTransitionHelper; 148 @ViewDebug.ExportedProperty(deepExport=true, prefix="touch_") 149 private RecentsViewTouchHandler mTouchHandler; 150 private final FlingAnimationUtils mFlingAnimationUtils; 151 RecentsView(Context context)152 public RecentsView(Context context) { 153 this(context, null); 154 } 155 RecentsView(Context context, AttributeSet attrs)156 public RecentsView(Context context, AttributeSet attrs) { 157 this(context, attrs, 0); 158 } 159 RecentsView(Context context, AttributeSet attrs, int defStyleAttr)160 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { 161 this(context, attrs, defStyleAttr, 0); 162 } 163 RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)164 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 165 super(context, attrs, defStyleAttr, defStyleRes); 166 setWillNotDraw(false); 167 168 SystemServicesProxy ssp = LegacyRecentsImpl.getSystemServices(); 169 mHandler = new Handler(); 170 mTransitionHelper = new RecentsTransitionComposer(getContext()); 171 mDividerSize = ssp.getDockedDividerSize(context); 172 mTouchHandler = new RecentsViewTouchHandler(this); 173 mFlingAnimationUtils = new FlingAnimationUtils(context, 0.3f); 174 mBackgroundScrim = new ScrimDrawable(); 175 mMultiWindowBackgroundScrim = new ColorDrawable(); 176 177 LayoutInflater inflater = LayoutInflater.from(context); 178 mEmptyView = (TextView) inflater.inflate(R.layout.recents_empty, this, false); 179 addView(mEmptyView); 180 181 if (mStackActionButton != null) { 182 removeView(mStackActionButton); 183 } 184 mStackActionButton = (TextView) inflater.inflate(LegacyRecentsImpl.getConfiguration() 185 .isLowRamDevice 186 ? R.layout.recents_low_ram_stack_action_button 187 : R.layout.recents_stack_action_button, 188 this, false); 189 190 mStackButtonShadowRadius = mStackActionButton.getShadowRadius(); 191 mStackButtonShadowDistance = new PointF(mStackActionButton.getShadowDx(), 192 mStackActionButton.getShadowDy()); 193 mStackButtonShadowColor = mStackActionButton.getShadowColor(); 194 addView(mStackActionButton); 195 196 reevaluateStyles(); 197 } 198 reevaluateStyles()199 public void reevaluateStyles() { 200 int textColor = Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor); 201 boolean usingDarkText = Color.luminance(textColor) < 0.5f; 202 203 mEmptyView.setTextColor(textColor); 204 mEmptyView.setCompoundDrawableTintList(new ColorStateList(new int[][]{ 205 {android.R.attr.state_enabled}}, new int[]{textColor})); 206 207 if (mStackActionButton != null) { 208 mStackActionButton.setTextColor(textColor); 209 // Enable/disable shadow if text color is already dark. 210 if (usingDarkText) { 211 mStackActionButton.setShadowLayer(0, 0, 0, 0); 212 } else { 213 mStackActionButton.setShadowLayer(mStackButtonShadowRadius, 214 mStackButtonShadowDistance.x, mStackButtonShadowDistance.y, 215 mStackButtonShadowColor); 216 } 217 } 218 219 // Let's also require dark status and nav bars if the text is dark 220 int systemBarsStyle = usingDarkText ? View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | 221 View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR : 0; 222 223 setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | 224 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 225 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | 226 systemBarsStyle); 227 } 228 229 /** 230 * Called from RecentsActivity when it is relaunched. 231 */ 232 public void onReload(TaskStack stack, boolean isResumingFromVisible) { 233 final RecentsConfiguration config = LegacyRecentsImpl.getConfiguration(); 234 final RecentsActivityLaunchState launchState = config.getLaunchState(); 235 final boolean isTaskStackEmpty = stack.getTaskCount() == 0; 236 237 if (mTaskStackView == null) { 238 isResumingFromVisible = false; 239 mTaskStackView = new TaskStackView(getContext()); 240 mTaskStackView.setSystemInsets(mSystemInsets); 241 addView(mTaskStackView); 242 } 243 244 // Reset the state 245 mAwaitingFirstLayout = !isResumingFromVisible; 246 247 // Update the stack 248 mTaskStackView.onReload(isResumingFromVisible); 249 updateStack(stack, true /* setStackViewTasks */); 250 updateBusyness(); 251 252 if (isResumingFromVisible) { 253 // If we are already visible, then restore the background scrim 254 animateBackgroundScrim(getOpaqueScrimAlpha(), DEFAULT_UPDATE_SCRIM_DURATION); 255 } else { 256 // If we are already occluded by the app, then set the final background scrim alpha now. 257 // Otherwise, defer until the enter animation completes to animate the scrim alpha with 258 // the tasks for the home animation. 259 if (launchState.launchedViaDockGesture || launchState.launchedFromApp 260 || isTaskStackEmpty) { 261 mBackgroundScrim.setAlpha((int) (getOpaqueScrimAlpha() * 255)); 262 } else { 263 mBackgroundScrim.setAlpha(0); 264 } 265 mMultiWindowBackgroundScrim.setAlpha(mBackgroundScrim.getAlpha()); 266 } 267 } 268 269 /** 270 * Called from RecentsActivity when the task stack is updated. 271 */ 272 public void updateStack(TaskStack stack, boolean setStackViewTasks) { 273 if (setStackViewTasks) { 274 mTaskStackView.setTasks(stack, true /* allowNotifyStackChanges */); 275 } 276 277 // Update the top level view's visibilities 278 if (stack.getTaskCount() > 0) { 279 hideEmptyView(); 280 } else { 281 showEmptyView(R.string.recents_empty_message); 282 } 283 } 284 285 /** 286 * Animates the scrim opacity based on how many tasks are visible. 287 * Called from {@link RecentsActivity} when tasks are dismissed. 288 */ 289 public void updateScrimOpacity() { 290 if (updateBusyness()) { 291 animateBackgroundScrim(getOpaqueScrimAlpha(), DEFAULT_UPDATE_SCRIM_DURATION); 292 } 293 } 294 295 /** 296 * Updates the busyness factor. 297 * 298 * @return True if it changed. 299 */ 300 private boolean updateBusyness() { 301 final int taskCount = mTaskStackView.getStack().getTaskCount(); 302 final float busyness = Math.min(taskCount, BUSY_RECENTS_TASK_COUNT) 303 / (float) BUSY_RECENTS_TASK_COUNT; 304 if (mBusynessFactor == busyness) { 305 return false; 306 } else { 307 mBusynessFactor = busyness; 308 return true; 309 } 310 } 311 312 /** 313 * Returns the current TaskStack. 314 */ 315 public TaskStack getStack() { 316 return mTaskStackView.getStack(); 317 } 318 319 /** 320 * Returns the window background scrim. 321 */ 322 public void updateBackgroundScrim(Window window, boolean isInMultiWindow) { 323 if (isInMultiWindow) { 324 mBackgroundScrim.setCallback(null); 325 window.setBackgroundDrawable(mMultiWindowBackgroundScrim); 326 } else { 327 mMultiWindowBackgroundScrim.setCallback(null); 328 window.setBackgroundDrawable(mBackgroundScrim); 329 } 330 } 331 332 /** Launches the focused task from the first stack if possible */ 333 public boolean launchFocusedTask(int logEvent) { 334 if (mTaskStackView != null) { 335 Task task = mTaskStackView.getFocusedTask(); 336 if (task != null) { 337 TaskView taskView = mTaskStackView.getChildViewForTask(task); 338 EventBus.getDefault().send(new LaunchTaskEvent(taskView, task, null, false)); 339 340 if (logEvent != 0) { 341 MetricsLogger.action(getContext(), logEvent, 342 task.key.getComponent().toString()); 343 } 344 return true; 345 } 346 } 347 return false; 348 } 349 350 /** Launches the task that recents was launched from if possible */ 351 public boolean launchPreviousTask() { 352 if (LegacyRecentsImpl.getConfiguration().getLaunchState().launchedFromPipApp) { 353 // If the app auto-entered PiP on the way to Recents, then just re-expand it 354 EventBus.getDefault().send(new ExpandPipEvent()); 355 return true; 356 } 357 358 if (mTaskStackView != null) { 359 Task task = getStack().getLaunchTarget(); 360 if (task != null) { 361 TaskView taskView = mTaskStackView.getChildViewForTask(task); 362 EventBus.getDefault().send(new LaunchTaskEvent(taskView, task, null, false)); 363 return true; 364 } 365 } 366 return false; 367 } 368 369 /** 370 * Hides the task stack and shows the empty view. 371 */ 372 public void showEmptyView(int msgResId) { 373 mTaskStackView.setVisibility(View.INVISIBLE); 374 mEmptyView.setText(msgResId); 375 mEmptyView.setVisibility(View.VISIBLE); 376 mEmptyView.bringToFront(); 377 mStackActionButton.bringToFront(); 378 } 379 380 /** 381 * Shows the task stack and hides the empty view. 382 */ 383 public void hideEmptyView() { 384 mEmptyView.setVisibility(View.INVISIBLE); 385 mTaskStackView.setVisibility(View.VISIBLE); 386 mTaskStackView.bringToFront(); 387 mStackActionButton.bringToFront(); 388 } 389 390 /** 391 * Set the color of the scrim. 392 * 393 * @param scrimColors Colors to use. 394 * @param animated Interpolate colors if true. 395 */ 396 public void setScrimColors(ColorExtractor.GradientColors scrimColors, boolean animated) { 397 mBackgroundScrim.setColor(scrimColors.getMainColor(), animated); 398 int alpha = mMultiWindowBackgroundScrim.getAlpha(); 399 mMultiWindowBackgroundScrim.setColor(scrimColors.getMainColor()); 400 mMultiWindowBackgroundScrim.setAlpha(alpha); 401 } 402 403 @Override 404 protected void onAttachedToWindow() { 405 EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1); 406 EventBus.getDefault().register(mTouchHandler, RecentsActivity.EVENT_BUS_PRIORITY + 2); 407 super.onAttachedToWindow(); 408 } 409 410 @Override 411 protected void onDetachedFromWindow() { 412 super.onDetachedFromWindow(); 413 EventBus.getDefault().unregister(this); 414 EventBus.getDefault().unregister(mTouchHandler); 415 } 416 417 /** 418 * This is called with the full size of the window since we are handling our own insets. 419 */ 420 @Override 421 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 422 int width = MeasureSpec.getSize(widthMeasureSpec); 423 int height = MeasureSpec.getSize(heightMeasureSpec); 424 425 if (mTaskStackView.getVisibility() != GONE) { 426 mTaskStackView.measure(widthMeasureSpec, heightMeasureSpec); 427 } 428 429 // Measure the empty view to the full size of the screen 430 if (mEmptyView.getVisibility() != GONE) { 431 measureChild(mEmptyView, MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 432 MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); 433 } 434 435 // Measure the stack action button within the constraints of the space above the stack 436 Rect buttonBounds = mTaskStackView.mLayoutAlgorithm.getStackActionButtonRect(); 437 measureChild(mStackActionButton, 438 MeasureSpec.makeMeasureSpec(buttonBounds.width(), MeasureSpec.AT_MOST), 439 MeasureSpec.makeMeasureSpec(buttonBounds.height(), MeasureSpec.AT_MOST)); 440 441 setMeasuredDimension(width, height); 442 } 443 444 /** 445 * This is called with the full size of the window since we are handling our own insets. 446 */ 447 @Override 448 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 449 if (mTaskStackView.getVisibility() != GONE) { 450 mTaskStackView.layout(left, top, left + getMeasuredWidth(), top + getMeasuredHeight()); 451 } 452 453 // Layout the empty view 454 if (mEmptyView.getVisibility() != GONE) { 455 int leftRightInsets = mSystemInsets.left + mSystemInsets.right; 456 int topBottomInsets = mSystemInsets.top + mSystemInsets.bottom; 457 int childWidth = mEmptyView.getMeasuredWidth(); 458 int childHeight = mEmptyView.getMeasuredHeight(); 459 int childLeft = left + mSystemInsets.left + 460 Math.max(0, (right - left - leftRightInsets - childWidth)) / 2; 461 int childTop = top + mSystemInsets.top + 462 Math.max(0, (bottom - top - topBottomInsets - childHeight)) / 2; 463 mEmptyView.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 464 } 465 466 // Needs to know the screen size since the gradient never scales up or down 467 // even when bounds change. 468 mContext.getDisplay().getRealSize(mTmpDisplaySize); 469 mBackgroundScrim.setBounds(left, top, right, bottom); 470 mMultiWindowBackgroundScrim.setBounds(0, 0, mTmpDisplaySize.x, mTmpDisplaySize.y); 471 472 // Layout the stack action button such that its drawable is start-aligned with the 473 // stack, vertically centered in the available space above the stack 474 Rect buttonBounds = getStackActionButtonBoundsFromStackLayout(); 475 mStackActionButton.layout(buttonBounds.left, buttonBounds.top, buttonBounds.right, 476 buttonBounds.bottom); 477 478 if (mAwaitingFirstLayout) { 479 mAwaitingFirstLayout = false; 480 // If launched via dragging from the nav bar, then we should translate the whole view 481 // down offscreen 482 RecentsActivityLaunchState launchState = LegacyRecentsImpl.getConfiguration().getLaunchState(); 483 if (launchState.launchedViaDragGesture) { 484 setTranslationY(getMeasuredHeight()); 485 } else { 486 setTranslationY(0f); 487 } 488 489 if (LegacyRecentsImpl.getConfiguration().isLowRamDevice 490 && mEmptyView.getVisibility() == View.VISIBLE) { 491 animateEmptyView(true /* show */, null /* postAnimationTrigger */); 492 } 493 } 494 } 495 496 @Override 497 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 498 mSystemInsets.set(insets.getSystemWindowInsetsAsRect()); 499 mTaskStackView.setSystemInsets(mSystemInsets); 500 requestLayout(); 501 return insets; 502 } 503 504 @Override 505 public boolean onInterceptTouchEvent(MotionEvent ev) { 506 return mTouchHandler.onInterceptTouchEvent(ev); 507 } 508 509 @Override 510 public boolean onTouchEvent(MotionEvent ev) { 511 return mTouchHandler.onTouchEvent(ev); 512 } 513 514 @Override 515 public void onDrawForeground(Canvas canvas) { 516 super.onDrawForeground(canvas); 517 518 ArrayList<DockState> visDockStates = mTouchHandler.getVisibleDockStates(); 519 for (int i = visDockStates.size() - 1; i >= 0; i--) { 520 visDockStates.get(i).viewState.draw(canvas); 521 } 522 } 523 524 @Override 525 protected boolean verifyDrawable(Drawable who) { 526 ArrayList<DockState> visDockStates = mTouchHandler.getVisibleDockStates(); 527 for (int i = visDockStates.size() - 1; i >= 0; i--) { 528 Drawable d = visDockStates.get(i).viewState.dockAreaOverlay; 529 if (d == who) { 530 return true; 531 } 532 } 533 return super.verifyDrawable(who); 534 } 535 536 /**** EventBus Events ****/ 537 538 public final void onBusEvent(LaunchTaskEvent event) { 539 launchTaskFromRecents(getStack(), event.task, mTaskStackView, event.taskView, 540 event.screenPinningRequested, event.targetWindowingMode, event.targetActivityType); 541 if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { 542 EventBus.getDefault().send(new HideStackActionButtonEvent(false /* translate */)); 543 } 544 } 545 546 public final void onBusEvent(DismissRecentsToHomeAnimationStarted event) { 547 int taskViewExitToHomeDuration = TaskStackAnimationHelper.EXIT_TO_HOME_TRANSLATION_DURATION; 548 // Hide the stack action button 549 EventBus.getDefault().send(new HideStackActionButtonEvent()); 550 animateBackgroundScrim(0f, taskViewExitToHomeDuration); 551 552 if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { 553 animateEmptyView(false /* show */, event.getAnimationTrigger()); 554 } 555 } 556 557 public final void onBusEvent(DragStartEvent event) { 558 updateVisibleDockRegions(LegacyRecentsImpl.getConfiguration().getDockStatesForCurrentOrientation(), 559 true /* isDefaultDockState */, DockState.NONE.viewState.dockAreaAlpha, 560 DockState.NONE.viewState.hintTextAlpha, 561 true /* animateAlpha */, false /* animateBounds */); 562 563 // Temporarily hide the stack action button without changing visibility 564 if (mStackActionButton != null) { 565 mStackActionButton.animate() 566 .alpha(0f) 567 .setDuration(HIDE_STACK_ACTION_BUTTON_DURATION) 568 .setInterpolator(Interpolators.ALPHA_OUT) 569 .start(); 570 } 571 } 572 573 public final void onBusEvent(DragDropTargetChangedEvent event) { 574 if (event.dropTarget == null || !(event.dropTarget instanceof DockState)) { 575 updateVisibleDockRegions( 576 LegacyRecentsImpl.getConfiguration().getDockStatesForCurrentOrientation(), 577 true /* isDefaultDockState */, DockState.NONE.viewState.dockAreaAlpha, 578 DockState.NONE.viewState.hintTextAlpha, 579 true /* animateAlpha */, true /* animateBounds */); 580 } else { 581 final DockState dockState = (DockState) event.dropTarget; 582 updateVisibleDockRegions(new DockState[] {dockState}, 583 false /* isDefaultDockState */, -1, -1, true /* animateAlpha */, 584 true /* animateBounds */); 585 } 586 if (mStackActionButton != null) { 587 event.addPostAnimationCallback(new Runnable() { 588 @Override 589 public void run() { 590 // Move the clear all button to its new position 591 Rect buttonBounds = getStackActionButtonBoundsFromStackLayout(); 592 mStackActionButton.setLeftTopRightBottom(buttonBounds.left, buttonBounds.top, 593 buttonBounds.right, buttonBounds.bottom); 594 } 595 }); 596 } 597 } 598 599 public final void onBusEvent(final DragEndEvent event) { 600 // Handle the case where we drop onto a dock region 601 if (event.dropTarget instanceof DockState) { 602 final DockState dockState = (DockState) event.dropTarget; 603 604 // Hide the dock region 605 updateVisibleDockRegions(null, false /* isDefaultDockState */, -1, -1, 606 false /* animateAlpha */, false /* animateBounds */); 607 608 // We translated the view but we need to animate it back from the current layout-space 609 // rect to its final layout-space rect 610 Utilities.setViewFrameFromTranslation(event.taskView); 611 612 final ActivityOptions options = ActivityOptionsCompat.makeSplitScreenOptions( 613 dockState.createMode == SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT); 614 if (ActivityManagerWrapper.getInstance().startActivityFromRecents(event.task.key.id, 615 options)) { 616 final Runnable animStartedListener = () -> { 617 EventBus.getDefault().send(new DockedFirstAnimationFrameEvent()); 618 // Remove the task and don't bother relaying out, as all the tasks 619 // will be relaid out when the stack changes on the multiwindow 620 // change event 621 getStack().removeTask(event.task, null, true /* fromDockGesture */); 622 }; 623 final Rect taskRect = getTaskRect(event.taskView); 624 AppTransitionAnimationSpecsFuture future = new AppTransitionAnimationSpecsFuture( 625 getHandler()) { 626 @Override 627 public List<AppTransitionAnimationSpecCompat> composeSpecs() { 628 return mTransitionHelper.composeDockAnimationSpec(event.taskView, taskRect); 629 } 630 }; 631 WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture( 632 future, animStartedListener, getHandler(), true /* scaleUp */, 633 getContext().getDisplayId()); 634 MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_DRAG_DROP, 635 event.task.getTopComponent().flattenToShortString()); 636 } else { 637 EventBus.getDefault().send(new DragEndCancelledEvent(getStack(), event.task, 638 event.taskView)); 639 } 640 } else { 641 // Animate the overlay alpha back to 0 642 updateVisibleDockRegions(null, true /* isDefaultDockState */, -1, -1, 643 true /* animateAlpha */, false /* animateBounds */); 644 } 645 646 // Show the stack action button again without changing visibility 647 if (mStackActionButton != null) { 648 mStackActionButton.animate() 649 .alpha(1f) 650 .setDuration(SHOW_STACK_ACTION_BUTTON_DURATION) 651 .setInterpolator(Interpolators.ALPHA_IN) 652 .start(); 653 } 654 } 655 656 public final void onBusEvent(final DragEndCancelledEvent event) { 657 // Animate the overlay alpha back to 0 658 updateVisibleDockRegions(null, true /* isDefaultDockState */, -1, -1, 659 true /* animateAlpha */, false /* animateBounds */); 660 } 661 662 private Rect getTaskRect(TaskView taskView) { 663 int[] location = taskView.getLocationOnScreen(); 664 int viewX = location[0]; 665 int viewY = location[1]; 666 return new Rect(viewX, viewY, 667 (int) (viewX + taskView.getWidth() * taskView.getScaleX()), 668 (int) (viewY + taskView.getHeight() * taskView.getScaleY())); 669 } 670 671 public final void onBusEvent(DraggingInRecentsEvent event) { 672 if (mTaskStackView.getTaskViews().size() > 0) { 673 setTranslationY(event.distanceFromTop - mTaskStackView.getTaskViews().get(0).getY()); 674 } 675 } 676 677 public final void onBusEvent(DraggingInRecentsEndedEvent event) { 678 ViewPropertyAnimator animator = animate(); 679 if (event.velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 680 animator.translationY(getHeight()); 681 animator.withEndAction(new Runnable() { 682 @Override 683 public void run() { 684 WindowManagerProxy.getInstance().maximizeDockedStack(); 685 } 686 }); 687 mFlingAnimationUtils.apply(animator, getTranslationY(), getHeight(), event.velocity); 688 } else { 689 animator.translationY(0f); 690 animator.setListener(null); 691 mFlingAnimationUtils.apply(animator, getTranslationY(), 0, event.velocity); 692 } 693 animator.start(); 694 } 695 696 public final void onBusEvent(EnterRecentsWindowAnimationCompletedEvent event) { 697 RecentsActivityLaunchState launchState = LegacyRecentsImpl.getConfiguration().getLaunchState(); 698 if (!launchState.launchedViaDockGesture && !launchState.launchedFromApp 699 && getStack().getTaskCount() > 0) { 700 animateBackgroundScrim(getOpaqueScrimAlpha(), 701 TaskStackAnimationHelper.ENTER_FROM_HOME_TRANSLATION_DURATION); 702 } 703 } 704 705 public final void onBusEvent(AllTaskViewsDismissedEvent event) { 706 EventBus.getDefault().send(new HideStackActionButtonEvent()); 707 } 708 709 public final void onBusEvent(DismissAllTaskViewsEvent event) { 710 SystemServicesProxy ssp = LegacyRecentsImpl.getSystemServices(); 711 if (!ssp.hasDockedTask()) { 712 // Animate the background away only if we are dismissing Recents to home 713 animateBackgroundScrim(0f, DEFAULT_UPDATE_SCRIM_DURATION); 714 } 715 } 716 717 public final void onBusEvent(ShowStackActionButtonEvent event) { 718 showStackActionButton(SHOW_STACK_ACTION_BUTTON_DURATION, event.translate); 719 } 720 721 public final void onBusEvent(HideStackActionButtonEvent event) { 722 hideStackActionButton(HIDE_STACK_ACTION_BUTTON_DURATION, true /* translate */); 723 } 724 725 public final void onBusEvent(MultiWindowStateChangedEvent event) { 726 updateStack(event.stack, false /* setStackViewTasks */); 727 } 728 729 public final void onBusEvent(ShowEmptyViewEvent event) { 730 showEmptyView(R.string.recents_empty_message); 731 } 732 733 /** 734 * Shows the stack action button. 735 */ 736 private void showStackActionButton(final int duration, final boolean translate) { 737 final ReferenceCountedTrigger postAnimationTrigger = new ReferenceCountedTrigger(); 738 if (mStackActionButton.getVisibility() == View.INVISIBLE) { 739 mStackActionButton.setVisibility(View.VISIBLE); 740 mStackActionButton.setAlpha(0f); 741 if (translate) { 742 mStackActionButton.setTranslationY(mStackActionButton.getMeasuredHeight() * 743 (LegacyRecentsImpl.getConfiguration().isLowRamDevice ? 1 : -0.25f)); 744 } else { 745 mStackActionButton.setTranslationY(0f); 746 } 747 postAnimationTrigger.addLastDecrementRunnable(new Runnable() { 748 @Override 749 public void run() { 750 if (translate) { 751 mStackActionButton.animate() 752 .translationY(0f); 753 } 754 mStackActionButton.animate() 755 .alpha(1f) 756 .setDuration(duration) 757 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 758 .start(); 759 } 760 }); 761 } 762 postAnimationTrigger.flushLastDecrementRunnables(); 763 } 764 765 /** 766 * Hides the stack action button. 767 */ 768 private void hideStackActionButton(int duration, boolean translate) { 769 final ReferenceCountedTrigger postAnimationTrigger = new ReferenceCountedTrigger(); 770 hideStackActionButton(duration, translate, postAnimationTrigger); 771 postAnimationTrigger.flushLastDecrementRunnables(); 772 } 773 774 /** 775 * Hides the stack action button. 776 */ 777 private void hideStackActionButton(int duration, boolean translate, 778 final ReferenceCountedTrigger postAnimationTrigger) { 779 if (mStackActionButton.getVisibility() == View.VISIBLE) { 780 if (translate) { 781 mStackActionButton.animate().translationY(mStackActionButton.getMeasuredHeight() 782 * (LegacyRecentsImpl.getConfiguration().isLowRamDevice ? 1 : -0.25f)); 783 } 784 mStackActionButton.animate() 785 .alpha(0f) 786 .setDuration(duration) 787 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 788 .withEndAction(new Runnable() { 789 @Override 790 public void run() { 791 mStackActionButton.setVisibility(View.INVISIBLE); 792 postAnimationTrigger.decrement(); 793 } 794 }) 795 .start(); 796 postAnimationTrigger.increment(); 797 } 798 } 799 800 /** 801 * Animates a translation in the Y direction and fades in/out for empty view to show or hide it. 802 * @param show whether to translate up and fade in the empty view to the center of the screen 803 * @param postAnimationTrigger to keep track of the animation 804 */ 805 private void animateEmptyView(boolean show, ReferenceCountedTrigger postAnimationTrigger) { 806 float start = mTaskStackView.getStackAlgorithm().getTaskRect().height() / 4; 807 mEmptyView.setTranslationY(show ? start : 0); 808 mEmptyView.setAlpha(show ? 0f : 1f); 809 ViewPropertyAnimator animator = mEmptyView.animate() 810 .setDuration(150) 811 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 812 .translationY(show ? 0 : start) 813 .alpha(show ? 1f : 0f); 814 815 if (postAnimationTrigger != null) { 816 animator.setListener(postAnimationTrigger.decrementOnAnimationEnd()); 817 postAnimationTrigger.increment(); 818 } 819 animator.start(); 820 } 821 822 /** 823 * Updates the dock region to match the specified dock state. 824 */ 825 private void updateVisibleDockRegions(DockState[] newDockStates, 826 boolean isDefaultDockState, int overrideAreaAlpha, int overrideHintAlpha, 827 boolean animateAlpha, boolean animateBounds) { 828 ArraySet<DockState> newDockStatesSet = Utilities.arrayToSet(newDockStates, 829 new ArraySet<DockState>()); 830 ArrayList<DockState> visDockStates = mTouchHandler.getVisibleDockStates(); 831 for (int i = visDockStates.size() - 1; i >= 0; i--) { 832 DockState dockState = visDockStates.get(i); 833 DockState.ViewState viewState = dockState.viewState; 834 if (newDockStates == null || !newDockStatesSet.contains(dockState)) { 835 // This is no longer visible, so hide it 836 viewState.startAnimation(null, 0, 0, TaskStackView.SLOW_SYNC_STACK_DURATION, 837 Interpolators.FAST_OUT_SLOW_IN, animateAlpha, animateBounds); 838 } else { 839 // This state is now visible, update the bounds and show it 840 int areaAlpha = overrideAreaAlpha != -1 841 ? overrideAreaAlpha 842 : viewState.dockAreaAlpha; 843 int hintAlpha = overrideHintAlpha != -1 844 ? overrideHintAlpha 845 : viewState.hintTextAlpha; 846 Rect bounds = isDefaultDockState 847 ? dockState.getPreDockedBounds(getMeasuredWidth(), getMeasuredHeight(), 848 mSystemInsets) 849 : dockState.getDockedBounds(getMeasuredWidth(), getMeasuredHeight(), 850 mDividerSize, mSystemInsets, getResources()); 851 if (viewState.dockAreaOverlay.getCallback() != this) { 852 viewState.dockAreaOverlay.setCallback(this); 853 viewState.dockAreaOverlay.setBounds(bounds); 854 } 855 viewState.startAnimation(bounds, areaAlpha, hintAlpha, 856 TaskStackView.SLOW_SYNC_STACK_DURATION, Interpolators.FAST_OUT_SLOW_IN, 857 animateAlpha, animateBounds); 858 } 859 } 860 } 861 862 /** 863 * Scrim alpha based on how busy recents is: 864 * Scrim will be {@link ScrimController#GRADIENT_SCRIM_ALPHA} when the stack is empty, 865 * and {@link ScrimController#GRADIENT_SCRIM_ALPHA_BUSY} when it's full. 866 * 867 * @return Alpha from 0 to 1. 868 */ 869 private float getOpaqueScrimAlpha() { 870 return MathUtils.map(0, 1, ScrimController.GRADIENT_SCRIM_ALPHA, 871 ScrimController.GRADIENT_SCRIM_ALPHA_BUSY, mBusynessFactor); 872 } 873 874 /** 875 * Animates the background scrim to the given {@param alpha}. 876 */ 877 private void animateBackgroundScrim(float alpha, int duration) { 878 Utilities.cancelAnimationWithoutCallbacks(mBackgroundScrimAnimator); 879 // Calculate the absolute alpha to animate from 880 final int fromAlpha = mBackgroundScrim.getAlpha(); 881 final int toAlpha = (int) (alpha * 255); 882 mBackgroundScrimAnimator = ValueAnimator.ofInt(fromAlpha, toAlpha); 883 mBackgroundScrimAnimator.setDuration(duration); 884 mBackgroundScrimAnimator.setInterpolator(toAlpha > fromAlpha 885 ? Interpolators.ALPHA_IN 886 : Interpolators.ALPHA_OUT); 887 mBackgroundScrimAnimator.addUpdateListener(mUpdateBackgroundScrimAlpha); 888 mBackgroundScrimAnimator.start(); 889 } 890 891 /** 892 * @return the bounds of the stack action button. 893 */ 894 Rect getStackActionButtonBoundsFromStackLayout() { 895 Rect actionButtonRect = new Rect( 896 mTaskStackView.mLayoutAlgorithm.getStackActionButtonRect()); 897 int left, top; 898 if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { 899 Rect windowRect = LegacyRecentsImpl.getSystemServices().getWindowRect(); 900 int spaceLeft = windowRect.width() - mSystemInsets.left - mSystemInsets.right; 901 left = (spaceLeft - mStackActionButton.getMeasuredWidth()) / 2 + mSystemInsets.left; 902 top = windowRect.height() - (mStackActionButton.getMeasuredHeight() 903 + mSystemInsets.bottom + mStackActionButton.getPaddingBottom() / 2); 904 } else { 905 left = isLayoutRtl() 906 ? actionButtonRect.left - mStackActionButton.getPaddingLeft() 907 : actionButtonRect.right + mStackActionButton.getPaddingRight() 908 - mStackActionButton.getMeasuredWidth(); 909 top = actionButtonRect.top + 910 (actionButtonRect.height() - mStackActionButton.getMeasuredHeight()) / 2; 911 } 912 actionButtonRect.set(left, top, left + mStackActionButton.getMeasuredWidth(), 913 top + mStackActionButton.getMeasuredHeight()); 914 return actionButtonRect; 915 } 916 917 View getStackActionButton() { 918 return mStackActionButton; 919 } 920 921 /** 922 * Launches the specified {@link Task}. 923 */ 924 public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task, 925 final TaskStackView stackView, final TaskView taskView, 926 final boolean screenPinningRequested, final int windowingMode, final int activityType) { 927 928 final Runnable animStartedListener; 929 final AppTransitionAnimationSpecsFuture transitionFuture; 930 if (taskView != null) { 931 932 // Fetch window rect here already in order not to be blocked on lock contention in WM 933 // when the future calls it. 934 final Rect windowRect = LegacyRecentsImpl.getSystemServices().getWindowRect(); 935 transitionFuture = new AppTransitionAnimationSpecsFuture(stackView.getHandler()) { 936 @Override 937 public List<AppTransitionAnimationSpecCompat> composeSpecs() { 938 return mTransitionHelper.composeAnimationSpecs(task, stackView, windowingMode, 939 activityType, windowRect); 940 } 941 }; 942 animStartedListener = new Runnable() { 943 private boolean mHandled; 944 945 @Override 946 public void run() { 947 if (mHandled) { 948 return; 949 } 950 mHandled = true; 951 952 // If we are launching into another task, cancel the previous task's 953 // window transition 954 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task)); 955 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); 956 stackView.cancelAllTaskViewAnimations(); 957 958 if (screenPinningRequested) { 959 // Request screen pinning after the animation runs 960 mHandler.postDelayed(() -> { 961 EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, 962 task.key.id)); 963 }, 350); 964 } 965 966 if (!LegacyRecentsImpl.getConfiguration().isLowRamDevice) { 967 // Reset the state where we are waiting for the transition to start 968 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false)); 969 } 970 } 971 }; 972 } else { 973 // This is only the case if the task is not on screen (scrolled offscreen for example) 974 transitionFuture = null; 975 animStartedListener = new Runnable() { 976 private boolean mHandled; 977 978 @Override 979 public void run() { 980 if (mHandled) { 981 return; 982 } 983 mHandled = true; 984 985 // If we are launching into another task, cancel the previous task's 986 // window transition 987 EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task)); 988 EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent()); 989 stackView.cancelAllTaskViewAnimations(); 990 991 if (!LegacyRecentsImpl.getConfiguration().isLowRamDevice) { 992 // Reset the state where we are waiting for the transition to start 993 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false)); 994 } 995 } 996 }; 997 } 998 999 EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(true)); 1000 final ActivityOptions opts = RecentsTransition.createAspectScaleAnimation(mContext, 1001 mHandler, true /* scaleUp */, transitionFuture != null ? transitionFuture : null, 1002 animStartedListener); 1003 if (taskView == null) { 1004 // If there is no task view, then we do not need to worry about animating out occluding 1005 // task views, and we can launch immediately 1006 startTaskActivity(stack, task, taskView, opts, transitionFuture, 1007 windowingMode, activityType); 1008 } else { 1009 LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView, 1010 screenPinningRequested); 1011 EventBus.getDefault().send(launchStartedEvent); 1012 startTaskActivity(stack, task, taskView, opts, transitionFuture, windowingMode, 1013 activityType); 1014 } 1015 ActivityManagerWrapper.getInstance().closeSystemWindows(SYSTEM_DIALOG_REASON_RECENT_APPS); 1016 } 1017 1018 /** 1019 * Starts the activity for the launch task. 1020 * 1021 * @param taskView this is the {@link TaskView} that we are launching from. This can be null if 1022 * we are toggling recents and the launch-to task is now offscreen. 1023 */ 1024 private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView, 1025 ActivityOptions opts, AppTransitionAnimationSpecsFuture transitionFuture, 1026 int windowingMode, int activityType) { 1027 ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(task.key, opts, 1028 windowingMode, activityType, succeeded -> { 1029 if (succeeded) { 1030 // Keep track of the index of the task launch 1031 int taskIndexFromFront = 0; 1032 int taskIndex = stack.indexOfTask(task); 1033 if (taskIndex > -1) { 1034 taskIndexFromFront = stack.getTaskCount() - taskIndex - 1; 1035 } 1036 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront)); 1037 } else { 1038 Log.e(TAG, mContext.getString(R.string.recents_launch_error_message, task.title)); 1039 1040 // Dismiss the task if we fail to launch it 1041 if (taskView != null) { 1042 taskView.dismissTask(); 1043 } 1044 1045 // Keep track of failed launches 1046 EventBus.getDefault().send(new LaunchTaskFailedEvent()); 1047 } 1048 }, getHandler()); 1049 if (transitionFuture != null) { 1050 mHandler.post(transitionFuture::composeSpecsSynchronous); 1051 } 1052 } 1053 1054 @Override 1055 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1056 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1057 mTouchHandler.cancelStackActionButtonClick(); 1058 } 1059 1060 public void dump(String prefix, PrintWriter writer) { 1061 String innerPrefix = prefix + " "; 1062 String id = Integer.toHexString(System.identityHashCode(this)); 1063 1064 writer.print(prefix); writer.print(TAG); 1065 writer.print(" awaitingFirstLayout="); writer.print(mAwaitingFirstLayout ? "Y" : "N"); 1066 writer.print(" insets="); writer.print(Utilities.dumpRect(mSystemInsets)); 1067 writer.print(" [0x"); writer.print(id); writer.print("]"); 1068 writer.println(); 1069 1070 if (getStack() != null) { 1071 getStack().dump(innerPrefix, writer); 1072 } 1073 if (mTaskStackView != null) { 1074 mTaskStackView.dump(innerPrefix, writer); 1075 } 1076 } 1077 } 1078