1 /* 2 * Copyright (C) 2017 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.quickstep.views; 18 19 import static android.widget.Toast.LENGTH_SHORT; 20 21 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; 22 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 23 import static com.android.launcher3.anim.Interpolators.LINEAR; 24 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.animation.ObjectAnimator; 29 import android.animation.TimeInterpolator; 30 import android.animation.ValueAnimator; 31 import android.app.ActivityOptions; 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.Outline; 35 import android.graphics.Rect; 36 import android.graphics.RectF; 37 import android.graphics.drawable.Drawable; 38 import android.os.Bundle; 39 import android.os.Handler; 40 import android.util.AttributeSet; 41 import android.util.FloatProperty; 42 import android.util.Log; 43 import android.view.Gravity; 44 import android.view.View; 45 import android.view.ViewOutlineProvider; 46 import android.view.accessibility.AccessibilityNodeInfo; 47 import android.widget.FrameLayout; 48 import android.widget.Toast; 49 50 import com.android.launcher3.BaseDraggingActivity; 51 import com.android.launcher3.R; 52 import com.android.launcher3.Utilities; 53 import com.android.launcher3.anim.AnimatorPlaybackController; 54 import com.android.launcher3.anim.Interpolators; 55 import com.android.launcher3.logging.UserEventDispatcher; 56 import com.android.launcher3.testing.TestProtocol; 57 import com.android.launcher3.userevent.nano.LauncherLogProto; 58 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 59 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 60 import com.android.launcher3.util.PendingAnimation; 61 import com.android.launcher3.util.ViewPool.Reusable; 62 import com.android.quickstep.RecentsModel; 63 import com.android.quickstep.TaskIconCache; 64 import com.android.quickstep.TaskOverlayFactory; 65 import com.android.quickstep.TaskSystemShortcut; 66 import com.android.quickstep.TaskThumbnailCache; 67 import com.android.quickstep.TaskUtils; 68 import com.android.quickstep.util.TaskCornerRadius; 69 import com.android.quickstep.views.RecentsView.PageCallbacks; 70 import com.android.quickstep.views.RecentsView.ScrollState; 71 import com.android.systemui.shared.recents.model.Task; 72 import com.android.systemui.shared.system.ActivityManagerWrapper; 73 import com.android.systemui.shared.system.ActivityOptionsCompat; 74 import com.android.systemui.shared.system.QuickStepContract; 75 76 import java.util.Collections; 77 import java.util.List; 78 import java.util.function.Consumer; 79 80 /** 81 * A task in the Recents view. 82 */ 83 public class TaskView extends FrameLayout implements PageCallbacks, Reusable { 84 85 private static final String TAG = TaskView.class.getSimpleName(); 86 87 /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ 88 private static final TimeInterpolator CURVE_INTERPOLATOR 89 = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; 90 91 /** 92 * The alpha of a black scrim on a page in the carousel as it leaves the screen. 93 * In the resting position of the carousel, the adjacent pages have about half this scrim. 94 */ 95 public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; 96 97 /** 98 * How much to scale down pages near the edge of the screen. 99 */ 100 public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; 101 102 public static final long SCALE_ICON_DURATION = 120; 103 private static final long DIM_ANIM_DURATION = 700; 104 105 private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT = 106 Collections.singletonList(new Rect()); 107 108 public static final FloatProperty<TaskView> FULLSCREEN_PROGRESS = 109 new FloatProperty<TaskView>("fullscreenProgress") { 110 @Override 111 public void setValue(TaskView taskView, float v) { 112 taskView.setFullscreenProgress(v); 113 } 114 115 @Override 116 public Float get(TaskView taskView) { 117 return taskView.mFullscreenProgress; 118 } 119 }; 120 121 private static final FloatProperty<TaskView> FOCUS_TRANSITION = 122 new FloatProperty<TaskView>("focusTransition") { 123 @Override 124 public void setValue(TaskView taskView, float v) { 125 taskView.setIconAndDimTransitionProgress(v, false /* invert */); 126 } 127 128 @Override 129 public Float get(TaskView taskView) { 130 return taskView.mFocusTransitionProgress; 131 } 132 }; 133 134 private final OnAttachStateChangeListener mTaskMenuStateListener = 135 new OnAttachStateChangeListener() { 136 @Override 137 public void onViewAttachedToWindow(View view) { 138 } 139 140 @Override 141 public void onViewDetachedFromWindow(View view) { 142 if (mMenuView != null) { 143 mMenuView.removeOnAttachStateChangeListener(this); 144 mMenuView = null; 145 } 146 } 147 }; 148 149 private final TaskOutlineProvider mOutlineProvider; 150 151 private Task mTask; 152 private TaskThumbnailView mSnapshotView; 153 private TaskMenuView mMenuView; 154 private IconView mIconView; 155 private final DigitalWellBeingToast mDigitalWellBeingToast; 156 private float mCurveScale; 157 private float mFullscreenProgress; 158 private final FullscreenDrawParams mCurrentFullscreenParams; 159 private final float mCornerRadius; 160 private final float mWindowCornerRadius; 161 private final BaseDraggingActivity mActivity; 162 163 private ObjectAnimator mIconAndDimAnimator; 164 private float mIconScaleAnimStartProgress = 0; 165 private float mFocusTransitionProgress = 1; 166 private float mStableAlpha = 1; 167 168 private boolean mShowScreenshot; 169 170 // The current background requests to load the task thumbnail and icon 171 private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest; 172 private TaskIconCache.IconLoadRequest mIconLoadRequest; 173 174 // Order in which the footers appear. Lower order appear below higher order. 175 public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0; 176 public static final int INDEX_PROACTIVE_SUGGEST = 1; 177 private final FooterWrapper[] mFooters = new FooterWrapper[2]; 178 private float mFooterVerticalOffset = 0; 179 private float mFooterAlpha = 1; 180 private int mStackHeight; 181 TaskView(Context context)182 public TaskView(Context context) { 183 this(context, null); 184 } 185 TaskView(Context context, AttributeSet attrs)186 public TaskView(Context context, AttributeSet attrs) { 187 this(context, attrs, 0); 188 } 189 TaskView(Context context, AttributeSet attrs, int defStyleAttr)190 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 191 super(context, attrs, defStyleAttr); 192 mActivity = BaseDraggingActivity.fromContext(context); 193 setOnClickListener((view) -> { 194 if (getTask() == null) { 195 return; 196 } 197 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 198 if (isRunningTask()) { 199 createLaunchAnimationForRunningTask().start(); 200 } else { 201 launchTask(true /* animate */); 202 } 203 } else { 204 launchTask(true /* animate */); 205 } 206 207 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 208 Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this), 209 TaskUtils.getLaunchComponentKeyForTask(getTask().key)); 210 mActivity.getStatsLogManager().logTaskLaunch(getRecentsView(), 211 TaskUtils.getLaunchComponentKeyForTask(getTask().key)); 212 }); 213 mCornerRadius = TaskCornerRadius.get(context); 214 mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources()); 215 mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius); 216 mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this); 217 218 mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams); 219 setOutlineProvider(mOutlineProvider); 220 } 221 222 @Override onFinishInflate()223 protected void onFinishInflate() { 224 super.onFinishInflate(); 225 mSnapshotView = findViewById(R.id.snapshot); 226 mIconView = findViewById(R.id.icon); 227 } 228 getMenuView()229 public TaskMenuView getMenuView() { 230 return mMenuView; 231 } 232 getDigitalWellBeingToast()233 public DigitalWellBeingToast getDigitalWellBeingToast() { 234 return mDigitalWellBeingToast; 235 } 236 237 /** 238 * Updates this task view to the given {@param task}. 239 */ bind(Task task)240 public void bind(Task task) { 241 cancelPendingLoadTasks(); 242 mTask = task; 243 mSnapshotView.bind(task); 244 } 245 getTask()246 public Task getTask() { 247 return mTask; 248 } 249 getThumbnail()250 public TaskThumbnailView getThumbnail() { 251 return mSnapshotView; 252 } 253 getIconView()254 public IconView getIconView() { 255 return mIconView; 256 } 257 createLaunchAnimationForRunningTask()258 public AnimatorPlaybackController createLaunchAnimationForRunningTask() { 259 final PendingAnimation pendingAnimation = 260 getRecentsView().createTaskLauncherAnimation(this, RECENTS_LAUNCH_DURATION); 261 pendingAnimation.anim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); 262 AnimatorPlaybackController currentAnimation = AnimatorPlaybackController 263 .wrap(pendingAnimation.anim, RECENTS_LAUNCH_DURATION, null); 264 currentAnimation.setEndAction(() -> { 265 pendingAnimation.finish(true, Touch.SWIPE); 266 launchTask(false); 267 }); 268 return currentAnimation; 269 } 270 launchTask(boolean animate)271 public void launchTask(boolean animate) { 272 launchTask(animate, false /* freezeTaskList */); 273 } 274 launchTask(boolean animate, boolean freezeTaskList)275 public void launchTask(boolean animate, boolean freezeTaskList) { 276 launchTask(animate, freezeTaskList, (result) -> { 277 if (!result) { 278 notifyTaskLaunchFailed(TAG); 279 } 280 }, getHandler()); 281 } 282 launchTask(boolean animate, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)283 public void launchTask(boolean animate, Consumer<Boolean> resultCallback, 284 Handler resultCallbackHandler) { 285 launchTask(animate, false /* freezeTaskList */, resultCallback, resultCallbackHandler); 286 } 287 launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)288 public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, 289 Handler resultCallbackHandler) { 290 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 291 if (isRunningTask()) { 292 getRecentsView().finishRecentsAnimation(false /* toRecents */, 293 () -> resultCallbackHandler.post(() -> resultCallback.accept(true))); 294 } else { 295 launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler); 296 } 297 } else { 298 launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler); 299 } 300 } 301 launchTaskInternal(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)302 private void launchTaskInternal(boolean animate, boolean freezeTaskList, 303 Consumer<Boolean> resultCallback, Handler resultCallbackHandler) { 304 if (mTask != null) { 305 final ActivityOptions opts; 306 if (animate) { 307 opts = mActivity.getActivityLaunchOptions(this); 308 if (freezeTaskList) { 309 ActivityOptionsCompat.setFreezeRecentTasksList(opts); 310 } 311 ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, 312 opts, resultCallback, resultCallbackHandler); 313 } else { 314 opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> { 315 if (resultCallback != null) { 316 // Only post the animation start after the system has indicated that the 317 // transition has started 318 resultCallbackHandler.post(() -> resultCallback.accept(true)); 319 } 320 }, resultCallbackHandler); 321 if (freezeTaskList) { 322 ActivityOptionsCompat.setFreezeRecentTasksList(opts); 323 } 324 ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, 325 opts, (success) -> { 326 if (resultCallback != null && !success) { 327 // If the call to start activity failed, then post the result 328 // immediately, otherwise, wait for the animation start callback 329 // from the activity options above 330 resultCallbackHandler.post(() -> resultCallback.accept(false)); 331 } 332 }, resultCallbackHandler); 333 } 334 } 335 } 336 onTaskListVisibilityChanged(boolean visible)337 public void onTaskListVisibilityChanged(boolean visible) { 338 if (mTask == null) { 339 return; 340 } 341 cancelPendingLoadTasks(); 342 if (visible) { 343 // These calls are no-ops if the data is already loaded, try and load the high 344 // resolution thumbnail if the state permits 345 RecentsModel model = RecentsModel.INSTANCE.get(getContext()); 346 TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); 347 TaskIconCache iconCache = model.getIconCache(); 348 mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground( 349 mTask, thumbnail -> mSnapshotView.setThumbnail(mTask, thumbnail)); 350 mIconLoadRequest = iconCache.updateIconInBackground(mTask, 351 (task) -> { 352 setIcon(task.icon); 353 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) { 354 getRecentsView().updateLiveTileIcon(task.icon); 355 } 356 mDigitalWellBeingToast.initialize(mTask); 357 }); 358 } else { 359 mSnapshotView.setThumbnail(null, null); 360 setIcon(null); 361 // Reset the task thumbnail reference as well (it will be fetched from the cache or 362 // reloaded next time we need it) 363 mTask.thumbnail = null; 364 } 365 } 366 cancelPendingLoadTasks()367 private void cancelPendingLoadTasks() { 368 if (mThumbnailLoadRequest != null) { 369 mThumbnailLoadRequest.cancel(); 370 mThumbnailLoadRequest = null; 371 } 372 if (mIconLoadRequest != null) { 373 mIconLoadRequest.cancel(); 374 mIconLoadRequest = null; 375 } 376 } 377 showTaskMenu(int action)378 private boolean showTaskMenu(int action) { 379 getRecentsView().snapToPage(getRecentsView().indexOfChild(this)); 380 mMenuView = TaskMenuView.showForTask(this); 381 UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE, 382 LauncherLogProto.ItemType.TASK_ICON); 383 if (mMenuView != null) { 384 mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener); 385 } 386 return mMenuView != null; 387 } 388 setIcon(Drawable icon)389 private void setIcon(Drawable icon) { 390 if (icon != null) { 391 mIconView.setDrawable(icon); 392 mIconView.setOnClickListener(v -> showTaskMenu(Touch.TAP)); 393 mIconView.setOnLongClickListener(v -> { 394 requestDisallowInterceptTouchEvent(true); 395 return showTaskMenu(Touch.LONGPRESS); 396 }); 397 } else { 398 mIconView.setDrawable(null); 399 mIconView.setOnClickListener(null); 400 mIconView.setOnLongClickListener(null); 401 } 402 } 403 setIconAndDimTransitionProgress(float progress, boolean invert)404 private void setIconAndDimTransitionProgress(float progress, boolean invert) { 405 if (invert) { 406 progress = 1 - progress; 407 } 408 mFocusTransitionProgress = progress; 409 mSnapshotView.setDimAlphaMultipler(progress); 410 float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION; 411 float lowerClamp = invert ? 1f - iconScalePercentage : 0; 412 float upperClamp = invert ? 1 : iconScalePercentage; 413 float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp) 414 .getInterpolation(progress); 415 mIconView.setScaleX(scale); 416 mIconView.setScaleY(scale); 417 418 mFooterVerticalOffset = 1.0f - scale; 419 for (FooterWrapper footer : mFooters) { 420 if (footer != null) { 421 footer.updateFooterOffset(); 422 } 423 } 424 } 425 setIconScaleAnimStartProgress(float startProgress)426 public void setIconScaleAnimStartProgress(float startProgress) { 427 mIconScaleAnimStartProgress = startProgress; 428 } 429 animateIconScaleAndDimIntoView()430 public void animateIconScaleAndDimIntoView() { 431 if (mIconAndDimAnimator != null) { 432 mIconAndDimAnimator.cancel(); 433 } 434 mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1); 435 mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress); 436 mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR); 437 mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() { 438 @Override 439 public void onAnimationEnd(Animator animation) { 440 mIconAndDimAnimator = null; 441 } 442 }); 443 mIconAndDimAnimator.start(); 444 } 445 setIconScaleAndDim(float iconScale)446 protected void setIconScaleAndDim(float iconScale) { 447 setIconScaleAndDim(iconScale, false); 448 } 449 setIconScaleAndDim(float iconScale, boolean invert)450 private void setIconScaleAndDim(float iconScale, boolean invert) { 451 if (mIconAndDimAnimator != null) { 452 mIconAndDimAnimator.cancel(); 453 } 454 setIconAndDimTransitionProgress(iconScale, invert); 455 } 456 resetViewTransforms()457 private void resetViewTransforms() { 458 setCurveScale(1); 459 setTranslationX(0f); 460 setTranslationY(0f); 461 setTranslationZ(0); 462 setAlpha(mStableAlpha); 463 setIconScaleAndDim(1); 464 } 465 resetVisualProperties()466 public void resetVisualProperties() { 467 resetViewTransforms(); 468 setFullscreenProgress(0); 469 } 470 setStableAlpha(float parentAlpha)471 public void setStableAlpha(float parentAlpha) { 472 mStableAlpha = parentAlpha; 473 setAlpha(mStableAlpha); 474 } 475 476 @Override onRecycle()477 public void onRecycle() { 478 resetViewTransforms(); 479 // Clear any references to the thumbnail (it will be re-read either from the cache or the 480 // system on next bind) 481 mSnapshotView.setThumbnail(mTask, null); 482 setOverlayEnabled(false); 483 onTaskListVisibilityChanged(false); 484 } 485 486 @Override onPageScroll(ScrollState scrollState)487 public void onPageScroll(ScrollState scrollState) { 488 float curveInterpolation = 489 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); 490 491 mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); 492 setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); 493 494 mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f); 495 for (FooterWrapper footer : mFooters) { 496 if (footer != null) { 497 footer.mView.setAlpha(mFooterAlpha); 498 } 499 } 500 501 if (mMenuView != null) { 502 mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY()); 503 mMenuView.setScaleX(getScaleX()); 504 mMenuView.setScaleY(getScaleY()); 505 } 506 } 507 508 509 /** 510 * Sets the footer at the specific index and returns the previously set footer. 511 */ setFooter(int index, View view)512 public View setFooter(int index, View view) { 513 View oldFooter = null; 514 515 // If the footer are is already collapsed, do not animate entry 516 boolean shouldAnimateEntry = mFooterVerticalOffset <= 0; 517 518 if (mFooters[index] != null) { 519 oldFooter = mFooters[index].mView; 520 mFooters[index].release(); 521 removeView(oldFooter); 522 523 // If we are replacing an existing footer, do not animate entry 524 shouldAnimateEntry = false; 525 } 526 if (view != null) { 527 int indexToAdd = getChildCount(); 528 for (int i = index - 1; i >= 0; i--) { 529 if (mFooters[i] != null) { 530 indexToAdd = indexOfChild(mFooters[i].mView); 531 break; 532 } 533 } 534 535 addView(view, indexToAdd); 536 ((LayoutParams) view.getLayoutParams()).gravity = 537 Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 538 view.setAlpha(mFooterAlpha); 539 mFooters[index] = new FooterWrapper(view); 540 if (shouldAnimateEntry) { 541 mFooters[index].animateEntry(); 542 } 543 } else { 544 mFooters[index] = null; 545 } 546 547 mStackHeight = 0; 548 for (FooterWrapper footer : mFooters) { 549 if (footer != null) { 550 footer.setVerticalShift(mStackHeight); 551 mStackHeight += footer.mExpectedHeight; 552 } 553 } 554 555 return oldFooter; 556 } 557 558 @Override onLayout(boolean changed, int left, int top, int right, int bottom)559 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 560 super.onLayout(changed, left, top, right, bottom); 561 setPivotX((right - left) * 0.5f); 562 setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); 563 if (Utilities.ATLEAST_Q) { 564 SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight()); 565 setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); 566 } 567 568 mStackHeight = 0; 569 for (FooterWrapper footer : mFooters) { 570 if (footer != null) { 571 mStackHeight += footer.mView.getHeight(); 572 } 573 } 574 for (FooterWrapper footer : mFooters) { 575 if (footer != null) { 576 footer.updateFooterOffset(); 577 } 578 } 579 } 580 getCurveScaleForInterpolation(float linearInterpolation)581 public static float getCurveScaleForInterpolation(float linearInterpolation) { 582 float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); 583 return getCurveScaleForCurveInterpolation(curveInterpolation); 584 } 585 getCurveScaleForCurveInterpolation(float curveInterpolation)586 private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { 587 return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; 588 } 589 setCurveScale(float curveScale)590 public void setCurveScale(float curveScale) { 591 mCurveScale = curveScale; 592 onScaleChanged(); 593 } 594 getCurveScale()595 public float getCurveScale() { 596 return mCurveScale; 597 } 598 onScaleChanged()599 private void onScaleChanged() { 600 float scale = mCurveScale; 601 setScaleX(scale); 602 setScaleY(scale); 603 } 604 605 @Override hasOverlappingRendering()606 public boolean hasOverlappingRendering() { 607 // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. 608 return false; 609 } 610 611 private static final class TaskOutlineProvider extends ViewOutlineProvider { 612 613 private final int mMarginTop; 614 private FullscreenDrawParams mFullscreenParams; 615 TaskOutlineProvider(Resources res, FullscreenDrawParams fullscreenParams)616 TaskOutlineProvider(Resources res, FullscreenDrawParams fullscreenParams) { 617 mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); 618 mFullscreenParams = fullscreenParams; 619 } 620 setFullscreenParams(FullscreenDrawParams params)621 public void setFullscreenParams(FullscreenDrawParams params) { 622 mFullscreenParams = params; 623 } 624 625 @Override getOutline(View view, Outline outline)626 public void getOutline(View view, Outline outline) { 627 RectF insets = mFullscreenParams.mCurrentDrawnInsets; 628 float scale = mFullscreenParams.mScale; 629 outline.setRoundRect(0, 630 (int) (mMarginTop * scale), 631 (int) ((insets.left + view.getWidth() + insets.right) * scale), 632 (int) ((insets.top + view.getHeight() + insets.bottom) * scale), 633 mFullscreenParams.mCurrentDrawnCornerRadius); 634 } 635 } 636 637 private class FooterWrapper extends ViewOutlineProvider { 638 639 final View mView; 640 final ViewOutlineProvider mOldOutlineProvider; 641 final ViewOutlineProvider mDelegate; 642 643 final int mExpectedHeight; 644 final int mOldPaddingBottom; 645 646 int mAnimationOffset = 0; 647 int mEntryAnimationOffset = 0; 648 FooterWrapper(View view)649 public FooterWrapper(View view) { 650 mView = view; 651 mOldOutlineProvider = view.getOutlineProvider(); 652 mDelegate = mOldOutlineProvider == null 653 ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider; 654 655 int h = view.getLayoutParams().height; 656 if (h > 0) { 657 mExpectedHeight = h; 658 } else { 659 int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST); 660 view.measure(m, m); 661 mExpectedHeight = view.getMeasuredHeight(); 662 } 663 mOldPaddingBottom = view.getPaddingBottom(); 664 665 if (mOldOutlineProvider != null) { 666 view.setOutlineProvider(this); 667 view.setClipToOutline(true); 668 } 669 } 670 setVerticalShift(int shift)671 public void setVerticalShift(int shift) { 672 mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(), 673 mView.getPaddingRight(), mOldPaddingBottom + shift); 674 } 675 676 @Override getOutline(View view, Outline outline)677 public void getOutline(View view, Outline outline) { 678 mDelegate.getOutline(view, outline); 679 outline.offset(0, -mAnimationOffset - mEntryAnimationOffset); 680 } 681 updateFooterOffset()682 void updateFooterOffset() { 683 mAnimationOffset = Math.round(mStackHeight * mFooterVerticalOffset); 684 mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset 685 + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom 686 + mCurrentFullscreenParams.mCurrentDrawnInsets.top); 687 mView.invalidateOutline(); 688 } 689 release()690 void release() { 691 mView.setOutlineProvider(mOldOutlineProvider); 692 setVerticalShift(0); 693 } 694 animateEntry()695 void animateEntry() { 696 ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 697 animator.addUpdateListener(anim -> { 698 float factor = 1 - anim.getAnimatedFraction(); 699 int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom; 700 mEntryAnimationOffset = Math.round(factor * totalShift); 701 updateFooterOffset(); 702 }); 703 animator.setDuration(100); 704 animator.start(); 705 } 706 } 707 708 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)709 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 710 super.onInitializeAccessibilityNodeInfo(info); 711 712 info.addAction( 713 new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task, 714 getContext().getText(R.string.accessibility_close_task))); 715 716 final Context context = getContext(); 717 final List<TaskSystemShortcut> shortcuts = 718 TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this); 719 final int count = shortcuts.size(); 720 for (int i = 0; i < count; ++i) { 721 final TaskSystemShortcut menuOption = shortcuts.get(i); 722 OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this); 723 if (onClickListener != null) { 724 info.addAction(menuOption.createAccessibilityAction(context)); 725 } 726 } 727 728 if (mDigitalWellBeingToast.hasLimit()) { 729 info.addAction( 730 new AccessibilityNodeInfo.AccessibilityAction( 731 R.string.accessibility_app_usage_settings, 732 getContext().getText(R.string.accessibility_app_usage_settings))); 733 } 734 735 final RecentsView recentsView = getRecentsView(); 736 final AccessibilityNodeInfo.CollectionItemInfo itemInfo = 737 AccessibilityNodeInfo.CollectionItemInfo.obtain( 738 0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1, 739 false); 740 info.setCollectionItemInfo(itemInfo); 741 } 742 743 @Override performAccessibilityAction(int action, Bundle arguments)744 public boolean performAccessibilityAction(int action, Bundle arguments) { 745 if (action == R.string.accessibility_close_task) { 746 getRecentsView().dismissTask(this, true /*animateTaskView*/, 747 true /*removeTask*/); 748 return true; 749 } 750 751 if (action == R.string.accessibility_app_usage_settings) { 752 mDigitalWellBeingToast.openAppUsageSettings(this); 753 return true; 754 } 755 756 final List<TaskSystemShortcut> shortcuts = 757 TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this); 758 final int count = shortcuts.size(); 759 for (int i = 0; i < count; ++i) { 760 final TaskSystemShortcut menuOption = shortcuts.get(i); 761 if (menuOption.hasHandlerForAction(action)) { 762 OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this); 763 if (onClickListener != null) { 764 onClickListener.onClick(this); 765 } 766 return true; 767 } 768 } 769 770 return super.performAccessibilityAction(action, arguments); 771 } 772 getRecentsView()773 public RecentsView getRecentsView() { 774 return (RecentsView) getParent(); 775 } 776 notifyTaskLaunchFailed(String tag)777 public void notifyTaskLaunchFailed(String tag) { 778 String msg = "Failed to launch task"; 779 if (mTask != null) { 780 msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; 781 } 782 Log.w(tag, msg); 783 Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); 784 } 785 786 /** 787 * Hides the icon and shows insets when this TaskView is about to be shown fullscreen. 788 * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. 789 */ setFullscreenProgress(float progress)790 public void setFullscreenProgress(float progress) { 791 progress = Utilities.boundToRange(progress, 0, 1); 792 if (progress == mFullscreenProgress) { 793 return; 794 } 795 mFullscreenProgress = progress; 796 boolean isFullscreen = mFullscreenProgress > 0; 797 mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE); 798 setClipChildren(!isFullscreen); 799 setClipToPadding(!isFullscreen); 800 801 TaskThumbnailView thumbnail = getThumbnail(); 802 boolean isMultiWindowMode = mActivity.getDeviceProfile().isMultiWindowMode; 803 RectF insets = thumbnail.getInsetsToDrawInFullscreen(isMultiWindowMode); 804 float currentInsetsLeft = insets.left * mFullscreenProgress; 805 float currentInsetsRight = insets.right * mFullscreenProgress; 806 mCurrentFullscreenParams.setInsets(currentInsetsLeft, 807 insets.top * mFullscreenProgress, 808 currentInsetsRight, 809 insets.bottom * mFullscreenProgress); 810 float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius; 811 mCurrentFullscreenParams.setCornerRadius(Utilities.mapRange(mFullscreenProgress, 812 mCornerRadius, fullscreenCornerRadius) / getRecentsView().getScaleX()); 813 // We scaled the thumbnail to fit the content (excluding insets) within task view width. 814 // Now that we are drawing left/right insets again, we need to scale down to fit them. 815 if (getWidth() > 0) { 816 mCurrentFullscreenParams.setScale(getWidth() 817 / (getWidth() + currentInsetsLeft + currentInsetsRight)); 818 } 819 820 if (!getRecentsView().isTaskIconScaledDown(this)) { 821 // Some of the items in here are dependent on the current fullscreen params, but don't 822 // update them if the icon is supposed to be scaled down. 823 setIconScaleAndDim(progress, true /* invert */); 824 } 825 826 thumbnail.setFullscreenParams(mCurrentFullscreenParams); 827 mOutlineProvider.setFullscreenParams(mCurrentFullscreenParams); 828 invalidateOutline(); 829 } 830 isRunningTask()831 public boolean isRunningTask() { 832 if (getRecentsView() == null) { 833 return false; 834 } 835 return this == getRecentsView().getRunningTaskView(); 836 } 837 setShowScreenshot(boolean showScreenshot)838 public void setShowScreenshot(boolean showScreenshot) { 839 mShowScreenshot = showScreenshot; 840 } 841 showScreenshot()842 public boolean showScreenshot() { 843 if (!isRunningTask()) { 844 return true; 845 } 846 return mShowScreenshot; 847 } 848 setOverlayEnabled(boolean overlayEnabled)849 public void setOverlayEnabled(boolean overlayEnabled) { 850 mSnapshotView.setOverlayEnabled(overlayEnabled); 851 } 852 853 /** 854 * We update and subsequently draw these in {@link #setFullscreenProgress(float)}. 855 */ 856 static class FullscreenDrawParams { 857 RectF mCurrentDrawnInsets = new RectF(); 858 float mCurrentDrawnCornerRadius; 859 /** The current scale we apply to the thumbnail to adjust for new left/right insets. */ 860 float mScale = 1; 861 FullscreenDrawParams(float cornerRadius)862 public FullscreenDrawParams(float cornerRadius) { 863 setCornerRadius(cornerRadius); 864 } 865 setInsets(float left, float top, float right, float bottom)866 public void setInsets(float left, float top, float right, float bottom) { 867 mCurrentDrawnInsets.set(left, top, right, bottom); 868 } 869 setCornerRadius(float cornerRadius)870 public void setCornerRadius(float cornerRadius) { 871 mCurrentDrawnCornerRadius = cornerRadius; 872 } 873 setScale(float scale)874 public void setScale(float scale) { 875 mScale = scale; 876 } 877 } 878 } 879