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