RecentsView.java revision dc00cbe2c15eef5141656311a6d05c753d7c6db6
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.provider.Settings; 31import android.util.AttributeSet; 32import android.view.LayoutInflater; 33import android.view.View; 34import android.view.WindowInsets; 35import android.widget.FrameLayout; 36import com.android.systemui.recents.Constants; 37import com.android.systemui.recents.RecentsConfiguration; 38import com.android.systemui.recents.misc.Console; 39import com.android.systemui.recents.misc.SystemServicesProxy; 40import com.android.systemui.recents.misc.Utilities; 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.HashSet; 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 onTaskViewClicked(stackView, tv, stack, task, false); 145 return true; 146 } 147 } 148 } 149 } 150 return false; 151 } 152 153 /** Launches the first task from the first stack if possible */ 154 public boolean launchFirstTask() { 155 // Get the first stack view 156 int childCount = getChildCount(); 157 for (int i = 0; i < childCount; i++) { 158 View child = getChildAt(i); 159 if (child != mSearchBar) { 160 TaskStackView stackView = (TaskStackView) child; 161 TaskStack stack = stackView.mStack; 162 ArrayList<Task> tasks = stack.getTasks(); 163 164 // Get the first task in the stack 165 if (!tasks.isEmpty()) { 166 Task task = tasks.get(tasks.size() - 1); 167 TaskView tv = null; 168 169 // Try and use the first child task view as the source of the launch animation 170 if (stackView.getChildCount() > 0) { 171 TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1); 172 if (stv.getTask() == task) { 173 tv = stv; 174 } 175 } 176 onTaskViewClicked(stackView, tv, stack, task, false); 177 return true; 178 } 179 } 180 } 181 return false; 182 } 183 184 /** Requests all task stacks to start their enter-recents animation */ 185 public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { 186 int childCount = getChildCount(); 187 for (int i = 0; i < childCount; i++) { 188 View child = getChildAt(i); 189 if (child != mSearchBar) { 190 TaskStackView stackView = (TaskStackView) child; 191 stackView.startEnterRecentsAnimation(ctx); 192 } 193 } 194 } 195 196 /** Requests all task stacks to start their exit-recents animation */ 197 public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 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.startExitToHomeAnimation(ctx); 204 } 205 } 206 207 // Notify of the exit animation 208 mCb.onExitToHomeAnimationTriggered(); 209 } 210 211 /** Adds the search bar */ 212 public void setSearchBar(View searchBar) { 213 // Create the search bar (and hide it if we have no recent tasks) 214 if (Constants.DebugFlags.App.EnableSearchLayout) { 215 // Remove the previous search bar if one exists 216 if (mSearchBar != null && indexOfChild(mSearchBar) > -1) { 217 removeView(mSearchBar); 218 } 219 // Add the new search bar 220 if (searchBar != null) { 221 mSearchBar = searchBar; 222 addView(mSearchBar); 223 } 224 } 225 } 226 227 /** Returns whether there is currently a search bar */ 228 public boolean hasSearchBar() { 229 return mSearchBar != null; 230 } 231 232 /** Sets the visibility of the search bar */ 233 public void setSearchBarVisibility(int visibility) { 234 if (mSearchBar != null) { 235 mSearchBar.setVisibility(visibility); 236 } 237 } 238 239 /** 240 * This is called with the full size of the window since we are handling our own insets. 241 */ 242 @Override 243 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 244 int width = MeasureSpec.getSize(widthMeasureSpec); 245 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 246 int height = MeasureSpec.getSize(heightMeasureSpec); 247 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 248 249 // Get the search bar bounds and measure the search bar layout 250 if (mSearchBar != null) { 251 Rect searchBarSpaceBounds = new Rect(); 252 mConfig.getSearchBarBounds(width, height - mConfig.systemInsets.top, searchBarSpaceBounds); 253 mSearchBar.measure( 254 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY), 255 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY)); 256 } 257 258 // We give the full width of the space, not including the right nav bar insets in landscape, 259 // to the stack view, since we want the tasks to render under the search bar in landscape. 260 // In addition, we give it the full height, not including the top inset or search bar space, 261 // since we want the tasks to render under the navigation buttons in portrait. 262 Rect taskStackBounds = new Rect(); 263 mConfig.getTaskStackBounds(width, height, taskStackBounds); 264 int childWidth = width - mConfig.systemInsets.right; 265 int childHeight = taskStackBounds.height() - mConfig.systemInsets.top; 266 267 // Measure each TaskStackView 268 int childCount = getChildCount(); 269 for (int i = 0; i < childCount; i++) { 270 View child = getChildAt(i); 271 if (child != mSearchBar && child.getVisibility() != GONE) { 272 child.measure(MeasureSpec.makeMeasureSpec(childWidth, widthMode), 273 MeasureSpec.makeMeasureSpec(childHeight, heightMode)); 274 } 275 } 276 277 setMeasuredDimension(width, height); 278 } 279 280 /** 281 * This is called with the full size of the window since we are handling our own insets. 282 */ 283 @Override 284 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 285 // Get the search bar bounds so that we lay it out 286 if (mSearchBar != null) { 287 Rect searchBarSpaceBounds = new Rect(); 288 mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), searchBarSpaceBounds); 289 mSearchBar.layout(mConfig.systemInsets.left + searchBarSpaceBounds.left, 290 mConfig.systemInsets.top + searchBarSpaceBounds.top, 291 mConfig.systemInsets.left + mSearchBar.getMeasuredWidth(), 292 mConfig.systemInsets.top + mSearchBar.getMeasuredHeight()); 293 } 294 295 // We offset the stack view by the left inset (if any), but lay it out under the search bar. 296 // In addition, we offset our stack views by the top inset and search bar height, but not 297 // the bottom insets because we want it to render under the navigation buttons. 298 Rect taskStackBounds = new Rect(); 299 mConfig.getTaskStackBounds(getMeasuredWidth(), getMeasuredHeight(), taskStackBounds); 300 left += mConfig.systemInsets.left; 301 top += mConfig.systemInsets.top + taskStackBounds.top; 302 303 // Layout each child 304 // XXX: Based on the space node for that task view 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 TaskStackView tsv = (TaskStackView) child; 310 child.layout(left, top, left + tsv.getMeasuredWidth(), top + tsv.getMeasuredHeight()); 311 } 312 } 313 } 314 315 @Override 316 protected void onDraw(Canvas canvas) { 317 super.onDraw(canvas); 318 // Debug mode drawing 319 if (mConfig.debugModeEnabled) { 320 canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mDebugModePaint); 321 } 322 } 323 324 @Override 325 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 326 // Update the configuration with the latest system insets and trigger a relayout 327 mConfig.updateSystemInsets(insets.getSystemWindowInsets()); 328 requestLayout(); 329 330 return insets.consumeSystemWindowInsets(false, false, false, true); 331 } 332 333 /** Notifies each task view of the user interaction. */ 334 public void onUserInteraction() { 335 // Get the first stack view 336 TaskStackView stackView = null; 337 int childCount = getChildCount(); 338 for (int i = 0; i < childCount; i++) { 339 View child = getChildAt(i); 340 if (child != mSearchBar) { 341 stackView = (TaskStackView) child; 342 stackView.onUserInteraction(); 343 } 344 } 345 } 346 347 /** Focuses the next task in the first stack view */ 348 public void focusNextTask(boolean forward) { 349 // Get the first stack view 350 TaskStackView stackView = null; 351 int childCount = getChildCount(); 352 for (int i = 0; i < childCount; i++) { 353 View child = getChildAt(i); 354 if (child != mSearchBar) { 355 stackView = (TaskStackView) child; 356 break; 357 } 358 } 359 360 if (stackView != null) { 361 stackView.focusNextTask(forward); 362 } 363 } 364 365 /** Unfilters any filtered stacks */ 366 public boolean unfilterFilteredStacks() { 367 if (mBSP != null) { 368 // Check if there are any filtered stacks and unfilter them before we back out of Recents 369 boolean stacksUnfiltered = false; 370 ArrayList<TaskStack> stacks = mBSP.getStacks(); 371 for (TaskStack stack : stacks) { 372 if (stack.hasFilteredTasks()) { 373 stack.unfilterTasks(); 374 stacksUnfiltered = true; 375 } 376 } 377 return stacksUnfiltered; 378 } 379 return false; 380 } 381 382 /**** TaskStackView.TaskStackCallbacks Implementation ****/ 383 384 @Override 385 public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv, 386 final TaskStack stack, final Task task, final boolean lockToTask) { 387 // Notify any callbacks of the launching of a new task 388 if (mCb != null) { 389 mCb.onTaskViewClicked(); 390 } 391 392 // Upfront the processing of the thumbnail 393 TaskViewTransform transform = new TaskViewTransform(); 394 View sourceView = tv; 395 int offsetX = 0; 396 int offsetY = 0; 397 int stackScroll = stackView.getStackScroll(); 398 if (tv == null) { 399 // If there is no actual task view, then use the stack view as the source view 400 // and then offset to the expected transform rect, but bound this to just 401 // outside the display rect (to ensure we don't animate from too far away) 402 sourceView = stackView; 403 transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform); 404 offsetX = transform.rect.left; 405 offsetY = Math.min(transform.rect.top, mConfig.displayRect.height()); 406 } else { 407 transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform); 408 } 409 410 // Compute the thumbnail to scale up from 411 final SystemServicesProxy ssp = 412 RecentsTaskLoader.getInstance().getSystemServicesProxy(); 413 ActivityOptions opts = null; 414 int thumbnailWidth = transform.rect.width(); 415 int thumbnailHeight = transform.rect.height(); 416 if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 && 417 task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { 418 // Resize the thumbnail to the size of the view that we are animating from 419 Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, 420 Bitmap.Config.ARGB_8888); 421 Canvas c = new Canvas(b); 422 c.drawBitmap(task.thumbnail, 423 new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()), 424 new Rect(0, 0, thumbnailWidth, thumbnailHeight), null); 425 c.setBitmap(null); 426 ActivityOptions.OnAnimationStartedListener animStartedListener = null; 427 if (lockToTask) { 428 animStartedListener = new ActivityOptions.OnAnimationStartedListener() { 429 boolean mTriggered = false; 430 @Override 431 public void onAnimationStarted() { 432 if (!mTriggered) { 433 postDelayed(new Runnable() { 434 @Override 435 public void run() { 436 ssp.lockCurrentTask(); 437 } 438 }, 350); 439 mTriggered = true; 440 } 441 } 442 }; 443 } 444 opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView, 445 b, offsetX, offsetY, animStartedListener); 446 } 447 448 final ActivityOptions launchOpts = opts; 449 final Runnable launchRunnable = new Runnable() { 450 @Override 451 public void run() { 452 if (task.isActive) { 453 // Bring an active task to the foreground 454 ssp.moveTaskToFront(task.key.id, launchOpts); 455 } else { 456 // Launch the activity anew with the desired animation 457 Intent i = new Intent(task.key.baseIntent); 458 i.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 459 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); 460 if (!Utilities.isDocument(i)) { 461 i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 462 } 463 try { 464 ssp.startActivityFromRecents(task.key.id, launchOpts); 465 if (launchOpts == null && lockToTask) { 466 ssp.lockCurrentTask(); 467 } 468 } catch (ActivityNotFoundException anfe) { 469 Console.logError(getContext(), "Could not start Activity"); 470 } 471 472 // And clean up the old task 473 onTaskViewDismissed(task); 474 } 475 } 476 }; 477 478 // Launch the app right away if there is no task view, otherwise, animate the icon out first 479 if (tv == null) { 480 post(launchRunnable); 481 } else { 482 stackView.animateOnLaunchingTask(tv, launchRunnable); 483 } 484 } 485 486 @Override 487 public void onTaskViewAppInfoClicked(Task t) { 488 // Create a new task stack with the application info details activity 489 Intent baseIntent = t.key.baseIntent; 490 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 491 Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null)); 492 intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); 493 TaskStackBuilder.create(getContext()) 494 .addNextIntentWithParentStack(intent).startActivities(); 495 } 496 497 @Override 498 public void onTaskViewDismissed(Task t) { 499 // Remove any stored data from the loader. We currently don't bother notifying the views 500 // that the data has been unloaded because at the point we call onTaskViewDismissed(), the views 501 // either don't need to be updated, or have already been removed. 502 RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); 503 loader.deleteTaskData(t, false); 504 505 // Remove the old task from activity manager 506 RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id, 507 Utilities.isDocument(t.key.baseIntent)); 508 } 509 510 @Override 511 public void onAllTaskViewsDismissed() { 512 mCb.onAllTaskViewsDismissed(); 513 } 514 515 @Override 516 public void onTaskStackFilterTriggered() { 517 // Hide the search bar 518 if (mSearchBar != null) { 519 mSearchBar.animate() 520 .alpha(0f) 521 .setStartDelay(0) 522 .setInterpolator(mConfig.fastOutSlowInInterpolator) 523 .setDuration(mConfig.filteringCurrentViewsAnimDuration) 524 .withLayer() 525 .start(); 526 } 527 } 528 529 @Override 530 public void onTaskStackUnfilterTriggered() { 531 // Show the search bar 532 if (mSearchBar != null) { 533 mSearchBar.animate() 534 .alpha(1f) 535 .setStartDelay(0) 536 .setInterpolator(mConfig.fastOutSlowInInterpolator) 537 .setDuration(mConfig.filteringNewViewsAnimDuration) 538 .withLayer() 539 .start(); 540 } 541 } 542 543 /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ 544 545 @Override 546 public void onComponentRemoved(HashSet<ComponentName> cns) { 547 // Propagate this event down to each task stack view 548 int childCount = getChildCount(); 549 for (int i = 0; i < childCount; i++) { 550 View child = getChildAt(i); 551 if (child != mSearchBar) { 552 TaskStackView stackView = (TaskStackView) child; 553 stackView.onComponentRemoved(cns); 554 } 555 } 556 } 557} 558