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