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