1 /*
2  * Copyright (C) 2014 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.systemui.recents.views;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorSet;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Outline;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.util.AttributeSet;
29 import android.util.FloatProperty;
30 import android.util.Property;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.ViewDebug;
34 import android.view.ViewOutlineProvider;
35 import android.widget.TextView;
36 import android.widget.Toast;
37 
38 import com.android.internal.logging.MetricsLogger;
39 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
40 import com.android.systemui.Interpolators;
41 import com.android.systemui.R;
42 import com.android.systemui.recents.LegacyRecentsImpl;
43 import com.android.systemui.recents.RecentsActivity;
44 import com.android.systemui.recents.RecentsConfiguration;
45 import com.android.systemui.recents.events.EventBus;
46 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
47 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
48 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
49 import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent;
50 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
51 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
52 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
53 import com.android.systemui.recents.misc.SystemServicesProxy;
54 import com.android.systemui.recents.utilities.AnimationProps;
55 import com.android.systemui.recents.utilities.Utilities;
56 import com.android.systemui.shared.recents.model.Task;
57 import com.android.systemui.shared.recents.model.ThumbnailData;
58 
59 import java.io.PrintWriter;
60 import java.util.ArrayList;
61 
62 /**
63  * A {@link TaskView} represents a fixed view of a task. Because the TaskView's layout is directed
64  * solely by the {@link TaskStackView}, we make it a fixed size layout which allows relayouts down
65  * the view hierarchy, but not upwards from any of its children (the TaskView will relayout itself
66  * with the previous bounds if any child requests layout).
67  */
68 public class TaskView extends FixedSizeFrameLayout implements Task.TaskCallbacks,
69         TaskStackAnimationHelper.Callbacks, View.OnClickListener, View.OnLongClickListener {
70 
71     /** The TaskView callbacks */
72     interface TaskViewCallbacks {
onTaskViewClipStateChanged(TaskView tv)73         void onTaskViewClipStateChanged(TaskView tv);
74     }
75 
76     /**
77      * The dim overlay is generally calculated from the task progress, but occasionally (like when
78      * launching) needs to be animated independently of the task progress.  This call is only used
79      * when animating the task into Recents, when the header dim is already applied
80      */
81     public static final Property<TaskView, Float> DIM_ALPHA_WITHOUT_HEADER =
82             new FloatProperty<TaskView>("dimAlphaWithoutHeader") {
83                 @Override
84                 public void setValue(TaskView tv, float dimAlpha) {
85                     tv.setDimAlphaWithoutHeader(dimAlpha);
86                 }
87 
88                 @Override
89                 public Float get(TaskView tv) {
90                     return tv.getDimAlpha();
91                 }
92             };
93 
94     /**
95      * The dim overlay is generally calculated from the task progress, but occasionally (like when
96      * launching) needs to be animated independently of the task progress.
97      */
98     public static final Property<TaskView, Float> DIM_ALPHA =
99             new FloatProperty<TaskView>("dimAlpha") {
100                 @Override
101                 public void setValue(TaskView tv, float dimAlpha) {
102                     tv.setDimAlpha(dimAlpha);
103                 }
104 
105                 @Override
106                 public Float get(TaskView tv) {
107                     return tv.getDimAlpha();
108                 }
109             };
110 
111     /**
112      * The dim overlay is generally calculated from the task progress, but occasionally (like when
113      * launching) needs to be animated independently of the task progress.
114      */
115     public static final Property<TaskView, Float> VIEW_OUTLINE_ALPHA =
116             new FloatProperty<TaskView>("viewOutlineAlpha") {
117                 @Override
118                 public void setValue(TaskView tv, float alpha) {
119                     tv.getViewBounds().setAlpha(alpha);
120                 }
121 
122                 @Override
123                 public Float get(TaskView tv) {
124                     return tv.getViewBounds().getAlpha();
125                 }
126             };
127 
128     @ViewDebug.ExportedProperty(category="recents")
129     private float mDimAlpha;
130     private float mActionButtonTranslationZ;
131 
132     @ViewDebug.ExportedProperty(deepExport=true, prefix="task_")
133     private Task mTask;
134     private boolean mTaskBound;
135     @ViewDebug.ExportedProperty(category="recents")
136     private boolean mClipViewInStack = true;
137     @ViewDebug.ExportedProperty(category="recents")
138     private boolean mTouchExplorationEnabled;
139     @ViewDebug.ExportedProperty(category="recents")
140     private boolean mIsDisabledInSafeMode;
141     @ViewDebug.ExportedProperty(deepExport=true, prefix="view_bounds_")
142     private AnimateableViewBounds mViewBounds;
143 
144     private AnimatorSet mTransformAnimation;
145     private ObjectAnimator mDimAnimator;
146     private ObjectAnimator mOutlineAnimator;
147     private final TaskViewTransform mTargetAnimationTransform = new TaskViewTransform();
148     private ArrayList<Animator> mTmpAnimators = new ArrayList<>();
149 
150     @ViewDebug.ExportedProperty(deepExport=true, prefix="thumbnail_")
151     protected TaskViewThumbnail mThumbnailView;
152     @ViewDebug.ExportedProperty(deepExport=true, prefix="header_")
153     protected TaskViewHeader mHeaderView;
154     private View mActionButtonView;
155     private View mIncompatibleAppToastView;
156     private TaskViewCallbacks mCb;
157 
158     @ViewDebug.ExportedProperty(category="recents")
159     private Point mDownTouchPos = new Point();
160 
161     private Toast mDisabledAppToast;
162 
TaskView(Context context)163     public TaskView(Context context) {
164         this(context, null);
165     }
166 
TaskView(Context context, AttributeSet attrs)167     public TaskView(Context context, AttributeSet attrs) {
168         this(context, attrs, 0);
169     }
170 
TaskView(Context context, AttributeSet attrs, int defStyleAttr)171     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
172         this(context, attrs, defStyleAttr, 0);
173     }
174 
TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)175     public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
176         super(context, attrs, defStyleAttr, defStyleRes);
177         RecentsConfiguration config = LegacyRecentsImpl.getConfiguration();
178         Resources res = context.getResources();
179         mViewBounds = createOutlineProvider();
180         if (config.fakeShadows) {
181             setBackground(new FakeShadowDrawable(res, config));
182         }
183         setOutlineProvider(mViewBounds);
184         setOnLongClickListener(this);
185         setAccessibilityDelegate(new TaskViewAccessibilityDelegate(this));
186     }
187 
188     /** Set callback */
setCallbacks(TaskViewCallbacks cb)189     void setCallbacks(TaskViewCallbacks cb) {
190         mCb = cb;
191     }
192 
193     /**
194      * Called from RecentsActivity when it is relaunched.
195      */
onReload(boolean isResumingFromVisible)196     void onReload(boolean isResumingFromVisible) {
197         resetNoUserInteractionState();
198         if (!isResumingFromVisible) {
199             resetViewProperties();
200         }
201     }
202 
203     /** Gets the task */
getTask()204     public Task getTask() {
205         return mTask;
206     }
207 
208     /* Create an outline provider to clip and outline the view */
createOutlineProvider()209     protected AnimateableViewBounds createOutlineProvider() {
210         return new AnimateableViewBounds(this, mContext.getResources().getDimensionPixelSize(
211             R.dimen.recents_task_view_shadow_rounded_corners_radius));
212     }
213 
214     /** Returns the view bounds. */
getViewBounds()215     AnimateableViewBounds getViewBounds() {
216         return mViewBounds;
217     }
218 
219     @Override
onFinishInflate()220     protected void onFinishInflate() {
221         // Bind the views
222         mHeaderView = findViewById(R.id.task_view_bar);
223         mThumbnailView = findViewById(R.id.task_view_thumbnail);
224         mThumbnailView.updateClipToTaskBar(mHeaderView);
225         mActionButtonView = findViewById(R.id.lock_to_app_fab);
226         mActionButtonView.setOutlineProvider(new ViewOutlineProvider() {
227             @Override
228             public void getOutline(View view, Outline outline) {
229                 // Set the outline to match the FAB background
230                 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight());
231                 outline.setAlpha(0.35f);
232             }
233         });
234         mActionButtonView.setOnClickListener(this);
235         mActionButtonTranslationZ = mActionButtonView.getTranslationZ();
236     }
237 
238     /**
239      * Update the task view when the configuration changes.
240      */
onConfigurationChanged()241     protected void onConfigurationChanged() {
242         mHeaderView.onConfigurationChanged();
243     }
244 
245     @Override
onSizeChanged(int w, int h, int oldw, int oldh)246     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
247         super.onSizeChanged(w, h, oldw, oldh);
248         if (w > 0 && h > 0) {
249             mHeaderView.onTaskViewSizeChanged(w, h);
250             mThumbnailView.onTaskViewSizeChanged(w, h);
251 
252             mActionButtonView.setTranslationX(w - getMeasuredWidth());
253             mActionButtonView.setTranslationY(h - getMeasuredHeight());
254         }
255     }
256 
257     @Override
hasOverlappingRendering()258     public boolean hasOverlappingRendering() {
259         return false;
260     }
261 
262     @Override
onInterceptTouchEvent(MotionEvent ev)263     public boolean onInterceptTouchEvent(MotionEvent ev) {
264         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
265             mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY()));
266         }
267         return super.onInterceptTouchEvent(ev);
268     }
269 
270     @Override
measureContents(int width, int height)271     protected void measureContents(int width, int height) {
272         int widthWithoutPadding = width - mPaddingLeft - mPaddingRight;
273         int heightWithoutPadding = height - mPaddingTop - mPaddingBottom;
274         int widthSpec = MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY);
275         int heightSpec = MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY);
276 
277         // Measure the content
278         measureChildren(widthSpec, heightSpec);
279 
280         setMeasuredDimension(width, height);
281     }
282 
updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback)283     void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform,
284             AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
285         RecentsConfiguration config = LegacyRecentsImpl.getConfiguration();
286         cancelTransformAnimation();
287 
288         // Compose the animations for the transform
289         mTmpAnimators.clear();
290         toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows);
291         if (toAnimation.isImmediate()) {
292             if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) {
293                 setDimAlpha(toTransform.dimAlpha);
294             }
295             if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) {
296                 mViewBounds.setAlpha(toTransform.viewOutlineAlpha);
297             }
298             // Manually call back to the animator listener and update callback
299             if (toAnimation.getListener() != null) {
300                 toAnimation.getListener().onAnimationEnd(null);
301             }
302             if (updateCallback != null) {
303                 updateCallback.onAnimationUpdate(null);
304             }
305         } else {
306             // Both the progress and the update are a function of the bounds movement of the task
307             if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) {
308                 mDimAnimator = ObjectAnimator.ofFloat(this, DIM_ALPHA, getDimAlpha(),
309                         toTransform.dimAlpha);
310                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mDimAnimator));
311             }
312             if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) {
313                 mOutlineAnimator = ObjectAnimator.ofFloat(this, VIEW_OUTLINE_ALPHA,
314                         mViewBounds.getAlpha(), toTransform.viewOutlineAlpha);
315                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mOutlineAnimator));
316             }
317             if (updateCallback != null) {
318                 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
319                 updateCallbackAnim.addUpdateListener(updateCallback);
320                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, updateCallbackAnim));
321             }
322 
323             // Create the animator
324             mTransformAnimation = toAnimation.createAnimator(mTmpAnimators);
325             mTransformAnimation.start();
326             mTargetAnimationTransform.copyFrom(toTransform);
327         }
328     }
329 
330     /** Resets this view's properties */
resetViewProperties()331     void resetViewProperties() {
332         cancelTransformAnimation();
333         setDimAlpha(0);
334         setVisibility(View.VISIBLE);
335         getViewBounds().reset();
336         getHeaderView().reset();
337         TaskViewTransform.reset(this);
338 
339         mActionButtonView.setScaleX(1f);
340         mActionButtonView.setScaleY(1f);
341         mActionButtonView.setAlpha(0f);
342         mActionButtonView.setTranslationX(0f);
343         mActionButtonView.setTranslationY(0f);
344         mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
345         if (mIncompatibleAppToastView != null) {
346             mIncompatibleAppToastView.setVisibility(View.INVISIBLE);
347         }
348     }
349 
350     /**
351      * @return whether we are animating towards {@param transform}
352      */
isAnimatingTo(TaskViewTransform transform)353     boolean isAnimatingTo(TaskViewTransform transform) {
354         return mTransformAnimation != null && mTransformAnimation.isStarted()
355                 && mTargetAnimationTransform.isSame(transform);
356     }
357 
358     /**
359      * Cancels any current transform animations.
360      */
cancelTransformAnimation()361     public void cancelTransformAnimation() {
362         cancelDimAnimationIfExists();
363         Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
364         Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator);
365     }
366 
cancelDimAnimationIfExists()367     private void cancelDimAnimationIfExists() {
368         if (mDimAnimator != null) {
369             mDimAnimator.cancel();
370         }
371     }
372 
373     /** Enables/disables handling touch on this task view. */
setTouchEnabled(boolean enabled)374     public void setTouchEnabled(boolean enabled) {
375         setOnClickListener(enabled ? this : null);
376     }
377 
378     /** Animates this task view if the user does not interact with the stack after a certain time. */
startNoUserInteractionAnimation()379     public void startNoUserInteractionAnimation() {
380         mHeaderView.startNoUserInteractionAnimation();
381     }
382 
383     /** Mark this task view that the user does has not interacted with the stack after a certain time. */
setNoUserInteractionState()384     void setNoUserInteractionState() {
385         mHeaderView.setNoUserInteractionState();
386     }
387 
388     /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
resetNoUserInteractionState()389     void resetNoUserInteractionState() {
390         mHeaderView.resetNoUserInteractionState();
391     }
392 
393     /** Dismisses this task. */
dismissTask()394     void dismissTask() {
395         // Animate out the view and call the callback
396         final TaskView tv = this;
397         DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv);
398         dismissEvent.addPostAnimationCallback(new Runnable() {
399             @Override
400             public void run() {
401                 EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv,
402                         new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION,
403                                 Interpolators.FAST_OUT_SLOW_IN)));
404             }
405         });
406         EventBus.getDefault().send(dismissEvent);
407     }
408 
409     /**
410      * Returns whether this view should be clipped, or any views below should clip against this
411      * view.
412      */
shouldClipViewInStack()413     boolean shouldClipViewInStack() {
414         if (getVisibility() != View.VISIBLE || LegacyRecentsImpl.getConfiguration().isLowRamDevice) {
415             return false;
416         }
417         return mClipViewInStack;
418     }
419 
420     /** Sets whether this view should be clipped, or clipped against. */
setClipViewInStack(boolean clip)421     void setClipViewInStack(boolean clip) {
422         if (clip != mClipViewInStack) {
423             mClipViewInStack = clip;
424             if (mCb != null) {
425                 mCb.onTaskViewClipStateChanged(this);
426             }
427         }
428     }
429 
getHeaderView()430     public TaskViewHeader getHeaderView() {
431         return mHeaderView;
432     }
433 
434     /**
435      * Sets the current dim.
436      */
setDimAlpha(float dimAlpha)437     public void setDimAlpha(float dimAlpha) {
438         mDimAlpha = dimAlpha;
439         mThumbnailView.setDimAlpha(dimAlpha);
440         mHeaderView.setDimAlpha(dimAlpha);
441     }
442 
443     /**
444      * Sets the current dim without updating the header's dim.
445      */
setDimAlphaWithoutHeader(float dimAlpha)446     public void setDimAlphaWithoutHeader(float dimAlpha) {
447         mDimAlpha = dimAlpha;
448         mThumbnailView.setDimAlpha(dimAlpha);
449     }
450 
451     /**
452      * Returns the current dim.
453      */
getDimAlpha()454     public float getDimAlpha() {
455         return mDimAlpha;
456     }
457 
458     /**
459      * Explicitly sets the focused state of this task.
460      */
setFocusedState(boolean isFocused, boolean requestViewFocus)461     public void setFocusedState(boolean isFocused, boolean requestViewFocus) {
462         if (isFocused) {
463             if (requestViewFocus && !isFocused()) {
464                 requestFocus();
465             }
466         } else {
467             if (isAccessibilityFocused() && mTouchExplorationEnabled) {
468                 clearAccessibilityFocus();
469             }
470         }
471     }
472 
473     /**
474      * Shows the action button.
475      * @param fadeIn whether or not to animate the action button in.
476      * @param fadeInDuration the duration of the action button animation, only used if
477      *                       {@param fadeIn} is true.
478      */
showActionButton(boolean fadeIn, int fadeInDuration)479     public void showActionButton(boolean fadeIn, int fadeInDuration) {
480         mActionButtonView.setVisibility(View.VISIBLE);
481 
482         if (fadeIn && mActionButtonView.getAlpha() < 1f) {
483             mActionButtonView.animate()
484                     .alpha(1f)
485                     .scaleX(1f)
486                     .scaleY(1f)
487                     .setDuration(fadeInDuration)
488                     .setInterpolator(Interpolators.ALPHA_IN)
489                     .start();
490         } else {
491             mActionButtonView.setScaleX(1f);
492             mActionButtonView.setScaleY(1f);
493             mActionButtonView.setAlpha(1f);
494             mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
495         }
496     }
497 
498     /**
499      * Immediately hides the action button.
500      *
501      * @param fadeOut whether or not to animate the action button out.
502      */
hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, final Animator.AnimatorListener animListener)503     public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
504             final Animator.AnimatorListener animListener) {
505         if (fadeOut && mActionButtonView.getAlpha() > 0f) {
506             if (scaleDown) {
507                 float toScale = 0.9f;
508                 mActionButtonView.animate()
509                         .scaleX(toScale)
510                         .scaleY(toScale);
511             }
512             mActionButtonView.animate()
513                     .alpha(0f)
514                     .setDuration(fadeOutDuration)
515                     .setInterpolator(Interpolators.ALPHA_OUT)
516                     .withEndAction(new Runnable() {
517                         @Override
518                         public void run() {
519                             if (animListener != null) {
520                                 animListener.onAnimationEnd(null);
521                             }
522                             mActionButtonView.setVisibility(View.INVISIBLE);
523                         }
524                     })
525                     .start();
526         } else {
527             mActionButtonView.setAlpha(0f);
528             mActionButtonView.setVisibility(View.INVISIBLE);
529             if (animListener != null) {
530                 animListener.onAnimationEnd(null);
531             }
532         }
533     }
534 
535     /**** TaskStackAnimationHelper.Callbacks Implementation ****/
536 
537     @Override
onPrepareLaunchTargetForEnterAnimation()538     public void onPrepareLaunchTargetForEnterAnimation() {
539         // These values will be animated in when onStartLaunchTargetEnterAnimation() is called
540         setDimAlphaWithoutHeader(0);
541         mActionButtonView.setAlpha(0f);
542         if (mIncompatibleAppToastView != null &&
543                 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) {
544             mIncompatibleAppToastView.setAlpha(0f);
545         }
546     }
547 
548     @Override
onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)549     public void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration,
550             boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger) {
551         cancelDimAnimationIfExists();
552 
553         // Dim the view after the app window transitions down into recents
554         postAnimationTrigger.increment();
555         AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT);
556         mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this,
557                 DIM_ALPHA_WITHOUT_HEADER, getDimAlpha(), transform.dimAlpha));
558         mDimAnimator.addListener(postAnimationTrigger.decrementOnAnimationEnd());
559         mDimAnimator.start();
560 
561         if (screenPinningEnabled) {
562             showActionButton(true /* fadeIn */, duration /* fadeInDuration */);
563         }
564 
565         if (mIncompatibleAppToastView != null &&
566                 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) {
567             mIncompatibleAppToastView.animate()
568                     .alpha(1f)
569                     .setDuration(duration)
570                     .setInterpolator(Interpolators.ALPHA_IN)
571                     .start();
572         }
573     }
574 
575     @Override
onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)576     public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
577             ReferenceCountedTrigger postAnimationTrigger) {
578         Utilities.cancelAnimationWithoutCallbacks(mDimAnimator);
579 
580         // Un-dim the view before/while launching the target
581         AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT);
582         mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this,
583                 DIM_ALPHA, getDimAlpha(), 0));
584         mDimAnimator.start();
585 
586         postAnimationTrigger.increment();
587         hideActionButton(true /* fadeOut */, duration,
588                 !screenPinningRequested /* scaleDown */,
589                 postAnimationTrigger.decrementOnAnimationEnd());
590     }
591 
592     @Override
onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)593     public void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled) {
594         if (screenPinningEnabled) {
595             showActionButton(false /* fadeIn */, 0 /* fadeInDuration */);
596         }
597     }
598 
599     /**** TaskCallbacks Implementation ****/
600 
onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, Rect displayRect)601     public void onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation,
602             Rect displayRect) {
603         SystemServicesProxy ssp = LegacyRecentsImpl.getSystemServices();
604         mTouchExplorationEnabled = touchExplorationEnabled;
605         mTask = t;
606         mTaskBound = true;
607         mTask.addCallback(this);
608         mIsDisabledInSafeMode = !mTask.isSystemApp && ssp.isInSafeMode();
609         mThumbnailView.bindToTask(mTask, mIsDisabledInSafeMode, displayOrientation, displayRect);
610         mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode);
611 
612         if (!t.isDockable && ssp.hasDockedTask()) {
613             if (mIncompatibleAppToastView == null) {
614                 mIncompatibleAppToastView = Utilities.findViewStubById(this,
615                         R.id.incompatible_app_toast_stub).inflate();
616                 TextView msg = findViewById(com.android.internal.R.id.message);
617                 msg.setText(R.string.dock_non_resizeble_failed_to_dock_text);
618             }
619             mIncompatibleAppToastView.setVisibility(View.VISIBLE);
620         } else if (mIncompatibleAppToastView != null) {
621             mIncompatibleAppToastView.setVisibility(View.INVISIBLE);
622         }
623     }
624 
625     @Override
onTaskDataLoaded(Task task, ThumbnailData thumbnailData)626     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
627         if (mTaskBound) {
628             // Update each of the views to the new task data
629             mThumbnailView.onTaskDataLoaded(thumbnailData);
630             mHeaderView.onTaskDataLoaded();
631         }
632     }
633 
634     @Override
onTaskDataUnloaded()635     public void onTaskDataUnloaded() {
636         // Unbind each of the views from the task and remove the task callback
637         mTask.removeCallback(this);
638         mThumbnailView.unbindFromTask();
639         mHeaderView.unbindFromTask(mTouchExplorationEnabled);
640         mTaskBound = false;
641     }
642 
643     @Override
onTaskWindowingModeChanged()644     public void onTaskWindowingModeChanged() {
645         // Force rebind the header, the thumbnail does not change due to stack changes
646         mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode);
647         mHeaderView.onTaskDataLoaded();
648     }
649 
650     /**** View.OnClickListener Implementation ****/
651 
652     @Override
onClick(final View v)653      public void onClick(final View v) {
654         if (mIsDisabledInSafeMode) {
655             Context context = getContext();
656             String msg = context.getString(R.string.recents_launch_disabled_message, mTask.title);
657             if (mDisabledAppToast != null) {
658                 mDisabledAppToast.cancel();
659             }
660             mDisabledAppToast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
661             mDisabledAppToast.show();
662             return;
663         }
664 
665         boolean screenPinningRequested = false;
666         if (v == mActionButtonView) {
667             // Reset the translation of the action button before we animate it out
668             mActionButtonView.setTranslationZ(0f);
669             screenPinningRequested = true;
670         }
671         EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, screenPinningRequested));
672 
673         MetricsLogger.action(v.getContext(), MetricsEvent.ACTION_OVERVIEW_SELECT,
674                 mTask.key.getComponent().toString());
675     }
676 
677     /**** View.OnLongClickListener Implementation ****/
678 
679     @Override
onLongClick(View v)680     public boolean onLongClick(View v) {
681         if (!LegacyRecentsImpl.getConfiguration().dragToSplitEnabled) {
682             return false;
683         }
684         SystemServicesProxy ssp = LegacyRecentsImpl.getSystemServices();
685         boolean inBounds = false;
686         Rect clipBounds = new Rect(mViewBounds.getClipBounds());
687         if (!clipBounds.isEmpty()) {
688             // If we are clipping the view to the bounds, manually do the hit test.
689             clipBounds.scale(getScaleX());
690             inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
691         } else {
692             // Otherwise just make sure we're within the view's bounds.
693             inBounds = mDownTouchPos.x <= getWidth() && mDownTouchPos.y <= getHeight();
694         }
695         if (v == this && inBounds && !ssp.hasDockedTask()) {
696             // Start listening for drag events
697             setClipViewInStack(false);
698 
699             mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
700             mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
701 
702             EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
703             EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos));
704             return true;
705         }
706         return false;
707     }
708 
709     /**** Events ****/
710 
onBusEvent(DragEndEvent event)711     public final void onBusEvent(DragEndEvent event) {
712         if (!(event.dropTarget instanceof DockState)) {
713             event.addPostAnimationCallback(() -> {
714                 // Reset the clip state for the drag view after the end animation completes
715                 setClipViewInStack(true);
716             });
717         }
718         EventBus.getDefault().unregister(this);
719     }
720 
onBusEvent(DragEndCancelledEvent event)721     public final void onBusEvent(DragEndCancelledEvent event) {
722         // Reset the clip state for the drag view after the cancel animation completes
723         event.addPostAnimationCallback(() -> {
724             setClipViewInStack(true);
725         });
726     }
727 
dump(String prefix, PrintWriter writer)728     public void dump(String prefix, PrintWriter writer) {
729         String innerPrefix = prefix + "  ";
730 
731         writer.print(prefix); writer.print("TaskView");
732         writer.print(" mTask="); writer.print(mTask.key.id);
733         writer.println();
734 
735         mThumbnailView.dump(innerPrefix, writer);
736     }
737 }
738