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