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