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.server.wm.flicker.helpers
18 
19 import android.graphics.Rect
20 import android.tools.device.apphelpers.IStandardAppHelper
21 import android.tools.helpers.SYSTEMUI_PACKAGE
22 import android.tools.traces.parsers.WindowManagerStateHelper
23 import android.tools.traces.wm.WindowingMode
24 import androidx.test.uiautomator.By
25 import androidx.test.uiautomator.BySelector
26 import androidx.test.uiautomator.UiDevice
27 import androidx.test.uiautomator.UiObject2
28 import androidx.test.uiautomator.Until
29 
30 /**
31  * Wrapper class around App helper classes. This class adds functionality to the apps that the
32  * desktop apps would have.
33  */
34 open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) :
<lambda>null35     IStandardAppHelper by innerHelper {
36 
37     enum class Corners {
38         LEFT_TOP,
39         RIGHT_TOP,
40         LEFT_BOTTOM,
41         RIGHT_BOTTOM
42     }
43 
44     private val TIMEOUT_MS = 3_000L
45     private val CAPTION = "desktop_mode_caption"
46     private val CAPTION_HANDLE = "caption_handle"
47     private val MAXIMIZE_BUTTON = "maximize_window"
48     private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
49     private val CLOSE_BUTTON = "close_window"
50 
51     private val caption: BySelector
52         get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
53 
54     /** Wait for an app moved to desktop to finish its transition. */
55     private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) {
56         wmHelper
57             .StateSyncBuilder()
58             .withWindowSurfaceAppeared(innerHelper)
59             .withFreeformApp(innerHelper)
60             .withAppTransitionIdle()
61             .waitForAndVerify()
62     }
63 
64     /** Move an app to Desktop by dragging the app handle at the top. */
65     fun enterDesktopWithDrag(
66         wmHelper: WindowManagerStateHelper,
67         device: UiDevice,
68     ) {
69         innerHelper.launchViaIntent(wmHelper)
70         dragToDesktop(wmHelper, device)
71         waitForAppToMoveToDesktop(wmHelper)
72     }
73 
74     private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) {
75         val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
76         val startX = windowRect.centerX()
77 
78         // Start dragging a little under the top to prevent dragging the notification shade.
79         val startY = 10
80 
81         val displayRect =
82             wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
83                 ?: throw IllegalStateException("Default display is null")
84 
85         // The position we want to drag to
86         val endY = displayRect.centerY() / 2
87 
88         // drag the window to move to desktop
89         device.drag(startX, startY, startX, endY, 100)
90     }
91 
92     /** Click maximise button on the app header for the given app. */
93     fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
94         val caption = getCaptionForTheApp(wmHelper, device)
95         val maximizeButton =
96             caption
97                 ?.children
98                 ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) }
99                 ?.children
100                 ?.get(0)
101         maximizeButton?.click()
102         wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
103     }
104     /** Click close button on the app header for the given app. */
105     fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
106         val caption = getCaptionForTheApp(wmHelper, device)
107         val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) }
108         closeButton?.click()
109         wmHelper
110             .StateSyncBuilder()
111             .withAppTransitionIdle()
112             .withWindowSurfaceDisappeared(innerHelper)
113             .waitForAndVerify()
114     }
115 
116     private fun getCaptionForTheApp(
117         wmHelper: WindowManagerStateHelper,
118         device: UiDevice
119     ): UiObject2? {
120         if (
121             wmHelper.getWindow(innerHelper)?.windowingMode !=
122                 WindowingMode.WINDOWING_MODE_FREEFORM.value
123         )
124             error("expected a freeform window with caption but window is not in freeform mode")
125         val captions =
126             device.wait(Until.findObjects(caption), TIMEOUT_MS)
127                 ?: error("Unable to find view $caption\n")
128 
129         return captions.find {
130             wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds)
131         }
132     }
133 
134     /** Resize a desktop app from its corners. */
135     fun cornerResize(
136         wmHelper: WindowManagerStateHelper,
137         device: UiDevice,
138         corner: Corners,
139         horizontalChange: Int,
140         verticalChange: Int
141     ) {
142         val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
143         val (startX, startY) = getStartCoordinatesForCornerResize(windowRect, corner)
144 
145         // The position we want to drag to
146         val endY = startY + verticalChange
147         val endX = startX + horizontalChange
148 
149         // drag the specified corner of the window to the end coordinate.
150         device.drag(startX, startY, endX, endY, 100)
151         wmHelper
152             .StateSyncBuilder()
153             .withAppTransitionIdle()
154             .waitForAndVerify()
155     }
156 
157     private fun getStartCoordinatesForCornerResize(
158         windowRect: Rect,
159         corner: Corners
160     ): Pair<Int, Int> {
161         return when (corner) {
162             Corners.LEFT_TOP -> Pair(windowRect.left, windowRect.top)
163             Corners.RIGHT_TOP -> Pair(windowRect.right, windowRect.top)
164             Corners.LEFT_BOTTOM -> Pair(windowRect.left, windowRect.bottom)
165             Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom)
166         }
167     }
168 }
169