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