RecentsView.java revision 7aceb9a07fd3883c8f301ba93726d55494b0446e
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.views; 18 19import android.app.ActivityOptions; 20import android.app.TaskStackBuilder; 21import android.content.ActivityNotFoundException; 22import android.content.ComponentName; 23import android.content.Context; 24import android.content.Intent; 25import android.graphics.Bitmap; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.Rect; 29import android.net.Uri; 30import android.os.UserHandle; 31import android.provider.Settings; 32import android.util.AttributeSet; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.WindowInsets; 36import android.widget.FrameLayout; 37import com.android.systemui.recents.Console; 38import com.android.systemui.recents.Constants; 39import com.android.systemui.recents.RecentsConfiguration; 40import com.android.systemui.recents.model.RecentsPackageMonitor; 41import com.android.systemui.recents.model.RecentsTaskLoader; 42import com.android.systemui.recents.model.SpaceNode; 43import com.android.systemui.recents.model.Task; 44import com.android.systemui.recents.model.TaskStack; 45 46import java.util.ArrayList; 47import java.util.Set; 48 49/** 50 * This view is the the top level layout that contains TaskStacks (which are laid out according 51 * to their SpaceNode bounds. 52 */ 53public class RecentsView extends FrameLayout implements TaskStackView.TaskStackViewCallbacks, 54 RecentsPackageMonitor.PackageCallbacks { 55 56 /** The RecentsView callbacks */ 57 public interface RecentsViewCallbacks { 58 public void onTaskViewClicked(); 59 public void onAllTaskViewsDismissed(); 60 public void onExitToHomeAnimationTriggered(); 61 } 62 63 RecentsConfiguration mConfig; 64 LayoutInflater mInflater; 65 Paint mDebugModePaint; 66 67 // The space partitioning root of this container 68 SpaceNode mBSP; 69 // Whether there are any tasks 70 boolean mHasTasks; 71 // Search bar view 72 View mSearchBar; 73 // Recents view callbacks 74 RecentsViewCallbacks mCb; 75 76 public RecentsView(Context context) { 77 super(context); 78 } 79 80 public RecentsView(Context context, AttributeSet attrs) { 81 this(context, attrs, 0); 82 } 83 84 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { 85 this(context, attrs, defStyleAttr, 0); 86 } 87 88 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 89 super(context, attrs, defStyleAttr, defStyleRes); 90 mConfig = RecentsConfiguration.getInstance(); 91 mInflater = LayoutInflater.from(context); 92 } 93 94 /** Sets the callbacks */ 95 public void setCallbacks(RecentsViewCallbacks cb) { 96 mCb = cb; 97 } 98 99 /** Set/get the bsp root node */ 100 public void setBSP(SpaceNode n) { 101 mBSP = n; 102 103 // Remove all TaskStackViews (but leave the search bar) 104 int childCount = getChildCount(); 105 for (int i = childCount - 1; i >= 0; i--) { 106 View v = getChildAt(i); 107 if (v != mSearchBar) { 108 removeViewAt(i); 109 } 110 } 111 112 // Create and add all the stacks for this partition of space. 113 mHasTasks = false; 114 ArrayList<TaskStack> stacks = mBSP.getStacks(); 115 for (TaskStack stack : stacks) { 116 TaskStackView stackView = new TaskStackView(getContext(), stack); 117 stackView.setCallbacks(this); 118 addView(stackView); 119 mHasTasks |= (stack.getTaskCount() > 0); 120 } 121 122 // Enable debug mode drawing 123 if (mConfig.debugModeEnabled) { 124 mDebugModePaint = new Paint(); 125 mDebugModePaint.setColor(0xFFff0000); 126 mDebugModePaint.setStyle(Paint.Style.STROKE); 127 mDebugModePaint.setStrokeWidth(5f); 128 setWillNotDraw(false); 129 } 130 } 131 132 /** Launches the focused task from the first stack if possible */ 133 public boolean launchFocusedTask() { 134 // Get the first stack view 135 int childCount = getChildCount(); 136 for (int i = 0; i < childCount; i++) { 137 View child = getChildAt(i); 138 if (child != mSearchBar) { 139 TaskStackView stackView = (TaskStackView) child; 140 TaskStack stack = stackView.mStack; 141 // Iterate the stack views and try and find the focused task 142 int taskCount = stackView.getChildCount(); 143 for (int j = 0; j < taskCount; j++) { 144 TaskView tv = (TaskView) stackView.getChildAt(j); 145 Task task = tv.getTask(); 146 if (tv.isFocusedTask()) { 147 if (Console.Enabled) { 148 Console.log(Constants.Log.UI.Focus, "[RecentsView|launchFocusedTask]", 149 "Found focused Task"); 150 } 151 onTaskViewClicked(stackView, tv, stack, task); 152 return true; 153 } 154 } 155 } 156 } 157 if (Console.Enabled) { 158 Console.log(Constants.Log.UI.Focus, "[RecentsView|launchFocusedTask]", 159 "No Tasks focused"); 160 } 161 return false; 162 } 163 164 /** Launches the first task from the first stack if possible */ 165 public boolean launchFirstTask() { 166 // Get the first stack view 167 int childCount = getChildCount(); 168 for (int i = 0; i < childCount; i++) { 169 View child = getChildAt(i); 170 if (child != mSearchBar) { 171 TaskStackView stackView = (TaskStackView) child; 172 TaskStack stack = stackView.mStack; 173 ArrayList<Task> tasks = stack.getTasks(); 174 175 // Get the first task in the stack 176 if (!tasks.isEmpty()) { 177 Task task = tasks.get(tasks.size() - 1); 178 TaskView tv = null; 179 180 // Try and use the first child task view as the source of the launch animation 181 if (stackView.getChildCount() > 0) { 182 TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1); 183 if (stv.getTask() == task) { 184 tv = stv; 185 } 186 } 187 onTaskViewClicked(stackView, tv, stack, task); 188 return true; 189 } 190 } 191 } 192 return false; 193 } 194 195 /** Requests all task stacks to start their enter-recents animation */ 196 public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { 197 // Handle the case when there are no views by incrementing and decrementing after all 198 // animations are started. 199 ctx.postAnimationTrigger.increment(); 200 201 int childCount = getChildCount(); 202 for (int i = 0; i < childCount; i++) { 203 View child = getChildAt(i); 204 if (child != mSearchBar) { 205 TaskStackView stackView = (TaskStackView) child; 206 stackView.startEnterRecentsAnimation(ctx); 207 } 208 } 209 210 // Handle the case when there are no views by incrementing and decrementing after all 211 // animations are started. 212 ctx.postAnimationTrigger.decrement(); 213 } 214 215 /** Requests all task stacks to start their exit-recents animation */ 216 public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 217 // Handle the case when there are no views by incrementing and decrementing after all 218 // animations are started. 219 ctx.postAnimationTrigger.increment(); 220 221 int childCount = getChildCount(); 222 for (int i = 0; i < childCount; i++) { 223 View child = getChildAt(i); 224 if (child != mSearchBar) { 225 TaskStackView stackView = (TaskStackView) child; 226 stackView.startExitToHomeAnimation(ctx); 227 } 228 } 229 230 // Handle the case when there are no views by incrementing and decrementing after all 231 // animations are started. 232 ctx.postAnimationTrigger.decrement(); 233 234 // Notify of the exit animation 235 mCb.onExitToHomeAnimationTriggered(); 236 } 237 238 /** Adds the search bar */ 239 public void setSearchBar(View searchBar) { 240 // Create the search bar (and hide it if we have no recent tasks) 241 if (Constants.DebugFlags.App.EnableSearchLayout) { 242 // Remove the previous search bar if one exists 243 if (mSearchBar != null && indexOfChild(mSearchBar) > -1) { 244 removeView(mSearchBar); 245 } 246 // Add the new search bar 247 if (searchBar != null) { 248 mSearchBar = searchBar; 249 mSearchBar.setVisibility(mHasTasks ? View.VISIBLE : View.GONE); 250 addView(mSearchBar); 251 252 if (Console.Enabled) { 253 Console.log(Constants.Log.App.SystemUIHandshake, "[RecentsView|setSearchBar]", 254 "" + (mSearchBar.getVisibility() == View.VISIBLE), 255 Console.AnsiBlue); 256 } 257 } 258 } 259 } 260 261 /** 262 * This is called with the full size of the window since we are handling our own insets. 263 */ 264 @Override 265 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 266 int width = MeasureSpec.getSize(widthMeasureSpec); 267 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 268 int height = MeasureSpec.getSize(heightMeasureSpec); 269 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 270 271 if (Console.Enabled) { 272 Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|measure]", 273 "width: " + width + " height: " + height, Console.AnsiGreen); 274 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 275 Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onMeasure"); 276 } 277 278 // Get the search bar bounds and measure the search bar layout 279 if (mSearchBar != null) { 280 Rect searchBarSpaceBounds = new Rect(); 281 mConfig.getSearchBarBounds(width, height - mConfig.systemInsets.top, searchBarSpaceBounds); 282 mSearchBar.measure( 283 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY), 284 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY)); 285 } 286 287 // We give the full width of the space, not including the right nav bar insets in landscape, 288 // to the stack view, since we want the tasks to render under the search bar in landscape. 289 // In addition, we give it the full height, not including the top inset or search bar space, 290 // since we want the tasks to render under the navigation buttons in portrait. 291 Rect taskStackBounds = new Rect(); 292 mConfig.getTaskStackBounds(width, height, taskStackBounds); 293 int childWidth = width - mConfig.systemInsets.right; 294 int childHeight = taskStackBounds.height() - mConfig.systemInsets.top; 295 296 // Measure each TaskStackView 297 int childCount = getChildCount(); 298 for (int i = 0; i < childCount; i++) { 299 View child = getChildAt(i); 300 if (child != mSearchBar && child.getVisibility() != GONE) { 301 child.measure(MeasureSpec.makeMeasureSpec(childWidth, widthMode), 302 MeasureSpec.makeMeasureSpec(childHeight, heightMode)); 303 } 304 } 305 306 setMeasuredDimension(width, height); 307 } 308 309 /** 310 * This is called with the full size of the window since we are handling our own insets. 311 */ 312 @Override 313 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 314 if (Console.Enabled) { 315 Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|layout]", 316 new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen); 317 Console.logTraceTime(Constants.Log.App.TimeRecentsStartup, 318 Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onLayout"); 319 } 320 321 // Get the search bar bounds so that we lay it out 322 if (mSearchBar != null) { 323 Rect searchBarSpaceBounds = new Rect(); 324 mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), searchBarSpaceBounds); 325 mSearchBar.layout(mConfig.systemInsets.left + searchBarSpaceBounds.left, 326 mConfig.systemInsets.top + searchBarSpaceBounds.top, 327 mConfig.systemInsets.left + mSearchBar.getMeasuredWidth(), 328 mConfig.systemInsets.top + mSearchBar.getMeasuredHeight()); 329 } 330 331 // We offset the stack view by the left inset (if any), but lay it out under the search bar. 332 // In addition, we offset our stack views by the top inset and search bar height, but not 333 // the bottom insets because we want it to render under the navigation buttons. 334 Rect taskStackBounds = new Rect(); 335 mConfig.getTaskStackBounds(getMeasuredWidth(), getMeasuredHeight(), taskStackBounds); 336 left += mConfig.systemInsets.left; 337 top += mConfig.systemInsets.top + taskStackBounds.top; 338 339 // Layout each child 340 // XXX: Based on the space node for that task view 341 int childCount = getChildCount(); 342 for (int i = 0; i < childCount; i++) { 343 View child = getChildAt(i); 344 if (child != mSearchBar && child.getVisibility() != GONE) { 345 TaskStackView tsv = (TaskStackView) child; 346 child.layout(left, top, left + tsv.getMeasuredWidth(), top + tsv.getMeasuredHeight()); 347 } 348 } 349 } 350 351 @Override 352 protected void onDraw(Canvas canvas) { 353 super.onDraw(canvas); 354 // Debug mode drawing 355 if (mConfig.debugModeEnabled) { 356 canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mDebugModePaint); 357 } 358 } 359 360 @Override 361 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 362 if (Console.Enabled) { 363 Console.log(Constants.Log.UI.MeasureAndLayout, 364 "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen); 365 } 366 367 // Update the configuration with the latest system insets and trigger a relayout 368 mConfig.updateSystemInsets(insets.getSystemWindowInsets()); 369 requestLayout(); 370 371 return insets.consumeSystemWindowInsets(false, false, false, true); 372 } 373 374 /** Notifies each task view of the user interaction. */ 375 public void onUserInteraction() { 376 // Get the first stack view 377 TaskStackView stackView = null; 378 int childCount = getChildCount(); 379 for (int i = 0; i < childCount; i++) { 380 View child = getChildAt(i); 381 if (child != mSearchBar) { 382 stackView = (TaskStackView) child; 383 stackView.onUserInteraction(); 384 } 385 } 386 } 387 388 /** Focuses the next task in the first stack view */ 389 public void focusNextTask(boolean forward) { 390 // Get the first stack view 391 TaskStackView stackView = null; 392 int childCount = getChildCount(); 393 for (int i = 0; i < childCount; i++) { 394 View child = getChildAt(i); 395 if (child != mSearchBar) { 396 stackView = (TaskStackView) child; 397 break; 398 } 399 } 400 401 if (stackView != null) { 402 stackView.focusNextTask(forward); 403 } 404 } 405 406 /** Unfilters any filtered stacks */ 407 public boolean unfilterFilteredStacks() { 408 if (mBSP != null) { 409 // Check if there are any filtered stacks and unfilter them before we back out of Recents 410 boolean stacksUnfiltered = false; 411 ArrayList<TaskStack> stacks = mBSP.getStacks(); 412 for (TaskStack stack : stacks) { 413 if (stack.hasFilteredTasks()) { 414 stack.unfilterTasks(); 415 stacksUnfiltered = true; 416 } 417 } 418 return stacksUnfiltered; 419 } 420 return false; 421 } 422 423 /**** TaskStackView.TaskStackCallbacks Implementation ****/ 424 425 @Override 426 public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv, 427 final TaskStack stack, final Task task) { 428 // Notify any callbacks of the launching of a new task 429 if (mCb != null) { 430 mCb.onTaskViewClicked(); 431 } 432 433 // Upfront the processing of the thumbnail 434 TaskViewTransform transform; 435 View sourceView = tv; 436 int offsetX = 0; 437 int offsetY = 0; 438 int stackScroll = stackView.getStackScroll(); 439 if (tv == null) { 440 // If there is no actual task view, then use the stack view as the source view 441 // and then offset to the expected transform rect, but bound this to just 442 // outside the display rect (to ensure we don't animate from too far away) 443 sourceView = stackView; 444 transform = stackView.getStackAlgorithm().getStackTransform(stack.indexOfTask(task), 445 stackScroll); 446 offsetX = transform.rect.left; 447 offsetY = Math.min(transform.rect.top, mConfig.displayRect.height()); 448 } else { 449 transform = stackView.getStackAlgorithm().getStackTransform(stack.indexOfTask(task), 450 stackScroll); 451 } 452 453 // Compute the thumbnail to scale up from 454 ActivityOptions opts = null; 455 int thumbnailWidth = transform.rect.width(); 456 int thumbnailHeight = transform.rect.height(); 457 if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 && 458 task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { 459 // Resize the thumbnail to the size of the view that we are animating from 460 Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, 461 Bitmap.Config.ARGB_8888); 462 Canvas c = new Canvas(b); 463 c.drawBitmap(task.thumbnail, 464 new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()), 465 new Rect(0, 0, thumbnailWidth, thumbnailHeight), null); 466 c.setBitmap(null); 467 opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView, 468 b, offsetX, offsetY); 469 } 470 471 final ActivityOptions launchOpts = opts; 472 final Runnable launchRunnable = new Runnable() { 473 @Override 474 public void run() { 475 if (Console.Enabled) { 476 Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, 477 Constants.Log.App.TimeRecentsLaunchKey, "preStartActivity"); 478 } 479 480 if (task.isActive) { 481 // Bring an active task to the foreground 482 RecentsTaskLoader.getInstance().getSystemServicesProxy() 483 .moveTaskToFront(task.key.id, launchOpts); 484 } else { 485 // Launch the activity anew with the desired animation 486 Intent i = new Intent(task.key.baseIntent); 487 i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 488 | Intent.FLAG_ACTIVITY_TASK_ON_HOME 489 | Intent.FLAG_ACTIVITY_NEW_TASK); 490 try { 491 UserHandle taskUser = new UserHandle(task.userId); 492 if (launchOpts != null) { 493 getContext().startActivityAsUser(i, launchOpts.toBundle(), taskUser); 494 } else { 495 getContext().startActivityAsUser(i, taskUser); 496 } 497 } catch (ActivityNotFoundException anfe) { 498 Console.logError(getContext(), "Could not start Activity"); 499 } 500 501 // And clean up the old task 502 onTaskViewDismissed(task); 503 } 504 505 if (Console.Enabled) { 506 Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, 507 Constants.Log.App.TimeRecentsLaunchKey, "startActivity"); 508 } 509 } 510 }; 511 512 if (Console.Enabled) { 513 Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask, 514 Constants.Log.App.TimeRecentsLaunchKey, "onTaskLaunched"); 515 } 516 517 // Launch the app right away if there is no task view, otherwise, animate the icon out first 518 if (tv == null) { 519 post(launchRunnable); 520 } else { 521 stackView.animateOnLaunchingTask(tv, launchRunnable); 522 } 523 } 524 525 @Override 526 public void onTaskViewAppInfoClicked(Task t) { 527 // Create a new task stack with the application info details activity 528 Intent baseIntent = t.key.baseIntent; 529 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 530 Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null)); 531 intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); 532 TaskStackBuilder.create(getContext()) 533 .addNextIntentWithParentStack(intent).startActivities(); 534 } 535 536 @Override 537 public void onTaskViewDismissed(Task t) { 538 // Remove any stored data from the loader. We currently don't bother notifying the views 539 // that the data has been unloaded because at the point we call onTaskViewDismissed(), the views 540 // either don't need to be updated, or have already been removed. 541 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 542 loader.deleteTaskData(t, false); 543 544 // Remove the old task from activity manager 545 int flags = t.key.baseIntent.getFlags(); 546 boolean isDocument = (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) == 547 Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 548 RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id, 549 isDocument); 550 } 551 552 @Override 553 public void onAllTaskViewsDismissed() { 554 mCb.onAllTaskViewsDismissed(); 555 } 556 557 @Override 558 public void onTaskStackFilterTriggered() { 559 // Hide the search bar 560 if (mSearchBar != null) { 561 mSearchBar.animate() 562 .alpha(0f) 563 .setStartDelay(0) 564 .setInterpolator(mConfig.fastOutSlowInInterpolator) 565 .setDuration(mConfig.filteringCurrentViewsAnimDuration) 566 .withLayer() 567 .start(); 568 } 569 } 570 571 @Override 572 public void onTaskStackUnfilterTriggered() { 573 // Show the search bar 574 if (mSearchBar != null) { 575 mSearchBar.animate() 576 .alpha(1f) 577 .setStartDelay(0) 578 .setInterpolator(mConfig.fastOutSlowInInterpolator) 579 .setDuration(mConfig.filteringNewViewsAnimDuration) 580 .withLayer() 581 .start(); 582 } 583 } 584 585 /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ 586 587 @Override 588 public void onComponentRemoved(Set<ComponentName> cns) { 589 // Propagate this event down to each task stack view 590 int childCount = getChildCount(); 591 for (int i = 0; i < childCount; i++) { 592 View child = getChildAt(i); 593 if (child != mSearchBar) { 594 TaskStackView stackView = (TaskStackView) child; 595 stackView.onComponentRemoved(cns); 596 } 597 } 598 } 599} 600