RecentsImpl.java revision d64ef3ef33c50a03b4be3b2baaa93aab7319fca8
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.FREEFORM_WORKSPACE_STACK_ID;
20
21import android.app.ActivityManager;
22import android.app.ActivityOptions;
23import android.app.ITaskStackListener;
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.os.Handler;
33import android.os.SystemClock;
34import android.os.UserHandle;
35import android.util.MutableBoolean;
36import android.view.AppTransitionAnimationSpec;
37import android.view.LayoutInflater;
38import android.view.View;
39
40import com.android.internal.logging.MetricsLogger;
41import com.android.systemui.Prefs;
42import com.android.systemui.R;
43import com.android.systemui.SystemUIApplication;
44import com.android.systemui.recents.events.EventBus;
45import com.android.systemui.recents.events.activity.EnterRecentsWindowAnimationStartedEvent;
46import com.android.systemui.recents.events.activity.HideRecentsEvent;
47import com.android.systemui.recents.events.activity.IterateRecentsEvent;
48import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
49import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
50import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
51import com.android.systemui.recents.misc.Console;
52import com.android.systemui.recents.misc.ForegroundThread;
53import com.android.systemui.recents.misc.SystemServicesProxy;
54import com.android.systemui.recents.model.RecentsTaskLoadPlan;
55import com.android.systemui.recents.model.RecentsTaskLoader;
56import com.android.systemui.recents.model.Task;
57import com.android.systemui.recents.model.TaskGrouping;
58import com.android.systemui.recents.model.TaskStack;
59import com.android.systemui.recents.views.TaskStackView;
60import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm;
61import com.android.systemui.recents.views.TaskViewHeader;
62import com.android.systemui.recents.views.TaskViewTransform;
63import com.android.systemui.statusbar.phone.PhoneStatusBar;
64
65import java.util.ArrayList;
66
67/**
68 * An implementation of the Recents component for the current user.  For secondary users, this can
69 * be called remotely from the system user.
70 */
71public class RecentsImpl extends IRecentsNonSystemUserCallbacks.Stub
72        implements ActivityOptions.OnAnimationStartedListener {
73
74    private final static String TAG = "RecentsImpl";
75    private final static boolean DEBUG = false;
76
77    private final static int sMinToggleDelay = 350;
78
79    public final static String RECENTS_PACKAGE = "com.android.systemui";
80    public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
81
82
83    /**
84     * An implementation of ITaskStackListener, that allows us to listen for changes to the system
85     * task stacks and update recents accordingly.
86     */
87    class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable {
88        Handler mHandler;
89
90        public TaskStackListenerImpl(Handler handler) {
91            mHandler = handler;
92        }
93
94        @Override
95        public void onTaskStackChanged() {
96            // Debounce any task stack changes
97            mHandler.removeCallbacks(this);
98            mHandler.post(this);
99        }
100
101        /** Preloads the next task */
102        public void run() {
103            // TODO: Temporarily skip this if multi stack is enabled
104            /*
105            RecentsConfiguration config = RecentsConfiguration.getInstance();
106            if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
107                RecentsTaskLoader loader = Recents.getTaskLoader();
108                SystemServicesProxy ssp = Recents.getSystemServices();
109                ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask();
110
111                // Load the next task only if we aren't svelte
112                RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
113                loader.preloadTasks(plan, true);
114                RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
115                // This callback is made when a new activity is launched and the old one is paused
116                // so ignore the current activity and try and preload the thumbnail for the
117                // previous one.
118                if (runningTaskInfo != null) {
119                    launchOpts.runningTaskId = runningTaskInfo.id;
120                }
121                launchOpts.numVisibleTasks = 2;
122                launchOpts.numVisibleTaskThumbnails = 2;
123                launchOpts.onlyLoadForCache = true;
124                launchOpts.onlyLoadPausedActivities = true;
125                loader.loadTasks(mContext, plan, launchOpts);
126            }
127            */
128        }
129    }
130
131    private static RecentsTaskLoadPlan sInstanceLoadPlan;
132
133    Context mContext;
134    Handler mHandler;
135    TaskStackListenerImpl mTaskStackListener;
136    RecentsAppWidgetHost mAppWidgetHost;
137    boolean mBootCompleted;
138    boolean mStartAnimationTriggered;
139    boolean mCanReuseTaskStackViews = true;
140
141    // Task launching
142    RecentsConfiguration mConfig;
143    Rect mSearchBarBounds = new Rect();
144    Rect mTaskStackBounds = new Rect();
145    Rect mLastTaskViewBounds = new Rect();
146    TaskViewTransform mTmpTransform = new TaskViewTransform();
147    int mStatusBarHeight;
148    int mNavBarHeight;
149    int mNavBarWidth;
150    int mTaskBarHeight;
151
152    // Header (for transition)
153    TaskViewHeader mHeaderBar;
154    final Object mHeaderBarLock = new Object();
155    TaskStackView mDummyStackView;
156
157    // Variables to keep track of if we need to start recents after binding
158    boolean mTriggeredFromAltTab;
159    long mLastToggleTime;
160
161    Bitmap mThumbnailTransitionBitmapCache;
162    Task mThumbnailTransitionBitmapCacheKey;
163
164
165    public RecentsImpl(Context context) {
166        mContext = context;
167        mHandler = new Handler();
168        mAppWidgetHost = new RecentsAppWidgetHost(mContext, Constants.Values.App.AppWidgetHostId);
169        Resources res = mContext.getResources();
170        LayoutInflater inflater = LayoutInflater.from(mContext);
171
172        // Initialize the static foreground thread
173        ForegroundThread.get();
174
175        // Register the task stack listener
176        mTaskStackListener = new TaskStackListenerImpl(mHandler);
177        SystemServicesProxy ssp = Recents.getSystemServices();
178        ssp.registerTaskStackListener(mTaskStackListener);
179
180        // Initialize the static configuration resources
181        mConfig = RecentsConfiguration.initialize(mContext, ssp);
182        mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
183        mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height);
184        mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width);
185        mTaskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height);
186        mDummyStackView = new TaskStackView(mContext, new TaskStack());
187        mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header,
188                null, false);
189        reloadHeaderBarLayout(true /* tryAndBindSearchWidget */);
190
191        // When we start, preload the data associated with the previous recent tasks.
192        // We can use a new plan since the caches will be the same.
193        RecentsTaskLoader loader = Recents.getTaskLoader();
194        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
195        loader.preloadTasks(plan, true /* isTopTaskHome */);
196        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
197        launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize();
198        launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
199        launchOpts.onlyLoadForCache = true;
200        loader.loadTasks(mContext, plan, launchOpts);
201    }
202
203    public void onBootCompleted() {
204        mBootCompleted = true;
205        reloadHeaderBarLayout(true /* tryAndBindSearchWidget */);
206    }
207
208    @Override
209    public void onConfigurationChanged() {
210        // Don't reuse task stack views if the configuration changes
211        mCanReuseTaskStackViews = false;
212        mConfig.updateOnConfigurationChange();
213    }
214
215    /**
216     * This is only called from the system user's Recents.  Secondary users will instead proxy their
217     * visibility change events through to the system user via
218     * {@link Recents#onBusEvent(RecentsVisibilityChangedEvent)}.
219     */
220    public void onVisibilityChanged(Context context, boolean visible) {
221        SystemUIApplication app = (SystemUIApplication) context;
222        PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class);
223        if (statusBar != null) {
224            statusBar.updateRecentsVisibility(visible);
225        }
226    }
227
228    /**
229     * This is only called from the system user's Recents.  Secondary users will instead proxy their
230     * visibility change events through to the system user via
231     * {@link Recents#onBusEvent(ScreenPinningRequestEvent)}.
232     */
233    public void onStartScreenPinning(Context context) {
234        SystemUIApplication app = (SystemUIApplication) context;
235        PhoneStatusBar statusBar = app.getComponent(PhoneStatusBar.class);
236        if (statusBar != null) {
237            statusBar.showScreenPinningRequest(false);
238        }
239    }
240
241    @Override
242    public void showRecents(boolean triggeredFromAltTab) {
243        mTriggeredFromAltTab = triggeredFromAltTab;
244
245        try {
246            // Check if the top task is in the home stack, and start the recents activity
247            SystemServicesProxy ssp = Recents.getSystemServices();
248            ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
249            MutableBoolean isTopTaskHome = new MutableBoolean(true);
250            if (topTask == null || !ssp.isRecentsTopMost(topTask, isTopTaskHome)) {
251                startRecentsActivity(topTask, isTopTaskHome.value);
252            }
253        } catch (ActivityNotFoundException e) {
254            Console.logRawError("Failed to launch RecentAppsIntent", e);
255        }
256    }
257
258    @Override
259    public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
260        if (mBootCompleted) {
261            // Defer to the activity to handle hiding recents, if it handles it, then it must still
262            // be visible
263            EventBus.getDefault().post(new HideRecentsEvent(triggeredFromAltTab,
264                    triggeredFromHomeKey));
265        }
266    }
267
268    @Override
269    public void toggleRecents() {
270        mTriggeredFromAltTab = false;
271
272        try {
273            SystemServicesProxy ssp = Recents.getSystemServices();
274            ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
275            MutableBoolean isTopTaskHome = new MutableBoolean(true);
276            if (topTask != null && ssp.isRecentsTopMost(topTask, isTopTaskHome)) {
277                if (Constants.DebugFlags.App.EnableFastToggleRecents) {
278                    // Notify recents to move onto the next task
279                    EventBus.getDefault().post(new IterateRecentsEvent());
280                } else {
281                    // If the user has toggled it too quickly, then just eat up the event here (it's
282                    // better than showing a janky screenshot).
283                    // NOTE: Ideally, the screenshot mechanism would take the window transform into
284                    // account
285                    if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
286                        return;
287                    }
288
289                    EventBus.getDefault().post(new ToggleRecentsEvent());
290                    mLastToggleTime = SystemClock.elapsedRealtime();
291                }
292                return;
293            } else {
294                // If the user has toggled it too quickly, then just eat up the event here (it's
295                // better than showing a janky screenshot).
296                // NOTE: Ideally, the screenshot mechanism would take the window transform into
297                // account
298                if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
299                    return;
300                }
301
302                // Otherwise, start the recents activity
303                startRecentsActivity(topTask, isTopTaskHome.value);
304                mLastToggleTime = SystemClock.elapsedRealtime();
305            }
306        } catch (ActivityNotFoundException e) {
307            Console.logRawError("Failed to launch RecentAppsIntent", e);
308        }
309    }
310
311    @Override
312    public void preloadRecents() {
313        // Preload only the raw task list into a new load plan (which will be consumed by the
314        // RecentsActivity) only if there is a task to animate to.
315        SystemServicesProxy ssp = Recents.getSystemServices();
316        ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
317        MutableBoolean topTaskHome = new MutableBoolean(true);
318        RecentsTaskLoader loader = Recents.getTaskLoader();
319        sInstanceLoadPlan = loader.createLoadPlan(mContext);
320        if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) {
321            sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
322            loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value);
323            TaskStack stack = sInstanceLoadPlan.getTaskStack();
324            if (stack.getTaskCount() > 0) {
325                // We try and draw the thumbnail transition bitmap in parallel before
326                // toggle/show recents is called
327                preCacheThumbnailTransitionBitmapAsync(topTask, stack, mDummyStackView);
328            }
329        }
330    }
331
332    @Override
333    public void cancelPreloadingRecents() {
334        // Do nothing
335    }
336
337    public void showRelativeAffiliatedTask(boolean showNextTask) {
338        // Return early if there is no focused stack
339        SystemServicesProxy ssp = Recents.getSystemServices();
340        int focusedStackId = ssp.getFocusedStack();
341        RecentsTaskLoader loader = Recents.getTaskLoader();
342        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
343        loader.preloadTasks(plan, true /* isTopTaskHome */);
344        TaskStack focusedStack = plan.getTaskStack();
345
346        // Return early if there are no tasks in the focused stack
347        if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
348
349        ActivityManager.RunningTaskInfo runningTask = ssp.getTopMostTask();
350        // Return early if there is no running task (can't determine affiliated tasks in this case)
351        if (runningTask == null) return;
352        // Return early if the running task is in the home stack (optimization)
353        if (SystemServicesProxy.isHomeStack(runningTask.stackId)) return;
354
355        // Find the task in the recents list
356        ArrayList<Task> tasks = focusedStack.getTasks();
357        Task toTask = null;
358        ActivityOptions launchOpts = null;
359        int taskCount = tasks.size();
360        int numAffiliatedTasks = 0;
361        for (int i = 0; i < taskCount; i++) {
362            Task task = tasks.get(i);
363            if (task.key.id == runningTask.id) {
364                TaskGrouping group = task.group;
365                Task.TaskKey toTaskKey;
366                if (showNextTask) {
367                    toTaskKey = group.getNextTaskInGroup(task);
368                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
369                            R.anim.recents_launch_next_affiliated_task_target,
370                            R.anim.recents_launch_next_affiliated_task_source);
371                } else {
372                    toTaskKey = group.getPrevTaskInGroup(task);
373                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
374                            R.anim.recents_launch_prev_affiliated_task_target,
375                            R.anim.recents_launch_prev_affiliated_task_source);
376                }
377                if (toTaskKey != null) {
378                    toTask = focusedStack.findTaskWithId(toTaskKey.id);
379                }
380                numAffiliatedTasks = group.getTaskCount();
381                break;
382            }
383        }
384
385        // Return early if there is no next task
386        if (toTask == null) {
387            if (numAffiliatedTasks > 1) {
388                if (showNextTask) {
389                    ssp.startInPlaceAnimationOnFrontMostApplication(
390                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
391                                    R.anim.recents_launch_next_affiliated_task_bounce));
392                } else {
393                    ssp.startInPlaceAnimationOnFrontMostApplication(
394                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
395                                    R.anim.recents_launch_prev_affiliated_task_bounce));
396                }
397            }
398            return;
399        }
400
401        // Keep track of actually launched affiliated tasks
402        MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1);
403
404        // Launch the task
405        if (toTask.isActive) {
406            // Bring an active task to the foreground
407            ssp.moveTaskToFront(toTask.key.id, launchOpts);
408        } else {
409            ssp.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts);
410        }
411    }
412
413    public void showNextAffiliatedTask() {
414        // Keep track of when the affiliated task is triggered
415        MetricsLogger.count(mContext, "overview_affiliated_task_next", 1);
416        showRelativeAffiliatedTask(true);
417    }
418
419    public void showPrevAffiliatedTask() {
420        // Keep track of when the affiliated task is triggered
421        MetricsLogger.count(mContext, "overview_affiliated_task_prev", 1);
422        showRelativeAffiliatedTask(false);
423    }
424
425    public void dockTopTask() {
426        SystemServicesProxy ssp = Recents.getSystemServices();
427        ActivityManager.RunningTaskInfo topTask = ssp.getTopMostTask();
428        if (topTask != null && !SystemServicesProxy.isHomeStack(topTask.stackId)) {
429            ssp.startTaskInDockedMode(topTask.id,
430                    ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT);
431            showRecents(false /* triggeredFromAltTab */);
432        }
433    }
434
435    /**
436     * Returns the preloaded load plan and invalidates it.
437     */
438    public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
439        RecentsTaskLoadPlan plan = sInstanceLoadPlan;
440        sInstanceLoadPlan = null;
441        return plan;
442    }
443
444    /**
445     * Prepares the header bar layout for the next transition, if the task view bounds has changed
446     * since the last call, it will attempt to re-measure and layout the header bar to the new size.
447     *
448     * @param tryAndBindSearchWidget if set, will attempt to fetch and bind the search widget if one
449     *                               is not already bound (can be expensive)
450     */
451    private void reloadHeaderBarLayout(boolean tryAndBindSearchWidget) {
452        SystemServicesProxy ssp = Recents.getSystemServices();
453        Rect windowRect = ssp.getWindowRect();
454
455        // Update the configuration for the current state
456        mConfig.update(mContext, ssp, ssp.getWindowRect());
457
458        if (!Constants.DebugFlags.App.DisableSearchBar && tryAndBindSearchWidget) {
459            // Try and pre-emptively bind the search widget on startup to ensure that we
460            // have the right thumbnail bounds to animate to.
461            // Note: We have to reload the widget id before we get the task stack bounds below
462            if (ssp.getOrBindSearchAppWidget(mContext, mAppWidgetHost) != null) {
463                mConfig.getSearchBarBounds(windowRect, mStatusBarHeight, mSearchBarBounds);
464            }
465        }
466        Rect systemInsets = new Rect(0, mStatusBarHeight,
467                (mConfig.hasTransposedNavBar ? mNavBarWidth : 0),
468                (mConfig.hasTransposedNavBar ? 0 : mNavBarHeight));
469        mConfig.getTaskStackBounds(windowRect, systemInsets.top, systemInsets.right,
470                mSearchBarBounds, mTaskStackBounds);
471
472        // Rebind the header bar and draw it for the transition
473        TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm();
474        Rect taskStackBounds = new Rect(mTaskStackBounds);
475        algo.setSystemInsets(systemInsets);
476        algo.computeRects(windowRect.width(), windowRect.height(), taskStackBounds);
477        Rect taskViewBounds = algo.getUntransformedTaskViewBounds();
478        if (!taskViewBounds.equals(mLastTaskViewBounds)) {
479            mLastTaskViewBounds.set(taskViewBounds);
480
481            int taskViewWidth = taskViewBounds.width();
482            synchronized (mHeaderBarLock) {
483                mHeaderBar.measure(
484                    View.MeasureSpec.makeMeasureSpec(taskViewWidth, View.MeasureSpec.EXACTLY),
485                    View.MeasureSpec.makeMeasureSpec(mTaskBarHeight, View.MeasureSpec.EXACTLY));
486                mHeaderBar.layout(0, 0, taskViewWidth, mTaskBarHeight);
487            }
488        }
489    }
490
491    /**
492     * Preloads the icon of a task.
493     */
494    private void preloadIcon(ActivityManager.RunningTaskInfo task) {
495        // Ensure that we load the running task's icon
496        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
497        launchOpts.runningTaskId = task.id;
498        launchOpts.loadThumbnails = false;
499        launchOpts.onlyLoadForCache = true;
500        Recents.getTaskLoader().loadTasks(mContext, sInstanceLoadPlan, launchOpts);
501    }
502
503    /**
504     * Caches the header thumbnail used for a window animation asynchronously into
505     * {@link #mThumbnailTransitionBitmapCache}.
506     */
507    private void preCacheThumbnailTransitionBitmapAsync(ActivityManager.RunningTaskInfo topTask,
508            TaskStack stack, TaskStackView stackView) {
509        preloadIcon(topTask);
510
511        // Update the header bar if necessary
512        reloadHeaderBarLayout(false /* tryAndBindSearchWidget */);
513
514        // Update the destination rect
515        mDummyStackView.updateMinMaxScrollForStack(stack);
516        final Task toTask = new Task();
517        final TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
518                topTask.id, toTask);
519        ForegroundThread.getHandler().post(new Runnable() {
520            @Override
521            public void run() {
522                final Bitmap transitionBitmap = drawThumbnailTransitionBitmap(toTask, toTransform);
523                mHandler.post(new Runnable() {
524                    @Override
525                    public void run() {
526                        mThumbnailTransitionBitmapCache = transitionBitmap;
527                        mThumbnailTransitionBitmapCacheKey = toTask;
528                    }
529                });
530            }
531        });
532    }
533
534    /**
535     * Creates the activity options for a unknown state->recents transition.
536     */
537    private ActivityOptions getUnknownTransitionActivityOptions() {
538        return ActivityOptions.makeCustomAnimation(mContext,
539                R.anim.recents_from_unknown_enter,
540                R.anim.recents_from_unknown_exit,
541                mHandler, this);
542    }
543
544    /**
545     * Creates the activity options for a home->recents transition.
546     */
547    private ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) {
548        if (fromSearchHome) {
549            return ActivityOptions.makeCustomAnimation(mContext,
550                    R.anim.recents_from_search_launcher_enter,
551                    R.anim.recents_from_search_launcher_exit,
552                    mHandler, this);
553        }
554        return ActivityOptions.makeCustomAnimation(mContext,
555                R.anim.recents_from_launcher_enter,
556                R.anim.recents_from_launcher_exit,
557                mHandler, this);
558    }
559
560    /**
561     * Creates the activity options for an app->recents transition.
562     */
563    private ActivityOptions getThumbnailTransitionActivityOptions(
564            ActivityManager.RunningTaskInfo topTask, TaskStack stack, TaskStackView stackView) {
565        if (topTask.stackId == FREEFORM_WORKSPACE_STACK_ID) {
566            ArrayList<AppTransitionAnimationSpec> specs = new ArrayList<>();
567            stackView.getScroller().setStackScrollToInitialState();
568            ArrayList<Task> tasks = stack.getTasks();
569            for (int i = tasks.size() - 1; i >= 0; i--) {
570                Task task = tasks.get(i);
571                if (SystemServicesProxy.isFreeformStack(task.key.stackId)) {
572                    mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
573                            stackView.getScroller().getStackScroll(), mTmpTransform, null);
574                    Rect toTaskRect = new Rect();
575                    mTmpTransform.rect.round(toTaskRect);
576                    Bitmap thumbnail = getThumbnailBitmap(topTask, task, mTmpTransform);
577                    specs.add(new AppTransitionAnimationSpec(task.key.id, thumbnail, toTaskRect));
578                }
579            }
580            AppTransitionAnimationSpec[] specsArray = new AppTransitionAnimationSpec[specs.size()];
581            specs.toArray(specsArray);
582            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
583                    specsArray, mHandler, this);
584        } else {
585            // Update the destination rect
586            Task toTask = new Task();
587            TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
588                    topTask.id, toTask);
589            RectF toTaskRect = toTransform.rect;
590            Bitmap thumbnail = getThumbnailBitmap(topTask, toTask, toTransform);
591            if (thumbnail != null) {
592                return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
593                        thumbnail, (int) toTaskRect.left, (int) toTaskRect.top,
594                        (int) toTaskRect.width(), (int) toTaskRect.height(), mHandler, this);
595            }
596            // If both the screenshot and thumbnail fails, then just fall back to the default transition
597            return getUnknownTransitionActivityOptions();
598        }
599    }
600
601    private Bitmap getThumbnailBitmap(ActivityManager.RunningTaskInfo topTask, Task toTask,
602            TaskViewTransform toTransform) {
603        Bitmap thumbnail;
604        if (mThumbnailTransitionBitmapCacheKey != null
605                && mThumbnailTransitionBitmapCacheKey.key != null
606                && mThumbnailTransitionBitmapCacheKey.key.equals(toTask.key)) {
607            thumbnail = mThumbnailTransitionBitmapCache;
608            mThumbnailTransitionBitmapCacheKey = null;
609            mThumbnailTransitionBitmapCache = null;
610        } else {
611            preloadIcon(topTask);
612            thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform);
613        }
614        return thumbnail;
615    }
616
617    /**
618     * Returns the transition rect for the given task id.
619     */
620    private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack,
621            TaskStackView stackView, int runningTaskId, Task runningTaskOut) {
622        // Find the running task in the TaskStack
623        Task task = null;
624        ArrayList<Task> tasks = stack.getTasks();
625        if (runningTaskId != -1) {
626            // Otherwise, try and find the task with the
627            int taskCount = tasks.size();
628            for (int i = taskCount - 1; i >= 0; i--) {
629                Task t = tasks.get(i);
630                if (t.key.id == runningTaskId) {
631                    task = t;
632                    runningTaskOut.copyFrom(t);
633                    break;
634                }
635            }
636        }
637        if (task == null) {
638            // If no task is specified or we can not find the task just use the front most one
639            task = tasks.get(tasks.size() - 1);
640            runningTaskOut.copyFrom(task);
641        }
642
643        // Get the transform for the running task
644        stackView.getScroller().setStackScrollToInitialState();
645        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
646                stackView.getScroller().getStackScroll(), mTmpTransform, null);
647        return mTmpTransform;
648    }
649
650    /**
651     * Draws the header of a task used for the window animation into a bitmap.
652     */
653    private Bitmap drawThumbnailTransitionBitmap(Task toTask, TaskViewTransform toTransform) {
654        if (toTransform != null && toTask.key != null) {
655            Bitmap thumbnail;
656            synchronized (mHeaderBarLock) {
657                int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale);
658                int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale);
659                thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight,
660                        Bitmap.Config.ARGB_8888);
661                if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
662                    thumbnail.eraseColor(0xFFff0000);
663                } else {
664                    Canvas c = new Canvas(thumbnail);
665                    c.scale(toTransform.scale, toTransform.scale);
666                    mHeaderBar.rebindToTask(toTask);
667                    mHeaderBar.draw(c);
668                    c.setBitmap(null);
669                }
670            }
671            return thumbnail.createAshmemBitmap();
672        }
673        return null;
674    }
675
676    /**
677     * Shows the recents activity
678     */
679    private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
680            boolean isTopTaskHome) {
681        SystemServicesProxy ssp = Recents.getSystemServices();
682        RecentsTaskLoader loader = Recents.getTaskLoader();
683
684        // Update the header bar if necessary
685        reloadHeaderBarLayout(false /* tryAndBindSearchWidget */);
686
687        if (sInstanceLoadPlan == null) {
688            // Create a new load plan if onPreloadRecents() was never triggered
689            sInstanceLoadPlan = loader.createLoadPlan(mContext);
690        }
691
692        if (!sInstanceLoadPlan.hasTasks()) {
693            loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
694        }
695        TaskStack stack = sInstanceLoadPlan.getTaskStack();
696
697        // Prepare the dummy stack for the transition
698        mDummyStackView.updateMinMaxScrollForStack(stack);
699        TaskStackViewLayoutAlgorithm.VisibilityReport stackVr =
700                mDummyStackView.computeStackVisibilityReport();
701        boolean hasRecentTasks = stack.getTaskCount() > 0;
702        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
703
704        if (useThumbnailTransition) {
705            // Try starting with a thumbnail transition
706            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack,
707                    mDummyStackView);
708            if (opts != null) {
709                startRecentsActivity(topTask, opts, false /* fromHome */,
710                        false /* fromSearchHome */, true /* fromThumbnail */, stackVr);
711            } else {
712                // Fall through below to the non-thumbnail transition
713                useThumbnailTransition = false;
714            }
715        }
716
717        if (!useThumbnailTransition) {
718            // If there is no thumbnail transition, but is launching from home into recents, then
719            // use a quick home transition and do the animation from home
720            if (!Constants.DebugFlags.App.DisableSearchBar && hasRecentTasks) {
721                String homeActivityPackage = ssp.getHomeActivityPackageName();
722                String searchWidgetPackage = Prefs.getString(mContext,
723                        Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null);
724
725                // Determine whether we are coming from a search owned home activity
726                boolean fromSearchHome = (homeActivityPackage != null) &&
727                        homeActivityPackage.equals(searchWidgetPackage);
728                ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome);
729                startRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome,
730                        false /* fromThumbnail */, stackVr);
731            } else {
732                // Otherwise we do the normal fade from an unknown source
733                ActivityOptions opts = getUnknownTransitionActivityOptions();
734                startRecentsActivity(topTask, opts, true /* fromHome */,
735                        false /* fromSearchHome */, false /* fromThumbnail */, stackVr);
736            }
737        }
738        mLastToggleTime = SystemClock.elapsedRealtime();
739    }
740
741    /**
742     * Starts the recents activity.
743     */
744    private void startRecentsActivity(ActivityManager.RunningTaskInfo topTask,
745              ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail,
746              TaskStackViewLayoutAlgorithm.VisibilityReport vr) {
747        mStartAnimationTriggered = false;
748
749        // Update the configuration based on the launch options
750        RecentsActivityLaunchState launchState = mConfig.getLaunchState();
751        launchState.launchedFromHome = fromSearchHome || fromHome;
752        launchState.launchedFromSearchHome = fromSearchHome;
753        launchState.launchedFromAppWithThumbnail = fromThumbnail;
754        launchState.launchedToTaskId = (topTask != null) ? topTask.id : -1;
755        launchState.launchedWithAltTab = mTriggeredFromAltTab;
756        launchState.launchedReuseTaskStackViews = mCanReuseTaskStackViews;
757        launchState.launchedNumVisibleTasks = vr.numVisibleTasks;
758        launchState.launchedNumVisibleThumbnails = vr.numVisibleThumbnails;
759        launchState.launchedHasConfigurationChanged = false;
760
761        Intent intent = new Intent();
762        intent.setClassName(RECENTS_PACKAGE, RECENTS_ACTIVITY);
763        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
764                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
765                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
766        if (opts != null) {
767            mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
768        } else {
769            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
770        }
771        mCanReuseTaskStackViews = true;
772    }
773
774    /**** OnAnimationStartedListener Implementation ****/
775
776    @Override
777    public void onAnimationStarted() {
778        // Notify recents to start the enter animation
779        if (!mStartAnimationTriggered) {
780            mStartAnimationTriggered = true;
781            EventBus.getDefault().post(new EnterRecentsWindowAnimationStartedEvent());
782        }
783    }
784}
785