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