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

Commit 152e5b5f authored by helencheuk's avatar helencheuk
Browse files

[Contextual Edu] Check if device is connected before incrementing signal count

Only increment signal count when the target device is connected and passed the initial delay after setup.
e.g. As we will provide touchpad education for BACK gesture, only increment signal count when touchpad is connected and it is 72 hours after OOBE is launched or device is set up.

Bug: 362495235
Test: KeyboardTouchpadStatsInteractorTest
Flag: com.android.systemui.keyboard_touchpad_contextual_education
Change-Id: I8a40fc8386c0a3df729a1f6ac8f50255c2b311b8
parent 10bf2e16
Loading
Loading
Loading
Loading
+179 −6
Original line number Diff line number Diff line
@@ -19,16 +19,23 @@ package com.android.systemui.education.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.contextualeducation.GestureType.ALL_APPS
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.education.data.repository.contextualEducationRepository
import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.inputdevice.data.model.UserDeviceConnectionStatus
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -36,24 +43,190 @@ class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val underTest = kosmos.keyboardTouchpadEduStatsInteractor
    private val repository = kosmos.contextualEducationRepository
    private val fakeClock = kosmos.fakeEduClock
    private val initialDelayElapsedDuration =
        KeyboardTouchpadEduStatsInteractorImpl.initialDelayDuration + 1.seconds

    @Test
    fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
        testScope.runTest {
            setUpForInitialDelayElapse()
            whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser)
                .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0)))

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
        testScope.runTest {
            setUpForInitialDelayElapse()
            whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser)
                .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = false, userId = 0)))

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
        testScope.runTest {
            setUpForInitialDelayElapse()
            whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser)
                .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0)))

            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(ALL_APPS)

            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
        testScope.runTest {
            setUpForInitialDelayElapse()
            whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser)
                .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = false, userId = 0)))

            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(ALL_APPS)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataUpdatedOnIncrementSignalCountAfterOobeLaunchInitialDelay() =
        testScope.runTest {
            setUpForDeviceConnection()
            whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>()))
                .thenReturn(fakeClock.instant())
            fakeClock.offset(initialDelayElapsedDuration)

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountBeforeOobeLaunchInitialDelay() =
        testScope.runTest {
            setUpForDeviceConnection()
            whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>()))
                .thenReturn(fakeClock.instant())

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataUpdatedOnIncrementSignalCount() =
    fun dataUpdatedOnIncrementSignalCountAfterTouchpadConnectionInitialDelay() =
        testScope.runTest {
            val model by
                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
            setUpForDeviceConnection()
            repository.updateEduDeviceConnectionTime { model ->
                model.copy(touchpadFirstConnectionTime = fakeClock.instant())
            }
            fakeClock.offset(initialDelayElapsedDuration)

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountBeforeTouchpadConnectionInitialDelay() =
        testScope.runTest {
            setUpForDeviceConnection()
            repository.updateEduDeviceConnectionTime { model ->
                model.copy(touchpadFirstConnectionTime = fakeClock.instant())
            }

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataUpdatedOnIncrementSignalCountAfterKeyboardConnectionInitialDelay() =
        testScope.runTest {
            setUpForDeviceConnection()
            repository.updateEduDeviceConnectionTime { model ->
                model.copy(keyboardFirstConnectionTime = fakeClock.instant())
            }
            fakeClock.offset(initialDelayElapsedDuration)

            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(ALL_APPS)

            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountBeforeKeyboardConnectionInitialDelay() =
        testScope.runTest {
            setUpForDeviceConnection()
            repository.updateEduDeviceConnectionTime { model ->
                model.copy(keyboardFirstConnectionTime = fakeClock.instant())
            }

            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(ALL_APPS)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataUnchangedOnIncrementSignalCountWhenNoSetupTime() =
        testScope.runTest {
            whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser)
                .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0)))

            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            val originalValue = model!!.signalCount
            underTest.incrementSignalCount(BACK)

            assertThat(model?.signalCount).isEqualTo(originalValue)
        }

    @Test
    fun dataAddedOnUpdateShortcutTriggerTime() =
        testScope.runTest {
            val model by
                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
            assertThat(model?.lastShortcutTriggeredTime).isNull()
            underTest.updateShortcutTriggerTime(BACK)
            assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
        }

    private suspend fun setUpForInitialDelayElapse() {
        whenever(mockTutorialSchedulerRepository.launchTime(any<DeviceType>()))
            .thenReturn(fakeClock.instant())
        fakeClock.offset(initialDelayElapsedDuration)
    }

    private fun setUpForDeviceConnection() {
        whenever(mockUserInputDeviceRepository.isAnyTouchpadConnectedForUser)
            .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0)))
        whenever(mockUserInputDeviceRepository.isAnyKeyboardConnectedForUser)
            .thenReturn(flowOf(UserDeviceConnectionStatus(isConnected = true, userId = 0)))
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -87,6 +87,7 @@ constructor(
    }

    override fun start() {
        // Listen to back gesture model changes and trigger education if needed
        backgroundScope.launch {
            contextualEducationInteractor.backGestureModelFlow.collect {
                if (isUsageSessionExpired(it)) {
@@ -98,6 +99,7 @@ constructor(
            }
        }

        // Listen to touchpad connection changes and update the first connection time
        backgroundScope.launch {
            userInputDeviceRepository.isAnyTouchpadConnectedForUser.collect {
                if (
@@ -111,6 +113,7 @@ constructor(
            }
        }

        // Listen to keyboard connection changes and update the first connection time
        backgroundScope.launch {
            userInputDeviceRepository.isAnyKeyboardConnectedForUser.collect {
                if (
@@ -124,6 +127,7 @@ constructor(
            }
        }

        // Listen to keyboard shortcut triggered and update the last trigger time
        backgroundScope.launch {
            keyboardShortcutTriggered.collect {
                contextualEducationInteractor.updateShortcutTriggerTime(it)
+62 −4
Original line number Diff line number Diff line
@@ -16,11 +16,25 @@

package com.android.systemui.education.domain.interactor

import android.os.SystemProperties
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.contextualeducation.GestureType.ALL_APPS
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD
import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
import java.time.Clock
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

/**
@@ -39,12 +53,29 @@ class KeyboardTouchpadEduStatsInteractorImpl
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    private val contextualEducationInteractor: ContextualEducationInteractor
    private val contextualEducationInteractor: ContextualEducationInteractor,
    private val inputDeviceRepository: UserInputDeviceRepository,
    private val tutorialRepository: TutorialSchedulerRepository,
    @EduClock private val clock: Clock,
) : KeyboardTouchpadEduStatsInteractor {

    companion object {
        val initialDelayDuration: Duration
            get() =
                SystemProperties.getLong(
                        "persist.contextual_edu.initial_delay_sec",
                        /* defaultValue= */ 72.hours.inWholeSeconds
                    )
                    .toDuration(DurationUnit.SECONDS)
    }

    override fun incrementSignalCount(gestureType: GestureType) {
        // Todo: check if keyboard/touchpad is connected before update
        backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) }
        backgroundScope.launch {
            val targetDevice = getTargetDevice(gestureType)
            if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) {
                contextualEducationInteractor.incrementSignalCount(gestureType)
            }
        }
    }

    override fun updateShortcutTriggerTime(gestureType: GestureType) {
@@ -52,4 +83,31 @@ constructor(
            contextualEducationInteractor.updateShortcutTriggerTime(gestureType)
        }
    }

    private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean {
        if (deviceType == KEYBOARD) {
            return inputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected
        } else if (deviceType == TOUCHPAD) {
            return inputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected
        }
        return false
    }

    /**
     * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would
     * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps
     * gesture to its target education device.
     */
    private fun getTargetDevice(gestureType: GestureType) =
        when (gestureType) {
            ALL_APPS -> KEYBOARD
            else -> TOUCHPAD
        }

    private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean {
        val oobeLaunchTime = tutorialRepository.launchTime(deviceType) ?: return false
        return clock
            .instant()
            .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds))
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.education.domain.interactor
import android.hardware.input.InputManager
import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
import com.android.systemui.keyboard.data.repository.keyboardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
@@ -50,6 +51,12 @@ var Kosmos.keyboardTouchpadEduStatsInteractor by
    Kosmos.Fixture {
        KeyboardTouchpadEduStatsInteractorImpl(
            backgroundScope = testScope.backgroundScope,
            contextualEducationInteractor = contextualEducationInteractor
            contextualEducationInteractor = contextualEducationInteractor,
            inputDeviceRepository = mockUserInputDeviceRepository,
            tutorialRepository = mockTutorialSchedulerRepository,
            clock = fakeEduClock
        )
    }

var mockUserInputDeviceRepository = mock<UserInputDeviceRepository>()
var mockTutorialSchedulerRepository = mock<TutorialSchedulerRepository>()