1 /* <lambda>null2 * Copyright (C) 2017 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 package com.android.quickstep.views 17 18 import android.animation.Animator 19 import android.animation.AnimatorListenerAdapter 20 import android.animation.AnimatorSet 21 import android.animation.ObjectAnimator 22 import android.annotation.IdRes 23 import android.app.ActivityOptions 24 import android.content.Context 25 import android.content.Intent 26 import android.graphics.Canvas 27 import android.graphics.PointF 28 import android.graphics.Rect 29 import android.graphics.drawable.Drawable 30 import android.os.Bundle 31 import android.util.AttributeSet 32 import android.util.FloatProperty 33 import android.util.Log 34 import android.view.Display 35 import android.view.MotionEvent 36 import android.view.View 37 import android.view.View.OnClickListener 38 import android.view.ViewGroup 39 import android.view.ViewStub 40 import android.view.accessibility.AccessibilityNodeInfo 41 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction 42 import android.widget.FrameLayout 43 import android.widget.Toast 44 import androidx.annotation.IntDef 45 import androidx.annotation.VisibleForTesting 46 import androidx.core.view.updateLayoutParams 47 import com.android.app.animation.Interpolators 48 import com.android.launcher3.Flags.enableCursorHoverStates 49 import com.android.launcher3.Flags.enableFocusOutline 50 import com.android.launcher3.Flags.enableGridOnlyOverview 51 import com.android.launcher3.Flags.enableOverviewIconMenu 52 import com.android.launcher3.Flags.enableRefactorTaskThumbnail 53 import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag 54 import com.android.launcher3.LauncherSettings 55 import com.android.launcher3.R 56 import com.android.launcher3.Utilities 57 import com.android.launcher3.anim.AnimatedFloat 58 import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH 59 import com.android.launcher3.logging.StatsLogManager.LauncherEvent 60 import com.android.launcher3.model.data.ItemInfo 61 import com.android.launcher3.model.data.ItemInfoWithIcon 62 import com.android.launcher3.model.data.WorkspaceItemInfo 63 import com.android.launcher3.pm.UserCache 64 import com.android.launcher3.testing.TestLogging 65 import com.android.launcher3.testing.shared.TestProtocol 66 import com.android.launcher3.util.CancellableTask 67 import com.android.launcher3.util.DisplayController 68 import com.android.launcher3.util.Executors 69 import com.android.launcher3.util.MultiPropertyFactory 70 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE 71 import com.android.launcher3.util.RunnableList 72 import com.android.launcher3.util.SafeCloseable 73 import com.android.launcher3.util.SplitConfigurationOptions 74 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED 75 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption 76 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition 77 import com.android.launcher3.util.TraceHelper 78 import com.android.launcher3.util.TransformingTouchDelegate 79 import com.android.launcher3.util.ViewPool 80 import com.android.launcher3.util.rects.set 81 import com.android.launcher3.views.ActivityContext 82 import com.android.quickstep.RecentsModel 83 import com.android.quickstep.RemoteAnimationTargets 84 import com.android.quickstep.TaskAnimationManager 85 import com.android.quickstep.TaskOverlayFactory 86 import com.android.quickstep.TaskOverlayFactory.TaskOverlay 87 import com.android.quickstep.TaskUtils 88 import com.android.quickstep.TaskViewUtils 89 import com.android.quickstep.orientation.RecentsPagedOrientationHandler 90 import com.android.quickstep.task.thumbnail.TaskThumbnail 91 import com.android.quickstep.task.thumbnail.TaskThumbnailView 92 import com.android.quickstep.task.viewmodel.TaskViewData 93 import com.android.quickstep.util.ActiveGestureErrorDetector 94 import com.android.quickstep.util.ActiveGestureLog 95 import com.android.quickstep.util.BorderAnimator 96 import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator 97 import com.android.quickstep.util.RecentsOrientedState 98 import com.android.quickstep.util.TaskCornerRadius 99 import com.android.quickstep.util.TaskRemovedDuringLaunchListener 100 import com.android.quickstep.views.RecentsView.UNBOUND_TASK_VIEW_ID 101 import com.android.systemui.shared.recents.model.Task 102 import com.android.systemui.shared.recents.model.ThumbnailData 103 import com.android.systemui.shared.system.ActivityManagerWrapper 104 import com.android.systemui.shared.system.QuickStepContract 105 106 /** A task in the Recents view. */ 107 open class TaskView 108 @JvmOverloads 109 constructor( 110 context: Context, 111 attrs: AttributeSet? = null, 112 defStyleAttr: Int = 0, 113 defStyleRes: Int = 0, 114 focusBorderAnimator: BorderAnimator? = null, 115 hoverBorderAnimator: BorderAnimator? = null 116 ) : FrameLayout(context, attrs), ViewPool.Reusable { 117 /** 118 * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which 119 * components of this task require an update 120 */ 121 @Retention(AnnotationRetention.SOURCE) 122 @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS) 123 annotation class TaskDataChanges 124 125 /** Type of task view */ 126 @Retention(AnnotationRetention.SOURCE) 127 @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP) 128 annotation class Type { 129 companion object { 130 const val SINGLE = 1 131 const val GROUPED = 2 132 const val DESKTOP = 3 133 } 134 } 135 136 val taskViewData = TaskViewData() 137 val taskIds: IntArray 138 /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ 139 get() = taskContainers.map { it.task.key.id }.toIntArray() 140 141 val thumbnailViews: Array<TaskThumbnailViewDeprecated> 142 get() = taskContainers.map { it.thumbnailViewDeprecated }.toTypedArray() 143 144 val isGridTask: Boolean 145 /** Returns whether the task is part of overview grid and not being focused. */ 146 get() = container.deviceProfile.isTablet && !isFocusedTask 147 148 val isRunningTask: Boolean 149 get() = this === recentsView?.runningTaskView 150 151 val isFocusedTask: Boolean 152 get() = this === recentsView?.focusedTaskView 153 154 val taskCornerRadius: Float 155 get() = currentFullscreenParams.cornerRadius 156 157 val recentsView: RecentsView<*, *>? 158 get() = parent as? RecentsView<*, *> 159 160 val pagedOrientationHandler: RecentsPagedOrientationHandler 161 get() = orientedState.orientationHandler 162 163 @get:Deprecated("Use [taskContainers] instead.") 164 val firstTask: Task 165 /** Returns the first task bound to this TaskView. */ 166 get() = taskContainers[0].task 167 168 @get:Deprecated("Use [taskContainers] instead.") 169 val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated 170 /** Returns the first thumbnailView of the TaskView. */ 171 get() = taskContainers[0].thumbnailViewDeprecated 172 173 @get:Deprecated("Use [taskContainers] instead.") 174 val firstItemInfo: ItemInfo 175 get() = taskContainers[0].itemInfo 176 177 private val currentFullscreenParams = FullscreenDrawParams(context) 178 protected val container: RecentsViewContainer = 179 RecentsViewContainer.containerFromContext(context) 180 protected val lastTouchDownPosition = PointF() 181 182 // Derived view properties 183 protected val persistentScale: Float 184 /** 185 * Returns multiplication of scale that is persistent (e.g. fullscreen and grid), and does 186 * not change according to a temporary state. 187 */ 188 get() = Utilities.mapRange(gridProgress, nonGridScale, 1f) 189 190 protected val persistentTranslationX: Float 191 /** 192 * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does 193 * not change according to a temporary state (e.g. task offset). 194 */ 195 get() = 196 (getNonGridTrans(nonGridTranslationX) + 197 getGridTrans(this.gridTranslationX) + 198 getNonGridTrans(nonGridPivotTranslationX)) 199 200 protected val persistentTranslationY: Float 201 /** 202 * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does 203 * not change according to a temporary state (e.g. task offset). 204 */ 205 get() = boxTranslationY + getGridTrans(gridTranslationY) 206 207 protected val primarySplitTranslationProperty: FloatProperty<TaskView> 208 get() = 209 pagedOrientationHandler.getPrimaryValue( 210 SPLIT_SELECT_TRANSLATION_X, 211 SPLIT_SELECT_TRANSLATION_Y 212 ) 213 214 protected val secondarySplitTranslationProperty: FloatProperty<TaskView> 215 get() = 216 pagedOrientationHandler.getSecondaryValue( 217 SPLIT_SELECT_TRANSLATION_X, 218 SPLIT_SELECT_TRANSLATION_Y 219 ) 220 221 protected val primaryDismissTranslationProperty: FloatProperty<TaskView> 222 get() = 223 pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) 224 225 protected val secondaryDismissTranslationProperty: FloatProperty<TaskView> 226 get() = 227 pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) 228 229 protected val primaryTaskOffsetTranslationProperty: FloatProperty<TaskView> 230 get() = 231 pagedOrientationHandler.getPrimaryValue( 232 TASK_OFFSET_TRANSLATION_X, 233 TASK_OFFSET_TRANSLATION_Y 234 ) 235 236 protected val secondaryTaskOffsetTranslationProperty: FloatProperty<TaskView> 237 get() = 238 pagedOrientationHandler.getSecondaryValue( 239 TASK_OFFSET_TRANSLATION_X, 240 TASK_OFFSET_TRANSLATION_Y 241 ) 242 243 protected val taskResistanceTranslationProperty: FloatProperty<TaskView> 244 get() = 245 pagedOrientationHandler.getSecondaryValue( 246 TASK_RESISTANCE_TRANSLATION_X, 247 TASK_RESISTANCE_TRANSLATION_Y 248 ) 249 250 private val tempCoordinates = FloatArray(2) 251 private val focusBorderAnimator: BorderAnimator? 252 private val hoverBorderAnimator: BorderAnimator? 253 private val rootViewDisplayId: Int 254 get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY 255 256 /** Returns a list of all TaskContainers in the TaskView. */ 257 lateinit var taskContainers: List<TaskContainer> 258 protected set 259 260 lateinit var orientedState: RecentsOrientedState 261 262 var taskViewId = UNBOUND_TASK_VIEW_ID 263 var isEndQuickSwitchCuj = false 264 265 // Various animation progress variables. 266 // progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. 267 protected var fullscreenProgress = 0f 268 set(value) { 269 field = Utilities.boundToRange(value, 0f, 1f) 270 onFullscreenProgressChanged(field) 271 } 272 273 // gridProgress 0 = carousel; 1 = 2 row grid. 274 protected var gridProgress = 0f 275 set(value) { 276 field = value 277 onGridProgressChanged() 278 } 279 280 /** 281 * The modalness of this view is how it should be displayed when it is shown on its own in the 282 * modal state of overview. 0 being in context with other tasks, 1 being shown on its own. 283 */ 284 protected var modalness = 0f 285 set(value) { 286 if (field == value) { 287 return 288 } 289 field = value 290 onModalnessUpdated(field) 291 } 292 293 protected var taskThumbnailSplashAlpha = 0f 294 set(value) { 295 field = value 296 applyThumbnailSplashAlpha() 297 } 298 299 protected var nonGridScale = 1f 300 set(value) { 301 field = value 302 applyScale() 303 } 304 305 private var dismissScale = 1f 306 set(value) { 307 field = value 308 applyScale() 309 } 310 311 private var dismissTranslationX = 0f 312 set(value) { 313 field = value 314 applyTranslationX() 315 } 316 317 private var dismissTranslationY = 0f 318 set(value) { 319 field = value 320 applyTranslationY() 321 } 322 323 private var taskOffsetTranslationX = 0f 324 set(value) { 325 field = value 326 applyTranslationX() 327 } 328 329 private var taskOffsetTranslationY = 0f 330 set(value) { 331 field = value 332 applyTranslationY() 333 } 334 335 private var taskResistanceTranslationX = 0f 336 set(value) { 337 field = value 338 applyTranslationX() 339 } 340 341 private var taskResistanceTranslationY = 0f 342 set(value) { 343 field = value 344 applyTranslationY() 345 } 346 347 // The following translation variables should only be used in the same orientation as Launcher. 348 private var boxTranslationY = 0f 349 set(value) { 350 field = value 351 applyTranslationY() 352 } 353 354 // The following grid translations scales with mGridProgress. 355 protected var gridTranslationX = 0f 356 set(value) { 357 field = value 358 applyTranslationX() 359 } 360 361 var gridTranslationY = 0f 362 protected set(value) { 363 field = value 364 applyTranslationY() 365 } 366 367 // The following grid translation is used to animate closing the gap between grid and clear all. 368 private var gridEndTranslationX = 0f 369 set(value) { 370 field = value 371 applyTranslationX() 372 } 373 374 // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick 375 // switch. 376 protected var nonGridTranslationX = 0f 377 set(value) { 378 field = value 379 applyTranslationX() 380 } 381 382 protected var nonGridPivotTranslationX = 0f 383 set(value) { 384 field = value 385 applyTranslationX() 386 } 387 388 // Used when in SplitScreenSelectState 389 private var splitSelectTranslationY = 0f 390 set(value) { 391 field = value 392 applyTranslationY() 393 } 394 395 private var splitSelectTranslationX = 0f 396 set(value) { 397 field = value 398 applyTranslationX() 399 } 400 401 protected var stableAlpha = 1f 402 set(value) { 403 field = value 404 alpha = stableAlpha 405 } 406 407 protected var shouldShowScreenshot = false 408 get() = !isRunningTask || field 409 410 /** Enable or disable showing border on hover and focus change */ 411 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 412 var borderEnabled = false 413 set(value) { 414 if (field == value) { 415 return 416 } 417 field = value 418 // Set the animation correctly in case it misses the hover/focus event during state 419 // transition 420 hoverBorderAnimator?.setBorderVisibility(visible = field && isHovered, animated = true) 421 focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) 422 } 423 424 private var focusTransitionProgress = 1f 425 set(value) { 426 field = value 427 onFocusTransitionProgressUpdated(field) 428 } 429 430 private val focusTransitionPropertyFactory = 431 MultiPropertyFactory( 432 this, 433 FOCUS_TRANSITION, 434 FOCUS_TRANSITION_INDEX_COUNT, 435 { x: Float, y: Float -> x * y }, 436 1f 437 ) 438 private val focusTransitionFullscreen = 439 focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN) 440 private val focusTransitionScaleAndDim = 441 focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM) 442 443 /** 444 * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in 445 * interpolator. 446 */ 447 fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator = 448 AnimatedFloat { v -> 449 focusTransitionScaleAndDim.value = 450 FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v) 451 } 452 .animateToValue(1f, 0f) 453 454 private var iconAndDimAnimator: ObjectAnimator? = null 455 // The current background requests to load the task thumbnail and icon 456 private val pendingThumbnailLoadRequests = mutableListOf<CancellableTask<*>>() 457 private val pendingIconLoadRequests = mutableListOf<CancellableTask<*>>() 458 private var isClickableAsLiveTile = true 459 460 init { 461 setOnClickListener { _ -> onClick() } 462 val keyboardFocusHighlightEnabled = 463 (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline()) 464 val cursorHoverStatesEnabled = enableCursorHoverStates() 465 setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled) 466 context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use { 467 this.focusBorderAnimator = 468 focusBorderAnimator 469 ?: if (keyboardFocusHighlightEnabled) 470 createSimpleBorderAnimator( 471 currentFullscreenParams.cornerRadius.toInt(), 472 context.resources.getDimensionPixelSize( 473 R.dimen.keyboard_quick_switch_border_width 474 ), 475 { bounds: Rect -> getThumbnailBounds(bounds) }, 476 this, 477 it.getColor( 478 R.styleable.TaskView_focusBorderColor, 479 BorderAnimator.DEFAULT_BORDER_COLOR 480 ) 481 ) 482 else null 483 this.hoverBorderAnimator = 484 hoverBorderAnimator 485 ?: if (cursorHoverStatesEnabled) 486 createSimpleBorderAnimator( 487 currentFullscreenParams.cornerRadius.toInt(), 488 context.resources.getDimensionPixelSize( 489 R.dimen.task_hover_border_width 490 ), 491 { bounds: Rect -> getThumbnailBounds(bounds) }, 492 this, 493 it.getColor( 494 R.styleable.TaskView_hoverBorderColor, 495 BorderAnimator.DEFAULT_BORDER_COLOR 496 ) 497 ) 498 else null 499 } 500 } 501 502 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 503 public override fun onFocusChanged( 504 gainFocus: Boolean, 505 direction: Int, 506 previouslyFocusedRect: Rect? 507 ) { 508 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) 509 if (borderEnabled) { 510 focusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true) 511 } 512 } 513 514 override fun onHoverEvent(event: MotionEvent): Boolean { 515 if (borderEnabled) { 516 when (event.action) { 517 MotionEvent.ACTION_HOVER_ENTER -> 518 hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true) 519 MotionEvent.ACTION_HOVER_EXIT -> 520 hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true) 521 else -> {} 522 } 523 } 524 return super.onHoverEvent(event) 525 } 526 527 // avoid triggering hover event on child elements which would cause HOVER_EXIT for this 528 // task view 529 override fun onInterceptHoverEvent(event: MotionEvent) = 530 if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event) 531 532 override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 533 val recentsView = recentsView ?: return false 534 val splitSelectStateController = recentsView.splitSelectController 535 // Disable taps for split selection animation unless we have a task not being selected 536 if ( 537 splitSelectStateController.isSplitSelectActive && 538 taskContainers.none { it.task.key.id != splitSelectStateController.initialTaskId } 539 ) { 540 return false 541 } 542 if (ev.action == MotionEvent.ACTION_DOWN) { 543 with(lastTouchDownPosition) { 544 x = ev.x 545 y = ev.y 546 } 547 } 548 return super.dispatchTouchEvent(ev) 549 } 550 551 override fun draw(canvas: Canvas) { 552 // Draw border first so any child views outside of the thumbnail bounds are drawn above it. 553 focusBorderAnimator?.drawBorder(canvas) 554 hoverBorderAnimator?.drawBorder(canvas) 555 super.draw(canvas) 556 } 557 558 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 559 super.onLayout(changed, left, top, right, bottom) 560 val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx 561 if (container.deviceProfile.isTablet) { 562 pivotX = (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() 563 pivotY = thumbnailTopMargin.toFloat() 564 } else { 565 pivotX = (right - left) * 0.5f 566 pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f 567 } 568 systemGestureExclusionRects = 569 SYSTEM_GESTURE_EXCLUSION_RECT.onEach { 570 it.right = width 571 it.bottom = height 572 } 573 } 574 575 override fun onRecycle() { 576 resetPersistentViewTransforms() 577 // Clear any references to the thumbnail (it will be re-read either from the cache or the 578 // system on next bind) 579 if (enableRefactorTaskThumbnail()) { 580 notifyIsRunningTaskUpdated() 581 } else { 582 taskContainers.forEach { it.thumbnailViewDeprecated.setThumbnail(it.task, null) } 583 } 584 setOverlayEnabled(false) 585 onTaskListVisibilityChanged(false) 586 borderEnabled = false 587 taskViewId = UNBOUND_TASK_VIEW_ID 588 taskContainers.forEach { it.destroy() } 589 } 590 591 // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. 592 override fun hasOverlappingRendering() = false 593 594 override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { 595 super.onInitializeAccessibilityNodeInfo(info) 596 with(info) { 597 addAction( 598 AccessibilityAction( 599 R.id.action_close, 600 context.getText(R.string.accessibility_close) 601 ) 602 ) 603 604 taskContainers.forEach { 605 TraceHelper.allowIpcs("TV.a11yInfo") { 606 TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut -> 607 addAction(shortcut.createAccessibilityAction(context)) 608 } 609 } 610 } 611 612 // Add DWB accessibility action at the end of the list 613 taskContainers.forEach { 614 it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction) 615 } 616 617 recentsView?.let { 618 collectionItemInfo = 619 AccessibilityNodeInfo.CollectionItemInfo.obtain( 620 0, 621 1, 622 it.taskViewCount - it.indexOfChild(this@TaskView) - 1, 623 1, 624 false 625 ) 626 } 627 } 628 } 629 630 override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean { 631 // TODO(b/343708271): Add support for multiple tasks per action. 632 if (action == R.id.action_close) { 633 recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/) 634 return true 635 } 636 637 taskContainers.forEach { 638 if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) { 639 return true 640 } 641 642 TaskOverlayFactory.getEnabledShortcuts(this, it).forEach { shortcut -> 643 if (shortcut.hasHandlerForAction(action)) { 644 shortcut.onClick(this) 645 return true 646 } 647 } 648 } 649 650 return super.performAccessibilityAction(action, arguments) 651 } 652 653 /** Updates this task view to the given {@param task}. */ 654 open fun bind( 655 task: Task, 656 orientedState: RecentsOrientedState, 657 taskOverlayFactory: TaskOverlayFactory 658 ) { 659 cancelPendingLoadTasks() 660 taskContainers = 661 listOf( 662 createTaskContainer( 663 task, 664 R.id.snapshot, 665 R.id.icon, 666 R.id.show_windows, 667 STAGE_POSITION_UNDEFINED, 668 taskOverlayFactory 669 ) 670 ) 671 setOrientationState(orientedState) 672 } 673 674 protected fun createTaskContainer( 675 task: Task, 676 @IdRes thumbnailViewId: Int, 677 @IdRes iconViewId: Int, 678 @IdRes showWindowViewId: Int, 679 @StagePosition stagePosition: Int, 680 taskOverlayFactory: TaskOverlayFactory 681 ): TaskContainer { 682 val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!! 683 val thumbnailView: TaskThumbnailView? 684 if (enableRefactorTaskThumbnail()) { 685 val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated) 686 thumbnailView = 687 TaskThumbnailView(context).apply { 688 layoutParams = thumbnailViewDeprecated.layoutParams 689 addView(this, indexOfSnapshotView) 690 } 691 thumbnailViewDeprecated.visibility = GONE 692 } else { 693 thumbnailView = null 694 } 695 val iconView = getOrInflateIconView(iconViewId) 696 return TaskContainer( 697 task, 698 thumbnailView, 699 thumbnailViewDeprecated, 700 iconView, 701 TransformingTouchDelegate(iconView.asView()), 702 stagePosition, 703 DigitalWellBeingToast(container, this), 704 findViewById(showWindowViewId)!!, 705 taskOverlayFactory 706 ) 707 .apply { 708 if (enableRefactorTaskThumbnail()) { 709 thumbnailViewDeprecated.setTaskOverlay(overlay) 710 bindThumbnailView() 711 } else { 712 thumbnailViewDeprecated.bind(task, overlay) 713 } 714 } 715 } 716 717 protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon { 718 val iconView = findViewById<View>(iconViewId)!! 719 return iconView as? TaskViewIcon 720 ?: (iconView as ViewStub) 721 .apply { 722 layoutResource = 723 if (enableOverviewIconMenu()) R.layout.icon_app_chip_view 724 else R.layout.icon_view 725 } 726 .inflate() as TaskViewIcon 727 } 728 729 protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized 730 731 fun containsMultipleTasks() = taskContainers.size > 1 732 733 /** 734 * Returns the TaskContainer corresponding to a given taskId, or null if the TaskView does not 735 * contain a Task with that ID. 736 */ 737 fun getTaskContainerById(taskId: Int) = taskContainers.firstOrNull { it.task.key.id == taskId } 738 739 /** Check if given `taskId` is tracked in this view */ 740 fun containsTaskId(taskId: Int) = getTaskContainerById(taskId) != null 741 742 open fun setOrientationState(orientationState: RecentsOrientedState) { 743 this.orientedState = orientationState 744 taskContainers.forEach { it.iconView.setIconOrientation(orientationState, isGridTask) } 745 setThumbnailOrientation(orientationState) 746 } 747 748 protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) { 749 taskContainers.forEach { 750 it.overlay.updateOrientationState(orientationState) 751 it.digitalWellBeingToast?.initialize(it.task) 752 } 753 } 754 755 /** 756 * Updates TaskView scaling and translation required to support variable width if enabled, while 757 * ensuring TaskView fits into screen in fullscreen. 758 */ 759 fun updateTaskSize( 760 lastComputedTaskSize: Rect, 761 lastComputedGridTaskSize: Rect, 762 lastComputedCarouselTaskSize: Rect 763 ) { 764 val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx 765 val taskWidth = lastComputedTaskSize.width() 766 val taskHeight = lastComputedTaskSize.height() 767 val nonGridScale: Float 768 val boxTranslationY: Float 769 val expectedWidth: Int 770 val expectedHeight: Int 771 if (container.deviceProfile.isTablet) { 772 val boxWidth: Int 773 val boxHeight: Int 774 if (isFocusedTask) { 775 // Task will be focused and should use focused task size. Use focusTaskRatio 776 // that is associated with the original orientation of the focused task. 777 boxWidth = taskWidth 778 boxHeight = taskHeight 779 } else { 780 // Otherwise task is in grid, and should use lastComputedGridTaskSize. 781 boxWidth = lastComputedGridTaskSize.width() 782 boxHeight = lastComputedGridTaskSize.height() 783 } 784 785 // Bound width/height to the box size. 786 expectedWidth = boxWidth 787 expectedHeight = boxHeight + thumbnailPadding 788 789 // Scale to to fit task Rect. 790 nonGridScale = 791 if (enableGridOnlyOverview()) { 792 lastComputedCarouselTaskSize.width() / taskWidth.toFloat() 793 } else { 794 taskWidth / boxWidth.toFloat() 795 } 796 797 // Align to top of task Rect. 798 boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f 799 } else { 800 nonGridScale = 1f 801 boxTranslationY = 0f 802 expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT 803 expectedHeight = 804 if (enableOverviewIconMenu()) taskHeight + thumbnailPadding 805 else LayoutParams.MATCH_PARENT 806 } 807 this.nonGridScale = nonGridScale 808 this.boxTranslationY = boxTranslationY 809 updateLayoutParams<ViewGroup.LayoutParams> { 810 width = expectedWidth 811 height = expectedHeight 812 } 813 updateThumbnailSize() 814 } 815 816 protected open fun updateThumbnailSize() { 817 // TODO(b/271468547), we should default to setting translations only on the snapshot instead 818 // of a hybrid of both margins and translations 819 taskContainers[0].snapshotView.updateLayoutParams<LayoutParams> { 820 topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx 821 } 822 } 823 824 /** Returns the thumbnail's bounds, optionally relative to the screen. */ 825 @JvmOverloads 826 open fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean = false) { 827 bounds.setEmpty() 828 taskContainers.forEach { 829 val thumbnailBounds = Rect() 830 if (relativeToDragLayer) { 831 container.dragLayer.getDescendantRectRelativeToSelf( 832 it.snapshotView, 833 thumbnailBounds 834 ) 835 } else { 836 thumbnailBounds.set(it.snapshotView) 837 } 838 bounds.union(thumbnailBounds) 839 } 840 } 841 842 /** 843 * See [TaskDataChanges] 844 * 845 * @param visible If this task view will be visible to the user in overview or hidden 846 */ 847 fun onTaskListVisibilityChanged(visible: Boolean) { 848 onTaskListVisibilityChanged(visible, FLAG_UPDATE_ALL) 849 } 850 851 /** 852 * See [TaskDataChanges] 853 * 854 * @param visible If this task view will be visible to the user in overview or hidden 855 */ 856 open fun onTaskListVisibilityChanged(visible: Boolean, @TaskDataChanges changes: Int) { 857 cancelPendingLoadTasks() 858 val recentsModel = RecentsModel.INSTANCE.get(context) 859 // These calls are no-ops if the data is already loaded, try and load the high 860 // resolution thumbnail if the state permits 861 if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL) && !enableRefactorTaskThumbnail()) { 862 taskContainers.forEach { 863 if (visible) { 864 recentsModel.thumbnailCache 865 .updateThumbnailInBackground(it.task) { thumbnailData -> 866 it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData) 867 } 868 ?.also { request -> pendingThumbnailLoadRequests.add(request) } 869 } else { 870 it.thumbnailViewDeprecated.setThumbnail(null, null) 871 // Reset the task thumbnail reference as well (it will be fetched from the 872 // cache or reloaded next time we need it) 873 it.task.thumbnail = null 874 } 875 } 876 } 877 if (needsUpdate(changes, FLAG_UPDATE_ICON)) { 878 taskContainers.forEach { 879 if (visible) { 880 recentsModel.iconCache 881 .updateIconInBackground(it.task) { task -> 882 setIcon(it.iconView, task.icon) 883 if (enableOverviewIconMenu()) { 884 setText(it.iconView, task.title) 885 } 886 it.digitalWellBeingToast?.initialize(task) 887 } 888 ?.also { request -> pendingIconLoadRequests.add(request) } 889 } else { 890 setIcon(it.iconView, null) 891 if (enableOverviewIconMenu()) { 892 setText(it.iconView, null) 893 } 894 } 895 } 896 } 897 if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { 898 currentFullscreenParams.updateCornerRadius(context) 899 } 900 } 901 902 protected open fun needsUpdate(@TaskDataChanges dataChange: Int, @TaskDataChanges flag: Int) = 903 (dataChange and flag) == flag 904 905 protected open fun cancelPendingLoadTasks() { 906 pendingThumbnailLoadRequests.forEach { it.cancel() } 907 pendingThumbnailLoadRequests.clear() 908 pendingIconLoadRequests.forEach { it.cancel() } 909 pendingIconLoadRequests.clear() 910 } 911 912 protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) { 913 with(iconView) { 914 if (icon != null) { 915 setDrawable(icon) 916 setOnClickListener { 917 if (!confirmSecondSplitSelectApp()) { 918 showTaskMenu(this) 919 } 920 } 921 setOnLongClickListener { 922 requestDisallowInterceptTouchEvent(true) 923 showTaskMenu(this) 924 } 925 } else { 926 setDrawable(null) 927 setOnClickListener(null) 928 setOnLongClickListener(null) 929 } 930 } 931 } 932 933 protected fun setText(iconView: TaskViewIcon, text: CharSequence?) { 934 iconView.setText(text) 935 } 936 937 open fun refreshThumbnails(thumbnailDatas: HashMap<Int, ThumbnailData?>?) { 938 if (enableRefactorTaskThumbnail()) { 939 // TODO(b/342560598) add thumbnail logic 940 return 941 } 942 943 taskContainers.forEach { 944 val thumbnailData = thumbnailDatas?.get(it.task.key.id) 945 if (thumbnailData != null) { 946 it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData) 947 } else { 948 it.thumbnailViewDeprecated.refresh() 949 } 950 } 951 } 952 953 private fun onClick() { 954 if (confirmSecondSplitSelectApp()) { 955 Log.d("b/310064698", "${taskIds.contentToString()} - onClick - split select is active") 956 return 957 } 958 val callbackList = 959 launchTasks()?.apply { 960 add { 961 Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted") 962 } 963 } 964 Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList") 965 container.statsLogManager 966 .logger() 967 .withItemInfo(firstItemInfo) 968 .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP) 969 } 970 971 /** 972 * Starts the task associated with this view and animates the startup. 973 * 974 * @return CompletionStage to indicate the animation completion or null if the launch failed. 975 */ 976 open fun launchTaskAnimated(): RunnableList? { 977 TestLogging.recordEvent( 978 TestProtocol.SEQUENCE_MAIN, 979 "startActivityFromRecentsAsync", 980 taskIds.contentToString() 981 ) 982 val opts = 983 container.getActivityLaunchOptions(this, null).apply { 984 options.launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY 985 } 986 if ( 987 ActivityManagerWrapper.getInstance() 988 .startActivityFromRecents(taskContainers[0].task.key, opts.options) 989 ) { 990 Log.d( 991 TAG, 992 "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}" 993 ) 994 ActiveGestureLog.INSTANCE.trackEvent( 995 ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED 996 ) 997 val recentsView = recentsView ?: return null 998 if (recentsView.runningTaskViewId != -1) { 999 recentsView.onTaskLaunchedInLiveTileMode() 1000 1001 // Return a fresh callback in the live tile case, so that it's not accidentally 1002 // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. 1003 return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) } 1004 } 1005 if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { 1006 // If the recents transition is running (ie. in live tile mode), then the start 1007 // of a new task will merge into the existing transition and it currently will 1008 // not be run independently, so we need to rely on the onTaskAppeared() call 1009 // for the new task to trigger the side launch callback to flush this runnable 1010 // list (which is usually flushed when the app launch animation finishes) 1011 recentsView.addSideTaskLaunchCallback(opts.onEndCallback) 1012 } 1013 return opts.onEndCallback 1014 } else { 1015 notifyTaskLaunchFailed() 1016 return null 1017 } 1018 } 1019 1020 /** Starts the task associated with this view without any animation */ 1021 fun launchTask(callback: (launched: Boolean) -> Unit) { 1022 launchTask(callback, isQuickSwitch = false) 1023 } 1024 1025 /** Starts the task associated with this view without any animation */ 1026 open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { 1027 TestLogging.recordEvent( 1028 TestProtocol.SEQUENCE_MAIN, 1029 "startActivityFromRecentsAsync", 1030 taskIds.contentToString() 1031 ) 1032 val firstContainer = taskContainers[0] 1033 val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) 1034 if (isQuickSwitch) { 1035 // We only listen for failures to launch in quickswitch because the during this 1036 // gesture launcher is in the background state, vs other launches which are in 1037 // the actual overview state 1038 failureListener.register(container, firstContainer.task.key.id) { 1039 notifyTaskLaunchFailed() 1040 recentsView?.let { 1041 // Disable animations for now, as it is an edge case and the app usually 1042 // covers launcher and also any state transition animation also gets 1043 // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations 1044 // when launcher shows again 1045 it.startHome(false /* animated */) 1046 // LauncherTaskbarUIController depends on the launcher state when 1047 // checking whether to handle resume, but that can come in before 1048 // startHome() changes the state, so force-refresh here to ensure the 1049 // taskbar is updated 1050 it.mSizeStrategy.taskbarController?.refreshResumedState() 1051 } 1052 } 1053 } 1054 // Indicate success once the system has indicated that the transition has started 1055 val opts = 1056 ActivityOptions.makeCustomTaskAnimation( 1057 context, 1058 0, 1059 0, 1060 Executors.MAIN_EXECUTOR.handler, 1061 { callback(true) } 1062 ) { 1063 failureListener.onTransitionFinished() 1064 } 1065 .apply { 1066 launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY 1067 if (isQuickSwitch) { 1068 setFreezeRecentTasksReordering() 1069 } 1070 // TODO(b/334826842) add splash functionality to new TTV 1071 if (!enableRefactorTaskThumbnail()) { 1072 disableStartingWindow = 1073 firstContainer.thumbnailViewDeprecated.shouldShowSplashView() 1074 } 1075 } 1076 Executors.UI_HELPER_EXECUTOR.execute { 1077 if ( 1078 !ActivityManagerWrapper.getInstance() 1079 .startActivityFromRecents(firstContainer.task.key, opts) 1080 ) { 1081 // If the call to start activity failed, then post the result immediately, 1082 // otherwise, wait for the animation start callback from the activity options 1083 // above 1084 Executors.MAIN_EXECUTOR.post { 1085 notifyTaskLaunchFailed() 1086 callback(false) 1087 } 1088 } 1089 Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}") 1090 } 1091 } 1092 1093 /** Launch of the current task (both live and inactive tasks) with an animation. */ 1094 fun launchTasks(): RunnableList? { 1095 val recentsView = recentsView ?: return null 1096 val remoteTargetHandles = recentsView.mRemoteTargetHandles 1097 if (!isRunningTask || remoteTargetHandles == null) { 1098 return launchTaskAnimated() 1099 } 1100 if (!isClickableAsLiveTile) { 1101 Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.") 1102 return null 1103 } 1104 isClickableAsLiveTile = false 1105 val targets = 1106 if (remoteTargetHandles.size == 1) { 1107 remoteTargetHandles[0].transformParams.targetSet 1108 } else { 1109 val apps = 1110 remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() } 1111 val wallpapers = 1112 remoteTargetHandles.flatMap { 1113 it.transformParams.targetSet.wallpapers.asIterable() 1114 } 1115 RemoteAnimationTargets( 1116 apps.toTypedArray(), 1117 wallpapers.toTypedArray(), 1118 remoteTargetHandles[0].transformParams.targetSet.nonApps, 1119 remoteTargetHandles[0].transformParams.targetSet.targetMode 1120 ) 1121 } 1122 if (targets == null) { 1123 // If the recents animation is cancelled somehow between the parent if block and 1124 // here, try to launch the task as a non live tile task. 1125 val runnableList = launchTaskAnimated() 1126 if (runnableList == null) { 1127 Log.e( 1128 TAG, 1129 "Recents animation cancelled and cannot launch task as non-live tile" + 1130 "; returning to home" 1131 ) 1132 } 1133 isClickableAsLiveTile = true 1134 return runnableList 1135 } 1136 val runnableList = RunnableList() 1137 with(AnimatorSet()) { 1138 TaskViewUtils.composeRecentsLaunchAnimator( 1139 this, 1140 this@TaskView, 1141 targets.apps, 1142 targets.wallpapers, 1143 targets.nonApps, 1144 true /* launcherClosing */, 1145 recentsView.stateManager, 1146 recentsView, 1147 recentsView.depthController 1148 ) 1149 addListener( 1150 object : AnimatorListenerAdapter() { 1151 override fun onAnimationEnd(animator: Animator) { 1152 if (taskContainers.any { it.task.key.displayId != rootViewDisplayId }) { 1153 launchTaskAnimated() 1154 } 1155 isClickableAsLiveTile = true 1156 runEndCallback() 1157 } 1158 1159 override fun onAnimationCancel(animation: Animator) { 1160 runEndCallback() 1161 } 1162 1163 private fun runEndCallback() { 1164 runnableList.executeAllAndDestroy() 1165 } 1166 } 1167 ) 1168 start() 1169 } 1170 Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") 1171 recentsView.onTaskLaunchedInLiveTileMode() 1172 return runnableList 1173 } 1174 1175 private fun notifyTaskLaunchFailed() { 1176 val sb = StringBuilder("Failed to launch task \n") 1177 taskContainers.forEach { 1178 sb.append("(task=${it.task.key.baseIntent} userId=${it.task.key.userId})\n") 1179 } 1180 Log.w(TAG, sb.toString()) 1181 Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show() 1182 } 1183 1184 fun initiateSplitSelect(splitPositionOption: SplitPositionOption) { 1185 recentsView?.initiateSplitSelect( 1186 this, 1187 splitPositionOption.stagePosition, 1188 SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition) 1189 ) 1190 } 1191 1192 /** 1193 * Returns `true` if user is already in split select mode and this tap was to choose the second 1194 * app. `false` otherwise 1195 */ 1196 protected open fun confirmSecondSplitSelectApp(): Boolean { 1197 val index = getLastSelectedChildTaskIndex() 1198 if (index >= taskContainers.size) { 1199 return false 1200 } 1201 val container = taskContainers[index] 1202 val recentsView = recentsView ?: return false 1203 return recentsView.confirmSplitSelect( 1204 this, 1205 container.task, 1206 container.iconView.drawable, 1207 container.thumbnailViewDeprecated, 1208 container.thumbnailViewDeprecated.thumbnail, /* intent */ 1209 null, /* user */ 1210 null, 1211 container.itemInfo 1212 ) 1213 } 1214 1215 /** 1216 * Returns the task index of the last selected child task (0 or 1). If we contain multiple tasks 1217 * and this TaskView is used as part of split selection, the selected child task index will be 1218 * that of the remaining task. 1219 */ 1220 protected open fun getLastSelectedChildTaskIndex() = 0 1221 1222 private fun showTaskMenu(iconView: TaskViewIcon): Boolean { 1223 val recentsView = recentsView ?: return false 1224 if (!recentsView.canLaunchFullscreenTask()) { 1225 // Don't show menu when selecting second split screen app 1226 return true 1227 } 1228 if (!container.deviceProfile.isTablet && !recentsView.isClearAllHidden) { 1229 recentsView.snapToPage(recentsView.indexOfChild(this)) 1230 return false 1231 } 1232 val menuContainer = taskContainers.firstOrNull { it.iconView === iconView } ?: return false 1233 container.statsLogManager 1234 .logger() 1235 .withItemInfo(menuContainer.itemInfo) 1236 .log(LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS) 1237 return showTaskMenuWithContainer(menuContainer) 1238 } 1239 1240 private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean { 1241 val recentsView = recentsView ?: return false 1242 return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) { 1243 menuContainer.iconView.revealAnim(/* isRevealing= */ true) 1244 TaskMenuView.showForTask(menuContainer) { 1245 menuContainer.iconView.revealAnim(/* isRevealing= */ false) 1246 } 1247 } else if (container.deviceProfile.isTablet) { 1248 val alignedOptionIndex = 1249 if ( 1250 recentsView.isOnGridBottomRow(menuContainer.taskView) && 1251 container.deviceProfile.isLandscape 1252 ) { 1253 if (enableGridOnlyOverview()) { 1254 // With no focused task, there is less available space below the tasks, so 1255 // align the arrow to the third option in the menu. 1256 2 1257 } else { 1258 // Bottom row of landscape grid aligns arrow to second option to avoid 1259 // clipping 1260 1 1261 } 1262 } else { 1263 0 1264 } 1265 TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) 1266 } else { 1267 TaskMenuView.showForTask(menuContainer) 1268 } 1269 } 1270 1271 /** 1272 * Whether the taskview should take the touch event from parent. Events passed to children that 1273 * might require special handling. 1274 */ 1275 open fun offerTouchToChildren(event: MotionEvent): Boolean { 1276 taskContainers.forEach { 1277 if (event.action == MotionEvent.ACTION_DOWN) { 1278 computeAndSetIconTouchDelegate(it.iconView, tempCoordinates, it.iconTouchDelegate) 1279 if (it.iconTouchDelegate.onTouchEvent(event)) { 1280 return true 1281 } 1282 } 1283 } 1284 return false 1285 } 1286 1287 private fun computeAndSetIconTouchDelegate( 1288 view: TaskViewIcon, 1289 tempCenterCoordinates: FloatArray, 1290 transformingTouchDelegate: TransformingTouchDelegate 1291 ) { 1292 val viewHalfWidth = view.width / 2f 1293 val viewHalfHeight = view.height / 2f 1294 Utilities.getDescendantCoordRelativeToAncestor( 1295 view.asView(), 1296 container.dragLayer, 1297 tempCenterCoordinates.apply { 1298 this[0] = viewHalfWidth 1299 this[1] = viewHalfHeight 1300 }, 1301 false 1302 ) 1303 transformingTouchDelegate.setBounds( 1304 (tempCenterCoordinates[0] - viewHalfWidth).toInt(), 1305 (tempCenterCoordinates[1] - viewHalfHeight).toInt(), 1306 (tempCenterCoordinates[0] + viewHalfWidth).toInt(), 1307 (tempCenterCoordinates[1] + viewHalfHeight).toInt() 1308 ) 1309 } 1310 1311 /** Sets up an on-click listener and the visibility for show_windows icon on top of the task. */ 1312 open fun setUpShowAllInstancesListener() { 1313 taskContainers.forEach { 1314 it.showWindowsView?.let { showWindowsView -> 1315 updateFilterCallback( 1316 showWindowsView, 1317 getFilterUpdateCallback(it.task.key.packageName) 1318 ) 1319 } 1320 } 1321 } 1322 1323 /** 1324 * Returns a callback that updates the state of the filter and the recents overview 1325 * 1326 * @param taskPackageName package name of the task to filter by 1327 */ 1328 private fun getFilterUpdateCallback(taskPackageName: String?) = 1329 if (recentsView?.filterState?.shouldShowFilterUI(taskPackageName) == true) 1330 OnClickListener { recentsView?.setAndApplyFilter(taskPackageName) } 1331 else null 1332 1333 /** 1334 * Sets the correct visibility and callback on the provided filterView based on whether the 1335 * callback is null or not 1336 */ 1337 private fun updateFilterCallback(filterView: View, callback: OnClickListener?) { 1338 // Filtering changes alpha instead of the visibility since visibility 1339 // can be altered separately through RecentsView#resetFromSplitSelectionState() 1340 with(filterView) { 1341 alpha = if (callback == null) 0f else 1f 1342 setOnClickListener(callback) 1343 } 1344 } 1345 1346 /** 1347 * Called to animate a smooth transition when going directly from an app into Overview (and vice 1348 * versa). Icons fade in, and DWB banners slide in with a "shift up" animation. 1349 */ 1350 private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) { 1351 taskContainers.forEach { 1352 it.iconView.setContentAlpha(focusTransitionProgress) 1353 it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress) 1354 } 1355 } 1356 1357 fun animateIconScaleAndDimIntoView() { 1358 iconAndDimAnimator?.cancel() 1359 iconAndDimAnimator = 1360 ObjectAnimator.ofFloat(focusTransitionScaleAndDim, MULTI_PROPERTY_VALUE, 0f, 1f).apply { 1361 duration = SCALE_ICON_DURATION 1362 interpolator = Interpolators.LINEAR 1363 addListener( 1364 object : AnimatorListenerAdapter() { 1365 override fun onAnimationEnd(animation: Animator) { 1366 iconAndDimAnimator = null 1367 } 1368 } 1369 ) 1370 start() 1371 } 1372 } 1373 1374 fun setIconScaleAndDim(iconScale: Float) { 1375 iconAndDimAnimator?.cancel() 1376 focusTransitionScaleAndDim.value = iconScale 1377 } 1378 1379 /** Set a color tint on the snapshot and supporting views. */ 1380 open fun setColorTint(amount: Float, tintColor: Int) { 1381 taskContainers.forEach { 1382 if (!enableRefactorTaskThumbnail()) { 1383 // TODO(b/334832108) Add scrim to new TTV 1384 it.thumbnailViewDeprecated.dimAlpha = amount 1385 } 1386 it.iconView.setIconColorTint(tintColor, amount) 1387 it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount) 1388 } 1389 } 1390 1391 /** 1392 * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). 1393 * IconView is unaffected. 1394 * 1395 * @param taskId is only used when setting visibility to a non-[View.VISIBLE] value 1396 */ 1397 open fun setThumbnailVisibility(visibility: Int, taskId: Int) { 1398 taskContainers.forEach { 1399 if (visibility == VISIBLE || it.task.key.id == taskId) { 1400 it.snapshotView.visibility = visibility 1401 it.digitalWellBeingToast?.setBannerVisibility(visibility) 1402 it.showWindowsView?.visibility = visibility 1403 it.overlay.setVisibility(visibility) 1404 } 1405 } 1406 } 1407 1408 open fun setOverlayEnabled(overlayEnabled: Boolean) { 1409 // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. 1410 // and if it's still necessary we should support that in the new TTV class. 1411 if (!enableRefactorTaskThumbnail()) { 1412 taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) } 1413 } 1414 } 1415 1416 protected open fun refreshTaskThumbnailSplash() { 1417 if (!enableRefactorTaskThumbnail()) { 1418 // TODO(b/334826842) add splash functionality to new TTV 1419 taskContainers.forEach { it.thumbnailViewDeprecated.refreshSplashView() } 1420 } 1421 } 1422 1423 protected fun getScrollAdjustment(gridEnabled: Boolean) = 1424 if (gridEnabled) gridTranslationX else nonGridTranslationX 1425 1426 protected fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) 1427 1428 fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f 1429 1430 private fun applyScale() { 1431 val scale = persistentScale * dismissScale 1432 scaleX = scale 1433 scaleY = scale 1434 if (enableRefactorTaskThumbnail()) { 1435 taskViewData.scale.value = scale 1436 } 1437 updateSnapshotRadius() 1438 } 1439 1440 protected open fun applyThumbnailSplashAlpha() { 1441 if (!enableRefactorTaskThumbnail()) { 1442 // TODO(b/334826842) add splash functionality to new TTV 1443 taskContainers.forEach { 1444 it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha) 1445 } 1446 } 1447 } 1448 1449 private fun applyTranslationX() { 1450 translationX = 1451 dismissTranslationX + 1452 taskOffsetTranslationX + 1453 taskResistanceTranslationX + 1454 splitSelectTranslationX + 1455 gridEndTranslationX + 1456 persistentTranslationX 1457 } 1458 1459 private fun applyTranslationY() { 1460 translationY = 1461 dismissTranslationY + 1462 taskOffsetTranslationY + 1463 taskResistanceTranslationY + 1464 splitSelectTranslationY + 1465 persistentTranslationY 1466 } 1467 1468 private fun onGridProgressChanged() { 1469 applyTranslationX() 1470 applyTranslationY() 1471 applyScale() 1472 } 1473 1474 protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) { 1475 taskContainers.forEach { 1476 it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) 1477 it.overlay.setFullscreenProgress(fullscreenProgress) 1478 } 1479 focusTransitionFullscreen.value = 1480 FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress) 1481 updateSnapshotRadius() 1482 } 1483 1484 protected open fun updateSnapshotRadius() { 1485 updateCurrentFullscreenParams() 1486 taskContainers.forEach { 1487 it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams()) 1488 it.overlay.setFullscreenParams(getThumbnailFullscreenParams()) 1489 } 1490 } 1491 1492 protected open fun updateCurrentFullscreenParams() { 1493 updateFullscreenParams(currentFullscreenParams) 1494 } 1495 1496 protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) { 1497 recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) } 1498 } 1499 1500 protected open fun getThumbnailFullscreenParams(): FullscreenDrawParams = 1501 currentFullscreenParams 1502 1503 private fun onModalnessUpdated(modalness: Float) { 1504 taskContainers.forEach { 1505 it.iconView.setModalAlpha(1 - modalness) 1506 it.digitalWellBeingToast?.updateBannerOffset(modalness) 1507 } 1508 } 1509 1510 /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */ 1511 fun notifyIsRunningTaskUpdated() { 1512 // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM 1513 // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView 1514 taskContainers.forEach { it.bindThumbnailView() } 1515 } 1516 1517 fun resetPersistentViewTransforms() { 1518 nonGridTranslationX = 0f 1519 gridTranslationX = 0f 1520 gridTranslationY = 0f 1521 boxTranslationY = 0f 1522 nonGridPivotTranslationX = 0f 1523 resetViewTransforms() 1524 } 1525 1526 open fun resetViewTransforms() { 1527 // fullscreenTranslation and accumulatedTranslation should not be reset, as 1528 // resetViewTransforms is called during QuickSwitch scrolling. 1529 dismissTranslationX = 0f 1530 taskOffsetTranslationX = 0f 1531 taskResistanceTranslationX = 0f 1532 splitSelectTranslationX = 0f 1533 gridEndTranslationX = 0f 1534 dismissTranslationY = 0f 1535 taskOffsetTranslationY = 0f 1536 taskResistanceTranslationY = 0f 1537 if (recentsView?.isSplitSelectionActive != true) { 1538 splitSelectTranslationY = 0f 1539 } 1540 dismissScale = 1f 1541 translationZ = 0f 1542 alpha = stableAlpha 1543 setIconScaleAndDim(1f) 1544 setColorTint(0f, 0) 1545 if (!enableRefactorTaskThumbnail()) { 1546 // TODO(b/335399428) add split select functionality to new TTV 1547 taskContainers.forEach { it.thumbnailViewDeprecated.resetViewTransforms() } 1548 } 1549 } 1550 1551 private fun getGridTrans(endTranslation: Float) = 1552 Utilities.mapRange(gridProgress, 0f, endTranslation) 1553 1554 private fun getNonGridTrans(endTranslation: Float) = 1555 endTranslation - getGridTrans(endTranslation) 1556 1557 /** We update and subsequently draw these in [fullscreenProgress]. */ 1558 open class FullscreenDrawParams(context: Context) : SafeCloseable { 1559 var cornerRadius = 0f 1560 private var windowCornerRadius = 0f 1561 var currentDrawnCornerRadius = 0f 1562 1563 init { 1564 updateCornerRadius(context) 1565 } 1566 1567 /** Recomputes the start and end corner radius for the given Context. */ 1568 fun updateCornerRadius(context: Context) { 1569 cornerRadius = computeTaskCornerRadius(context) 1570 windowCornerRadius = computeWindowCornerRadius(context) 1571 } 1572 1573 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 1574 open fun computeTaskCornerRadius(context: Context): Float { 1575 return TaskCornerRadius.get(context) 1576 } 1577 1578 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 1579 open fun computeWindowCornerRadius(context: Context): Float { 1580 val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context) 1581 1582 // The corner radius is fixed to match when Taskbar is persistent mode 1583 return if ( 1584 activityContext != null && 1585 activityContext.deviceProfile?.isTaskbarPresent == true && 1586 DisplayController.isTransientTaskbar(context) 1587 ) { 1588 context.resources 1589 .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius) 1590 .toFloat() 1591 } else { 1592 QuickStepContract.getWindowCornerRadius(context) 1593 } 1594 } 1595 1596 /** Sets the progress in range [0, 1] */ 1597 fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) { 1598 currentDrawnCornerRadius = 1599 Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) / 1600 parentScale / 1601 taskViewScale 1602 } 1603 1604 override fun close() {} 1605 } 1606 1607 /** Holder for all Task dependent information. */ 1608 inner class TaskContainer( 1609 val task: Task, 1610 val thumbnailView: TaskThumbnailView?, 1611 val thumbnailViewDeprecated: TaskThumbnailViewDeprecated, 1612 val iconView: TaskViewIcon, 1613 /** 1614 * This technically can be a vanilla [android.view.TouchDelegate] class, however that class 1615 * requires setting the touch bounds at construction, so we'd repeatedly be created many 1616 * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows 1617 * touch delegated bounds only to be updated. 1618 */ 1619 val iconTouchDelegate: TransformingTouchDelegate, 1620 /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ 1621 @StagePosition val stagePosition: Int, 1622 val digitalWellBeingToast: DigitalWellBeingToast?, 1623 val showWindowsView: View?, 1624 taskOverlayFactory: TaskOverlayFactory 1625 ) { 1626 val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this) 1627 1628 val snapshotView: View 1629 get() = thumbnailView ?: thumbnailViewDeprecated 1630 1631 /** Builds proto for logging */ 1632 val itemInfo: WorkspaceItemInfo 1633 get() = 1634 WorkspaceItemInfo().apply { 1635 itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK 1636 container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER 1637 val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key) 1638 user = componentKey.user 1639 intent = Intent().setComponent(componentKey.componentName) 1640 title = task.title 1641 recentsView?.let { screenId = it.indexOfChild(this@TaskView) } 1642 if (privateSpaceRestrictAccessibilityDrag()) { 1643 if ( 1644 UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate 1645 ) { 1646 runtimeStatusFlags = 1647 runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE 1648 } 1649 } 1650 } 1651 1652 val taskView: TaskView 1653 get() = this@TaskView 1654 1655 fun destroy() { 1656 digitalWellBeingToast?.destroy() 1657 thumbnailView?.let { taskView.removeView(it) } 1658 } 1659 1660 // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM 1661 // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView 1662 fun bindThumbnailView() { 1663 // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but 1664 // this should be decided inside TaskThumbnailViewModel. 1665 thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask)) 1666 } 1667 } 1668 1669 companion object { 1670 private const val TAG = "TaskView" 1671 const val FLAG_UPDATE_ICON = 1 1672 const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1 1673 const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1 1674 const val FLAG_UPDATE_ALL = 1675 (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS) 1676 1677 const val FOCUS_TRANSITION_INDEX_FULLSCREEN = 0 1678 const val FOCUS_TRANSITION_INDEX_SCALE_AND_DIM = 1 1679 const val FOCUS_TRANSITION_INDEX_COUNT = 2 1680 1681 /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ 1682 const val MAX_PAGE_SCRIM_ALPHA = 0.4f 1683 const val SCALE_ICON_DURATION: Long = 120 1684 private const val DIM_ANIM_DURATION: Long = 700 1685 private const val FOCUS_TRANSITION_THRESHOLD = 1686 SCALE_ICON_DURATION.toFloat() / DIM_ANIM_DURATION 1687 val FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR = 1688 Interpolators.clampToProgress( 1689 Interpolators.FAST_OUT_SLOW_IN, 1690 1f - FOCUS_TRANSITION_THRESHOLD, 1691 1f 1692 )!! 1693 private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect()) 1694 1695 private val FOCUS_TRANSITION: FloatProperty<TaskView> = 1696 object : FloatProperty<TaskView>("focusTransition") { 1697 override fun setValue(taskView: TaskView, v: Float) { 1698 taskView.focusTransitionProgress = v 1699 } 1700 1701 override fun get(taskView: TaskView) = taskView.focusTransitionProgress 1702 } 1703 1704 private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> = 1705 object : FloatProperty<TaskView>("splitSelectTranslationX") { 1706 override fun setValue(taskView: TaskView, v: Float) { 1707 taskView.splitSelectTranslationX = v 1708 } 1709 1710 override fun get(taskView: TaskView) = taskView.splitSelectTranslationX 1711 } 1712 1713 private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty<TaskView> = 1714 object : FloatProperty<TaskView>("splitSelectTranslationY") { 1715 override fun setValue(taskView: TaskView, v: Float) { 1716 taskView.splitSelectTranslationY = v 1717 } 1718 1719 override fun get(taskView: TaskView) = taskView.splitSelectTranslationY 1720 } 1721 1722 private val DISMISS_TRANSLATION_X: FloatProperty<TaskView> = 1723 object : FloatProperty<TaskView>("dismissTranslationX") { 1724 override fun setValue(taskView: TaskView, v: Float) { 1725 taskView.dismissTranslationX = v 1726 } 1727 1728 override fun get(taskView: TaskView) = taskView.dismissTranslationX 1729 } 1730 1731 private val DISMISS_TRANSLATION_Y: FloatProperty<TaskView> = 1732 object : FloatProperty<TaskView>("dismissTranslationY") { 1733 override fun setValue(taskView: TaskView, v: Float) { 1734 taskView.dismissTranslationY = v 1735 } 1736 1737 override fun get(taskView: TaskView) = taskView.dismissTranslationY 1738 } 1739 1740 private val TASK_OFFSET_TRANSLATION_X: FloatProperty<TaskView> = 1741 object : FloatProperty<TaskView>("taskOffsetTranslationX") { 1742 override fun setValue(taskView: TaskView, v: Float) { 1743 taskView.taskOffsetTranslationX = v 1744 } 1745 1746 override fun get(taskView: TaskView) = taskView.taskOffsetTranslationX 1747 } 1748 1749 private val TASK_OFFSET_TRANSLATION_Y: FloatProperty<TaskView> = 1750 object : FloatProperty<TaskView>("taskOffsetTranslationY") { 1751 override fun setValue(taskView: TaskView, v: Float) { 1752 taskView.taskOffsetTranslationY = v 1753 } 1754 1755 override fun get(taskView: TaskView) = taskView.taskOffsetTranslationY 1756 } 1757 1758 private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty<TaskView> = 1759 object : FloatProperty<TaskView>("taskResistanceTranslationX") { 1760 override fun setValue(taskView: TaskView, v: Float) { 1761 taskView.taskResistanceTranslationX = v 1762 } 1763 1764 override fun get(taskView: TaskView) = taskView.taskResistanceTranslationX 1765 } 1766 1767 private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty<TaskView> = 1768 object : FloatProperty<TaskView>("taskResistanceTranslationY") { 1769 override fun setValue(taskView: TaskView, v: Float) { 1770 taskView.taskResistanceTranslationY = v 1771 } 1772 1773 override fun get(taskView: TaskView) = taskView.taskResistanceTranslationY 1774 } 1775 1776 @JvmField 1777 val GRID_END_TRANSLATION_X: FloatProperty<TaskView> = 1778 object : FloatProperty<TaskView>("gridEndTranslationX") { 1779 override fun setValue(taskView: TaskView, v: Float) { 1780 taskView.gridEndTranslationX = v 1781 } 1782 1783 override fun get(taskView: TaskView) = taskView.gridEndTranslationX 1784 } 1785 1786 @JvmField 1787 val DISMISS_SCALE: FloatProperty<TaskView> = 1788 object : FloatProperty<TaskView>("dismissScale") { 1789 override fun setValue(taskView: TaskView, v: Float) { 1790 taskView.dismissScale = v 1791 } 1792 1793 override fun get(taskView: TaskView) = taskView.dismissScale 1794 } 1795 } 1796 } 1797