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