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    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