diff options
10 files changed, 252 insertions, 355 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 8201bbe4dc47..e7d2ef10b4ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -30,8 +30,11 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.contextualEducationRepository import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.education.shared.model.EducationUiType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.fakeUserRepository @@ -42,10 +45,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -56,14 +62,19 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor + private val repository = kosmos.contextualEducationRepository private val touchpadRepository = kosmos.touchpadRepository private val keyboardRepository = kosmos.keyboardRepository + private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository private val userRepository = kosmos.fakeUserRepository + private val overviewProxyService = kosmos.mockOverviewProxyService private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor private val eduClock = kosmos.fakeEduClock private val minDurationForNextEdu = KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds + private val initialDelayElapsedDuration = + KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds @Before fun setup() { @@ -271,6 +282,131 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : assertThat(model?.lastShortcutTriggeredTime).isEqualTo(eduClock.instant()) } + @Test + fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() = + testScope.runTest { + assumeTrue(gestureType != ALL_APPS) + setUpForInitialDelayElapse() + touchpadRepository.setIsAnyTouchpadConnected(true) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + touchpadRepository.setIsAnyTouchpadConnected(false) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() = + testScope.runTest { + assumeTrue(gestureType == ALL_APPS) + setUpForInitialDelayElapse() + keyboardRepository.setIsAnyKeyboardConnected(true) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() = + testScope.runTest { + setUpForInitialDelayElapse() + keyboardRepository.setIsAnyKeyboardConnected(false) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataAddedOnUpdateShortcutTriggerTime() = + testScope.runTest { + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + assertThat(model?.lastShortcutTriggeredTime).isNull() + + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) + + assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeInitialDelay() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + // No offset to the clock to simulate update before initial delay + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = + testScope.runTest { + // No update to OOBE launch time to simulate no OOBE is launched yet + setUpForDeviceConnection() + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + val listener = getOverviewProxyListener() + listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + private suspend fun setUpForInitialDelayElapse() { + tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) + tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, eduClock.instant()) + eduClock.offset(initialDelayElapsedDuration) + } + + @After + fun clear() { + testScope.launch { tutorialSchedulerRepository.clearDataStore() } + } + private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { @@ -288,9 +424,15 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : runCurrent() } - private suspend fun setUpForDeviceConnection() { - contextualEduInteractor.updateKeyboardFirstConnectionTime() - contextualEduInteractor.updateTouchpadFirstConnectionTime() + private fun setUpForDeviceConnection() { + touchpadRepository.setIsAnyTouchpadConnected(true) + keyboardRepository.setIsAnyKeyboardConnected(true) + } + + private fun getOverviewProxyListener(): OverviewProxyListener { + val listenerCaptor = argumentCaptor<OverviewProxyListener>() + verify(overviewProxyService).addCallback(listenerCaptor.capture()) + return listenerCaptor.firstValue } companion object { 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 deleted file mode 100644 index 98e09474d5f2..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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.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.tutorial.data.repository.DeviceType -import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository -import com.android.systemui.keyboard.data.repository.keyboardRepository -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.android.systemui.touchpad.data.repository.touchpadRepository -import com.google.common.truth.Truth.assertThat -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val underTest = kosmos.keyboardTouchpadEduStatsInteractor - private val keyboardRepository = kosmos.keyboardRepository - private val touchpadRepository = kosmos.touchpadRepository - private val repository = kosmos.contextualEducationRepository - private val fakeClock = kosmos.fakeEduClock - private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository - private val initialDelayElapsedDuration = - KeyboardTouchpadEduStatsInteractorImpl.initialDelayDuration + 1.seconds - - @Test - fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() = - testScope.runTest { - setUpForInitialDelayElapse() - touchpadRepository.setIsAnyTouchpadConnected(true) - - 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() - touchpadRepository.setIsAnyTouchpadConnected(false) - - 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() - keyboardRepository.setIsAnyKeyboardConnected(true) - - 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() - keyboardRepository.setIsAnyKeyboardConnected(false) - - val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(ALL_APPS) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - @Test - fun dataAddedOnUpdateShortcutTriggerTime() = - testScope.runTest { - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - assertThat(model?.lastShortcutTriggeredTime).isNull() - underTest.updateShortcutTriggerTime(BACK) - assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) - } - - @Test - fun dataUpdatedOnIncrementSignalCountAfterInitialDelay() = - testScope.runTest { - setUpForDeviceConnection() - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, 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 dataUnchangedOnIncrementSignalCountBeforeInitialDelay() = - testScope.runTest { - setUpForDeviceConnection() - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, fakeClock.instant()) - - // No offset to the clock to simulate update before initial delay - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - @Test - fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = - testScope.runTest { - // No update to OOBE launch time to simulate no OOBE is launched yet - setUpForDeviceConnection() - - val model by collectLastValue(repository.readGestureEduModelFlow(BACK)) - val originalValue = model!!.signalCount - underTest.incrementSignalCount(BACK) - - assertThat(model?.signalCount).isEqualTo(originalValue) - } - - private suspend fun setUpForInitialDelayElapse() { - tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, fakeClock.instant()) - tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, fakeClock.instant()) - fakeClock.offset(initialDelayElapsedDuration) - } - - private fun setUpForDeviceConnection() { - touchpadRepository.setIsAnyTouchpadConnected(true) - keyboardRepository.setIsAnyKeyboardConnected(true) - } - - @After - fun clear() { - testScope.launch { tutorialSchedulerRepository.clearDataStore() } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index a5b22775f3d5..c6be0dd76a06 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -34,6 +34,7 @@ import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule; @@ -153,6 +154,7 @@ import javax.inject.Named; VolumeModule.class, WallpaperModule.class, ShortcutHelperModule.class, + ContextualEducationModule.class, }) public abstract class ReferenceSystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index b55108d6ab1d..450863fb53c9 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -63,7 +63,6 @@ import com.android.systemui.display.DisplayModule; import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; -import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; @@ -272,8 +271,7 @@ import javax.inject.Named; UserModule.class, UtilModule.class, NoteTaskModule.class, - WalletModule.class, - ContextualEducationModule.class + WalletModule.class }, subcomponents = { ComplicationComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index 7fa7da192ad0..abe0289baec8 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -18,15 +18,12 @@ package com.android.systemui.education.dagger import com.android.systemui.CoreStartable import com.android.systemui.Flags -import com.android.systemui.contextualeducation.GestureType import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.education.data.repository.UserContextualEducationRepository import com.android.systemui.education.domain.interactor.ContextualEducationInteractor import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl import com.android.systemui.education.ui.view.ContextualEduUiCoordinator import dagger.Binds import dagger.Lazy @@ -83,18 +80,6 @@ interface ContextualEducationModule { } @Provides - fun provideKeyboardTouchpadEduStatsInteractor( - implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl> - ): KeyboardTouchpadEduStatsInteractor { - return if (Flags.keyboardTouchpadContextualEducation()) { - implLazy.get() - } else { - // No-op implementation when the flag is disabled. - return NoOpKeyboardTouchpadEduStatsInteractor - } - } - - @Provides @IntoMap @ClassKey(KeyboardTouchpadEduInteractor::class) fun provideKeyboardTouchpadEduInteractor( @@ -124,12 +109,6 @@ interface ContextualEducationModule { } } -private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { - override fun incrementSignalCount(gestureType: GestureType) {} - - override fun updateShortcutTriggerTime(gestureType: GestureType) {} -} - private object NoOpCoreStartable : CoreStartable { override fun start() {} } 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 faee32694964..c17f3fb6dfe4 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 @@ -18,6 +18,9 @@ package com.android.systemui.education.domain.interactor import android.os.SystemProperties import com.android.systemui.CoreStartable +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +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.education.dagger.ContextualEducationModule.EduClock @@ -25,6 +28,13 @@ import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType 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 com.android.systemui.recents.OverviewProxyService +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Clock import javax.inject.Inject import kotlin.time.Duration @@ -33,9 +43,11 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch @@ -48,6 +60,8 @@ constructor( @Background private val backgroundScope: CoroutineScope, private val contextualEducationInteractor: ContextualEducationInteractor, private val userInputDeviceRepository: UserInputDeviceRepository, + private val tutorialRepository: TutorialSchedulerRepository, + private val overviewProxyService: OverviewProxyService, @EduClock private val clock: Clock, ) : CoreStartable { @@ -59,14 +73,16 @@ constructor( getDurationForConfig("persist.contextual_edu.usage_session_sec", 3.days) val minIntervalBetweenEdu = getDurationForConfig("persist.contextual_edu.edu_interval_sec", 7.days) + val initialDelayDuration = + getDurationForConfig("persist.contextual_edu.initial_delay_sec", 7.days) private fun getDurationForConfig( systemPropertyKey: String, - defaultDuration: Duration + defaultDuration: Duration, ): Duration = SystemProperties.getLong( systemPropertyKey, - /* defaultValue= */ defaultDuration.inWholeSeconds + /* defaultValue= */ defaultDuration.inWholeSeconds, ) .toDuration(DurationUnit.SECONDS) } @@ -74,6 +90,24 @@ constructor( private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) val educationTriggered = _educationTriggered.asStateFlow() + private val statsUpdateRequests: Flow<StatsUpdateRequest> = conflatedCallbackFlow { + val listener: OverviewProxyListener = + object : OverviewProxyListener { + override fun updateContextualEduStats( + isTrackpadGesture: Boolean, + gestureType: GestureType, + ) { + trySendWithFailureLogging( + StatsUpdateRequest(isTrackpadGesture, gestureType), + TAG, + ) + } + } + + overviewProxyService.addCallback(listener) + awaitClose { overviewProxyService.removeCallback(listener) } + } + @OptIn(ExperimentalCoroutinesApi::class) override fun start() { backgroundScope.launch { @@ -133,6 +167,16 @@ constructor( contextualEducationInteractor.updateShortcutTriggerTime(it) } } + + backgroundScope.launch { + statsUpdateRequests.collect { + if (it.isTrackpadGesture) { + contextualEducationInteractor.updateShortcutTriggerTime(it.gestureType) + } else { + incrementSignalCount(it.gestureType) + } + } + } } private fun isEducationNeeded(model: GestureEduModel): Boolean { @@ -160,4 +204,41 @@ constructor( private fun getEduType(model: GestureEduModel) = if (model.educationShownCount > 0) EducationUiType.Notification else EducationUiType.Toast + + private suspend fun incrementSignalCount(gestureType: GestureType) { + val targetDevice = getTargetDevice(gestureType) + if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) { + contextualEducationInteractor.incrementSignalCount(gestureType) + } + } + + private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean { + return when (deviceType) { + KEYBOARD -> userInputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected + TOUCHPAD -> userInputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected + } + } + + /** + * 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)) + } + + private data class StatsUpdateRequest( + val isTrackpadGesture: Boolean, + val gestureType: GestureType, + ) } 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 deleted file mode 100644 index 43e39cf08e01..000000000000 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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.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.days -import kotlin.time.DurationUnit -import kotlin.time.toDuration -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -/** - * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is - * for having a different implementation of interactor when the feature flag is off. - */ -interface KeyboardTouchpadEduStatsInteractor { - fun incrementSignalCount(gestureType: GestureType) - - fun updateShortcutTriggerTime(gestureType: GestureType) -} - -/** Allow update to education data related to keyboard/touchpad. */ -@SysUISingleton -class KeyboardTouchpadEduStatsInteractorImpl -@Inject -constructor( - @Background private val backgroundScope: CoroutineScope, - 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= */ 7.days.inWholeSeconds, - ) - .toDuration(DurationUnit.SECONDS) - } - - override fun incrementSignalCount(gestureType: GestureType) { - backgroundScope.launch { - val targetDevice = getTargetDevice(gestureType) - if (isTargetDeviceConnected(targetDevice) && hasInitialDelayElapsed(targetDevice)) { - contextualEducationInteractor.incrementSignalCount(gestureType) - } - } - } - - override fun updateShortcutTriggerTime(gestureType: GestureType) { - backgroundScope.launch { - contextualEducationInteractor.updateShortcutTriggerTime(gestureType) - } - } - - private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean { - return when (deviceType) { - KEYBOARD -> inputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected - TOUCHPAD -> inputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected - } - } - - /** - * 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/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 559c2637ed4f..ce9c441654bf 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -87,7 +87,6 @@ import com.android.systemui.contextualeducation.GestureType; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -160,8 +159,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final Provider<SceneInteractor> mSceneInteractor; private final Provider<ShadeInteractor> mShadeInteractor; - private final KeyboardTouchpadEduStatsInteractor mKeyboardTouchpadEduStatsInteractor; - private final Runnable mConnectionRunnable = () -> internalConnectToCurrentUser("runnable: startConnectionToCurrentUser"); private final ComponentName mRecentsComponentName; @@ -660,8 +657,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis AssistUtils assistUtils, DumpManager dumpManager, Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder, - BroadcastDispatcher broadcastDispatcher, - KeyboardTouchpadEduStatsInteractor keyboardTouchpadEduStatsInteractor + BroadcastDispatcher broadcastDispatcher ) { // b/241601880: This component should only be running for primary users or // secondaryUsers when visibleBackgroundUsers are supported. @@ -699,7 +695,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; mBroadcastDispatcher = broadcastDispatcher; - mKeyboardTouchpadEduStatsInteractor = keyboardTouchpadEduStatsInteractor; if (!KeyguardWmStateRefactor.isEnabled()) { mSysuiUnlockAnimationController = sysuiUnlockAnimationController; @@ -940,19 +935,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis return isEnabled() && !QuickStepContract.isLegacyMode(mNavBarMode); } - /** - * Updates contextual education stats when a gesture is triggered - * @param isTrackpadGesture indicates if the gesture is triggered by trackpad - * @param gestureType type of gesture triggered - */ - public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) { - if (isTrackpadGesture) { - mKeyboardTouchpadEduStatsInteractor.updateShortcutTriggerTime(gestureType); - } else { - mKeyboardTouchpadEduStatsInteractor.incrementSignalCount(gestureType); - } - } - public boolean isEnabled() { return mIsEnabled; } @@ -978,6 +960,17 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } + /** + * Updates contextual education stats when a gesture is triggered + * @param isTrackpadGesture indicates if the gesture is triggered by trackpad + * @param gestureType type of gesture triggered + */ + public void updateContextualEduStats(boolean isTrackpadGesture, GestureType gestureType) { + for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { + mConnectionCallbacks.get(i).updateContextualEduStats(isTrackpadGesture, gestureType); + } + } + private void notifyHomeRotationEnabled(boolean enabled) { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled); @@ -1207,6 +1200,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis /** Set override of home button long press duration, touch slop multiplier, and haptic. */ default void setOverrideHomeButtonLongPress( long override, float slopMultiplier, boolean haptic) {} + /** Updates contextual education stats when target gesture type is triggered. */ + default void updateContextualEduStats( + boolean isTrackpadGesture, GestureType gestureType) {} } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 4959224ead2d..3bfde68def50 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager @@ -122,9 +121,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { Optional<UnfoldTransitionProgressForwarder> @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock - private lateinit var keyboardTouchpadEduStatsInteractor: KeyboardTouchpadEduStatsInteractor - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -293,7 +289,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { dumpManager, unfoldTransitionProgressForwarder, broadcastDispatcher, - keyboardTouchpadEduStatsInteractor, ) } } 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 2d275f9e9691..3fd2503096b5 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 @@ -24,6 +24,7 @@ import com.android.systemui.keyboard.data.repository.keyboardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.recents.OverviewProxyService import com.android.systemui.touchpad.data.repository.touchpadRepository import com.android.systemui.user.data.repository.userRepository import org.mockito.kotlin.mock @@ -38,27 +39,13 @@ var Kosmos.keyboardTouchpadEduInteractor by testDispatcher, keyboardRepository, touchpadRepository, - userRepository + userRepository, ), - clock = fakeEduClock + tutorialSchedulerRepository, + mockOverviewProxyService, + clock = fakeEduClock, ) } +var Kosmos.mockOverviewProxyService by Kosmos.Fixture { mock<OverviewProxyService>() } var Kosmos.mockEduInputManager by Kosmos.Fixture { mock<InputManager>() } - -var Kosmos.keyboardTouchpadEduStatsInteractor by - Kosmos.Fixture { - KeyboardTouchpadEduStatsInteractorImpl( - backgroundScope = testScope.backgroundScope, - contextualEducationInteractor = contextualEducationInteractor, - inputDeviceRepository = - UserInputDeviceRepository( - testDispatcher, - keyboardRepository, - touchpadRepository, - userRepository - ), - tutorialSchedulerRepository, - fakeEduClock - ) - } |