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