1 /*
<lambda>null2  * Copyright (C) 2022 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 android.input.cts
18 
19 import android.Manifest
20 import android.app.Activity
21 import android.app.ActivityOptions
22 import android.app.Instrumentation
23 import android.content.Context
24 import android.content.pm.PackageManager
25 import android.graphics.PixelFormat
26 import android.hardware.display.DisplayManager
27 import android.hardware.display.VirtualDisplay
28 import android.media.ImageReader
29 import android.os.Handler
30 import android.os.Looper
31 import android.os.SystemClock
32 import android.server.wm.WindowManagerStateHelper
33 import android.support.test.uiautomator.UiDevice
34 import android.view.Display
35 import android.view.InputDevice
36 import android.view.MotionEvent
37 import android.view.MotionEvent.ACTION_DOWN
38 import android.view.MotionEvent.ACTION_UP
39 import android.view.View
40 import android.view.ViewTreeObserver
41 import androidx.test.core.app.ActivityScenario
42 import androidx.test.ext.junit.rules.ActivityScenarioRule
43 import androidx.test.ext.junit.runners.AndroidJUnit4
44 import androidx.test.filters.MediumTest
45 import androidx.test.platform.app.InstrumentationRegistry
46 import com.android.compatibility.common.util.AdoptShellPermissionsRule
47 import com.android.compatibility.common.util.PollingCheck
48 import com.android.compatibility.common.util.SystemUtil
49 import com.android.compatibility.common.util.WindowUtil
50 import com.google.common.truth.Truth.assertThat
51 import java.lang.AutoCloseable
52 import java.util.Arrays
53 import java.util.concurrent.CountDownLatch
54 import java.util.concurrent.TimeUnit
55 import org.junit.After
56 import org.junit.Assert.fail
57 import org.junit.Assume.assumeFalse
58 import org.junit.Assume.assumeTrue
59 import org.junit.Before
60 import org.junit.Rule
61 import org.junit.Test
62 import org.junit.runner.RunWith
63 
64 private const val TOUCH_MODE_PROPAGATION_TIMEOUT_MILLIS: Long = 5000 // 5 sec
65 
66 @MediumTest
67 @RunWith(AndroidJUnit4::class)
68 class TouchModeTest {
69     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
70     private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
71     private var virtualDisplay: VirtualDisplay? = null
72     private var imageReader: ImageReader? = null
73 
74     @get:Rule
75     val activityRule = ActivityScenarioRule<Activity>(Activity::class.java)
76     private lateinit var activity: Activity
77     private lateinit var targetContext: Context
78     private lateinit var displayManager: DisplayManager
79     private var secondScenario: ActivityScenario<Activity>? = null
80 
81     @Rule
82     fun permissionsRule() = AdoptShellPermissionsRule(
83             instrumentation.getUiAutomation(),
84             Manifest.permission.ADD_TRUSTED_DISPLAY
85     )
86 
87     @Before
88     fun setUp() {
89         targetContext = instrumentation.targetContext
90         displayManager = targetContext.getSystemService(DisplayManager::class.java)
91         activityRule.scenario.onActivity {
92             activity = it
93         }
94         WindowUtil.waitForFocus(activity)
95         instrumentation.setInTouchMode(false)
96     }
97 
98     @After
99     fun tearDown() {
100         val scenario = secondScenario
101         if (scenario != null) {
102             scenario.close()
103         }
104         val display = virtualDisplay
105         if (display != null) {
106             display.release()
107         }
108         val reader = imageReader
109         if (reader != null) {
110             reader.close()
111         }
112     }
113 
114     fun isInTouchMode(): Boolean {
115         return activity.window.decorView.isInTouchMode
116     }
117 
118     fun isRunningActivitiesOnSecondaryDisplaysSupported(): Boolean {
119         return instrumentation.context.packageManager.hasSystemFeature(
120                 PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS
121         )
122     }
123 
124     @Test
125     fun testFocusedWindowOwnerCanChangeTouchMode() {
126         instrumentation.setInTouchMode(true)
127         PollingCheck.waitFor { isInTouchMode() }
128         assertThat(isInTouchMode()).isTrue()
129     }
130 
131     @Test
132     fun testOnTouchModeChangeNotification() {
133         val touchModeChangeListener = OnTouchModeChangeListenerImpl()
134         val observer = activity.window.decorView.rootView.viewTreeObserver
135         observer.addOnTouchModeChangeListener(touchModeChangeListener)
136         val newTouchMode = !isInTouchMode()
137 
138         instrumentation.setInTouchMode(newTouchMode)
139         try {
140             assertThat(touchModeChangeListener.countDownLatch.await(
141                     TOUCH_MODE_PROPAGATION_TIMEOUT_MILLIS,
142                     TimeUnit.MILLISECONDS
143             )).isTrue()
144         } catch (e: InterruptedException) {
145             Thread.currentThread().interrupt()
146             throw RuntimeException(e)
147         }
148 
149         assertThat(touchModeChangeListener.isInTouchMode).isEqualTo(newTouchMode)
150     }
151 
152     private class OnTouchModeChangeListenerImpl : ViewTreeObserver.OnTouchModeChangeListener {
153         val countDownLatch = CountDownLatch(1)
154         var isInTouchMode = false
155 
156         override fun onTouchModeChanged(mode: Boolean) {
157             isInTouchMode = mode
158             countDownLatch.countDown()
159         }
160     }
161 
162     @Test
163     fun testNonFocusedWindowOwnerCannotChangeTouchMode() {
164         // It takes 400-500 milliseconds in average for DecorView to receive the touch mode changed
165         // event on 2021 hardware, so we set the timeout to 10x that. It's still possible that a
166         // test would fail, but we don't have a better way to check that an event does not occur.
167         // Due to the 2 expected touch mode events to occur, this test may take few seconds to run.
168         uiDevice.pressHome()
169         WindowManagerStateHelper().waitForAppTransitionIdleOnDisplay(activity.display.displayId)
170         PollingCheck.waitFor(WindowUtil.WINDOW_FOCUS_TIMEOUT_MILLIS) { !activity.hasWindowFocus() }
171 
172         instrumentation.setInTouchMode(true)
173 
174         SystemClock.sleep(TOUCH_MODE_PROPAGATION_TIMEOUT_MILLIS)
175         assertThat(isInTouchMode()).isFalse()
176     }
177 
178     @Test
179     fun testDetachedViewReturnsDefaultTouchMode() {
180         val context = instrumentation.targetContext
181         val defaultInTouchMode = context.resources.getBoolean(
182             context.resources.getIdentifier("config_defaultInTouchMode", "bool", "android")
183         )
184 
185         val detachedView = View(activity)
186 
187         // Detached view (view with mAttachInfo null) will just return the default touch mode value
188         assertThat(detachedView.isInTouchMode()).isEqualTo(defaultInTouchMode)
189     }
190 
191     /**
192      * When per-display focus is disabled ({@code config_perDisplayFocusEnabled} is set to false),
193      * touch mode changes affect all displays.
194      *
195      * In this test, we tap the main display, and ensure that touch mode becomes
196      * true on both the main display and the secondary display
197      */
198     @Test
199     fun testTouchModeUpdate_PerDisplayFocusDisabled() {
200         assumeTrue(isRunningActivitiesOnSecondaryDisplaysSupported())
201         assumeFalse(
202                 "This test requires config_perDisplayFocusEnabled to be false",
203                 targetContext.resources.getBoolean(targetContext.resources.getIdentifier(
204                         "config_perDisplayFocusEnabled",
205                         "bool",
206                         "android",
207                 ))
208         )
209 
210         val secondaryDisplayId = findOrCreateSecondaryDisplay()
211 
212         touchDownOnDefaultDisplay().use {
213             assertThat(isInTouchMode()).isTrue()
214             assertSecondaryDisplayTouchModeState(secondaryDisplayId, isInTouch = true)
215         }
216     }
217 
218     /**
219      * When per-display focus is enabled ({@code config_perDisplayFocusEnabled} is set to true),
220      * touch mode changes does not affect all displays.
221      *
222      * In this test, we tap the main display, and ensure that touch mode becomes
223      * true on main display only. Touch mode on secondary display must remain false.
224      */
225     @Test
226     fun testTouchModeUpdate_PerDisplayFocusEnabled() {
227         assumeTrue(isRunningActivitiesOnSecondaryDisplaysSupported())
228         assumeTrue(
229                 "This test requires config_perDisplayFocusEnabled to be true",
230                 targetContext.resources.getBoolean(targetContext.resources.getIdentifier(
231                         "config_perDisplayFocusEnabled",
232                         "bool",
233                         "android"
234                 ))
235         )
236 
237         val secondaryDisplayId = findOrCreateSecondaryDisplay()
238 
239         touchDownOnDefaultDisplay().use {
240             assertThat(isInTouchMode()).isTrue()
241             assertSecondaryDisplayTouchModeState(
242                 secondaryDisplayId,
243                 isInTouch = false,
244                 delayBeforeChecking = true
245             )
246         }
247     }
248 
249     /**
250      * Regardless of the {@code config_perDisplayFocusEnabled} value,
251      * touch mode changes does not affect displays with own focus.
252      *
253      * In this test, we tap the main display, and ensure that touch mode becomes
254      * true only on the main display. Touch mode on the secondary display must remain false because
255      * it maintains its own focus and touch mode.
256      */
257     @Test
258     fun testTouchModeUpdate_DisplayHasOwnFocus() {
259         assumeTrue(isRunningActivitiesOnSecondaryDisplaysSupported())
260         val secondaryDisplayId = createVirtualDisplay(
261                 VIRTUAL_DISPLAY_FLAG_OWN_FOCUS or VIRTUAL_DISPLAY_FLAG_TRUSTED
262         )
263 
264         touchDownOnDefaultDisplay().use {
265             assertThat(isInTouchMode()).isTrue()
266             assertSecondaryDisplayTouchModeState(
267                 secondaryDisplayId,
268                 isInTouch = false,
269                 delayBeforeChecking = true
270             )
271         }
272     }
273 
274     private fun findOrCreateSecondaryDisplay(): Int {
275         // Pick a random secondary external display if there is any.
276         // A virtual display is only created if the device only has a single (default) display.
277         val display = Arrays.stream(displayManager.displays).filter { d ->
278             d.displayId != Display.DEFAULT_DISPLAY && d.type == Display.TYPE_EXTERNAL
279         }.findFirst()
280         if (display.isEmpty) {
281             return createVirtualDisplay(flags = 0)
282         }
283         return display.get().displayId
284     }
285 
286     private fun assertSecondaryDisplayTouchModeState(
287             displayId: Int,
288             isInTouch: Boolean,
289             delayBeforeChecking: Boolean = false
290     ) {
291         if (delayBeforeChecking) {
292             SystemClock.sleep(TOUCH_MODE_PROPAGATION_TIMEOUT_MILLIS)
293         }
294         PollingCheck.waitFor(TOUCH_MODE_PROPAGATION_TIMEOUT_MILLIS) {
295             isSecondaryDisplayInTouchMode(displayId) == isInTouch
296         }
297         assertThat(isSecondaryDisplayInTouchMode(displayId)).isEqualTo(isInTouch)
298     }
299 
300     private fun isSecondaryDisplayInTouchMode(displayId: Int): Boolean {
301         if (secondScenario == null) {
302             launchSecondScenarioActivity(displayId)
303         }
304         val scenario = secondScenario
305         var inTouch: Boolean? = null
306         if (scenario != null) {
307             scenario.onActivity {
308                 inTouch = it.window.decorView.isInTouchMode
309             }
310         } else {
311             fail("Fail to launch secondScenario")
312         }
313         return inTouch == true
314     }
315 
316     private fun launchSecondScenarioActivity(displayId: Int) {
317         // Launch activity on the picked display
318         val bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle()
319         SystemUtil.runWithShellPermissionIdentity({
320             secondScenario = ActivityScenario.launch(Activity::class.java, bundle)
321         }, Manifest.permission.INTERNAL_SYSTEM_WINDOW)
322     }
323 
324     private fun touchDownOnDefaultDisplay(): AutoCloseable {
325         val downTime = SystemClock.uptimeMillis()
326         val down = MotionEvent.obtain(
327             downTime,
328             downTime,
329             ACTION_DOWN,
330             100f, // x
331             100f, // y
332             0 // metaState
333         )
334         down.source = InputDevice.SOURCE_TOUCHSCREEN
335         val sync = true
336         instrumentation.uiAutomation.injectInputEvent(down, sync)
337 
338         // Clean up by sending an up event so that we ensure gestures are injected consistently.
339         return AutoCloseable {
340             val upEventTime = SystemClock.uptimeMillis()
341             val up = MotionEvent.obtain(
342                 downTime,
343                 upEventTime,
344                 ACTION_UP,
345                 100f, // x
346                 100f, // y
347                 0 // metaState
348             )
349             up.source = InputDevice.SOURCE_TOUCHSCREEN
350             instrumentation.uiAutomation.injectInputEvent(up, sync)
351         }
352     }
353 
354     private fun createVirtualDisplay(flags: Int): Int {
355         val displayCreated = CountDownLatch(1)
356         displayManager.registerDisplayListener(object : DisplayManager.DisplayListener {
357             override fun onDisplayAdded(displayId: Int) {}
358             override fun onDisplayRemoved(displayId: Int) {}
359             override fun onDisplayChanged(displayId: Int) {
360                 displayCreated.countDown()
361                 displayManager.unregisterDisplayListener(this)
362             }
363         }, Handler(Looper.getMainLooper()))
364         imageReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2)
365         val reader = imageReader
366         virtualDisplay = displayManager.createVirtualDisplay(
367                 VIRTUAL_DISPLAY_NAME, WIDTH, HEIGHT, DENSITY, reader!!.surface, flags)
368 
369         assertThat(displayCreated.await(5, TimeUnit.SECONDS)).isTrue()
370         assertThat(virtualDisplay).isNotNull()
371         instrumentation.setInTouchMode(false)
372         return virtualDisplay!!.display.displayId
373     }
374 
375     companion object {
376         const val VIRTUAL_DISPLAY_NAME = "CtsVirtualDisplay"
377         const val WIDTH = 480
378         const val HEIGHT = 800
379         const val DENSITY = 160
380 
381         /** See [DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_FOCUS].  */
382         const val VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 shl 14
383 
384         /** See [DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED].  */
385         const val VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 shl 10
386     }
387 }
388