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