AlternateRecentsComponent.java revision 083baf99ff1228e96ede96aac88c8200c4fdc2b2
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.ActivityManager;
20import android.app.ActivityOptions;
21import android.content.ActivityNotFoundException;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Configuration;
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Rect;
29import android.os.Handler;
30import android.os.UserHandle;
31import android.view.View;
32import com.android.systemui.R;
33import com.android.systemui.RecentsComponent;
34import com.android.systemui.recents.misc.Console;
35import com.android.systemui.recents.misc.SystemServicesProxy;
36import com.android.systemui.recents.model.RecentsTaskLoader;
37import com.android.systemui.recents.model.Task;
38import com.android.systemui.recents.model.TaskStack;
39import com.android.systemui.recents.views.TaskStackView;
40import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm;
41import com.android.systemui.recents.views.TaskViewTransform;
42
43import java.util.ArrayList;
44import java.util.List;
45import java.util.concurrent.atomic.AtomicBoolean;
46
47/** A proxy implementation for the recents component */
48public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener {
49
50    final public static String EXTRA_FROM_HOME = "recents.triggeredOverHome";
51    final public static String EXTRA_FROM_APP_THUMBNAIL = "recents.animatingWithThumbnail";
52    final public static String EXTRA_FROM_APP_FULL_SCREENSHOT = "recents.thumbnail";
53    final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab";
54    final public static String EXTRA_TRIGGERED_FROM_TASK_ID = "recents.activeTaskId";
55
56    final static int sMinToggleDelay = 425;
57
58    final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
59    final static String sRecentsPackage = "com.android.systemui";
60    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
61
62    static Bitmap sLastScreenshot;
63    static RecentsComponent.Callbacks sRecentsComponentCallbacks;
64
65    Context mContext;
66    SystemServicesProxy mSystemServicesProxy;
67
68    // Recents service binding
69    Handler mHandler;
70    boolean mBootCompleted = false;
71
72    // Task launching
73    RecentsConfiguration mConfig;
74    Rect mWindowRect;
75    Rect mTaskStackBounds;
76    TaskViewTransform mTmpTransform = new TaskViewTransform();
77    int mStatusBarHeight;
78
79    // Variables to keep track of if we need to start recents after binding
80    View mStatusBarView;
81    boolean mTriggeredFromAltTab;
82    long mLastToggleTime;
83
84    public AlternateRecentsComponent(Context context) {
85        mContext = context;
86        mSystemServicesProxy = new SystemServicesProxy(context);
87        mHandler = new Handler();
88        mConfig = RecentsConfiguration.reinitialize(context);
89        mWindowRect = mSystemServicesProxy.getWindowRect();
90        mTaskStackBounds = new Rect();
91        mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mTaskStackBounds);
92        mStatusBarHeight = mContext.getResources().getDimensionPixelSize(
93                com.android.internal.R.dimen.status_bar_height);
94    }
95
96    public void onStart() {
97        if (Console.Enabled) {
98            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|start]");
99        }
100    }
101
102    public void onBootCompleted() {
103        mBootCompleted = true;
104    }
105
106    /** Shows the recents */
107    public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) {
108        if (Console.Enabled) {
109            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|showRecents]");
110        }
111        mStatusBarView = statusBarView;
112        mTriggeredFromAltTab = triggeredFromAltTab;
113
114        try {
115            startRecentsActivity();
116        } catch (ActivityNotFoundException e) {
117            Console.logRawError("Failed to launch RecentAppsIntent", e);
118        }
119    }
120
121    /** Hides the recents */
122    public void onHideRecents(boolean triggeredFromAltTab) {
123        if (Console.Enabled) {
124            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]");
125        }
126
127        if (mBootCompleted) {
128            if (isRecentsTopMost(getTopMostTask(), null)) {
129                // Notify recents to hide itself
130                Intent intent = new Intent(RecentsActivity.ACTION_HIDE_RECENTS_ACTIVITY);
131                intent.setPackage(mContext.getPackageName());
132                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
133                intent.putExtra(RecentsActivity.EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab);
134                mContext.sendBroadcast(intent);
135            }
136        }
137    }
138
139    /** Toggles the alternate recents activity */
140    public void onToggleRecents(View statusBarView) {
141        if (Console.Enabled) {
142            Console.logStartTracingTime(Constants.Log.App.TimeRecentsStartup,
143                    Constants.Log.App.TimeRecentsStartupKey);
144            Console.logStartTracingTime(Constants.Log.App.TimeRecentsLaunchTask,
145                    Constants.Log.App.TimeRecentsLaunchKey);
146            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|toggleRecents]", "");
147        }
148        mStatusBarView = statusBarView;
149        mTriggeredFromAltTab = false;
150
151        try {
152            toggleRecentsActivity();
153        } catch (ActivityNotFoundException e) {
154            Console.logRawError("Failed to launch RecentAppsIntent", e);
155        }
156    }
157
158    public void onPreloadRecents() {
159        // Do nothing
160    }
161
162    public void onCancelPreloadingRecents() {
163        // Do nothing
164    }
165
166    public void onConfigurationChanged(Configuration newConfig) {
167        mConfig = RecentsConfiguration.reinitialize(mContext);
168        mConfig.updateOnConfigurationChange();
169        mWindowRect = mSystemServicesProxy.getWindowRect();
170        mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mTaskStackBounds);
171        sLastScreenshot = null;
172    }
173
174    /** Gets the top task. */
175    ActivityManager.RunningTaskInfo getTopMostTask() {
176        SystemServicesProxy ssp = mSystemServicesProxy;
177        List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1);
178        if (!tasks.isEmpty()) {
179            return tasks.get(0);
180        }
181        return null;
182    }
183
184    /** Returns whether the recents is currently running */
185    boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, AtomicBoolean isHomeTopMost) {
186        SystemServicesProxy ssp = mSystemServicesProxy;
187        if (topTask != null) {
188            ComponentName topActivity = topTask.topActivity;
189
190            // Check if the front most activity is recents
191            if (topActivity.getPackageName().equals(sRecentsPackage) &&
192                    topActivity.getClassName().equals(sRecentsActivity)) {
193                if (isHomeTopMost != null) {
194                    isHomeTopMost.set(false);
195                }
196                return true;
197            }
198
199            if (isHomeTopMost != null) {
200                isHomeTopMost.set(ssp.isInHomeStack(topTask.id));
201            }
202        }
203        return false;
204    }
205
206    /** Toggles the recents activity */
207    void toggleRecentsActivity() {
208        // If the user has toggled it too quickly, then just eat up the event here (it's better than
209        // showing a janky screenshot).
210        // NOTE: Ideally, the screenshot mechanism would take the window transform into account
211        if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) {
212            return;
213        }
214
215        // If Recents is the front most activity, then we should just communicate with it directly
216        // to launch the first task or dismiss itself
217        ActivityManager.RunningTaskInfo topTask = getTopMostTask();
218        AtomicBoolean isTopTaskHome = new AtomicBoolean();
219        if (isRecentsTopMost(topTask, isTopTaskHome)) {
220            // Notify recents to toggle itself
221            Intent intent = new Intent(RecentsActivity.ACTION_TOGGLE_RECENTS_ACTIVITY);
222            intent.setPackage(mContext.getPackageName());
223            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
224            mContext.sendBroadcast(intent);
225
226            // Time this path
227            if (Console.Enabled) {
228                Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
229                        Constants.Log.App.TimeRecentsStartupKey, "receivedToggleRecents");
230                Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
231                        Constants.Log.App.TimeRecentsLaunchKey, "receivedToggleRecents");
232            }
233            mLastToggleTime = System.currentTimeMillis();
234            return;
235        } else {
236            // Otherwise, start the recents activity
237            startRecentsActivity(topTask, isTopTaskHome.get());
238        }
239    }
240
241    /** Starts the recents activity if it is not already running */
242    void startRecentsActivity() {
243        // Check if the top task is in the home stack, and start the recents activity
244        ActivityManager.RunningTaskInfo topTask = getTopMostTask();
245        AtomicBoolean isTopTaskHome = new AtomicBoolean();
246        if (!isRecentsTopMost(topTask, isTopTaskHome)) {
247            startRecentsActivity(topTask, isTopTaskHome.get());
248        }
249    }
250
251    /**
252     * Creates the activity options for a unknown state->recents transition.
253     */
254    ActivityOptions getUnknownTransitionActivityOptions() {
255        return ActivityOptions.makeCustomAnimation(mContext,
256                R.anim.recents_from_unknown_enter,
257                R.anim.recents_from_unknown_exit, mHandler, this);
258    }
259
260    /**
261     * Creates the activity options for a home->recents transition.
262     */
263    ActivityOptions getHomeTransitionActivityOptions() {
264        return ActivityOptions.makeCustomAnimation(mContext,
265                R.anim.recents_from_launcher_enter,
266                R.anim.recents_from_launcher_exit, mHandler, this);
267    }
268
269    /**
270     * Creates the activity options for an app->recents transition.  If this method sets the static
271     * screenshot, then we will use that for the transition.
272     */
273    ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask) {
274
275        if (Constants.DebugFlags.App.EnableScreenshotAppTransition) {
276            // Recycle the last screenshot
277            consumeLastScreenshot();
278
279            // Take the full screenshot
280            sLastScreenshot = mSystemServicesProxy.takeAppScreenshot();
281            if (sLastScreenshot != null) {
282                return ActivityOptions.makeCustomAnimation(mContext,
283                        R.anim.recents_from_app_enter,
284                        R.anim.recents_from_app_exit, mHandler, this);
285            }
286        }
287
288        // If the screenshot fails, then load the first task thumbnail and use that
289        Bitmap firstThumbnail = mSystemServicesProxy.getTaskThumbnail(topTask.id);
290        if (firstThumbnail != null) {
291            // Update the destination rect
292            Rect toTaskRect = getThumbnailTransitionRect(topTask.id);
293            if (toTaskRect.width() > 0 && toTaskRect.height() > 0) {
294                // Create the new thumbnail for the animation down
295                // XXX: We should find a way to optimize this so we don't need to create a new bitmap
296                Bitmap thumbnail = Bitmap.createBitmap(toTaskRect.width(), toTaskRect.height(),
297                        Bitmap.Config.ARGB_8888);
298                int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
299                Canvas c = new Canvas(thumbnail);
300                c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
301                        new Rect(0, 0, toTaskRect.width(), toTaskRect.height()), null);
302                c.setBitmap(null);
303                // Recycle the old thumbnail
304                firstThumbnail.recycle();
305                return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView,
306                        thumbnail, toTaskRect.left, toTaskRect.top, this);
307            }
308        }
309
310        // If both the screenshot and thumbnail fails, then just fall back to the default transition
311        return getUnknownTransitionActivityOptions();
312    }
313
314    /** Returns the transition rect for the given task id. */
315    Rect getThumbnailTransitionRect(int runningTaskId) {
316        // Get the stack of tasks that we are animating into
317        TaskStack stack = RecentsTaskLoader.getShallowTaskStack(mSystemServicesProxy);
318        if (stack.getTaskCount() == 0) {
319            return new Rect();
320        }
321
322        // Get the stack
323        TaskStackView tsv = new TaskStackView(mContext, stack);
324        TaskStackViewLayoutAlgorithm algo = tsv.getStackAlgorithm();
325        tsv.computeRects(mTaskStackBounds.width(), mTaskStackBounds.height() - mStatusBarHeight, 0, 0);
326        tsv.setStackScrollToInitialState();
327
328        // Find the running task in the TaskStack
329        Task task = null;
330        ArrayList<Task> tasks = stack.getTasks();
331        if (runningTaskId != -1) {
332            // Otherwise, try and find the task with the
333            int taskCount = tasks.size();
334            for (int i = taskCount - 1; i >= 0; i--) {
335                Task t = tasks.get(i);
336                if (t.key.id == runningTaskId) {
337                    task = t;
338                    break;
339                }
340            }
341        }
342        if (task == null) {
343            // If no task is specified or we can not find the task just use the front most one
344            task = tasks.get(tasks.size() - 1);
345        }
346
347        // Get the transform for the running task
348        mTmpTransform = algo.getStackTransform(task, tsv.getStackScroll(), mTmpTransform);
349        mTmpTransform.rect.offset(mTaskStackBounds.left, mTaskStackBounds.top);
350        mTmpTransform.rect.offset(0, mStatusBarHeight);
351        return new Rect(mTmpTransform.rect);
352    }
353
354    /** Starts the recents activity */
355    void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) {
356        // If Recents is not the front-most activity and we should animate into it.  If
357        // the activity at the root of the top task stack in the home stack, then we just do a
358        // simple transition.  Otherwise, we animate to the rects defined by the Recents service,
359        // which can differ depending on the number of items in the list.
360        SystemServicesProxy ssp = mSystemServicesProxy;
361        List<ActivityManager.RecentTaskInfo> recentTasks =
362                ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier());
363        boolean useThumbnailTransition = !isTopTaskHome;
364        boolean hasRecentTasks = !recentTasks.isEmpty();
365
366        if (useThumbnailTransition) {
367            // Try starting with a thumbnail transition
368            ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask);
369            if (opts != null) {
370                if (sLastScreenshot != null) {
371                    startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_FULL_SCREENSHOT);
372                } else {
373                    startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_THUMBNAIL);
374                }
375            } else {
376                // Fall through below to the non-thumbnail transition
377                useThumbnailTransition = false;
378            }
379        }
380
381        if (!useThumbnailTransition) {
382            // If there is no thumbnail transition, but is launching from home into recents, then
383            // use a quick home transition and do the animation from home
384            if (hasRecentTasks) {
385                ActivityOptions opts = getHomeTransitionActivityOptions();
386                startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_HOME);
387            } else {
388                // Otherwise we do the normal fade from an unknown source
389                ActivityOptions opts = getUnknownTransitionActivityOptions();
390                startAlternateRecentsActivity(topTask, opts, null);
391            }
392        }
393
394        if (Console.Enabled) {
395            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
396                    Constants.Log.App.TimeRecentsStartupKey, "startRecentsActivity");
397        }
398        mLastToggleTime = System.currentTimeMillis();
399    }
400
401    /** Starts the recents activity */
402    void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask,
403                                       ActivityOptions opts, String extraFlag) {
404        Intent intent = new Intent(sToggleRecentsAction);
405        intent.setClassName(sRecentsPackage, sRecentsActivity);
406        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
407                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
408                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
409        if (extraFlag != null) {
410            intent.putExtra(extraFlag, true);
411        }
412        intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab);
413        intent.putExtra(EXTRA_TRIGGERED_FROM_TASK_ID, (topTask != null) ? topTask.id : -1);
414        if (opts != null) {
415            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
416                    UserHandle.USER_CURRENT));
417        } else {
418            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
419        }
420    }
421
422    /** Returns the last screenshot taken, this will be called by the RecentsActivity. */
423    public static Bitmap getLastScreenshot() {
424        return sLastScreenshot;
425    }
426
427    /** Recycles the last screenshot taken, this will be called by the RecentsActivity. */
428    public static void consumeLastScreenshot() {
429        if (sLastScreenshot != null) {
430            sLastScreenshot.recycle();
431            sLastScreenshot = null;
432        }
433    }
434
435    /** Sets the RecentsComponent callbacks. */
436    public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) {
437        sRecentsComponentCallbacks = cb;
438    }
439
440    /** Notifies the callbacks that the visibility of Recents has changed. */
441    public static void notifyVisibilityChanged(boolean visible) {
442        if (sRecentsComponentCallbacks != null) {
443            sRecentsComponentCallbacks.onVisibilityChanged(visible);
444        }
445    }
446
447    /**** OnAnimationStartedListener Implementation ****/
448
449    @Override
450    public void onAnimationStarted() {
451        // Notify recents to start the enter animation
452        Intent intent = new Intent(RecentsActivity.ACTION_START_ENTER_ANIMATION);
453        intent.setPackage(mContext.getPackageName());
454        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
455        mContext.sendBroadcast(intent);
456    }
457}
458