1 /* <lambda>null2 * Copyright (C) 2022 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.annotation.SuppressLint 19 import android.content.Context 20 import android.graphics.Matrix 21 import android.graphics.PointF 22 import android.graphics.Rect 23 import android.graphics.Rect.intersects 24 import android.graphics.RectF 25 import android.util.AttributeSet 26 import android.util.Log 27 import android.util.Size 28 import android.view.Display.INVALID_DISPLAY 29 import android.view.Gravity 30 import android.view.View 31 import android.view.ViewStub 32 import androidx.core.content.res.ResourcesCompat 33 import androidx.core.view.updateLayoutParams 34 import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix 35 import com.android.launcher3.Flags.enableDesktopExplodedView 36 import com.android.launcher3.Flags.enableOverviewIconMenu 37 import com.android.launcher3.Flags.enableRefactorTaskThumbnail 38 import com.android.launcher3.R 39 import com.android.launcher3.statehandlers.DesktopVisibilityController 40 import com.android.launcher3.testing.TestLogging 41 import com.android.launcher3.testing.shared.TestProtocol 42 import com.android.launcher3.util.RunnableList 43 import com.android.launcher3.util.SplitConfigurationOptions 44 import com.android.launcher3.util.TransformingTouchDelegate 45 import com.android.launcher3.util.ViewPool 46 import com.android.launcher3.util.rects.lerpRect 47 import com.android.launcher3.util.rects.set 48 import com.android.quickstep.BaseContainerInterface 49 import com.android.quickstep.DesktopFullscreenDrawParams 50 import com.android.quickstep.FullscreenDrawParams 51 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle 52 import com.android.quickstep.TaskOverlayFactory 53 import com.android.quickstep.ViewUtils 54 import com.android.quickstep.recents.di.RecentsDependencies 55 import com.android.quickstep.recents.di.get 56 import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData 57 import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel 58 import com.android.quickstep.recents.ui.viewmodel.TaskData 59 import com.android.quickstep.task.thumbnail.TaskThumbnailView 60 import com.android.quickstep.util.DesktopTask 61 import com.android.quickstep.util.RecentsOrientedState 62 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops 63 import kotlin.math.roundToInt 64 65 /** TaskView that contains all tasks that are part of the desktop. */ 66 class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : 67 TaskView( 68 context, 69 attrs, 70 type = TaskViewType.DESKTOP, 71 thumbnailFullscreenParams = DesktopFullscreenDrawParams(context), 72 ) { 73 val deskId 74 get() = desktopTask?.deskId ?: DesktopVisibilityController.INACTIVE_DESK_ID 75 76 private var desktopTask: DesktopTask? = null 77 78 private val contentViewFullscreenParams = FullscreenDrawParams(context) 79 80 private val taskThumbnailViewDeprecatedPool = 81 if (!enableRefactorTaskThumbnail()) { 82 ViewPool<TaskThumbnailViewDeprecated>( 83 context, 84 this, 85 R.layout.task_thumbnail_deprecated, 86 VIEW_POOL_MAX_SIZE, 87 VIEW_POOL_INITIAL_SIZE, 88 ) 89 } else null 90 91 private val taskThumbnailViewPool = 92 if (enableRefactorTaskThumbnail()) { 93 ViewPool<TaskThumbnailView>( 94 context, 95 this, 96 R.layout.task_thumbnail, 97 VIEW_POOL_MAX_SIZE, 98 VIEW_POOL_INITIAL_SIZE, 99 ) 100 } else null 101 102 private val tempPointF = PointF() 103 private val lastComputedTaskSize = Rect() 104 private lateinit var iconView: TaskViewIcon 105 private lateinit var contentView: DesktopTaskContentView 106 private lateinit var backgroundView: View 107 108 private var viewModel: DesktopTaskViewModel? = null 109 110 /** 111 * Holds the default (user placed) positions of task windows. This can be moved into the 112 * viewModel once RefactorTaskThumbnail has been launched. 113 */ 114 private var fullscreenTaskPositions: List<DesktopTaskBoundsData> = emptyList() 115 116 /** 117 * When enableDesktopExplodedView is enabled, this controls the gradual transition from the 118 * default positions to the organized non-overlapping positions. 119 */ 120 var explodeProgress = 0.0f 121 set(value) { 122 field = value 123 positionTaskWindows() 124 } 125 126 var remoteTargetHandles: Array<RemoteTargetHandle>? = null 127 set(value) { 128 field = value 129 positionTaskWindows() 130 } 131 132 override val displayId: Int 133 get() = 134 if (enableMultipleDesktops(context)) { 135 desktopTask?.displayId ?: INVALID_DISPLAY 136 } else { 137 super.displayId 138 } 139 140 private fun getRemoteTargetHandle(taskId: Int): RemoteTargetHandle? = 141 remoteTargetHandles?.firstOrNull { 142 it.transformParams.targetSet.firstAppTargetTaskId == taskId 143 } 144 145 override fun onFinishInflate() { 146 super.onFinishInflate() 147 iconView = 148 (findViewById<View>(R.id.icon) as TaskViewIcon).apply { 149 setIcon( 150 this, 151 ResourcesCompat.getDrawable( 152 context.resources, 153 R.drawable.ic_desktop_with_bg, 154 context.theme, 155 ), 156 ) 157 setText(resources.getText(R.string.recent_task_desktop)) 158 } 159 contentView = 160 findViewById<DesktopTaskContentView>(R.id.desktop_content).apply { 161 updateLayoutParams<LayoutParams> { 162 topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx 163 } 164 cornerRadius = contentViewFullscreenParams.currentCornerRadius 165 backgroundView = findViewById(R.id.background) 166 backgroundView.setBackgroundColor( 167 resources.getColor(android.R.color.system_neutral2_300, context.theme) 168 ) 169 } 170 } 171 172 override fun inflateViewStubs() { 173 findViewById<ViewStub>(R.id.icon) 174 ?.apply { 175 layoutResource = 176 if (enableOverviewIconMenu()) R.layout.icon_app_chip_view 177 else R.layout.icon_view 178 } 179 ?.inflate() 180 } 181 182 private fun positionTaskWindows() { 183 if (taskContainers.isEmpty()) { 184 return 185 } 186 187 val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx 188 189 val taskViewWidth = layoutParams.width 190 val taskViewHeight = layoutParams.height - thumbnailTopMarginPx 191 192 BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) 193 194 val screenWidth = tempPointF.x.toInt() 195 val screenHeight = tempPointF.y.toInt() 196 val screenRect = Rect(0, 0, screenWidth, screenHeight) 197 val scaleWidth = taskViewWidth / screenWidth.toFloat() 198 val scaleHeight = taskViewHeight / screenHeight.toFloat() 199 200 taskContainers.forEach { 201 val taskId = it.task.key.id 202 val fullscreenTaskPosition = 203 fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return 204 val overviewTaskPosition = 205 if (enableDesktopExplodedView()) { 206 viewModel!! 207 .organizedDesktopTaskPositions 208 .firstOrNull { it.taskId == taskId } 209 ?.let { organizedPosition -> 210 TEMP_OVERVIEW_TASK_POSITION.apply { 211 lerpRect( 212 fullscreenTaskPosition.bounds, 213 organizedPosition.bounds, 214 explodeProgress, 215 ) 216 } 217 } ?: fullscreenTaskPosition.bounds 218 } else { 219 fullscreenTaskPosition.bounds 220 } 221 222 if (enableDesktopExplodedView()) { 223 getRemoteTargetHandle(taskId)?.let { remoteTargetHandle -> 224 val fromRect = 225 TEMP_FROM_RECTF.apply { 226 set(fullscreenTaskPosition.bounds) 227 scale(scaleWidth) 228 offset( 229 lastComputedTaskSize.left.toFloat(), 230 lastComputedTaskSize.top.toFloat(), 231 ) 232 } 233 val toRect = 234 TEMP_TO_RECTF.apply { 235 set(overviewTaskPosition) 236 scale(scaleWidth) 237 offset( 238 lastComputedTaskSize.left.toFloat(), 239 lastComputedTaskSize.top.toFloat(), 240 ) 241 } 242 val transform = Matrix() 243 transform.setRectToRect(fromRect, toRect, Matrix.ScaleToFit.FILL) 244 remoteTargetHandle.taskViewSimulator.setTaskRectTransform(transform) 245 remoteTargetHandle.taskViewSimulator.apply(remoteTargetHandle.transformParams) 246 } 247 } 248 249 val taskLeft = overviewTaskPosition.left * scaleWidth 250 val taskTop = overviewTaskPosition.top * scaleHeight 251 val taskWidth = overviewTaskPosition.width() * scaleWidth 252 val taskHeight = overviewTaskPosition.height() * scaleHeight 253 // TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1. 254 // To run the explode animation in reverse, it may be simpler to use translation/scale 255 // for all cases where the progress is non-zero. 256 if (explodeProgress == 0.0f || explodeProgress == 1.0f) { 257 // Reset scaling and translation that may have been applied during animation. 258 it.snapshotView.apply { 259 scaleX = 1.0f 260 scaleY = 1.0f 261 translationX = 0.0f 262 translationY = 0.0f 263 } 264 265 // Position the task to the same position as it would be on the desktop 266 it.snapshotView.updateLayoutParams<LayoutParams> { 267 gravity = Gravity.LEFT or Gravity.TOP 268 width = taskWidth.toInt() 269 height = taskHeight.toInt() 270 leftMargin = taskLeft.toInt() 271 topMargin = taskTop.toInt() 272 } 273 274 if ( 275 enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail() 276 ) { 277 it.thumbnailView.outlineBounds = 278 if (intersects(overviewTaskPosition, screenRect)) 279 Rect(overviewTaskPosition).apply { 280 intersectUnchecked(screenRect) 281 // Offset to 0,0 to transform into TaskThumbnailView's coordinate 282 // system. 283 offset(-overviewTaskPosition.left, -overviewTaskPosition.top) 284 left = (left * scaleWidth).roundToInt() 285 top = (top * scaleHeight).roundToInt() 286 right = (right * scaleWidth).roundToInt() 287 bottom = (bottom * scaleHeight).roundToInt() 288 } 289 else null 290 } 291 } else { 292 // During the animation, apply translation and scale such that the view is 293 // transformed to where we want, without triggering layout. 294 it.snapshotView.apply { 295 pivotX = 0.0f 296 pivotY = 0.0f 297 translationX = taskLeft - left 298 translationY = taskTop - top 299 scaleX = taskWidth / width.toFloat() 300 scaleY = taskHeight / height.toFloat() 301 } 302 } 303 } 304 } 305 306 /** Updates this desktop task to the gives task list defined in `tasks` */ 307 fun bind( 308 desktopTask: DesktopTask, 309 orientedState: RecentsOrientedState, 310 taskOverlayFactory: TaskOverlayFactory, 311 ) { 312 this.desktopTask = desktopTask 313 // TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view 314 // support. 315 // Minimized tasks should not be shown in Overview. 316 val tasks = desktopTask.tasks.filterNot { it.isMinimized } 317 if (DEBUG) { 318 val sb = StringBuilder() 319 sb.append("bind tasks=").append(tasks.size).append("\n") 320 tasks.forEach { sb.append(" key=${it.key}\n") } 321 Log.d(TAG, sb.toString()) 322 } 323 324 cancelPendingLoadTasks() 325 val backgroundViewIndex = contentView.indexOfChild(backgroundView) 326 taskContainers = 327 tasks.map { task -> 328 val snapshotView = 329 if (enableRefactorTaskThumbnail()) { 330 taskThumbnailViewPool!!.view 331 } else { 332 taskThumbnailViewDeprecatedPool!!.view 333 } 334 contentView.addView(snapshotView, backgroundViewIndex + 1) 335 336 TaskContainer( 337 this, 338 task, 339 snapshotView, 340 iconView, 341 TransformingTouchDelegate(iconView.asView()), 342 SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, 343 digitalWellBeingToast = null, 344 showWindowsView = null, 345 taskOverlayFactory, 346 ) 347 } 348 onBind(orientedState) 349 } 350 351 override fun onBind(orientedState: RecentsOrientedState) { 352 super.onBind(orientedState) 353 354 if (enableRefactorTaskThumbnail()) { 355 viewModel = 356 DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get(context)) 357 } 358 } 359 360 override fun onRecycle() { 361 super.onRecycle() 362 desktopTask = null 363 explodeProgress = 0.0f 364 viewModel = null 365 visibility = VISIBLE 366 taskContainers.forEach { removeAndRecycleThumbnailView(it) } 367 } 368 369 override fun setOrientationState(orientationState: RecentsOrientedState) { 370 super.setOrientationState(orientationState) 371 iconView.setIconOrientation(orientationState, isGridTask) 372 } 373 374 @SuppressLint("RtlHardcoded") 375 override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) { 376 super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize) 377 this.lastComputedTaskSize.set(lastComputedTaskSize) 378 379 updateTaskPositions() 380 } 381 382 override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { 383 super.onTaskListVisibilityChanged(visible, changes) 384 if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { 385 contentViewFullscreenParams.updateCornerRadius(context) 386 } 387 } 388 389 override fun onIconLoaded(taskContainer: TaskContainer) { 390 // Update contentDescription of snapshotView only, individual task icon is unused. 391 taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription 392 } 393 394 override fun setIconState(container: TaskContainer, state: TaskData?) { 395 container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription 396 } 397 398 // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon 399 override fun onIconUnloaded(taskContainer: TaskContainer) {} 400 401 // thumbnailView is laid out differently and is handled in onMeasure 402 override fun updateThumbnailSize() {} 403 404 override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { 405 if (relativeToDragLayer) { 406 container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds) 407 } else { 408 bounds.set(contentView) 409 } 410 } 411 412 private fun launchTaskWithDesktopController(animated: Boolean): RunnableList? { 413 val recentsView = recentsView ?: return null 414 TestLogging.recordEvent( 415 TestProtocol.SEQUENCE_MAIN, 416 "launchDesktopFromRecents", 417 taskIds.contentToString(), 418 ) 419 val endCallback = RunnableList() 420 val desktopController = recentsView.desktopRecentsController 421 checkNotNull(desktopController) { "recentsController is null" } 422 desktopController.launchDesktopFromRecents(this, animated) { 423 endCallback.executeAllAndDestroy() 424 } 425 Log.d( 426 TAG, 427 "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated", 428 ) 429 430 // Callbacks get run from recentsView for case when recents animation already running 431 recentsView.addSideTaskLaunchCallback(endCallback) 432 return endCallback 433 } 434 435 override fun launchAsStaticTile() = launchTaskWithDesktopController(animated = true) 436 437 override fun launchWithoutAnimation( 438 isQuickSwitch: Boolean, 439 callback: (launched: Boolean) -> Unit, 440 ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false) 441 442 // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip 443 // putting DesktopTaskView to split as it's not supported. 444 override fun confirmSecondSplitSelectApp(): Boolean = 445 recentsView?.canLaunchFullscreenTask() != true 446 447 // TODO(b/330685808) support overlay for Screenshot action 448 override fun setOverlayEnabled(overlayEnabled: Boolean) {} 449 450 override fun onFullscreenProgressChanged(fullscreenProgress: Float) { 451 backgroundView.alpha = 1 - fullscreenProgress 452 } 453 454 override fun updateFullscreenParams() { 455 super.updateFullscreenParams() 456 updateFullscreenParams(contentViewFullscreenParams) 457 contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius 458 } 459 460 override fun addChildrenForAccessibility(outChildren: ArrayList<View>) { 461 super.addChildrenForAccessibility(outChildren) 462 ViewUtils.addAccessibleChildToList(backgroundView, outChildren) 463 } 464 465 fun removeTaskFromExplodedView(taskId: Int, animate: Boolean) { 466 if (!enableDesktopExplodedView()) { 467 Log.e( 468 TAG, 469 "removeTaskFromExplodedView called when enableDesktopExplodedView flag is false", 470 ) 471 return 472 } 473 474 // Remove the task's [taskContainer] and its associated Views. 475 val taskContainer = getTaskContainerById(taskId) ?: return 476 removeAndRecycleThumbnailView(taskContainer) 477 taskContainer.destroy() 478 taskContainers = taskContainers.filterNot { it == taskContainer } 479 480 // Dismiss the current DesktopTaskView if all its windows are closed. 481 if (taskContainers.isEmpty()) { 482 recentsView?.dismissTaskView(this, animate, /* removeTask= */ true) 483 } else { 484 // Otherwise, re-position the remaining task windows. 485 // TODO(b/353949276): Implement the re-layout animations. 486 updateTaskPositions() 487 } 488 } 489 490 private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) { 491 contentView.removeView(taskContainer.snapshotView) 492 if (enableRefactorTaskThumbnail()) { 493 taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView) 494 } else { 495 taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated) 496 } 497 } 498 499 private fun updateTaskPositions() { 500 BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) 501 val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt()) 502 DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4) 503 504 fullscreenTaskPositions = 505 taskContainers.map { 506 DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS) 507 } 508 509 if (enableDesktopExplodedView()) { 510 viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions) 511 } 512 positionTaskWindows() 513 } 514 515 companion object { 516 private const val TAG = "DesktopTaskView" 517 private const val DEBUG = false 518 private const val VIEW_POOL_MAX_SIZE = 5 519 520 // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool. 521 private const val VIEW_POOL_INITIAL_SIZE = 0 522 private val DEFAULT_BOUNDS = Rect() 523 // Temporaries used for various purposes to avoid allocations. 524 private val TEMP_OVERVIEW_TASK_POSITION = Rect() 525 private val TEMP_FROM_RECTF = RectF() 526 private val TEMP_TO_RECTF = RectF() 527 } 528 } 529