• 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.hardware.input.InputSettings
25 import android.os.Handler
26 import android.util.ArrayMap
27 import android.util.Log
28 import android.view.InputDevice
29 import com.android.internal.annotations.VisibleForTesting
30 import com.android.internal.logging.InstanceId
31 import com.android.internal.logging.InstanceIdSequence
32 import com.android.internal.logging.UiEventLogger
33 import com.android.systemui.dagger.SysUISingleton
34 import com.android.systemui.dagger.qualifiers.Background
35 import com.android.systemui.flags.FeatureFlags
36 import com.android.systemui.flags.Flags
37 import com.android.systemui.log.DebugLogger.debugLog
38 import com.android.systemui.shared.hardware.hasInputDevice
39 import com.android.systemui.shared.hardware.isInternalStylusSource
40 import java.util.concurrent.CopyOnWriteArrayList
41 import java.util.concurrent.Executor
42 import javax.inject.Inject
43 
44 /**
45  * A class which keeps track of InputDevice events related to stylus devices, and notifies
46  * registered callbacks of stylus events.
47  */
48 @SysUISingleton
49 class StylusManager
50 @Inject
51 constructor(
52     private val context: Context,
53     private val inputManager: InputManager,
54     private val bluetoothAdapter: BluetoothAdapter?,
55     @Background private val handler: Handler,
56     @Background private val executor: Executor,
57     private val featureFlags: FeatureFlags,
58     private val uiEventLogger: UiEventLogger,
59 ) :
60     InputManager.InputDeviceListener,
61     InputManager.InputDeviceBatteryListener,
62     BluetoothAdapter.OnMetadataChangedListener {
63 
64     private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
65 
66     // This map should only be accessed on the handler
67     private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
68     private val inputDeviceBtSessionIdMap: MutableMap<Int, InstanceId> = ArrayMap()
69 
70     // These variables should only be accessed on the handler
71     private var hasStarted: Boolean = false
72     private var isInUsiSession: Boolean = false
73     private var usiSessionId: InstanceId? = null
74 
75     @VisibleForTesting var instanceIdSequence = InstanceIdSequence(1 shl 13)
76 
77     /**
78      * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
79      * at time of starting.
80      */
81     fun startListener() {
82         handler.post {
83             if (hasStarted) return@post
84             debugLog { "Listener has started." }
85 
86             hasStarted = true
87             isInUsiSession =
88                 inputManager.hasInputDevice {
89                     it.isInternalStylusSource && isBatteryStateValid(it.batteryState)
90                 }
91             addExistingStylusToMap()
92 
93             inputManager.registerInputDeviceListener(this, handler)
94         }
95     }
96 
97     /** Registers a StylusCallback to listen to stylus events. */
98     fun registerCallback(callback: StylusCallback) {
99         stylusCallbacks.add(callback)
100     }
101 
102     /** Unregisters a StylusCallback. If StylusCallback is not registered, is a no-op. */
103     fun unregisterCallback(callback: StylusCallback) {
104         stylusCallbacks.remove(callback)
105     }
106 
107     override fun onInputDeviceAdded(deviceId: Int) {
108         if (!hasStarted) return
109 
110         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
111         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
112         debugLog {
113             "Stylus InputDevice added: $deviceId ${device.name}, " +
114                 "External: ${device.isExternal}"
115         }
116 
117         if (!device.isExternal) {
118             registerBatteryListener(deviceId)
119         }
120 
121         val btAddress: String? = device.bluetoothAddress
122         inputDeviceAddressMap[deviceId] = btAddress
123         executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) }
124 
125         if (btAddress != null) {
126             onStylusUsed()
127             onStylusBluetoothConnected(deviceId, btAddress)
128             executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
129         }
130     }
131 
132     override fun onInputDeviceChanged(deviceId: Int) {
133         if (!hasStarted) return
134 
135         val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return
136         if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return
137         debugLog { "Stylus InputDevice changed: $deviceId ${device.name}" }
138 
139         val currAddress: String? = device.bluetoothAddress
140         val prevAddress: String? = inputDeviceAddressMap[deviceId]
141         inputDeviceAddressMap[deviceId] = currAddress
142 
143         if (prevAddress == null && currAddress != null) {
144             onStylusBluetoothConnected(deviceId, currAddress)
145             executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, currAddress) }
146         }
147 
148         if (prevAddress != null && currAddress == null) {
149             onStylusBluetoothDisconnected(deviceId, prevAddress)
150             executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, prevAddress) }
151         }
152     }
153 
154     override fun onInputDeviceRemoved(deviceId: Int) {
155         if (!hasStarted) return
156 
157         if (!inputDeviceAddressMap.contains(deviceId)) return
158         debugLog { "Stylus InputDevice removed: $deviceId" }
159 
160         unregisterBatteryListener(deviceId)
161 
162         val btAddress: String? = inputDeviceAddressMap[deviceId]
163         inputDeviceAddressMap.remove(deviceId)
164         if (btAddress != null) {
165             onStylusBluetoothDisconnected(deviceId, btAddress)
166             executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, btAddress) }
167         }
168         executeStylusCallbacks { cb -> cb.onStylusRemoved(deviceId) }
169     }
170 
171     override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) {
172         handler.post {
173             if (!hasStarted) return@post
174 
175             if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post
176 
177             val inputDeviceId: Int =
178                 inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull()
179                     ?: return@post
180 
181             val isCharging = String(value) == "true"
182 
183             debugLog {
184                 "Charging state metadata changed for device $inputDeviceId " +
185                     "${device.address}: $isCharging"
186             }
187 
188             executeStylusCallbacks { cb ->
189                 cb.onStylusBluetoothChargingStateChanged(inputDeviceId, device, isCharging)
190             }
191         }
192     }
193 
194     override fun onBatteryStateChanged(
195         deviceId: Int,
196         eventTimeMillis: Long,
197         batteryState: BatteryState
198     ) {
199         handler.post {
200             if (!hasStarted) return@post
201 
202             debugLog {
203                 "Battery state changed for $deviceId. " +
204                     "batteryState present: ${batteryState.isPresent}, " +
205                     "capacity: ${batteryState.capacity}"
206             }
207 
208             val batteryStateValid = isBatteryStateValid(batteryState)
209             trackAndLogUsiSession(deviceId, batteryStateValid)
210             if (batteryStateValid) {
211                 onStylusUsed()
212             }
213 
214             executeStylusCallbacks { cb ->
215                 cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState)
216             }
217         }
218     }
219 
220     private fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
221         trackAndLogBluetoothSession(deviceId, btAddress, true)
222         val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
223         try {
224             bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
225         } catch (e: IllegalArgumentException) {
226             Log.e(TAG, "$e: Metadata listener already registered for device. Ignoring.")
227         }
228     }
229 
230     private fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {
231         trackAndLogBluetoothSession(deviceId, btAddress, false)
232         val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
233         try {
234             bluetoothAdapter.removeOnMetadataChangedListener(device, this)
235         } catch (e: IllegalArgumentException) {
236             Log.e(TAG, "$e: Metadata listener does not exist for device. Ignoring.")
237         }
238     }
239 
240     /**
241      * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a
242      * physical stylus device has never been used. This method is run when 1) a USI stylus battery
243      * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a
244      * physical stylus device has actually been used.
245      */
246     private fun onStylusUsed() {
247         if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return
248         if (InputSettings.isStylusEverUsed(context)) return
249 
250         debugLog { "Stylus used for the first time." }
251         InputSettings.setStylusEverUsed(context, true)
252         executeStylusCallbacks { cb -> cb.onStylusFirstUsed() }
253     }
254 
255     /**
256      * Uses the input device battery state to track whether a current USI session is active. The
257      * InputDevice battery state updates USI battery on USI stylus input, and removes the last-known
258      * USI stylus battery presence after 1 hour of not detecting input. As SysUI and StylusManager
259      * is persistently running, relies on tracking sessions via an in-memory isInUsiSession boolean.
260      */
261     private fun trackAndLogUsiSession(deviceId: Int, batteryStateValid: Boolean) {
262         // TODO(b/268618918) handle cases where an invalid battery callback from a previous stylus
263         //  is sent after the actual valid callback
264         val hasBtConnection = if (inputDeviceBtSessionIdMap.isEmpty()) 0 else 1
265 
266         if (batteryStateValid && usiSessionId == null) {
267             debugLog { "USI battery newly present, entering new USI session: $deviceId" }
268             usiSessionId = instanceIdSequence.newInstanceId()
269             uiEventLogger.logWithInstanceIdAndPosition(
270                 StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED,
271                 0,
272                 null,
273                 usiSessionId,
274                 hasBtConnection,
275             )
276         } else if (!batteryStateValid && usiSessionId != null) {
277             debugLog { "USI battery newly absent, exiting USI session: $deviceId" }
278             uiEventLogger.logWithInstanceIdAndPosition(
279                 StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED,
280                 0,
281                 null,
282                 usiSessionId,
283                 hasBtConnection,
284             )
285             usiSessionId = null
286         }
287     }
288 
289     private fun trackAndLogBluetoothSession(
290         deviceId: Int,
291         btAddress: String,
292         btConnected: Boolean
293     ) {
294         debugLog {
295             "Bluetooth stylus ${if (btConnected) "connected" else "disconnected"}:" +
296                 " $deviceId $btAddress"
297         }
298 
299         if (btConnected) {
300             inputDeviceBtSessionIdMap[deviceId] = instanceIdSequence.newInstanceId()
301             uiEventLogger.logWithInstanceId(
302                 StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED,
303                 0,
304                 null,
305                 inputDeviceBtSessionIdMap[deviceId]
306             )
307         } else {
308             uiEventLogger.logWithInstanceId(
309                 StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED,
310                 0,
311                 null,
312                 inputDeviceBtSessionIdMap[deviceId]
313             )
314             inputDeviceBtSessionIdMap.remove(deviceId)
315         }
316     }
317 
318     private fun isBatteryStateValid(batteryState: BatteryState): Boolean {
319         return batteryState.isPresent && batteryState.capacity > 0.0f
320     }
321 
322     private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) {
323         stylusCallbacks.forEach(run)
324     }
325 
326     private fun registerBatteryListener(deviceId: Int) {
327         try {
328             inputManager.addInputDeviceBatteryListener(deviceId, executor, this)
329         } catch (e: SecurityException) {
330             Log.e(TAG, "$e: Failed to register battery listener for $deviceId.")
331         }
332     }
333 
334     private fun unregisterBatteryListener(deviceId: Int) {
335         // If deviceId wasn't registered, the result is a no-op, so an "is registered"
336         // check is not needed.
337         try {
338             inputManager.removeInputDeviceBatteryListener(deviceId, this)
339         } catch (e: SecurityException) {
340             Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.")
341         }
342     }
343 
344     private fun addExistingStylusToMap() {
345         for (deviceId: Int in inputManager.inputDeviceIds) {
346             val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue
347             if (device.supportsSource(InputDevice.SOURCE_STYLUS)) {
348                 inputDeviceAddressMap[deviceId] = device.bluetoothAddress
349 
350                 if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available
351                     // For most devices, an active (non-bluetooth) stylus is represented by an
352                     // internal InputDevice. This InputDevice will be present in InputManager
353                     // before CoreStartables run, and will not be removed.
354                     // In many cases, it reports the battery level of the stylus.
355                     registerBatteryListener(deviceId)
356                 } else {
357                     device.bluetoothAddress?.let { onStylusBluetoothConnected(deviceId, it) }
358                 }
359             }
360         }
361     }
362 
363     /**
364      * Callback interface to receive events from the StylusManager. All callbacks are run on the
365      * same background handler.
366      */
367     interface StylusCallback {
368         fun onStylusAdded(deviceId: Int) {}
369         fun onStylusRemoved(deviceId: Int) {}
370         fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {}
371         fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {}
372         fun onStylusFirstUsed() {}
373         fun onStylusBluetoothChargingStateChanged(
374             inputDeviceId: Int,
375             btDevice: BluetoothDevice,
376             isCharging: Boolean
377         ) {}
378         fun onStylusUsiBatteryStateChanged(
379             deviceId: Int,
380             eventTimeMillis: Long,
381             batteryState: BatteryState,
382         ) {}
383     }
384 
385     companion object {
386         val TAG = StylusManager::class.simpleName.orEmpty()
387     }
388 }
389