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 
17 package com.android.systemui.screenshot.data.model
18 
19 import android.content.ComponentName
20 import android.graphics.Rect
21 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREEFORM_FULL_SCREEN
22 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREEFORM_MAXIMIZED
23 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FREE_FORM
24 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.FULL_SCREEN
25 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Bounds.PIP
26 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.HORIZONTAL
27 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.Orientation.VERTICAL
28 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.emptyRootSplit
29 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.freeForm
30 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.fullScreen
31 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.launcher
32 import com.android.systemui.screenshot.data.model.DisplayContentScenarios.RootTasks.pictureInPicture
33 import com.android.systemui.screenshot.policy.ActivityType
34 import com.android.systemui.screenshot.policy.TestUserIds
35 import com.android.systemui.screenshot.policy.WindowingMode
36 import com.android.systemui.screenshot.policy.newChildTask
37 import com.android.systemui.screenshot.policy.newRootTaskInfo
38 
39 /** Tools for creating a [DisplayContentModel] for different usage scenarios. */
40 object DisplayContentScenarios {
41 
42     data class TaskSpec(val taskId: Int, val userId: Int, val name: String)
43 
44     val emptyDisplayContent = DisplayContentModel(0, SystemUiState(shadeExpanded = false), listOf())
45 
46     /** Home screen, with only the launcher visible */
launcherOnlynull47     fun launcherOnly(shadeExpanded: Boolean = false) =
48         DisplayContentModel(
49             displayId = 0,
50             systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
51             rootTasks = listOf(launcher(visible = true), emptyRootSplit),
52         )
53 
54     /** A Full screen activity for the personal (primary) user, with launcher behind it */
55     fun singleFullScreen(spec: TaskSpec, shadeExpanded: Boolean = false) =
56         DisplayContentModel(
57             displayId = 0,
58             systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
59             rootTasks =
60                 listOf(fullScreen(spec, visible = true), launcher(visible = false), emptyRootSplit),
61         )
62 
63     enum class Orientation {
64         HORIZONTAL,
65         VERTICAL,
66     }
67 
splitLeftnull68     internal fun Rect.splitLeft(margin: Int = 0) = Rect(left, top, centerX() - margin, bottom)
69 
70     internal fun Rect.splitRight(margin: Int = 0) = Rect(centerX() + margin, top, right, bottom)
71 
72     internal fun Rect.splitTop(margin: Int = 0) = Rect(left, top, right, centerY() - margin)
73 
74     internal fun Rect.splitBottom(margin: Int = 0) = Rect(left, centerY() + margin, right, bottom)
75 
76     fun splitScreenApps(
77         displayId: Int = 0,
78         parentBounds: Rect = FULL_SCREEN,
79         taskMargin: Int = 0,
80         orientation: Orientation = VERTICAL,
81         first: TaskSpec,
82         second: TaskSpec,
83         focusedTaskId: Int,
84         parentTaskId: Int = 2,
85         shadeExpanded: Boolean = false,
86     ): DisplayContentModel {
87 
88         val firstBounds =
89             when (orientation) {
90                 VERTICAL -> parentBounds.splitTop(taskMargin)
91                 HORIZONTAL -> parentBounds.splitLeft(taskMargin)
92             }
93         val secondBounds =
94             when (orientation) {
95                 VERTICAL -> parentBounds.splitBottom(taskMargin)
96                 HORIZONTAL -> parentBounds.splitRight(taskMargin)
97             }
98 
99         return DisplayContentModel(
100             displayId = displayId,
101             systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
102             rootTasks =
103                 listOf(
104                     newRootTaskInfo(
105                         taskId = parentTaskId,
106                         userId = TestUserIds.PERSONAL,
107                         bounds = parentBounds,
108                         topActivity =
109                             ComponentName.unflattenFromString(
110                                 if (first.taskId == focusedTaskId) first.name else second.name
111                             ),
112                     ) {
113                         listOf(
114                                 newChildTask(
115                                     taskId = first.taskId,
116                                     bounds = firstBounds,
117                                     userId = first.userId,
118                                     name = first.name,
119                                 ),
120                                 newChildTask(
121                                     taskId = second.taskId,
122                                     bounds = secondBounds,
123                                     userId = second.userId,
124                                     name = second.name,
125                                 ),
126                             )
127                             // Child tasks are ordered bottom-up in RootTaskInfo.
128                             // Sort 'focusedTaskId' last.
129                             // Boolean natural ordering: [false, true].
130                             .sortedBy { it.id == focusedTaskId }
131                     },
132                     launcher(visible = false),
133                 ),
134         )
135     }
136 
pictureInPictureAppnull137     fun pictureInPictureApp(
138         pip: TaskSpec,
139         fullScreen: TaskSpec? = null,
140         shadeExpanded: Boolean = false,
141     ): DisplayContentModel {
142         return DisplayContentModel(
143             displayId = 0,
144             systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
145             rootTasks =
146                 buildList {
147                     add(pictureInPicture(pip))
148                     fullScreen?.also { add(fullScreen(it, visible = true)) }
149                     add(launcher(visible = (fullScreen == null)))
150                     add(emptyRootSplit)
151                 },
152         )
153     }
154 
freeFormAppsnull155     fun freeFormApps(
156         vararg tasks: TaskSpec,
157         focusedTaskId: Int,
158         maximizedTaskId: Int = -1,
159         shadeExpanded: Boolean = false,
160     ): DisplayContentModel {
161         val freeFormTasks =
162             tasks
163                 .map {
164                     freeForm(
165                         task = it,
166                         bounds =
167                             if (it.taskId == maximizedTaskId) {
168                                 FREEFORM_MAXIMIZED
169                             } else {
170                                 FREE_FORM
171                             },
172                         maxBounds = FREEFORM_FULL_SCREEN,
173                     )
174                 }
175                 // Root tasks are ordered top-down in List<RootTaskInfo>.
176                 // Sort 'focusedTaskId' last (Boolean natural ordering: [false, true])
177                 .sortedBy { it.childTaskIds[0] != focusedTaskId }
178         return DisplayContentModel(
179             displayId = 0,
180             systemUiState = SystemUiState(shadeExpanded = shadeExpanded),
181             rootTasks = freeFormTasks + launcher(visible = true) + emptyRootSplit,
182         )
183     }
184 
185     /**
186      * All of these are arbitrary dimensions exposed for asserting equality on test data.
187      *
188      * They should not be updated nor compared with any real device usage, except to keep them
189      * somewhat sensible in terms of logical position (Re: PIP, SPLIT, etc).
190      */
191     object Bounds {
192         // "Phone" size
193         val FULL_SCREEN = Rect(0, 0, 1080, 2400)
194         val PIP = Rect(440, 1458, 1038, 1794)
195         val SPLIT_TOP = Rect(0, 0, 1080, 1187)
196         val SPLIT_BOTTOM = Rect(0, 1213, 1080, 2400)
197 
198         // "Tablet" size
199         val FREE_FORM = Rect(119, 332, 1000, 1367)
200         val FREEFORM_FULL_SCREEN = Rect(0, 0, 2560, 1600)
201         val FREEFORM_MAXIMIZED = Rect(0, 48, 2560, 1480)
202         val FREEFORM_SPLIT_LEFT = Rect(0, 0, 1270, 1600)
203         val FREEFORM_SPLIT_RIGHT = Rect(1290, 0, 2560, 1600)
204     }
205 
206     /** A collection of task names used in test scenarios */
207     object ActivityNames {
208         /** The main YouTube activity */
209         const val YOUTUBE =
210             "com.google.android.youtube/" +
211                 "com.google.android.youtube.app.honeycomb.Shell\$HomeActivity"
212 
213         /** The main Files Activity */
214         const val FILES =
215             "com.google.android.apps.nbu.files/" +
216                 "com.google.android.apps.nbu.files.home.HomeActivity"
217 
218         /** The YouTube picture-in-picture activity */
219         const val YOUTUBE_PIP =
220             "com.google.android.youtube/" +
221                 "com.google.android.apps.youtube.app.watchwhile.WatchWhileActivity"
222 
223         const val MESSAGES = "com.google.android.apps.messaging/.ui.ConversationListActivity"
224 
225         /** The NexusLauncher activity */
226         const val LAUNCHER =
227             "com.google.android.apps.nexuslauncher/" +
228                 "com.google.android.apps.nexuslauncher.NexusLauncherActivity"
229     }
230 
231     /**
232      * A set of predefined RootTaskInfo used in test scenarios, matching as closely as possible
233      * actual values returned by ActivityTaskManager
234      */
235     object RootTasks {
236         /** An empty RootTaskInfo with no child tasks. */
237         val emptyWithNoChildTasks =
238             newRootTaskInfo(
239                 taskId = 2,
240                 visible = true,
241                 running = true,
242                 numActivities = 0,
243                 bounds = FULL_SCREEN,
<lambda>null244             ) {
245                 emptyList()
246             }
247 
248         /**
249          * The empty RootTaskInfo that is always at the end of a list from ActivityTaskManager when
250          * no other visible activities are in split mode
251          */
252         val emptyRootSplit =
253             newRootTaskInfo(
254                 taskId = 2,
255                 visible = false,
256                 running = false,
257                 numActivities = 0,
258                 bounds = FULL_SCREEN,
259                 activityType = ActivityType.Undefined,
<lambda>null260             ) {
261                 listOf(
262                     newChildTask(taskId = 3, bounds = FULL_SCREEN, name = ""),
263                     newChildTask(taskId = 4, bounds = Rect(0, 2400, 1080, 3600), name = ""),
264                 )
265             }
266 
267         /** NexusLauncher on the default display. Usually below all other visible tasks */
launchernull268         fun launcher(visible: Boolean, bounds: Rect = FULL_SCREEN) =
269             newRootTaskInfo(
270                 taskId = 1,
271                 activityType = ActivityType.Home,
272                 visible = visible,
273                 bounds = FULL_SCREEN,
274                 topActivity = ComponentName.unflattenFromString(ActivityNames.LAUNCHER),
275                 topActivityType = ActivityType.Home,
276             ) {
277                 listOf(newChildTask(taskId = 1002, name = ActivityNames.LAUNCHER, bounds = bounds))
278             }
279 
280         /** A full screen Activity */
fullScreennull281         fun fullScreen(task: TaskSpec, visible: Boolean, bounds: Rect = FULL_SCREEN) =
282             newRootTaskInfo(
283                 taskId = task.taskId,
284                 userId = task.userId,
285                 visible = visible,
286                 bounds = bounds,
287                 topActivity = ComponentName.unflattenFromString(task.name),
288             ) {
289                 listOf(
290                     newChildTask(
291                         taskId = task.taskId,
292                         userId = task.userId,
293                         name = task.name,
294                         bounds = bounds,
295                     )
296                 )
297             }
298 
299         /** An activity in Picture-in-Picture mode */
pictureInPicturenull300         fun pictureInPicture(task: TaskSpec, bounds: Rect = PIP) =
301             newRootTaskInfo(
302                 taskId = task.taskId,
303                 userId = task.userId,
304                 windowingMode = WindowingMode.PictureInPicture,
305                 topActivity = ComponentName.unflattenFromString(task.name),
306             ) {
307                 listOf(
308                     newChildTask(
309                         taskId = task.taskId,
310                         userId = userId,
311                         name = task.name,
312                         bounds = bounds,
313                     )
314                 )
315             }
316 
317         /** An activity in FreeForm mode */
freeFormnull318         fun freeForm(task: TaskSpec, bounds: Rect = FREE_FORM, maxBounds: Rect = bounds) =
319             newRootTaskInfo(
320                 taskId = task.taskId,
321                 userId = task.userId,
322                 bounds = bounds,
323                 maxBounds = maxBounds,
324                 windowingMode = WindowingMode.Freeform,
325                 topActivity = ComponentName.unflattenFromString(task.name),
326             ) {
327                 listOf(
328                     newChildTask(
329                         taskId = task.taskId,
330                         userId = userId,
331                         name = task.name,
332                         bounds = bounds,
333                     )
334                 )
335             }
336     }
337 }
338