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