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