Skip to content

Commit 172007b

Browse files
Paige McAuliffecopybara-androidxtest
authored andcommitted
Support setting screen orientation on physical devices
PiperOrigin-RevId: 536509405
1 parent 990677f commit 172007b

File tree

5 files changed

+119
-35
lines changed

5 files changed

+119
-35
lines changed

espresso/device/java/androidx/test/espresso/device/action/ScreenOrientationAction.kt

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,29 @@ package androidx.test.espresso.device.action
1818

1919
import android.app.Activity
2020
import android.content.ComponentCallbacks
21-
import android.content.ContentResolver
2221
import android.content.Context
2322
import android.content.pm.ActivityInfo.CONFIG_ORIENTATION
2423
import android.content.res.Configuration
25-
import android.database.ContentObserver
2624
import android.os.Handler
2725
import android.os.HandlerThread
2826
import android.provider.Settings.System
2927
import android.util.Log
3028
import androidx.test.espresso.device.context.ActionContext
29+
import androidx.test.espresso.device.controller.DeviceControllerOperationException
30+
import androidx.test.espresso.device.util.SettingsObserver
3131
import androidx.test.espresso.device.util.executeShellCommand
3232
import androidx.test.espresso.device.util.getDeviceApiLevel
3333
import androidx.test.espresso.device.util.getResumedActivityOrNull
3434
import androidx.test.espresso.device.util.isConfigurationChangeHandled
3535
import androidx.test.espresso.device.util.isRobolectricTest
36+
import androidx.test.espresso.device.util.isTestDeviceAnEmulator
3637
import androidx.test.platform.device.DeviceController
3738
import androidx.test.platform.device.UnsupportedDeviceOperationException
3839
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
3940
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
4041
import androidx.test.runner.lifecycle.Stage
4142
import java.util.concurrent.CountDownLatch
43+
import java.util.concurrent.TimeUnit
4244

4345
/** Action to set the test device to the provided screen orientation. */
4446
internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation) : DeviceAction {
@@ -66,12 +68,7 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
6668
* @param deviceController the controller to use to interact with the device.
6769
*/
6870
override fun perform(context: ActionContext, deviceController: DeviceController) {
69-
var currentOrientation =
70-
context.applicationContext.getResources().getConfiguration().orientation
71-
val requestedOrientation =
72-
if (screenOrientation == ScreenOrientation.LANDSCAPE) Configuration.ORIENTATION_LANDSCAPE
73-
else Configuration.ORIENTATION_PORTRAIT
74-
if (currentOrientation == requestedOrientation) {
71+
if (screenOrientation == getCurrentScreenOrientation(context)) {
7572
if (Log.isLoggable(TAG, Log.DEBUG)) {
7673
Log.d(TAG, "Device screen is already in the requested orientation, no need to rotate.")
7774
}
@@ -84,7 +81,8 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
8481
}
8582

8683
var oldAccelRotationSetting = getAccelerometerRotationSetting(context.applicationContext)
87-
if (oldAccelRotationSetting != AccelerometerRotation.ENABLED) {
84+
// For emulators, auto-rotate must be enabled. For physical devices, it must be disabled.
85+
if (isTestDeviceAnEmulator() && oldAccelRotationSetting != AccelerometerRotation.ENABLED) {
8886
// Executing shell commands requires API 21+.
8987
if (getDeviceApiLevel() >= 21) {
9088
Log.d(TAG, "Enabling auto-rotate.")
@@ -94,6 +92,17 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
9492
"Screen orientation cannot be set on this device because auto-rotate is disabled. Please manually enable auto-rotate and try again."
9593
)
9694
}
95+
} else if (
96+
!isTestDeviceAnEmulator() && oldAccelRotationSetting != AccelerometerRotation.DISABLED
97+
) {
98+
if (getDeviceApiLevel() >= 21) {
99+
Log.d(TAG, "Disabling auto-rotate.")
100+
setAccelerometerRotation(AccelerometerRotation.DISABLED, context.applicationContext)
101+
} else {
102+
throw UnsupportedDeviceOperationException(
103+
"Screen orientation cannot be set on this device because auto-rotate is enabled. Please manually disable auto-rotate and try again."
104+
)
105+
}
97106
}
98107

99108
val currentActivity = getResumedActivityOrNull()
@@ -106,6 +115,9 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
106115
}
107116

108117
val latch: CountDownLatch = CountDownLatch(1)
118+
val requestedOrientation =
119+
if (screenOrientation == ScreenOrientation.LANDSCAPE) Configuration.ORIENTATION_LANDSCAPE
120+
else Configuration.ORIENTATION_PORTRAIT
109121

110122
if (currentActivity == null || configChangesHandled) {
111123
if (currentActivity == null) {
@@ -124,6 +136,7 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
124136
latch.countDown()
125137
}
126138
}
139+
127140
override fun onLowMemory() {}
128141
}
129142
)
@@ -153,13 +166,18 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
153166
)
154167
}
155168
deviceController.setScreenOrientation(screenOrientation.orientation)
156-
latch.await()
169+
latch.await(5, TimeUnit.SECONDS)
157170
if (
158171
getDeviceApiLevel() >= 21 &&
159172
oldAccelRotationSetting != getAccelerometerRotationSetting(context.applicationContext)
160173
) {
161174
setAccelerometerRotation(oldAccelRotationSetting, context.applicationContext)
162175
}
176+
if (getCurrentScreenOrientation(context) != screenOrientation) {
177+
throw DeviceControllerOperationException(
178+
"Device could not be set to the requested screen orientation."
179+
)
180+
}
163181
}
164182

165183
private fun getAccelerometerRotationSetting(context: Context): AccelerometerRotation =
@@ -181,30 +199,26 @@ internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation)
181199
SettingsObserver(runnableHandler, context, settingsLatch, System.ACCELEROMETER_ROTATION)
182200
settingsObserver.observe()
183201
executeShellCommand("settings put system accelerometer_rotation ${accelerometerRotation.value}")
184-
settingsLatch.await()
202+
settingsLatch.await(5, TimeUnit.SECONDS)
185203
settingsObserver.stopObserver()
186204
thread.quitSafely()
187-
}
188205

189-
private class SettingsObserver(
190-
handler: Handler,
191-
val context: Context,
192-
val latch: CountDownLatch,
193-
val settingToObserve: String
194-
) : ContentObserver(handler) {
195-
fun observe() {
196-
val resolver: ContentResolver = context.getContentResolver()
197-
resolver.registerContentObserver(System.getUriFor(settingToObserve), false, this)
198-
}
199-
200-
fun stopObserver() {
201-
val resolver: ContentResolver = context.getContentResolver()
202-
resolver.unregisterContentObserver(this)
206+
if (
207+
executeShellCommand("settings get system accelerometer_rotation").trim().toInt() !=
208+
accelerometerRotation.value
209+
) {
210+
throw DeviceControllerOperationException(
211+
"Device could not be set to the requested screen orientation."
212+
)
203213
}
214+
}
204215

205-
override fun onChange(selfChange: Boolean) {
206-
latch.countDown()
207-
}
216+
private fun getCurrentScreenOrientation(context: ActionContext): ScreenOrientation {
217+
var currentOrientation =
218+
context.applicationContext.getResources().getConfiguration().orientation
219+
return if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE)
220+
ScreenOrientation.LANDSCAPE
221+
else ScreenOrientation.PORTRAIT
208222
}
209223

210224
companion object {

espresso/device/java/androidx/test/espresso/device/controller/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ kt_android_library(
3232
"PhysicalDeviceController.kt",
3333
],
3434
deps = [
35+
"//espresso/device/java/androidx/test/espresso/device/controller",
36+
"//espresso/device/java/androidx/test/espresso/device/util",
3537
"//runner/monitor",
3638
"@maven//:androidx_annotation_annotation",
3739
],

espresso/device/java/androidx/test/espresso/device/controller/PhysicalDeviceController.kt

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,66 @@
1616

1717
package androidx.test.espresso.device.controller
1818

19+
import android.content.Context
20+
import android.os.Handler
21+
import android.os.HandlerThread
22+
import android.provider.Settings.System
23+
import android.view.Surface
1924
import androidx.annotation.RestrictTo
25+
import androidx.test.espresso.device.util.SettingsObserver
26+
import androidx.test.espresso.device.util.executeShellCommand
2027
import androidx.test.platform.device.DeviceController
2128
import androidx.test.platform.device.UnsupportedDeviceOperationException
29+
import java.util.concurrent.CountDownLatch
30+
import java.util.concurrent.TimeUnit
2231

2332
/**
2433
* Implementation of {@link DeviceController} for tests run on a physical device.
2534
*
2635
* @hide
2736
*/
2837
@RestrictTo(RestrictTo.Scope.LIBRARY)
29-
class PhysicalDeviceController : DeviceController {
38+
class PhysicalDeviceController(private val context: Context) : DeviceController {
3039
override fun setDeviceMode(deviceMode: Int) {
3140
throw UnsupportedDeviceOperationException(
3241
"Setting a device mode is not supported on physical devices."
3342
)
3443
}
3544

3645
override fun setScreenOrientation(screenOrientation: Int) {
37-
// TODO(b/203092519) Investigate supporting screen orientation rotation on real devices.
38-
throw UnsupportedDeviceOperationException(
39-
"Setting screen orientation is not supported on physical devices."
40-
)
46+
// System user_rotation values must be one of the Surface rotation constants and these values
47+
// can indicate different orientations on different devices, since we check if the device is
48+
// already in correct orientation in ScreenOrientationAction, set user_rotation to its opposite
49+
// here to rotate the device to the opposite orientation
50+
val startingUserRotation =
51+
executeShellCommand("settings get system user_rotation").trim().toInt()
52+
val userRotationToSet =
53+
if (
54+
startingUserRotation == Surface.ROTATION_0 || startingUserRotation == Surface.ROTATION_270
55+
) {
56+
Surface.ROTATION_90
57+
} else {
58+
Surface.ROTATION_0
59+
}
60+
61+
val settingsLatch: CountDownLatch = CountDownLatch(1)
62+
val thread: HandlerThread = HandlerThread("Observer_Thread")
63+
thread.start()
64+
val runnableHandler: Handler = Handler(thread.getLooper())
65+
val settingsObserver: SettingsObserver =
66+
SettingsObserver(runnableHandler, context, settingsLatch, System.USER_ROTATION)
67+
settingsObserver.observe()
68+
executeShellCommand("settings put system user_rotation ${userRotationToSet}")
69+
settingsLatch.await(5, TimeUnit.SECONDS)
70+
settingsObserver.stopObserver()
71+
thread.quitSafely()
72+
73+
if (
74+
executeShellCommand("settings get system user_rotation").trim().toInt() != userRotationToSet
75+
) {
76+
throw DeviceControllerOperationException(
77+
"Device could not be set to the requested screen orientation."
78+
)
79+
}
4180
}
4281
}

espresso/device/java/androidx/test/espresso/device/dagger/DeviceControllerModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ internal class DeviceControllerModule {
6161
val connection = getEmulatorConnection()
6262
return EmulatorController(connection.emulatorController())
6363
} else {
64-
return PhysicalDeviceController()
64+
return PhysicalDeviceController(InstrumentationTestActionContext().applicationContext)
6565
}
6666
} else {
6767
return EspressoDeviceControllerAdpater(platformDeviceController)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package androidx.test.espresso.device.util
2+
3+
import android.content.ContentResolver
4+
import android.content.Context
5+
import android.database.ContentObserver
6+
import android.os.Handler
7+
import android.provider.Settings.System
8+
import java.util.concurrent.CountDownLatch
9+
10+
class SettingsObserver(
11+
handler: Handler,
12+
val context: Context,
13+
val latch: CountDownLatch,
14+
val settingToObserve: String
15+
) : ContentObserver(handler) {
16+
fun observe() {
17+
val resolver: ContentResolver = context.getContentResolver()
18+
resolver.registerContentObserver(System.getUriFor(settingToObserve), false, this)
19+
}
20+
21+
fun stopObserver() {
22+
val resolver: ContentResolver = context.getContentResolver()
23+
resolver.unregisterContentObserver(this)
24+
}
25+
26+
override fun onChange(selfChange: Boolean) {
27+
latch.countDown()
28+
}
29+
}

0 commit comments

Comments
 (0)