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 package android.platform.helpers.foldable 17 18 import android.hardware.Sensor 19 import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY 20 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY 21 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY 22 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN 23 import android.hardware.devicestate.DeviceStateManager 24 import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback 25 import android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER 26 import android.hardware.devicestate.DeviceStateRequest 27 import android.hardware.devicestate.feature.flags.Flags as DeviceStateManagerFlags 28 import android.platform.test.rule.isLargeScreen 29 import android.platform.uiautomatorhelpers.DeviceHelpers.isScreenOnSettled 30 import android.platform.uiautomatorhelpers.DeviceHelpers.printInstrumentationStatus 31 import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice 32 import android.platform.uiautomatorhelpers.TracingUtils.trace 33 import android.platform.uiautomatorhelpers.WaitUtils.ensureThat 34 import android.util.Log 35 import androidx.annotation.FloatRange 36 import androidx.test.platform.app.InstrumentationRegistry 37 import com.android.internal.R 38 import java.time.Duration 39 import java.util.concurrent.CountDownLatch 40 import java.util.concurrent.TimeUnit 41 import kotlin.properties.Delegates.notNull 42 import org.junit.Assume.assumeTrue 43 44 /** Helper to set the folded state to a device. */ 45 internal class FoldableDeviceController { 46 47 private val context = InstrumentationRegistry.getInstrumentation().context 48 49 private val resources = context.resources 50 private val deviceStateManager = context.getSystemService(DeviceStateManager::class.java)!! 51 private val hingeAngleSensor = SensorInjectionController(Sensor.TYPE_HINGE_ANGLE) 52 53 private var foldedState by notNull<Int>() 54 private var unfoldedState by notNull<Int>() 55 private var halfFoldedState by notNull<Int>() 56 private var rearDisplayState by notNull<Int>() 57 private var currentState: Int? = null 58 59 private var deviceStateLatch = CountDownLatch(1) 60 private var pendingRequest: DeviceStateRequest? = null 61 62 /** Sets device state to folded. */ 63 fun fold() { 64 trace("FoldableDeviceController#fold") { 65 printInstrumentationStatus(TAG, "Folding") 66 setHingeAngle(0f) 67 setDeviceState(foldedState) 68 } 69 } 70 71 /** Sets device state to an unfolded state. */ 72 fun unfold() { 73 trace("FoldableDeviceController#unfold") { 74 printInstrumentationStatus(TAG, "Unfolding") 75 setHingeAngle(180f) 76 setDeviceState(unfoldedState) 77 } 78 } 79 80 /** Sets device state to half folded. */ 81 fun halfFold() { 82 trace("FoldableDeviceController#halfFold") { 83 printInstrumentationStatus(TAG, "Half-folding") 84 setHingeAngle(100f) 85 setDeviceState(halfFoldedState) 86 } 87 } 88 89 /** Sets device state to rear display */ 90 fun rearDisplay() { 91 trace("FoldableDeviceController#rearDisplay") { 92 printInstrumentationStatus(TAG, "Rear display") 93 setDeviceState(rearDisplayState) 94 } 95 } 96 97 /** Removes the override on the device state. */ 98 private fun resetDeviceState() { 99 printInstrumentationStatus(TAG, "resetDeviceState") 100 deviceStateManager.cancelBaseStateOverride() 101 // This might cause the screen to turn off if the default state is folded. 102 if (!uiDevice.isScreenOnSettled) { 103 uiDevice.wakeUp() 104 ensureThat("screen is on after cancelling base state override.") { uiDevice.isScreenOn } 105 } 106 } 107 108 fun init() { 109 deviceStateManager.registerCallback(context.mainExecutor, deviceStateCallback) 110 findStates() 111 hingeAngleSensor.init() 112 } 113 114 fun uninit() { 115 deviceStateManager.unregisterCallback(deviceStateCallback) 116 resetDeviceState() 117 hingeAngleSensor.uninit() 118 } 119 120 val isFolded: Boolean 121 get() = currentState == foldedState 122 123 val isUnfolded: Boolean 124 get() = currentState == unfoldedState 125 126 val isHalfFolded: Boolean 127 get() = currentState == halfFoldedState 128 129 val isOnRearDisplay: Boolean 130 get() = currentState == rearDisplayState 131 132 fun setHingeAngle(@FloatRange(from = 0.0, to = 180.0) angle: Float) { 133 hingeAngleSensor.setValue(angle) 134 } 135 136 private fun findStates() { 137 if (DeviceStateManagerFlags.deviceStatePropertyMigration()) { 138 findStates_deviceStateManager() 139 } else { 140 findStates_configValues() 141 } 142 } 143 144 private fun findStates_configValues() { 145 val foldedStates = resources.getIntArray(R.array.config_foldedDeviceStates) 146 assumeTrue("Skipping on non-foldable devices", foldedStates.isNotEmpty()) 147 foldedState = foldedStates.first() 148 unfoldedState = resources.getIntArray(R.array.config_openDeviceStates).first() 149 halfFoldedState = resources.getIntArray(R.array.config_halfFoldedDeviceStates).first() 150 rearDisplayState = resources.getIntArray(R.array.config_rearDisplayDeviceStates).first() 151 } 152 153 private fun findStates_deviceStateManager() { 154 val deviceStates = deviceStateManager.supportedDeviceStates 155 foldedState = 156 deviceStates 157 .firstOrNull { deviceState -> 158 deviceState.hasProperty( 159 PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY 160 ) && !deviceState.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY) 161 } 162 ?.identifier ?: INVALID_DEVICE_STATE_IDENTIFIER 163 164 assumeTrue( 165 "Skipping on non-foldable devices", 166 foldedState != INVALID_DEVICE_STATE_IDENTIFIER, 167 ) 168 169 halfFoldedState = 170 deviceStates 171 .firstOrNull { deviceState -> 172 deviceState.hasProperties( 173 PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY, 174 PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, 175 ) 176 } 177 ?.identifier ?: INVALID_DEVICE_STATE_IDENTIFIER 178 179 unfoldedState = 180 deviceStates 181 .firstOrNull { deviceState -> 182 deviceState.hasProperty( 183 PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY 184 ) && 185 !deviceState.hasProperty( 186 PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN 187 ) 188 } 189 ?.identifier ?: INVALID_DEVICE_STATE_IDENTIFIER 190 191 rearDisplayState = 192 deviceStates 193 .firstOrNull { deviceState -> 194 deviceState.hasProperty(PROPERTY_FEATURE_REAR_DISPLAY) 195 } 196 ?.identifier ?: INVALID_DEVICE_STATE_IDENTIFIER 197 } 198 199 private fun setDeviceState(state: Int) { 200 if (currentState == state) { 201 Log.e(TAG, "setting device state to the same state already set.") 202 return 203 } 204 deviceStateLatch = CountDownLatch(1) 205 val request = DeviceStateRequest.newBuilder(state).build() 206 pendingRequest = request 207 trace("Requesting base state override to ${state.desc()}") { 208 deviceStateManager.requestBaseStateOverride( 209 request, 210 context.mainExecutor, 211 deviceStateRequestCallback, 212 ) 213 deviceStateLatch.await { "Device state didn't change within the timeout" } 214 ensureStateSet(state) 215 } 216 Log.d(TAG, "Device state set to ${state.desc()}") 217 } 218 219 private fun ensureStateSet(state: Int) { 220 when (state) { 221 foldedState -> 222 ensureThat("Device folded") { currentState == foldedState && !isLargeScreen() } 223 unfoldedState -> 224 ensureThat("Device unfolded") { currentState == unfoldedState && isLargeScreen() } 225 halfFoldedState -> 226 ensureThat("Device half folded") { 227 currentState == halfFoldedState && isLargeScreen() 228 } 229 rearDisplayState -> 230 ensureThat("Device rear display") { 231 currentState == rearDisplayState && !isLargeScreen() 232 } 233 } 234 } 235 236 private fun Int.desc() = 237 when (this) { 238 foldedState -> "Folded" 239 unfoldedState -> "Unfolded" 240 halfFoldedState -> "Half Folded" 241 rearDisplayState -> "Rear Display" 242 else -> "unknown" 243 } 244 245 private fun CountDownLatch.await(error: () -> String) { 246 check(this.await(DEVICE_STATE_MAX_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), error) 247 } 248 249 private val deviceStateCallback = DeviceStateCallback { state -> 250 currentState = state.identifier 251 } 252 253 private val deviceStateRequestCallback = 254 object : DeviceStateRequest.Callback { 255 override fun onRequestActivated(request: DeviceStateRequest) { 256 Log.d(TAG, "Request activated: ${request.state.desc()}") 257 if (request == pendingRequest) { 258 deviceStateLatch.countDown() 259 } 260 currentState = request.state 261 } 262 263 override fun onRequestCanceled(request: DeviceStateRequest) { 264 Log.d(TAG, "Request cancelled: ${request.state.desc()}") 265 if (currentState == request.state) { 266 currentState = null 267 } 268 } 269 270 override fun onRequestSuspended(request: DeviceStateRequest) { 271 Log.d(TAG, "Request suspended: ${request.state.desc()}") 272 if (currentState == request.state) { 273 currentState = null 274 } 275 } 276 } 277 278 private companion object { 279 const val TAG = "FoldableController" 280 val DEVICE_STATE_MAX_TIMEOUT = Duration.ofSeconds(10) 281 } 282 } 283