AlternateRecentsComponent.java revision 083baf99ff1228e96ede96aac88c8200c4fdc2b2
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.res.Configuration; 26import android.graphics.Bitmap; 27import android.graphics.Canvas; 28import android.graphics.Rect; 29import android.os.Handler; 30import android.os.UserHandle; 31import android.view.View; 32import com.android.systemui.R; 33import com.android.systemui.RecentsComponent; 34import com.android.systemui.recents.misc.Console; 35import com.android.systemui.recents.misc.SystemServicesProxy; 36import com.android.systemui.recents.model.RecentsTaskLoader; 37import com.android.systemui.recents.model.Task; 38import com.android.systemui.recents.model.TaskStack; 39import com.android.systemui.recents.views.TaskStackView; 40import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm; 41import com.android.systemui.recents.views.TaskViewTransform; 42 43import java.util.ArrayList; 44import java.util.List; 45import java.util.concurrent.atomic.AtomicBoolean; 46 47/** A proxy implementation for the recents component */ 48public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { 49 50 final public static String EXTRA_FROM_HOME = "recents.triggeredOverHome"; 51 final public static String EXTRA_FROM_APP_THUMBNAIL = "recents.animatingWithThumbnail"; 52 final public static String EXTRA_FROM_APP_FULL_SCREENSHOT = "recents.thumbnail"; 53 final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab"; 54 final public static String EXTRA_TRIGGERED_FROM_TASK_ID = "recents.activeTaskId"; 55 56 final static int sMinToggleDelay = 425; 57 58 final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; 59 final static String sRecentsPackage = "com.android.systemui"; 60 final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; 61 62 static Bitmap sLastScreenshot; 63 static RecentsComponent.Callbacks sRecentsComponentCallbacks; 64 65 Context mContext; 66 SystemServicesProxy mSystemServicesProxy; 67 68 // Recents service binding 69 Handler mHandler; 70 boolean mBootCompleted = false; 71 72 // Task launching 73 RecentsConfiguration mConfig; 74 Rect mWindowRect; 75 Rect mTaskStackBounds; 76 TaskViewTransform mTmpTransform = new TaskViewTransform(); 77 int mStatusBarHeight; 78 79 // Variables to keep track of if we need to start recents after binding 80 View mStatusBarView; 81 boolean mTriggeredFromAltTab; 82 long mLastToggleTime; 83 84 public AlternateRecentsComponent(Context context) { 85 mContext = context; 86 mSystemServicesProxy = new SystemServicesProxy(context); 87 mHandler = new Handler(); 88 mConfig = RecentsConfiguration.reinitialize(context); 89 mWindowRect = mSystemServicesProxy.getWindowRect(); 90 mTaskStackBounds = new Rect(); 91 mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mTaskStackBounds); 92 mStatusBarHeight = mContext.getResources().getDimensionPixelSize( 93 com.android.internal.R.dimen.status_bar_height); 94 } 95 96 public void onStart() { 97 if (Console.Enabled) { 98 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|start]"); 99 } 100 } 101 102 public void onBootCompleted() { 103 mBootCompleted = true; 104 } 105 106 /** Shows the recents */ 107 public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) { 108 if (Console.Enabled) { 109 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|showRecents]"); 110 } 111 mStatusBarView = statusBarView; 112 mTriggeredFromAltTab = triggeredFromAltTab; 113 114 try { 115 startRecentsActivity(); 116 } catch (ActivityNotFoundException e) { 117 Console.logRawError("Failed to launch RecentAppsIntent", e); 118 } 119 } 120 121 /** Hides the recents */ 122 public void onHideRecents(boolean triggeredFromAltTab) { 123 if (Console.Enabled) { 124 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]"); 125 } 126 127 if (mBootCompleted) { 128 if (isRecentsTopMost(getTopMostTask(), null)) { 129 // Notify recents to hide itself 130 Intent intent = new Intent(RecentsActivity.ACTION_HIDE_RECENTS_ACTIVITY); 131 intent.setPackage(mContext.getPackageName()); 132 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 133 intent.putExtra(RecentsActivity.EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); 134 mContext.sendBroadcast(intent); 135 } 136 } 137 } 138 139 /** Toggles the alternate recents activity */ 140 public void onToggleRecents(View statusBarView) { 141 if (Console.Enabled) { 142 Console.logStartTracingTime(Constants.Log.App.TimeRecentsStartup, 143 Constants.Log.App.TimeRecentsStartupKey); 144 Console.logStartTracingTime(Constants.Log.App.TimeRecentsLaunchTask, 145 Constants.Log.App.TimeRecentsLaunchKey); 146 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|toggleRecents]", ""); 147 } 148 mStatusBarView = statusBarView; 149 mTriggeredFromAltTab = false; 150 151 try { 152 toggleRecentsActivity(); 153 } catch (ActivityNotFoundException e) { 154 Console.logRawError("Failed to launch RecentAppsIntent", e); 155 } 156 } 157 158 public void onPreloadRecents() { 159 // Do nothing 160 } 161 162 public void onCancelPreloadingRecents() { 163 // Do nothing 164 } 165 166 public void onConfigurationChanged(Configuration newConfig) { 167 mConfig = RecentsConfiguration.reinitialize(mContext); 168 mConfig.updateOnConfigurationChange(); 169 mWindowRect = mSystemServicesProxy.getWindowRect(); 170 mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mTaskStackBounds); 171 sLastScreenshot = null; 172 } 173 174 /** Gets the top task. */ 175 ActivityManager.RunningTaskInfo getTopMostTask() { 176 SystemServicesProxy ssp = mSystemServicesProxy; 177 List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1); 178 if (!tasks.isEmpty()) { 179 return tasks.get(0); 180 } 181 return null; 182 } 183 184 /** Returns whether the recents is currently running */ 185 boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, AtomicBoolean isHomeTopMost) { 186 SystemServicesProxy ssp = mSystemServicesProxy; 187 if (topTask != null) { 188 ComponentName topActivity = topTask.topActivity; 189 190 // Check if the front most activity is recents 191 if (topActivity.getPackageName().equals(sRecentsPackage) && 192 topActivity.getClassName().equals(sRecentsActivity)) { 193 if (isHomeTopMost != null) { 194 isHomeTopMost.set(false); 195 } 196 return true; 197 } 198 199 if (isHomeTopMost != null) { 200 isHomeTopMost.set(ssp.isInHomeStack(topTask.id)); 201 } 202 } 203 return false; 204 } 205 206 /** Toggles the recents activity */ 207 void toggleRecentsActivity() { 208 // If the user has toggled it too quickly, then just eat up the event here (it's better than 209 // showing a janky screenshot). 210 // NOTE: Ideally, the screenshot mechanism would take the window transform into account 211 if (System.currentTimeMillis() - mLastToggleTime < sMinToggleDelay) { 212 return; 213 } 214 215 // If Recents is the front most activity, then we should just communicate with it directly 216 // to launch the first task or dismiss itself 217 ActivityManager.RunningTaskInfo topTask = getTopMostTask(); 218 AtomicBoolean isTopTaskHome = new AtomicBoolean(); 219 if (isRecentsTopMost(topTask, isTopTaskHome)) { 220 // Notify recents to toggle itself 221 Intent intent = new Intent(RecentsActivity.ACTION_TOGGLE_RECENTS_ACTIVITY); 222 intent.setPackage(mContext.getPackageName()); 223 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 224 mContext.sendBroadcast(intent); 225 226 // Time this path 227 if (Console.Enabled) { 228 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 229 Constants.Log.App.TimeRecentsStartupKey, "receivedToggleRecents"); 230 Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, 231 Constants.Log.App.TimeRecentsLaunchKey, "receivedToggleRecents"); 232 } 233 mLastToggleTime = System.currentTimeMillis(); 234 return; 235 } else { 236 // Otherwise, start the recents activity 237 startRecentsActivity(topTask, isTopTaskHome.get()); 238 } 239 } 240 241 /** Starts the recents activity if it is not already running */ 242 void startRecentsActivity() { 243 // Check if the top task is in the home stack, and start the recents activity 244 ActivityManager.RunningTaskInfo topTask = getTopMostTask(); 245 AtomicBoolean isTopTaskHome = new AtomicBoolean(); 246 if (!isRecentsTopMost(topTask, isTopTaskHome)) { 247 startRecentsActivity(topTask, isTopTaskHome.get()); 248 } 249 } 250 251 /** 252 * Creates the activity options for a unknown state->recents transition. 253 */ 254 ActivityOptions getUnknownTransitionActivityOptions() { 255 return ActivityOptions.makeCustomAnimation(mContext, 256 R.anim.recents_from_unknown_enter, 257 R.anim.recents_from_unknown_exit, mHandler, this); 258 } 259 260 /** 261 * Creates the activity options for a home->recents transition. 262 */ 263 ActivityOptions getHomeTransitionActivityOptions() { 264 return ActivityOptions.makeCustomAnimation(mContext, 265 R.anim.recents_from_launcher_enter, 266 R.anim.recents_from_launcher_exit, mHandler, this); 267 } 268 269 /** 270 * Creates the activity options for an app->recents transition. If this method sets the static 271 * screenshot, then we will use that for the transition. 272 */ 273 ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask) { 274 275 if (Constants.DebugFlags.App.EnableScreenshotAppTransition) { 276 // Recycle the last screenshot 277 consumeLastScreenshot(); 278 279 // Take the full screenshot 280 sLastScreenshot = mSystemServicesProxy.takeAppScreenshot(); 281 if (sLastScreenshot != null) { 282 return ActivityOptions.makeCustomAnimation(mContext, 283 R.anim.recents_from_app_enter, 284 R.anim.recents_from_app_exit, mHandler, this); 285 } 286 } 287 288 // If the screenshot fails, then load the first task thumbnail and use that 289 Bitmap firstThumbnail = mSystemServicesProxy.getTaskThumbnail(topTask.id); 290 if (firstThumbnail != null) { 291 // Update the destination rect 292 Rect toTaskRect = getThumbnailTransitionRect(topTask.id); 293 if (toTaskRect.width() > 0 && toTaskRect.height() > 0) { 294 // Create the new thumbnail for the animation down 295 // XXX: We should find a way to optimize this so we don't need to create a new bitmap 296 Bitmap thumbnail = Bitmap.createBitmap(toTaskRect.width(), toTaskRect.height(), 297 Bitmap.Config.ARGB_8888); 298 int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); 299 Canvas c = new Canvas(thumbnail); 300 c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), 301 new Rect(0, 0, toTaskRect.width(), toTaskRect.height()), null); 302 c.setBitmap(null); 303 // Recycle the old thumbnail 304 firstThumbnail.recycle(); 305 return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, 306 thumbnail, toTaskRect.left, toTaskRect.top, this); 307 } 308 } 309 310 // If both the screenshot and thumbnail fails, then just fall back to the default transition 311 return getUnknownTransitionActivityOptions(); 312 } 313 314 /** Returns the transition rect for the given task id. */ 315 Rect getThumbnailTransitionRect(int runningTaskId) { 316 // Get the stack of tasks that we are animating into 317 TaskStack stack = RecentsTaskLoader.getShallowTaskStack(mSystemServicesProxy); 318 if (stack.getTaskCount() == 0) { 319 return new Rect(); 320 } 321 322 // Get the stack 323 TaskStackView tsv = new TaskStackView(mContext, stack); 324 TaskStackViewLayoutAlgorithm algo = tsv.getStackAlgorithm(); 325 tsv.computeRects(mTaskStackBounds.width(), mTaskStackBounds.height() - mStatusBarHeight, 0, 0); 326 tsv.setStackScrollToInitialState(); 327 328 // Find the running task in the TaskStack 329 Task task = null; 330 ArrayList<Task> tasks = stack.getTasks(); 331 if (runningTaskId != -1) { 332 // Otherwise, try and find the task with the 333 int taskCount = tasks.size(); 334 for (int i = taskCount - 1; i >= 0; i--) { 335 Task t = tasks.get(i); 336 if (t.key.id == runningTaskId) { 337 task = t; 338 break; 339 } 340 } 341 } 342 if (task == null) { 343 // If no task is specified or we can not find the task just use the front most one 344 task = tasks.get(tasks.size() - 1); 345 } 346 347 // Get the transform for the running task 348 mTmpTransform = algo.getStackTransform(task, tsv.getStackScroll(), mTmpTransform); 349 mTmpTransform.rect.offset(mTaskStackBounds.left, mTaskStackBounds.top); 350 mTmpTransform.rect.offset(0, mStatusBarHeight); 351 return new Rect(mTmpTransform.rect); 352 } 353 354 /** Starts the recents activity */ 355 void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) { 356 // If Recents is not the front-most activity and we should animate into it. If 357 // the activity at the root of the top task stack in the home stack, then we just do a 358 // simple transition. Otherwise, we animate to the rects defined by the Recents service, 359 // which can differ depending on the number of items in the list. 360 SystemServicesProxy ssp = mSystemServicesProxy; 361 List<ActivityManager.RecentTaskInfo> recentTasks = 362 ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier()); 363 boolean useThumbnailTransition = !isTopTaskHome; 364 boolean hasRecentTasks = !recentTasks.isEmpty(); 365 366 if (useThumbnailTransition) { 367 // Try starting with a thumbnail transition 368 ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask); 369 if (opts != null) { 370 if (sLastScreenshot != null) { 371 startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_FULL_SCREENSHOT); 372 } else { 373 startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_APP_THUMBNAIL); 374 } 375 } else { 376 // Fall through below to the non-thumbnail transition 377 useThumbnailTransition = false; 378 } 379 } 380 381 if (!useThumbnailTransition) { 382 // If there is no thumbnail transition, but is launching from home into recents, then 383 // use a quick home transition and do the animation from home 384 if (hasRecentTasks) { 385 ActivityOptions opts = getHomeTransitionActivityOptions(); 386 startAlternateRecentsActivity(topTask, opts, EXTRA_FROM_HOME); 387 } else { 388 // Otherwise we do the normal fade from an unknown source 389 ActivityOptions opts = getUnknownTransitionActivityOptions(); 390 startAlternateRecentsActivity(topTask, opts, null); 391 } 392 } 393 394 if (Console.Enabled) { 395 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 396 Constants.Log.App.TimeRecentsStartupKey, "startRecentsActivity"); 397 } 398 mLastToggleTime = System.currentTimeMillis(); 399 } 400 401 /** Starts the recents activity */ 402 void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask, 403 ActivityOptions opts, String extraFlag) { 404 Intent intent = new Intent(sToggleRecentsAction); 405 intent.setClassName(sRecentsPackage, sRecentsActivity); 406 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 407 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 408 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 409 if (extraFlag != null) { 410 intent.putExtra(extraFlag, true); 411 } 412 intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab); 413 intent.putExtra(EXTRA_TRIGGERED_FROM_TASK_ID, (topTask != null) ? topTask.id : -1); 414 if (opts != null) { 415 mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( 416 UserHandle.USER_CURRENT)); 417 } else { 418 mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); 419 } 420 } 421 422 /** Returns the last screenshot taken, this will be called by the RecentsActivity. */ 423 public static Bitmap getLastScreenshot() { 424 return sLastScreenshot; 425 } 426 427 /** Recycles the last screenshot taken, this will be called by the RecentsActivity. */ 428 public static void consumeLastScreenshot() { 429 if (sLastScreenshot != null) { 430 sLastScreenshot.recycle(); 431 sLastScreenshot = null; 432 } 433 } 434 435 /** Sets the RecentsComponent callbacks. */ 436 public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) { 437 sRecentsComponentCallbacks = cb; 438 } 439 440 /** Notifies the callbacks that the visibility of Recents has changed. */ 441 public static void notifyVisibilityChanged(boolean visible) { 442 if (sRecentsComponentCallbacks != null) { 443 sRecentsComponentCallbacks.onVisibilityChanged(visible); 444 } 445 } 446 447 /**** OnAnimationStartedListener Implementation ****/ 448 449 @Override 450 public void onAnimationStarted() { 451 // Notify recents to start the enter animation 452 Intent intent = new Intent(RecentsActivity.ACTION_START_ENTER_ANIMATION); 453 intent.setPackage(mContext.getPackageName()); 454 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); 455 mContext.sendBroadcast(intent); 456 } 457} 458