diff options
4 files changed, 253 insertions, 11 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt index cd0c58feebed..8b5f59457e6e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt @@ -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 dataUpdatedOnIncrementSignalCount() = + fun dataUnchangedOnIncrementSignalCountBeforeOobeLaunchInitialDelay() = testScope.runTest { - val model by - collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) + 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 dataUpdatedOnIncrementSignalCountAfterTouchpadConnectionInitialDelay() = + testScope.runTest { + 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))) + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 87eeebf333e9..9520f474f936 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt index 3223433568b9..7821f6940da4 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt @@ -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)) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index 811c6533c656..7ccacb66e124 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -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>() |