AlternateRecentsComponent.java revision 743d5c95f3a107639c0ff22f099cab2624da3e27
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.ServiceConnection;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.graphics.Bitmap;
29import android.graphics.Canvas;
30import android.graphics.Rect;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.Message;
35import android.os.Messenger;
36import android.os.RemoteException;
37import android.os.UserHandle;
38import android.view.View;
39import android.view.WindowManager;
40import com.android.systemui.R;
41import com.android.systemui.RecentsComponent;
42
43import java.lang.ref.WeakReference;
44import java.util.Iterator;
45import java.util.List;
46import java.util.concurrent.atomic.AtomicBoolean;
47
48/** A proxy implementation for the recents component */
49public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener {
50
51    /** A handler for messages from the recents implementation */
52    class RecentsMessageHandler extends Handler {
53        @Override
54        public void handleMessage(Message msg) {
55            if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) {
56                Resources res = mContext.getResources();
57                float statusBarHeight = res.getDimensionPixelSize(
58                        com.android.internal.R.dimen.status_bar_height);
59                Bundle replyData = msg.getData().getParcelable(KEY_CONFIGURATION_DATA);
60                mSingleCountFirstTaskRect = replyData.getParcelable(KEY_SINGLE_TASK_STACK_RECT);
61                mSingleCountFirstTaskRect.offset(0, (int) statusBarHeight);
62                mTwoCountFirstTaskRect = replyData.getParcelable(KEY_TWO_TASK_STACK_RECT);
63                mTwoCountFirstTaskRect.offset(0, (int) statusBarHeight);
64                mMultipleCountFirstTaskRect = replyData.getParcelable(KEY_MULTIPLE_TASK_STACK_RECT);
65                mMultipleCountFirstTaskRect.offset(0, (int) statusBarHeight);
66                if (Console.Enabled) {
67                    Console.log(Constants.Log.App.RecentsComponent,
68                            "[RecentsComponent|RecentsMessageHandler|handleMessage]",
69                            "singleTaskRect: " + mSingleCountFirstTaskRect +
70                            " twoTaskRect: " + mTwoCountFirstTaskRect +
71                            " multipleTaskRect: " + mMultipleCountFirstTaskRect);
72                }
73
74                // If we had the update the animation rects as a result of onServiceConnected, then
75                // we check for whether we need to toggle the recents here.
76                if (mToggleRecentsUponServiceBound) {
77                    startRecentsActivity();
78                    mToggleRecentsUponServiceBound = false;
79                }
80            }
81        }
82    }
83
84    /** A service connection to the recents implementation */
85    class RecentsServiceConnection implements ServiceConnection {
86        @Override
87        public void onServiceConnected(ComponentName className, IBinder service) {
88            if (Console.Enabled) {
89                Console.log(Constants.Log.App.RecentsComponent,
90                        "[RecentsComponent|ServiceConnection|onServiceConnected]",
91                        "toggleRecents: " + mToggleRecentsUponServiceBound);
92            }
93            mService = new Messenger(service);
94            mServiceIsBound = true;
95
96            if (hasValidTaskRects()) {
97                // Start recents if this new service connection was triggered by hitting recents
98                if (mToggleRecentsUponServiceBound) {
99                    startRecentsActivity();
100                    mToggleRecentsUponServiceBound = false;
101                }
102            } else {
103                // Otherwise, update the animation rects before starting the recents if requested
104                updateAnimationRects();
105            }
106        }
107
108        @Override
109        public void onServiceDisconnected(ComponentName className) {
110            if (Console.Enabled) {
111                Console.log(Constants.Log.App.RecentsComponent,
112                        "[RecentsComponent|ServiceConnection|onServiceDisconnected]");
113            }
114            mService = null;
115            mServiceIsBound = false;
116        }
117    }
118
119    final public static int MSG_UPDATE_FOR_CONFIGURATION = 0;
120    final public static int MSG_UPDATE_TASK_THUMBNAIL = 1;
121    final public static int MSG_PRELOAD_TASKS = 2;
122    final public static int MSG_CANCEL_PRELOAD_TASKS = 3;
123    final public static int MSG_SHOW_RECENTS = 4;
124    final public static int MSG_HIDE_RECENTS = 5;
125    final public static int MSG_TOGGLE_RECENTS = 6;
126    final public static int MSG_START_ENTER_ANIMATION = 7;
127
128    final public static String EXTRA_FROM_HOME = "recents.triggeredOverHome";
129    final public static String EXTRA_FROM_APP_THUMBNAIL = "recents.animatingWithThumbnail";
130    final public static String EXTRA_FROM_APP_FULL_SCREENSHOT = "recents.thumbnail";
131    final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab";
132    final public static String KEY_CONFIGURATION_DATA = "recents.data.updateForConfiguration";
133    final public static String KEY_WINDOW_RECT = "recents.windowRect";
134    final public static String KEY_SYSTEM_INSETS = "recents.systemInsets";
135    final public static String KEY_SINGLE_TASK_STACK_RECT = "recents.singleCountTaskRect";
136    final public static String KEY_TWO_TASK_STACK_RECT = "recents.twoCountTaskRect";
137    final public static String KEY_MULTIPLE_TASK_STACK_RECT = "recents.multipleCountTaskRect";
138
139    final static int sMinToggleDelay = 425;
140
141    final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
142    final static String sRecentsPackage = "com.android.systemui";
143    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
144    final static String sRecentsService = "com.android.systemui.recents.RecentsService";
145
146    static Bitmap sLastScreenshot;
147    static RecentsComponent.Callbacks sRecentsComponentCallbacks;
148
149    Context mContext;
150    SystemServicesProxy mSystemServicesProxy;
151
152    // Recents service binding
153    Messenger mService = null;
154    Messenger mMessenger;
155    RecentsMessageHandler mHandler;
156    boolean mBootCompleted = false;
157    boolean mServiceIsBound = false;
158    boolean mToggleRecentsUponServiceBound;
159    RecentsServiceConnection mConnection = new RecentsServiceConnection();
160
161    // Variables to keep track of if we need to start recents after binding
162    View mStatusBarView;
163    boolean mTriggeredFromAltTab;
164
165    Rect mSingleCountFirstTaskRect = new Rect();
166    Rect mTwoCountFirstTaskRect = new Rect();
167    Rect mMultipleCountFirstTaskRect = new Rect();
168    long mLastToggleTime;
169
170    public AlternateRecentsComponent(Context context) {
171        mContext = context;
172        mSystemServicesProxy = new SystemServicesProxy(context);
173        mHandler = new RecentsMessageHandler();
174        mMessenger = new Messenger(mHandler);
175    }
176
177    public void onStart() {
178        if (Console.Enabled) {
179            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|start]");
180        }
181
182        // Try to create a long-running connection to the recents service
183        bindToRecentsService(false);
184    }
185
186    public void onBootCompleted() {
187        mBootCompleted = true;
188    }
189
190    /** Shows the recents */
191    public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) {
192        if (Console.Enabled) {
193            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|showRecents]");
194        }
195        mStatusBarView = statusBarView;
196        mTriggeredFromAltTab = triggeredFromAltTab;
197        if (!mServiceIsBound) {
198            // Try to create a long-running connection to the recents service before toggling
199            // recents
200            bindToRecentsService(true);
201            return;
202        }
203
204        try {
205            startRecentsActivity();
206        } catch (ActivityNotFoundException e) {
207            Console.logRawError("Failed to launch RecentAppsIntent", e);
208        }
209    }
210
211    /** Hides the recents */
212    public void onHideRecents(boolean triggeredFromAltTab) {
213        if (Console.Enabled) {
214            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]");
215        }
216
217        if (mServiceIsBound && mBootCompleted) {
218            if (isRecentsTopMost(null)) {
219                // Notify recents to close it
220                try {
221                    Bundle data = new Bundle();
222                    Message msg = Message.obtain(null, MSG_HIDE_RECENTS,
223                            triggeredFromAltTab ? 1 : 0, 0);
224                    msg.setData(data);
225                    mService.send(msg);
226                } catch (RemoteException re) {
227                    re.printStackTrace();
228                }
229            }
230        }
231    }
232
233    /** Toggles the alternate recents activity */
234    public void onToggleRecents(View statusBarView) {
235        if (Console.Enabled) {
236            Console.logStartTracingTime(Constants.Log.App.TimeRecentsStartup,
237                    Constants.Log.App.TimeRecentsStartupKey);
238            Console.logStartTracingTime(Constants.Log.App.TimeRecentsLaunchTask,
239                    Constants.Log.App.TimeRecentsLaunchKey);
240            Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|toggleRecents]",
241                    "serviceIsBound: " + mServiceIsBound);
242        }
243        mStatusBarView = statusBarView;
244        mTriggeredFromAltTab = false;
245        if (!mServiceIsBound) {
246            // Try to create a long-running connection to the recents service before toggling
247            // recents
248            bindToRecentsService(true);
249            return;
250        }
251
252        try {
253            toggleRecentsActivity();
254        } catch (ActivityNotFoundException e) {
255            Console.logRawError("Failed to launch RecentAppsIntent", e);
256        }
257    }
258
259    public void onPreloadRecents() {
260        // Do nothing
261    }
262
263    public void onCancelPreloadingRecents() {
264        // Do nothing
265    }
266
267    public void onConfigurationChanged(Configuration newConfig) {
268        updateAnimationRects();
269    }
270
271    /** Binds to the recents implementation */
272    private void bindToRecentsService(boolean toggleRecentsUponConnection) {
273        mToggleRecentsUponServiceBound = toggleRecentsUponConnection;
274        Intent intent = new Intent();
275        intent.setClassName(sRecentsPackage, sRecentsService);
276        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
277    }
278
279    /** Returns whether we have valid task rects to animate to. */
280    boolean hasValidTaskRects() {
281        return mSingleCountFirstTaskRect != null && mSingleCountFirstTaskRect.width() > 0 &&
282                mSingleCountFirstTaskRect.height() > 0 && mTwoCountFirstTaskRect != null &&
283                mTwoCountFirstTaskRect.width() > 0 && mTwoCountFirstTaskRect.height() > 0 &&
284                mMultipleCountFirstTaskRect != null && mMultipleCountFirstTaskRect.width() > 0 &&
285                mMultipleCountFirstTaskRect.height() > 0;
286    }
287
288    /** Updates each of the task animation rects. */
289    void updateAnimationRects() {
290        if (mServiceIsBound && mBootCompleted) {
291            Resources res = mContext.getResources();
292            int statusBarHeight = res.getDimensionPixelSize(
293                    com.android.internal.R.dimen.status_bar_height);
294            int navBarHeight = res.getDimensionPixelSize(
295                    com.android.internal.R.dimen.navigation_bar_height);
296            Rect rect = new Rect();
297            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
298            wm.getDefaultDisplay().getRectSize(rect);
299
300            // Try and update the recents configuration
301            try {
302                Bundle data = new Bundle();
303                data.putParcelable(KEY_WINDOW_RECT, rect);
304                data.putParcelable(KEY_SYSTEM_INSETS, new Rect(0, statusBarHeight, 0, 0));
305                Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0);
306                msg.setData(data);
307                msg.replyTo = mMessenger;
308                mService.send(msg);
309            } catch (RemoteException re) {
310                re.printStackTrace();
311            }
312        }
313    }
314
315    /** Loads the first task thumbnail */
316    Bitmap loadFirstTaskThumbnail() {
317        SystemServicesProxy ssp = mSystemServicesProxy;
318        List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1);
319
320        for (ActivityManager.RunningTaskInfo t : tasks) {
321            return ssp.getTaskThumbnail(t.id);
322        }
323        return null;
324    }
325
326    /** Returns the proper rect to use for the animation, given the number of tasks. */
327    Rect getAnimationTaskRect(List<ActivityManager.RecentTaskInfo> tasks) {
328        // NOTE: Currently there's no method to get the number of non-home tasks, so we have to
329        // compute this ourselves
330        SystemServicesProxy ssp = mSystemServicesProxy;
331        Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator();
332        while (iter.hasNext()) {
333            ActivityManager.RecentTaskInfo t = iter.next();
334
335            // Skip tasks in the home stack
336            if (ssp.isInHomeStack(t.persistentId)) {
337                iter.remove();
338                continue;
339            }
340        }
341        if (tasks.size() <= 1) {
342            return mSingleCountFirstTaskRect;
343        } else if (tasks.size() <= 2) {
344            return mTwoCountFirstTaskRect;
345        } else {
346            return mMultipleCountFirstTaskRect;
347        }
348    }
349
350    /** Returns whether the recents is currently running */
351    boolean isRecentsTopMost(AtomicBoolean isHomeTopMost) {
352        SystemServicesProxy ssp = mSystemServicesProxy;
353        List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1);
354        if (!tasks.isEmpty()) {
355            ActivityManager.RunningTaskInfo topTask = tasks.get(0);
356            ComponentName topActivity = topTask.topActivity;
357
358            // Check if the front most activity is recents
359            if (topActivity.getPackageName().equals(sRecentsPackage) &&
360                    topActivity.getClassName().equals(sRecentsActivity)) {
361                if (isHomeTopMost != null) {
362                    isHomeTopMost.set(false);
363                }
364                return true;
365            }
366
367            if (isHomeTopMost != null) {
368                isHomeTopMost.set(ssp.isInHomeStack(topTask.id));
369            }
370        }
371        return false;
372    }
373
374    /** Toggles the recents activity */
375    void toggleRecentsActivity() {
376        // If the user has toggled it too quickly, then just eat up the event here (it's better than
377        // showing a janky screenshot).
378        // NOTE: Ideally, the screenshot mechanism would take the window transform into account
379        if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) {
380            return;
381        }
382
383        // If Recents is the front most activity, then we should just communicate with it directly
384        // to launch the first task or dismiss itself
385        AtomicBoolean isTopTaskHome = new AtomicBoolean();
386        if (isRecentsTopMost(isTopTaskHome)) {
387            // Notify recents to close itself
388            try {
389                Bundle data = new Bundle();
390                Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0);
391                msg.setData(data);
392                mService.send(msg);
393
394                // Time this path
395                if (Console.Enabled) {
396                    Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
397                            Constants.Log.App.TimeRecentsStartupKey, "sendToggleRecents");
398                    Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
399                            Constants.Log.App.TimeRecentsLaunchKey, "sendToggleRecents");
400                }
401            } catch (RemoteException re) {
402                re.printStackTrace();
403            }
404            mLastToggleTime = System.currentTimeMillis();
405            return;
406        } else {
407            // Otherwise, start the recents activity
408            startRecentsActivity(isTopTaskHome.get());
409        }
410    }
411
412    /** Starts the recents activity if it is not already running */
413    void startRecentsActivity() {
414        // Check if the top task is in the home stack, and start the recents activity
415        AtomicBoolean isTopTaskHome = new AtomicBoolean();
416        if (!isRecentsTopMost(isTopTaskHome)) {
417            startRecentsActivity(isTopTaskHome.get());
418        }
419    }
420
421    /**
422     * Creates the activity options for a unknown state->recents transition.
423     */
424    ActivityOptions getUnknownTransitionActivityOptions() {
425        // Reset the last screenshot
426        consumeLastScreenshot();
427        return ActivityOptions.makeCustomAnimation(mContext,
428                R.anim.recents_from_unknown_enter,
429                R.anim.recents_from_unknown_exit, mHandler, this);
430    }
431
432    /**
433     * Creates the activity options for a home->recents transition.
434     */
435    ActivityOptions getHomeTransitionActivityOptions() {
436        // Reset the last screenshot
437        consumeLastScreenshot();
438        return ActivityOptions.makeCustomAnimation(mContext,
439                R.anim.recents_from_launcher_enter,
440                R.anim.recents_from_launcher_exit, mHandler, this);
441    }
442
443    /**
444     * Creates the activity options for an app->recents transition.  If this method sets the static
445     * screenshot, then we will use that for the transition.
446     */
447    ActivityOptions getThumbnailTransitionActivityOptions(Rect taskRect) {
448        // Recycle the last screenshot
449        consumeLastScreenshot();
450
451        // Take the full screenshot
452        if (Constants.DebugFlags.App.EnableScreenshotAppTransition) {
453            sLastScreenshot = mSystemServicesProxy.takeScreenshot();
454            if (sLastScreenshot != null) {
455                return ActivityOptions.makeCustomAnimation(mContext,
456                        R.anim.recents_from_app_enter,
457                        R.anim.recents_from_app_exit, mHandler, this);
458            }
459        }
460
461        // If the screenshot fails, then load the first task thumbnail and use that
462        Bitmap firstThumbnail = loadFirstTaskThumbnail();
463        if (firstThumbnail != null) {
464            // Create the new thumbnail for the animation down
465            // XXX: We should find a way to optimize this so we don't need to create a new bitmap
466            Bitmap thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(),
467                    Bitmap.Config.ARGB_8888);
468            int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
469            Canvas c = new Canvas(thumbnail);
470            c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
471                    new Rect(0, 0, taskRect.width(), taskRect.height()), null);
472            c.setBitmap(null);
473            // Recycle the old thumbnail
474            firstThumbnail.recycle();
475            return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView,
476                    thumbnail, taskRect.left, taskRect.top, this);
477        }
478
479        // If both the screenshot and thumbnail fails, then just fall back to the default transition
480        return getUnknownTransitionActivityOptions();
481    }
482
483    /** Starts the recents activity */
484    void startRecentsActivity(boolean isTopTaskHome) {
485        // If Recents is not the front-most activity and we should animate into it.  If
486        // the activity at the root of the top task stack in the home stack, then we just do a
487        // simple transition.  Otherwise, we animate to the rects defined by the Recents service,
488        // which can differ depending on the number of items in the list.
489        SystemServicesProxy ssp = mSystemServicesProxy;
490        List<ActivityManager.RecentTaskInfo> recentTasks =
491                ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier());
492        Rect taskRect = getAnimationTaskRect(recentTasks);
493        boolean useThumbnailTransition = !isTopTaskHome &&
494                hasValidTaskRects();
495        boolean hasRecentTasks = !recentTasks.isEmpty();
496
497        if (useThumbnailTransition) {
498            // Try starting with a thumbnail transition
499            ActivityOptions opts = getThumbnailTransitionActivityOptions(taskRect);
500            if (opts != null) {
501                if (sLastScreenshot != null) {
502                    startAlternateRecentsActivity(opts, EXTRA_FROM_APP_FULL_SCREENSHOT);
503                } else {
504                    startAlternateRecentsActivity(opts, EXTRA_FROM_APP_THUMBNAIL);
505                }
506            } else {
507                // Fall through below to the non-thumbnail transition
508                useThumbnailTransition = false;
509            }
510        } else {
511            // If there is no thumbnail transition, but is launching from home into recents, then
512            // use a quick home transition and do the animation from home
513            if (hasRecentTasks && Constants.DebugFlags.App.EnableHomeTransition) {
514                ActivityOptions opts = getHomeTransitionActivityOptions();
515                startAlternateRecentsActivity(opts, EXTRA_FROM_HOME);
516            } else {
517                // Otherwise we do the normal fade from an unknown source
518                ActivityOptions opts = getUnknownTransitionActivityOptions();
519                startAlternateRecentsActivity(opts, null);
520            }
521        }
522
523        if (Console.Enabled) {
524            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
525                    Constants.Log.App.TimeRecentsStartupKey, "startRecentsActivity");
526        }
527        mLastToggleTime = System.currentTimeMillis();
528    }
529
530    /** Starts the recents activity */
531    void startAlternateRecentsActivity(ActivityOptions opts, String extraFlag) {
532        Intent intent = new Intent(sToggleRecentsAction);
533        intent.setClassName(sRecentsPackage, sRecentsActivity);
534        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
535                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
536                | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
537        if (extraFlag != null) {
538            intent.putExtra(extraFlag, true);
539        }
540        intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab);
541        if (opts != null) {
542            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
543                    UserHandle.USER_CURRENT));
544        } else {
545            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
546        }
547    }
548
549    /** Returns the last screenshot taken, this will be called by the RecentsActivity. */
550    public static Bitmap getLastScreenshot() {
551        return sLastScreenshot;
552    }
553
554    /** Recycles the last screenshot taken, this will be called by the RecentsActivity. */
555    public static void consumeLastScreenshot() {
556        if (sLastScreenshot != null) {
557            sLastScreenshot.recycle();
558            sLastScreenshot = null;
559        }
560    }
561
562    /** Sets the RecentsComponent callbacks. */
563    public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) {
564        sRecentsComponentCallbacks = cb;
565    }
566
567    /** Notifies the callbacks that the visibility of Recents has changed. */
568    public static void notifyVisibilityChanged(boolean visible) {
569        if (sRecentsComponentCallbacks != null) {
570            sRecentsComponentCallbacks.onVisibilityChanged(visible);
571        }
572    }
573
574    /**** OnAnimationStartedListener Implementation ****/
575
576    @Override
577    public void onAnimationStarted() {
578        // Notify recents to start the enter animation
579        try {
580            Message msg = Message.obtain(null, MSG_START_ENTER_ANIMATION, 0, 0);
581            mService.send(msg);
582        } catch (RemoteException re) {
583            re.printStackTrace();
584        }
585    }
586}
587