/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.recents; import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.util.DisplayMetrics; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import com.android.systemui.R; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** A proxy implementation for the recents component */ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { /** A handler for messages from the recents implementation */ class RecentsMessageHandler extends Handler { @Override public void handleMessage(Message msg) { if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) { Resources res = mContext.getResources(); float statusBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); Bundle replyData = msg.getData().getParcelable(KEY_CONFIGURATION_DATA); mSingleCountFirstTaskRect = replyData.getParcelable(KEY_SINGLE_TASK_STACK_RECT); mSingleCountFirstTaskRect.offset(0, (int) statusBarHeight); mTwoCountFirstTaskRect = replyData.getParcelable(KEY_TWO_TASK_STACK_RECT); mTwoCountFirstTaskRect.offset(0, (int) statusBarHeight); mMultipleCountFirstTaskRect = replyData.getParcelable(KEY_MULTIPLE_TASK_STACK_RECT); mMultipleCountFirstTaskRect.offset(0, (int) statusBarHeight); if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|RecentsMessageHandler|handleMessage]", "singleTaskRect: " + mSingleCountFirstTaskRect + " twoTaskRect: " + mTwoCountFirstTaskRect + " multipleTaskRect: " + mMultipleCountFirstTaskRect); } // If we had the update the animation rects as a result of onServiceConnected, then // we check for whether we need to toggle the recents here. if (mToggleRecentsUponServiceBound) { startRecentsActivity(); mToggleRecentsUponServiceBound = false; } } } } /** A service connection to the recents implementation */ class RecentsServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName className, IBinder service) { if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|ServiceConnection|onServiceConnected]", "toggleRecents: " + mToggleRecentsUponServiceBound); } mService = new Messenger(service); mServiceIsBound = true; if (hasValidTaskRects()) { // Start recents if this new service connection was triggered by hitting recents if (mToggleRecentsUponServiceBound) { startRecentsActivity(); mToggleRecentsUponServiceBound = false; } } else { // Otherwise, update the animation rects before starting the recents if requested updateAnimationRects(); } } @Override public void onServiceDisconnected(ComponentName className) { if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|ServiceConnection|onServiceDisconnected]"); } mService = null; mServiceIsBound = false; } } final public static int MSG_UPDATE_FOR_CONFIGURATION = 0; final public static int MSG_UPDATE_TASK_THUMBNAIL = 1; final public static int MSG_PRELOAD_TASKS = 2; final public static int MSG_CANCEL_PRELOAD_TASKS = 3; final public static int MSG_SHOW_RECENTS = 4; final public static int MSG_HIDE_RECENTS = 5; final public static int MSG_TOGGLE_RECENTS = 6; final public static int MSG_START_ENTER_ANIMATION = 7; final public static String EXTRA_ANIMATING_WITH_THUMBNAIL = "recents.animatingWithThumbnail"; final public static String EXTRA_FROM_ALT_TAB = "recents.triggeredFromAltTab"; final public static String KEY_CONFIGURATION_DATA = "recents.data.updateForConfiguration"; final public static String KEY_WINDOW_RECT = "recents.windowRect"; final public static String KEY_SYSTEM_INSETS = "recents.systemInsets"; final public static String KEY_SINGLE_TASK_STACK_RECT = "recents.singleCountTaskRect"; final public static String KEY_TWO_TASK_STACK_RECT = "recents.twoCountTaskRect"; final public static String KEY_MULTIPLE_TASK_STACK_RECT = "recents.multipleCountTaskRect"; final static int sMinToggleDelay = 425; final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; final static String sRecentsPackage = "com.android.systemui"; final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; final static String sRecentsService = "com.android.systemui.recents.RecentsService"; Context mContext; SystemServicesProxy mSystemServicesProxy; // Recents service binding Messenger mService = null; Messenger mMessenger; RecentsMessageHandler mHandler; boolean mServiceIsBound = false; boolean mToggleRecentsUponServiceBound; RecentsServiceConnection mConnection = new RecentsServiceConnection(); // Variables to keep track of if we need to start recents after binding View mStatusBarView; boolean mTriggeredFromAltTab; Rect mSingleCountFirstTaskRect = new Rect(); Rect mTwoCountFirstTaskRect = new Rect(); Rect mMultipleCountFirstTaskRect = new Rect(); long mLastToggleTime; public AlternateRecentsComponent(Context context) { mContext = context; mSystemServicesProxy = new SystemServicesProxy(context); mHandler = new RecentsMessageHandler(); mMessenger = new Messenger(mHandler); } public void onStart() { if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|start]"); } // Try to create a long-running connection to the recents service bindToRecentsService(false); } /** Shows the recents */ public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) { if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|showRecents]"); } mStatusBarView = statusBarView; mTriggeredFromAltTab = triggeredFromAltTab; if (!mServiceIsBound) { // Try to create a long-running connection to the recents service before toggling // recents bindToRecentsService(true); return; } try { startRecentsActivity(); } catch (ActivityNotFoundException e) { Console.logRawError("Failed to launch RecentAppsIntent", e); } } /** Hides the recents */ public void onHideRecents(boolean triggeredFromAltTab) { if (Console.Enabled) { Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]"); } if (mServiceIsBound) { // Notify recents to close it try { Bundle data = new Bundle(); Message msg = Message.obtain(null, MSG_HIDE_RECENTS, triggeredFromAltTab ? 1 : 0, 0); msg.setData(data); mService.send(msg); } catch (RemoteException re) { re.printStackTrace(); } } } /** Toggles the alternate recents activity */ public void onToggleRecents(View statusBarView) { if (Console.Enabled) { Console.logStartTracingTime(Constants.Log.App.TimeRecentsStartup, Constants.Log.App.TimeRecentsStartupKey); Console.logStartTracingTime(Constants.Log.App.TimeRecentsLaunchTask, Constants.Log.App.TimeRecentsLaunchKey); Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|toggleRecents]", "serviceIsBound: " + mServiceIsBound); } mStatusBarView = statusBarView; mTriggeredFromAltTab = false; if (!mServiceIsBound) { // Try to create a long-running connection to the recents service before toggling // recents bindToRecentsService(true); return; } try { toggleRecentsActivity(); } catch (ActivityNotFoundException e) { Console.logRawError("Failed to launch RecentAppsIntent", e); } } public void onPreloadRecents() { // Do nothing } public void onCancelPreloadingRecents() { // Do nothing } public void onConfigurationChanged(Configuration newConfig) { updateAnimationRects(); } /** Binds to the recents implementation */ private void bindToRecentsService(boolean toggleRecentsUponConnection) { mToggleRecentsUponServiceBound = toggleRecentsUponConnection; Intent intent = new Intent(); intent.setClassName(sRecentsPackage, sRecentsService); mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } /** Returns whether we have valid task rects to animate to. */ boolean hasValidTaskRects() { return mSingleCountFirstTaskRect != null && mSingleCountFirstTaskRect.width() > 0 && mSingleCountFirstTaskRect.height() > 0 && mTwoCountFirstTaskRect != null && mTwoCountFirstTaskRect.width() > 0 && mTwoCountFirstTaskRect.height() > 0 && mMultipleCountFirstTaskRect != null && mMultipleCountFirstTaskRect.width() > 0 && mMultipleCountFirstTaskRect.height() > 0; } /** Updates each of the task animation rects. */ void updateAnimationRects() { if (mServiceIsBound) { Resources res = mContext.getResources(); int statusBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); int navBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_height); Rect rect = new Rect(); WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getRectSize(rect); // Try and update the recents configuration try { Bundle data = new Bundle(); data.putParcelable(KEY_WINDOW_RECT, rect); data.putParcelable(KEY_SYSTEM_INSETS, new Rect(0, statusBarHeight, 0, 0)); Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0); msg.setData(data); msg.replyTo = mMessenger; mService.send(msg); } catch (RemoteException re) { re.printStackTrace(); } } } /** Loads the first task thumbnail */ Bitmap loadFirstTaskThumbnail() { SystemServicesProxy ssp = mSystemServicesProxy; List tasks = ssp.getRunningTasks(1); for (ActivityManager.RunningTaskInfo t : tasks) { return ssp.getTaskThumbnail(t.id); } return null; } /** Returns the proper rect to use for the animation, given the number of tasks. */ Rect getAnimationTaskRect(List tasks) { // NOTE: Currently there's no method to get the number of non-home tasks, so we have to // compute this ourselves SystemServicesProxy ssp = mSystemServicesProxy; Iterator iter = tasks.iterator(); while (iter.hasNext()) { ActivityManager.RecentTaskInfo t = iter.next(); // Skip tasks in the home stack if (ssp.isInHomeStack(t.persistentId)) { iter.remove(); continue; } } if (tasks.size() <= 1) { return mSingleCountFirstTaskRect; } else if (tasks.size() <= 2) { return mTwoCountFirstTaskRect; } else { return mMultipleCountFirstTaskRect; } } /** Converts from the device rotation to the degree */ float getDegreesForRotation(int value) { switch (value) { case Surface.ROTATION_90: return 360f - 90f; case Surface.ROTATION_180: return 360f - 180f; case Surface.ROTATION_270: return 360f - 270f; } return 0f; } /** Takes a screenshot of the surface */ Bitmap takeScreenshot(Display display) { DisplayMetrics dm = new DisplayMetrics(); display.getRealMetrics(dm); float[] dims = {dm.widthPixels, dm.heightPixels}; float degrees = getDegreesForRotation(display.getRotation()); boolean requiresRotation = (degrees > 0); if (requiresRotation) { // Get the dimensions of the device in its native orientation Matrix m = new Matrix(); m.preRotate(-degrees); m.mapPoints(dims); dims[0] = Math.abs(dims[0]); dims[1] = Math.abs(dims[1]); } return SurfaceControl.screenshot((int) dims[0], (int) dims[1]); } /** Creates the activity options for a thumbnail transition. */ ActivityOptions getThumbnailTransitionActivityOptions(Rect taskRect) { // Loading from thumbnail Bitmap thumbnail; Bitmap firstThumbnail = loadFirstTaskThumbnail(); if (firstThumbnail != null) { // Create the thumbnail thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(), Bitmap.Config.ARGB_8888); int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); Canvas c = new Canvas(thumbnail); c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), new Rect(0, 0, taskRect.width(), taskRect.height()), null); c.setBitmap(null); // Recycle the old thumbnail firstThumbnail.recycle(); } else { // Load the thumbnail from the screenshot if can't get one from the system WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); Bitmap screenshot = takeScreenshot(display); if (screenshot != null) { Resources res = mContext.getResources(); int size = Math.min(screenshot.getWidth(), screenshot.getHeight()); int statusBarHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.status_bar_height); thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(thumbnail); c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size), new Rect(0, 0, taskRect.width(), taskRect.height()), null); c.setBitmap(null); // Recycle the temporary screenshot screenshot.recycle(); } else { return null; } } return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, thumbnail, taskRect.left, taskRect.top, this); } /** Returns whether the recents is currently running */ boolean isRecentsTopMost(AtomicBoolean isHomeTopMost) { SystemServicesProxy ssp = mSystemServicesProxy; List tasks = ssp.getRunningTasks(1); if (!tasks.isEmpty()) { ActivityManager.RunningTaskInfo topTask = tasks.get(0); ComponentName topActivity = topTask.topActivity; // Check if the front most activity is recents if (topActivity.getPackageName().equals(sRecentsPackage) && topActivity.getClassName().equals(sRecentsActivity)) { if (isHomeTopMost != null) { isHomeTopMost.set(false); } return true; } if (isHomeTopMost != null) { isHomeTopMost.set(ssp.isInHomeStack(topTask.id)); } } return false; } /** Toggles the recents activity */ void toggleRecentsActivity() { // If the user has toggled it too quickly, then just eat up the event here (it's better than // showing a janky screenshot). // NOTE: Ideally, the screenshot mechanism would take the window transform into account if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) { return; } // If Recents is the front most activity, then we should just communicate with it directly // to launch the first task or dismiss itself AtomicBoolean isTopTaskHome = new AtomicBoolean(); if (isRecentsTopMost(isTopTaskHome)) { // Notify recents to close itself try { Bundle data = new Bundle(); Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0); msg.setData(data); mService.send(msg); // Time this path Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, Constants.Log.App.TimeRecentsStartupKey, "sendToggleRecents"); Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, Constants.Log.App.TimeRecentsLaunchKey, "sendToggleRecents"); } catch (RemoteException re) { re.printStackTrace(); } mLastToggleTime = System.currentTimeMillis(); return; } else { // Otherwise, start the recents activity startRecentsActivity(isTopTaskHome.get()); } } /** Starts the recents activity if it is not already running */ void startRecentsActivity() { // Check if the top task is in the home stack, and start the recents activity AtomicBoolean isTopTaskHome = new AtomicBoolean(); if (!isRecentsTopMost(isTopTaskHome)) { startRecentsActivity(isTopTaskHome.get()); } } /** Starts the recents activity */ void startRecentsActivity(boolean isTopTaskHome) { // If Recents is not the front-most activity and we should animate into it. If // the activity at the root of the top task stack in the home stack, then we just do a // simple transition. Otherwise, we animate to the rects defined by the Recents service, // which can differ depending on the number of items in the list. SystemServicesProxy ssp = mSystemServicesProxy; List recentTasks = ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier()); Rect taskRect = getAnimationTaskRect(recentTasks); boolean useThumbnailTransition = !isTopTaskHome && hasValidTaskRects(); if (useThumbnailTransition) { // Try starting with a thumbnail transition ActivityOptions opts = getThumbnailTransitionActivityOptions(taskRect); if (opts != null) { startAlternateRecentsActivity(opts, true); } else { // Fall through below to the non-thumbnail transition useThumbnailTransition = false; } } // If there is no thumbnail transition, then just use a generic transition if (!useThumbnailTransition) { ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_launcher_enter, R.anim.recents_from_launcher_exit, mHandler, this); startAlternateRecentsActivity(opts, false); } Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, Constants.Log.App.TimeRecentsStartupKey, "startRecentsActivity"); mLastToggleTime = System.currentTimeMillis(); } /** Starts the recents activity */ void startAlternateRecentsActivity(ActivityOptions opts, boolean animatingWithThumbnail) { Intent intent = new Intent(sToggleRecentsAction); intent.setClassName(sRecentsPackage, sRecentsActivity); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); intent.putExtra(EXTRA_ANIMATING_WITH_THUMBNAIL, animatingWithThumbnail); intent.putExtra(EXTRA_FROM_ALT_TAB, mTriggeredFromAltTab); if (opts != null) { mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( UserHandle.USER_CURRENT)); } else { mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); } } /**** OnAnimationStartedListener Implementation ****/ @Override public void onAnimationStarted() { // Notify recents to start the enter animation try { Message msg = Message.obtain(null, MSG_START_ENTER_ANIMATION, 0, 0); mService.send(msg); } catch (RemoteException re) { re.printStackTrace(); } } }