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