• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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