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