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