1 /* 2 * 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.DeviceStateRequest 21 import android.platform.test.rule.isLargeScreen 22 import android.platform.uiautomator_helpers.DeviceHelpers.isScreenOnSettled 23 import android.platform.uiautomator_helpers.DeviceHelpers.printInstrumentationStatus 24 import android.platform.uiautomator_helpers.DeviceHelpers.uiDevice 25 import android.platform.uiautomator_helpers.TracingUtils.trace 26 import android.platform.uiautomator_helpers.WaitUtils.ensureThat 27 import android.util.Log 28 import androidx.annotation.FloatRange 29 import androidx.test.platform.app.InstrumentationRegistry 30 import com.android.internal.R 31 import java.time.Duration 32 import java.util.concurrent.CountDownLatch 33 import java.util.concurrent.TimeUnit 34 import kotlin.properties.Delegates.notNull 35 import org.junit.Assume.assumeTrue 36 37 /** Helper to set the folded state to a device. */ 38 internal class FoldableDeviceController { 39 40 private val context = InstrumentationRegistry.getInstrumentation().context 41 42 private val resources = context.resources 43 private val deviceStateManager = context.getSystemService(DeviceStateManager::class.java)!! 44 private val hingeAngleSensor = SensorInjectionController(Sensor.TYPE_HINGE_ANGLE) 45 46 private var foldedState by notNull<Int>() 47 private var unfoldedState by notNull<Int>() 48 // [currentState] is meant to be not null only when there is an override active. 49 private var currentState: Int? = null 50 51 private var deviceStateLatch = CountDownLatch(1) 52 private var pendingRequest: DeviceStateRequest? = null 53 54 /** Sets device state to folded. */ foldnull55 fun fold() { 56 trace("FoldableDeviceController#fold") { 57 printInstrumentationStatus(TAG, "Folding") 58 setDeviceState(foldedState) 59 } 60 } 61 62 /** Sets device state to an unfolded state. */ unfoldnull63 fun unfold() { 64 trace("FoldableDeviceController#unfold") { 65 printInstrumentationStatus(TAG, "Unfolding") 66 setDeviceState(unfoldedState) 67 } 68 } 69 70 /** Removes the override on the device state. */ resetDeviceStatenull71 private fun resetDeviceState() { 72 printInstrumentationStatus(TAG, "resetDeviceState") 73 deviceStateManager.cancelBaseStateOverride() 74 // This might cause the screen to turn off if the default state is folded. 75 if (!uiDevice.isScreenOnSettled) { 76 uiDevice.wakeUp() 77 ensureThat("screen is on after cancelling base state override.") { uiDevice.isScreenOn } 78 } 79 } 80 81 /** Fetches folded and unfolded state identifier from the device. */ initnull82 fun init() { 83 findFoldedUnfoldedStates() 84 currentState = if (isLargeScreen()) unfoldedState else foldedState 85 Log.d(TAG, "Initial state. Folded=$isFolded") 86 hingeAngleSensor.init() 87 } 88 uninitnull89 fun uninit() { 90 resetDeviceState() 91 hingeAngleSensor.uninit() 92 } 93 94 val isFolded: Boolean 95 get() { <lambda>null96 check(currentState != null) { 97 "Trying to get the current state while there is no state override set." 98 } 99 return currentState == foldedState 100 } 101 setHingeAnglenull102 fun setHingeAngle(@FloatRange(from = 0.0, to = 180.0) angle: Float) { 103 hingeAngleSensor.setValue(angle) 104 } 105 findFoldedUnfoldedStatesnull106 private fun findFoldedUnfoldedStates() { 107 val foldedStates = resources.getIntArray(R.array.config_foldedDeviceStates) 108 assumeTrue("Skipping on non-foldable devices", foldedStates.isNotEmpty()) 109 foldedState = foldedStates[0] 110 unfoldedState = 111 deviceStateManager.supportedStates.firstOrNull { it != foldedState } 112 ?: throw IllegalStateException("Unfolded state not found.") 113 } 114 setDeviceStatenull115 private fun setDeviceState(state: Int) { 116 if (currentState == state) { 117 Log.e(TAG, "setting device state to the same state already set.") 118 return 119 } 120 deviceStateLatch = CountDownLatch(1) 121 val request = DeviceStateRequest.newBuilder(state).build() 122 pendingRequest = request 123 trace("Requesting base state override to ${state.desc()}") { 124 deviceStateManager.requestBaseStateOverride( 125 request, 126 context.mainExecutor, 127 deviceStateRequestCallback 128 ) 129 deviceStateLatch.await { "Device state didn't change within the timeout" } 130 ensureStateSet(state) 131 } 132 Log.d(TAG, "Device state set to ${state.desc()}") 133 } 134 ensureStateSetnull135 private fun ensureStateSet(state: Int) { 136 when (state) { 137 foldedState -> ensureThat("Device folded") { !isLargeScreen() } 138 unfoldedState -> ensureThat("Device unfolded") { isLargeScreen() } 139 else -> TODO("Implement a way to know if states other than un/folded are set.") 140 } 141 } 142 Intnull143 private fun Int.desc() = 144 when (this) { 145 foldedState -> "Folded" 146 unfoldedState -> "Unfolded" 147 else -> "unknown" 148 } 149 awaitnull150 private fun CountDownLatch.await(error: () -> String) { 151 check(this.await(DEVICE_STATE_MAX_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS), error) 152 } 153 154 private val deviceStateRequestCallback = 155 object : DeviceStateRequest.Callback { onRequestActivatednull156 override fun onRequestActivated(request: DeviceStateRequest) { 157 Log.d(TAG, "Request activated: ${request.state.desc()}") 158 if (request == pendingRequest) { 159 deviceStateLatch.countDown() 160 } 161 currentState = request.state 162 } 163 onRequestCancelednull164 override fun onRequestCanceled(request: DeviceStateRequest) { 165 Log.d(TAG, "Request cancelled: ${request.state.desc()}") 166 if (currentState == request.state) { 167 currentState = null 168 } 169 } 170 onRequestSuspendednull171 override fun onRequestSuspended(request: DeviceStateRequest) { 172 Log.d(TAG, "Request suspended: ${request.state.desc()}") 173 if (currentState == request.state) { 174 currentState = null 175 } 176 } 177 } 178 179 private companion object { 180 const val TAG = "FoldableController" 181 val DEVICE_STATE_MAX_TIMEOUT = Duration.ofSeconds(10) 182 } 183 } 184