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