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 static android.app.ActivityManager.StackId.INVALID_STACK_ID;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.animation.ValueAnimator;
25 import android.app.ActivityManager;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.Outline;
29 import android.graphics.Point;
30 import android.graphics.Rect;
31 import android.util.AttributeSet;
32 import android.util.FloatProperty;
33 import android.util.Property;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewDebug;
37 import android.view.ViewOutlineProvider;
38 import android.widget.TextView;
39 import android.widget.Toast;
40 
41 import com.android.internal.logging.MetricsLogger;
42 import com.android.internal.logging.MetricsProto.MetricsEvent;
43 import com.android.systemui.Interpolators;
44 import com.android.systemui.R;
45 import com.android.systemui.recents.Recents;
46 import com.android.systemui.recents.RecentsActivity;
47 import com.android.systemui.recents.RecentsConfiguration;
48 import com.android.systemui.recents.events.EventBus;
49 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
50 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
51 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
52 import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent;
53 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
54 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
55 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
56 import com.android.systemui.recents.misc.SystemServicesProxy;
57 import com.android.systemui.recents.misc.Utilities;
58 import com.android.systemui.recents.model.Task;
59 import com.android.systemui.recents.model.TaskStack;
60 
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     @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     TaskViewThumbnail mThumbnailView;
152     @ViewDebug.ExportedProperty(deepExport=true, prefix="header_")
153     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 = Recents.getConfiguration();
178         Resources res = context.getResources();
179         mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize(
180                 R.dimen.recents_task_view_shadow_rounded_corners_radius));
181         if (config.fakeShadows) {
182             setBackground(new FakeShadowDrawable(res, config));
183         }
184         setOutlineProvider(mViewBounds);
185         setOnLongClickListener(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     /** Returns the view bounds. */
getViewBounds()209     AnimateableViewBounds getViewBounds() {
210         return mViewBounds;
211     }
212 
213     @Override
onFinishInflate()214     protected void onFinishInflate() {
215         // Bind the views
216         mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar);
217         mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail);
218         mThumbnailView.updateClipToTaskBar(mHeaderView);
219         mActionButtonView = findViewById(R.id.lock_to_app_fab);
220         mActionButtonView.setOutlineProvider(new ViewOutlineProvider() {
221             @Override
222             public void getOutline(View view, Outline outline) {
223                 // Set the outline to match the FAB background
224                 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight());
225                 outline.setAlpha(0.35f);
226             }
227         });
228         mActionButtonView.setOnClickListener(this);
229         mActionButtonTranslationZ = mActionButtonView.getTranslationZ();
230     }
231 
232     /**
233      * Update the task view when the configuration changes.
234      */
onConfigurationChanged()235     void onConfigurationChanged() {
236         mHeaderView.onConfigurationChanged();
237     }
238 
239     @Override
onSizeChanged(int w, int h, int oldw, int oldh)240     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
241         super.onSizeChanged(w, h, oldw, oldh);
242         if (w > 0 && h > 0) {
243             mHeaderView.onTaskViewSizeChanged(w, h);
244             mThumbnailView.onTaskViewSizeChanged(w, h);
245 
246             mActionButtonView.setTranslationX(w - getMeasuredWidth());
247             mActionButtonView.setTranslationY(h - getMeasuredHeight());
248         }
249     }
250 
251     @Override
hasOverlappingRendering()252     public boolean hasOverlappingRendering() {
253         return false;
254     }
255 
256     @Override
onInterceptTouchEvent(MotionEvent ev)257     public boolean onInterceptTouchEvent(MotionEvent ev) {
258         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
259             mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY()));
260         }
261         return super.onInterceptTouchEvent(ev);
262     }
263 
264 
265     @Override
measureContents(int width, int height)266     protected void measureContents(int width, int height) {
267         int widthWithoutPadding = width - mPaddingLeft - mPaddingRight;
268         int heightWithoutPadding = height - mPaddingTop - mPaddingBottom;
269         int widthSpec = MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY);
270         int heightSpec = MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY);
271 
272         // Measure the content
273         measureChildren(widthSpec, heightSpec);
274 
275         setMeasuredDimension(width, height);
276     }
277 
updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback)278     void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform,
279             AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
280         RecentsConfiguration config = Recents.getConfiguration();
281         cancelTransformAnimation();
282 
283         // Compose the animations for the transform
284         mTmpAnimators.clear();
285         toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows);
286         if (toAnimation.isImmediate()) {
287             if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) {
288                 setDimAlpha(toTransform.dimAlpha);
289             }
290             if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) {
291                 mViewBounds.setAlpha(toTransform.viewOutlineAlpha);
292             }
293             // Manually call back to the animator listener and update callback
294             if (toAnimation.getListener() != null) {
295                 toAnimation.getListener().onAnimationEnd(null);
296             }
297             if (updateCallback != null) {
298                 updateCallback.onAnimationUpdate(null);
299             }
300         } else {
301             // Both the progress and the update are a function of the bounds movement of the task
302             if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) {
303                 mDimAnimator = ObjectAnimator.ofFloat(this, DIM_ALPHA, getDimAlpha(),
304                         toTransform.dimAlpha);
305                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mDimAnimator));
306             }
307             if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) {
308                 mOutlineAnimator = ObjectAnimator.ofFloat(this, VIEW_OUTLINE_ALPHA,
309                         mViewBounds.getAlpha(), toTransform.viewOutlineAlpha);
310                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mOutlineAnimator));
311             }
312             if (updateCallback != null) {
313                 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
314                 updateCallbackAnim.addUpdateListener(updateCallback);
315                 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, updateCallbackAnim));
316             }
317 
318             // Create the animator
319             mTransformAnimation = toAnimation.createAnimator(mTmpAnimators);
320             mTransformAnimation.start();
321             mTargetAnimationTransform.copyFrom(toTransform);
322         }
323     }
324 
325     /** Resets this view's properties */
resetViewProperties()326     void resetViewProperties() {
327         cancelTransformAnimation();
328         setDimAlpha(0);
329         setVisibility(View.VISIBLE);
330         getViewBounds().reset();
331         getHeaderView().reset();
332         TaskViewTransform.reset(this);
333 
334         mActionButtonView.setScaleX(1f);
335         mActionButtonView.setScaleY(1f);
336         mActionButtonView.setAlpha(0f);
337         mActionButtonView.setTranslationX(0f);
338         mActionButtonView.setTranslationY(0f);
339         mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
340         if (mIncompatibleAppToastView != null) {
341             mIncompatibleAppToastView.setVisibility(View.INVISIBLE);
342         }
343     }
344 
345     /**
346      * @return whether we are animating towards {@param transform}
347      */
isAnimatingTo(TaskViewTransform transform)348     boolean isAnimatingTo(TaskViewTransform transform) {
349         return mTransformAnimation != null && mTransformAnimation.isStarted()
350                 && mTargetAnimationTransform.isSame(transform);
351     }
352 
353     /**
354      * Cancels any current transform animations.
355      */
cancelTransformAnimation()356     public void cancelTransformAnimation() {
357         Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
358         Utilities.cancelAnimationWithoutCallbacks(mDimAnimator);
359         Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator);
360     }
361 
362     /** Enables/disables handling touch on this task view. */
setTouchEnabled(boolean enabled)363     void setTouchEnabled(boolean enabled) {
364         setOnClickListener(enabled ? this : null);
365     }
366 
367     /** Animates this task view if the user does not interact with the stack after a certain time. */
startNoUserInteractionAnimation()368     void startNoUserInteractionAnimation() {
369         mHeaderView.startNoUserInteractionAnimation();
370     }
371 
372     /** Mark this task view that the user does has not interacted with the stack after a certain time. */
setNoUserInteractionState()373     void setNoUserInteractionState() {
374         mHeaderView.setNoUserInteractionState();
375     }
376 
377     /** Resets the state tracking that the user has not interacted with the stack after a certain time. */
resetNoUserInteractionState()378     void resetNoUserInteractionState() {
379         mHeaderView.resetNoUserInteractionState();
380     }
381 
382     /** Dismisses this task. */
dismissTask()383     void dismissTask() {
384         // Animate out the view and call the callback
385         final TaskView tv = this;
386         DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv);
387         dismissEvent.addPostAnimationCallback(new Runnable() {
388             @Override
389             public void run() {
390                 EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv,
391                         new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION,
392                                 Interpolators.FAST_OUT_SLOW_IN)));
393             }
394         });
395         EventBus.getDefault().send(dismissEvent);
396     }
397 
398     /**
399      * Returns whether this view should be clipped, or any views below should clip against this
400      * view.
401      */
shouldClipViewInStack()402     boolean shouldClipViewInStack() {
403         // Never clip for freeform tasks or if invisible
404         if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) {
405             return false;
406         }
407         return mClipViewInStack;
408     }
409 
410     /** Sets whether this view should be clipped, or clipped against. */
setClipViewInStack(boolean clip)411     void setClipViewInStack(boolean clip) {
412         if (clip != mClipViewInStack) {
413             mClipViewInStack = clip;
414             if (mCb != null) {
415                 mCb.onTaskViewClipStateChanged(this);
416             }
417         }
418     }
419 
getHeaderView()420     public TaskViewHeader getHeaderView() {
421         return mHeaderView;
422     }
423 
424     /**
425      * Sets the current dim.
426      */
setDimAlpha(float dimAlpha)427     public void setDimAlpha(float dimAlpha) {
428         mDimAlpha = dimAlpha;
429         mThumbnailView.setDimAlpha(dimAlpha);
430         mHeaderView.setDimAlpha(dimAlpha);
431     }
432 
433     /**
434      * Sets the current dim without updating the header's dim.
435      */
setDimAlphaWithoutHeader(float dimAlpha)436     public void setDimAlphaWithoutHeader(float dimAlpha) {
437         mDimAlpha = dimAlpha;
438         mThumbnailView.setDimAlpha(dimAlpha);
439     }
440 
441     /**
442      * Returns the current dim.
443      */
getDimAlpha()444     public float getDimAlpha() {
445         return mDimAlpha;
446     }
447 
448     /**
449      * Explicitly sets the focused state of this task.
450      */
setFocusedState(boolean isFocused, boolean requestViewFocus)451     public void setFocusedState(boolean isFocused, boolean requestViewFocus) {
452         if (isFocused) {
453             if (requestViewFocus && !isFocused()) {
454                 requestFocus();
455             }
456         } else {
457             if (isAccessibilityFocused() && mTouchExplorationEnabled) {
458                 clearAccessibilityFocus();
459             }
460         }
461     }
462 
463     /**
464      * Shows the action button.
465      * @param fadeIn whether or not to animate the action button in.
466      * @param fadeInDuration the duration of the action button animation, only used if
467      *                       {@param fadeIn} is true.
468      */
showActionButton(boolean fadeIn, int fadeInDuration)469     public void showActionButton(boolean fadeIn, int fadeInDuration) {
470         mActionButtonView.setVisibility(View.VISIBLE);
471 
472         if (fadeIn && mActionButtonView.getAlpha() < 1f) {
473             mActionButtonView.animate()
474                     .alpha(1f)
475                     .scaleX(1f)
476                     .scaleY(1f)
477                     .setDuration(fadeInDuration)
478                     .setInterpolator(Interpolators.ALPHA_IN)
479                     .start();
480         } else {
481             mActionButtonView.setScaleX(1f);
482             mActionButtonView.setScaleY(1f);
483             mActionButtonView.setAlpha(1f);
484             mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
485         }
486     }
487 
488     /**
489      * Immediately hides the action button.
490      *
491      * @param fadeOut whether or not to animate the action button out.
492      */
hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, final Animator.AnimatorListener animListener)493     public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
494             final Animator.AnimatorListener animListener) {
495         if (fadeOut && mActionButtonView.getAlpha() > 0f) {
496             if (scaleDown) {
497                 float toScale = 0.9f;
498                 mActionButtonView.animate()
499                         .scaleX(toScale)
500                         .scaleY(toScale);
501             }
502             mActionButtonView.animate()
503                     .alpha(0f)
504                     .setDuration(fadeOutDuration)
505                     .setInterpolator(Interpolators.ALPHA_OUT)
506                     .withEndAction(new Runnable() {
507                         @Override
508                         public void run() {
509                             if (animListener != null) {
510                                 animListener.onAnimationEnd(null);
511                             }
512                             mActionButtonView.setVisibility(View.INVISIBLE);
513                         }
514                     })
515                     .start();
516         } else {
517             mActionButtonView.setAlpha(0f);
518             mActionButtonView.setVisibility(View.INVISIBLE);
519             if (animListener != null) {
520                 animListener.onAnimationEnd(null);
521             }
522         }
523     }
524 
525     /**** TaskStackAnimationHelper.Callbacks Implementation ****/
526 
527     @Override
onPrepareLaunchTargetForEnterAnimation()528     public void onPrepareLaunchTargetForEnterAnimation() {
529         // These values will be animated in when onStartLaunchTargetEnterAnimation() is called
530         setDimAlphaWithoutHeader(0);
531         mActionButtonView.setAlpha(0f);
532         if (mIncompatibleAppToastView != null &&
533                 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) {
534             mIncompatibleAppToastView.setAlpha(0f);
535         }
536     }
537 
538     @Override
onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)539     public void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration,
540             boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger) {
541         Utilities.cancelAnimationWithoutCallbacks(mDimAnimator);
542 
543         // Dim the view after the app window transitions down into recents
544         postAnimationTrigger.increment();
545         AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT);
546         mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this,
547                 DIM_ALPHA_WITHOUT_HEADER, getDimAlpha(), transform.dimAlpha));
548         mDimAnimator.addListener(postAnimationTrigger.decrementOnAnimationEnd());
549         mDimAnimator.start();
550 
551         if (screenPinningEnabled) {
552             showActionButton(true /* fadeIn */, duration /* fadeInDuration */);
553         }
554 
555         if (mIncompatibleAppToastView != null &&
556                 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) {
557             mIncompatibleAppToastView.animate()
558                     .alpha(1f)
559                     .setDuration(duration)
560                     .setInterpolator(Interpolators.ALPHA_IN)
561                     .start();
562         }
563     }
564 
565     @Override
onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)566     public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
567             ReferenceCountedTrigger postAnimationTrigger) {
568         Utilities.cancelAnimationWithoutCallbacks(mDimAnimator);
569 
570         // Un-dim the view before/while launching the target
571         AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT);
572         mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this,
573                 DIM_ALPHA, getDimAlpha(), 0));
574         mDimAnimator.start();
575 
576         postAnimationTrigger.increment();
577         hideActionButton(true /* fadeOut */, duration,
578                 !screenPinningRequested /* scaleDown */,
579                 postAnimationTrigger.decrementOnAnimationEnd());
580     }
581 
582     @Override
onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)583     public void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled) {
584         if (screenPinningEnabled) {
585             showActionButton(false /* fadeIn */, 0 /* fadeInDuration */);
586         }
587     }
588 
589     /**** TaskCallbacks Implementation ****/
590 
onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, Rect displayRect)591     public void onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation,
592             Rect displayRect) {
593         SystemServicesProxy ssp = Recents.getSystemServices();
594         mTouchExplorationEnabled = touchExplorationEnabled;
595         mTask = t;
596         mTask.addCallback(this);
597         mIsDisabledInSafeMode = !mTask.isSystemApp && ssp.isInSafeMode();
598         mThumbnailView.bindToTask(mTask, mIsDisabledInSafeMode, displayOrientation, displayRect);
599         mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode);
600 
601         if (!t.isDockable && ssp.hasDockedTask()) {
602             if (mIncompatibleAppToastView == null) {
603                 mIncompatibleAppToastView = Utilities.findViewStubById(this,
604                         R.id.incompatible_app_toast_stub).inflate();
605                 TextView msg = (TextView) findViewById(com.android.internal.R.id.message);
606                 msg.setText(R.string.recents_incompatible_app_message);
607             }
608             mIncompatibleAppToastView.setVisibility(View.VISIBLE);
609         } else if (mIncompatibleAppToastView != null) {
610             mIncompatibleAppToastView.setVisibility(View.INVISIBLE);
611         }
612     }
613 
614     @Override
onTaskDataLoaded(Task task, ActivityManager.TaskThumbnailInfo thumbnailInfo)615     public void onTaskDataLoaded(Task task, ActivityManager.TaskThumbnailInfo thumbnailInfo) {
616         // Update each of the views to the new task data
617         mThumbnailView.onTaskDataLoaded(thumbnailInfo);
618         mHeaderView.onTaskDataLoaded();
619     }
620 
621     @Override
onTaskDataUnloaded()622     public void onTaskDataUnloaded() {
623         // Unbind each of the views from the task and remove the task callback
624         mTask.removeCallback(this);
625         mThumbnailView.unbindFromTask();
626         mHeaderView.unbindFromTask(mTouchExplorationEnabled);
627     }
628 
629     @Override
onTaskStackIdChanged()630     public void onTaskStackIdChanged() {
631         // Force rebind the header, the thumbnail does not change due to stack changes
632         mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode);
633         mHeaderView.onTaskDataLoaded();
634     }
635 
636     /**** View.OnClickListener Implementation ****/
637 
638     @Override
onClick(final View v)639      public void onClick(final View v) {
640         if (mIsDisabledInSafeMode) {
641             Context context = getContext();
642             String msg = context.getString(R.string.recents_launch_disabled_message, mTask.title);
643             if (mDisabledAppToast != null) {
644                 mDisabledAppToast.cancel();
645             }
646             mDisabledAppToast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
647             mDisabledAppToast.show();
648             return;
649         }
650 
651         boolean screenPinningRequested = false;
652         if (v == mActionButtonView) {
653             // Reset the translation of the action button before we animate it out
654             mActionButtonView.setTranslationZ(0f);
655             screenPinningRequested = true;
656         }
657         EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID,
658                 screenPinningRequested));
659 
660         MetricsLogger.action(v.getContext(), MetricsEvent.ACTION_OVERVIEW_SELECT,
661                 mTask.key.getComponent().toString());
662     }
663 
664     /**** View.OnLongClickListener Implementation ****/
665 
666     @Override
onLongClick(View v)667     public boolean onLongClick(View v) {
668         SystemServicesProxy ssp = Recents.getSystemServices();
669         // Since we are clipping the view to the bounds, manually do the hit test
670         Rect clipBounds = new Rect(mViewBounds.mClipBounds);
671         clipBounds.scale(getScaleX());
672         boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y);
673         if (v == this && inBounds && !ssp.hasDockedTask()) {
674             // Start listening for drag events
675             setClipViewInStack(false);
676 
677             mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
678             mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
679 
680             EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1);
681             EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos));
682             return true;
683         }
684         return false;
685     }
686 
687     /**** Events ****/
688 
onBusEvent(DragEndEvent event)689     public final void onBusEvent(DragEndEvent event) {
690         if (!(event.dropTarget instanceof TaskStack.DockState)) {
691             event.addPostAnimationCallback(() -> {
692                 // Reset the clip state for the drag view after the end animation completes
693                 setClipViewInStack(true);
694             });
695         }
696         EventBus.getDefault().unregister(this);
697     }
698 
onBusEvent(DragEndCancelledEvent event)699     public final void onBusEvent(DragEndCancelledEvent event) {
700         // Reset the clip state for the drag view after the cancel animation completes
701         event.addPostAnimationCallback(() -> {
702             setClipViewInStack(true);
703         });
704     }
705 }
706