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