1 /* <lambda>null2 * Copyright (C) 2023 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.wm.shell.flicker.utils 18 19 import android.app.Instrumentation 20 import android.graphics.Point 21 import android.os.SystemClock 22 import android.tools.Rotation 23 import android.tools.device.apphelpers.IStandardAppHelper 24 import android.tools.device.apphelpers.StandardAppHelper 25 import android.tools.flicker.rules.ChangeDisplayOrientationRule 26 import android.tools.traces.component.ComponentNameMatcher 27 import android.tools.traces.component.IComponentMatcher 28 import android.tools.traces.component.IComponentNameMatcher 29 import android.tools.traces.parsers.WindowManagerStateHelper 30 import android.tools.traces.parsers.toFlickerComponent 31 import android.view.InputDevice 32 import android.view.MotionEvent 33 import android.view.ViewConfiguration 34 import androidx.test.uiautomator.By 35 import androidx.test.uiautomator.BySelector 36 import androidx.test.uiautomator.UiDevice 37 import androidx.test.uiautomator.UiObject2 38 import androidx.test.uiautomator.Until 39 import com.android.launcher3.tapl.LauncherInstrumentation 40 import com.android.server.wm.flicker.helpers.ImeAppHelper 41 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper 42 import com.android.server.wm.flicker.helpers.NotificationAppHelper 43 import com.android.server.wm.flicker.helpers.SimpleAppHelper 44 import com.android.server.wm.flicker.testapp.ActivityOptions 45 import com.android.server.wm.flicker.testapp.ActivityOptions.SplitScreen.Primary 46 import org.junit.Assert.assertNotNull 47 48 object SplitScreenUtils { 49 private const val TIMEOUT_MS = 3_000L 50 private const val DRAG_DURATION_MS = 1_000L 51 private const val NOTIFICATION_SCROLLER = "notification_stack_scroller" 52 private const val DIVIDER_BAR = "docked_divider_handle" 53 private const val OVERVIEW_SNAPSHOT = "snapshot" 54 private const val GESTURE_STEP_MS = 16L 55 private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L 56 private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#") 57 58 private val notificationScrollerSelector: BySelector 59 get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER) 60 private val notificationContentSelector: BySelector 61 get() = By.text("Flicker Test Notification") 62 private val dividerBarSelector: BySelector 63 get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR) 64 private val overviewSnapshotSelector: BySelector 65 get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT) 66 67 fun getPrimary(instrumentation: Instrumentation): StandardAppHelper = 68 SimpleAppHelper( 69 instrumentation, 70 ActivityOptions.SplitScreen.Primary.LABEL, 71 ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent() 72 ) 73 74 fun getSecondary(instrumentation: Instrumentation): StandardAppHelper = 75 SimpleAppHelper( 76 instrumentation, 77 ActivityOptions.SplitScreen.Secondary.LABEL, 78 ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent() 79 ) 80 81 fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper = 82 NonResizeableAppHelper(instrumentation) 83 84 fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper = 85 NotificationAppHelper(instrumentation) 86 87 fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation) 88 89 fun waitForSplitComplete( 90 wmHelper: WindowManagerStateHelper, 91 primaryApp: IComponentMatcher, 92 secondaryApp: IComponentMatcher, 93 ) { 94 wmHelper 95 .StateSyncBuilder() 96 .withWindowSurfaceAppeared(primaryApp) 97 .withWindowSurfaceAppeared(secondaryApp) 98 .withSplitDividerVisible() 99 .waitForAndVerify() 100 } 101 102 fun enterSplit( 103 wmHelper: WindowManagerStateHelper, 104 tapl: LauncherInstrumentation, 105 device: UiDevice, 106 primaryApp: IStandardAppHelper, 107 secondaryApp: IStandardAppHelper, 108 rotation: Rotation 109 ) { 110 primaryApp.launchViaIntent(wmHelper) 111 secondaryApp.launchViaIntent(wmHelper) 112 ChangeDisplayOrientationRule.setRotation(rotation) 113 tapl.goHome() 114 wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() 115 splitFromOverview(tapl, device, rotation) 116 waitForSplitComplete(wmHelper, primaryApp, secondaryApp) 117 } 118 119 fun enterSplitViaIntent( 120 wmHelper: WindowManagerStateHelper, 121 primaryApp: IStandardAppHelper, 122 secondaryApp: IStandardAppHelper 123 ) { 124 val stringExtras = mapOf(Primary.EXTRA_LAUNCH_ADJACENT to "true") 125 primaryApp.launchViaIntent(wmHelper, null, null, stringExtras) 126 waitForSplitComplete(wmHelper, primaryApp, secondaryApp) 127 } 128 129 fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice, rotation: Rotation) { 130 // Note: The initial split position in landscape is different between tablet and phone. 131 // In landscape, tablet will let the first app split to right side, and phone will 132 // split to left side. 133 if (tapl.isTablet) { 134 // TAPL's currentTask on tablet is sometimes not what we expected if the overview 135 // contains more than 3 task views. We need to use uiautomator directly to find the 136 // second task to split. 137 val home = tapl.workspace.switchToOverview() 138 ChangeDisplayOrientationRule.setRotation(rotation) 139 val isGridOnlyOverviewEnabled = tapl.isGridOnlyOverviewEnabled 140 if (isGridOnlyOverviewEnabled) { 141 home.currentTask.tapMenu().tapSplitMenuItem() 142 } else { 143 home.overviewActions.clickSplit() 144 } 145 val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS) 146 if (snapshots == null || snapshots.size < 1) { 147 error("Fail to find a overview snapshot to split.") 148 } 149 150 // Find the second task in the upper (or bottom for grid only Overview) right corner in 151 // split select mode by sorting 'left' in descending order and 'top' in ascending (or 152 // descending for grid only Overview) order. 153 snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> 154 t2.getVisibleBounds().left - t1.getVisibleBounds().left 155 } 156 snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> 157 if (isGridOnlyOverviewEnabled) { 158 t2.getVisibleBounds().top - t1.getVisibleBounds().top 159 } else { 160 t1.getVisibleBounds().top - t2.getVisibleBounds().top 161 } 162 } 163 snapshots[0].click() 164 } else { 165 val rotationCheckEnabled = tapl.getExpectedRotationCheckEnabled() 166 tapl.setExpectedRotationCheckEnabled(false) // disable rotation check to enter overview 167 val home = tapl.workspace.switchToOverview() 168 tapl.setExpectedRotationCheckEnabled(rotationCheckEnabled) // restore rotation checks 169 ChangeDisplayOrientationRule.setRotation(rotation) 170 home.currentTask.tapMenu().tapSplitMenuItem().currentTask.open() 171 } 172 SystemClock.sleep(TIMEOUT_MS) 173 } 174 175 fun dragFromNotificationToSplit( 176 instrumentation: Instrumentation, 177 device: UiDevice, 178 wmHelper: WindowManagerStateHelper 179 ) { 180 val displayBounds = 181 wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace 182 ?: error("Display not found") 183 val swipeXCoordinate = displayBounds.centerX() / 2 184 185 // Pull down the notifications 186 device.swipe(swipeXCoordinate, 5, swipeXCoordinate, displayBounds.bottom, 50 /* steps */) 187 SystemClock.sleep(TIMEOUT_MS) 188 189 // Find the target notification 190 val notificationScroller = 191 device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS) 192 ?: error("Unable to find view $notificationScrollerSelector") 193 var notificationContent = notificationScroller.findObject(notificationContentSelector) 194 195 while (notificationContent == null) { 196 device.swipe( 197 displayBounds.centerX(), 198 displayBounds.centerY(), 199 displayBounds.centerX(), 200 displayBounds.centerY() - 150, 201 20 /* steps */ 202 ) 203 notificationContent = notificationScroller.findObject(notificationContentSelector) 204 } 205 206 // Drag to split 207 val dragStart = notificationContent.visibleCenter 208 val dragMiddle = Point(dragStart.x + 50, dragStart.y) 209 val dragEnd = Point(displayBounds.width() / 4, displayBounds.width() / 4) 210 val downTime = SystemClock.uptimeMillis() 211 212 touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart) 213 // It needs a horizontal movement to trigger the drag 214 touchMove( 215 instrumentation, 216 downTime, 217 SystemClock.uptimeMillis(), 218 DRAG_DURATION_MS, 219 dragStart, 220 dragMiddle 221 ) 222 touchMove( 223 instrumentation, 224 downTime, 225 SystemClock.uptimeMillis(), 226 DRAG_DURATION_MS, 227 dragMiddle, 228 dragEnd 229 ) 230 // Wait for a while to start splitting 231 SystemClock.sleep(TIMEOUT_MS) 232 touch( 233 instrumentation, 234 MotionEvent.ACTION_UP, 235 downTime, 236 SystemClock.uptimeMillis(), 237 GESTURE_STEP_MS, 238 dragEnd 239 ) 240 SystemClock.sleep(TIMEOUT_MS) 241 } 242 243 fun touch( 244 instrumentation: Instrumentation, 245 action: Int, 246 downTime: Long, 247 eventTime: Long, 248 duration: Long, 249 point: Point 250 ) { 251 val motionEvent = 252 MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0) 253 motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN 254 instrumentation.uiAutomation.injectInputEvent(motionEvent, true) 255 motionEvent.recycle() 256 SystemClock.sleep(duration) 257 } 258 259 fun touchMove( 260 instrumentation: Instrumentation, 261 downTime: Long, 262 eventTime: Long, 263 duration: Long, 264 from: Point, 265 to: Point 266 ) { 267 val steps: Long = duration / GESTURE_STEP_MS 268 var currentTime = eventTime 269 var currentX = from.x.toFloat() 270 var currentY = from.y.toFloat() 271 val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat() 272 val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat() 273 274 for (i in 1..steps) { 275 val motionMove = 276 MotionEvent.obtain( 277 downTime, 278 currentTime, 279 MotionEvent.ACTION_MOVE, 280 currentX, 281 currentY, 282 0 283 ) 284 motionMove.source = InputDevice.SOURCE_TOUCHSCREEN 285 instrumentation.uiAutomation.injectInputEvent(motionMove, true) 286 motionMove.recycle() 287 288 currentTime += GESTURE_STEP_MS 289 if (i == steps - 1) { 290 currentX = to.x.toFloat() 291 currentY = to.y.toFloat() 292 } else { 293 currentX += stepX 294 currentY += stepY 295 } 296 SystemClock.sleep(GESTURE_STEP_MS) 297 } 298 } 299 300 fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) { 301 tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0)) 302 val allApps = tapl.workspace.switchToAllApps() 303 allApps.freeze() 304 try { 305 allApps.getAppIcon(appName).dragToHotseat(0) 306 } finally { 307 allApps.unfreeze() 308 } 309 } 310 311 fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) { 312 val displayBounds = 313 wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace 314 ?: error("Display not found") 315 val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) 316 dividerBar.drag(Point(displayBounds.width() * 1 / 3, displayBounds.height() * 2 / 3), 200) 317 318 wmHelper 319 .StateSyncBuilder() 320 .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER) 321 .waitForAndVerify() 322 } 323 324 fun dragDividerToDismissSplit( 325 device: UiDevice, 326 wmHelper: WindowManagerStateHelper, 327 dragToRight: Boolean, 328 dragToBottom: Boolean 329 ) { 330 val displayBounds = 331 wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace 332 ?: error("Display not found") 333 val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) 334 dividerBar.drag( 335 Point( 336 if (dragToRight) { 337 displayBounds.right 338 } else { 339 displayBounds.left 340 }, 341 if (dragToBottom) { 342 displayBounds.bottom 343 } else { 344 displayBounds.top 345 } 346 ) 347 ) 348 } 349 350 fun doubleTapDividerToSwitch(device: UiDevice) { 351 val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS) 352 val interval = 353 (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2 354 dividerBar.click() 355 SystemClock.sleep(interval.toLong()) 356 dividerBar.click() 357 } 358 359 fun copyContentInSplit( 360 instrumentation: Instrumentation, 361 device: UiDevice, 362 sourceApp: IComponentNameMatcher, 363 destinationApp: IComponentNameMatcher, 364 ) { 365 // Copy text from sourceApp 366 val textView = 367 device.wait( 368 Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")), 369 TIMEOUT_MS 370 ) 371 assertNotNull("Unable to find the TextView", textView) 372 textView.click(LONG_PRESS_TIME_MS) 373 374 val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS) 375 assertNotNull("Unable to find the copy button", copyBtn) 376 copyBtn.click() 377 378 // Paste text to destinationApp 379 val editText = 380 device.wait( 381 Until.findObject(By.res(destinationApp.packageName, "plain_text_input")), 382 TIMEOUT_MS 383 ) 384 assertNotNull("Unable to find the EditText", editText) 385 editText.click(LONG_PRESS_TIME_MS) 386 387 val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS) 388 assertNotNull("Unable to find the paste button", pasteBtn) 389 pasteBtn.click() 390 391 // Verify text 392 if (!textView.text.contentEquals(editText.text)) { 393 error("Fail to copy content in split") 394 } 395 } 396 } 397