AlternateRecentsComponent.java revision 2a764949c943681a4d25a17a0b203a0127a4a486
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.pm.ActivityInfo;
27import android.content.pm.PackageManager;
28import android.content.res.Configuration;
29import android.content.res.Resources;
30import android.graphics.Bitmap;
31import android.graphics.Canvas;
32import android.graphics.Matrix;
33import android.graphics.Paint;
34import android.graphics.Rect;
35import android.graphics.drawable.BitmapDrawable;
36import android.graphics.drawable.Drawable;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.IBinder;
40import android.os.Message;
41import android.os.Messenger;
42import android.os.RemoteException;
43import android.os.SystemProperties;
44import android.os.UserHandle;
45import android.util.DisplayMetrics;
46import android.util.Log;
47import android.view.Display;
48import android.view.Surface;
49import android.view.SurfaceControl;
50import android.view.View;
51import android.view.WindowManager;
52import com.android.systemui.R;
53import com.android.systemui.RecentsComponent;
54import com.android.systemui.SystemUI;
55
56import java.util.List;
57
58/** A proxy implementation for the recents component */
59public class AlternateRecentsComponent {
60
61    /** A handler for messages from the recents implementation */
62    class RecentsMessageHandler extends Handler {
63        @Override
64        public void handleMessage(Message msg) {
65            if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) {
66                Resources res = mContext.getResources();
67                float statusBarHeight = res.getDimensionPixelSize(
68                        com.android.internal.R.dimen.status_bar_height);
69                mFirstTaskRect = (Rect) msg.getData().getParcelable("taskRect");
70                mFirstTaskRect.offset(0, (int) statusBarHeight);
71            }
72        }
73    }
74
75    /** A service connection to the recents implementation */
76    class RecentsServiceConnection implements ServiceConnection {
77        @Override
78        public void onServiceConnected(ComponentName className, IBinder service) {
79            Console.log(Constants.DebugFlags.App.RecentsComponent,
80                    "[RecentsComponent|ServiceConnection|onServiceConnected]",
81                    "toggleRecents: " + mToggleRecentsUponServiceBound);
82            mService = new Messenger(service);
83            mServiceIsBound = true;
84
85            // Toggle recents if this service connection was triggered by hitting the recents button
86            if (mToggleRecentsUponServiceBound) {
87                startAlternateRecentsActivity();
88            }
89            mToggleRecentsUponServiceBound = false;
90        }
91
92        @Override
93        public void onServiceDisconnected(ComponentName className) {
94            Console.log(Constants.DebugFlags.App.RecentsComponent,
95                    "[RecentsComponent|ServiceConnection|onServiceDisconnected]");
96            mService = null;
97            mServiceIsBound = false;
98        }
99    }
100
101    final static int MSG_UPDATE_FOR_CONFIGURATION = 0;
102    final static int MSG_UPDATE_TASK_THUMBNAIL = 1;
103    final static int MSG_PRELOAD_TASKS = 2;
104    final static int MSG_CANCEL_PRELOAD_TASKS = 3;
105    final static int MSG_CLOSE_RECENTS = 4;
106    final static int MSG_TOGGLE_RECENTS = 5;
107
108    final static int sMinToggleDelay = 425;
109
110    final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS";
111    final static String sRecentsPackage = "com.android.systemui";
112    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
113    final static String sRecentsService = "com.android.systemui.recents.RecentsService";
114
115    Context mContext;
116
117    // Recents service binding
118    Messenger mService = null;
119    Messenger mMessenger;
120    boolean mServiceIsBound = false;
121    boolean mToggleRecentsUponServiceBound;
122    RecentsServiceConnection mConnection = new RecentsServiceConnection();
123
124    View mStatusBarView;
125    Rect mFirstTaskRect = new Rect();
126    long mLastToggleTime;
127
128    public AlternateRecentsComponent(Context context) {
129        mContext = context;
130        mMessenger = new Messenger(new RecentsMessageHandler());
131    }
132
133    public void onStart() {
134        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|start]");
135
136        // Try to create a long-running connection to the recents service
137        bindToRecentsService(false);
138    }
139
140    /** Toggles the alternate recents activity */
141    public void onToggleRecents(Display display, int layoutDirection, View statusBarView) {
142        Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsStartup,
143                Constants.DebugFlags.App.TimeRecentsStartupKey);
144        Console.logStartTracingTime(Constants.DebugFlags.App.TimeRecentsLaunchTask,
145                Constants.DebugFlags.App.TimeRecentsLaunchKey);
146        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|toggleRecents]",
147                "serviceIsBound: " + mServiceIsBound);
148        mStatusBarView = statusBarView;
149        if (!mServiceIsBound) {
150            // Try to create a long-running connection to the recents service before toggling
151            // recents
152            bindToRecentsService(true);
153            return;
154        }
155
156        try {
157            startAlternateRecentsActivity();
158        } catch (ActivityNotFoundException e) {
159            Console.logRawError("Failed to launch RecentAppsIntent", e);
160        }
161    }
162
163    public void onPreloadRecents() {
164        // Do nothing
165    }
166
167    public void onCancelPreloadingRecents() {
168        // Do nothing
169    }
170
171    public void onCloseRecents() {
172        Console.log(Constants.DebugFlags.App.RecentsComponent, "[RecentsComponent|closeRecents]");
173        if (mServiceIsBound) {
174            // Try and update the recents configuration
175            try {
176                Bundle data = new Bundle();
177                Message msg = Message.obtain(null, MSG_CLOSE_RECENTS, 0, 0);
178                msg.setData(data);
179                mService.send(msg);
180            } catch (RemoteException re) {
181                re.printStackTrace();
182            }
183        }
184    }
185
186    public void onConfigurationChanged(Configuration newConfig) {
187        if (mServiceIsBound) {
188            Resources res = mContext.getResources();
189            int statusBarHeight = res.getDimensionPixelSize(
190                    com.android.internal.R.dimen.status_bar_height);
191            int navBarHeight = res.getDimensionPixelSize(
192                    com.android.internal.R.dimen.navigation_bar_height);
193            Rect rect = new Rect();
194            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
195            wm.getDefaultDisplay().getRectSize(rect);
196
197            // Try and update the recents configuration
198            try {
199                Bundle data = new Bundle();
200                data.putParcelable("windowRect", rect);
201                data.putParcelable("systemInsets", new Rect(0, statusBarHeight, 0, 0));
202                Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0);
203                msg.setData(data);
204                msg.replyTo = mMessenger;
205                mService.send(msg);
206            } catch (RemoteException re) {
207                re.printStackTrace();
208            }
209        }
210    }
211
212    /** Binds to the recents implementation */
213    private void bindToRecentsService(boolean toggleRecentsUponConnection) {
214        mToggleRecentsUponServiceBound = toggleRecentsUponConnection;
215        Intent intent = new Intent();
216        intent.setClassName(sRecentsPackage, sRecentsService);
217        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
218    }
219
220    /** Loads the first task thumbnail */
221    Bitmap loadFirstTaskThumbnail() {
222        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
223        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
224                ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES,
225                UserHandle.CURRENT.getIdentifier());
226        for (ActivityManager.RecentTaskInfo t : tasks) {
227            // Skip tasks in the home stack
228            if (am.isInHomeStack(t.persistentId)) {
229                return null;
230            }
231
232            Bitmap thumbnail = am.getTaskTopThumbnail(t.persistentId);
233            return thumbnail;
234        }
235        return null;
236    }
237
238    /** Returns whether there is a first task */
239    boolean hasFirstTask() {
240        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
241        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
242                ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES,
243                UserHandle.CURRENT.getIdentifier());
244        for (ActivityManager.RecentTaskInfo t : tasks) {
245            // Skip tasks in the home stack
246            if (am.isInHomeStack(t.persistentId)) {
247                continue;
248            }
249
250            return true;
251        }
252        return false;
253    }
254
255    /** Converts from the device rotation to the degree */
256    float getDegreesForRotation(int value) {
257        switch (value) {
258            case Surface.ROTATION_90:
259                return 360f - 90f;
260            case Surface.ROTATION_180:
261                return 360f - 180f;
262            case Surface.ROTATION_270:
263                return 360f - 270f;
264        }
265        return 0f;
266    }
267
268    /** Takes a screenshot of the surface */
269    Bitmap takeScreenshot(Display display) {
270        DisplayMetrics dm = new DisplayMetrics();
271        display.getRealMetrics(dm);
272        float[] dims = {dm.widthPixels, dm.heightPixels};
273        float degrees = getDegreesForRotation(display.getRotation());
274        boolean requiresRotation = (degrees > 0);
275        if (requiresRotation) {
276            // Get the dimensions of the device in its native orientation
277            Matrix m = new Matrix();
278            m.preRotate(-degrees);
279            m.mapPoints(dims);
280            dims[0] = Math.abs(dims[0]);
281            dims[1] = Math.abs(dims[1]);
282        }
283        return SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
284    }
285
286    /** Starts the recents activity */
287    void startAlternateRecentsActivity() {
288        // If the user has toggled it too quickly, then just eat up the event here (it's better than
289        // showing a janky screenshot).
290        // NOTE: Ideally, the screenshot mechanism would take the window transform into account
291        if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) {
292            return;
293        }
294
295        // If Recents is the front most activity, then we should just communicate with it directly
296        // to launch the first task or dismiss itself
297        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
298        List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
299        if (!tasks.isEmpty()) {
300            ComponentName topActivity = tasks.get(0).topActivity;
301
302            // Check if the front most activity is recents
303            if (topActivity.getPackageName().equals(sRecentsPackage) &&
304                    topActivity.getClassName().equals(sRecentsActivity)) {
305                // Notify Recents to toggle itself
306                try {
307                    Bundle data = new Bundle();
308                    Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0);
309                    msg.setData(data);
310                    mService.send(msg);
311
312                    // Time this path
313                    Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup,
314                            Constants.DebugFlags.App.TimeRecentsStartupKey, "sendToggleRecents");
315                    Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsLaunchTask,
316                            Constants.DebugFlags.App.TimeRecentsLaunchKey, "sendToggleRecents");
317                } catch (RemoteException re) {
318                    re.printStackTrace();
319                }
320                mLastToggleTime = System.currentTimeMillis();
321                return;
322            }
323        }
324
325        // Otherwise, Recents is not the front-most activity and we should animate into it
326        Rect taskRect = mFirstTaskRect;
327        if (taskRect != null && taskRect.width() > 0 && taskRect.height() > 0 && hasFirstTask()) {
328            // Loading from thumbnail
329            Bitmap thumbnail;
330            Bitmap firstThumbnail = loadFirstTaskThumbnail();
331            if (firstThumbnail == null) {
332                // Load the thumbnail from the screenshot
333                WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
334                Display display = wm.getDefaultDisplay();
335                Bitmap screenshot = takeScreenshot(display);
336                Resources res = mContext.getResources();
337                int size = Math.min(screenshot.getWidth(), screenshot.getHeight());
338                int statusBarHeight = res.getDimensionPixelSize(
339                        com.android.internal.R.dimen.status_bar_height);
340                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
341                        Bitmap.Config.ARGB_8888);
342                Canvas c = new Canvas(thumbnail);
343                c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size),
344                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
345                c.setBitmap(null);
346                // Recycle the old screenshot
347                screenshot.recycle();
348            } else {
349                // Create the thumbnail
350                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
351                        Bitmap.Config.ARGB_8888);
352                int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
353                Canvas c = new Canvas(thumbnail);
354                c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
355                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
356                c.setBitmap(null);
357                // Recycle the old thumbnail
358                firstThumbnail.recycle();
359            }
360
361            ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView,
362                    thumbnail, mFirstTaskRect.left, mFirstTaskRect.top, null);
363            startAlternateRecentsActivity(opts);
364        } else {
365            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
366                    R.anim.recents_from_launcher_enter,
367                    R.anim.recents_from_launcher_exit);
368            startAlternateRecentsActivity(opts);
369        }
370
371        Console.logTraceTime(Constants.DebugFlags.App.TimeRecentsStartup,
372                Constants.DebugFlags.App.TimeRecentsStartupKey, "startRecentsActivity");
373        mLastToggleTime = System.currentTimeMillis();
374    }
375
376    /** Starts the recents activity */
377    void startAlternateRecentsActivity(ActivityOptions opts) {
378        Intent intent = new Intent(sToggleRecentsAction);
379        intent.setClassName(sRecentsPackage, sRecentsActivity);
380        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
381                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
382        if (opts != null) {
383            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
384                    UserHandle.USER_CURRENT));
385        } else {
386            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
387        }
388    }
389}
390