Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 19bbf038 authored by Vania Januar's avatar Vania Januar
Browse files

Log instanceId with stylus UIEvents.

For bluetooth styluses, an instance ID represents a single session where a single bluetooth stylus connects and disconnects. This distinguishes between multiple styluses. For USI styluses, the presence and then removal of any USI battery represents a single session/instance ID, and does not distinguish multiple styluses.

For USI stylus notifications, a unique instance ID is generated when a notification is first shown, and is reset when the same notification is cancelled, whether through user dismissal or e.g. connection of a bluetooth stylus, representing the 'lifetime' of the notification.

Bug: 267815315
Test: StylusUsiPowerUiTest, StylusManagerTest
Change-Id: Ie34143328a6a44a0086bec8ceb4f9502e8d4e81f
parent cf576eab
Loading
Loading
Loading
Loading
+52 −15
Original line number Diff line number Diff line
@@ -26,6 +26,9 @@ import android.os.Handler
import android.util.ArrayMap
import android.util.Log
import android.view.InputDevice
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -33,7 +36,6 @@ 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
@@ -61,12 +63,17 @@ constructor(
    private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList()
    private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> =
        CopyOnWriteArrayList()

    // This map should only be accessed on the handler
    private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap()
    private val inputDeviceBtSessionIdMap: MutableMap<Int, InstanceId> = ArrayMap()

    // These variables should only be accessed on the handler
    private var hasStarted: Boolean = false
    private var isInUsiSession: Boolean = false
    private var usiSessionId: InstanceId? = null

    @VisibleForTesting var instanceIdSequence = InstanceIdSequence(1 shl 13)

    /**
     * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot
@@ -120,7 +127,7 @@ constructor(

        if (btAddress != null) {
            onStylusUsed()
            onStylusBluetoothConnected(btAddress)
            onStylusBluetoothConnected(deviceId, btAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) }
        }
    }
@@ -136,12 +143,12 @@ constructor(
        inputDeviceAddressMap[deviceId] = currAddress

        if (prevAddress == null && currAddress != null) {
            onStylusBluetoothConnected(currAddress)
            onStylusBluetoothConnected(deviceId, currAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, currAddress) }
        }

        if (prevAddress != null && currAddress == null) {
            onStylusBluetoothDisconnected(prevAddress)
            onStylusBluetoothDisconnected(deviceId, prevAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, prevAddress) }
        }
    }
@@ -155,7 +162,7 @@ constructor(
        val btAddress: String? = inputDeviceAddressMap[deviceId]
        inputDeviceAddressMap.remove(deviceId)
        if (btAddress != null) {
            onStylusBluetoothDisconnected(btAddress)
            onStylusBluetoothDisconnected(deviceId, btAddress)
            executeStylusCallbacks { cb -> cb.onStylusBluetoothDisconnected(deviceId, btAddress) }
        }
        executeStylusCallbacks { cb -> cb.onStylusRemoved(deviceId) }
@@ -208,8 +215,8 @@ constructor(
        }
    }

    private fun onStylusBluetoothConnected(btAddress: String) {
        uiEventLogger.log(StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED)
    private fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {
        trackAndLogBluetoothSession(deviceId, true)
        val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
        try {
            bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
@@ -218,8 +225,8 @@ constructor(
        }
    }

    private fun onStylusBluetoothDisconnected(btAddress: String) {
        uiEventLogger.log(StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED)
    private fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {
        trackAndLogBluetoothSession(deviceId, false)
        val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
        try {
            bluetoothAdapter.removeOnMetadataChangedListener(device, this)
@@ -251,21 +258,51 @@ constructor(
    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 (batteryStateValid && usiSessionId == null) {
            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) {
            usiSessionId = instanceIdSequence.newInstanceId()
            uiEventLogger.logWithInstanceId(
                StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED,
                0,
                null,
                usiSessionId
            )
        } else if (!batteryStateValid && usiSessionId != null) {
            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)
            uiEventLogger.logWithInstanceId(
                StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED,
                0,
                null,
                usiSessionId
            )
            usiSessionId = null
        }
    }

    private fun trackAndLogBluetoothSession(deviceId: Int, bluetoothConnected: Boolean) {
        if (bluetoothConnected) {
            inputDeviceBtSessionIdMap[deviceId] = instanceIdSequence.newInstanceId()
            uiEventLogger.logWithInstanceId(
                StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED,
                0,
                null,
                inputDeviceBtSessionIdMap[deviceId]
            )
        } else {
            uiEventLogger.logWithInstanceId(
                StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED,
                0,
                null,
                inputDeviceBtSessionIdMap[deviceId]
            )
            inputDeviceBtSessionIdMap.remove(deviceId)
        }
    }

+20 −1
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
import com.android.internal.logging.UiEventLogger
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
@@ -62,6 +64,9 @@ constructor(
    private var batteryCapacity = 1.0f
    private var suppressed = false
    private var inputDeviceId: Int? = null
    private var instanceId: InstanceId? = null

    @VisibleForTesting var instanceIdSequence = InstanceIdSequence(1 shl 13)

    fun init() {
        val filter =
@@ -126,6 +131,7 @@ constructor(
    }

    private fun hideNotification() {
        instanceId = null
        notificationManager.cancel(USI_NOTIFICATION_ID)
    }

@@ -204,15 +210,28 @@ constructor(
            }
        }

    /**
     * Logs a stylus USI battery event with instance ID and battery level. The instance ID
     * represents the notification instance, and is reset when a notification is cancelled.
     */
    private fun logUiEvent(metricId: StylusUiEvent) {
        uiEventLogger.logWithPosition(
        uiEventLogger.logWithInstanceIdAndPosition(
            metricId,
            ActivityManager.getCurrentUser(),
            context.packageName,
            getInstanceId(),
            (batteryCapacity * 100.0).toInt()
        )
    }

    @VisibleForTesting
    fun getInstanceId(): InstanceId? {
        if (instanceId == null) {
            instanceId = instanceId ?: instanceIdSequence.newInstanceId()
        }
        return instanceId
    }

    companion object {
        // Low battery threshold matches CrOS, see:
        // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41
+42 −13
Original line number Diff line number Diff line
@@ -29,7 +29,9 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.ExtendedMockito.times
import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.internal.logging.InstanceId
import com.android.internal.logging.UiEventLogger
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
@@ -59,21 +61,16 @@ class StylusManagerTest : SysuiTestCase() {
    @Mock lateinit var bluetoothAdapter: BluetoothAdapter
    @Mock lateinit var bluetoothDevice: BluetoothDevice
    @Mock lateinit var handler: Handler

    @Mock lateinit var featureFlags: FeatureFlags

    @Mock lateinit var uiEventLogger: UiEventLogger

    @Mock lateinit var stylusCallback: StylusManager.StylusCallback

    @Mock lateinit var otherStylusCallback: StylusManager.StylusCallback

    @Mock lateinit var stylusBatteryCallback: StylusManager.StylusBatteryCallback

    @Mock lateinit var otherStylusBatteryCallback: StylusManager.StylusBatteryCallback

    private lateinit var mockitoSession: StaticMockitoSession
    private lateinit var stylusManager: StylusManager
    private val instanceIdSequenceFake = InstanceIdSequenceFake(10)

    @Before
    fun setUp() {
@@ -100,6 +97,8 @@ class StylusManagerTest : SysuiTestCase() {
                uiEventLogger
            )

        stylusManager.instanceIdSequence = instanceIdSequenceFake

        whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false)
        whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
        whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true)
@@ -403,7 +402,13 @@ class StylusManagerTest : SysuiTestCase() {
    fun onStylusBluetoothConnected_logsEvent() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)

        verify(uiEventLogger, times(1)).log(StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED)
        verify(uiEventLogger, times(1))
            .logWithInstanceId(
                StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED,
                0,
                null,
                InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId)
            )
    }

    @Test
@@ -416,12 +421,16 @@ class StylusManagerTest : SysuiTestCase() {
    }

    @Test
    fun onStylusBluetoothDisconnected_logsEvent() {
    fun onStylusBluetoothDisconnected_logsEventInSameSession() {
        stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID)
        val instanceId = InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId)

        stylusManager.onInputDeviceRemoved(BT_STYLUS_DEVICE_ID)

        verify(uiEventLogger, times(1)).log(StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED)
        verify(uiEventLogger, times(1))
            .logWithInstanceId(StylusUiEvent.BLUETOOTH_STYLUS_CONNECTED, 0, null, instanceId)
        verify(uiEventLogger, times(1))
            .logWithInstanceId(StylusUiEvent.BLUETOOTH_STYLUS_DISCONNECTED, 0, null, instanceId)
    }

    @Test
@@ -519,7 +528,12 @@ class StylusManagerTest : SysuiTestCase() {
        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)

        verify(uiEventLogger, times(1))
            .log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED)
            .logWithInstanceId(
                StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED,
                0,
                null,
                InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId)
            )
    }

    @Test
@@ -530,7 +544,7 @@ class StylusManagerTest : SysuiTestCase() {

        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)

        verify(uiEventLogger, never()).log(any())
        verifyZeroInteractions(uiEventLogger)
    }

    @Test
@@ -539,18 +553,33 @@ class StylusManagerTest : SysuiTestCase() {

        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)

        verify(uiEventLogger, never()).log(any())
        verifyZeroInteractions(uiEventLogger)
    }

    @Test
    fun onBatteryStateChanged_batteryAbsent_inUsiSession_logSessionEnd() {
        whenever(batteryState.isPresent).thenReturn(true)
        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)
        val instanceId = InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId)
        whenever(batteryState.isPresent).thenReturn(false)

        stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState)

        verify(uiEventLogger, times(1)).log(StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED)
        verify(uiEventLogger, times(1))
            .logWithInstanceId(
                StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_FIRST_DETECTED,
                0,
                null,
                instanceId
            )

        verify(uiEventLogger, times(1))
            .logWithInstanceId(
                StylusUiEvent.USI_STYLUS_BATTERY_PRESENCE_REMOVED,
                0,
                null,
                instanceId
            )
    }

    @Test
+12 −4
Original line number Diff line number Diff line
@@ -28,7 +28,9 @@ import android.testing.AndroidTestingRunner
import android.view.InputDevice
import androidx.core.app.NotificationManagerCompat
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.internal.logging.UiEventLogger
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
@@ -64,13 +66,14 @@ class StylusUsiPowerUiTest : SysuiTestCase() {
    @Mock lateinit var btStylusDevice: InputDevice

    @Mock lateinit var uiEventLogger: UiEventLogger

    @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification>

    private lateinit var stylusUsiPowerUi: StylusUsiPowerUI
    private lateinit var broadcastReceiver: BroadcastReceiver
    private lateinit var contextSpy: Context

    private val instanceIdSequenceFake = InstanceIdSequenceFake(10)

    private val uid = ActivityManager.getCurrentUser()

    @Before
@@ -92,6 +95,8 @@ class StylusUsiPowerUiTest : SysuiTestCase() {

        stylusUsiPowerUi =
            StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler, uiEventLogger)
        stylusUsiPowerUi.instanceIdSequence = instanceIdSequenceFake

        broadcastReceiver = stylusUsiPowerUi.receiver
    }

@@ -212,10 +217,11 @@ class StylusUsiPowerUiTest : SysuiTestCase() {
        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))

        verify(uiEventLogger, times(1))
            .logWithPosition(
            .logWithInstanceIdAndPosition(
                StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_SHOWN,
                uid,
                contextSpy.packageName,
                InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId),
                10
            )
    }
@@ -250,10 +256,11 @@ class StylusUsiPowerUiTest : SysuiTestCase() {
        broadcastReceiver.onReceive(contextSpy, intent)

        verify(uiEventLogger, times(1))
            .logWithPosition(
            .logWithInstanceIdAndPosition(
                StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_CLICKED,
                uid,
                contextSpy.packageName,
                InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId),
                100
            )
    }
@@ -264,10 +271,11 @@ class StylusUsiPowerUiTest : SysuiTestCase() {
        broadcastReceiver.onReceive(contextSpy, intent)

        verify(uiEventLogger, times(1))
            .logWithPosition(
            .logWithInstanceIdAndPosition(
                StylusUiEvent.STYLUS_LOW_BATTERY_NOTIFICATION_DISMISSED,
                uid,
                contextSpy.packageName,
                InstanceId.fakeInstanceId(instanceIdSequenceFake.lastInstanceId),
                100
            )
    }