1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.education.domain.interactor 18 19 import androidx.test.ext.junit.runners.AndroidJUnit4 20 import androidx.test.filters.SmallTest 21 import com.android.systemui.SysuiTestCase 22 import com.android.systemui.contextualeducation.GestureType 23 import com.android.systemui.contextualeducation.GestureType.BACK 24 import com.android.systemui.contextualeducation.GestureType.HOME 25 import com.android.systemui.contextualeducation.GestureType.OVERVIEW 26 import com.android.systemui.coroutines.collectLastValue 27 import com.android.systemui.coroutines.collectValues 28 import com.android.systemui.education.data.repository.fakeEduClock 29 import com.android.systemui.education.shared.model.EducationInfo 30 import com.android.systemui.education.shared.model.EducationUiType 31 import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType 32 import com.android.systemui.inputdevice.tutorial.tutorialSchedulerRepository 33 import com.android.systemui.keyboard.data.repository.keyboardRepository 34 import com.android.systemui.kosmos.testScope 35 import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener 36 import com.android.systemui.testKosmos 37 import com.android.systemui.touchpad.data.repository.touchpadRepository 38 import com.google.common.truth.Truth.assertThat 39 import kotlin.time.Duration.Companion.seconds 40 import kotlinx.coroutines.flow.filterNotNull 41 import kotlinx.coroutines.launch 42 import kotlinx.coroutines.test.TestScope 43 import kotlinx.coroutines.test.runCurrent 44 import kotlinx.coroutines.test.runTest 45 import org.junit.Before 46 import org.junit.Test 47 import org.junit.runner.RunWith 48 import org.mockito.kotlin.argumentCaptor 49 import org.mockito.kotlin.verify 50 51 @SmallTest 52 @RunWith(AndroidJUnit4::class) 53 @kotlinx.coroutines.ExperimentalCoroutinesApi 54 class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { 55 private val kosmos = testKosmos() 56 private val testScope = kosmos.testScope 57 private val contextualEduInteractor = kosmos.contextualEducationInteractor 58 private val touchpadRepository = kosmos.touchpadRepository 59 private val keyboardRepository = kosmos.keyboardRepository 60 private val tutorialSchedulerRepository = kosmos.tutorialSchedulerRepository 61 private val overviewProxyService = kosmos.mockOverviewProxyService 62 63 private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor 64 private val eduClock = kosmos.fakeEduClock 65 private val initialDelayElapsedDuration = 66 KeyboardTouchpadEduInteractor.initialDelayDuration + 1.seconds 67 private val minIntervalForEduNotification = 68 KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds 69 70 @Before setupnull71 fun setup() { 72 underTest.start() 73 contextualEduInteractor.start() 74 testScope.launch { 75 contextualEduInteractor.updateKeyboardFirstConnectionTime() 76 contextualEduInteractor.updateTouchpadFirstConnectionTime() 77 } 78 } 79 80 @Test newEducationToastBeforeMaxToastsPerSessionTriggerednull81 fun newEducationToastBeforeMaxToastsPerSessionTriggered() = 82 testScope.runTest { 83 setUpForDeviceConnection() 84 setUpForInitialDelayElapse() 85 val model by collectLastValue(underTest.educationTriggered) 86 87 triggerEducation(HOME) 88 89 assertThat(model).isEqualTo(EducationInfo(HOME, EducationUiType.Toast, userId = 0)) 90 } 91 92 @Test noEducationToastAfterMaxToastsPerSessionTriggerednull93 fun noEducationToastAfterMaxToastsPerSessionTriggered() = 94 testScope.runTest { 95 setUpForDeviceConnection() 96 setUpForInitialDelayElapse() 97 val models by collectValues(underTest.educationTriggered.filterNotNull()) 98 // Show two toasts of other gestures 99 triggerEducation(HOME) 100 triggerEducation(BACK) 101 102 triggerEducation(OVERVIEW) 103 104 // No new toast education besides the 2 triggered at first 105 val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0) 106 val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0) 107 assertThat(models).containsExactly(firstEdu, secondEdu).inOrder() 108 } 109 110 @Test newEducationToastAfterMinIntervalElapsedWhenMaxToastsPerSessionTriggerednull111 fun newEducationToastAfterMinIntervalElapsedWhenMaxToastsPerSessionTriggered() = 112 testScope.runTest { 113 setUpForDeviceConnection() 114 setUpForInitialDelayElapse() 115 val models by collectValues(underTest.educationTriggered.filterNotNull()) 116 // Show two toasts of other gestures 117 triggerEducation(HOME) 118 triggerEducation(BACK) 119 120 // Trigger toast after an usage session has elapsed 121 eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration + 1.seconds) 122 triggerEducation(OVERVIEW) 123 124 val firstEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0) 125 val secondEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0) 126 val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0) 127 assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu).inOrder() 128 } 129 130 @Test newEducationNotificationAfterMaxToastsPerSessionTriggerednull131 fun newEducationNotificationAfterMaxToastsPerSessionTriggered() = 132 testScope.runTest { 133 setUpForDeviceConnection() 134 setUpForInitialDelayElapse() 135 val models by collectValues(underTest.educationTriggered.filterNotNull()) 136 triggerEducation(BACK) 137 138 // Offset to let min interval for notification elapse so we could show edu notification 139 // for BACK. It would be a new usage session too because the interval (7 days) is 140 // longer than a usage session (3 days) 141 eduClock.offset(minIntervalForEduNotification) 142 triggerEducation(HOME) 143 triggerEducation(OVERVIEW) 144 triggerEducation(BACK) 145 146 val firstEdu = EducationInfo(BACK, EducationUiType.Toast, userId = 0) 147 val secondEdu = EducationInfo(HOME, EducationUiType.Toast, userId = 0) 148 val thirdEdu = EducationInfo(OVERVIEW, EducationUiType.Toast, userId = 0) 149 val fourthEdu = EducationInfo(BACK, EducationUiType.Notification, userId = 0) 150 assertThat(models).containsExactly(firstEdu, secondEdu, thirdEdu, fourthEdu).inOrder() 151 } 152 setUpForInitialDelayElapsenull153 private suspend fun setUpForInitialDelayElapse() { 154 tutorialSchedulerRepository.updateLaunchTime(DeviceType.TOUCHPAD, eduClock.instant()) 155 tutorialSchedulerRepository.updateLaunchTime(DeviceType.KEYBOARD, eduClock.instant()) 156 eduClock.offset(initialDelayElapsedDuration) 157 } 158 setUpForDeviceConnectionnull159 private fun setUpForDeviceConnection() { 160 touchpadRepository.setIsAnyTouchpadConnected(true) 161 keyboardRepository.setIsAnyKeyboardConnected(true) 162 } 163 getOverviewProxyListenernull164 private fun getOverviewProxyListener(): OverviewProxyListener { 165 val listenerCaptor = argumentCaptor<OverviewProxyListener>() 166 verify(overviewProxyService).addCallback(listenerCaptor.capture()) 167 return listenerCaptor.firstValue 168 } 169 triggerEducationnull170 private fun TestScope.triggerEducation(gestureType: GestureType) { 171 // Increment max number of signal to try triggering education 172 for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { 173 val listener = getOverviewProxyListener() 174 listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) 175 } 176 runCurrent() 177 } 178 } 179