Recents.java revision 303e1ff1fec8b240b587bb18b981247a99833aa8
1/*
2 * Copyright (C) 2013 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.recent;
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
59public class Recents extends SystemUI implements RecentsComponent {
60    /** A handler for messages from the recents implementation */
61    class RecentsMessageHandler extends Handler {
62        @Override
63        public void handleMessage(Message msg) {
64            if (!mUseAlternateRecents) return;
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            if (!mUseAlternateRecents) return;
80
81            Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceConnected] toggleRecents: " +
82                    mToggleRecentsUponServiceBound);
83            mService = new Messenger(service);
84            mServiceIsBound = true;
85
86            // Toggle recents if this service connection was triggered by hitting the recents button
87            if (mToggleRecentsUponServiceBound) {
88                startAlternateRecentsActivity();
89            }
90            mToggleRecentsUponServiceBound = false;
91        }
92
93        @Override
94        public void onServiceDisconnected(ComponentName className) {
95            if (!mUseAlternateRecents) return;
96
97            Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceDisconnected]");
98            mService = null;
99            mServiceIsBound = false;
100        }
101    }
102
103    private static final String TAG = "Recents";
104    private static final boolean DEBUG = true;
105
106    final static int MSG_UPDATE_FOR_CONFIGURATION = 0;
107    final static int MSG_UPDATE_TASK_THUMBNAIL = 1;
108    final static int MSG_PRELOAD_TASKS = 2;
109    final static int MSG_CANCEL_PRELOAD_TASKS = 3;
110
111    final static String sToggleRecentsAction = "com.android.systemui.recents.TOGGLE_RECENTS";
112    final static String sRecentsPackage = "com.android.systemui";
113    final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity";
114    final static String sRecentsService = "com.android.systemui.recents.RecentsService";
115
116    // Which recents to use
117    boolean mUseAlternateRecents;
118
119    // Recents service binding
120    Messenger mService = null;
121    Messenger mMessenger;
122    boolean mServiceIsBound = false;
123    boolean mToggleRecentsUponServiceBound;
124    RecentsServiceConnection mConnection = new RecentsServiceConnection();
125
126    View mStatusBarView;
127    Rect mFirstTaskRect = new Rect();
128
129    public Recents() {
130        mMessenger = new Messenger(new RecentsMessageHandler());
131    }
132
133    @Override
134    public void start() {
135        mUseAlternateRecents =
136                SystemProperties.getBoolean("persist.recents.use_alternate", false);
137
138        putComponent(RecentsComponent.class, this);
139
140        if (mUseAlternateRecents) {
141            Log.d(TAG, "[RecentsComponent|start]");
142
143            // Try to create a long-running connection to the recents service
144            bindToRecentsService(false);
145        }
146    }
147
148    @Override
149    public void toggleRecents(Display display, int layoutDirection, View statusBarView) {
150        if (mUseAlternateRecents) {
151            // Launch the alternate recents if required
152            toggleAlternateRecents(display, layoutDirection, statusBarView);
153            return;
154        }
155
156        if (DEBUG) Log.d(TAG, "toggle recents panel");
157        try {
158            TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask();
159
160            Intent intent = new Intent(RecentsActivity.TOGGLE_RECENTS_INTENT);
161            intent.setClassName("com.android.systemui",
162                    "com.android.systemui.recent.RecentsActivity");
163            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
164                    | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
165
166            if (firstTask == null) {
167                if (RecentsActivity.forceOpaqueBackground(mContext)) {
168                    ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
169                            R.anim.recents_launch_from_launcher_enter,
170                            R.anim.recents_launch_from_launcher_exit);
171                    mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
172                            UserHandle.USER_CURRENT));
173                } else {
174                    // The correct window animation will be applied via the activity's style
175                    mContext.startActivityAsUser(intent, new UserHandle(
176                            UserHandle.USER_CURRENT));
177                }
178
179            } else {
180                Bitmap first = null;
181                if (firstTask.getThumbnail() instanceof BitmapDrawable) {
182                    first = ((BitmapDrawable) firstTask.getThumbnail()).getBitmap();
183                } else {
184                    first = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
185                    Drawable d = RecentTasksLoader.getInstance(mContext).getDefaultThumbnail();
186                    d.draw(new Canvas(first));
187                }
188                final Resources res = mContext.getResources();
189
190                float thumbWidth = res
191                        .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_width);
192                float thumbHeight = res
193                        .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_height);
194                if (first == null) {
195                    throw new RuntimeException("Recents thumbnail is null");
196                }
197                if (first.getWidth() != thumbWidth || first.getHeight() != thumbHeight) {
198                    first = Bitmap.createScaledBitmap(first, (int) thumbWidth, (int) thumbHeight,
199                            true);
200                    if (first == null) {
201                        throw new RuntimeException("Recents thumbnail is null");
202                    }
203                }
204
205
206                DisplayMetrics dm = new DisplayMetrics();
207                display.getMetrics(dm);
208                // calculate it here, but consider moving it elsewhere
209                // first, determine which orientation you're in.
210                final Configuration config = res.getConfiguration();
211                int x, y;
212
213                if (config.orientation == Configuration.ORIENTATION_PORTRAIT) {
214                    float appLabelLeftMargin = res.getDimensionPixelSize(
215                            R.dimen.status_bar_recents_app_label_left_margin);
216                    float appLabelWidth = res.getDimensionPixelSize(
217                            R.dimen.status_bar_recents_app_label_width);
218                    float thumbLeftMargin = res.getDimensionPixelSize(
219                            R.dimen.status_bar_recents_thumbnail_left_margin);
220                    float thumbBgPadding = res.getDimensionPixelSize(
221                            R.dimen.status_bar_recents_thumbnail_bg_padding);
222
223                    float width = appLabelLeftMargin +
224                            +appLabelWidth
225                            + thumbLeftMargin
226                            + thumbWidth
227                            + 2 * thumbBgPadding;
228
229                    x = (int) ((dm.widthPixels - width) / 2f + appLabelLeftMargin + appLabelWidth
230                            + thumbBgPadding + thumbLeftMargin);
231                    y = (int) (dm.heightPixels
232                            - res.getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_height)
233                            - thumbBgPadding);
234                    if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
235                        x = dm.widthPixels - x - res.getDimensionPixelSize(
236                                R.dimen.status_bar_recents_thumbnail_width);
237                    }
238
239                } else { // if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
240                    float thumbTopMargin = res.getDimensionPixelSize(
241                            R.dimen.status_bar_recents_thumbnail_top_margin);
242                    float thumbBgPadding = res.getDimensionPixelSize(
243                            R.dimen.status_bar_recents_thumbnail_bg_padding);
244                    float textPadding = res.getDimensionPixelSize(
245                            R.dimen.status_bar_recents_text_description_padding);
246                    float labelTextSize = res.getDimensionPixelSize(
247                            R.dimen.status_bar_recents_app_label_text_size);
248                    Paint p = new Paint();
249                    p.setTextSize(labelTextSize);
250                    float labelTextHeight = p.getFontMetricsInt().bottom
251                            - p.getFontMetricsInt().top;
252                    float descriptionTextSize = res.getDimensionPixelSize(
253                            R.dimen.status_bar_recents_app_description_text_size);
254                    p.setTextSize(descriptionTextSize);
255                    float descriptionTextHeight = p.getFontMetricsInt().bottom
256                            - p.getFontMetricsInt().top;
257
258                    float statusBarHeight = res.getDimensionPixelSize(
259                            com.android.internal.R.dimen.status_bar_height);
260                    float recentsItemTopPadding = statusBarHeight;
261
262                    float height = thumbTopMargin
263                            + thumbHeight
264                            + 2 * thumbBgPadding + textPadding + labelTextHeight
265                            + recentsItemTopPadding + textPadding + descriptionTextHeight;
266                    float recentsItemRightPadding = res
267                            .getDimensionPixelSize(R.dimen.status_bar_recents_item_padding);
268                    float recentsScrollViewRightPadding = res
269                            .getDimensionPixelSize(R.dimen.status_bar_recents_right_glow_margin);
270                    x = (int) (dm.widthPixels - res
271                            .getDimensionPixelSize(R.dimen.status_bar_recents_thumbnail_width)
272                            - thumbBgPadding - recentsItemRightPadding
273                            - recentsScrollViewRightPadding);
274                    y = (int) ((dm.heightPixels - statusBarHeight - height) / 2f + thumbTopMargin
275                            + recentsItemTopPadding + thumbBgPadding + statusBarHeight);
276                }
277
278                ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(
279                        statusBarView,
280                        first, x, y,
281                        new ActivityOptions.OnAnimationStartedListener() {
282                            public void onAnimationStarted() {
283                                Intent intent =
284                                        new Intent(RecentsActivity.WINDOW_ANIMATION_START_INTENT);
285                                intent.setPackage("com.android.systemui");
286                                mContext.sendBroadcastAsUser(intent,
287                                        new UserHandle(UserHandle.USER_CURRENT));
288                            }
289                        });
290                intent.putExtra(RecentsActivity.WAITING_FOR_WINDOW_ANIMATION_PARAM, true);
291                mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
292                        UserHandle.USER_CURRENT));
293            }
294        } catch (ActivityNotFoundException e) {
295            Log.e(TAG, "Failed to launch RecentAppsIntent", e);
296        }
297    }
298
299    /** Toggles the alternate recents activity */
300    public void toggleAlternateRecents(Display display, int layoutDirection, View statusBarView) {
301        if (!mUseAlternateRecents) return;
302
303        Log.d(TAG, "[RecentsComponent|toggleRecents] serviceIsBound: " + mServiceIsBound);
304        mStatusBarView = statusBarView;
305        if (!mServiceIsBound) {
306            // Try to create a long-running connection to the recents service before toggling
307            // recents
308            bindToRecentsService(true);
309            return;
310        }
311
312        try {
313            startAlternateRecentsActivity();
314        } catch (ActivityNotFoundException e) {
315            Log.e(TAG, "Failed to launch RecentAppsIntent", e);
316        }
317    }
318
319    @Override
320    protected void onConfigurationChanged(Configuration newConfig) {
321        if (mServiceIsBound) {
322            Resources res = mContext.getResources();
323            int statusBarHeight = res.getDimensionPixelSize(
324                    com.android.internal.R.dimen.status_bar_height);
325            int navBarHeight = res.getDimensionPixelSize(
326                    com.android.internal.R.dimen.navigation_bar_height);
327            Rect rect = new Rect();
328            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
329            wm.getDefaultDisplay().getRectSize(rect);
330
331            // Try and update the recents configuration
332            try {
333                Bundle data = new Bundle();
334                data.putParcelable("windowRect", rect);
335                data.putParcelable("systemInsets", new Rect(0, statusBarHeight, 0, 0));
336                Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0);
337                msg.setData(data);
338                msg.replyTo = mMessenger;
339                mService.send(msg);
340            } catch (RemoteException re) {
341                re.printStackTrace();
342            }
343        }
344    }
345
346    /** Binds to the recents implementation */
347    private void bindToRecentsService(boolean toggleRecentsUponConnection) {
348        if (!mUseAlternateRecents) return;
349
350        mToggleRecentsUponServiceBound = toggleRecentsUponConnection;
351        Intent intent = new Intent();
352        intent.setClassName(sRecentsPackage, sRecentsService);
353        mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
354    }
355
356    /** Loads the first task thumbnail */
357    Bitmap loadFirstTaskThumbnail() {
358        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
359        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
360                ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
361        for (ActivityManager.RecentTaskInfo t : tasks) {
362            // Skip tasks in the home stack
363            if (am.isInHomeStack(t.persistentId)) {
364                return null;
365            }
366
367            Bitmap thumbnail = am.getTaskTopThumbnail(t.persistentId);
368            return thumbnail;
369        }
370        return null;
371    }
372
373    /** Returns whether there is a first task */
374    boolean hasFirstTask() {
375        ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
376        List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1,
377                ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier());
378        for (ActivityManager.RecentTaskInfo t : tasks) {
379            // Skip tasks in the home stack
380            if (am.isInHomeStack(t.persistentId)) {
381                continue;
382            }
383
384            return true;
385        }
386        return false;
387    }
388
389    /** Converts from the device rotation to the degree */
390    float getDegreesForRotation(int value) {
391        switch (value) {
392            case Surface.ROTATION_90:
393                return 360f - 90f;
394            case Surface.ROTATION_180:
395                return 360f - 180f;
396            case Surface.ROTATION_270:
397                return 360f - 270f;
398        }
399        return 0f;
400    }
401
402    /** Takes a screenshot of the surface */
403    Bitmap takeScreenshot(Display display) {
404        DisplayMetrics dm = new DisplayMetrics();
405        display.getRealMetrics(dm);
406        float[] dims = {dm.widthPixels, dm.heightPixels};
407        float degrees = getDegreesForRotation(display.getRotation());
408        boolean requiresRotation = (degrees > 0);
409        if (requiresRotation) {
410            // Get the dimensions of the device in its native orientation
411            Matrix m = new Matrix();
412            m.preRotate(-degrees);
413            m.mapPoints(dims);
414            dims[0] = Math.abs(dims[0]);
415            dims[1] = Math.abs(dims[1]);
416        }
417        return SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
418    }
419
420    /** Starts the recents activity */
421    void startAlternateRecentsActivity() {
422        Rect taskRect = mFirstTaskRect;
423        if (taskRect != null && taskRect.width() > 0 && taskRect.height() > 0 && hasFirstTask()) {
424            // Loading from thumbnail
425            Bitmap thumbnail;
426            Bitmap firstThumbnail = loadFirstTaskThumbnail();
427            if (firstThumbnail == null) {
428                // Load the thumbnail from the screenshot
429                WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
430                Display display = wm.getDefaultDisplay();
431                Bitmap screenshot = takeScreenshot(display);
432                Resources res = mContext.getResources();
433                int size = Math.min(screenshot.getWidth(), screenshot.getHeight());
434                int statusBarHeight = res.getDimensionPixelSize(
435                        com.android.internal.R.dimen.status_bar_height);
436                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
437                        Bitmap.Config.ARGB_8888);
438                Canvas c = new Canvas(thumbnail);
439                c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size),
440                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
441                c.setBitmap(null);
442                // Recycle the old screenshot
443                screenshot.recycle();
444            } else {
445                // Create the thumbnail
446                thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(),
447                        Bitmap.Config.ARGB_8888);
448                int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight());
449                Canvas c = new Canvas(thumbnail);
450                c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size),
451                        new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null);
452                c.setBitmap(null);
453                // Recycle the old thumbnail
454                firstThumbnail.recycle();
455            }
456
457            ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView,
458                    thumbnail, mFirstTaskRect.left, mFirstTaskRect.top, null);
459            startAlternateRecentsActivity(opts);
460        } else {
461            ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
462                    R.anim.recents_from_launcher_enter,
463                    R.anim.recents_from_launcher_exit);
464            startAlternateRecentsActivity(opts);
465        }
466    }
467
468    /** Starts the recents activity */
469    void startAlternateRecentsActivity(ActivityOptions opts) {
470        Intent intent = new Intent(sToggleRecentsAction);
471        intent.setClassName(sRecentsPackage, sRecentsActivity);
472        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
473                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
474        if (opts != null) {
475            mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle(
476                    UserHandle.USER_CURRENT));
477        } else {
478            mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
479        }
480    }
481
482    @Override
483    public void preloadRecentTasksList() {
484        if (mUseAlternateRecents) {
485            Log.d(TAG, "[RecentsComponent|preloadRecents]");
486        } else {
487            Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT);
488            intent.setClassName("com.android.systemui",
489                    "com.android.systemui.recent.RecentsPreloadReceiver");
490            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
491
492            RecentTasksLoader.getInstance(mContext).preloadFirstTask();
493        }
494    }
495
496    @Override
497    public void cancelPreloadingRecentTasksList() {
498        if (mUseAlternateRecents) {
499            Log.d(TAG, "[RecentsComponent|cancelPreload]");
500        } else {
501            Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT);
502            intent.setClassName("com.android.systemui",
503                    "com.android.systemui.recent.RecentsPreloadReceiver");
504            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
505
506            RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask();
507        }
508    }
509
510    @Override
511    public void closeRecents() {
512        if (mUseAlternateRecents) {
513            Log.d(TAG, "[RecentsComponent|closeRecents]");
514        } else {
515            Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT);
516            intent.setPackage("com.android.systemui");
517            mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT));
518
519            RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask();
520        }
521    }
522}
523