• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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