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