ScreenOrientationAction.kt
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.espresso.device.action
import android.app.Activity
import android.content.ComponentCallbacks
import android.content.Context
import android.content.pm.ActivityInfo.CONFIG_ORIENTATION
import android.content.res.Configuration
import android.util.Log
import androidx.test.espresso.device.common.getAccelerometerRotationSetting
import androidx.test.espresso.device.common.getDeviceApiLevel
import androidx.test.espresso.device.common.getResumedActivityOrNull
import androidx.test.espresso.device.common.isConfigurationChangeHandled
import androidx.test.espresso.device.common.isRobolectricTest
import androidx.test.espresso.device.common.setAccelerometerRotationSetting
import androidx.test.espresso.device.controller.DeviceControllerOperationException
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.platform.device.DeviceController
import androidx.test.runner.lifecycle.ActivityLifecycleCallback
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/** Action to set the test device to the provided screen orientation. */
internal class ScreenOrientationAction(val screenOrientation: ScreenOrientation) : DeviceAction {
/**
* Performs a screen rotation to the provided orientation.
*
* <p>If the device is already in the requested orientation, it's a no-op. Otherwise, the device
* will be set to the provided orientation.
*
* <p>Note, this method takes care of synchronization with the device and the app/activity under
* test after rotating the screen. Specifically, <ul>
* <li>if no activity is found in a RESUMED state, this method waits for the application's
* orientation to change to the requested orientation. </li>
* <li>if the activity handles device orientation change,this method waits for the application's
* orientation to change to the requested orientation, and relies on Espresso's {@code onView()}
* method to ensure it synchronizes properly with the updated activity. </li>
* <li>if the activity doesn't handle device orientation change, it waits until the activity is
* PAUSED and relies on Espresso's {@code onView()} method to ensure it synchronizes properly
* with the recreated activity. </li>
*
* </ul>
*
* @param deviceController the controller to use to interact with the device.
*/
override fun perform(deviceController: DeviceController) {
val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
if (screenOrientation == getCurrentScreenOrientation(context)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Device screen is already in the requested orientation, no need to rotate.")
}
return
}
if (isRobolectricTest()) {
deviceController.setScreenOrientation(screenOrientation.getOrientation())
return
}
var startingAccelRotationSetting = getAccelerometerRotationSetting()
val currentActivity = getResumedActivityOrNull()
val currentActivityName: String? = currentActivity?.getLocalClassName()
val configChangesHandled =
if (currentActivity != null) {
currentActivity.isConfigurationChangeHandled(CONFIG_ORIENTATION)
} else {
false
}
val latch: CountDownLatch = CountDownLatch(1)
val requestedOrientation =
if (screenOrientation == ScreenOrientation.LANDSCAPE) Configuration.ORIENTATION_LANDSCAPE
else Configuration.ORIENTATION_PORTRAIT
if (currentActivity == null || configChangesHandled) {
if (currentActivity == null) {
Log.d(TAG, "No activity was found in the RESUMED stage.")
} else if (configChangesHandled) {
Log.d(TAG, "The current activity handles configuration changes.")
}
context.registerComponentCallbacks(
object : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {
if (newConfig.orientation == requestedOrientation) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Application's orientation was set to the requested orientation.")
}
context.unregisterComponentCallbacks(this)
latch.countDown()
}
}
override fun onLowMemory() {}
}
)
} else {
Log.d(
TAG,
"The current activity does not handle configuration changes and will be recreated when " +
"its orientation changes."
)
ActivityLifecycleMonitorRegistry.getInstance()
.addLifecycleCallback(
object : ActivityLifecycleCallback {
override fun onActivityLifecycleChanged(activity: Activity, stage: Stage) {
if (
activity.getLocalClassName() == currentActivityName &&
stage == Stage.RESUMED &&
activity.getResources().getConfiguration().orientation == requestedOrientation
) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Test activity was resumed in the requested orientation.")
}
ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(this)
latch.countDown()
}
}
}
)
}
deviceController.setScreenOrientation(screenOrientation.getOrientation())
latch.await(5, TimeUnit.SECONDS)
// Restore accelerometer rotation setting if it was changed
if (
getDeviceApiLevel() >= 21 && startingAccelRotationSetting != getAccelerometerRotationSetting()
) {
setAccelerometerRotationSetting(startingAccelRotationSetting)
}
if (getCurrentScreenOrientation(context) != screenOrientation) {
throw DeviceControllerOperationException(
"Device could not be set to the requested screen orientation."
)
}
}
private fun getCurrentScreenOrientation(context: Context): ScreenOrientation {
var currentOrientation = context.getResources().getConfiguration().orientation
return if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE)
ScreenOrientation.LANDSCAPE
else ScreenOrientation.PORTRAIT
}
companion object {
private val TAG = ScreenOrientationAction::class.java.simpleName
}
}