Loading packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * 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) } } } packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +19 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -73,7 +74,7 @@ interface ContextualEducationModule { implLazy.get() } else { // No-op implementation when the flag is disabled. return NoOpCoreStartable return NoOpContextualEducationInteractor } } Loading @@ -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 { Loading @@ -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() {} } } packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +17 −0 Original line number Diff line number Diff line Loading @@ -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 /** Loading @@ -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) Loading packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt 0 → 100644 +72 −0 Original line number Diff line number Diff line /* * 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 } } packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * 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, } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt 0 → 100644 +78 −0 Original line number Diff line number Diff line /* * 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) } } }
packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +19 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -73,7 +74,7 @@ interface ContextualEducationModule { implLazy.get() } else { // No-op implementation when the flag is disabled. return NoOpCoreStartable return NoOpContextualEducationInteractor } } Loading @@ -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 { Loading @@ -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() {} } }
packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +17 −0 Original line number Diff line number Diff line Loading @@ -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 /** Loading @@ -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) Loading
packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt 0 → 100644 +72 −0 Original line number Diff line number Diff line /* * 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 } }
packages/SystemUI/src/com/android/systemui/education/shared/model/EducationInfo.kt 0 → 100644 +30 −0 Original line number Diff line number Diff line /* * 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, }