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