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