1 /*
<lambda>null2  * Copyright (C) 2020 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 android.tools.device.apphelpers
18 
19 import android.app.Instrumentation
20 import android.graphics.Rect
21 import android.graphics.Region
22 import android.tools.datatypes.coversMoreThan
23 import android.tools.helpers.FIND_TIMEOUT
24 import android.tools.helpers.GestureHelper
25 import android.tools.helpers.SYSTEMUI_PACKAGE
26 import android.tools.traces.ConditionsFactory
27 import android.tools.traces.component.IComponentNameMatcher
28 import android.tools.traces.parsers.WindowManagerStateHelper
29 import android.util.Log
30 import androidx.test.uiautomator.By
31 import androidx.test.uiautomator.Until
32 
33 abstract class BasePipAppHelper(
34     instrumentation: Instrumentation,
35     appName: String,
36     componentMatcher: IComponentNameMatcher,
37 ) : StandardAppHelper(instrumentation, appName, componentMatcher), PipApp {
38     private val gestureHelper: GestureHelper = GestureHelper(instrumentation)
39 
40     open fun clickObject(resId: String) {
41         val selector = By.res(packageName, resId)
42         val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object")
43 
44         obj.click()
45     }
46 
47     override fun waitForPip(wmHelper: WindowManagerStateHelper) {
48         wmHelper
49             .StateSyncBuilder()
50             .withWindowSurfaceAppeared(this)
51             .withPipShown()
52             .waitForAndVerify()
53     }
54 
55     /** Drags the PIP window to the provided final coordinates without releasing the pointer. */
56     override fun dragPipWindowAwayFromEdgeWithoutRelease(
57         wmHelper: WindowManagerStateHelper,
58         steps: Int,
59     ) {
60         val initWindowRect = Rect(getWindowRect(wmHelper))
61 
62         // initial pointer at the center of the window
63         val initialCoord =
64             GestureHelper.Tuple(
65                 initWindowRect.centerX().toFloat(),
66                 initWindowRect.centerY().toFloat(),
67             )
68 
69         // the offset to the right (or left) of the window center to drag the window to
70         val offset = 50
71 
72         // the actual final x coordinate with the offset included;
73         // if the pip window is closer to the right edge of the display the offset is negative
74         // otherwise the offset is positive
75         val endX =
76             initWindowRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) -1 else 1)
77         val finalCoord = GestureHelper.Tuple(endX.toFloat(), initWindowRect.centerY().toFloat())
78 
79         // drag to the final coordinate
80         gestureHelper.dragWithoutRelease(initialCoord, finalCoord, steps)
81     }
82 
83     /**
84      * Releases the primary pointer.
85      *
86      * Injects the release of the primary pointer if the primary pointer info was cached after
87      * another gesture was injected without pointer release.
88      */
89     override fun releasePipAfterDragging() {
90         gestureHelper.releasePrimaryPointer()
91     }
92 
93     /**
94      * Drags the PIP window away from the screen edge while not crossing the display center.
95      *
96      * @throws IllegalStateException if default display bounds are not available
97      */
98     override fun dragPipWindowAwayFromEdge(wmHelper: WindowManagerStateHelper, steps: Int) {
99         val initWindowRect = Rect(getWindowRect(wmHelper))
100 
101         // initial pointer at the center of the window
102         val startX = initWindowRect.centerX()
103         val y = initWindowRect.centerY()
104 
105         val displayRect =
106             wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
107                 ?: throw IllegalStateException("Default display is null")
108 
109         // the offset to the right (or left) of the display center to drag the window to
110         val offset = 20
111 
112         // the actual final x coordinate with the offset included;
113         // if the pip window is closer to the right edge of the display the offset is positive
114         // otherwise the offset is negative
115         val endX = displayRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) 1 else -1)
116 
117         // drag the window to the left but not beyond the center of the display
118         uiDevice.drag(startX, y, endX, y, steps)
119     }
120 
121     /**
122      * Returns true if PIP window is closer to the right edge of the display than left.
123      *
124      * @throws IllegalStateException if default display bounds are not available
125      */
126     override fun isCloserToRightEdge(wmHelper: WindowManagerStateHelper): Boolean {
127         val windowRect = getWindowRect(wmHelper)
128 
129         val displayRect =
130             wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
131                 ?: throw IllegalStateException("Default display is null")
132 
133         return windowRect.centerX() > displayRect.centerX()
134     }
135 
136     /**
137      * Expands the PIP window by using the pinch out gesture.
138      *
139      * @param percent The percentage by which to increase the pip window size.
140      * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
141      */
142     override fun pinchOpenPipWindow(
143         wmHelper: WindowManagerStateHelper,
144         percent: Float,
145         steps: Int,
146     ) {
147         // the percentage must be between 0.0f and 1.0f
148         if (percent <= 0.0f || percent > 1.0f) {
149             throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
150         }
151 
152         val windowRect = getWindowRect(wmHelper)
153 
154         // first pointer's initial x coordinate is halfway between the left edge and the center
155         val initLeftX = (windowRect.centerX() - windowRect.width() / 4).toFloat()
156         // second pointer's initial x coordinate is halfway between the right edge and the center
157         val initRightX = (windowRect.centerX() + windowRect.width() / 4).toFloat()
158 
159         // horizontal distance the window should increase by
160         val distIncrease = windowRect.width() * percent
161 
162         // final x-coordinates
163         val finalLeftX = initLeftX - (distIncrease / 2)
164         val finalRightX = initRightX + (distIncrease / 2)
165 
166         // y-coordinate is the same throughout this animation
167         val yCoord = windowRect.centerY().toFloat()
168 
169         var adjustedSteps = MIN_STEPS_TO_ANIMATE
170 
171         // if distance per step is at least 1, then we can use the number of steps requested
172         if (distIncrease.toInt() / (steps * 2) >= 1) {
173             adjustedSteps = steps
174         }
175 
176         // if the distance per step is less than 1, carry out the animation in two steps
177         gestureHelper.pinch(
178             GestureHelper.Tuple(initLeftX, yCoord),
179             GestureHelper.Tuple(initRightX, yCoord),
180             GestureHelper.Tuple(finalLeftX, yCoord),
181             GestureHelper.Tuple(finalRightX, yCoord),
182             adjustedSteps,
183         )
184 
185         waitForPipWindowToExpandFrom(wmHelper, Region(windowRect))
186     }
187 
188     /**
189      * Minimizes the PIP window by using the pinch in gesture.
190      *
191      * @param percent The percentage by which to decrease the pip window size.
192      * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f
193      */
194     override fun pinchInPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) {
195         // the percentage must be between 0.0f and 1.0f
196         if (percent <= 0.0f || percent > 1.0f) {
197             throw IllegalArgumentException("Percent must be between 0.0f and 1.0f")
198         }
199 
200         val windowRect = getWindowRect(wmHelper)
201 
202         // first pointer's initial x coordinate is halfway between the left edge and the center
203         val initLeftX = (windowRect.centerX() - windowRect.width() / 4).toFloat()
204         // second pointer's initial x coordinate is halfway between the right edge and the center
205         val initRightX = (windowRect.centerX() + windowRect.width() / 4).toFloat()
206 
207         // decrease by the distance specified through the percentage
208         val distDecrease = windowRect.width() * percent
209 
210         // get the final x-coordinates and make sure they are not passing the center of the window
211         val finalLeftX = Math.min(initLeftX + (distDecrease / 2), windowRect.centerX().toFloat())
212         val finalRightX = Math.max(initRightX - (distDecrease / 2), windowRect.centerX().toFloat())
213 
214         // y-coordinate is the same throughout this animation
215         val yCoord = windowRect.centerY().toFloat()
216 
217         var adjustedSteps = MIN_STEPS_TO_ANIMATE
218 
219         // if distance per step is at least 1, then we can use the number of steps requested
220         if (distDecrease.toInt() / (steps * 2) >= 1) {
221             adjustedSteps = steps
222         }
223 
224         // if the distance per step is less than 1, carry out the animation in two steps
225         gestureHelper.pinch(
226             GestureHelper.Tuple(initLeftX, yCoord),
227             GestureHelper.Tuple(initRightX, yCoord),
228             GestureHelper.Tuple(finalLeftX, yCoord),
229             GestureHelper.Tuple(finalRightX, yCoord),
230             adjustedSteps,
231         )
232 
233         waitForPipWindowToMinimizeFrom(wmHelper, Region(windowRect))
234     }
235 
236     /** Returns the pip window bounds. */
237     override fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect {
238         val windowRegion = wmHelper.getWindowRegion(this)
239         require(!windowRegion.isEmpty) { "Unable to find a PIP window in the current state" }
240         return windowRegion.bounds
241     }
242 
243     /** Taps the pip window and dismisses it by clicking on the X button. */
244     open fun closePipWindow(wmHelper: WindowManagerStateHelper) {
245         val windowRect = getWindowRect(wmHelper)
246         uiDevice.click(windowRect.centerX(), windowRect.centerY())
247         // search and interact with the dismiss button
248         val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
249         uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
250         val dismissPipObject =
251             uiDevice.findObject(dismissSelector) ?: error("PIP window dismiss button not found")
252         val dismissButtonBounds = dismissPipObject.visibleBounds
253         uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY())
254 
255         // Wait for animation to complete.
256         wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify()
257     }
258 
259     open fun tapPipToShowMenu(wmHelper: WindowManagerStateHelper) {
260         val windowRect = getWindowRect(wmHelper)
261         uiDevice.click(windowRect.centerX(), windowRect.centerY())
262         // search and interact with the dismiss button
263         val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss")
264         uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT)
265     }
266 
267     /** Close the pip window by pressing the expand button */
268     fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) {
269         val windowRect = getWindowRect(wmHelper)
270         uiDevice.click(windowRect.centerX(), windowRect.centerY())
271         // search and interact with the expand button
272         val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button")
273         uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT)
274         val expandPipObject =
275             uiDevice.findObject(expandSelector) ?: error("PIP window expand button not found")
276         val expandButtonBounds = expandPipObject.visibleBounds
277         uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY())
278         wmHelper.StateSyncBuilder().withPipGone().withFullScreenApp(this).waitForAndVerify()
279     }
280 
281     /** Double click on the PIP window to expand it */
282     override fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) {
283         val windowRect = getWindowRect(wmHelper)
284         Log.d(TAG, "First click")
285         uiDevice.click(windowRect.centerX(), windowRect.centerY())
286         Log.d(TAG, "Second click")
287         uiDevice.click(windowRect.centerX(), windowRect.centerY())
288         Log.d(TAG, "Wait for app transition to end")
289         wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
290         waitForPipWindowToExpandFrom(wmHelper, Region(windowRect))
291     }
292 
293     private fun waitForPipWindowToExpandFrom(
294         wmHelper: WindowManagerStateHelper,
295         windowRect: Region,
296     ) {
297         wmHelper
298             .StateSyncBuilder()
299             .add("pipWindowExpanded") {
300                 val pipAppWindow =
301                     it.wmState.visibleWindows.firstOrNull { window ->
302                         this.windowMatchesAnyOf(window)
303                     } ?: return@add false
304                 val pipRegion = pipAppWindow.frameRegion
305                 return@add pipRegion.coversMoreThan(windowRect)
306             }
307             .waitForAndVerify()
308     }
309 
310     private fun waitForPipWindowToMinimizeFrom(
311         wmHelper: WindowManagerStateHelper,
312         windowRect: Region,
313     ) {
314         wmHelper
315             .StateSyncBuilder()
316             .add("pipWindowMinimized") {
317                 val pipAppWindow =
318                     it.wmState.visibleWindows.firstOrNull { window ->
319                         this.windowMatchesAnyOf(window)
320                     }
321                 Log.d(TAG, "window $pipAppWindow")
322                 if (pipAppWindow == null) return@add false
323                 val pipRegion = pipAppWindow.frameRegion
324                 Log.d(
325                     TAG,
326                     "region " + pipRegion + " covers " + windowRect.coversMoreThan(pipRegion),
327                 )
328                 return@add windowRect.coversMoreThan(pipRegion)
329             }
330             .waitForAndVerify()
331     }
332 
333     /**
334      * Waits until the PIP window snaps horizontally to the provided bounds.
335      *
336      * @param finalBounds the bounds to wait for PIP window to snap to
337      */
338     override fun waitForPipToSnapTo(wmHelper: WindowManagerStateHelper, finalBounds: Rect) {
339         wmHelper
340             .StateSyncBuilder()
341             .add("pipWindowSnapped") {
342                 val pipAppWindow =
343                     it.wmState.visibleWindows.firstOrNull { window ->
344                         this.windowMatchesAnyOf(window)
345                     } ?: return@add false
346                 val pipRegionBounds = pipAppWindow.frameRegion.bounds
347                 return@add pipRegionBounds.left == finalBounds.left &&
348                     pipRegionBounds.right == finalBounds.right
349             }
350             .add(ConditionsFactory.isWMStateComplete())
351             .waitForAndVerify()
352     }
353 
354     companion object {
355         private const val TAG = "BasePipAppHelper"
356         // minimum number of steps to take, when animating gestures, needs to be 2
357         // so that there is at least a single intermediate layer that flicker tests can check
358         private const val MIN_STEPS_TO_ANIMATE = 2
359     }
360 }
361