1 /* 2 * Copyright (C) 2022 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.qs.footer.ui.viewmodel 18 19 import android.graphics.drawable.Drawable 20 import android.os.UserManager 21 import android.testing.TestableLooper 22 import android.testing.TestableLooper.RunWithLooper 23 import android.view.ContextThemeWrapper 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SmallTest 26 import com.android.settingslib.Utils 27 import com.android.settingslib.drawable.UserIconDrawable 28 import com.android.systemui.SysuiTestCase 29 import com.android.systemui.broadcast.BroadcastDispatcher 30 import com.android.systemui.common.shared.model.ContentDescription 31 import com.android.systemui.common.shared.model.Icon 32 import com.android.systemui.coroutines.collectLastValue 33 import com.android.systemui.qs.FakeFgsManagerController 34 import com.android.systemui.qs.QSSecurityFooterUtils 35 import com.android.systemui.qs.footer.FooterActionsTestUtils 36 import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig 37 import com.android.systemui.res.R 38 import com.android.systemui.security.data.model.SecurityModel 39 import com.android.systemui.statusbar.policy.FakeSecurityController 40 import com.android.systemui.statusbar.policy.FakeUserInfoController 41 import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo 42 import com.android.systemui.statusbar.policy.MockUserSwitcherControllerWrapper 43 import com.android.systemui.util.mockito.any 44 import com.android.systemui.util.mockito.mock 45 import com.android.systemui.util.mockito.nullable 46 import com.android.systemui.util.settings.FakeGlobalSettings 47 import com.google.common.truth.Truth.assertThat 48 import kotlinx.coroutines.flow.flowOf 49 import kotlinx.coroutines.launch 50 import kotlinx.coroutines.test.StandardTestDispatcher 51 import kotlinx.coroutines.test.TestScope 52 import kotlinx.coroutines.test.advanceUntilIdle 53 import kotlinx.coroutines.test.runTest 54 import org.junit.Before 55 import org.junit.Test 56 import org.junit.runner.RunWith 57 import org.mockito.Mockito.anyInt 58 import org.mockito.Mockito.`when` as whenever 59 60 @SmallTest 61 @RunWith(AndroidJUnit4::class) 62 @RunWithLooper 63 class FooterActionsViewModelTest : SysuiTestCase() { 64 private val testDispatcher = StandardTestDispatcher() 65 private val testScope = TestScope(testDispatcher) 66 private lateinit var utils: FooterActionsTestUtils 67 68 private val themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) 69 70 @Before setUpnull71 fun setUp() { 72 utils = FooterActionsTestUtils(context, TestableLooper.get(this), testScope.testScheduler) 73 } 74 runTestnull75 private fun runTest(block: suspend TestScope.() -> Unit) { 76 testScope.runTest(testBody = block) 77 } 78 79 @Test <lambda>null80 fun settingsButton() = runTest { 81 val underTest = utils.footerActionsViewModel(showPowerButton = false) 82 val settings = underTest.settings 83 84 assertThat(settings.icon) 85 .isEqualTo( 86 Icon.Resource( 87 R.drawable.ic_settings, 88 ContentDescription.Resource(R.string.accessibility_quick_settings_settings) 89 ) 90 ) 91 assertThat(settings.backgroundColor).isEqualTo(R.attr.shadeInactive) 92 assertThat(settings.iconTint) 93 .isEqualTo( 94 Utils.getColorAttrDefaultColor( 95 themedContext, 96 R.attr.onShadeInactiveVariant, 97 ) 98 ) 99 } 100 101 @Test <lambda>null102 fun powerButton() = runTest { 103 // Without power button. 104 val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false) 105 assertThat(underTestWithoutPower.power).isNull() 106 107 // With power button. 108 val underTestWithPower = utils.footerActionsViewModel(showPowerButton = true) 109 val power = underTestWithPower.power 110 assertThat(power).isNotNull() 111 assertThat(power!!.icon) 112 .isEqualTo( 113 Icon.Resource( 114 android.R.drawable.ic_lock_power_off, 115 ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu) 116 ) 117 ) 118 assertThat(power.backgroundColor).isEqualTo(R.attr.shadeActive) 119 assertThat(power.iconTint) 120 .isEqualTo( 121 Utils.getColorAttrDefaultColor( 122 themedContext, 123 R.attr.onShadeActive, 124 ), 125 ) 126 } 127 128 @Test <lambda>null129 fun userSwitcher() = runTest { 130 val picture: Drawable = mock() 131 val userInfoController = FakeUserInfoController(FakeInfo(picture = picture)) 132 val settings = FakeGlobalSettings(testDispatcher) 133 val userId = 42 134 val userSwitcherControllerWrapper = 135 MockUserSwitcherControllerWrapper(currentUserName = "foo") 136 137 // Mock UserManager. 138 val userManager = mock<UserManager>() 139 var isUserSwitcherEnabled = false 140 var isGuestUser = false 141 whenever(userManager.isUserSwitcherEnabled(any())).thenAnswer { isUserSwitcherEnabled } 142 whenever(userManager.isGuestUser(any())).thenAnswer { isGuestUser } 143 144 val underTest = 145 utils.footerActionsViewModel( 146 showPowerButton = false, 147 footerActionsInteractor = 148 utils.footerActionsInteractor( 149 userSwitcherRepository = 150 utils.userSwitcherRepository( 151 settings = settings, 152 userManager = userManager, 153 userInfoController = userInfoController, 154 userSwitcherController = userSwitcherControllerWrapper.controller, 155 ), 156 ) 157 ) 158 159 // Collect the user switcher into currentUserSwitcher. 160 val currentUserSwitcher = collectLastValue(underTest.userSwitcher) 161 162 // The user switcher is disabled. 163 assertThat(currentUserSwitcher()).isNull() 164 165 // Make the user manager return that the User Switcher is enabled. A change of the setting 166 // for the current user will be fired to notify us of that change. 167 isUserSwitcherEnabled = true 168 169 // Update the setting for the observed user: now we will be notified and the button should 170 // be there. 171 utils.setUserSwitcherEnabled(settings, true) 172 val userSwitcher = currentUserSwitcher() 173 assertThat(userSwitcher).isNotNull() 174 assertThat(userSwitcher!!.icon) 175 .isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo"))) 176 assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.shadeInactive) 177 178 // Change the current user name. 179 userSwitcherControllerWrapper.currentUserName = "bar" 180 assertThat(currentUserSwitcher()?.icon?.contentDescription) 181 .isEqualTo(ContentDescription.Loaded("Signed in as bar")) 182 183 fun iconTint(): Int? = currentUserSwitcher()!!.iconTint 184 185 // We tint the icon if the current user is not the guest. 186 assertThat(iconTint()).isNull() 187 188 // Make the UserManager return that the current user is the guest. A change of the user 189 // info will be fired to notify us of that change. 190 isGuestUser = true 191 192 // At this point, there was no change of the user info yet so we still didn't pick the 193 // UserManager change. 194 assertThat(iconTint()).isNull() 195 196 // Make sure we don't tint the icon if it is a user image (and not the default image), even 197 // in guest mode. 198 userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() } 199 assertThat(iconTint()).isNull() 200 } 201 202 @Test <lambda>null203 fun security() = runTest { 204 val securityController = FakeSecurityController() 205 val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() 206 207 // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the 208 // logic in securityToConfig. 209 var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } 210 whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { 211 securityToConfig(it.arguments.first() as SecurityModel) 212 } 213 214 val underTest = 215 utils.footerActionsViewModel( 216 footerActionsInteractor = 217 utils.footerActionsInteractor( 218 qsSecurityFooterUtils = qsSecurityFooterUtils, 219 securityRepository = 220 utils.securityRepository( 221 securityController = securityController, 222 ), 223 ), 224 ) 225 226 // Collect the security model into currentSecurity. 227 val currentSecurity = collectLastValue(underTest.security) 228 229 // By default, we always return a null SecurityButtonConfig. 230 assertThat(currentSecurity()).isNull() 231 232 // Map any SecurityModel into a non-null SecurityButtonConfig. 233 val buttonConfig = 234 SecurityButtonConfig( 235 icon = Icon.Resource(res = 0, contentDescription = null), 236 text = "foo", 237 isClickable = true, 238 ) 239 securityToConfig = { buttonConfig } 240 241 // There was no change of the security info yet, so the mapper was not called yet. 242 assertThat(currentSecurity()).isNull() 243 244 // Trigger a SecurityModel change, which will call the mapper and add a button. 245 securityController.updateState {} 246 var security = currentSecurity() 247 assertThat(security).isNotNull() 248 assertThat(security!!.icon).isEqualTo(buttonConfig.icon) 249 assertThat(security.text).isEqualTo(buttonConfig.text) 250 assertThat(security.onClick).isNotNull() 251 252 // If the config.clickable = false, then onClick should be null. 253 securityToConfig = { buttonConfig.copy(isClickable = false) } 254 securityController.updateState {} 255 security = currentSecurity() 256 assertThat(security).isNotNull() 257 assertThat(security!!.onClick).isNull() 258 } 259 260 @Test <lambda>null261 fun foregroundServices() = runTest { 262 val securityController = FakeSecurityController() 263 val fgsManagerController = 264 FakeFgsManagerController( 265 showFooterDot = false, 266 numRunningPackages = 0, 267 ) 268 val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() 269 270 // Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the 271 // logic in securityToConfig. 272 var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null } 273 whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer { 274 securityToConfig(it.arguments.first() as SecurityModel) 275 } 276 277 val underTest = 278 utils.footerActionsViewModel( 279 footerActionsInteractor = 280 utils.footerActionsInteractor( 281 qsSecurityFooterUtils = qsSecurityFooterUtils, 282 securityRepository = 283 utils.securityRepository( 284 securityController, 285 ), 286 foregroundServicesRepository = 287 utils.foregroundServicesRepository(fgsManagerController), 288 ), 289 ) 290 291 // Collect the security model into currentSecurity. 292 val currentForegroundServices = collectLastValue(underTest.foregroundServices) 293 294 // We don't show the foreground services button if the number of running packages is not 295 // > 1. 296 assertThat(currentForegroundServices()).isNull() 297 298 // We show it at soon as the number of services is at least 1. Given that there is no 299 // security, it should be displayed with text. 300 fgsManagerController.numRunningPackages = 1 301 val foregroundServices = currentForegroundServices() 302 assertThat(foregroundServices).isNotNull() 303 assertThat(foregroundServices!!.foregroundServicesCount).isEqualTo(1) 304 assertThat(foregroundServices.text).isEqualTo("1 app is active") 305 assertThat(foregroundServices.displayText).isTrue() 306 assertThat(foregroundServices.onClick).isNotNull() 307 308 // We handle plurals correctly. 309 fgsManagerController.numRunningPackages = 3 310 assertThat(currentForegroundServices()?.text).isEqualTo("3 apps are active") 311 312 // Showing new changes (the footer dot) is currently disabled. 313 assertThat(foregroundServices.hasNewChanges).isFalse() 314 315 // Enabling it will show the new changes. 316 fgsManagerController.showFooterDot.value = true 317 assertThat(currentForegroundServices()?.hasNewChanges).isTrue() 318 319 // Dismissing the dialog should remove the new changes dot. 320 fgsManagerController.simulateDialogDismiss() 321 assertThat(currentForegroundServices()?.hasNewChanges).isFalse() 322 323 // Showing the security button will make this show as a simple button without text. 324 assertThat(foregroundServices.displayText).isTrue() 325 securityToConfig = { 326 SecurityButtonConfig( 327 icon = Icon.Resource(res = 0, contentDescription = null), 328 text = "foo", 329 isClickable = true, 330 ) 331 } 332 securityController.updateState {} 333 assertThat(currentForegroundServices()?.displayText).isFalse() 334 } 335 336 @Test <lambda>null337 fun observeDeviceMonitoringDialogRequests() = runTest { 338 val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>() 339 val broadcastDispatcher = mock<BroadcastDispatcher>() 340 341 // Return a fake broadcastFlow that emits 3 fake events when collected. 342 val broadcastFlow = flowOf(Unit, Unit, Unit) 343 whenever( 344 broadcastDispatcher.broadcastFlow( 345 any(), 346 nullable(), 347 anyInt(), 348 nullable(), 349 ) 350 ) 351 .thenAnswer { broadcastFlow } 352 353 // Increment nDialogRequests whenever a request to show the dialog is made by the 354 // FooterActionsInteractor. 355 var nDialogRequests = 0 356 whenever(qsSecurityFooterUtils.showDeviceMonitoringDialog(any(), nullable())).then { 357 nDialogRequests++ 358 } 359 360 val underTest = 361 utils.footerActionsViewModel( 362 footerActionsInteractor = 363 utils.footerActionsInteractor( 364 qsSecurityFooterUtils = qsSecurityFooterUtils, 365 broadcastDispatcher = broadcastDispatcher, 366 ), 367 ) 368 369 val job = launch { underTest.observeDeviceMonitoringDialogRequests(mock()) } 370 371 advanceUntilIdle() 372 assertThat(nDialogRequests).isEqualTo(3) 373 374 job.cancel() 375 } 376 377 @Test alpha_inSplitShade_followsExpansionnull378 fun alpha_inSplitShade_followsExpansion() { 379 val underTest = utils.footerActionsViewModel() 380 381 underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true) 382 assertThat(underTest.alpha.value).isEqualTo(0f) 383 384 underTest.onQuickSettingsExpansionChanged(0.25f, isInSplitShade = true) 385 assertThat(underTest.alpha.value).isEqualTo(0.25f) 386 387 underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = true) 388 assertThat(underTest.alpha.value).isEqualTo(0.5f) 389 390 underTest.onQuickSettingsExpansionChanged(0.75f, isInSplitShade = true) 391 assertThat(underTest.alpha.value).isEqualTo(0.75f) 392 393 underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true) 394 assertThat(underTest.alpha.value).isEqualTo(1f) 395 } 396 397 @Test backgroundAlpha_inSplitShade_followsExpansion_with_0_15_delaynull398 fun backgroundAlpha_inSplitShade_followsExpansion_with_0_15_delay() { 399 val underTest = utils.footerActionsViewModel() 400 val floatTolerance = 0.01f 401 402 underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true) 403 assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) 404 405 underTest.onQuickSettingsExpansionChanged(0.1f, isInSplitShade = true) 406 assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) 407 408 underTest.onQuickSettingsExpansionChanged(0.14f, isInSplitShade = true) 409 assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) 410 411 underTest.onQuickSettingsExpansionChanged(0.235f, isInSplitShade = true) 412 assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.1f) 413 414 underTest.onQuickSettingsExpansionChanged(0.575f, isInSplitShade = true) 415 assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.5f) 416 417 underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true) 418 assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) 419 } 420 421 @Test alpha_inSingleShade_followsExpansion_with_0_9_delaynull422 fun alpha_inSingleShade_followsExpansion_with_0_9_delay() { 423 val underTest = utils.footerActionsViewModel() 424 val floatTolerance = 0.01f 425 426 underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = false) 427 assertThat(underTest.alpha.value).isEqualTo(0f) 428 429 underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = false) 430 assertThat(underTest.alpha.value).isEqualTo(0f) 431 432 underTest.onQuickSettingsExpansionChanged(0.9f, isInSplitShade = false) 433 assertThat(underTest.alpha.value).isEqualTo(0f) 434 435 underTest.onQuickSettingsExpansionChanged(0.91f, isInSplitShade = false) 436 assertThat(underTest.alpha.value).isWithin(floatTolerance).of(0.1f) 437 438 underTest.onQuickSettingsExpansionChanged(0.95f, isInSplitShade = false) 439 assertThat(underTest.alpha.value).isWithin(floatTolerance).of(0.5f) 440 441 underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false) 442 assertThat(underTest.alpha.value).isEqualTo(1f) 443 } 444 445 @Test backgroundAlpha_inSingleShade_always1null446 fun backgroundAlpha_inSingleShade_always1() { 447 val underTest = utils.footerActionsViewModel() 448 449 underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = false) 450 assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) 451 452 underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = false) 453 assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) 454 455 underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = false) 456 assertThat(underTest.backgroundAlpha.value).isEqualTo(1f) 457 } 458 } 459