diff options
8 files changed, 234 insertions, 4 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 new file mode 100644 index 000000000000..01dbc6bf396c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -0,0 +1,78 @@ +/* + * 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.coroutines.collectLastValue +import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.education.GestureType +import com.android.systemui.shared.education.GestureType.BACK_GESTURE +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.contextualEducationRepository + private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor + + @Before + fun setup() { + underTest.start() + } + + @Test + fun newEducationInfoOnMaxSignalCountReached() = + testScope.runTest { + tryTriggeringEducation(BACK_GESTURE) + val model by collectLastValue(underTest.educationTriggered) + assertThat(model?.gestureType).isEqualTo(BACK_GESTURE) + } + + @Test + fun noEducationInfoBeforeMaxSignalCountReached() = + testScope.runTest { + repository.incrementSignalCount(BACK_GESTURE) + val model by collectLastValue(underTest.educationTriggered) + assertThat(model).isNull() + } + + @Test + fun noEducationInfoWhenShortcutTriggeredPreviously() = + testScope.runTest { + val model by collectLastValue(underTest.educationTriggered) + repository.updateShortcutTriggerTime(BACK_GESTURE) + tryTriggeringEducation(BACK_GESTURE) + assertThat(model).isNull() + } + + private suspend fun tryTriggeringEducation(gestureType: GestureType) { + // Increment max number of signal to try triggering education + for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { + repository.incrementSignalCount(gestureType) + } + } +} 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 0e2e2e6277f2..b8019ab9ce0c 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl 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.shared.education.GestureType @@ -73,7 +74,7 @@ interface ContextualEducationModule { implLazy.get() } else { // No-op implementation when the flag is disabled. - return NoOpCoreStartable + return NoOpContextualEducationInteractor } } @@ -88,6 +89,18 @@ interface ContextualEducationModule { return NoOpKeyboardTouchpadEduStatsInteractor } } + + @Provides + fun provideKeyboardTouchpadEduInteractor( + implLazy: Lazy<KeyboardTouchpadEduInteractor> + ): CoreStartable { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpKeyboardTouchpadEduInteractor + } + } } private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { @@ -96,7 +109,11 @@ interface ContextualEducationModule { override fun updateShortcutTriggerTime(gestureType: GestureType) {} } - private object NoOpCoreStartable : CoreStartable { + private object NoOpContextualEducationInteractor : CoreStartable { + override fun start() {} + } + + private object NoOpKeyboardTouchpadEduInteractor : CoreStartable { override fun start() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt index e2aa9111246b..3036d970e985 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -19,12 +19,17 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.shared.education.GestureType import com.android.systemui.user.domain.interactor.SelectedUserInteractor import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch /** @@ -36,16 +41,28 @@ class ContextualEducationInteractor @Inject constructor( @Background private val backgroundScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, private val selectedUserInteractor: SelectedUserInteractor, private val repository: ContextualEducationRepository, ) : CoreStartable { + val backGestureModelFlow = readEduModelsOnSignalCountChanged(GestureType.BACK_GESTURE) + override fun start() { backgroundScope.launch { selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) } } } + private fun readEduModelsOnSignalCountChanged(gestureType: GestureType): Flow<GestureEduModel> { + return repository + .readGestureEduModelFlow(gestureType) + .distinctUntilChanged( + areEquivalent = { old, new -> old.signalCount == new.signalCount } + ) + .flowOn(backgroundDispatcher) + } + suspend fun incrementSignalCount(gestureType: GestureType) = repository.incrementSignalCount(gestureType) 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 new file mode 100644 index 000000000000..247abf1a7ecc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -0,0 +1,72 @@ +/* + * 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 com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +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.shared.education.GestureType.BACK_GESTURE +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +/** Allow listening to new contextual education triggered */ +@SysUISingleton +class KeyboardTouchpadEduInteractor +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val contextualEducationInteractor: ContextualEducationInteractor +) : CoreStartable { + + companion object { + const val MAX_SIGNAL_COUNT: Int = 2 + } + + private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) + val educationTriggered = _educationTriggered.asStateFlow() + + override fun start() { + backgroundScope.launch { + contextualEducationInteractor.backGestureModelFlow + .mapNotNull { getEduType(it) } + .collect { _educationTriggered.value = EducationInfo(BACK_GESTURE, it) } + } + } + + private fun getEduType(model: GestureEduModel): EducationUiType? { + if (isEducationNeeded(model)) { + return EducationUiType.Toast + } else { + return null + } + } + + private fun isEducationNeeded(model: GestureEduModel): Boolean { + // Todo: b/354884305 - add complete education logic to show education in correct scenarios + val shortcutWasTriggered = model.lastShortcutTriggeredTime == null + val signalCountReached = model.signalCount >= MAX_SIGNAL_COUNT + + return shortcutWasTriggered && signalCountReached + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt new file mode 100644 index 000000000000..85f4012ddbd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt @@ -0,0 +1,30 @@ +/* + * 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.shared.model + +import com.android.systemui.shared.education.GestureType + +/** + * Model for education triggered. [gestureType] indicates what gesture it is trying to educate about + * and [educationUiType] is how we educate user in the UI + */ +data class EducationInfo(val gestureType: GestureType, val educationUiType: EducationUiType) + +enum class EducationUiType { + Toast, + Notification, +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt index 5410882c9283..bade91a55534 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt @@ -28,11 +28,15 @@ class FakeContextualEducationRepository(private val clock: Clock) : ContextualEd private val userGestureMap = mutableMapOf<Int, GestureEduModel>() private val _gestureEduModels = MutableStateFlow(GestureEduModel()) private val gestureEduModelsFlow = _gestureEduModels.asStateFlow() + private var currentUser: Int = 0 override fun setUser(userId: Int) { if (!userGestureMap.contains(userId)) { userGestureMap[userId] = GestureEduModel() } + // save data of current user to the map + userGestureMap[currentUser] = _gestureEduModels.value + // switch to data of new user _gestureEduModels.value = userGestureMap[userId]!! } @@ -41,13 +45,15 @@ class FakeContextualEducationRepository(private val clock: Clock) : ContextualEd } override suspend fun incrementSignalCount(gestureType: GestureType) { + val originalModel = _gestureEduModels.value _gestureEduModels.value = - GestureEduModel( + originalModel.copy( signalCount = _gestureEduModels.value.signalCount + 1, ) } override suspend fun updateShortcutTriggerTime(gestureType: GestureType) { - _gestureEduModels.value = GestureEduModel(lastShortcutTriggeredTime = clock.instant()) + val originalModel = _gestureEduModels.value + _gestureEduModels.value = originalModel.copy(lastShortcutTriggeredTime = clock.instant()) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt index 5b2dc2b39e27..a7b322b5a86d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.education.data.repository.contextualEducationRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor @@ -25,6 +26,7 @@ val Kosmos.contextualEducationInteractor by Kosmos.Fixture { ContextualEducationInteractor( backgroundScope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, repository = contextualEducationRepository, selectedUserInteractor = selectedUserInteractor ) 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 8f84e0482b83..fb4e9012f79d 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,14 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +var Kosmos.keyboardTouchpadEduInteractor by + Kosmos.Fixture { + KeyboardTouchpadEduInteractor( + backgroundScope = testScope.backgroundScope, + contextualEducationInteractor = contextualEducationInteractor + ) + } + var Kosmos.keyboardTouchpadEduStatsInteractor by Kosmos.Fixture { KeyboardTouchpadEduStatsInteractorImpl( |