AlternateRecentsComponent.java revision 743d5c95f3a107639c0ff22f099cab2624da3e27
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.Rect; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.IBinder; 34import android.os.Message; 35import android.os.Messenger; 36import android.os.RemoteException; 37import android.os.UserHandle; 38import android.view.View; 39import android.view.WindowManager; 40import com.android.systemui.R; 41import com.android.systemui.RecentsComponent; 42 43import java.lang.ref.WeakReference; 44import java.util.Iterator; 45import java.util.List; 46import java.util.concurrent.atomic.AtomicBoolean; 47 48/** A proxy implementation for the recents component */ 49public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { 50 51 /** A handler for messages from the recents implementation */ 52 class RecentsMessageHandler extends Handler { 53 @Override 54 public void handleMessage(Message msg) { 55 if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) { 56 Resources res = mContext.getResources(); 57 float statusBarHeight = res.getDimensionPixelSize( 58 com.android.internal.R.dimen.status_bar_height); 59 Bundle replyData = msg.getData().getParcelable(KEY_CONFIGURATION_DATA); 60 mSingleCountFirstTaskRect = replyData.getParcelable(KEY_SINGLE_TASK_STACK_RECT); 61 mSingleCountFirstTaskRect.offset(0, (int) statusBarHeight); 62 mTwoCountFirstTaskRect = replyData.getParcelable(KEY_TWO_TASK_STACK_RECT); 63 mTwoCountFirstTaskRect.offset(0, (int) statusBarHeight); 64 mMultipleCountFirstTaskRect = replyData.getParcelable(KEY_MULTIPLE_TASK_STACK_RECT); 65 mMultipleCountFirstTaskRect.offset(0, (int) statusBarHeight); 66 if (Console.Enabled) { 67 Console.log(Constants.Log.App.RecentsComponent, 68 "[RecentsComponent|RecentsMessageHandler|handleMessage]", 69 "singleTaskRect: " + mSingleCountFirstTaskRect + 70 " twoTaskRect: " + mTwoCountFirstTaskRect + 71 " multipleTaskRect: " + mMultipleCountFirstTaskRect); 72 } 73 74 // If we had the update the animation rects as a result of onServiceConnected, then 75 // we check for whether we need to toggle the recents here. 76 if (mToggleRecentsUponServiceBound) { 77 startRecentsActivity(); 78 mToggleRecentsUponServiceBound = false; 79 } 80 } 81 } 82 } 83 84 /** A service connection to the recents implementation */ 85 class RecentsServiceConnection implements ServiceConnection { 86 @Override 87 public void onServiceConnected(ComponentName className, IBinder service) { 88 if (Console.Enabled) { 89 Console.log(Constants.Log.App.RecentsComponent, 90 "[RecentsComponent|ServiceConnection|onServiceConnected]", 91 "toggleRecents: " + mToggleRecentsUponServiceBound); 92 } 93 mService = new Messenger(service); 94 mServiceIsBound = true; 95 96 if (hasValidTaskRects()) { 97 // Start recents if this new service connection was triggered by hitting recents 98 if (mToggleRecentsUponServiceBound) { 99 startRecentsActivity(); 100 mToggleRecentsUponServiceBound = false; 101 } 102 } else { 103 // Otherwise, update the animation rects before starting the recents if requested 104 updateAnimationRects(); 105 } 106 } 107 108 @Override 109 public void onServiceDisconnected(ComponentName className) { 110 if (Console.Enabled) { 111 Console.log(Constants.Log.App.RecentsComponent, 112 "[RecentsComponent|ServiceConnection|onServiceDisconnected]"); 113 } 114 mService = null; 115 mServiceIsBound = false; 116 } 117 } 118 119 final public static int MSG_UPDATE_FOR_CONFIGURATION = 0; 120 final public static int MSG_UPDATE_TASK_THUMBNAIL = 1; 121 final public static int MSG_PRELOAD_TASKS = 2; 122 final public static int MSG_CANCEL_PRELOAD_TASKS = 3; 123 final public static int MSG_SHOW_RECENTS = 4; 124 final public static int MSG_HIDE_RECENTS = 5; 125 final public static int MSG_TOGGLE_RECENTS = 6; 126 final public static int MSG_START_ENTER_ANIMATION = 7; 127 128 final public static String EXTRA_FROM_HOME = "recents.triggeredOverHome"; 129 final public static String EXTRA_FROM_APP_THUMBNAIL = "recents.animatingWithThumbnail"; 130 final public static String EXTRA_FROM_APP_FULL_SCREENSHOT = "recents.thumbnail"; 131 final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "recents.triggeredFromAltTab"; 132 final public static String KEY_CONFIGURATION_DATA = "recents.data.updateForConfiguration"; 133 final public static String KEY_WINDOW_RECT = "recents.windowRect"; 134 final public static String KEY_SYSTEM_INSETS = "recents.systemInsets"; 135 final public static String KEY_SINGLE_TASK_STACK_RECT = "recents.singleCountTaskRect"; 136 final public static String KEY_TWO_TASK_STACK_RECT = "recents.twoCountTaskRect"; 137 final public static String KEY_MULTIPLE_TASK_STACK_RECT = "recents.multipleCountTaskRect"; 138 139 final static int sMinToggleDelay = 425; 140 141 final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; 142 final static String sRecentsPackage = "com.android.systemui"; 143 final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; 144 final static String sRecentsService = "com.android.systemui.recents.RecentsService"; 145 146 static Bitmap sLastScreenshot; 147 static RecentsComponent.Callbacks sRecentsComponentCallbacks; 148 149 Context mContext; 150 SystemServicesProxy mSystemServicesProxy; 151 152 // Recents service binding 153 Messenger mService = null; 154 Messenger mMessenger; 155 RecentsMessageHandler mHandler; 156 boolean mBootCompleted = false; 157 boolean mServiceIsBound = false; 158 boolean mToggleRecentsUponServiceBound; 159 RecentsServiceConnection mConnection = new RecentsServiceConnection(); 160 161 // Variables to keep track of if we need to start recents after binding 162 View mStatusBarView; 163 boolean mTriggeredFromAltTab; 164 165 Rect mSingleCountFirstTaskRect = new Rect(); 166 Rect mTwoCountFirstTaskRect = new Rect(); 167 Rect mMultipleCountFirstTaskRect = new Rect(); 168 long mLastToggleTime; 169 170 public AlternateRecentsComponent(Context context) { 171 mContext = context; 172 mSystemServicesProxy = new SystemServicesProxy(context); 173 mHandler = new RecentsMessageHandler(); 174 mMessenger = new Messenger(mHandler); 175 } 176 177 public void onStart() { 178 if (Console.Enabled) { 179 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|start]"); 180 } 181 182 // Try to create a long-running connection to the recents service 183 bindToRecentsService(false); 184 } 185 186 public void onBootCompleted() { 187 mBootCompleted = true; 188 } 189 190 /** Shows the recents */ 191 public void onShowRecents(boolean triggeredFromAltTab, View statusBarView) { 192 if (Console.Enabled) { 193 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|showRecents]"); 194 } 195 mStatusBarView = statusBarView; 196 mTriggeredFromAltTab = triggeredFromAltTab; 197 if (!mServiceIsBound) { 198 // Try to create a long-running connection to the recents service before toggling 199 // recents 200 bindToRecentsService(true); 201 return; 202 } 203 204 try { 205 startRecentsActivity(); 206 } catch (ActivityNotFoundException e) { 207 Console.logRawError("Failed to launch RecentAppsIntent", e); 208 } 209 } 210 211 /** Hides the recents */ 212 public void onHideRecents(boolean triggeredFromAltTab) { 213 if (Console.Enabled) { 214 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|hideRecents]"); 215 } 216 217 if (mServiceIsBound && mBootCompleted) { 218 if (isRecentsTopMost(null)) { 219 // Notify recents to close it 220 try { 221 Bundle data = new Bundle(); 222 Message msg = Message.obtain(null, MSG_HIDE_RECENTS, 223 triggeredFromAltTab ? 1 : 0, 0); 224 msg.setData(data); 225 mService.send(msg); 226 } catch (RemoteException re) { 227 re.printStackTrace(); 228 } 229 } 230 } 231 } 232 233 /** Toggles the alternate recents activity */ 234 public void onToggleRecents(View statusBarView) { 235 if (Console.Enabled) { 236 Console.logStartTracingTime(Constants.Log.App.TimeRecentsStartup, 237 Constants.Log.App.TimeRecentsStartupKey); 238 Console.logStartTracingTime(Constants.Log.App.TimeRecentsLaunchTask, 239 Constants.Log.App.TimeRecentsLaunchKey); 240 Console.log(Constants.Log.App.RecentsComponent, "[RecentsComponent|toggleRecents]", 241 "serviceIsBound: " + mServiceIsBound); 242 } 243 mStatusBarView = statusBarView; 244 mTriggeredFromAltTab = false; 245 if (!mServiceIsBound) { 246 // Try to create a long-running connection to the recents service before toggling 247 // recents 248 bindToRecentsService(true); 249 return; 250 } 251 252 try { 253 toggleRecentsActivity(); 254 } catch (ActivityNotFoundException e) { 255 Console.logRawError("Failed to launch RecentAppsIntent", e); 256 } 257 } 258 259 public void onPreloadRecents() { 260 // Do nothing 261 } 262 263 public void onCancelPreloadingRecents() { 264 // Do nothing 265 } 266 267 public void onConfigurationChanged(Configuration newConfig) { 268 updateAnimationRects(); 269 } 270 271 /** Binds to the recents implementation */ 272 private void bindToRecentsService(boolean toggleRecentsUponConnection) { 273 mToggleRecentsUponServiceBound = toggleRecentsUponConnection; 274 Intent intent = new Intent(); 275 intent.setClassName(sRecentsPackage, sRecentsService); 276 mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); 277 } 278 279 /** Returns whether we have valid task rects to animate to. */ 280 boolean hasValidTaskRects() { 281 return mSingleCountFirstTaskRect != null && mSingleCountFirstTaskRect.width() > 0 && 282 mSingleCountFirstTaskRect.height() > 0 && mTwoCountFirstTaskRect != null && 283 mTwoCountFirstTaskRect.width() > 0 && mTwoCountFirstTaskRect.height() > 0 && 284 mMultipleCountFirstTaskRect != null && mMultipleCountFirstTaskRect.width() > 0 && 285 mMultipleCountFirstTaskRect.height() > 0; 286 } 287 288 /** Updates each of the task animation rects. */ 289 void updateAnimationRects() { 290 if (mServiceIsBound && mBootCompleted) { 291 Resources res = mContext.getResources(); 292 int statusBarHeight = res.getDimensionPixelSize( 293 com.android.internal.R.dimen.status_bar_height); 294 int navBarHeight = res.getDimensionPixelSize( 295 com.android.internal.R.dimen.navigation_bar_height); 296 Rect rect = new Rect(); 297 WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 298 wm.getDefaultDisplay().getRectSize(rect); 299 300 // Try and update the recents configuration 301 try { 302 Bundle data = new Bundle(); 303 data.putParcelable(KEY_WINDOW_RECT, rect); 304 data.putParcelable(KEY_SYSTEM_INSETS, new Rect(0, statusBarHeight, 0, 0)); 305 Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0); 306 msg.setData(data); 307 msg.replyTo = mMessenger; 308 mService.send(msg); 309 } catch (RemoteException re) { 310 re.printStackTrace(); 311 } 312 } 313 } 314 315 /** Loads the first task thumbnail */ 316 Bitmap loadFirstTaskThumbnail() { 317 SystemServicesProxy ssp = mSystemServicesProxy; 318 List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1); 319 320 for (ActivityManager.RunningTaskInfo t : tasks) { 321 return ssp.getTaskThumbnail(t.id); 322 } 323 return null; 324 } 325 326 /** Returns the proper rect to use for the animation, given the number of tasks. */ 327 Rect getAnimationTaskRect(List<ActivityManager.RecentTaskInfo> tasks) { 328 // NOTE: Currently there's no method to get the number of non-home tasks, so we have to 329 // compute this ourselves 330 SystemServicesProxy ssp = mSystemServicesProxy; 331 Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); 332 while (iter.hasNext()) { 333 ActivityManager.RecentTaskInfo t = iter.next(); 334 335 // Skip tasks in the home stack 336 if (ssp.isInHomeStack(t.persistentId)) { 337 iter.remove(); 338 continue; 339 } 340 } 341 if (tasks.size() <= 1) { 342 return mSingleCountFirstTaskRect; 343 } else if (tasks.size() <= 2) { 344 return mTwoCountFirstTaskRect; 345 } else { 346 return mMultipleCountFirstTaskRect; 347 } 348 } 349 350 /** Returns whether the recents is currently running */ 351 boolean isRecentsTopMost(AtomicBoolean isHomeTopMost) { 352 SystemServicesProxy ssp = mSystemServicesProxy; 353 List<ActivityManager.RunningTaskInfo> tasks = ssp.getRunningTasks(1); 354 if (!tasks.isEmpty()) { 355 ActivityManager.RunningTaskInfo topTask = tasks.get(0); 356 ComponentName topActivity = topTask.topActivity; 357 358 // Check if the front most activity is recents 359 if (topActivity.getPackageName().equals(sRecentsPackage) && 360 topActivity.getClassName().equals(sRecentsActivity)) { 361 if (isHomeTopMost != null) { 362 isHomeTopMost.set(false); 363 } 364 return true; 365 } 366 367 if (isHomeTopMost != null) { 368 isHomeTopMost.set(ssp.isInHomeStack(topTask.id)); 369 } 370 } 371 return false; 372 } 373 374 /** Toggles the recents activity */ 375 void toggleRecentsActivity() { 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 AtomicBoolean isTopTaskHome = new AtomicBoolean(); 386 if (isRecentsTopMost(isTopTaskHome)) { 387 // Notify recents to close itself 388 try { 389 Bundle data = new Bundle(); 390 Message msg = Message.obtain(null, MSG_TOGGLE_RECENTS, 0, 0); 391 msg.setData(data); 392 mService.send(msg); 393 394 // Time this path 395 if (Console.Enabled) { 396 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 397 Constants.Log.App.TimeRecentsStartupKey, "sendToggleRecents"); 398 Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, 399 Constants.Log.App.TimeRecentsLaunchKey, "sendToggleRecents"); 400 } 401 } catch (RemoteException re) { 402 re.printStackTrace(); 403 } 404 mLastToggleTime = System.currentTimeMillis(); 405 return; 406 } else { 407 // Otherwise, start the recents activity 408 startRecentsActivity(isTopTaskHome.get()); 409 } 410 } 411 412 /** Starts the recents activity if it is not already running */ 413 void startRecentsActivity() { 414 // Check if the top task is in the home stack, and start the recents activity 415 AtomicBoolean isTopTaskHome = new AtomicBoolean(); 416 if (!isRecentsTopMost(isTopTaskHome)) { 417 startRecentsActivity(isTopTaskHome.get()); 418 } 419 } 420 421 /** 422 * Creates the activity options for a unknown state->recents transition. 423 */ 424 ActivityOptions getUnknownTransitionActivityOptions() { 425 // Reset the last screenshot 426 consumeLastScreenshot(); 427 return ActivityOptions.makeCustomAnimation(mContext, 428 R.anim.recents_from_unknown_enter, 429 R.anim.recents_from_unknown_exit, mHandler, this); 430 } 431 432 /** 433 * Creates the activity options for a home->recents transition. 434 */ 435 ActivityOptions getHomeTransitionActivityOptions() { 436 // Reset the last screenshot 437 consumeLastScreenshot(); 438 return ActivityOptions.makeCustomAnimation(mContext, 439 R.anim.recents_from_launcher_enter, 440 R.anim.recents_from_launcher_exit, mHandler, this); 441 } 442 443 /** 444 * Creates the activity options for an app->recents transition. If this method sets the static 445 * screenshot, then we will use that for the transition. 446 */ 447 ActivityOptions getThumbnailTransitionActivityOptions(Rect taskRect) { 448 // Recycle the last screenshot 449 consumeLastScreenshot(); 450 451 // Take the full screenshot 452 if (Constants.DebugFlags.App.EnableScreenshotAppTransition) { 453 sLastScreenshot = mSystemServicesProxy.takeScreenshot(); 454 if (sLastScreenshot != null) { 455 return ActivityOptions.makeCustomAnimation(mContext, 456 R.anim.recents_from_app_enter, 457 R.anim.recents_from_app_exit, mHandler, this); 458 } 459 } 460 461 // If the screenshot fails, then load the first task thumbnail and use that 462 Bitmap firstThumbnail = loadFirstTaskThumbnail(); 463 if (firstThumbnail != null) { 464 // Create the new thumbnail for the animation down 465 // XXX: We should find a way to optimize this so we don't need to create a new bitmap 466 Bitmap thumbnail = Bitmap.createBitmap(taskRect.width(), taskRect.height(), 467 Bitmap.Config.ARGB_8888); 468 int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); 469 Canvas c = new Canvas(thumbnail); 470 c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), 471 new Rect(0, 0, taskRect.width(), taskRect.height()), null); 472 c.setBitmap(null); 473 // Recycle the old thumbnail 474 firstThumbnail.recycle(); 475 return ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, 476 thumbnail, taskRect.left, taskRect.top, this); 477 } 478 479 // If both the screenshot and thumbnail fails, then just fall back to the default transition 480 return getUnknownTransitionActivityOptions(); 481 } 482 483 /** Starts the recents activity */ 484 void startRecentsActivity(boolean isTopTaskHome) { 485 // If Recents is not the front-most activity and we should animate into it. If 486 // the activity at the root of the top task stack in the home stack, then we just do a 487 // simple transition. Otherwise, we animate to the rects defined by the Recents service, 488 // which can differ depending on the number of items in the list. 489 SystemServicesProxy ssp = mSystemServicesProxy; 490 List<ActivityManager.RecentTaskInfo> recentTasks = 491 ssp.getRecentTasks(3, UserHandle.CURRENT.getIdentifier()); 492 Rect taskRect = getAnimationTaskRect(recentTasks); 493 boolean useThumbnailTransition = !isTopTaskHome && 494 hasValidTaskRects(); 495 boolean hasRecentTasks = !recentTasks.isEmpty(); 496 497 if (useThumbnailTransition) { 498 // Try starting with a thumbnail transition 499 ActivityOptions opts = getThumbnailTransitionActivityOptions(taskRect); 500 if (opts != null) { 501 if (sLastScreenshot != null) { 502 startAlternateRecentsActivity(opts, EXTRA_FROM_APP_FULL_SCREENSHOT); 503 } else { 504 startAlternateRecentsActivity(opts, EXTRA_FROM_APP_THUMBNAIL); 505 } 506 } else { 507 // Fall through below to the non-thumbnail transition 508 useThumbnailTransition = false; 509 } 510 } else { 511 // If there is no thumbnail transition, but is launching from home into recents, then 512 // use a quick home transition and do the animation from home 513 if (hasRecentTasks && Constants.DebugFlags.App.EnableHomeTransition) { 514 ActivityOptions opts = getHomeTransitionActivityOptions(); 515 startAlternateRecentsActivity(opts, EXTRA_FROM_HOME); 516 } else { 517 // Otherwise we do the normal fade from an unknown source 518 ActivityOptions opts = getUnknownTransitionActivityOptions(); 519 startAlternateRecentsActivity(opts, null); 520 } 521 } 522 523 if (Console.Enabled) { 524 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 525 Constants.Log.App.TimeRecentsStartupKey, "startRecentsActivity"); 526 } 527 mLastToggleTime = System.currentTimeMillis(); 528 } 529 530 /** Starts the recents activity */ 531 void startAlternateRecentsActivity(ActivityOptions opts, String extraFlag) { 532 Intent intent = new Intent(sToggleRecentsAction); 533 intent.setClassName(sRecentsPackage, sRecentsActivity); 534 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 535 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 536 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 537 if (extraFlag != null) { 538 intent.putExtra(extraFlag, true); 539 } 540 intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, mTriggeredFromAltTab); 541 if (opts != null) { 542 mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( 543 UserHandle.USER_CURRENT)); 544 } else { 545 mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); 546 } 547 } 548 549 /** Returns the last screenshot taken, this will be called by the RecentsActivity. */ 550 public static Bitmap getLastScreenshot() { 551 return sLastScreenshot; 552 } 553 554 /** Recycles the last screenshot taken, this will be called by the RecentsActivity. */ 555 public static void consumeLastScreenshot() { 556 if (sLastScreenshot != null) { 557 sLastScreenshot.recycle(); 558 sLastScreenshot = null; 559 } 560 } 561 562 /** Sets the RecentsComponent callbacks. */ 563 public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) { 564 sRecentsComponentCallbacks = cb; 565 } 566 567 /** Notifies the callbacks that the visibility of Recents has changed. */ 568 public static void notifyVisibilityChanged(boolean visible) { 569 if (sRecentsComponentCallbacks != null) { 570 sRecentsComponentCallbacks.onVisibilityChanged(visible); 571 } 572 } 573 574 /**** OnAnimationStartedListener Implementation ****/ 575 576 @Override 577 public void onAnimationStarted() { 578 // Notify recents to start the enter animation 579 try { 580 Message msg = Message.obtain(null, MSG_START_ENTER_ANIMATION, 0, 0); 581 mService.send(msg); 582 } catch (RemoteException re) { 583 re.printStackTrace(); 584 } 585 } 586} 587