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.server.wm.flicker.helpers 18 19 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 20 import android.content.Context 21 import android.graphics.Insets 22 import android.graphics.Rect 23 import android.graphics.Region 24 import android.os.SystemClock 25 import android.platform.uiautomatorhelpers.DeviceHelpers 26 import android.tools.PlatformConsts 27 import android.tools.device.apphelpers.IStandardAppHelper 28 import android.tools.helpers.SYSTEMUI_PACKAGE 29 import android.tools.traces.parsers.WindowManagerStateHelper 30 import android.tools.traces.wm.WindowingMode 31 import android.view.KeyEvent.KEYCODE_DPAD_DOWN 32 import android.view.KeyEvent.KEYCODE_DPAD_UP 33 import android.view.KeyEvent.KEYCODE_EQUALS 34 import android.view.KeyEvent.KEYCODE_LEFT_BRACKET 35 import android.view.KeyEvent.KEYCODE_MINUS 36 import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET 37 import android.view.KeyEvent.META_CTRL_ON 38 import android.view.KeyEvent.META_META_ON 39 import android.view.WindowInsets 40 import android.view.WindowManager 41 import android.window.DesktopModeFlags 42 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 43 import androidx.test.uiautomator.By 44 import androidx.test.uiautomator.BySelector 45 import androidx.test.uiautomator.UiDevice 46 import androidx.test.uiautomator.UiObject2 47 import androidx.test.uiautomator.Until 48 import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod.TOUCH 49 import java.time.Duration 50 import kotlin.math.abs 51 52 /** 53 * Wrapper class around App helper classes. This class adds functionality to the apps that the 54 * desktop apps would have. 55 */ 56 open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : 57 IStandardAppHelper by innerHelper { 58 59 enum class Corners { 60 LEFT_TOP, 61 RIGHT_TOP, 62 LEFT_BOTTOM, 63 RIGHT_BOTTOM 64 } 65 66 enum class Edges { 67 LEFT, 68 RIGHT, 69 TOP, 70 BOTTOM 71 } 72 73 enum class AppProperty { 74 STANDARD, 75 NON_RESIZABLE 76 } 77 78 /** Wait for an app moved to desktop to finish its transition. */ 79 private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { 80 wmHelper 81 .StateSyncBuilder() 82 .withWindowSurfaceAppeared(innerHelper) 83 .withFreeformApp(innerHelper) 84 .withAppTransitionIdle() 85 .waitForAndVerify() 86 } 87 88 /** Launch an app and ensure it's moved to Desktop if it has not. */ 89 fun enterDesktopMode( 90 wmHelper: WindowManagerStateHelper, 91 device: UiDevice, 92 motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH), 93 shouldUseDragToDesktop: Boolean = false, 94 ) { 95 innerHelper.launchViaIntent(wmHelper) 96 if (isInDesktopWindowingMode(wmHelper)) return 97 if (shouldUseDragToDesktop) { 98 enterDesktopModeWithDrag( 99 wmHelper = wmHelper, 100 device = device, 101 motionEventHelper = motionEventHelper 102 ) 103 } else { 104 enterDesktopModeFromAppHandleMenu(wmHelper, device) 105 } 106 } 107 108 /** Move an app to Desktop by dragging the app handle at the top. */ 109 private fun enterDesktopModeWithDrag( 110 wmHelper: WindowManagerStateHelper, 111 device: UiDevice, 112 motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) 113 ) { 114 dragToDesktop( 115 wmHelper = wmHelper, 116 device = device, 117 motionEventHelper = motionEventHelper 118 ) 119 waitForAppToMoveToDesktop(wmHelper) 120 } 121 122 private fun dragToDesktop( 123 wmHelper: WindowManagerStateHelper, 124 device: UiDevice, 125 motionEventHelper: MotionEventHelper 126 ) { 127 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 128 val startX = windowRect.centerX() 129 130 // Start dragging a little under the top to prevent dragging the notification shade. 131 val startY = 10 132 133 val displayRect = getDisplayRect(wmHelper) 134 135 // The position we want to drag to 136 val endY = displayRect.centerY() / 2 137 138 // drag the window to move to desktop 139 if (motionEventHelper.inputMethod == TOUCH 140 && DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue) { 141 // Touch requires hold-to-drag. 142 motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100) 143 } else { 144 device.drag(startX, startY, startX, endY, 100) 145 } 146 } 147 148 private fun getMaximizeButtonForTheApp(caption: UiObject2?): UiObject2 { 149 return caption 150 ?.children 151 ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) } 152 ?.children 153 ?.get(0) 154 ?: error("Unable to find resource $MAXIMIZE_BUTTON_VIEW\n") 155 } 156 157 /** Maximize a given app to fill the stable bounds. */ 158 fun maximiseDesktopApp( 159 wmHelper: WindowManagerStateHelper, 160 device: UiDevice, 161 trigger: MaximizeDesktopAppTrigger = MaximizeDesktopAppTrigger.MAXIMIZE_MENU, 162 ) { 163 val caption = getCaptionForTheApp(wmHelper, device)!! 164 val maximizeButton = getMaximizeButtonForTheApp(caption) 165 166 when (trigger) { 167 MaximizeDesktopAppTrigger.MAXIMIZE_MENU -> maximizeButton.click() 168 MaximizeDesktopAppTrigger.DOUBLE_TAP_APP_HEADER -> { 169 caption.click() 170 Thread.sleep(50) 171 caption.click() 172 } 173 174 MaximizeDesktopAppTrigger.KEYBOARD_SHORTCUT -> { 175 val keyEventHelper = KeyEventHelper(getInstrumentation()) 176 keyEventHelper.press(KEYCODE_EQUALS, META_META_ON) 177 } 178 179 MaximizeDesktopAppTrigger.MAXIMIZE_BUTTON_IN_MENU -> { 180 maximizeButton.longClick() 181 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 182 val buttonResId = MAXIMIZE_BUTTON_IN_MENU 183 val maximizeMenu = getDesktopAppViewByRes(MAXIMIZE_MENU) 184 val maximizeButtonInMenu = 185 maximizeMenu 186 ?.wait( 187 Until.findObject(By.res(SYSTEMUI_PACKAGE, buttonResId)), 188 TIMEOUT.toMillis() 189 ) 190 ?: error("Unable to find object with resource id $buttonResId") 191 maximizeButtonInMenu.click() 192 } 193 } 194 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 195 } 196 197 private fun getMinimizeButtonForTheApp(caption: UiObject2?): UiObject2 { 198 return caption 199 ?.children 200 ?.find { it.resourceName.endsWith(MINIMIZE_BUTTON_VIEW) } 201 ?: error("Unable to find resource $MINIMIZE_BUTTON_VIEW\n") 202 } 203 204 fun minimizeDesktopApp( 205 wmHelper: WindowManagerStateHelper, 206 device: UiDevice, 207 isPip: Boolean = false, 208 usingKeyboard: Boolean = false, 209 ) { 210 if (usingKeyboard) { 211 val keyEventHelper = KeyEventHelper(getInstrumentation()) 212 keyEventHelper.press(KEYCODE_MINUS, META_META_ON) 213 } else { 214 val caption = getCaptionForTheApp(wmHelper, device) 215 val minimizeButton = getMinimizeButtonForTheApp(caption) 216 minimizeButton.click() 217 } 218 219 wmHelper 220 .StateSyncBuilder() 221 .withAppTransitionIdle() 222 .apply { 223 if (isPip) withPipShown() 224 else 225 withWindowSurfaceDisappeared(innerHelper) 226 .withActivityState(innerHelper, PlatformConsts.STATE_STOPPED) 227 } 228 .waitForAndVerify() 229 } 230 231 private fun getHeaderEmptyView(caption: UiObject2?): UiObject2 { 232 return caption 233 ?.children 234 ?.find { it.resourceName.endsWith(HEADER_EMPTY_VIEW) } 235 ?: error("Unable to find resource $HEADER_EMPTY_VIEW\n") 236 } 237 238 /** Click on an existing window's header to bring it to the front. */ 239 fun bringToFront(wmHelper: WindowManagerStateHelper, device: UiDevice) { 240 val caption = getCaptionForTheApp(wmHelper, device) 241 val openHeaderView = getHeaderEmptyView(caption) 242 openHeaderView.click() 243 wmHelper 244 .StateSyncBuilder() 245 .withAppTransitionIdle() 246 .withTopVisibleApp(innerHelper) 247 .waitForAndVerify() 248 } 249 250 /** Open maximize menu and click snap resize button on the app header for the given app. */ 251 fun snapResizeDesktopApp( 252 wmHelper: WindowManagerStateHelper, 253 device: UiDevice, 254 context: Context, 255 toLeft: Boolean 256 ) { 257 val caption = getCaptionForTheApp(wmHelper, device) 258 val maximizeButton = getMaximizeButtonForTheApp(caption) 259 maximizeButton.longClick() 260 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 261 262 val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON 263 val maximizeMenu = getDesktopAppViewByRes(MAXIMIZE_MENU) 264 265 val snapResizeButton = 266 maximizeMenu 267 ?.wait(Until.findObject(By.res(SYSTEMUI_PACKAGE, buttonResId)), TIMEOUT.toMillis()) 268 ?: error("Unable to find object with resource id $buttonResId") 269 snapResizeButton.click() 270 271 waitAndVerifySnapResize(wmHelper, context, toLeft) 272 } 273 274 fun snapResizeWithKeyboard( 275 wmHelper: WindowManagerStateHelper, 276 context: Context, 277 keyEventHelper: KeyEventHelper, 278 toLeft: Boolean, 279 ) { 280 val bracketKey = if (toLeft) KEYCODE_LEFT_BRACKET else KEYCODE_RIGHT_BRACKET 281 keyEventHelper.press(bracketKey, META_META_ON) 282 waitAndVerifySnapResize(wmHelper, context, toLeft) 283 } 284 285 private fun waitAndVerifySnapResize( 286 wmHelper: WindowManagerStateHelper, 287 context: Context, 288 toLeft: Boolean 289 ) { 290 val displayRect = getDisplayRect(wmHelper) 291 val insets = getWindowInsets( 292 context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars() 293 ) 294 displayRect.inset(insets) 295 296 val expectedWidth = displayRect.width() / 2 297 val expectedRect = Rect(displayRect).apply { 298 if (toLeft) right -= expectedWidth else left += expectedWidth 299 } 300 wmHelper.StateSyncBuilder() 301 .withAppTransitionIdle() 302 .withSurfaceMatchingVisibleRegion( 303 this, 304 Region(expectedRect), 305 { surfaceRegion, expectedRegion -> 306 areSnapWindowRegionsMatchingWithinThreshold( 307 surfaceRegion, expectedRegion, toLeft 308 ) 309 }) 310 .waitForAndVerify() 311 } 312 313 /** Close a desktop app by clicking the close button on the app header for the given app or by 314 * pressing back. */ 315 fun closeDesktopApp( 316 wmHelper: WindowManagerStateHelper, 317 device: UiDevice, 318 usingBackNavigation: Boolean = false 319 ) { 320 if (usingBackNavigation) { 321 device.pressBack() 322 } else { 323 val caption = getCaptionForTheApp(wmHelper, device) 324 val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) } 325 closeButton?.click() 326 } 327 wmHelper 328 .StateSyncBuilder() 329 .withAppTransitionIdle() 330 .withWindowSurfaceDisappeared(innerHelper) 331 .waitForAndVerify() 332 } 333 334 private fun getCaptionForTheApp( 335 wmHelper: WindowManagerStateHelper, 336 device: UiDevice 337 ): UiObject2? { 338 if ( 339 wmHelper.getWindow(innerHelper)?.windowingMode != 340 WindowingMode.WINDOWING_MODE_FREEFORM.value 341 ) error("expected a freeform window with caption but window is not in freeform mode") 342 val captions = 343 device.wait(Until.findObjects(caption), TIMEOUT.toMillis()) 344 ?: error("Unable to find view $caption\n") 345 346 return captions.find { 347 wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds) 348 } 349 } 350 351 /** Resize a desktop app from its corners. */ 352 fun cornerResize( 353 wmHelper: WindowManagerStateHelper, 354 device: UiDevice, 355 corner: Corners, 356 horizontalChange: Int, 357 verticalChange: Int 358 ) { 359 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 360 val (startX, startY) = getStartCoordinatesForCornerResize(windowRect, corner) 361 362 // The position we want to drag to 363 val endY = startY + verticalChange 364 val endX = startX + horizontalChange 365 366 // drag the specified corner of the window to the end coordinate. 367 dragWindow(startX, startY, endX, endY, wmHelper, device) 368 } 369 370 /** Resize a desktop app from its edges. */ 371 fun edgeResize( 372 wmHelper: WindowManagerStateHelper, 373 motionEvent: MotionEventHelper, 374 edge: Edges 375 ) { 376 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 377 val (startX, startY) = getStartCoordinatesForEdgeResize(windowRect, edge) 378 val verticalChange = when (edge) { 379 Edges.LEFT -> 0 380 Edges.RIGHT -> 0 381 Edges.TOP -> -100 382 Edges.BOTTOM -> 100 383 } 384 val horizontalChange = when (edge) { 385 Edges.LEFT -> -100 386 Edges.RIGHT -> 100 387 Edges.TOP -> 0 388 Edges.BOTTOM -> 0 389 } 390 391 // The position we want to drag to 392 val endY = startY + verticalChange 393 val endX = startX + horizontalChange 394 395 val downTime = SystemClock.uptimeMillis() 396 motionEvent.actionDown(startX, startY, time = downTime) 397 motionEvent.actionMove(startX, startY, endX, endY, /* steps= */100, downTime = downTime) 398 motionEvent.actionUp(endX, endY, downTime = downTime) 399 wmHelper 400 .StateSyncBuilder() 401 .withAppTransitionIdle() 402 .waitForAndVerify() 403 } 404 405 /** Drag a window from a source coordinate to a destination coordinate. */ 406 fun dragWindow( 407 startX: Int, startY: Int, 408 endX: Int, endY: Int, 409 wmHelper: WindowManagerStateHelper, 410 device: UiDevice 411 ) { 412 device.drag(startX, startY, endX, endY, /* steps= */ 100) 413 wmHelper 414 .StateSyncBuilder() 415 .withAppTransitionIdle() 416 .waitForAndVerify() 417 } 418 419 /** Drag a window to a snap resize region, found at the left and right edges of the screen. */ 420 fun dragToSnapResizeRegion( 421 wmHelper: WindowManagerStateHelper, 422 device: UiDevice, 423 isLeft: Boolean, 424 ) { 425 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 426 // Set start x-coordinate as center of app header. 427 val startX = windowRect.centerX() 428 val startY = windowRect.top 429 430 val displayRect = getDisplayRect(wmHelper) 431 432 val endX = if (isLeft) { 433 displayRect.left + SNAP_RESIZE_DRAG_INSET 434 } else { 435 displayRect.right - SNAP_RESIZE_DRAG_INSET 436 } 437 val endY = displayRect.centerY() / 2 438 439 // drag the window to snap resize 440 device.drag(startX, startY, endX, endY, /* steps= */ 100) 441 wmHelper 442 .StateSyncBuilder() 443 .withAppTransitionIdle() 444 .waitForAndVerify() 445 } 446 447 private fun getStartCoordinatesForCornerResize( 448 windowRect: Rect, 449 corner: Corners 450 ): Pair<Int, Int> { 451 return when (corner) { 452 Corners.LEFT_TOP -> Pair(windowRect.left, windowRect.top) 453 Corners.RIGHT_TOP -> Pair(windowRect.right, windowRect.top) 454 Corners.LEFT_BOTTOM -> Pair(windowRect.left, windowRect.bottom) 455 Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom) 456 } 457 } 458 459 private fun getStartCoordinatesForEdgeResize( 460 windowRect: Rect, 461 edge: Edges 462 ): Pair<Int, Int> { 463 return when (edge) { 464 Edges.LEFT -> Pair(windowRect.left, windowRect.bottom / 2) 465 Edges.RIGHT -> Pair(windowRect.right, windowRect.bottom / 2) 466 Edges.TOP -> Pair(windowRect.right / 2, windowRect.top) 467 Edges.BOTTOM -> Pair(windowRect.right / 2, windowRect.bottom) 468 } 469 } 470 471 /** Exit desktop mode by dragging the app handle to the top drag zone. */ 472 fun exitDesktopWithDragToTopDragZone( 473 wmHelper: WindowManagerStateHelper, 474 device: UiDevice, 475 ) { 476 dragAppWindowToTopDragZone(wmHelper, device) 477 waitForTransitionToFullscreen(wmHelper) 478 } 479 480 /** Maximize an app by dragging the app handle to the top drag zone. */ 481 fun maximizeAppWithDragToTopDragZone( 482 wmHelper: WindowManagerStateHelper, 483 device: UiDevice, 484 ) { 485 dragAppWindowToTopDragZone(wmHelper, device) 486 } 487 488 private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) { 489 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 490 val displayRect = getDisplayRect(wmHelper) 491 492 val startX = windowRect.centerX() 493 val endX = displayRect.centerX() 494 val startY = windowRect.top 495 val endY = 0 // top of the screen 496 497 // drag the app window to top drag zone 498 device.drag(startX, startY, endX, endY, 100) 499 } 500 501 fun enterDesktopModeViaKeyboard( 502 wmHelper: WindowManagerStateHelper, 503 ) { 504 val keyEventHelper = KeyEventHelper(getInstrumentation()) 505 keyEventHelper.press(KEYCODE_DPAD_DOWN, META_META_ON or META_CTRL_ON) 506 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 507 } 508 509 fun exitDesktopModeToFullScreenViaKeyboard( 510 wmHelper: WindowManagerStateHelper, 511 ) { 512 val keyEventHelper = KeyEventHelper(getInstrumentation()) 513 keyEventHelper.press(KEYCODE_DPAD_UP, META_META_ON or META_CTRL_ON) 514 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 515 } 516 517 fun enterDesktopModeFromAppHandleMenu( 518 wmHelper: WindowManagerStateHelper, 519 device: UiDevice 520 ) { 521 val windowRect = wmHelper.getWindowRegion(innerHelper).bounds 522 val startX = windowRect.centerX() 523 // Click a little under the top to prevent opening the notification shade. 524 val startY = 10 525 526 // Click on the app handle coordinates. 527 device.click(startX, startY) 528 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 529 530 val pill = getDesktopAppViewByRes(PILL_CONTAINER) 531 val desktopModeButton = 532 pill 533 ?.children 534 ?.find { it.resourceName.endsWith(DESKTOP_MODE_BUTTON) } 535 536 desktopModeButton?.click() 537 wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() 538 } 539 540 private fun getDesktopAppViewByRes(viewResId: String): UiObject2? = 541 DeviceHelpers.waitForObj(By.res(SYSTEMUI_PACKAGE, viewResId), TIMEOUT) 542 543 private fun getDisplayRect(wmHelper: WindowManagerStateHelper): Rect = 544 wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect 545 ?: throw IllegalStateException("Default display is null") 546 547 548 /** Wait for transition to full screen to finish. */ 549 private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) { 550 wmHelper 551 .StateSyncBuilder() 552 .withFullScreenApp(innerHelper) 553 .withAppTransitionIdle() 554 .waitForAndVerify() 555 } 556 557 private fun getWindowInsets(context: Context, typeMask: Int): Insets { 558 val wm: WindowManager = context.getSystemService(WindowManager::class.java) 559 ?: error("Unable to connect to WindowManager service") 560 val metricInsets = wm.currentWindowMetrics.windowInsets 561 return metricInsets.getInsetsIgnoringVisibility(typeMask) 562 } 563 564 // Requirement of DesktopWindowingMode is having a minimum of 1 app in WINDOWING_MODE_FREEFORM. 565 private fun isInDesktopWindowingMode(wmHelper: WindowManagerStateHelper) = 566 wmHelper.getWindow(innerHelper)?.windowingMode == WINDOWING_MODE_FREEFORM 567 568 private fun areSnapWindowRegionsMatchingWithinThreshold( 569 surfaceRegion: Region, expectedRegion: Region, toLeft: Boolean 570 ): Boolean { 571 val surfaceBounds = surfaceRegion.bounds 572 val expectedBounds = expectedRegion.bounds 573 // If snapped to left, right bounds will be cut off by the center divider. 574 // Else if snapped to right, the left bounds will be cut off. 575 val leftSideMatching: Boolean 576 val rightSideMatching: Boolean 577 if (toLeft) { 578 leftSideMatching = surfaceBounds.left == expectedBounds.left 579 rightSideMatching = 580 abs(surfaceBounds.right - expectedBounds.right) <= 581 surfaceBounds.right * SNAP_WINDOW_MAX_THRESHOLD_DIFF 582 } else { 583 leftSideMatching = 584 abs(surfaceBounds.left - expectedBounds.left) <= 585 surfaceBounds.left * SNAP_WINDOW_MAX_THRESHOLD_DIFF 586 rightSideMatching = surfaceBounds.right == expectedBounds.right 587 } 588 589 return surfaceBounds.top == expectedBounds.top && 590 surfaceBounds.bottom == expectedBounds.bottom && 591 leftSideMatching && 592 rightSideMatching 593 } 594 595 enum class MaximizeDesktopAppTrigger { 596 MAXIMIZE_MENU, 597 DOUBLE_TAP_APP_HEADER, 598 KEYBOARD_SHORTCUT, 599 MAXIMIZE_BUTTON_IN_MENU 600 } 601 602 private companion object { 603 val TIMEOUT: Duration = Duration.ofSeconds(3) 604 const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge 605 const val CAPTION: String = "desktop_mode_caption" 606 const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view" 607 const val MAXIMIZE_MENU: String = "maximize_menu" 608 const val CLOSE_BUTTON: String = "close_window" 609 const val PILL_CONTAINER: String = "windowing_pill" 610 const val DESKTOP_MODE_BUTTON: String = "desktop_button" 611 const val SNAP_LEFT_BUTTON: String = "maximize_menu_snap_left_button" 612 const val SNAP_RIGHT_BUTTON: String = "maximize_menu_snap_right_button" 613 const val MAXIMIZE_BUTTON_IN_MENU: String = "maximize_menu_size_toggle_button" 614 const val MINIMIZE_BUTTON_VIEW: String = "minimize_window" 615 const val HEADER_EMPTY_VIEW: String = "caption_handle" 616 val caption: BySelector 617 get() = By.res(SYSTEMUI_PACKAGE, CAPTION) 618 // In DesktopMode, window snap can be done with just a single window. In this case, the 619 // divider tiling between left and right window won't be shown, and hence its states are not 620 // obtainable in test. 621 // As the test should just focus on ensuring window goes to one side of the screen, an 622 // acceptable approach is to ensure snapped window still fills > 95% of either side of the 623 // screen. 624 const val SNAP_WINDOW_MAX_THRESHOLD_DIFF = 0.05 625 } 626 } 627