AlternateRecentsComponent.java revision 98e8f7ac5e75a47abaa0061aab343b9f06770258
1/*
2 * Copyright (C) 2014 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.Activity;
20import android.app.ActivityManager;
21import android.app.ActivityOptions;
22import android.app.ITaskStackListener;
23import android.appwidget.AppWidgetHost;
24import android.appwidget.AppWidgetProviderInfo;
25import android.content.ActivityNotFoundException;
26import android.content.BroadcastReceiver;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.content.res.Configuration;
32import android.content.res.Resources;
33import android.graphics.Bitmap;
34import android.graphics.Canvas;
35import android.graphics.Rect;
36import android.os.Handler;
37import android.os.SystemClock;
38import android.os.UserHandle;
39import android.util.Pair;
40import android.view.LayoutInflater;
41import android.view.View;
42import com.android.systemui.R;
43import com.android.systemui.RecentsComponent;
44import com.android.systemui.recents.misc.Console;
45import com.android.systemui.recents.misc.SystemServicesProxy;
46import com.android.systemui.recents.model.RecentsTaskLoadPlan;
47import com.android.systemui.recents.model.RecentsTaskLoader;
48import com.android.systemui.recents.model.Task;
49import com.android.systemui.recents.model.TaskGrouping;
50import com.android.systemui.recents.model.TaskStack;
51import com.android.systemui.recents.views.TaskStackView;
52import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm;
53import com.android.systemui.recents.views.TaskViewHeader;
54import com.android.systemui.recents.views.TaskViewTransform;
55
56import java.util.ArrayList;
57import java.util.List;
58import java.util.concurrent.atomic.AtomicBoolean;
59
60/**
61 * Annotation for a method that is only called from the primary user's SystemUI process and will be
62 * proxied to the current user.
63 */
64@interface ProxyFromPrimaryToCurrentUser {}
65/**
66 * Annotation for a method that may be called from any user's SystemUI process and will be proxied
67 * to the primary user.
68 */
69@interface ProxyFromAnyToPrimaryUser {}
70
71/** A proxy implementation for the recents component */
72public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener {
73
74    final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "triggeredFromAltTab";
75    final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "triggeredFromHomeKey";
76    final public static String EXTRA_RECENTS_VISIBILITY = "recentsVisibility";
77
78    // Owner proxy events
79    final public static String ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER =
80            "action_notify_recents_visibility_change";
81
82    final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation";
83    final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity";
84    final public static String ACTION_HIDE_RECENTS_ACTIVITY = "action_hide_recents_activity";
85
86    final static int sMinToggleDelay = 350;
87
88    final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
89    final static String sRecentsPackage = "com.android.systemui";
90    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
91
92    /**
93     * An implementation of ITaskStackListener, that allows us to listen for changes to the system
94     * task stacks and update recents accordingly.
95     */
96    class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable {
97        Handler mHandler;
98
99        public TaskStackListenerImpl(Handler handler) {
100            mHandler = handler;
101        }
102
103        @Override
104        public void onTaskStackChanged() {
105            // Debounce any task stack changes
106            mHandler.removeCallbacks(this);
107            mHandler.post(this);
108        }
109
110        /** Preloads the next task */
111        public void run() {
112            RecentsConfiguration config = RecentsConfiguration.getInstance();
113            if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) {
114                ActivityManager.RunningTaskInfo runningTaskInfo = getTopMostTask();
115
116                // Load the next task only if we aren't svelte
117                RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
118                RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
119                loader.preloadTasks(plan, true /* isTopTaskHome */);
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     * A proxy for Recents events which happens strictly for the owner.
138     */
139    class RecentsOwnerEventProxyReceiver extends BroadcastReceiver {
140        @Override
141        public void onReceive(Context context, Intent intent) {
142            switch (intent.getAction()) {
143                case ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER:
144                    visibilityChanged(intent.getBooleanExtra(EXTRA_RECENTS_VISIBILITY, false));
145                    break;
146            }
147        }
148    }
149
150    static RecentsComponent.Callbacks sRecentsComponentCallbacks;
151    static RecentsTaskLoadPlan sInstanceLoadPlan;
152
153    Context mContext;
154    LayoutInflater mInflater;
155    SystemServicesProxy mSystemServicesProxy;
156    Handler mHandler;
157    TaskStackListenerImpl mTaskStackListener;
158    RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver;
159    boolean mBootCompleted;
160    boolean mStartAnimationTriggered;
161    boolean mCanReuseTaskStackViews = true;
162
163    // Task launching
164    RecentsConfiguration mConfig;
165    Rect mWindowRect = new Rect();
166    Rect mTaskStackBounds = new Rect();
167    Rect mSystemInsets = new Rect();
168    TaskViewTransform mTmpTransform = new TaskViewTransform();
169    int mStatusBarHeight;
170    int mNavBarHeight;
171    int mNavBarWidth;
172
173    // Header (for transition)
174    TaskViewHeader mHeaderBar;
175    TaskStackView mDummyStackView;
176
177    // Variables to keep track of if we need to start recents after binding
178    boolean mTriggeredFromAltTab;
179    long mLastToggleTime;
180
181    public AlternateRecentsComponent(Context context) {
182        RecentsTaskLoader.initialize(context);
183        mInflater = LayoutInflater.from(context);
184        mContext = context;
185        mSystemServicesProxy = new SystemServicesProxy(context);
186        mHandler = new Handler();
187        mTaskStackBounds = new Rect();
188
189        // Register the task stack listener
190        mTaskStackListener = new TaskStackListenerImpl(mHandler);
191        mSystemServicesProxy.registerTaskStackListener(mTaskStackListener);
192
193        // Only the owner has the callback to update the SysUI visibility flags, so all non-owner
194        // instances of AlternateRecentsComponent needs to notify the owner when the visibility
195        // changes.
196        if (mSystemServicesProxy.isForegroundUserOwner()) {
197            mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver();
198            IntentFilter filter = new IntentFilter();
199            filter.addAction(AlternateRecentsComponent.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER);
200            mContext.registerReceiverAsUser(mProxyBroadcastReceiver, UserHandle.CURRENT, filter,
201                    null, mHandler);
202        }
203    }
204
205    /** Creates a new broadcast intent */
206    static Intent createLocalBroadcastIntent(Context context, String action) {
207        Intent intent = new Intent(action);
208        intent.setPackage(context.getPackageName());
209        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
210                Intent.FLAG_RECEIVER_FOREGROUND);
211        return intent;
212    }
213
214    /** Initializes the Recents. */
215    @ProxyFromPrimaryToCurrentUser
216    public void onStart() {
217        // Initialize some static datastructures
218        TaskStackViewLayoutAlgorithm.initializeCurve();
219        // Load the header bar layout
220        reloadHeaderBarLayout(true);
221
222        // When we start, preload the data associated with the previous recent tasks.
223        // We can use a new plan since the caches will be the same.
224        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
225        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
226        loader.preloadTasks(plan, true /* isTopTaskHome */);
227        RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
228        launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize();
229        launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
230        launchOpts.onlyLoadForCache = true;
231        loader.loadTasks(mContext, plan, launchOpts);
232    }
233
234    public void onBootCompleted() {
235        mBootCompleted = true;
236    }
237
238    /** Shows the Recents. */
239    @ProxyFromPrimaryToCurrentUser
240    public void onShowRecents(boolean triggeredFromAltTab) {
241        if (mSystemServicesProxy.isForegroundUserOwner()) {
242            showRecents(triggeredFromAltTab);
243        } else {
244            Intent intent = createLocalBroadcastIntent(mContext,
245                    RecentsUserEventProxyReceiver.ACTION_PROXY_SHOW_RECENTS_TO_USER);
246            intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
247            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
248        }
249    }
250    void showRecents(boolean triggeredFromAltTab) {
251        mTriggeredFromAltTab = triggeredFromAltTab;
252
253        try {
254            startRecentsActivity();
255        } catch (ActivityNotFoundException e) {
256            Console.logRawError("Failed to launch RecentAppsIntent", e);
257        }
258    }
259
260    /** Hides the Recents. */
261    @ProxyFromPrimaryToCurrentUser
262    public void onHideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
263        if (mSystemServicesProxy.isForegroundUserOwner()) {
264            hideRecents(triggeredFromAltTab, triggeredFromHomeKey);
265        } else {
266            Intent intent = createLocalBroadcastIntent(mContext,
267                    RecentsUserEventProxyReceiver.ACTION_PROXY_HIDE_RECENTS_TO_USER);
268            intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
269            intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey);
270            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
271        }
272    }
273    void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
274        if (mBootCompleted) {
275            ActivityManager.RunningTaskInfo topTask = getTopMostTask();
276            if (topTask != null && isRecentsTopMost(topTask, null)) {
277                // Notify recents to hide itself
278                Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY);
279                intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
280                intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey);
281                mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
282            }
283        }
284    }
285
286    /** Toggles the Recents activity. */
287    @ProxyFromPrimaryToCurrentUser
288    public void onToggleRecents() {
289        if (mSystemServicesProxy.isForegroundUserOwner()) {
290            toggleRecents();
291        } else {
292            Intent intent = createLocalBroadcastIntent(mContext,
293                    RecentsUserEventProxyReceiver.ACTION_PROXY_TOGGLE_RECENTS_TO_USER);
294            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
295        }
296    }
297    void toggleRecents() {
298        mTriggeredFromAltTab = false;
299
300        try {
301            toggleRecentsActivity();
302        } catch (ActivityNotFoundException e) {
303            Console.logRawError("Failed to launch RecentAppsIntent", e);
304        }
305    }
306
307    /** Preloads info for the Recents activity. */
308    @ProxyFromPrimaryToCurrentUser
309    public void onPreloadRecents() {
310        if (mSystemServicesProxy.isForegroundUserOwner()) {
311            preloadRecents();
312        } else {
313            Intent intent = createLocalBroadcastIntent(mContext,
314                    RecentsUserEventProxyReceiver.ACTION_PROXY_PRELOAD_RECENTS_TO_USER);
315            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
316        }
317    }
318    void preloadRecents() {
319        // Preload only the raw task list into a new load plan (which will be consumed by the
320        // RecentsActivity)
321        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
322        sInstanceLoadPlan = loader.createLoadPlan(mContext);
323        sInstanceLoadPlan.preloadRawTasks(true);
324    }
325
326    public void onCancelPreloadingRecents() {
327        // Do nothing
328    }
329
330    void showRelativeAffiliatedTask(boolean showNextTask) {
331        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
332        RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
333        loader.preloadTasks(plan, true /* isTopTaskHome */);
334        TaskStack stack = plan.getTaskStack();
335
336        // Return early if there are no tasks
337        if (stack.getTaskCount() == 0) return;
338
339        ActivityManager.RunningTaskInfo runningTask = getTopMostTask();
340        // Return early if there is no running task (can't determine affiliated tasks in this case)
341        if (runningTask == null) return;
342        // Return early if the running task is in the home stack (optimization)
343        if (mSystemServicesProxy.isInHomeStack(runningTask.id)) return;
344
345        // Find the task in the recents list
346        ArrayList<Task> tasks = stack.getTasks();
347        Task toTask = null;
348        ActivityOptions launchOpts = null;
349        int taskCount = tasks.size();
350        int numAffiliatedTasks = 0;
351        for (int i = 0; i < taskCount; i++) {
352            Task task = tasks.get(i);
353            if (task.key.id == runningTask.id) {
354                TaskGrouping group = task.group;
355                Task.TaskKey toTaskKey;
356                if (showNextTask) {
357                    toTaskKey = group.getNextTaskInGroup(task);
358                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
359                            R.anim.recents_launch_next_affiliated_task_target,
360                            R.anim.recents_launch_next_affiliated_task_source);
361                } else {
362                    toTaskKey = group.getPrevTaskInGroup(task);
363                    launchOpts = ActivityOptions.makeCustomAnimation(mContext,
364                            R.anim.recents_launch_prev_affiliated_task_target,
365                            R.anim.recents_launch_prev_affiliated_task_source);
366                }
367                if (toTaskKey != null) {
368                    toTask = stack.findTaskWithId(toTaskKey.id);
369                }
370                numAffiliatedTasks = group.getTaskCount();
371                break;
372            }
373        }
374
375        // Return early if there is no next task
376        if (toTask == null) {
377            if (numAffiliatedTasks > 1) {
378                if (showNextTask) {
379                    mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication(
380                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
381                                    R.anim.recents_launch_next_affiliated_task_bounce));
382                } else {
383                    mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication(
384                            ActivityOptions.makeCustomInPlaceAnimation(mContext,
385                                    R.anim.recents_launch_prev_affiliated_task_bounce));
386                }
387            }
388            return;
389        }
390
391        // Launch the task
392        if (toTask.isActive) {
393            // Bring an active task to the foreground
394            mSystemServicesProxy.moveTaskToFront(toTask.key.id, launchOpts);
395        } else {
396            mSystemServicesProxy.startActivityFromRecents(mContext, toTask.key.id,
397                    toTask.activityLabel, launchOpts);
398        }
399    }
400
401    public void onShowNextAffiliatedTask() {
402        showRelativeAffiliatedTask(true);
403    }
404
405    public void onShowPrevAffiliatedTask() {
406        showRelativeAffiliatedTask(false);
407    }
408
409    /** Updates on configuration change. */
410    @ProxyFromPrimaryToCurrentUser
411    public void onConfigurationChanged(Configuration newConfig) {
412        if (mSystemServicesProxy.isForegroundUserOwner()) {
413            configurationChanged();
414        } else {
415            Intent intent = createLocalBroadcastIntent(mContext,
416                    RecentsUserEventProxyReceiver.ACTION_PROXY_CONFIG_CHANGE_TO_USER);
417            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
418        }
419    }
420    void configurationChanged() {
421        // Don't reuse task stack views if the configuration changes
422        mCanReuseTaskStackViews = false;
423        // Reload the header bar layout
424        reloadHeaderBarLayout(false);
425    }
426
427    /** Prepares the header bar layout. */
428    void reloadHeaderBarLayout(boolean reloadWidget) {
429        Resources res = mContext.getResources();
430        mWindowRect = mSystemServicesProxy.getWindowRect();
431        mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
432        mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height);
433        mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width);
434        mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy);
435        mConfig.updateOnConfigurationChange();
436        if (reloadWidget) {
437            // Reload the widget id before we get the task stack bounds
438            reloadSearchBarAppWidget(mContext, mSystemServicesProxy);
439        }
440        mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mStatusBarHeight,
441                (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), mTaskStackBounds);
442        if (mConfig.isLandscape && mConfig.hasTransposedNavBar) {
443            mSystemInsets.set(0, mStatusBarHeight, mNavBarWidth, 0);
444        } else {
445            mSystemInsets.set(0, mStatusBarHeight, 0, mNavBarHeight);
446        }
447
448        // Inflate the header bar layout so that we can rebind and draw it for the transition
449        TaskStack stack = new TaskStack();
450        mDummyStackView = new TaskStackView(mContext, stack);
451        TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm();
452        Rect taskStackBounds = new Rect(mTaskStackBounds);
453        taskStackBounds.bottom -= mSystemInsets.bottom;
454        algo.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds);
455        Rect taskViewSize = algo.getUntransformedTaskViewSize();
456        int taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height);
457        mHeaderBar = (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null,
458                false);
459        mHeaderBar.measure(
460                View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY),
461                View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY));
462        mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight);
463    }
464
465    /** Prepares the search bar app widget */
466    void reloadSearchBarAppWidget(Context context, SystemServicesProxy ssp) {
467        // Try and pre-emptively bind the search widget on startup to ensure that we
468        // have the right thumbnail bounds to animate to.
469        if (Constants.DebugFlags.App.EnableSearchLayout) {
470            // If there is no id, then bind a new search app widget
471            if (mConfig.searchBarAppWidgetId < 0) {
472                AppWidgetHost host = new RecentsAppWidgetHost(context,
473                        Constants.Values.App.AppWidgetHostId);
474                Pair<Integer, AppWidgetProviderInfo> widgetInfo = ssp.bindSearchAppWidget(host);
475                if (widgetInfo != null) {
476                    // Save the app widget id into the settings
477                    mConfig.updateSearchBarAppWidgetId(context, widgetInfo.first);
478                }
479            }
480        }
481    }
482
483    /** Gets the top task. */
484    ActivityManager.RunningTaskInfo getTopMostTask() {
485        SystemServicesProxy ssp = mSystemServicesProxy;
486        List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1);
487        if (!tasks.isEmpty()) {
488            return tasks.get(0);
489        }
490        return null;
491    }
492
493    /** Returns whether the recents is currently running */
494    boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, AtomicBoolean isHomeTopMost) {
495        SystemServicesProxy ssp = mSystemServicesProxy;
496        if (topTask != null) {
497            ComponentName topActivity = topTask.topActivity;
498
499            // Check if the front most activity is recents
500            if (topActivity.getPackageName().equals(sRecentsPackage) &&
501                    topActivity.getClassName().equals(sRecentsActivity)) {
502                if (isHomeTopMost != null) {
503                    isHomeTopMost.set(false);
504                }
505                return true;
506            }
507
508            if (isHomeTopMost != null) {
509                isHomeTopMost.set(ssp.isInHomeStack(topTask.id));
510            }
511        }
512        return false;
513    }
514
515    /** Toggles the recents activity */
516    void toggleRecentsActivity() {
517        // If the user has toggled it too quickly, then just eat up the event here (it's better than
518        // showing a janky screenshot).
519        // NOTE: Ideally, the screenshot mechanism would take the window transform into account
520        if ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) {
521            return;
522        }
523
524        // If Recents is the front most activity, then we should just communicate with it directly
525        // to launch the first task or dismiss itself
526        ActivityManager.RunningTaskInfo topTask = getTopMostTask();
527        AtomicBoolean isTopTaskHome = new AtomicBoolean(true);
528        if (topTask != null && isRecentsTopMost(topTask, isTopTaskHome)) {
529            // Notify recents to toggle itself
530            Intent intent = createLocalBroadcastIntent(mContext, ACTION_TOGGLE_RECENTS_ACTIVITY);
531            mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
532            mLastToggleTime = SystemClock.elapsedRealtime();
533            return;
534        } else {
535            // Otherwise, start the recents activity
536            startRecentsActivity(topTask, isTopTaskHome.get());
537        }
538    }
539
540    /** Starts the recents activity if it is not already running */
541    void startRecentsActivity() {
542        // Check if the top task is in the home stack, and start the recents activity
543        ActivityManager.RunningTaskInfo topTask = getTopMostTask();
544        AtomicBoolean isTopTaskHome = new AtomicBoolean(true);
545        if (topTask == null || !isRecentsTopMost(topTask, isTopTaskHome)) {
546            startRecentsActivity(topTask, isTopTaskHome.get());
547        }
548    }
549
550    /**
551     * Creates the activity options for a unknown state->recents transition.
552     */
553    ActivityOptions getUnknownTransitionActivityOptions() {
554        mStartAnimationTriggered = false;
555        return ActivityOptions.makeCustomAnimation(mContext,
556                R.anim.recents_from_unknown_enter,
557                R.anim.recents_from_unknown_exit,
558                mHandler, this);
559    }
560
561    /**
562     * Creates the activity options for a home->recents transition.
563     */
564    ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) {
565        mStartAnimationTriggered = false;
566        if (fromSearchHome) {
567            return ActivityOptions.makeCustomAnimation(mContext,
568                    R.anim.recents_from_search_launcher_enter,
569                    R.anim.recents_from_search_launcher_exit,
570                    mHandler, this);
571        }
572        return ActivityOptions.makeCustomAnimation(mContext,
573                R.anim.recents_from_launcher_enter,
574                R.anim.recents_from_launcher_exit,
575                mHandler, this);
576    }
577
578    /**
579     * Creates the activity options for an app->recents transition.
580     */
581    ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask,
582            TaskStack stack, TaskStackView stackView) {
583        // Update the destination rect
584        Task toTask = new Task();
585        TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView,
586                topTask.id, toTask);
587        if (toTransform != null && toTask.key != null) {
588            Rect toTaskRect = toTransform.rect;
589            int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale);
590            int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale);
591            Bitmap thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight,
592                    Bitmap.Config.ARGB_8888);
593            if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
594                thumbnail.eraseColor(0xFFff0000);
595            } else {
596                Canvas c = new Canvas(thumbnail);
597                c.scale(toTransform.scale, toTransform.scale);
598                mHeaderBar.rebindToTask(toTask);
599                mHeaderBar.draw(c);
600                c.setBitmap(null);
601            }
602
603            mStartAnimationTriggered = false;
604            return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView,
605                    thumbnail, toTaskRect.left, toTaskRect.top, toTaskRect.width(),
606                    toTaskRect.height(), mHandler, this);
607        }
608
609        // If both the screenshot and thumbnail fails, then just fall back to the default transition
610        return getUnknownTransitionActivityOptions();
611    }
612
613    /** Returns the transition rect for the given task id. */
614    TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView,
615            int runningTaskId, Task runningTaskOut) {
616        // Find the running task in the TaskStack
617        Task task = null;
618        ArrayList<Task> tasks = stack.getTasks();
619        if (runningTaskId != -1) {
620            // Otherwise, try and find the task with the
621            int taskCount = tasks.size();
622            for (int i = taskCount - 1; i >= 0; i--) {
623                Task t = tasks.get(i);
624                if (t.key.id == runningTaskId) {
625                    task = t;
626                    runningTaskOut.copyFrom(t);
627                    break;
628                }
629            }
630        }
631        if (task == null) {
632            // If no task is specified or we can not find the task just use the front most one
633            task = tasks.get(tasks.size() - 1);
634        }
635
636        // Get the transform for the running task
637        stackView.getScroller().setStackScrollToInitialState();
638        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
639                stackView.getScroller().getStackScroll(), mTmpTransform, null);
640        return mTmpTransform;
641    }
642
643    /** Starts the recents activity */
644    void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) {
645        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
646        RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy);
647
648        if (sInstanceLoadPlan == null) {
649            // Create a new load plan if onPreloadRecents() was never triggered
650            sInstanceLoadPlan = loader.createLoadPlan(mContext);
651        }
652        loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
653        TaskStack stack = sInstanceLoadPlan.getTaskStack();
654
655        // Prepare the dummy stack for the transition
656        mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome);
657        TaskStackViewLayoutAlgorithm.VisibilityReport stackVr =
658                mDummyStackView.computeStackVisibilityReport();
659        boolean hasRecentTasks = stack.getTaskCount() > 0;
660        boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks;
661
662        if (useThumbnailTransition) {
663            // Ensure that we load the running task's icon
664            RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
665            launchOpts.runningTaskId = topTask.id;
666            launchOpts.loadThumbnails = false;
667            launchOpts.onlyLoadForCache = true;
668            loader.loadTasks(mContext, sInstanceLoadPlan, launchOpts);
669
670            // Try starting with a thumbnail transition
671            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack,
672                    mDummyStackView);
673            if (opts != null) {
674                startAlternateRecentsActivity(topTask, opts, false /* fromHome */,
675                        false /* fromSearchHome */, true /* fromThumbnail */, stackVr);
676            } else {
677                // Fall through below to the non-thumbnail transition
678                useThumbnailTransition = false;
679            }
680        }
681
682        if (!useThumbnailTransition) {
683            // If there is no thumbnail transition, but is launching from home into recents, then
684            // use a quick home transition and do the animation from home
685            if (hasRecentTasks) {
686                // Get the home activity info
687                String homeActivityPackage = mSystemServicesProxy.getHomeActivityPackageName();
688                // Get the search widget info
689                AppWidgetProviderInfo searchWidget = null;
690                String searchWidgetPackage = null;
691                if (mConfig.hasSearchBarAppWidget()) {
692                    searchWidget = mSystemServicesProxy.getAppWidgetInfo(
693                            mConfig.searchBarAppWidgetId);
694                } else {
695                    searchWidget = mSystemServicesProxy.resolveSearchAppWidget();
696                }
697                if (searchWidget != null && searchWidget.provider != null) {
698                    searchWidgetPackage = searchWidget.provider.getPackageName();
699                }
700                // Determine whether we are coming from a search owned home activity
701                boolean fromSearchHome = false;
702                if (homeActivityPackage != null && searchWidgetPackage != null &&
703                        homeActivityPackage.equals(searchWidgetPackage)) {
704                    fromSearchHome = true;
705                }
706
707                ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome);
708                startAlternateRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome,
709                        false /* fromThumbnail */, stackVr);
710            } else {
711                // Otherwise we do the normal fade from an unknown source
712                ActivityOptions opts = getUnknownTransitionActivityOptions();
713                startAlternateRecentsActivity(topTask, opts, true /* fromHome */,
714                        false /* fromSearchHome */, false /* fromThumbnail */, stackVr);
715            }
716        }
717        mLastToggleTime = SystemClock.elapsedRealtime();
718    }
719
720    /** Starts the recents activity */
721    void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask,
722            ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail,
723            TaskStackViewLayoutAlgorithm.VisibilityReport vr) {
724        // Update the configuration based on the launch options
725        mConfig.launchedFromHome = fromSearchHome || fromHome;
726        mConfig.launchedFromSearchHome = fromSearchHome;
727        mConfig.launchedFromAppWithThumbnail = fromThumbnail;
728        mConfig.launchedToTaskId = (topTask != null) ? topTask.id : -1;
729        mConfig.launchedWithAltTab = mTriggeredFromAltTab;
730        mConfig.launchedReuseTaskStackViews = mCanReuseTaskStackViews;
731        mConfig.launchedNumVisibleTasks = vr.numVisibleTasks;
732        mConfig.launchedNumVisibleThumbnails = vr.numVisibleThumbnails;
733        mConfig.launchedHasConfigurationChanged = false;
734
735        Intent intent = new Intent(sToggleRecentsAction);
736        intent.setClassName(sRecentsPackage, sRecentsActivity);
737        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
738                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
739                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
740        if (opts != null) {
741            mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT);
742        } else {
743            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
744        }
745        mCanReuseTaskStackViews = true;
746    }
747
748    /** Sets the RecentsComponent callbacks. */
749    public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) {
750        sRecentsComponentCallbacks = cb;
751    }
752
753    /** Notifies the callbacks that the visibility of Recents has changed. */
754    @ProxyFromAnyToPrimaryUser
755    public static void notifyVisibilityChanged(Context context, SystemServicesProxy ssp,
756            boolean visible) {
757        if (ssp.isForegroundUserOwner()) {
758            visibilityChanged(visible);
759        } else {
760            Intent intent = createLocalBroadcastIntent(context,
761                    ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER);
762            intent.putExtra(EXTRA_RECENTS_VISIBILITY, visible);
763            context.sendBroadcastAsUser(intent, UserHandle.OWNER);
764        }
765    }
766    static void visibilityChanged(boolean visible) {
767        if (sRecentsComponentCallbacks != null) {
768            sRecentsComponentCallbacks.onVisibilityChanged(visible);
769        }
770    }
771
772    /**
773     * Returns the preloaded load plan and invalidates it.
774     */
775    public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
776        RecentsTaskLoadPlan plan = sInstanceLoadPlan;
777        sInstanceLoadPlan = null;
778        return plan;
779    }
780
781    /**** OnAnimationStartedListener Implementation ****/
782
783    @Override
784    public void onAnimationStarted() {
785        // Notify recents to start the enter animation
786        if (!mStartAnimationTriggered) {
787            // There can be a race condition between the start animation callback and
788            // the start of the new activity (where we register the receiver that listens
789            // to this broadcast, so we add our own receiver and if that gets called, then
790            // we know the activity has not yet started and we can retry sending the broadcast.
791            BroadcastReceiver fallbackReceiver = new BroadcastReceiver() {
792                @Override
793                public void onReceive(Context context, Intent intent) {
794                    if (getResultCode() == Activity.RESULT_OK) {
795                        mStartAnimationTriggered = true;
796                        return;
797                    }
798
799                    // Schedule for the broadcast to be sent again after some time
800                    mHandler.postDelayed(new Runnable() {
801                        @Override
802                        public void run() {
803                            onAnimationStarted();
804                        }
805                    }, 25);
806                }
807            };
808
809            // Send the broadcast to notify Recents that the animation has started
810            Intent intent = createLocalBroadcastIntent(mContext, ACTION_START_ENTER_ANIMATION);
811            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
812                    fallbackReceiver, null, Activity.RESULT_CANCELED, null, null);
813        }
814    }
815}
816