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