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