• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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