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