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