Home | History | Annotate | Download | only in views
      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 {
     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 
    165     public TaskView(Context context) {
    166         this(context, null);
    167     }
    168 
    169     public TaskView(Context context, AttributeSet attrs) {
    170         this(context, attrs, 0);
    171     }
    172 
    173     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
    174         this(context, attrs, defStyleAttr, 0);
    175     }
    176 
    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 */
    191     void setCallbacks(TaskViewCallbacks cb) {
    192         mCb = cb;
    193     }
    194 
    195     /**
    196      * Called from RecentsActivity when it is relaunched.
    197      */
    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 */
    208     public Task getTask() {
    209         return mTask;
    210     }
    211 
    212     /* Create an outline provider to clip and outline the view */
    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. */
    219     AnimateableViewBounds getViewBounds() {
    220         return mViewBounds;
    221     }
    222 
    223     @Override
    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      */
    245     protected void onConfigurationChanged() {
    246         mHeaderView.onConfigurationChanged();
    247     }
    248 
    249     @Override
    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
    262     public boolean hasOverlappingRendering() {
    263         return false;
    264     }
    265 
    266     @Override
    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
    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 
    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 */
    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      */
    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      */
    365     public void cancelTransformAnimation() {
    366         cancelDimAnimationIfExists();
    367         Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation);
    368         Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator);
    369     }
    370 
    371     private void cancelDimAnimationIfExists() {
    372         if (mDimAnimator != null) {
    373             mDimAnimator.cancel();
    374         }
    375     }
    376 
    377     /** Enables/disables handling touch on this task view. */
    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. */
    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. */
    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. */
    393     void resetNoUserInteractionState() {
    394         mHeaderView.resetNoUserInteractionState();
    395     }
    396 
    397     /** Dismisses this task. */
    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      */
    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. */
    426     void setClipViewInStack(boolean clip) {
    427         if (clip != mClipViewInStack) {
    428             mClipViewInStack = clip;
    429             if (mCb != null) {
    430                 mCb.onTaskViewClipStateChanged(this);
    431             }
    432         }
    433     }
    434 
    435     public TaskViewHeader getHeaderView() {
    436         return mHeaderView;
    437     }
    438 
    439     /**
    440      * Sets the current dim.
    441      */
    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      */
    451     public void setDimAlphaWithoutHeader(float dimAlpha) {
    452         mDimAlpha = dimAlpha;
    453         mThumbnailView.setDimAlpha(dimAlpha);
    454     }
    455 
    456     /**
    457      * Returns the current dim.
    458      */
    459     public float getDimAlpha() {
    460         return mDimAlpha;
    461     }
    462 
    463     /**
    464      * Explicitly sets the focused state of this task.
    465      */
    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      */
    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      */
    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
    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
    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
    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
    598     public void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled) {
    599         if (screenPinningEnabled) {
    600             showActionButton(false /* fadeIn */, 0 /* fadeInDuration */);
    601         }
    602     }
    603 
    604     /**** TaskCallbacks Implementation ****/
    605 
    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
    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
    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
    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
    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
    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 
    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 
    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 
    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