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