AlternateRecentsComponent.java revision 96e3bc1f8d7c199df6fca603d0c5e59d9b70ca1b
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.Matrix;
31import android.graphics.Rect;
32import android.os.Bundle;
33import android.os.Handler;
34import android.os.IBinder;
35import android.os.Message;
36import android.os.Messenger;
37import android.os.RemoteException;
38import android.os.UserHandle;
39import android.util.DisplayMetrics;
40import android.view.Display;
41import android.view.Surface;
42import android.view.SurfaceControl;
43import android.view.View;
44import android.view.WindowManager;
45import com.android.systemui.R;
46
47import java.util.ArrayList;
48import java.util.Iterator;
49import java.util.List;
50
51/** A proxy implementation for the recents component */
52public class AlternateRecentsComponent {
53
54    /** A handler for messages from the recents implementation */
55    class RecentsMessageHandler extends Handler {
56        @Override
57        public void handleMessage(Message msg) {
58            if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) {
59                Resources res = mContext.getResources();
60                float statusBarHeight = res.getDimensionPixelSize(
61                        com.android.internal.R.dimen.status_bar_height);
62                Bundle replyData = msg.getData().getParcelable(KEY_CONFIGURATION_DATA);
63                mSingleCountFirstTaskRect = replyData.getParcelable(KEY_SINGLE_TASK_STACK_RECT);
64                mSingleCountFirstTaskRect.offset(0, (int) statusBarHeight);
65                mMultipleCountFirstTaskRect = replyData.getParcelable(KEY_MULTIPLE_TASK_STACK_RECT);
66                mMultipleCountFirstTaskRect.offset(0, (int) statusBarHeight);
67                Console.log(Constants.DebugFlags.App.RecentsComponent,
68                        "[RecentsComponent|RecentsMessageHandler|handleMessage]",
69                        "singleTaskRect: " + mSingleCountFirstTaskRect +
70                        " multipleTaskRect: " + mMultipleCountFirstTaskRect);
71
72                // If we had the update the animation rects as a result of onServiceConnected, then
73                // we check for whether we need to toggle the recents here.
74                if (mToggleRecentsUponServiceBound) {
75                    startAlternateRecentsActivity();
76                    mToggleRecentsUponServiceBound = false;
77                }
78            }
79        }
80    }
81
82    /** A service connection to the recents implementation */
83    class RecentsServiceConnection implements ServiceConnection {
84        @Override
85        public void onServiceConnected(ComponentName className, IBinder service) {
86            Console.log(Constants.DebugFlags.App.RecentsComponent,
87                    "[RecentsComponent|ServiceConnection|onServiceConnected]",
88                    "toggleRecents: " + mToggleRecentsUponServiceBound);
89            mService = new Messenger(service);
90            mServiceIsBound = true;
91
92            if (hasValidTaskRects()) {
93                // Toggle recents if this new service connection was triggered by hitting recents
94                if (mToggleRecentsUponServiceBound) {
95                    startAlternateRecentsActivity();
96                    mToggleRecentsUponServiceBound = false;
97                }
98            } else {
99                // Otherwise, update the animation rects before starting the recents if requested
100                updateAnimationRects();
101            }
102        }
103
104        @Override
105        public void onServiceDisconnected(ComponentName className) {
106            Console.log(Constants.DebugFlags.App.RecentsComponent,
107                    "[RecentsComponent|ServiceConnection|onServiceDisconnected]");
108            mService = null;
109            mServiceIsBound = false;
110        }
111    }
112
113    final public static int MSG_UPDATE_FOR_CONFIGURATION = 0;
114    final public static int MSG_UPDATE_TASK_THUMBNAIL = 1;
115    final public static int MSG_PRELOAD_TASKS = 2;
116    final public static int MSG_CANCEL_PRELOAD_TASKS = 3;
117    final public static int MSG_CLOSE_RECENTS = 4;
118    final public static int MSG_TOGGLE_RECENTS = 5;
119
120    final public static String EXTRA_ANIMATING_WITH_THUMBNAIL = "recents.animatingWithThumbnail";
121    final public static String KEY_CONFIGURATION_DATA = "recents.data.updateForConfiguration";
122    final public static String KEY_WINDOW_RECT = "recents.windowRect";
123    final public static String KEY_SYSTEM_INSETS = "recents.systemInsets";
124    final public static String KEY_SINGLE_TASK_STACK_RECT = "recents.singleCountTaskRect";
125    final public static String KEY_MULTIPLE_TASK_STACK_RECT = "recents.multipleCountTaskRect";
126
127
128    final static int sMinToggleDelay = 425;
129
130    final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
131    final static String sRecentsPackage = "com.android.systemui";
132    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
133    final static String sRecentsService = "com.android.systemui.recents.RecentsService";
134
135    Context mContext;
136    SystemServicesProxy mSystemServicesProxy;
137
138    // Recents service binding
139    Messenger mService = null;
140    Messenger mMessenger;
141    boolean mServiceIsBound = false;
142    boolean mToggleRecentsUponServiceBound;
143    RecentsServiceConnection mConnection = new RecentsServiceConnection();
144
145    View mStatusBarView;
146    Rect mSingleCountFirstTaskRect = new Rect();
147    Rect mMultipleCountFirstTaskRect = new Rect();
148    long mLastToggleTime;
149
150    public AlternateRecentsComponent(Context context) {
151        mContext = context;
152        mSystemServicesProxy = new SystemServicesProxy(context);
153        mMessenger = new Messenger(new RecentsMessageHandler());
154    }
155
156    public void onStart() {
157        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|start]");
158
159        // Try to create a long-running connection to the recents service
160        bindToRecentsService(false);
161    }
162
163    /** Toggles the alternate recents activity */
164    public void onToggleRecents(Display display, int layoutDirection, View statusBarView) {
165        Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsStartup,
166                Constants.DebugFlags.App.TimeRecentsStartupKey);
167        Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsLaunchTask,
168                Constants.DebugFlags.App.TimeRecentsLaunchKey);
169        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|toggleRecents]",
170                "serviceIsBound: " + mServiceIsBound);
171        mStatusBarView = statusBarView;
172        if (!mServiceIsBound) {
173            // Try to create a long-running connection to the recents service before toggling
174            // recents
175            bindToRecentsService(true);
176            return;
177        }
178
179        try {
180            startAlternateRecentsActivity();
181        } catch (ActivityNotFoundException e) {
182            Console.logRawError("Failed to launch RecentAppsIntent", e);
183        }
184    }
185
186    public void onPreloadRecents() {
187        // Do nothing
188    }
189
190    public void onCancelPreloadingRecents() {
191        // Do nothing
192    }
193
194    public void onCloseRecents() {
195        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|closeRecents]");
196        if (mServiceIsBound) {
197            // Try and update the recents configuration
198            try {
199                Bundle data = new Bundle();
200                Message msg = Message.obtain(null, MSG_CLOSE_RECENTS, 0, 0);
201                msg.setData(data);
202                mService.send(msg);
203            } catch (RemoteException re) {
204                re.printStackTrace();
205            }
206        }
207    }
208
209    public void onConfigurationChanged(Configuration newConfig) {
210        updateAnimationRects();
211    }
212
213    /** Binds to the recents implementation */
214    private void bindToRecentsService(boolean toggleRecentsUponConnection) {
215        mToggleRecentsUponServiceBound = toggleRecentsUponConnection;
216        Intent intent = new Intent();
217        intent.setClassName(sRecentsPackage, sRecentsService);
218        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
219    }
220
221    /** Returns whether we have valid task rects to animate to. */
222    boolean hasValidTaskRects() {
223        return mSingleCountFirstTaskRect != null && mSingleCountFirstTaskRect.width() > 0 &&
224                mSingleCountFirstTaskRect.height() > 0 && mMultipleCountFirstTaskRect != null &&
225                mMultipleCountFirstTaskRect.width() > 0 && mMultipleCountFirstTaskRect.height() > 0;
226    }
227
228    /** Updates each of the task animation rects. */
229    void updateAnimationRects() {
230        if (mServiceIsBound) {
231            Resources res = mContext.getResources();
232            int statusBarHeight = res.getDimensionPixelSize(
233                    com.android.internal.R.dimen.status_bar_height);
234            int navBarHeight = res.getDimensionPixelSize(
235                    com.android.internal.R.dimen.navigation_bar_height);
236            Rect rect = new Rect();
237            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
238            wm.getDefaultDisplay().getRectSize(rect);
239
240            // Try and update the recents configuration
241            try {
242                Bundle data = new Bundle();
243                data.putParcelable(KEY_WINDOW_RECT, rect);
244                data.putParcelable(KEY_SYSTEM_INSETS, new Rect(0, statusBarHeight, 0, 0));
245                Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0);
246                msg.setData(data);
247                msg.replyTo = mMessenger;
248                mService.send(msg);
249            } catch (RemoteException re) {
250                re.printStackTrace();
251            }
252        }
253    }
254
255    /** Loads the first task thumbnail */
256    Bitmap loadFirstTaskThumbnail() {
257        SystemServicesProxy ssp = mSystemServicesProxy;
258        List<ActivityManager.RecentTaskInfo> tasks = ssp.getRecentTasks(1,
259                UserHandle.CURRENT.getIdentifier());
260        for (ActivityManager.RecentTaskInfo t : tasks) {
261            // Skip tasks in the home stack
262            if (ssp.isInHomeStack(t.persistentId)) {
263                return null;
264            }
265
266            return ssp.getTaskThumbnail(t.persistentId);
267        }
268        return null;
269    }
270
271    /** Returns whether there is are multiple recents tasks */
272    boolean hasMultipleRecentsTask(List<ActivityManager.RecentTaskInfo> tasks) {
273        // NOTE: Currently there's no method to get the number of non-home tasks, so we have to
274        // compute this ourselves
275        SystemServicesProxy ssp = mSystemServicesProxy;
276        Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator();
277        while (iter.hasNext()) {
278            ActivityManager.RecentTaskInfo t = iter.next();
279
280            // Skip tasks in the home stack
281            if (ssp.isInHomeStack(t.persistentId)) {
282                iter.remove();
283                continue;
284            }
285        }
286        return (tasks.size() > 1);
287    }
288
289    /** Returns whether the base intent of the top task stack was launched with the flag
290     * Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS. */
291    boolean isTopTaskExcludeFromRecents(List<ActivityManager.RecentTaskInfo> tasks) {
292        if (tasks.size() > 0) {
293            ActivityManager.RecentTaskInfo t = tasks.get(0);
294            Console.log(t.baseIntent.toString());
295            return (t.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
296        }
297        return false;
298    }
299
300    /** Converts from the device rotation to the degree */
301    float getDegreesForRotation(int value) {
302        switch (value) {
303            case Surface.ROTATION_90:
304                return 360f - 90f;
305            case Surface.ROTATION_180:
306                return 360f - 180f;
307            case Surface.ROTATION_270:
308                return 360f - 270f;
309        }
310        return 0f;
311    }
312
313    /** Takes a screenshot of the surface */
314    Bitmap takeScreenshot(Display display) {
315        DisplayMetrics dm = new DisplayMetrics();
316        display.getRealMetrics(dm);
317        float[] dims = {dm.widthPixels, dm.heightPixels};
318        float degrees = getDegreesForRotation(display.getRotation());
319        boolean requiresRotation = (degrees > 0);
320        if (requiresRotation) {
321            // Get the dimensions of the device in its native orientation
322            Matrix m = new Matrix();
323            m.preRotate(-degrees);
324            m.mapPoints(dims);
325            dims[0] = Math.abs(dims[0]);
326            dims[1] = Math.abs(dims[1]);
327        }
328        return SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
329    }
330
331    /** Creates the activity options for a thumbnail transition. */
332    ActivityOptions getThumbnailTransitionActivityOptions(Rect taskRect) {
333        // Loading from thumbnail
334        Bitmap thumbnail;
335        Bitmap firstThumbnail = loadFirstTaskThumbnail();
336        if (firstThumbnail != null) {
337            // Create the thumbnail
338            thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(),
339                    Bitmap.Config.ARGB_8888);
340            int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
341            Canvas c = new Canvas(thumbnail);
342            c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
343                    new Rect(0, 0, taskRect.width(), taskRect.height()), null);
344            c.setBitmap(null);
345            // Recycle the old thumbnail
346            firstThumbnail.recycle();
347        } else {
348            // Load the thumbnail from the screenshot if can't get one from the system
349            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
350            Display display = wm.getDefaultDisplay();
351            Bitmap screenshot = takeScreenshot(display);
352            if (screenshot != null) {
353                Resources res = mContext.getResources();
354                int size = Math.min(screenshot.getWidth(), screenshot.getHeight());
355                int statusBarHeight = res.getDimensionPixelSize(
356                        com.android.internal.R.dimen.status_bar_height);
357                thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(),
358                        Bitmap.Config.ARGB_8888);
359                Canvas c = new Canvas(thumbnail);
360                c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight +
361                        size), new Rect(0, 0, taskRect.width(), taskRect.height()), null);
362                c.setBitmap(null);
363                // Recycle the temporary screenshot
364                screenshot.recycle();
365            } else {
366                return null;
367            }
368        }
369
370        return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, thumbnail,
371                taskRect.left, taskRect.top, null);
372    }
373
374    /** Starts the recents activity */
375    void startAlternateRecentsActivity() {
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        SystemServicesProxy ssp = mSystemServicesProxy;
386        List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1);
387        boolean isTopTaskHome = false;
388        if (!tasks.isEmpty()) {
389            ActivityManager.RunningTaskInfo topTask = tasks.get(0);
390            ComponentName topActivity = topTask.topActivity;
391
392            // Check if the front most activity is recents
393            if (topActivity.getPackageName().equals(sRecentsPackage) &&
394                    topActivity.getClassName().equals(sRecentsActivity)) {
395                // Notify Recents to toggle itself
396                try {
397                    Bundle data = new Bundle();
398                    Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0);
399                    msg.setData(data);
400                    mService.send(msg);
401
402                    // Time this path
403                    Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup,
404                            Constants.DebugFlags.App.TimeRecentsStartupKey, "sendToggleRecents");
405                    Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask,
406                            Constants.DebugFlags.App.TimeRecentsLaunchKey, "sendToggleRecents");
407                } catch (RemoteException re) {
408                    re.printStackTrace();
409                }
410                mLastToggleTime = System.currentTimeMillis();
411                return;
412            }
413
414            // Determine whether the top task is currently home
415            isTopTaskHome = ssp.isInHomeStack(topTask.id);
416        }
417
418        // Otherwise, Recents is not the front-most activity and we should animate into it.  If
419        // the activity at the root of the top task stack is excluded from recents, or if that
420        // task stack is in the home stack, then we just do a simple transition.  Otherwise, we
421        // animate to the rects defined by the Recents service, which can differ depending on the
422        // number of items in the list.
423        List<ActivityManager.RecentTaskInfo> recentTasks =
424                ssp.getRecentTasks(4, UserHandle.CURRENT.getIdentifier());
425        Rect taskRect = hasMultipleRecentsTask(recentTasks) ? mMultipleCountFirstTaskRect :
426                mSingleCountFirstTaskRect;
427        boolean isTaskExcludedFromRecents = isTopTaskExcludeFromRecents(recentTasks);
428        boolean useThumbnailTransition = !isTopTaskHome && !isTaskExcludedFromRecents &&
429                hasValidTaskRects();
430
431        if (useThumbnailTransition) {
432            // Try starting with a thumbnail transition
433            ActivityOptions opts = getThumbnailTransitionActivityOptions(taskRect);
434            if (opts != null) {
435                startAlternateRecentsActivity(opts, true);
436            } else {
437                // Fall through below to the non-thumbnail transition
438                useThumbnailTransition = false;
439            }
440        }
441
442        // If there is no thumbnail transition, then just use a generic transition
443        // XXX: This should be different between home and from a recents-excluded app, perhaps the
444        //      recents-excluded app should still show up in recents, when the app is in the
445        //      foreground
446        if (!useThumbnailTransition) {
447            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
448                    R.anim.recents_from_launcher_enter,
449                    R.anim.recents_from_launcher_exit);
450            startAlternateRecentsActivity(opts, false);
451        }
452
453        Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup,
454                Constants.DebugFlags.App.TimeRecentsStartupKey, "startRecentsActivity");
455        mLastToggleTime = System.currentTimeMillis();
456    }
457
458    /** Starts the recents activity */
459    void startAlternateRecentsActivity(ActivityOptions opts, boolean animatingWithThumbnail) {
460        Intent intent = new Intent(sToggleRecentsAction);
461        intent.setClassName(sRecentsPackage, sRecentsActivity);
462        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
463                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
464        intent.putExtra(EXTRA_ANIMATING_WITH_THUMBNAIL, animatingWithThumbnail);
465        if (opts != null) {
466            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
467                    UserHandle.USER_CURRENT));
468        } else {
469            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
470        }
471    }
472}
473