1 /*
2  * 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 package com.android.quickstep.views
17 
18 import android.app.ActivityTaskManager.INVALID_TASK_ID
19 import android.content.Context
20 import android.graphics.PointF
21 import android.util.AttributeSet
22 import android.util.Log
23 import android.view.View
24 import com.android.internal.jank.Cuj
25 import com.android.launcher3.Flags.enableOverviewIconMenu
26 import com.android.launcher3.R
27 import com.android.launcher3.Utilities
28 import com.android.launcher3.config.FeatureFlags
29 import com.android.launcher3.util.RunnableList
30 import com.android.launcher3.util.SplitConfigurationOptions
31 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
32 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
33 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
34 import com.android.quickstep.TaskOverlayFactory
35 import com.android.quickstep.util.RecentsOrientedState
36 import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell
37 import com.android.quickstep.util.SplitSelectStateController
38 import com.android.systemui.shared.recents.model.Task
39 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
40 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
41 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition
42 
43 /**
44  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
45  *
46  * That's right. If you call within the next 5 minutes we'll go ahead and double your order and send
47  * you !! TWO !! Tasks along with their TaskThumbnailViews complimentary. On. The. House. And not
48  * only that, we'll even clean up your thumbnail request if you don't like it. All the benefits of
49  * one TaskView, except DOUBLED!
50  *
51  * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
52  */
53 class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
54     TaskView(context, attrs) {
55     // TODO(b/336612373): Support new TTV for GroupedTaskView
56     var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null
57         private set
58 
59     @get:PersistentSnapPosition
60     val snapPosition: Int
61         /** Returns the [PersistentSnapPosition] of this pair of tasks. */
62         get() = splitBoundsConfig?.snapPosition ?: STAGE_POSITION_UNDEFINED
63 
onMeasurenull64     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
65         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
66         val widthSize = MeasureSpec.getSize(widthMeasureSpec)
67         val heightSize = MeasureSpec.getSize(heightMeasureSpec)
68         setMeasuredDimension(widthSize, heightSize)
69         val splitBoundsConfig = splitBoundsConfig ?: return
70         val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
71         if (initSplitTaskId == INVALID_TASK_ID) {
72             pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
73                 taskContainers[0].thumbnailViewDeprecated,
74                 taskContainers[1].thumbnailViewDeprecated,
75                 widthSize,
76                 heightSize,
77                 splitBoundsConfig,
78                 container.deviceProfile,
79                 layoutDirection == LAYOUT_DIRECTION_RTL
80             )
81             // Should we be having a separate translation step apart from the measuring above?
82             // The following only applies to large screen for now, but for future reference
83             // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary
84             // translation directions
85             taskContainers[0]
86                 .thumbnailViewDeprecated
87                 .applySplitSelectTranslateX(taskContainers[0].thumbnailViewDeprecated.translationX)
88             taskContainers[0]
89                 .thumbnailViewDeprecated
90                 .applySplitSelectTranslateY(taskContainers[0].thumbnailViewDeprecated.translationY)
91             taskContainers[1]
92                 .thumbnailViewDeprecated
93                 .applySplitSelectTranslateX(taskContainers[1].thumbnailViewDeprecated.translationX)
94             taskContainers[1]
95                 .thumbnailViewDeprecated
96                 .applySplitSelectTranslateY(taskContainers[1].thumbnailViewDeprecated.translationY)
97         } else {
98             // Currently being split with this taskView, let the non-split selected thumbnail
99             // take up full thumbnail area
100             taskContainers
101                 .firstOrNull { it.task.key.id != initSplitTaskId }
102                 ?.thumbnailViewDeprecated
103                 ?.measure(
104                     widthMeasureSpec,
105                     MeasureSpec.makeMeasureSpec(
106                         heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx,
107                         MeasureSpec.EXACTLY
108                     )
109                 )
110         }
111         if (!enableOverviewIconMenu()) {
112             updateIconPlacement()
113         }
114     }
115 
onRecyclenull116     override fun onRecycle() {
117         super.onRecycle()
118         splitBoundsConfig = null
119     }
120 
bindnull121     fun bind(
122         primaryTask: Task,
123         secondaryTask: Task,
124         orientedState: RecentsOrientedState,
125         taskOverlayFactory: TaskOverlayFactory,
126         splitBoundsConfig: SplitConfigurationOptions.SplitBounds?,
127     ) {
128         cancelPendingLoadTasks()
129         taskContainers =
130             listOf(
131                 createTaskContainer(
132                     primaryTask,
133                     R.id.snapshot,
134                     R.id.icon,
135                     R.id.show_windows,
136                     STAGE_POSITION_TOP_OR_LEFT,
137                     taskOverlayFactory
138                 ),
139                 createTaskContainer(
140                     secondaryTask,
141                     R.id.bottomright_snapshot,
142                     R.id.bottomRight_icon,
143                     R.id.show_windows_right,
144                     STAGE_POSITION_BOTTOM_OR_RIGHT,
145                     taskOverlayFactory
146                 )
147             )
148         this.splitBoundsConfig =
149             splitBoundsConfig?.also {
150                 taskContainers[0]
151                     .thumbnailViewDeprecated
152                     .previewPositionHelper
153                     .setSplitBounds(
154                         convertLauncherSplitBoundsToShell(it),
155                         PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT
156                     )
157                 taskContainers[1]
158                     .thumbnailViewDeprecated
159                     .previewPositionHelper
160                     .setSplitBounds(
161                         convertLauncherSplitBoundsToShell(it),
162                         PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT
163                     )
164             }
165         taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
166         setOrientationState(orientedState)
167     }
168 
setOrientationStatenull169     override fun setOrientationState(orientationState: RecentsOrientedState) {
170         if (enableOverviewIconMenu()) {
171             splitBoundsConfig?.let {
172                 val groupedTaskViewSizes =
173                     orientationState.orientationHandler.getGroupedTaskViewSizes(
174                         container.deviceProfile,
175                         it,
176                         layoutParams.width,
177                         layoutParams.height
178                     )
179                 val iconViewMarginStart =
180                     resources.getDimensionPixelSize(
181                         R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin
182                     )
183                 val iconViewBackgroundMarginStart =
184                     resources.getDimensionPixelSize(
185                         R.dimen.task_thumbnail_icon_menu_background_margin_top_start
186                     )
187                 val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
188                 // setMaxWidth() needs to be called before mIconView.setIconOrientation which is
189                 // called in the super below.
190                 (taskContainers[0].iconView as IconAppChipView).setMaxWidth(
191                     groupedTaskViewSizes.first.x - iconMargins
192                 )
193                 (taskContainers[1].iconView as IconAppChipView).setMaxWidth(
194                     groupedTaskViewSizes.second.x - iconMargins
195                 )
196             }
197         }
198         super.setOrientationState(orientationState)
199         updateIconPlacement()
200     }
201 
updateIconPlacementnull202     private fun updateIconPlacement() {
203         val splitBoundsConfig = splitBoundsConfig ?: return
204         val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx
205         val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
206         if (enableOverviewIconMenu()) {
207             val groupedTaskViewSizes =
208                 pagedOrientationHandler.getGroupedTaskViewSizes(
209                     container.deviceProfile,
210                     splitBoundsConfig,
211                     layoutParams.width,
212                     layoutParams.height
213                 )
214             pagedOrientationHandler.setSplitIconParams(
215                 taskContainers[0].iconView.asView(),
216                 taskContainers[1].iconView.asView(),
217                 taskIconHeight,
218                 groupedTaskViewSizes.first.x,
219                 groupedTaskViewSizes.first.y,
220                 layoutParams.height,
221                 layoutParams.width,
222                 isRtl,
223                 container.deviceProfile,
224                 splitBoundsConfig
225             )
226         } else {
227             pagedOrientationHandler.setSplitIconParams(
228                 taskContainers[0].iconView.asView(),
229                 taskContainers[1].iconView.asView(),
230                 taskIconHeight,
231                 taskContainers[0].thumbnailViewDeprecated.measuredWidth,
232                 taskContainers[0].thumbnailViewDeprecated.measuredHeight,
233                 measuredHeight,
234                 measuredWidth,
235                 isRtl,
236                 container.deviceProfile,
237                 splitBoundsConfig
238             )
239         }
240     }
241 
updateSplitBoundsConfignull242     fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
243         splitBoundsConfig = splitBounds
244         taskContainers.forEach {
245             it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
246             it.digitalWellBeingToast?.initialize(it.task)
247         }
248         invalidate()
249     }
250 
launchTaskAnimatednull251     override fun launchTaskAnimated(): RunnableList? {
252         if (taskContainers.isEmpty()) {
253             Log.d(TAG, "launchTaskAnimated - task is not bound")
254             return null
255         }
256         val recentsView = recentsView ?: return null
257         val endCallback = RunnableList()
258         // Callbacks run from remote animation when recents animation not currently running
259         InteractionJankMonitorWrapper.begin(
260             this,
261             Cuj.CUJ_SPLIT_SCREEN_ENTER,
262             "Enter form GroupedTaskView"
263         )
264         launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) {
265             endCallback.executeAllAndDestroy()
266             InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER)
267         }
268 
269         // Callbacks get run from recentsView for case when recents animation already running
270         recentsView.addSideTaskLaunchCallback(endCallback)
271         return endCallback
272     }
273 
launchTasknull274     override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) {
275         launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/)
276     }
277 
278     /**
279      * @param launchingExistingTaskView [SplitSelectStateController.launchExistingSplitPair] uses
280      *   existence of GroupedTaskView as control flow of how to animate in the incoming task. If
281      *   we're launching from overview (from overview thumbnails) then pass in `true`, otherwise
282      *   pass in `false` for case like quickswitching from home to task
283      */
launchTaskInternalnull284     private fun launchTaskInternal(
285         isQuickSwitch: Boolean,
286         launchingExistingTaskView: Boolean,
287         callback: (launched: Boolean) -> Unit
288     ) {
289         recentsView?.let {
290             it.splitSelectController.launchExistingSplitPair(
291                 if (launchingExistingTaskView) this else null,
292                 taskContainers[0].task.key.id,
293                 taskContainers[1].task.key.id,
294                 STAGE_POSITION_TOP_OR_LEFT,
295                 callback,
296                 isQuickSwitch,
297                 snapPosition
298             )
299             Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}")
300         }
301     }
302 
303     /**
304      * Returns taskId that split selection was initiated with, [INVALID_TASK_ID] if no tasks in this
305      * TaskView are part of split selection
306      */
getThisTaskCurrentlyInSplitSelectionnull307     private fun getThisTaskCurrentlyInSplitSelection(): Int {
308         val initialTaskId = recentsView?.splitSelectController?.initialTaskId
309         return if (initialTaskId != null && containsTaskId(initialTaskId)) initialTaskId
310         else INVALID_TASK_ID
311     }
312 
getLastSelectedChildTaskIndexnull313     override fun getLastSelectedChildTaskIndex(): Int {
314         if (recentsView?.splitSelectController?.isDismissingFromSplitPair == true) {
315             // return the container index of the task that wasn't initially selected to split
316             // with because that is the only remaining app that can be selected. The coordinate
317             // checks below aren't reliable since both of those views may be gone/transformed
318             val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
319             if (initSplitTaskId != INVALID_TASK_ID) {
320                 return if (initSplitTaskId == taskContainers[0].task.key.id) 1 else 0
321             }
322         }
323 
324         // Check which of the two apps was selected
325         if (
326             taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) ||
327                 taskContainers[1].thumbnailViewDeprecated.containsPoint(lastTouchDownPosition)
328         ) {
329             return 1
330         }
331         return super.getLastSelectedChildTaskIndex()
332     }
333 
containsPointnull334     private fun View.containsPoint(position: PointF): Boolean {
335         val localPos = floatArrayOf(position.x, position.y)
336         Utilities.mapCoordInSelfToDescendant(this, this@GroupedTaskView, localPos)
337         return Utilities.pointInView(this, localPos[0], localPos[1], 0f /* slop */)
338     }
339 
setOverlayEnablednull340     override fun setOverlayEnabled(overlayEnabled: Boolean) {
341         if (FeatureFlags.enableAppPairs()) {
342             super.setOverlayEnabled(overlayEnabled)
343         } else {
344             // Intentional no-op to prevent setting smart actions overlay on thumbnails
345         }
346     }
347 
348     companion object {
349         private const val TAG = "GroupedTaskView"
350     }
351 }
352