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 17 package com.android.systemui.recents.views; 18 19 import android.app.ActivityOptions; 20 import android.app.TaskStackBuilder; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Rect; 26 import android.net.Uri; 27 import android.os.UserHandle; 28 import android.provider.Settings; 29 import android.util.AttributeSet; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.WindowInsets; 33 import android.widget.FrameLayout; 34 35 import com.android.systemui.recents.Constants; 36 import com.android.systemui.recents.RecentsConfiguration; 37 import com.android.systemui.recents.misc.SystemServicesProxy; 38 import com.android.systemui.recents.model.RecentsPackageMonitor; 39 import com.android.systemui.recents.model.RecentsTaskLoader; 40 import com.android.systemui.recents.model.Task; 41 import com.android.systemui.recents.model.TaskStack; 42 43 import java.util.ArrayList; 44 45 /** 46 * This view is the the top level layout that contains TaskStacks (which are laid out according 47 * to their SpaceNode bounds. 48 */ 49 public class RecentsView extends FrameLayout implements TaskStackView.TaskStackViewCallbacks, 50 RecentsPackageMonitor.PackageCallbacks { 51 52 /** The RecentsView callbacks */ 53 public interface RecentsViewCallbacks { onTaskViewClicked()54 public void onTaskViewClicked(); onTaskLaunchFailed()55 public void onTaskLaunchFailed(); onAllTaskViewsDismissed()56 public void onAllTaskViewsDismissed(); onExitToHomeAnimationTriggered()57 public void onExitToHomeAnimationTriggered(); onScreenPinningRequest()58 public void onScreenPinningRequest(); 59 } 60 61 RecentsConfiguration mConfig; 62 LayoutInflater mInflater; 63 DebugOverlayView mDebugOverlay; 64 65 ArrayList<TaskStack> mStacks; 66 View mSearchBar; 67 RecentsViewCallbacks mCb; 68 RecentsView(Context context)69 public RecentsView(Context context) { 70 super(context); 71 } 72 RecentsView(Context context, AttributeSet attrs)73 public RecentsView(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 RecentsView(Context context, AttributeSet attrs, int defStyleAttr)77 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { 78 this(context, attrs, defStyleAttr, 0); 79 } 80 RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)81 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 82 super(context, attrs, defStyleAttr, defStyleRes); 83 mConfig = RecentsConfiguration.getInstance(); 84 mInflater = LayoutInflater.from(context); 85 } 86 87 /** Sets the callbacks */ setCallbacks(RecentsViewCallbacks cb)88 public void setCallbacks(RecentsViewCallbacks cb) { 89 mCb = cb; 90 } 91 92 /** Sets the debug overlay */ setDebugOverlay(DebugOverlayView overlay)93 public void setDebugOverlay(DebugOverlayView overlay) { 94 mDebugOverlay = overlay; 95 } 96 97 /** Set/get the bsp root node */ setTaskStacks(ArrayList<TaskStack> stacks)98 public void setTaskStacks(ArrayList<TaskStack> stacks) { 99 int numStacks = stacks.size(); 100 101 // Make a list of the stack view children only 102 ArrayList<TaskStackView> stackViews = new ArrayList<TaskStackView>(); 103 int childCount = getChildCount(); 104 for (int i = 0; i < childCount; i++) { 105 View child = getChildAt(i); 106 if (child != mSearchBar) { 107 stackViews.add((TaskStackView) child); 108 } 109 } 110 111 // Remove all/extra stack views 112 int numTaskStacksToKeep = 0; // Keep no tasks if we are recreating the layout 113 if (mConfig.launchedReuseTaskStackViews) { 114 numTaskStacksToKeep = Math.min(childCount, numStacks); 115 } 116 for (int i = stackViews.size() - 1; i >= numTaskStacksToKeep; i--) { 117 removeView(stackViews.get(i)); 118 stackViews.remove(i); 119 } 120 121 // Update the stack views that we are keeping 122 for (int i = 0; i < numTaskStacksToKeep; i++) { 123 TaskStackView tsv = stackViews.get(i); 124 // If onRecentsHidden is not triggered, we need to the stack view again here 125 tsv.reset(); 126 tsv.setStack(stacks.get(i)); 127 } 128 129 // Add remaining/recreate stack views 130 mStacks = stacks; 131 for (int i = stackViews.size(); i < numStacks; i++) { 132 TaskStack stack = stacks.get(i); 133 TaskStackView stackView = new TaskStackView(getContext(), stack); 134 stackView.setCallbacks(this); 135 addView(stackView); 136 } 137 138 // Enable debug mode drawing on all the stacks if necessary 139 if (mConfig.debugModeEnabled) { 140 for (int i = childCount - 1; i >= 0; i--) { 141 View v = getChildAt(i); 142 if (v != mSearchBar) { 143 TaskStackView stackView = (TaskStackView) v; 144 stackView.setDebugOverlay(mDebugOverlay); 145 } 146 } 147 } 148 149 // Trigger a new layout 150 requestLayout(); 151 } 152 153 /** Launches the focused task from the first stack if possible */ launchFocusedTask()154 public boolean launchFocusedTask() { 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 // Iterate the stack views and try and find the focused task 163 int taskCount = stackView.getChildCount(); 164 for (int j = 0; j < taskCount; j++) { 165 TaskView tv = (TaskView) stackView.getChildAt(j); 166 Task task = tv.getTask(); 167 if (tv.isFocusedTask()) { 168 onTaskViewClicked(stackView, tv, stack, task, false); 169 return true; 170 } 171 } 172 } 173 } 174 return false; 175 } 176 177 /** Launches the task that Recents was launched from, if possible */ launchPreviousTask()178 public boolean launchPreviousTask() { 179 // Get the first stack view 180 int childCount = getChildCount(); 181 for (int i = 0; i < childCount; i++) { 182 View child = getChildAt(i); 183 if (child != mSearchBar) { 184 TaskStackView stackView = (TaskStackView) child; 185 TaskStack stack = stackView.mStack; 186 ArrayList<Task> tasks = stack.getTasks(); 187 188 // Find the launch task in the stack 189 if (!tasks.isEmpty()) { 190 int taskCount = tasks.size(); 191 for (int j = 0; j < taskCount; j++) { 192 if (tasks.get(j).isLaunchTarget) { 193 Task task = tasks.get(j); 194 TaskView tv = stackView.getChildViewForTask(task); 195 onTaskViewClicked(stackView, tv, stack, task, false); 196 return true; 197 } 198 } 199 } 200 } 201 } 202 return false; 203 } 204 205 /** Requests all task stacks to start their enter-recents animation */ startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx)206 public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { 207 // We have to increment/decrement the post animation trigger in case there are no children 208 // to ensure that it runs 209 ctx.postAnimationTrigger.increment(); 210 211 int childCount = getChildCount(); 212 for (int i = 0; i < childCount; i++) { 213 View child = getChildAt(i); 214 if (child != mSearchBar) { 215 TaskStackView stackView = (TaskStackView) child; 216 stackView.startEnterRecentsAnimation(ctx); 217 } 218 } 219 ctx.postAnimationTrigger.decrement(); 220 } 221 222 /** Requests all task stacks to start their exit-recents animation */ startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx)223 public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { 224 // We have to increment/decrement the post animation trigger in case there are no children 225 // to ensure that it runs 226 ctx.postAnimationTrigger.increment(); 227 int childCount = getChildCount(); 228 for (int i = 0; i < childCount; i++) { 229 View child = getChildAt(i); 230 if (child != mSearchBar) { 231 TaskStackView stackView = (TaskStackView) child; 232 stackView.startExitToHomeAnimation(ctx); 233 } 234 } 235 ctx.postAnimationTrigger.decrement(); 236 237 // Notify of the exit animation 238 mCb.onExitToHomeAnimationTriggered(); 239 } 240 241 /** Adds the search bar */ setSearchBar(View searchBar)242 public void setSearchBar(View searchBar) { 243 // Create the search bar (and hide it if we have no recent tasks) 244 if (Constants.DebugFlags.App.EnableSearchLayout) { 245 // Remove the previous search bar if one exists 246 if (mSearchBar != null && indexOfChild(mSearchBar) > -1) { 247 removeView(mSearchBar); 248 } 249 // Add the new search bar 250 if (searchBar != null) { 251 mSearchBar = searchBar; 252 addView(mSearchBar); 253 } 254 } 255 } 256 257 /** Returns whether there is currently a search bar */ hasSearchBar()258 public boolean hasSearchBar() { 259 return mSearchBar != null; 260 } 261 262 /** Sets the visibility of the search bar */ setSearchBarVisibility(int visibility)263 public void setSearchBarVisibility(int visibility) { 264 if (mSearchBar != null) { 265 mSearchBar.setVisibility(visibility); 266 // Always bring the search bar to the top 267 mSearchBar.bringToFront(); 268 } 269 } 270 271 /** 272 * This is called with the full size of the window since we are handling our own insets. 273 */ 274 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)275 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 276 int width = MeasureSpec.getSize(widthMeasureSpec); 277 int height = MeasureSpec.getSize(heightMeasureSpec); 278 279 // Get the search bar bounds and measure the search bar layout 280 if (mSearchBar != null) { 281 Rect searchBarSpaceBounds = new Rect(); 282 mConfig.getSearchBarBounds(width, height, mConfig.systemInsets.top, searchBarSpaceBounds); 283 mSearchBar.measure( 284 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY), 285 MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY)); 286 } 287 288 Rect taskStackBounds = new Rect(); 289 mConfig.getTaskStackBounds(width, height, mConfig.systemInsets.top, 290 mConfig.systemInsets.right, taskStackBounds); 291 292 // Measure each TaskStackView with the full width and height of the window since the 293 // transition view is a child of that stack view 294 int childCount = getChildCount(); 295 for (int i = 0; i < childCount; i++) { 296 View child = getChildAt(i); 297 if (child != mSearchBar && child.getVisibility() != GONE) { 298 TaskStackView tsv = (TaskStackView) child; 299 // Set the insets to be the top/left inset + search bounds 300 tsv.setStackInsetRect(taskStackBounds); 301 tsv.measure(widthMeasureSpec, heightMeasureSpec); 302 } 303 } 304 305 setMeasuredDimension(width, height); 306 } 307 308 /** 309 * This is called with the full size of the window since we are handling our own insets. 310 */ 311 @Override onLayout(boolean changed, int left, int top, int right, int bottom)312 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 313 // Get the search bar bounds so that we lay it out 314 if (mSearchBar != null) { 315 Rect searchBarSpaceBounds = new Rect(); 316 mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), 317 mConfig.systemInsets.top, searchBarSpaceBounds); 318 mSearchBar.layout(searchBarSpaceBounds.left, searchBarSpaceBounds.top, 319 searchBarSpaceBounds.right, searchBarSpaceBounds.bottom); 320 } 321 322 // Layout each TaskStackView with the full width and height of the window since the 323 // transition view is a child of that stack view 324 int childCount = getChildCount(); 325 for (int i = 0; i < childCount; i++) { 326 View child = getChildAt(i); 327 if (child != mSearchBar && child.getVisibility() != GONE) { 328 child.layout(left, top, left + child.getMeasuredWidth(), 329 top + child.getMeasuredHeight()); 330 } 331 } 332 } 333 334 @Override onApplyWindowInsets(WindowInsets insets)335 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 336 // Update the configuration with the latest system insets and trigger a relayout 337 mConfig.updateSystemInsets(insets.getSystemWindowInsets()); 338 requestLayout(); 339 return insets.consumeSystemWindowInsets(); 340 } 341 342 /** Notifies each task view of the user interaction. */ onUserInteraction()343 public void onUserInteraction() { 344 // Get the first stack view 345 int childCount = getChildCount(); 346 for (int i = 0; i < childCount; i++) { 347 View child = getChildAt(i); 348 if (child != mSearchBar) { 349 TaskStackView stackView = (TaskStackView) child; 350 stackView.onUserInteraction(); 351 } 352 } 353 } 354 355 /** Focuses the next task in the first stack view */ focusNextTask(boolean forward)356 public void focusNextTask(boolean forward) { 357 // Get the first stack view 358 int childCount = getChildCount(); 359 for (int i = 0; i < childCount; i++) { 360 View child = getChildAt(i); 361 if (child != mSearchBar) { 362 TaskStackView stackView = (TaskStackView) child; 363 stackView.focusNextTask(forward, true); 364 break; 365 } 366 } 367 } 368 369 /** Dismisses the focused task. */ dismissFocusedTask()370 public void dismissFocusedTask() { 371 // Get the first stack view 372 int childCount = getChildCount(); 373 for (int i = 0; i < childCount; i++) { 374 View child = getChildAt(i); 375 if (child != mSearchBar) { 376 TaskStackView stackView = (TaskStackView) child; 377 stackView.dismissFocusedTask(); 378 break; 379 } 380 } 381 } 382 383 /** Unfilters any filtered stacks */ unfilterFilteredStacks()384 public boolean unfilterFilteredStacks() { 385 if (mStacks != null) { 386 // Check if there are any filtered stacks and unfilter them before we back out of Recents 387 boolean stacksUnfiltered = false; 388 int numStacks = mStacks.size(); 389 for (int i = 0; i < numStacks; i++) { 390 TaskStack stack = mStacks.get(i); 391 if (stack.hasFilteredTasks()) { 392 stack.unfilterTasks(); 393 stacksUnfiltered = true; 394 } 395 } 396 return stacksUnfiltered; 397 } 398 return false; 399 } 400 401 /**** TaskStackView.TaskStackCallbacks Implementation ****/ 402 403 @Override onTaskViewClicked(final TaskStackView stackView, final TaskView tv, final TaskStack stack, final Task task, final boolean lockToTask)404 public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv, 405 final TaskStack stack, final Task task, final boolean lockToTask) { 406 // Notify any callbacks of the launching of a new task 407 if (mCb != null) { 408 mCb.onTaskViewClicked(); 409 } 410 411 // Upfront the processing of the thumbnail 412 TaskViewTransform transform = new TaskViewTransform(); 413 View sourceView; 414 int offsetX = 0; 415 int offsetY = 0; 416 float stackScroll = stackView.getScroller().getStackScroll(); 417 if (tv == null) { 418 // If there is no actual task view, then use the stack view as the source view 419 // and then offset to the expected transform rect, but bound this to just 420 // outside the display rect (to ensure we don't animate from too far away) 421 sourceView = stackView; 422 transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform, null); 423 offsetX = transform.rect.left; 424 offsetY = mConfig.displayRect.height(); 425 } else { 426 sourceView = tv.mThumbnailView; 427 transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform, null); 428 } 429 430 // Compute the thumbnail to scale up from 431 final SystemServicesProxy ssp = 432 RecentsTaskLoader.getInstance().getSystemServicesProxy(); 433 ActivityOptions opts = null; 434 if (task.thumbnail != null && task.thumbnail.getWidth() > 0 && 435 task.thumbnail.getHeight() > 0) { 436 Bitmap b; 437 if (tv != null) { 438 // Disable any focused state before we draw the header 439 if (tv.isFocusedTask()) { 440 tv.unsetFocusedTask(); 441 } 442 443 float scale = tv.getScaleX(); 444 int fromHeaderWidth = (int) (tv.mHeaderView.getMeasuredWidth() * scale); 445 int fromHeaderHeight = (int) (tv.mHeaderView.getMeasuredHeight() * scale); 446 b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight, 447 Bitmap.Config.ARGB_8888); 448 if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { 449 b.eraseColor(0xFFff0000); 450 } else { 451 Canvas c = new Canvas(b); 452 c.scale(tv.getScaleX(), tv.getScaleY()); 453 tv.mHeaderView.draw(c); 454 c.setBitmap(null); 455 } 456 } else { 457 // Notify the system to skip the thumbnail layer by using an ALPHA_8 bitmap 458 b = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8); 459 } 460 ActivityOptions.OnAnimationStartedListener animStartedListener = null; 461 if (lockToTask) { 462 animStartedListener = new ActivityOptions.OnAnimationStartedListener() { 463 boolean mTriggered = false; 464 @Override 465 public void onAnimationStarted() { 466 if (!mTriggered) { 467 postDelayed(new Runnable() { 468 @Override 469 public void run() { 470 mCb.onScreenPinningRequest(); 471 } 472 }, 350); 473 mTriggered = true; 474 } 475 } 476 }; 477 } 478 opts = ActivityOptions.makeThumbnailAspectScaleUpAnimation(sourceView, 479 b, offsetX, offsetY, transform.rect.width(), transform.rect.height(), 480 sourceView.getHandler(), animStartedListener); 481 } 482 483 final ActivityOptions launchOpts = opts; 484 final Runnable launchRunnable = new Runnable() { 485 @Override 486 public void run() { 487 if (task.isActive) { 488 // Bring an active task to the foreground 489 ssp.moveTaskToFront(task.key.id, launchOpts); 490 } else { 491 if (ssp.startActivityFromRecents(getContext(), task.key.id, 492 task.activityLabel, launchOpts)) { 493 if (launchOpts == null && lockToTask) { 494 mCb.onScreenPinningRequest(); 495 } 496 } else { 497 // Dismiss the task and return the user to home if we fail to 498 // launch the task 499 onTaskViewDismissed(task); 500 if (mCb != null) { 501 mCb.onTaskLaunchFailed(); 502 } 503 } 504 } 505 } 506 }; 507 508 // Launch the app right away if there is no task view, otherwise, animate the icon out first 509 if (tv == null) { 510 launchRunnable.run(); 511 } else { 512 if (!task.group.isFrontMostTask(task)) { 513 // For affiliated tasks that are behind other tasks, we must animate the front cards 514 // out of view before starting the task transition 515 stackView.startLaunchTaskAnimation(tv, launchRunnable, lockToTask); 516 } else { 517 // Otherwise, we can start the task transition immediately 518 stackView.startLaunchTaskAnimation(tv, null, lockToTask); 519 launchRunnable.run(); 520 } 521 } 522 } 523 524 @Override onTaskViewAppInfoClicked(Task t)525 public void onTaskViewAppInfoClicked(Task t) { 526 // Create a new task stack with the application info details activity 527 Intent baseIntent = t.key.baseIntent; 528 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 529 Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null)); 530 intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); 531 TaskStackBuilder.create(getContext()) 532 .addNextIntentWithParentStack(intent).startActivities(null, 533 new UserHandle(t.key.userId)); 534 } 535 536 @Override onTaskViewDismissed(Task t)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 loader.getSystemServicesProxy().removeTask(t.key.id); 546 } 547 548 @Override onAllTaskViewsDismissed()549 public void onAllTaskViewsDismissed() { 550 mCb.onAllTaskViewsDismissed(); 551 } 552 553 /** Final callback after Recents is finally hidden. */ onRecentsHidden()554 public void onRecentsHidden() { 555 // Notify each task stack view 556 int childCount = getChildCount(); 557 for (int i = 0; i < childCount; i++) { 558 View child = getChildAt(i); 559 if (child != mSearchBar) { 560 TaskStackView stackView = (TaskStackView) child; 561 stackView.onRecentsHidden(); 562 } 563 } 564 } 565 566 @Override onTaskStackFilterTriggered()567 public void onTaskStackFilterTriggered() { 568 // Hide the search bar 569 if (mSearchBar != null) { 570 mSearchBar.animate() 571 .alpha(0f) 572 .setStartDelay(0) 573 .setInterpolator(mConfig.fastOutSlowInInterpolator) 574 .setDuration(mConfig.filteringCurrentViewsAnimDuration) 575 .withLayer() 576 .start(); 577 } 578 } 579 580 @Override onTaskStackUnfilterTriggered()581 public void onTaskStackUnfilterTriggered() { 582 // Show the search bar 583 if (mSearchBar != null) { 584 mSearchBar.animate() 585 .alpha(1f) 586 .setStartDelay(0) 587 .setInterpolator(mConfig.fastOutSlowInInterpolator) 588 .setDuration(mConfig.filteringNewViewsAnimDuration) 589 .withLayer() 590 .start(); 591 } 592 } 593 594 /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/ 595 596 @Override onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId)597 public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) { 598 // Propagate this event down to each task stack view 599 int childCount = getChildCount(); 600 for (int i = 0; i < childCount; i++) { 601 View child = getChildAt(i); 602 if (child != mSearchBar) { 603 TaskStackView stackView = (TaskStackView) child; 604 stackView.onPackagesChanged(monitor, packageName, userId); 605 } 606 } 607 } 608 } 609