1 /*
<lambda>null2  * Copyright 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.ui.view
18 
19 import android.app.Dialog
20 import android.app.Notification
21 import android.app.NotificationManager
22 import android.content.applicationContext
23 import androidx.test.filters.SmallTest
24 import com.android.systemui.SysuiTestCase
25 import com.android.systemui.contextualeducation.GestureType
26 import com.android.systemui.contextualeducation.GestureType.ALL_APPS
27 import com.android.systemui.contextualeducation.GestureType.BACK
28 import com.android.systemui.contextualeducation.GestureType.HOME
29 import com.android.systemui.contextualeducation.GestureType.OVERVIEW
30 import com.android.systemui.education.data.repository.fakeEduClock
31 import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
32 import com.android.systemui.education.domain.interactor.contextualEducationInteractor
33 import com.android.systemui.education.domain.interactor.keyboardTouchpadEduInteractor
34 import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
35 import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
36 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity
37 import com.android.systemui.kosmos.applicationCoroutineScope
38 import com.android.systemui.kosmos.testScope
39 import com.android.systemui.res.R
40 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
41 import com.android.systemui.testKosmos
42 import com.google.common.truth.Truth.assertThat
43 import kotlin.time.Duration.Companion.seconds
44 import kotlinx.coroutines.launch
45 import kotlinx.coroutines.test.TestScope
46 import kotlinx.coroutines.test.advanceTimeBy
47 import kotlinx.coroutines.test.runCurrent
48 import kotlinx.coroutines.test.runTest
49 import org.junit.Before
50 import org.junit.Rule
51 import org.junit.Test
52 import org.junit.runner.RunWith
53 import org.mockito.ArgumentCaptor
54 import org.mockito.ArgumentMatchers.anyInt
55 import org.mockito.Mock
56 import org.mockito.junit.MockitoJUnit
57 import org.mockito.kotlin.any
58 import org.mockito.kotlin.mock
59 import org.mockito.kotlin.verify
60 import org.mockito.kotlin.whenever
61 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
62 import platform.test.runner.parameterized.Parameters
63 
64 @SmallTest
65 @RunWith(ParameterizedAndroidJunit4::class)
66 @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
67 class ContextualEduUiCoordinatorTest(private val gestureType: GestureType) : SysuiTestCase() {
68     private val kosmos = testKosmos()
69     private val testScope = kosmos.testScope
70     private val interactor = kosmos.contextualEducationInteractor
71     private val eduClock = kosmos.fakeEduClock
72     private val minDurationForNextEdu =
73         KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds
74     private lateinit var underTest: ContextualEduUiCoordinator
75     private lateinit var previousDialog: Dialog
76     @Mock private lateinit var dialog: Dialog
77     @Mock private lateinit var notificationManager: NotificationManager
78     @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
79     @get:Rule val mockitoRule = MockitoJUnit.rule()
80     private var toastContent = ""
81     private val timeoutMillis = 5000L
82 
83     @Before
84     fun setUp() {
85         testScope.launch {
86             interactor.updateKeyboardFirstConnectionTime()
87             interactor.updateTouchpadFirstConnectionTime()
88         }
89 
90         whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any()))
91             .thenReturn(timeoutMillis.toInt())
92 
93         val viewModel =
94             ContextualEduViewModel(
95                 kosmos.applicationContext.resources,
96                 kosmos.keyboardTouchpadEduInteractor,
97                 accessibilityManagerWrapper,
98             )
99 
100         underTest =
101             ContextualEduUiCoordinator(
102                 kosmos.applicationCoroutineScope,
103                 viewModel,
104                 kosmos.applicationContext,
105                 notificationManager,
106             ) { model ->
107                 toastContent = model.message
108                 previousDialog = dialog
109                 dialog = mock<Dialog>()
110                 dialog
111             }
112         underTest.start()
113         kosmos.keyboardTouchpadEduInteractor.start()
114     }
115 
116     @Test
117     fun showDialogOnNewEdu() =
118         testScope.runTest {
119             triggerEducation(gestureType)
120             verify(dialog).show()
121         }
122 
123     @Test
124     fun showNotificationOn2ndEdu() =
125         testScope.runTest {
126             triggerEducation(gestureType)
127             eduClock.offset(minDurationForNextEdu)
128             triggerEducation(gestureType)
129             verify(notificationManager).notifyAsUser(any(), anyInt(), any(), any())
130         }
131 
132     @Test
133     fun dismissDialogAfterTimeout() =
134         testScope.runTest {
135             triggerEducation(gestureType)
136             advanceTimeBy(timeoutMillis + 1)
137             verify(dialog).dismiss()
138         }
139 
140     @Test
141     fun dismissPreviousDialogOnNewDialog() =
142         testScope.runTest {
143             triggerEducation(BACK)
144             triggerEducation(HOME)
145             verify(previousDialog).dismiss()
146         }
147 
148     @Test
149     fun verifyEduToastContent() =
150         testScope.runTest {
151             triggerEducation(gestureType)
152 
153             val expectedContent =
154                 when (gestureType) {
155                     BACK -> R.string.back_edu_toast_content
156                     HOME -> R.string.home_edu_toast_content
157                     OVERVIEW -> R.string.overview_edu_toast_content
158                     ALL_APPS -> R.string.all_apps_edu_toast_content
159                 }
160 
161             assertThat(toastContent).isEqualTo(context.getString(expectedContent))
162         }
163 
164     @Test
165     fun verifyEduNotificationContent() =
166         testScope.runTest {
167             val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
168             triggerEducation(gestureType)
169 
170             eduClock.offset(minDurationForNextEdu)
171             triggerEducation(gestureType)
172 
173             verify(notificationManager)
174                 .notifyAsUser(any(), anyInt(), notificationCaptor.capture(), any())
175 
176             val expectedTitle =
177                 when (gestureType) {
178                     BACK -> R.string.back_edu_notification_title
179                     HOME -> R.string.home_edu_notification_title
180                     OVERVIEW -> R.string.overview_edu_notification_title
181                     ALL_APPS -> R.string.all_apps_edu_notification_title
182                 }
183 
184             val expectedContent =
185                 when (gestureType) {
186                     BACK -> R.string.back_edu_notification_content
187                     HOME -> R.string.home_edu_notification_content
188                     OVERVIEW -> R.string.overview_edu_notification_content
189                     ALL_APPS -> R.string.all_apps_edu_notification_content
190                 }
191 
192             val expectedTutorialClassName =
193                 when (gestureType) {
194                     OVERVIEW -> TUTORIAL_ACTION
195                     else -> KeyboardTouchpadTutorialActivity::class.qualifiedName
196                 }
197 
198             verifyNotificationContent(
199                 expectedTitle,
200                 expectedContent,
201                 expectedTutorialClassName,
202                 notificationCaptor.value,
203             )
204         }
205 
206     private fun verifyNotificationContent(
207         titleResId: Int,
208         contentResId: Int,
209         expectedTutorialClassName: String?,
210         notification: Notification,
211     ) {
212         val expectedContent = context.getString(contentResId)
213         val expectedTitle = context.getString(titleResId)
214         val actualContent = notification.getString(Notification.EXTRA_TEXT)
215         val actualTitle = notification.getString(Notification.EXTRA_TITLE)
216         assertThat(actualContent).isEqualTo(expectedContent)
217         assertThat(actualTitle).isEqualTo(expectedTitle)
218         val actualTutorialClassName =
219             notification.contentIntent.intent.component?.className
220                 ?: notification.contentIntent.intent.action
221         assertThat(actualTutorialClassName).isEqualTo(expectedTutorialClassName)
222     }
223 
224     private fun Notification.getString(key: String): String =
225         this.extras?.getCharSequence(key).toString()
226 
227     private suspend fun TestScope.triggerEducation(gestureType: GestureType) {
228         for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
229             interactor.incrementSignalCount(gestureType)
230         }
231         runCurrent()
232     }
233 
234     companion object {
235         @JvmStatic
236         @Parameters(name = "{0}")
237         fun getGestureTypes(): List<GestureType> {
238             return listOf(BACK, HOME, OVERVIEW, ALL_APPS)
239         }
240 
241         private const val TUTORIAL_ACTION: String = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
242     }
243 }
244