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