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