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