1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.views 18 19 import android.graphics.PointF 20 import android.graphics.Rect 21 import android.util.FloatProperty 22 import android.view.KeyEvent 23 import android.view.View 24 import android.view.View.LAYOUT_DIRECTION_LTR 25 import android.view.View.LAYOUT_DIRECTION_RTL 26 import androidx.core.view.children 27 import com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU 28 import com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType 29 import com.android.launcher3.Flags.enableGridOnlyOverview 30 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile 31 import com.android.launcher3.Flags.enableOverviewIconMenu 32 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks 33 import com.android.launcher3.Utilities.getPivotsForScalingRectToRect 34 import com.android.launcher3.statehandlers.DesktopVisibilityController 35 import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID 36 import com.android.launcher3.util.IntArray 37 import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled 38 import com.android.quickstep.util.DesktopTask 39 import com.android.quickstep.util.GroupTask 40 import com.android.quickstep.util.isExternalDisplay 41 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA 42 import com.android.systemui.shared.recents.model.Task 43 import com.android.systemui.shared.recents.model.ThumbnailData 44 import com.android.wm.shell.shared.GroupedTaskInfo 45 import java.util.function.BiConsumer 46 import kotlin.math.min 47 import kotlin.reflect.KMutableProperty1 48 49 /** 50 * Helper class for [RecentsView]. This util class contains refactored and extracted functions from 51 * RecentsView to facilitate the implementation of unit tests. 52 */ 53 class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { 54 val taskViews = TaskViewsIterable(recentsView) 55 56 /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */ 57 fun screenshotTasks(taskView: TaskView): Map<Int, ThumbnailData> { 58 val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap() 59 return taskView.taskContainers.associate { 60 it.task.key.id to recentsAnimationController.screenshotTask(it.task.key.id) 61 } 62 } 63 64 /** 65 * Sorts task groups to move desktop tasks to the end of the list. 66 * 67 * @param tasks List of group tasks to be sorted. 68 * @return Sorted list of GroupTasks to be used in the RecentsView. 69 */ 70 fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> { 71 var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP } 72 if (areMultiDesksFlagsEnabled()) { 73 // Desk IDs of newer desks are larger than those of older desks, hence we can use them 74 // to sort desks from old to new. 75 desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId } 76 } 77 return otherTasks + desktopTasks 78 } 79 80 fun sortExternalDisplayTasksToFront(tasks: List<GroupTask>): List<GroupTask> { 81 val (externalDisplayTasks, otherTasks) = 82 tasks.partition { it.tasks.firstOrNull().isExternalDisplay } 83 return otherTasks + externalDisplayTasks 84 } 85 86 class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable<TaskView> { 87 /** Iterates TaskViews when its index inside the RecentsView is needed. */ 88 fun forEachWithIndexInParent(consumer: BiConsumer<Int, TaskView>) { 89 recentsView.children.forEachIndexed { index, child -> 90 (child as? TaskView)?.let { consumer.accept(index, it) } 91 } 92 } 93 94 override fun iterator(): Iterator<TaskView> = 95 recentsView.children.mapNotNull { it as? TaskView }.iterator() 96 } 97 98 /** Counts [TaskView]s that are [DesktopTaskView] instances. */ 99 private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView } 100 101 /** Counts [TaskView]s that are not [DesktopTaskView] instances. */ 102 fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView } 103 104 /** Returns a list of all large TaskView Ids from [TaskView]s */ 105 fun getLargeTaskViewIds(): List<Int> = taskViews.filter { it.isLargeTile }.map { it.taskViewId } 106 107 /** Returns a list of all large TaskViews [TaskView]s */ 108 fun getLargeTaskViews(): List<TaskView> = taskViews.filter { it.isLargeTile } 109 110 /** Returns all the TaskViews in the top row, without the focused task */ 111 fun getTopRowTaskViews(): List<TaskView> = 112 taskViews.filter { recentsView.mTopRowIdSet.contains(it.taskViewId) } 113 114 /** Returns all the task Ids in the top row, without the focused task */ 115 fun getTopRowIdArray(): IntArray = getTopRowTaskViews().map { it.taskViewId }.toIntArray() 116 117 /** Returns all the TaskViews in the bottom row, without the focused task */ 118 fun getBottomRowTaskViews(): List<TaskView> = 119 taskViews.filter { !recentsView.mTopRowIdSet.contains(it.taskViewId) && !it.isLargeTile } 120 121 /** Returns all the task Ids in the bottom row, without the focused task */ 122 fun getBottomRowIdArray(): IntArray = getBottomRowTaskViews().map { it.taskViewId }.toIntArray() 123 124 private fun List<Int>.toIntArray() = IntArray(size).apply { this@toIntArray.forEach(::add) } 125 126 /** Counts [TaskView]s that are large tiles. */ 127 fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile } 128 129 /** Counts [TaskView]s that are grid tasks. */ 130 fun getGridTaskCount(): Int = taskViews.count { it.isGridTask } 131 132 /** Returns the first TaskView that should be displayed as a large tile. */ 133 fun getFirstLargeTaskView(): TaskView? = 134 taskViews.firstOrNull { 135 it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView) 136 } 137 138 /** 139 * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist. 140 */ 141 fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? { 142 if (deskId == INACTIVE_DESK_ID) { 143 return null 144 } 145 return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId } 146 as? DesktopTaskView 147 } 148 149 /** Returns the active desk ID of the display that contains the [recentsView] instance. */ 150 fun getActiveDeskIdOnThisDisplay(): Int = 151 DesktopVisibilityController.INSTANCE.get(recentsView.context) 152 .getActiveDeskId(recentsView.mContainer.display.displayId) 153 154 /** Returns the expected focus task. */ 155 fun getFirstNonDesktopTaskView(): TaskView? = 156 if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView } 157 else taskViews.firstOrNull() 158 159 /** 160 * Returns the [TaskView] that should be the current page during task binding, in the following 161 * priorities: 162 * 1. Running task 163 * 2. Focused task 164 * 3. First non-desktop task 165 * 4. Last desktop task 166 * 5. null otherwise 167 */ 168 fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? = 169 runningTaskView 170 ?: focusedTaskView 171 ?: taskViews.firstOrNull { 172 it !is DesktopTaskView && 173 !(enableSeparateExternalDisplayTasks() && it.isExternalDisplay) 174 } 175 ?: taskViews.lastOrNull() 176 177 private fun getDeviceProfile() = (recentsView.mContainer as RecentsViewContainer).deviceProfile 178 179 fun getRunningTaskExpectedIndex(runningTaskView: TaskView): Int { 180 if (areMultiDesksFlagsEnabled() && runningTaskView is DesktopTaskView) { 181 // Use the [deskId] to keep desks in the order of their creation, as a newer desk 182 // always has a larger [deskId] than the older desks. 183 val desktopTaskView = 184 taskViews.firstOrNull { 185 it is DesktopTaskView && 186 it.deskId != INACTIVE_DESK_ID && 187 it.deskId <= runningTaskView.deskId 188 } 189 if (desktopTaskView != null) return recentsView.indexOfChild(desktopTaskView) 190 } 191 val firstTaskViewIndex = recentsView.indexOfChild(getFirstTaskView()) 192 return if (getDeviceProfile().isTablet) { 193 var index = firstTaskViewIndex 194 if (enableLargeDesktopWindowingTile() && runningTaskView !is DesktopTaskView) { 195 // For fullsreen tasks, skip over Desktop tasks in its section 196 index += 197 if (enableSeparateExternalDisplayTasks()) { 198 if (runningTaskView.isExternalDisplay) { 199 taskViews.count { it is DesktopTaskView && it.isExternalDisplay } 200 } else { 201 taskViews.count { it is DesktopTaskView && !it.isExternalDisplay } 202 } 203 } else { 204 getDesktopTaskViewCount() 205 } 206 } 207 if (enableSeparateExternalDisplayTasks() && !runningTaskView.isExternalDisplay) { 208 // For main display section, skip over external display tasks 209 index += taskViews.count { it.isExternalDisplay } 210 } 211 index 212 } else { 213 val currentIndex: Int = recentsView.indexOfChild(runningTaskView) 214 return if (currentIndex != -1) { 215 currentIndex // Keep the position if running task already in layout. 216 } else { 217 // New running task are added to the front to begin with. 218 firstTaskViewIndex 219 } 220 } 221 } 222 223 /** Returns the first TaskView if it exists, or null otherwise. */ 224 fun getFirstTaskView(): TaskView? = taskViews.firstOrNull() 225 226 /** Returns the last TaskView if it exists, or null otherwise. */ 227 fun getLastTaskView(): TaskView? = taskViews.lastOrNull() 228 229 /** Returns the first TaskView that is not large */ 230 fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile } 231 232 /** Returns the last TaskView that should be displayed as a large tile. */ 233 fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile } 234 235 /** 236 * Gets the list of accessibility children. Currently all the children of RecentsViews are 237 * added, and in the reverse order to the list. 238 */ 239 fun getAccessibilityChildren(): List<View> = recentsView.children.toList().reversed() 240 241 @JvmOverloads 242 /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */ 243 fun getFirstTaskViewInCarousel( 244 nonRunningTaskCarouselHidden: Boolean, 245 runningTaskView: TaskView? = recentsView.runningTaskView, 246 ): TaskView? = 247 taskViews.firstOrNull { 248 it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden) 249 } 250 251 /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */ 252 fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? = 253 taskViews.lastOrNull { 254 it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden) 255 } 256 257 /** Returns if any small tasks are fully visible */ 258 fun isAnySmallTaskFullyVisible(): Boolean = 259 taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) } 260 261 /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */ 262 fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) { 263 taskViews.forEach { taskView -> 264 taskView.attachAlpha = 265 if (taskView == recentsView.runningTaskView) { 266 RUNNING_TASK_ATTACH_ALPHA.get(recentsView) 267 } else { 268 if ( 269 taskView.isVisibleInCarousel( 270 recentsView.runningTaskView, 271 nonRunningTaskCarouselHidden, 272 ) 273 ) 274 1f 275 else 0f 276 } 277 } 278 } 279 280 fun TaskView.isVisibleInCarousel( 281 runningTaskView: TaskView?, 282 nonRunningTaskCarouselHidden: Boolean, 283 ): Boolean = 284 if (!nonRunningTaskCarouselHidden) true 285 else getCarouselType() == runningTaskView.getCarouselType() 286 287 /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */ 288 private fun TaskView?.getCarouselType(): TaskViewCarousel = 289 if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN 290 291 private enum class TaskViewCarousel { 292 FULL_SCREEN, 293 DESKTOP, 294 } 295 296 /** Returns true if there are at least one TaskView has been added to the RecentsView. */ 297 fun hasTaskViews() = taskViews.any() 298 299 fun getTaskContainerById(taskId: Int) = 300 taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) } 301 302 private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) { 303 outRowRect.setEmpty() 304 firstView?.let { 305 it.getHitRect(TEMP_RECT) 306 outRowRect.union(TEMP_RECT) 307 } 308 lastView?.let { 309 it.getHitRect(TEMP_RECT) 310 outRowRect.union(TEMP_RECT) 311 } 312 } 313 314 private fun getRowRect(rowTaskViewIds: IntArray, outRowRect: Rect) { 315 if (rowTaskViewIds.isEmpty) { 316 outRowRect.setEmpty() 317 return 318 } 319 getRowRect( 320 recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(0)), 321 recentsView.getTaskViewFromTaskViewId(rowTaskViewIds.get(rowTaskViewIds.size() - 1)), 322 outRowRect, 323 ) 324 } 325 326 fun updateTaskViewDeadZoneRect( 327 outTaskViewRowRect: Rect, 328 outTopRowRect: Rect, 329 outBottomRowRect: Rect, 330 ) { 331 if (!getDeviceProfile().isTablet) { 332 getRowRect(getFirstTaskView(), getLastTaskView(), outTaskViewRowRect) 333 return 334 } 335 getRowRect(getFirstLargeTaskView(), getLastLargeTaskView(), outTaskViewRowRect) 336 getRowRect(getTopRowIdArray(), outTopRowRect) 337 getRowRect(getBottomRowIdArray(), outBottomRowRect) 338 339 // Expand large tile Rect to include space between top/bottom row. 340 val nonEmptyRowRect = 341 when { 342 !outTopRowRect.isEmpty -> outTopRowRect 343 !outBottomRowRect.isEmpty -> outBottomRowRect 344 else -> return 345 } 346 if (recentsView.isRtl) { 347 if (outTaskViewRowRect.left > nonEmptyRowRect.right) { 348 outTaskViewRowRect.left = nonEmptyRowRect.right 349 } 350 } else { 351 if (outTaskViewRowRect.right < nonEmptyRowRect.left) { 352 outTaskViewRowRect.right = nonEmptyRowRect.left 353 } 354 } 355 356 // Expand the shorter row Rect to include the space between the 2 rows. 357 if (outTopRowRect.isEmpty || outBottomRowRect.isEmpty) return 358 if (outTopRowRect.width() <= outBottomRowRect.width()) { 359 if (outTopRowRect.bottom < outBottomRowRect.top) { 360 outTopRowRect.bottom = outBottomRowRect.top 361 } 362 } else { 363 if (outBottomRowRect.top > outTopRowRect.bottom) { 364 outBottomRowRect.top = outTopRowRect.bottom 365 } 366 } 367 } 368 369 private fun getTaskMenu(): TaskMenuView? = 370 getTopOpenViewWithType(recentsView.mContainer, TYPE_TASK_MENU) as? TaskMenuView 371 372 fun shouldInterceptKeyEvent(event: KeyEvent): Boolean { 373 if (enableOverviewIconMenu()) { 374 return getTaskMenu()?.isOpen == true || event.keyCode == KeyEvent.KEYCODE_TAB 375 } 376 return false 377 } 378 379 fun updateChildTaskOrientations() { 380 with(recentsView) { 381 taskViews.forEach { it.setOrientationState(mOrientationState) } 382 if (enableOverviewIconMenu()) { 383 children.forEach { 384 it.layoutDirection = if (isRtl) LAYOUT_DIRECTION_LTR else LAYOUT_DIRECTION_RTL 385 } 386 } 387 388 // Return when it's not fake landscape 389 if (mOrientationState.isRecentsActivityRotationAllowed) return@with 390 391 // Rotation is supported on phone (details at b/254198019#comment4) 392 getTaskMenu()?.onRotationChanged() 393 } 394 } 395 396 fun updateCentralTask() { 397 val isTablet: Boolean = getDeviceProfile().isTablet 398 val actionsViewCanRelateToTaskView = !(isTablet && enableGridOnlyOverview()) 399 val focusedTaskView = recentsView.focusedTaskView 400 val currentPageTaskView = recentsView.currentPageTaskView 401 402 fun isInExpectedScrollPosition(taskView: TaskView?) = 403 taskView?.let { recentsView.isTaskInExpectedScrollPosition(it) } ?: false 404 405 val centralTaskIds: Set<Int> = 406 when { 407 !actionsViewCanRelateToTaskView -> emptySet() 408 isTablet && isInExpectedScrollPosition(focusedTaskView) -> 409 focusedTaskView!!.taskIdSet 410 isInExpectedScrollPosition(currentPageTaskView) -> currentPageTaskView!!.taskIdSet 411 else -> emptySet() 412 } 413 414 recentsView.mRecentsViewModel.updateCentralTaskIds(centralTaskIds) 415 } 416 417 var deskExplodeProgress: Float = 0f 418 set(value) { 419 field = value 420 taskViews.filterIsInstance<DesktopTaskView>().forEach { it.explodeProgress = field } 421 } 422 423 var selectedTaskView: TaskView? = null 424 set(newValue) { 425 val oldValue = field 426 field = newValue 427 if (oldValue != newValue) { 428 onSelectedTaskViewUpdated(oldValue, newValue) 429 } 430 } 431 432 private fun onSelectedTaskViewUpdated( 433 oldSelectedTaskView: TaskView?, 434 newSelectedTaskView: TaskView?, 435 ) { 436 if (!enableGridOnlyOverview()) return 437 with(recentsView) { 438 oldSelectedTaskView?.modalScale = 1f 439 oldSelectedTaskView?.modalPivot = null 440 441 if (newSelectedTaskView == null) return 442 443 val modalTaskBounds = mTempRect 444 getModalTaskSize(modalTaskBounds) 445 val selectedTaskBounds = getTaskBounds(newSelectedTaskView) 446 447 // Map bounds to selectedTaskView's coordinate system. 448 modalTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) 449 selectedTaskBounds.offset(-selectedTaskBounds.left, -selectedTaskBounds.top) 450 451 val modalScale = 452 min( 453 (modalTaskBounds.height().toFloat() / selectedTaskBounds.height()), 454 (modalTaskBounds.width().toFloat() / selectedTaskBounds.width()), 455 ) 456 val modalPivot = PointF() 457 getPivotsForScalingRectToRect(modalTaskBounds, selectedTaskBounds, modalPivot) 458 459 newSelectedTaskView.modalScale = modalScale 460 newSelectedTaskView.modalPivot = modalPivot 461 } 462 } 463 464 /** 465 * Creates a [DesktopTaskView] for the currently active desk on this display, which contains the 466 * tasks with the given [groupedTaskInfo]. 467 */ 468 fun createDesktopTaskViewForActiveDesk(groupedTaskInfo: GroupedTaskInfo): DesktopTaskView { 469 val desktopTaskView = 470 recentsView.getTaskViewFromPool(TaskViewType.DESKTOP) as DesktopTaskView 471 val tasks: List<Task> = groupedTaskInfo.taskInfoList.map { taskInfo -> Task.from(taskInfo) } 472 desktopTaskView.bind( 473 DesktopTask(groupedTaskInfo.deskId, groupedTaskInfo.deskDisplayId, tasks), 474 recentsView.mOrientationState, 475 recentsView.mTaskOverlayFactory, 476 ) 477 return desktopTaskView 478 } 479 480 companion object { 481 class RecentsViewFloatProperty( 482 private val utilsProperty: KMutableProperty1<RecentsViewUtils, Float> 483 ) : FloatProperty<RecentsView<*, *>>(utilsProperty.name) { 484 override fun get(recentsView: RecentsView<*, *>): Float = 485 utilsProperty.get(recentsView.mUtils) 486 487 override fun setValue(recentsView: RecentsView<*, *>, value: Float) { 488 utilsProperty.set(recentsView.mUtils, value) 489 } 490 } 491 492 @JvmField 493 val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress) 494 495 val TEMP_RECT = Rect() 496 } 497 } 498