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