From 99943d7d0bf4aefd50cf1e8fc6c97c039f3a72ae Mon Sep 17 00:00:00 2001 From: Vania Januar Date: Tue, 7 Feb 2023 16:50:48 +0000 Subject: [PATCH] UIEvent for tracking USI battery presence. Bug: 267816496 Test: StylusManagerTest Change-Id: I69edb7fc04f4af5ca1768aaf0001f62885bc71d3 --- .../android/systemui/stylus/StylusManager.kt | 56 ++++++++++++++++++- .../android/systemui/stylus/StylusUiEvent.kt | 6 +- .../systemui/stylus/StylusManagerTest.kt | 44 +++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 3f54aebf02639..dee4a6f5aa8cf 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -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 @@ -59,8 +62,10 @@ constructor( CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap = 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 @@ -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) @@ -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() } @@ -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) } @@ -295,5 +346,6 @@ constructor( companion object { private val TAG = StylusManager::class.simpleName.orEmpty() + private val DEBUG = false } } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt index 99da4ce9ab681..e77749b63797c 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUiEvent.kt @@ -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 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index 56203d9ab3ba3..6d7941f6f64f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -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) @@ -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)