Loading packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +10 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.domain.interactor.mockEduInputManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope Loading Loading @@ -62,7 +63,13 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { // Create TestContext here because TemporaryFolder.create() is called in @Before. It is // needed before calling TemporaryFolder.newFolder(). val testContext = TestContext(context, tmpFolder.newFolder()) underTest = UserContextualEducationRepository(testContext, dsScopeProvider) underTest = UserContextualEducationRepository( testContext, dsScopeProvider, kosmos.mockEduInputManager, kosmos.testDispatcher ) underTest.setUser(testUserId) } Loading Loading @@ -99,7 +106,8 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(), lastEducationTime = kosmos.fakeEduClock.instant(), usageSessionStartTime = kosmos.fakeEduClock.instant(), userId = testUserId userId = testUserId, gestureType = BACK ) underTest.updateGestureEduModel(BACK) { newModel } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) Loading packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +49 −46 Original line number Diff line number Diff line Loading @@ -17,15 +17,13 @@ package com.android.systemui.education.domain.interactor import android.content.pm.UserInfo import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import android.view.KeyEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.contextualeducation.GestureType.HOME import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.education.data.model.GestureEduModel Loading @@ -40,20 +38,21 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(ParameterizedAndroidJunit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor Loading @@ -71,21 +70,27 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { underTest.start() contextualEduInteractor.start() userRepository.setUserInfos(USER_INFOS) testScope.launch { contextualEduInteractor.updateKeyboardFirstConnectionTime() contextualEduInteractor.updateTouchpadFirstConnectionTime() } } @Test fun newEducationInfoOnMaxSignalCountReached() = testScope.runTest { triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) val model by collectLastValue(underTest.educationTriggered) assertThat(model?.gestureType).isEqualTo(BACK) assertThat(model?.gestureType).isEqualTo(gestureType) } @Test fun newEducationToastOn1stEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast) } Loading @@ -93,12 +98,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun newEducationNotificationOn2ndEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) // runCurrent() to trigger 1st education runCurrent() eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification) } Loading @@ -106,7 +111,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun noEducationInfoBeforeMaxSignalCountReached() = testScope.runTest { contextualEduInteractor.incrementSignalCount(BACK) contextualEduInteractor.incrementSignalCount(gestureType) val model by collectLastValue(underTest.educationTriggered) assertThat(model).isNull() } Loading @@ -115,8 +120,8 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun noEducationInfoWhenShortcutTriggeredPreviously() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) contextualEduInteractor.updateShortcutTriggerTime(BACK) triggerMaxEducationSignals(BACK) contextualEduInteractor.updateShortcutTriggerTime(gestureType) triggerMaxEducationSignals(gestureType) assertThat(model).isNull() } Loading @@ -124,12 +129,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun no2ndEducationBeforeMinEduIntervalReached() = testScope.runTest { val models by collectValues(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() // Offset a duration that is less than the required education interval eduClock.offset(1.seconds) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() assertThat(models.filterNotNull().size).isEqualTo(1) Loading @@ -140,15 +145,15 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { testScope.runTest { val models by collectValues(underTest.educationTriggered) // Trigger 2 educations triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() // Try triggering 3rd education eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(models.filterNotNull().size).isEqualTo(2) } Loading @@ -157,18 +162,21 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() = testScope.runTest { val model by collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) contextualEduInteractor.incrementSignalCount(BACK) collectLastValue( kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType) ) contextualEduInteractor.incrementSignalCount(gestureType) eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds)) val secondSignalReceivedTime = eduClock.instant() contextualEduInteractor.incrementSignalCount(BACK) contextualEduInteractor.incrementSignalCount(gestureType) assertThat(model) .isEqualTo( GestureEduModel( signalCount = 1, usageSessionStartTime = secondSignalReceivedTime, userId = 0 userId = 0, gestureType = gestureType ) ) } Loading Loading @@ -252,22 +260,9 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun updateShortcutTimeOnKeyboardShortcutTriggered() = testScope.runTest { // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the // interactor runCurrent() val listenerCaptor = ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java) verify(kosmos.mockEduInputManager) .registerKeyGestureEventListener(any(), listenerCaptor.capture()) val allAppsKeyGestureEvent = KeyGestureEvent.Builder() .setDeviceId(1) .setModifierState(KeyEvent.META_META_ON) .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent) // Only All Apps needs to update the keyboard shortcut assumeTrue(gestureType == ALL_APPS) kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS) val model by collectLastValue( Loading @@ -293,10 +288,18 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { runCurrent() } private suspend fun setUpForDeviceConnection() { contextualEduInteractor.updateKeyboardFirstConnectionTime() contextualEduInteractor.updateTouchpadFirstConnectionTime() } companion object { private val USER_INFOS = listOf( UserInfo(101, "Second User", 0), ) private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) @JvmStatic @Parameters(name = "{0}") fun getGestureTypes(): List<GestureType> { return listOf(BACK, HOME, OVERVIEW, ALL_APPS) } } } packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +6 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -69,6 +70,11 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { @Before fun setUp() { testScope.launch { interactor.updateKeyboardFirstConnectionTime() interactor.updateTouchpadFirstConnectionTime() } val viewModel = ContextualEduViewModel( kosmos.applicationContext.resources, Loading packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.education.data.model import com.android.systemui.contextualeducation.GestureType import java.time.Instant /** Loading @@ -23,6 +24,7 @@ import java.time.Instant * gesture stores its own model separately. */ data class GestureEduModel( val gestureType: GestureType, val signalCount: Int = 0, val educationShownCount: Int = 0, val lastShortcutTriggeredTime: Instant? = null, Loading packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +43 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.education.data.repository import android.content.Context import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.PreferenceDataStoreFactory Loading @@ -25,23 +27,31 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile 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.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Instant import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Provider import kotlin.properties.Delegates.notNull import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map /** Loading @@ -64,6 +74,8 @@ interface ContextualEducationRepository { suspend fun updateEduDeviceConnectionTime( transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime ) val keyboardShortcutTriggered: Flow<GestureType> } /** Loading @@ -75,9 +87,13 @@ class UserContextualEducationRepository @Inject constructor( @Application private val applicationContext: Context, @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope> @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>, private val inputManager: InputManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : ContextualEducationRepository { companion object { const val TAG = "UserContextualEducationRepository" const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" Loading @@ -98,6 +114,30 @@ constructor( @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data } override val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { val listener = InputManager.KeyGestureEventListener { event -> // Only store keyboard shortcut time for gestures providing keyboard // education val shortcutType = when (event.keyGestureType) { KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS else -> null } if (shortcutType != null) { trySendWithFailureLogging(shortcutType, TAG) } } inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } } .flowOn(backgroundDispatcher) override fun setUser(userId: Int) { dataStoreScope?.cancel() val newDsScope = dataStoreScopeProvider.get() Loading Loading @@ -136,7 +176,8 @@ constructor( preferences[getLastEducationTimeKey(gestureType)]?.let { Instant.ofEpochSecond(it) }, userId = userId userId = userId, gestureType = gestureType, ) } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +10 −2 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.domain.interactor.mockEduInputManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope Loading Loading @@ -62,7 +63,13 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { // Create TestContext here because TemporaryFolder.create() is called in @Before. It is // needed before calling TemporaryFolder.newFolder(). val testContext = TestContext(context, tmpFolder.newFolder()) underTest = UserContextualEducationRepository(testContext, dsScopeProvider) underTest = UserContextualEducationRepository( testContext, dsScopeProvider, kosmos.mockEduInputManager, kosmos.testDispatcher ) underTest.setUser(testUserId) } Loading Loading @@ -99,7 +106,8 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(), lastEducationTime = kosmos.fakeEduClock.instant(), usageSessionStartTime = kosmos.fakeEduClock.instant(), userId = testUserId userId = testUserId, gestureType = BACK ) underTest.updateGestureEduModel(BACK) { newModel } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +49 −46 Original line number Diff line number Diff line Loading @@ -17,15 +17,13 @@ package com.android.systemui.education.domain.interactor import android.content.pm.UserInfo import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import android.view.KeyEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.ALL_APPS import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.contextualeducation.GestureType.HOME import com.android.systemui.contextualeducation.GestureType.OVERVIEW import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.education.data.model.GestureEduModel Loading @@ -40,20 +38,21 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(ParameterizedAndroidJunit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor Loading @@ -71,21 +70,27 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { underTest.start() contextualEduInteractor.start() userRepository.setUserInfos(USER_INFOS) testScope.launch { contextualEduInteractor.updateKeyboardFirstConnectionTime() contextualEduInteractor.updateTouchpadFirstConnectionTime() } } @Test fun newEducationInfoOnMaxSignalCountReached() = testScope.runTest { triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) val model by collectLastValue(underTest.educationTriggered) assertThat(model?.gestureType).isEqualTo(BACK) assertThat(model?.gestureType).isEqualTo(gestureType) } @Test fun newEducationToastOn1stEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast) } Loading @@ -93,12 +98,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun newEducationNotificationOn2ndEducation() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) // runCurrent() to trigger 1st education runCurrent() eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification) } Loading @@ -106,7 +111,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun noEducationInfoBeforeMaxSignalCountReached() = testScope.runTest { contextualEduInteractor.incrementSignalCount(BACK) contextualEduInteractor.incrementSignalCount(gestureType) val model by collectLastValue(underTest.educationTriggered) assertThat(model).isNull() } Loading @@ -115,8 +120,8 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun noEducationInfoWhenShortcutTriggeredPreviously() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) contextualEduInteractor.updateShortcutTriggerTime(BACK) triggerMaxEducationSignals(BACK) contextualEduInteractor.updateShortcutTriggerTime(gestureType) triggerMaxEducationSignals(gestureType) assertThat(model).isNull() } Loading @@ -124,12 +129,12 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun no2ndEducationBeforeMinEduIntervalReached() = testScope.runTest { val models by collectValues(underTest.educationTriggered) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() // Offset a duration that is less than the required education interval eduClock.offset(1.seconds) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() assertThat(models.filterNotNull().size).isEqualTo(1) Loading @@ -140,15 +145,15 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { testScope.runTest { val models by collectValues(underTest.educationTriggered) // Trigger 2 educations triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) runCurrent() // Try triggering 3rd education eduClock.offset(minDurationForNextEdu) triggerMaxEducationSignals(BACK) triggerMaxEducationSignals(gestureType) assertThat(models.filterNotNull().size).isEqualTo(2) } Loading @@ -157,18 +162,21 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() = testScope.runTest { val model by collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) contextualEduInteractor.incrementSignalCount(BACK) collectLastValue( kosmos.contextualEducationRepository.readGestureEduModelFlow(gestureType) ) contextualEduInteractor.incrementSignalCount(gestureType) eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds)) val secondSignalReceivedTime = eduClock.instant() contextualEduInteractor.incrementSignalCount(BACK) contextualEduInteractor.incrementSignalCount(gestureType) assertThat(model) .isEqualTo( GestureEduModel( signalCount = 1, usageSessionStartTime = secondSignalReceivedTime, userId = 0 userId = 0, gestureType = gestureType ) ) } Loading Loading @@ -252,22 +260,9 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun updateShortcutTimeOnKeyboardShortcutTriggered() = testScope.runTest { // runCurrent() to trigger inputManager#registerKeyGestureEventListener in the // interactor runCurrent() val listenerCaptor = ArgumentCaptor.forClass(InputManager.KeyGestureEventListener::class.java) verify(kosmos.mockEduInputManager) .registerKeyGestureEventListener(any(), listenerCaptor.capture()) val allAppsKeyGestureEvent = KeyGestureEvent.Builder() .setDeviceId(1) .setModifierState(KeyEvent.META_META_ON) .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) .build() listenerCaptor.value.onKeyGestureEvent(allAppsKeyGestureEvent) // Only All Apps needs to update the keyboard shortcut assumeTrue(gestureType == ALL_APPS) kosmos.contextualEducationRepository.setKeyboardShortcutTriggered(ALL_APPS) val model by collectLastValue( Loading @@ -293,10 +288,18 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { runCurrent() } private suspend fun setUpForDeviceConnection() { contextualEduInteractor.updateKeyboardFirstConnectionTime() contextualEduInteractor.updateTouchpadFirstConnectionTime() } companion object { private val USER_INFOS = listOf( UserInfo(101, "Second User", 0), ) private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) @JvmStatic @Parameters(name = "{0}") fun getGestureTypes(): List<GestureType> { return listOf(BACK, HOME, OVERVIEW, ALL_APPS) } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +6 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -69,6 +70,11 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { @Before fun setUp() { testScope.launch { interactor.updateKeyboardFirstConnectionTime() interactor.updateTouchpadFirstConnectionTime() } val viewModel = ContextualEduViewModel( kosmos.applicationContext.resources, Loading
packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.education.data.model import com.android.systemui.contextualeducation.GestureType import java.time.Instant /** Loading @@ -23,6 +24,7 @@ import java.time.Instant * gesture stores its own model separately. */ data class GestureEduModel( val gestureType: GestureType, val signalCount: Int = 0, val educationShownCount: Int = 0, val lastShortcutTriggeredTime: Instant? = null, Loading
packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +43 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.systemui.education.data.repository import android.content.Context import android.hardware.input.InputManager import android.hardware.input.KeyGestureEvent import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.PreferenceDataStoreFactory Loading @@ -25,23 +27,31 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile 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.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope import com.android.systemui.education.data.model.EduDeviceConnectionTime import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.time.Instant import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Provider import kotlin.properties.Delegates.notNull import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map /** Loading @@ -64,6 +74,8 @@ interface ContextualEducationRepository { suspend fun updateEduDeviceConnectionTime( transform: (EduDeviceConnectionTime) -> EduDeviceConnectionTime ) val keyboardShortcutTriggered: Flow<GestureType> } /** Loading @@ -75,9 +87,13 @@ class UserContextualEducationRepository @Inject constructor( @Application private val applicationContext: Context, @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope> @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope>, private val inputManager: InputManager, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : ContextualEducationRepository { companion object { const val TAG = "UserContextualEducationRepository" const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" Loading @@ -98,6 +114,30 @@ constructor( @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data } override val keyboardShortcutTriggered: Flow<GestureType> = conflatedCallbackFlow { val listener = InputManager.KeyGestureEventListener { event -> // Only store keyboard shortcut time for gestures providing keyboard // education val shortcutType = when (event.keyGestureType) { KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS -> ALL_APPS else -> null } if (shortcutType != null) { trySendWithFailureLogging(shortcutType, TAG) } } inputManager.registerKeyGestureEventListener(Executor(Runnable::run), listener) awaitClose { inputManager.unregisterKeyGestureEventListener(listener) } } .flowOn(backgroundDispatcher) override fun setUser(userId: Int) { dataStoreScope?.cancel() val newDsScope = dataStoreScopeProvider.get() Loading Loading @@ -136,7 +176,8 @@ constructor( preferences[getLastEducationTimeKey(gestureType)]?.let { Instant.ofEpochSecond(it) }, userId = userId userId = userId, gestureType = gestureType, ) } Loading