RecentsImpl.java revision 7c3912e4abc71886ad41e7be67d4eff41974d21f
1/*
2 * Copyright (C) 2015 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;
18
19import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
20import static android.view.View.MeasureSpec;
21
22import android.app.ActivityManager;
23import android.app.ActivityOptions;
24import android.content.ActivityNotFoundException;
25import android.content.Context;
26import android.content.Intent;
27import android.content.res.Resources;
28import android.graphics.Bitmap;
29import android.graphics.Canvas;
30import android.graphics.Rect;
31import android.graphics.RectF;
32import android.graphics.drawable.Drawable;
33import android.os.Handler;
34import android.os.SystemClock;
35import android.os.UserHandle;
36import android.util.Log;
37import android.util.MutableBoolean;
38import android.view.AppTransitionAnimationSpec;
39import android.view.LayoutInflater;
40import android.view.ViewConfiguration;
41
42import com.android.internal.logging.MetricsLogger;
43import com.android.systemui.R;
44import com.android.systemui.SystemUIApplication;
45import com.android.systemui.recents.events.EventBus;
46import com.android.systemui.recents.events.activity.DockedTopTaskEvent;
47import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
48import com.android.systemui.recents.events.activity.HideRecentsEvent;
49import com.android.systemui.recents.events.activity.IterateRecentsEvent;
50import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
51import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
52import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
53import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
54import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
55import com.android.systemui.recents.events.ui.DraggingInRecentsEndedEvent;
56import com.android.systemui.recents.events.ui.DraggingInRecentsEvent;
57import com.android.systemui.recents.misc.DozeTrigger;
58import com.android.systemui.recents.misc.ForegroundThread;
59import com.android.systemui.recents.misc.SystemServicesProxy;
60import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
61import com.android.systemui.recents.model.RecentsTaskLoadPlan;
62import com.android.systemui.recents.model.RecentsTaskLoader;
63import com.android.systemui.recents.model.Task;
64import com.android.systemui.recents.model.TaskGrouping;
65import com.android.systemui.recents.model.TaskStack;
66import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
67import com.android.systemui.recents.views.TaskStackView;
68import com.android.systemui.recents.views.TaskStackViewScroller;
69import com.android.systemui.recents.views.TaskViewHeader;
70import com.android.systemui.recents.views.TaskViewTransform;
71import com.android.systemui.statusbar.BaseStatusBar;
72import com.android.systemui.statusbar.phone.NavigationBarGestureHelper;
73import com.android.systemui.statusbar.phone.PhoneStatusBar;
74
75import java.util.ArrayList;
76
77/**
78 * An implementation of the Recents component for the current user.  For secondary users, this can
79 * be called remotely from the system user.
80 */
81public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener {
82
83    private final static String TAG = "RecentsImpl";
84
85    // The minimum amount of time between each recents button press that we will handle
86    private final static int MIN_TOGGLE_DELAY_MS = 350;
87
88    // The duration within which the user releasing the alt tab (from when they pressed alt tab)
89    // that the fast alt-tab animation will run.  If the user's alt-tab takes longer than this
90    // duration, then we will toggle recents after this duration.
91    private final static int FAST_ALT_TAB_DELAY_MS = 225;
92
93    public final static String RECENTS_PACKAGE = "com.android.systemui";
94    public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
95
96    /**
97     * An implementation of TaskStackListener, that allows us to listen for changes to the system
98     * task stacks and update recents accordingly.
99     */
100    class TaskStackListenerImpl extends TaskStackListener {
101        @Override
102        public void onTaskStackChanged() {
103            // Preloads the next task
104            RecentsConfiguration config = Recents.getConfiguration();
105            if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
106                RecentsTaskLoader loader = Recents.getTaskLoader();
107                SystemServicesProxy ssp = Recents.getSystemServices();
108                ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask();
109
110                // Load the next task only if we aren't svelte
111                RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
112                loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
113                RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
114                // This callback is made when a new activity is launched and the old one is paused
115                // so ignore the current activity and try and preload the thumbnail for the
116                // previous one.
117                if (runningTaskInfo != null) {
118                    launchOpts.runningTaskId = runningTaskInfo.id;
119                }
120                launchOpts.numVisibleTasks = 2;
121                launchOpts.numVisibleTaskThumbnails = 2;
122                launchOpts.onlyLoadForCache = true;
123                launchOpts.onlyLoadPausedActivities = true;
124                loader.loadTasks(mContext, plan, launchOpts);
125            }
126        }
127    }
128
129    protected static RecentsTaskLoadPlan sInstanceLoadPlan;
130
131    protected Context mContext;
132    protected Handler mHandler;
133    TaskStackListenerImpl mTaskStackListener;
134    boolean mDraggingInRecents;
135    boolean mLaunchedWhileDocking;
136
137    // Task launching
138    Rect mTaskStackBounds = new Rect();
139    TaskViewTransform mTmpTransform = new TaskViewTransform();
140    int mStatusBarHeight;
141    int mNavBarHeight;
142    int mNavBarWidth;
143    int mTaskBarHeight;
144
145    // Header (for transition)
146    TaskViewHeader mHeaderBar;
147    final Object mHeaderBarLock = new Object();
148    protected TaskStackView mDummyStackView;
149
150    // Variables to keep track of if we need to start recents after binding
151    protected boolean mTriggeredFromAltTab;
152    protected long mLastToggleTime;
153    DozeTrigger mFastAltTabTrigger = new DozeTrigger(FAST_ALT_TAB_DELAY_MS, new Runnable() {
154        @Override
155        public void run() {
156            // When this fires, then the user has not released alt-tab for at least
157            // FAST_ALT_TAB_DELAY_MS milliseconds
158            showRecents(mTriggeredFromAltTab, false /* draggingInRecents */, true /* animate */,
159                    false /* reloadTasks */);
160        }
161    });
162
163    protected Bitmap mThumbTransitionBitmapCache;
164
165    public RecentsImpl(Context context) {
166        mContext = context;
167        mHandler = new Handler();
168
169        // Initialize the static foreground thread
170        ForegroundThread.get();
171
172        // Register the task stack listener
173        mTaskStackListener = new TaskStackListenerImpl();
174        SystemServicesProxy ssp = Recents.getSystemServices();
175        ssp.registerTaskStackListener(mTaskStackListener);
176
177        // Initialize the static configuration resources
178        reloadHeaderBarLayout();
179
180        // When we start, preload the data associated with the previous recent tasks.
181        // We can use a new plan since the caches will be the same.
182        RecentsTaskLoader loader = Recents.getTaskLoader();
183        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
184        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
185        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
186        launchOpts.numVisibleTasks = loader.getIconCacheSize();
187        launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
188        launchOpts.onlyLoadForCache = true;
189        loader.loadTasks(mContext, plan, launchOpts);
190    }
191
192    public void onBootCompleted() {
193        // Do nothing
194    }
195
196    public void onConfigurationChanged() {
197        reloadHeaderBarLayout();
198    }
199
200    /**
201     * This is only called from the system user's Recents.  Secondary users will instead proxy their
202     * visibility change events through to the system user via
203     * {@link Recents#onBusEvent(RecentsVisibilityChangedEvent)}.
204     */
205    public void onVisibilityChanged(Context context, boolean visible) {
206        SystemUIApplication app = (SystemUIApplication) context;
207        PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class);
208        if (statusBar != null) {
209            statusBar.updateRecentsVisibility(visible);
210        }
211    }
212
213    /**
214     * This is only called from the system user's Recents.  Secondary users will instead proxy their
215     * visibility change events through to the system user via
216     * {@link Recents#onBusEvent(ScreenPinningRequestEvent)}.
217     */
218    public void onStartScreenPinning(Context context) {
219        SystemUIApplication app = (SystemUIApplication) context;
220        PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class);
221        if (statusBar != null) {
222            statusBar.showScreenPinningRequest(false);
223        }
224    }
225
226    public void showRecents(boolean triggeredFromAltTab, boolean draggingInRecents,
227            boolean animate, boolean launchedWhileDockingTask) {
228        mTriggeredFromAltTab = triggeredFromAltTab;
229        mDraggingInRecents = draggingInRecents;
230        mLaunchedWhileDocking = launchedWhileDockingTask;
231        if (mFastAltTabTrigger.isAsleep()) {
232            // Fast alt-tab duration has elapsed, fall through to showing Recents and reset
233            mFastAltTabTrigger.stopDozing();
234        } else if (mFastAltTabTrigger.isDozing()) {
235            // Fast alt-tab duration has not elapsed.  If this is triggered by a different
236            // showRecents() call, then ignore that call for now.
237            // TODO: We can not handle quick tabs that happen between the initial showRecents() call
238            //       that started the activity and the activity starting up.  The severity of this
239            //       is inversely proportional to the FAST_ALT_TAB_DELAY_MS duration though.
240            if (!triggeredFromAltTab) {
241                return;
242            }
243            mFastAltTabTrigger.stopDozing();
244        } else if (triggeredFromAltTab) {
245            // The fast alt-tab detector is not yet running, so start the trigger and wait for the
246            // hideRecents() call, or for the fast alt-tab duration to elapse
247            mFastAltTabTrigger.startDozing();
248            return;
249        }
250
251        try {
252            // Check if the top task is in the home stack, and start the recents activity
253            SystemServicesProxy ssp = Recents.getSystemServices();
254            ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
255            MutableBoolean isTopTaskHome = new MutableBoolean(true);
256            if (topTask == null || !ssp.isRecentsTopMost(topTask, isTopTaskHome)) {
257                startRecentsActivity(topTask, isTopTaskHome.value, animate);
258            }
259        } catch (ActivityNotFoundException e) {
260            Log.e(TAG, "Failed to launch RecentsActivity", e);
261        }
262    }
263
264    public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
265        if (triggeredFromAltTab && mFastAltTabTrigger.isDozing()) {
266            // The user has released alt-tab before the trigger has run, so just show the next
267            // task immediately
268            showNextTask();
269
270            // Cancel the fast alt-tab trigger
271            mFastAltTabTrigger.stopDozing();
272            return;
273        }
274
275        // Defer to the activity to handle hiding recents, if it handles it, then it must still
276        // be visible
277        EventBus.getDefault().post(new HideRecentsEvent(triggeredFromAltTab,
278                triggeredFromHomeKey));
279    }
280
281    public void toggleRecents() {
282        // Skip this toggle if we are already waiting to trigger recents via alt-tab
283        if (mFastAltTabTrigger.isDozing()) {
284            return;
285        }
286
287        mDraggingInRecents = false;
288        mLaunchedWhileDocking = false;
289        mTriggeredFromAltTab = false;
290
291        try {
292            SystemServicesProxy ssp = Recents.getSystemServices();
293            ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
294            MutableBoolean isTopTaskHome = new MutableBoolean(true);
295            long elapsedTime = SystemClock.elapsedRealtime() - mLastToggleTime;
296
297            if (topTask != null && ssp.isRecentsTopMost(topTask, isTopTaskHome)) {
298                RecentsDebugFlags debugFlags = Recents.getDebugFlags();
299                RecentsConfiguration config = Recents.getConfiguration();
300                RecentsActivityLaunchState launchState = config.getLaunchState();
301                if (!launchState.launchedWithAltTab) {
302                    // If the user taps quickly
303                    if (!debugFlags.isPagingEnabled() ||
304                            (ViewConfiguration.getDoubleTapMinTime() < elapsedTime &&
305                                    elapsedTime < ViewConfiguration.getDoubleTapTimeout())) {
306                        // Launch the next focused task
307                        EventBus.getDefault().post(new LaunchNextTaskRequestEvent());
308                    } else {
309                        // Notify recents to move onto the next task
310                        EventBus.getDefault().post(new IterateRecentsEvent());
311                    }
312                } else {
313                    // If the user has toggled it too quickly, then just eat up the event here (it's
314                    // better than showing a janky screenshot).
315                    // NOTE: Ideally, the screenshot mechanism would take the window transform into
316                    // account
317                    if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
318                        return;
319                    }
320
321                    EventBus.getDefault().post(new ToggleRecentsEvent());
322                    mLastToggleTime = SystemClock.elapsedRealtime();
323                }
324                return;
325            } else {
326                // If the user has toggled it too quickly, then just eat up the event here (it's
327                // better than showing a janky screenshot).
328                // NOTE: Ideally, the screenshot mechanism would take the window transform into
329                // account
330                if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
331                    return;
332                }
333
334                // Otherwise, start the recents activity
335                startRecentsActivity(topTask, isTopTaskHome.value, true /* animate */);
336
337                // Only close the other system windows if we are actually showing recents
338                ssp.sendCloseSystemWindows(BaseStatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS);
339                mLastToggleTime = SystemClock.elapsedRealtime();
340            }
341        } catch (ActivityNotFoundException e) {
342            Log.e(TAG, "Failed to launch RecentsActivity", e);
343        }
344    }
345
346    public void preloadRecents() {
347        // Preload only the raw task list into a new load plan (which will be consumed by the
348        // RecentsActivity) only if there is a task to animate to.
349        SystemServicesProxy ssp = Recents.getSystemServices();
350        ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
351        MutableBoolean topTaskHome = new MutableBoolean(true);
352        if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) {
353            RecentsTaskLoader loader = Recents.getTaskLoader();
354            sInstanceLoadPlan = loader.createLoadPlan(mContext);
355            sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
356            loader.preloadTasks(sInstanceLoadPlan, topTask.id, topTaskHome.value);
357            TaskStack stack = sInstanceLoadPlan.getTaskStack();
358            if (stack.getTaskCount() > 0) {
359                // Only preload the icon (but not the thumbnail since it may not have been taken for
360                // the pausing activity)
361                preloadIcon(topTask);
362
363                // At this point, we don't know anything about the stack state.  So only calculate
364                // the dimensions of the thumbnail that we need for the transition into Recents, but
365                // do not draw it until we construct the activity options when we start Recents
366                updateHeaderBarLayout(stack);
367            }
368        }
369    }
370
371    public void cancelPreloadingRecents() {
372        // Do nothing
373    }
374
375    public void onDraggingInRecents(float distanceFromTop) {
376        EventBus.getDefault().sendOntoMainThread(new DraggingInRecentsEvent(distanceFromTop));
377    }
378
379    public void onDraggingInRecentsEnded(float velocity) {
380        EventBus.getDefault().sendOntoMainThread(new DraggingInRecentsEndedEvent(velocity));
381    }
382
383    /**
384     * Transitions to the next recent task in the stack.
385     */
386    public void showNextTask() {
387        SystemServicesProxy ssp = Recents.getSystemServices();
388        RecentsTaskLoader loader = Recents.getTaskLoader();
389        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
390        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
391        TaskStack focusedStack = plan.getTaskStack();
392
393        // Return early if there are no tasks in the focused stack
394        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
395
396        ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
397        // Return early if there is no running task
398        if (runningTask == null) return;
399
400        // Find the task in the recents list
401        boolean isTopTaskHome = SystemServicesProxy.isHomeStack(runningTask.stackId);
402        ArrayList<Task> tasks = focusedStack.getStackTasks();
403        Task toTask = null;
404        ActivityOptions launchOpts = null;
405        int taskCount = tasks.size();
406        for (int i = taskCount - 1; i >= 1; i--) {
407            Task task = tasks.get(i);
408            if (isTopTaskHome) {
409                toTask = tasks.get(i - 1);
410                launchOpts = ActivityOptions.makeCustomAnimation(mContext,
411                        R.anim.recents_launch_next_affiliated_task_target,
412                        R.anim.recents_fast_toggle_app_home_exit);
413                break;
414            } else if (task.key.id == runningTask.id) {
415                toTask = tasks.get(i - 1);
416                launchOpts = ActivityOptions.makeCustomAnimation(mContext,
417                        R.anim.recents_launch_prev_affiliated_task_target,
418                        R.anim.recents_launch_prev_affiliated_task_source);
419                break;
420            }
421        }
422
423        // Return early if there is no next task
424        if (toTask == null) {
425            ssp.startInPlaceAnimationOnFrontMostApplication(
426                    ActivityOptions.makeCustomInPlaceAnimation(mContext,
427                            R.anim.recents_launch_prev_affiliated_task_bounce));
428            return;
429        }
430
431        // Launch the task
432        ssp.startActivityFromRecents(mContext, toTask.key, toTask.title, launchOpts);
433    }
434
435    /**
436     * Transitions to the next affiliated task.
437     */
438    public void showRelativeAffiliatedTask(boolean showNextTask) {
439        SystemServicesProxy ssp = Recents.getSystemServices();
440        RecentsTaskLoader loader = Recents.getTaskLoader();
441        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
442        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
443        TaskStack focusedStack = plan.getTaskStack();
444
445        // Return early if there are no tasks in the focused stack
446        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
447
448        ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
449        // Return early if there is no running task (can't determine affiliated tasks in this case)
450        if (runningTask == null) return;
451        // Return early if the running task is in the home stack (optimization)
452        if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return;
453
454        // Find the task in the recents list
455        ArrayList<Task> tasks = focusedStack.getStackTasks();
456        Task toTask = null;
457        ActivityOptions launchOpts = null;
458        int taskCount = tasks.size();
459        int numAffiliatedTasks = 0;
460        for (int i = 0; i < taskCount; i++) {
461            Task task = tasks.get(i);
462            if (task.key.id == runningTask.id) {
463                TaskGrouping group = task.group;
464                Task.TaskKey toTaskKey;
465                if (showNextTask) {
466                    toTaskKey = group.getNextTaskInGroup(task);
467                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
468                            R.anim.recents_launch_next_affiliated_task_target,
469                            R.anim.recents_launch_next_affiliated_task_source);
470                } else {
471                    toTaskKey = group.getPrevTaskInGroup(task);
472                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
473                            R.anim.recents_launch_prev_affiliated_task_target,
474                            R.anim.recents_launch_prev_affiliated_task_source);
475                }
476                if (toTaskKey != null) {
477                    toTask = focusedStack.findTaskWithId(toTaskKey.id);
478                }
479                numAffiliatedTasks = group.getTaskCount();
480                break;
481            }
482        }
483
484        // Return early if there is no next task
485        if (toTask == null) {
486            if (numAffiliatedTasks > 1) {
487                if (showNextTask) {
488                    ssp.startInPlaceAnimationOnFrontMostApplication(
489                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
490                                    R.anim.recents_launch_next_affiliated_task_bounce));
491                } else {
492                    ssp.startInPlaceAnimationOnFrontMostApplication(
493                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
494                                    R.anim.recents_launch_prev_affiliated_task_bounce));
495                }
496            }
497            return;
498        }
499
500        // Keep track of actually launched affiliated tasks
501        MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1);
502
503        // Launch the task
504        ssp.startActivityFromRecents(mContext, toTask.key, toTask.title, launchOpts);
505    }
506
507    public void showNextAffiliatedTask() {
508        // Keep track of when the affiliated task is triggered
509        MetricsLogger.count(mContext, "overview_affiliated_task_next", 1);
510        showRelativeAffiliatedTask(true);
511    }
512
513    public void showPrevAffiliatedTask() {
514        // Keep track of when the affiliated task is triggered
515        MetricsLogger.count(mContext, "overview_affiliated_task_prev", 1);
516        showRelativeAffiliatedTask(false);
517    }
518
519    public void dockTopTask(int topTaskId, int dragMode,
520            int stackCreateMode, Rect initialBounds) {
521        SystemServicesProxy ssp = Recents.getSystemServices();
522
523        // Make sure we inform DividerView before we actually start the activity so we can change
524        // the resize mode already.
525        if (ssp.moveTaskToDockedStack(topTaskId, stackCreateMode, initialBounds)) {
526            EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds));
527            showRecents(
528                    false /* triggeredFromAltTab */,
529                    dragMode == NavigationBarGestureHelper.DRAG_MODE_RECENTS,
530                    false /* animate */,
531                    true /* launchedWhileDockingTask*/);
532        }
533    }
534
535    /**
536     * Returns the preloaded load plan and invalidates it.
537     */
538    public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
539        RecentsTaskLoadPlan plan = sInstanceLoadPlan;
540        sInstanceLoadPlan = null;
541        return plan;
542    }
543
544    /**
545     * Reloads all the layouts for the header bar transition.
546     */
547    private void reloadHeaderBarLayout() {
548        Resources res = mContext.getResources();
549        LayoutInflater inflater = LayoutInflater.from(mContext);
550
551        mStatusBarHeight = res.getDimensionPixelSize(
552                com.android.internal.R.dimen.status_bar_height);
553        mNavBarHeight = res.getDimensionPixelSize(
554                com.android.internal.R.dimen.navigation_bar_height);
555        mNavBarWidth = res.getDimensionPixelSize(
556                com.android.internal.R.dimen.navigation_bar_width);
557        mTaskBarHeight = TaskStackLayoutAlgorithm.getDimensionForDevice(res,
558                R.dimen.recents_task_view_header_height,
559                R.dimen.recents_task_view_header_height,
560                R.dimen.recents_task_view_header_height,
561                R.dimen.recents_task_view_header_height_tablet_land,
562                R.dimen.recents_task_view_header_height,
563                R.dimen.recents_task_view_header_height_tablet_land);
564        mDummyStackView = new TaskStackView(mContext);
565        mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header,
566                null, false);
567    }
568
569    /**
570     * Prepares the header bar layout for the next transition, if the task view bounds has changed
571     * since the last call, it will attempt to re-measure and layout the header bar to the new size.
572     *
573     * @param stack the stack to initialize the stack layout with
574     */
575    private void updateHeaderBarLayout(TaskStack stack) {
576        SystemServicesProxy ssp = Recents.getSystemServices();
577        Rect systemInsets = new Rect();
578        ssp.getStableInsets(systemInsets);
579        Rect windowRect = ssp.getWindowRect();
580        // When docked, the nav bar insets are consumed and the activity is measured without insets.
581        // However, the window bounds include the insets, so we need to subtract them here to make
582        // them identical.
583        if (ssp.hasDockedTask()) {
584            windowRect.bottom -= systemInsets.bottom;
585            systemInsets.bottom = 0;
586        }
587        calculateWindowStableInsets(systemInsets, windowRect);
588        windowRect.offsetTo(0, 0);
589
590        TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
591
592        // Rebind the header bar and draw it for the transition
593        stackLayout.setSystemInsets(systemInsets);
594        if (stack != null) {
595            stackLayout.getTaskStackBounds(windowRect, systemInsets.top, systemInsets.right,
596                    mTaskStackBounds);
597            stackLayout.reset();
598            stackLayout.initialize(windowRect, mTaskStackBounds,
599                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(stack));
600            mDummyStackView.setTasks(stack, false /* allowNotifyStackChanges */);
601
602            Rect taskViewBounds = stackLayout.getUntransformedTaskViewBounds();
603            if (!taskViewBounds.isEmpty()) {
604                int taskViewWidth = taskViewBounds.width();
605                synchronized (mHeaderBarLock) {
606                    if (mHeaderBar.getMeasuredWidth() != taskViewWidth ||
607                            mHeaderBar.getMeasuredHeight() != mTaskBarHeight) {
608                        mHeaderBar.measure(
609                                MeasureSpec.makeMeasureSpec(taskViewWidth, MeasureSpec.EXACTLY),
610                                MeasureSpec.makeMeasureSpec(mTaskBarHeight, MeasureSpec.EXACTLY));
611                    }
612                    mHeaderBar.layout(0, 0, taskViewWidth, mTaskBarHeight);
613                }
614
615                // Update the transition bitmap to match the new header bar height
616                if (mThumbTransitionBitmapCache == null ||
617                        (mThumbTransitionBitmapCache.getWidth() != taskViewWidth) ||
618                        (mThumbTransitionBitmapCache.getHeight() != mTaskBarHeight)) {
619                    mThumbTransitionBitmapCache = Bitmap.createBitmap(taskViewWidth,
620                            mTaskBarHeight, Bitmap.Config.ARGB_8888);
621                }
622            }
623        }
624    }
625
626    /**
627     * Given the stable insets and the rect for our window, calculates the insets that affect our
628     * window.
629     */
630    private void calculateWindowStableInsets(Rect inOutInsets, Rect windowRect) {
631        Rect displayRect = Recents.getSystemServices().getDisplayRect();
632
633        // Display rect without insets - available app space
634        Rect appRect = new Rect(displayRect);
635        appRect.inset(inOutInsets);
636
637        // Our window intersected with available app space
638        Rect windowRectWithInsets = new Rect(windowRect);
639        windowRectWithInsets.intersect(appRect);
640        inOutInsets.left = windowRectWithInsets.left - windowRect.left;
641        inOutInsets.top = windowRectWithInsets.top - windowRect.top;
642        inOutInsets.right = windowRect.right - windowRectWithInsets.right;
643        inOutInsets.bottom = windowRect.bottom - windowRectWithInsets.bottom;
644    }
645
646    /**
647     * Preloads the icon of a task.
648     */
649    private void preloadIcon(ActivityManager.RunningTaskInfo task) {
650        // Ensure that we load the running task's icon
651        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
652        launchOpts.runningTaskId = task.id;
653        launchOpts.loadThumbnails = false;
654        launchOpts.onlyLoadForCache = true;
655        Recents.getTaskLoader().loadTasks(mContext, sInstanceLoadPlan, launchOpts);
656    }
657
658    /**
659     * Creates the activity options for a unknown state->recents transition.
660     */
661    protected ActivityOptions getUnknownTransitionActivityOptions() {
662        return ActivityOptions.makeCustomAnimation(mContext,
663                R.anim.recents_from_unknown_enter,
664                R.anim.recents_from_unknown_exit,
665                mHandler, null);
666    }
667
668    /**
669     * Creates the activity options for a home->recents transition.
670     */
671    protected ActivityOptions getHomeTransitionActivityOptions() {
672        return ActivityOptions.makeCustomAnimation(mContext,
673                R.anim.recents_from_launcher_enter,
674                R.anim.recents_from_launcher_exit,
675                mHandler, null);
676    }
677
678    /**
679     * Creates the activity options for an app->recents transition.
680     */
681    private ActivityOptions getThumbnailTransitionActivityOptions(
682            ActivityManager.RunningTaskInfo topTask, TaskStackView stackView) {
683        if (topTask.stackId == FREEFORM_WORKSPACE_STACK_ID) {
684            ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>();
685            ArrayList<Task> tasks = stackView.getStack().getStackTasks();
686            TaskStackLayoutAlgorithm stackLayout = stackView.getStackAlgorithm();
687            TaskStackViewScroller stackScroller = stackView.getScroller();
688
689            stackView.updateLayoutAlgorithm(true /* boundScroll */);
690            stackView.updateToInitialState(true /* scrollToInitialState */);
691
692            for (int i = tasks.size() - 1; i >= 0; i--) {
693                Task task = tasks.get(i);
694                if (task.isFreeformTask()) {
695                    mTmpTransform = stackLayout.getStackTransformScreenCoordinates(task,
696                                    stackScroller.getStackScroll(), mTmpTransform, null);
697                    Bitmap thumbnail = drawThumbnailTransitionBitmap(task, mTmpTransform,
698                            mThumbTransitionBitmapCache);
699                    Rect toTaskRect = new Rect();
700                    mTmpTransform.rect.round(toTaskRect);
701                    specs.add(new AppTransitionAnimationSpec(task.key.id, thumbnail, toTaskRect));
702                }
703            }
704            AppTransitionAnimationSpec[] specsArray = new AppTransitionAnimationSpec[specs.size()];
705            specs.toArray(specsArray);
706            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
707                    specsArray, mHandler, null, this);
708        } else {
709            // Update the destination rect
710            Task toTask = new Task();
711            TaskViewTransform toTransform = getThumbnailTransitionTransform(stackView, toTask);
712            Bitmap thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform,
713                    mThumbTransitionBitmapCache);
714            if (thumbnail != null) {
715                RectF toTaskRect = toTransform.rect;
716                return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
717                        thumbnail, (int) toTaskRect.left, (int) toTaskRect.top,
718                        (int) toTaskRect.width(), (int) toTaskRect.height(), mHandler, null);
719            }
720            // If both the screenshot and thumbnail fails, then just fall back to the default transition
721            return getUnknownTransitionActivityOptions();
722        }
723    }
724
725    /**
726     * Returns the transition rect for the given task id.
727     */
728    private TaskViewTransform getThumbnailTransitionTransform(TaskStackView stackView,
729            Task runningTaskOut) {
730        // Find the running task in the TaskStack
731        TaskStack stack = stackView.getStack();
732        Task launchTask = stack.getLaunchTarget();
733        if (launchTask != null) {
734            runningTaskOut.copyFrom(launchTask);
735        } else {
736            // If no task is specified or we can not find the task just use the front most one
737            launchTask = stack.getStackFrontMostTask(true /* includeFreeform */);
738            runningTaskOut.copyFrom(launchTask);
739        }
740
741        // Get the transform for the running task
742        stackView.updateLayoutAlgorithm(true /* boundScroll */);
743        stackView.updateToInitialState(true /* scrollToInitialState */);
744        stackView.getStackAlgorithm().getStackTransformScreenCoordinates(launchTask,
745                stackView.getScroller().getStackScroll(), mTmpTransform, null);
746        return mTmpTransform;
747    }
748
749    /**
750     * Draws the header of a task used for the window animation into a bitmap.
751     */
752    private Bitmap drawThumbnailTransitionBitmap(Task toTask, TaskViewTransform toTransform,
753            Bitmap thumbnail) {
754        SystemServicesProxy ssp = Recents.getSystemServices();
755        if (toTransform != null && toTask.key != null) {
756            synchronized (mHeaderBarLock) {
757                boolean disabledInSafeMode = !toTask.isSystemApp && ssp.isInSafeMode();
758                mHeaderBar.onTaskViewSizeChanged((int) toTransform.rect.width(),
759                        (int) toTransform.rect.height());
760                if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
761                    thumbnail.eraseColor(0xFFff0000);
762                } else {
763                    thumbnail.eraseColor(0);
764                    Canvas c = new Canvas(thumbnail);
765                    // Workaround for b/27815919, reset the callback so that we do not trigger an
766                    // invalidate on the header bar as a result of updating the icon
767                    Drawable icon = mHeaderBar.getIconView().getDrawable();
768                    if (icon != null) {
769                        icon.setCallback(null);
770                    }
771                    mHeaderBar.rebindToTask(toTask, false /* touchExplorationEnabled */,
772                            disabledInSafeMode);
773                    mHeaderBar.setDimAlpha(toTransform.dimAlpha);
774                    mHeaderBar.draw(c);
775                    c.setBitmap(null);
776                }
777            }
778            return thumbnail.createAshmemBitmap();
779        }
780        return null;
781    }
782
783    /**
784     * Shows the recents activity
785     */
786    protected void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
787            boolean isTopTaskHome, boolean animate) {
788        RecentsTaskLoader loader = Recents.getTaskLoader();
789        RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
790
791        // In the case where alt-tab is triggered, we never get a preloadRecents() call, so we
792        // should always preload the tasks now. If we are dragging in recents, reload them as
793        // the stacks might have changed.
794        if (mLaunchedWhileDocking || mTriggeredFromAltTab ||sInstanceLoadPlan == null) {
795            // Create a new load plan if preloadRecents() was never triggered
796            sInstanceLoadPlan = loader.createLoadPlan(mContext);
797        }
798        if (mLaunchedWhileDocking || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
799            loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome);
800        }
801
802        TaskStack stack = sInstanceLoadPlan.getTaskStack();
803        boolean hasRecentTasks = stack.getTaskCount() > 0;
804        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
805
806        // Update the launch state that we need in updateHeaderBarLayout()
807        launchState.launchedFromHome = !useThumbnailTransition;
808        launchState.launchedFromApp = useThumbnailTransition || mLaunchedWhileDocking;
809        launchState.launchedViaDockGesture = mLaunchedWhileDocking;
810        launchState.launchedViaDragGesture = mDraggingInRecents;
811        launchState.launchedToTaskId = (topTask != null) ? topTask.id : -1;
812        launchState.launchedWithAltTab = mTriggeredFromAltTab;
813
814        // Preload the icon (this will be a null-op if we have preloaded the icon already in
815        // preloadRecents())
816        preloadIcon(topTask);
817
818        // Update the header bar if necessary
819        updateHeaderBarLayout(stack);
820
821        // Prepare the dummy stack for the transition
822        TaskStackLayoutAlgorithm.VisibilityReport stackVr =
823                mDummyStackView.computeStackVisibilityReport();
824
825        // Update the remaining launch state
826        launchState.launchedNumVisibleTasks = stackVr.numVisibleTasks;
827        launchState.launchedNumVisibleThumbnails = stackVr.numVisibleThumbnails;
828
829        if (!animate) {
830            startRecentsActivity(ActivityOptions.makeCustomAnimation(mContext, -1, -1));
831            return;
832        }
833
834        ActivityOptions opts;
835        if (useThumbnailTransition) {
836            // Try starting with a thumbnail transition
837            opts = getThumbnailTransitionActivityOptions(topTask, mDummyStackView);
838        } else {
839            // If there is no thumbnail transition, but is launching from home into recents, then
840            // use a quick home transition
841            opts = hasRecentTasks
842                ? getHomeTransitionActivityOptions()
843                : getUnknownTransitionActivityOptions();
844        }
845        startRecentsActivity(opts);
846        mLastToggleTime = SystemClock.elapsedRealtime();
847    }
848
849    /**
850     * Starts the recents activity.
851     */
852    private void startRecentsActivity(ActivityOptions opts) {
853        Intent intent = new Intent();
854        intent.setClassName(RECENTS_PACKAGE, RECENTS_ACTIVITY);
855        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
856                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
857                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
858
859        if (opts != null) {
860            mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
861        } else {
862            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
863        }
864        EventBus.getDefault().send(new RecentsActivityStartingEvent());
865    }
866
867    /**** OnAnimationFinishedListener Implementation ****/
868
869    @Override
870    public void onAnimationFinished() {
871        EventBus.getDefault().post(new EnterRecentsWindowLastAnimationFrameEvent());
872    }
873}
874