Loading packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +54 −2 Original line number Diff line number Diff line Loading @@ -30,6 +30,9 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.shared.hardware.hasInputDevice import com.android.systemui.shared.hardware.isInternalStylusSource import com.android.systemui.statusbar.notification.collection.listbuilder.DEBUG import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject Loading Loading @@ -59,8 +62,10 @@ constructor( CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() // This variable should only be accessed on the handler // These variables should only be accessed on the handler private var hasStarted: Boolean = false private var isInUsiSession: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot Loading @@ -70,6 +75,10 @@ constructor( handler.post { if (hasStarted) return@post hasStarted = true isInUsiSession = inputManager.hasInputDevice { it.isInternalStylusSource && isBatteryStateValid(it.batteryState) } addExistingStylusToMap() inputManager.registerInputDeviceListener(this, handler) Loading Loading @@ -177,7 +186,18 @@ constructor( handler.post { if (!hasStarted) return@post if (batteryState.isPresent) { if (DEBUG) { Log.d( TAG, "onBatteryStateChanged for $deviceId. " + "batteryState present: ${batteryState.isPresent}, " + "capacity: ${batteryState.capacity}" ) } val batteryStateValid = isBatteryStateValid(batteryState) trackAndLogUsiSession(deviceId, batteryStateValid) if (batteryStateValid) { onStylusUsed() } Loading Loading @@ -221,6 +241,37 @@ constructor( executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } } /** * Uses the input device battery state to track whether a current USI session is active. The * InputDevice battery state updates USI battery on USI stylus input, and removes the last-known * USI stylus battery presence after 1 hour of not detecting input. As SysUI and StylusManager * is persistently running, relies on tracking sessions via an in-memory isInUsiSession boolean. */ private fun trackAndLogUsiSession(deviceId: Int, batteryStateValid: Boolean) { // TODO(b/268618918) handle cases where an invalid battery callback from a previous stylus // is sent after the actual valid callback if (batteryStateValid && !isInUsiSession) { if (DEBUG) { Log.d( TAG, "USI battery newly present, entering new USI session. Device ID: $deviceId" ) } isInUsiSession = true uiEventLogger.log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED) } else if (!batteryStateValid && isInUsiSession) { if (DEBUG) { Log.d(TAG, "USI battery newly absent, exiting USI session Device ID: $deviceId") } isInUsiSession = false uiEventLogger.log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED) } } private fun isBatteryStateValid(batteryState: BatteryState): Boolean { return batteryState.isPresent && batteryState.capacity > 0.0f } private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } Loading Loading @@ -295,5 +346,6 @@ constructor( companion object { private val TAG = StylusManager::class.simpleName.orEmpty() private val DEBUG = false } } packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt +5 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,11 @@ enum class StylusUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "UIEvent for Toast shown when stylus stopped charging") STYLUS_STOPPED_CHARGING(1303), @UiEvent(doc = "UIEvent for bluetooth stylus connected") BLUETOOTH_STYLUS_CONNECTED(1304), @UiEvent(doc = "UIEvent for bluetooth stylus disconnected") BLUETOOTH_STYLUS_DISCONNECTED(1305); @UiEvent(doc = "UIEvent for bluetooth stylus disconnected") BLUETOOTH_STYLUS_DISCONNECTED(1305), @UiEvent(doc = "UIEvent for start of a USI session via battery presence") USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED(1306), @UiEvent(doc = "UIEvent for end of a USI session via battery absence") USI_STYLUS_BATTERY_PRESENCE_REMOVED(1307); override fun getId() = _id } packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +44 −0 Original line number Diff line number Diff line Loading @@ -96,6 +96,9 @@ class StylusManagerTest : SysuiTestCase() { whenever(stylusDevice.bluetoothAddress).thenReturn(null) whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) whenever(stylusDevice.batteryState).thenReturn(batteryState) whenever(batteryState.capacity).thenReturn(0.5f) whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) Loading Loading @@ -493,6 +496,47 @@ class StylusManagerTest : SysuiTestCase() { verify(stylusCallback, times(1)).onStylusFirstUsed() } @Test fun onBatteryStateChanged_batteryPresent_notInUsiSession_logsSessionStart() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, times(1)) .log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED) } @Test fun onBatteryStateChanged_batteryPresent_inUsiSession_doesNotLogSessionStart() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) clearInvocations(uiEventLogger) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, never()).log(any()) } @Test fun onBatteryStateChanged_batteryAbsent_notInUsiSession_doesNotLogSessionEnd() { whenever(batteryState.isPresent).thenReturn(false) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, never()).log(any()) } @Test fun onBatteryStateChanged_batteryAbsent_inUsiSession_logSessionEnd() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) whenever(batteryState.isPresent).thenReturn(false) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, times(1)).log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED) } @Test fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) Loading Loading
packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +54 −2 Original line number Diff line number Diff line Loading @@ -30,6 +30,9 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.shared.hardware.hasInputDevice import com.android.systemui.shared.hardware.isInternalStylusSource import com.android.systemui.statusbar.notification.collection.listbuilder.DEBUG import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject Loading Loading @@ -59,8 +62,10 @@ constructor( CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() // This variable should only be accessed on the handler // These variables should only be accessed on the handler private var hasStarted: Boolean = false private var isInUsiSession: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot Loading @@ -70,6 +75,10 @@ constructor( handler.post { if (hasStarted) return@post hasStarted = true isInUsiSession = inputManager.hasInputDevice { it.isInternalStylusSource && isBatteryStateValid(it.batteryState) } addExistingStylusToMap() inputManager.registerInputDeviceListener(this, handler) Loading Loading @@ -177,7 +186,18 @@ constructor( handler.post { if (!hasStarted) return@post if (batteryState.isPresent) { if (DEBUG) { Log.d( TAG, "onBatteryStateChanged for $deviceId. " + "batteryState present: ${batteryState.isPresent}, " + "capacity: ${batteryState.capacity}" ) } val batteryStateValid = isBatteryStateValid(batteryState) trackAndLogUsiSession(deviceId, batteryStateValid) if (batteryStateValid) { onStylusUsed() } Loading Loading @@ -221,6 +241,37 @@ constructor( executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } } /** * Uses the input device battery state to track whether a current USI session is active. The * InputDevice battery state updates USI battery on USI stylus input, and removes the last-known * USI stylus battery presence after 1 hour of not detecting input. As SysUI and StylusManager * is persistently running, relies on tracking sessions via an in-memory isInUsiSession boolean. */ private fun trackAndLogUsiSession(deviceId: Int, batteryStateValid: Boolean) { // TODO(b/268618918) handle cases where an invalid battery callback from a previous stylus // is sent after the actual valid callback if (batteryStateValid && !isInUsiSession) { if (DEBUG) { Log.d( TAG, "USI battery newly present, entering new USI session. Device ID: $deviceId" ) } isInUsiSession = true uiEventLogger.log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED) } else if (!batteryStateValid && isInUsiSession) { if (DEBUG) { Log.d(TAG, "USI battery newly absent, exiting USI session Device ID: $deviceId") } isInUsiSession = false uiEventLogger.log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED) } } private fun isBatteryStateValid(batteryState: BatteryState): Boolean { return batteryState.isPresent && batteryState.capacity > 0.0f } private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } Loading Loading @@ -295,5 +346,6 @@ constructor( companion object { private val TAG = StylusManager::class.simpleName.orEmpty() private val DEBUG = false } }
packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt +5 −1 Original line number Diff line number Diff line Loading @@ -31,7 +31,11 @@ enum class StylusUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "UIEvent for Toast shown when stylus stopped charging") STYLUS_STOPPED_CHARGING(1303), @UiEvent(doc = "UIEvent for bluetooth stylus connected") BLUETOOTH_STYLUS_CONNECTED(1304), @UiEvent(doc = "UIEvent for bluetooth stylus disconnected") BLUETOOTH_STYLUS_DISCONNECTED(1305); @UiEvent(doc = "UIEvent for bluetooth stylus disconnected") BLUETOOTH_STYLUS_DISCONNECTED(1305), @UiEvent(doc = "UIEvent for start of a USI session via battery presence") USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED(1306), @UiEvent(doc = "UIEvent for end of a USI session via battery absence") USI_STYLUS_BATTERY_PRESENCE_REMOVED(1307); override fun getId() = _id }
packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +44 −0 Original line number Diff line number Diff line Loading @@ -96,6 +96,9 @@ class StylusManagerTest : SysuiTestCase() { whenever(stylusDevice.bluetoothAddress).thenReturn(null) whenever(btStylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) whenever(stylusDevice.batteryState).thenReturn(batteryState) whenever(batteryState.capacity).thenReturn(0.5f) whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) Loading Loading @@ -493,6 +496,47 @@ class StylusManagerTest : SysuiTestCase() { verify(stylusCallback, times(1)).onStylusFirstUsed() } @Test fun onBatteryStateChanged_batteryPresent_notInUsiSession_logsSessionStart() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, times(1)) .log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED) } @Test fun onBatteryStateChanged_batteryPresent_inUsiSession_doesNotLogSessionStart() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) clearInvocations(uiEventLogger) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, never()).log(any()) } @Test fun onBatteryStateChanged_batteryAbsent_notInUsiSession_doesNotLogSessionEnd() { whenever(batteryState.isPresent).thenReturn(false) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, never()).log(any()) } @Test fun onBatteryStateChanged_batteryAbsent_inUsiSession_logSessionEnd() { whenever(batteryState.isPresent).thenReturn(true) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) whenever(batteryState.isPresent).thenReturn(false) stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) verify(uiEventLogger, times(1)).log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED) } @Test fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) Loading