• 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 
17 package com.android.systemui.stylus
18 
19 import android.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothDevice
21 import android.content.Context
22 import android.hardware.BatteryState
23 import android.hardware.input.InputManager
24 import android.os.Handler
25 import android.util.ArrayMap
26 import android.util.Log
27 import android.view.InputDevice
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.dagger.qualifiers.Background
30 import com.android.systemui.flags.FeatureFlags
31 import com.android.systemui.flags.Flags
32 import java.util.concurrent.CopyOnWriteArrayList
33 import java.util.concurrent.Executor
34 import javax.inject.Inject
35 
36 /**
37  * A class which keeps track of InputDevice events related to stylus devices, and notifies
38  * registered callbacks of stylus events.
39  */
40 @SysUISingleton
41 class StylusManager
42 @Inject
43 constructor(
44     private val context: Context,
45     private val inputManager: InputManager,
46     private val bluetoothAdapter: BluetoothAdapter?,
47     @Background private val handler: Handler,
48     @Background private val executor: Executor,
49     private val featureFlags: FeatureFlags,
50 ) :
51     InputManager.InputDeviceListener,
52     InputManager.InputDeviceBatteryListener,
53     BluetoothAdapter.OnMetadataChangedListener {
54 
55     private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
56     private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
57         CopyOnWriteArrayList()
58     // This map should only be accessed on the handler
59     private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
60     // This variable should only be accessed on the handler
61     private var hasStarted: Boolean = false
62 
63     /**
64      * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
65      * at time of starting.
66      */
67     fun startListener() {
68         handler.post {
69             if (hasStarted) return@post
70             hasStarted = true
71             addExistingStylusToMap()
72 
73             inputManager.registerInputDeviceListener(this, handler)
74         }
75     }
76 
77     /** Registers a StylusCallback to listen to stylus events. */
78     fun registerCallback(callback: StylusCallback) {
79         stylusCallbacks.add(callback)
80     }
81 
82     /** Unregisters a StylusCallback. If StylusCallback is not registered, is a no-op. */
83     fun unregisterCallback(callback: StylusCallback) {
84         stylusCallbacks.remove(callback)
85     }
86 
87     fun registerBatteryCallback(callback: StylusBatteryCallback) {
88         stylusBatteryCallbacks.add(callback)
89     }
90 
91     fun unregisterBatteryCallback(callback: StylusBatteryCallback) {
92         stylusBatteryCallbacks.remove(callback)
93     }
94 
95     override fun onInputDeviceAdded(deviceId: Int) {
96         if (!hasStarted) return
97 
98         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
99         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
100 
101         if (!device.isExternal) {
102             registerBatteryListener(deviceId)
103         }
104 
105         // TODO(b/257936830): get address once input api available
106         val btAddress: String? = null
107         inputDeviceAddressMap[deviceId] = btAddress
108         executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
109 
110         if (btAddress != null) {
111             onStylusUsed()
112             onStylusBluetoothConnected(btAddress)
113             executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
114         }
115     }
116 
117     override fun onInputDeviceChanged(deviceId: Int) {
118         if (!hasStarted) return
119 
120         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
121         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
122 
123         // TODO(b/257936830): get address once input api available
124         val currAddress: String? = null
125         val prevAddress: String? = inputDeviceAddressMap[deviceId]
126         inputDeviceAddressMap[deviceId] = currAddress
127 
128         if (prevAddress == null && currAddress != null) {
129             onStylusBluetoothConnected(currAddress)
130             executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, currAddress) }
131         }
132 
133         if (prevAddress != null && currAddress == null) {
134             onStylusBluetoothDisconnected(prevAddress)
135             executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, prevAddress) }
136         }
137     }
138 
139     override fun onInputDeviceRemoved(deviceId: Int) {
140         if (!hasStarted) return
141 
142         if (!inputDeviceAddressMap.contains(deviceId)) return
143         unregisterBatteryListener(deviceId)
144 
145         val btAddress: String? = inputDeviceAddressMap[deviceId]
146         inputDeviceAddressMap.remove(deviceId)
147         if (btAddress != null) {
148             onStylusBluetoothDisconnected(btAddress)
149             executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, btAddress) }
150         }
151         executeStylusCallbacks { cb -> cb.onStylusRemoved(deviceId) }
152     }
153 
154     override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
155         handler.post {
156             if (!hasStarted) return@post
157 
158             if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post
159 
160             val inputDeviceId: Int =
161                 inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
162                     ?: return@post
163 
164             val isCharging = String(value) == "true"
165 
166             executeStylusBatteryCallbacks { cb ->
167                 cb.onStylusBluetoothChargingStateChanged(inputDeviceId, device, isCharging)
168             }
169         }
170     }
171 
172     override fun onBatteryStateChanged(
173         deviceId: Int,
174         eventTimeMillis: Long,
175         batteryState: BatteryState
176     ) {
177         handler.post {
178             if (!hasStarted) return@post
179 
180             if (batteryState.isPresent) {
181                 onStylusUsed()
182             }
183 
184             executeStylusBatteryCallbacks { cb ->
185                 cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
186             }
187         }
188     }
189 
190     private fun onStylusBluetoothConnected(btAddress: String) {
191         val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
192         try {
193             bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
194         } catch (e: IllegalArgumentException) {
195             Log.e(TAG, "$e: Metadata listener already registered for device. Ignoring.")
196         }
197     }
198 
199     private fun onStylusBluetoothDisconnected(btAddress: String) {
200         val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
201         try {
202             bluetoothAdapter.removeOnMetadataChangedListener(device, this)
203         } catch (e: IllegalArgumentException) {
204             Log.e(TAG, "$e: Metadata listener does not exist for device. Ignoring.")
205         }
206     }
207 
208     /**
209      * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
210      * physical stylus device has never been used. This method is run when 1) a USI stylus battery
211      * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
212      * physical stylus device has actually been used.
213      */
214     private fun onStylusUsed() {
215         if (true) return // TODO(b/261826950): remove on main
216         if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
217         if (inputManager.isStylusEverUsed(context)) return
218 
219         inputManager.setStylusEverUsed(context, true)
220         executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
221     }
222 
223     private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
224         stylusCallbacks.forEach(run)
225     }
226 
227     private fun executeStylusBatteryCallbacks(run: (cb: StylusBatteryCallback) -> Unit) {
228         stylusBatteryCallbacks.forEach(run)
229     }
230 
231     private fun registerBatteryListener(deviceId: Int) {
232         try {
233             inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
234         } catch (e: SecurityException) {
235             Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
236         }
237     }
238 
239     private fun unregisterBatteryListener(deviceId: Int) {
240         // If deviceId wasn't registered, the result is a no-op, so an "is registered"
241         // check is not needed.
242         try {
243             inputManager.removeInputDeviceBatteryListener(deviceId, this)
244         } catch (e: SecurityException) {
245             Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
246         }
247     }
248 
249     private fun addExistingStylusToMap() {
250         for (deviceId: Int in inputManager.inputDeviceIds) {
251             val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
252             if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
253                 // TODO(b/257936830): get address once input api available
254                 inputDeviceAddressMap[deviceId] = null
255 
256                 if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
257                     // For most devices, an active (non-bluetooth) stylus is represented by an
258                     // internal InputDevice. This InputDevice will be present in InputManager
259                     // before CoreStartables run, and will not be removed.
260                     // In many cases, it reports the battery level of the stylus.
261                     registerBatteryListener(deviceId)
262                 }
263             }
264         }
265     }
266 
267     /**
268      * Callback interface to receive events from the StylusManager. All callbacks are run on the
269      * same background handler.
270      */
271     interface StylusCallback {
272         fun onStylusAdded(deviceId: Int) {}
273         fun onStylusRemoved(deviceId: Int) {}
274         fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
275         fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
276         fun onStylusFirstUsed() {}
277     }
278 
279     /**
280      * Callback interface to receive stylus battery events from the StylusManager. All callbacks are
281      * runs on the same background handler.
282      */
283     interface StylusBatteryCallback {
284         fun onStylusBluetoothChargingStateChanged(
285             inputDeviceId: Int,
286             btDevice: BluetoothDevice,
287             isCharging: Boolean
288         ) {}
289         fun onStylusUsiBatteryStateChanged(
290             deviceId: Int,
291             eventTimeMillis: Long,
292             batteryState: BatteryState,
293         ) {}
294     }
295 
296     companion object {
297         private val TAG = StylusManager::class.simpleName.orEmpty()
298     }
299 }
300