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