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