blob: e4751d13503517d2ce1d37b55771db3654c17e56 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.qs.footer.ui.viewmodel
import android.graphics.drawable.Drawable
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.settingslib.Utils
import com.android.settingslib.drawable.UserIconDrawable
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.qs.FakeFgsManagerController
import com.android.systemui.qs.QSSecurityFooterUtils
import com.android.systemui.qs.footer.FooterActionsTestUtils
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
import com.android.systemui.security.data.model.SecurityModel
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.FakeSecurityController
import com.android.systemui.statusbar.policy.FakeUserInfoController
import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo
import com.android.systemui.statusbar.policy.MockUserSwitcherControllerWrapper
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class FooterActionsViewModelTest : SysuiTestCase() {
private lateinit var utils: FooterActionsTestUtils
@Before
fun setUp() {
utils = FooterActionsTestUtils(context, TestableLooper.get(this))
}
@Test
fun settingsButton() = runBlockingTest {
val underTest = utils.footerActionsViewModel(showPowerButton = false)
val settings = underTest.settings
assertThat(settings.contentDescription)
.isEqualTo(ContentDescription.Resource(R.string.accessibility_quick_settings_settings))
assertThat(settings.icon).isEqualTo(Icon.Resource(R.drawable.ic_settings))
assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
assertThat(settings.iconTint).isNull()
}
@Test
fun powerButton() = runBlockingTest {
// Without power button.
val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false)
assertThat(underTestWithoutPower.power).isNull()
// With power button.
val underTestWithPower = utils.footerActionsViewModel(showPowerButton = true)
val power = underTestWithPower.power
assertThat(power).isNotNull()
assertThat(power!!.contentDescription)
.isEqualTo(
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
)
assertThat(power.icon).isEqualTo(Icon.Resource(android.R.drawable.ic_lock_power_off))
assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
assertThat(power.iconTint)
.isEqualTo(
Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.textColorOnAccent,
),
)
}
@Test
fun userSwitcher() = runBlockingTest {
val picture: Drawable = mock()
val userInfoController = FakeUserInfoController(FakeInfo(picture = picture))
val settings = FakeSettings()
val userId = 42
val userTracker = FakeUserTracker(userId)
val userSwitcherControllerWrapper =
MockUserSwitcherControllerWrapper(currentUserName = "foo")
// Mock UserManager.
val userManager = mock<UserManager>()
var isUserSwitcherEnabled = false
var isGuestUser = false
whenever(userManager.isUserSwitcherEnabled(any())).thenAnswer { isUserSwitcherEnabled }
whenever(userManager.isGuestUser(any())).thenAnswer { isGuestUser }
val underTest =
utils.footerActionsViewModel(
showPowerButton = false,
footerActionsInteractor =
utils.footerActionsInteractor(
userSwitcherRepository =
utils.userSwitcherRepository(
userTracker = userTracker,
settings = settings,
userManager = userManager,
userInfoController = userInfoController,
userSwitcherController = userSwitcherControllerWrapper.controller,
),
)
)
// Collect the user switcher into currentUserSwitcher.
var currentUserSwitcher: FooterActionsButtonViewModel? = null
val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } }
fun currentUserSwitcher(): FooterActionsButtonViewModel? {
// Make sure we finish collecting the current user switcher. This is necessary because
// combined flows launch multiple coroutines in the current scope so we need to make
// sure we process all coroutines triggered by our flow collection before we make
// assertions on the current buttons.
advanceUntilIdle()
return currentUserSwitcher
}
// The user switcher is disabled.
assertThat(currentUserSwitcher()).isNull()
// Make the user manager return that the User Switcher is enabled. A change of the setting
// for the current user will be fired to notify us of that change.
isUserSwitcherEnabled = true
// Update the setting for a random user: nothing should change, given that at this point we
// weren't notified of the change yet.
utils.setUserSwitcherEnabled(settings, true, 3)
assertThat(currentUserSwitcher()).isNull()
// Update the setting for the observed user: now we will be notified and the button should
// be there.
utils.setUserSwitcherEnabled(settings, true, userId)
val userSwitcher = currentUserSwitcher()
assertThat(userSwitcher).isNotNull()
assertThat(userSwitcher!!.contentDescription)
.isEqualTo(ContentDescription.Loaded("Signed in as foo"))
assertThat(userSwitcher.icon).isEqualTo(Icon.Loaded(picture))
assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
// Change the current user name.
userSwitcherControllerWrapper.currentUserName = "bar"
assertThat(currentUserSwitcher()?.contentDescription)
.isEqualTo(ContentDescription.Loaded("Signed in as bar"))
fun iconTint(): Int? = currentUserSwitcher()!!.iconTint
// We tint the icon if the current user is not the guest.
assertThat(iconTint()).isNull()
// Make the UserManager return that the current user is the guest. A change of the user
// info will be fired to notify us of that change.
isGuestUser = true
// At this point, there was no change of the user info yet so we still didn't pick the
// UserManager change.
assertThat(iconTint()).isNull()
// Trigger a user info change: there should now be a tint.
userInfoController.updateInfo { userAccount = "doe" }
assertThat(iconTint())
.isEqualTo(
Utils.getColorAttrDefaultColor(
context,
android.R.attr.colorForeground,
)
)
// Make sure we don't tint the icon if it is a user image (and not the default image), even
// in guest mode.
userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() }
assertThat(iconTint()).isNull()
job.cancel()
}
@Test
fun security() = runBlockingTest {
val securityController = FakeSecurityController()
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
// logic in securityToConfig.
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
securityToConfig(it.arguments.first() as SecurityModel)
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
securityRepository =
utils.securityRepository(
securityController = securityController,
),
),
)
// Collect the security model into currentSecurity.
var currentSecurity: FooterActionsSecurityButtonViewModel? = null
val job = launch { underTest.security.collect { currentSecurity = it } }
fun currentSecurity(): FooterActionsSecurityButtonViewModel? {
advanceUntilIdle()
return currentSecurity
}
// By default, we always return a null SecurityButtonConfig.
assertThat(currentSecurity()).isNull()
// Map any SecurityModel into a non-null SecurityButtonConfig.
val buttonConfig =
SecurityButtonConfig(
icon = Icon.Resource(0),
text = "foo",
isClickable = true,
)
securityToConfig = { buttonConfig }
// There was no change of the security info yet, so the mapper was not called yet.
assertThat(currentSecurity()).isNull()
// Trigger a SecurityModel change, which will call the mapper and add a button.
securityController.updateState {}
var security = currentSecurity()
assertThat(security).isNotNull()
assertThat(security!!.icon).isEqualTo(buttonConfig.icon)
assertThat(security.text).isEqualTo(buttonConfig.text)
assertThat(security.onClick).isNotNull()
// If the config.clickable = false, then onClick should be null.
securityToConfig = { buttonConfig.copy(isClickable = false) }
securityController.updateState {}
security = currentSecurity()
assertThat(security).isNotNull()
assertThat(security!!.onClick).isNull()
job.cancel()
}
@Test
fun foregroundServices() = runBlockingTest {
val securityController = FakeSecurityController()
val fgsManagerController =
FakeFgsManagerController(
isAvailable = true,
showFooterDot = false,
numRunningPackages = 0,
)
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
// logic in securityToConfig.
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
securityToConfig(it.arguments.first() as SecurityModel)
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
securityRepository = utils.securityRepository(securityController),
foregroundServicesRepository =
utils.foregroundServicesRepository(fgsManagerController),
),
)
// Collect the security model into currentSecurity.
var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } }
fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? {
advanceUntilIdle()
return currentForegroundServices
}
// We don't show the foreground services button if the number of running packages is not
// > 1.
assertThat(currentForegroundServices()).isNull()
// We show it at soon as the number of services is at least 1. Given that there is no
// security, it should be displayed with text.
fgsManagerController.numRunningPackages = 1
val foregroundServices = currentForegroundServices()
assertThat(foregroundServices).isNotNull()
assertThat(foregroundServices!!.foregroundServicesCount).isEqualTo(1)
assertThat(foregroundServices.text).isEqualTo("1 app is active")
assertThat(foregroundServices.displayText).isTrue()
assertThat(foregroundServices.onClick).isNotNull()
// We handle plurals correctly.
fgsManagerController.numRunningPackages = 3
assertThat(currentForegroundServices()?.text).isEqualTo("3 apps are active")
// Showing new changes (the footer dot) is currently disabled.
assertThat(foregroundServices.hasNewChanges).isFalse()
// Enabling it will show the new changes.
fgsManagerController.showFooterDot.value = true
assertThat(currentForegroundServices()?.hasNewChanges).isTrue()
// Dismissing the dialog should remove the new changes dot.
fgsManagerController.simulateDialogDismiss()
assertThat(currentForegroundServices()?.hasNewChanges).isFalse()
// Showing the security button will make this show as a simple button without text.
assertThat(foregroundServices.displayText).isTrue()
securityToConfig = {
SecurityButtonConfig(
icon = Icon.Resource(0),
text = "foo",
isClickable = true,
)
}
securityController.updateState {}
assertThat(currentForegroundServices()?.displayText).isFalse()
job.cancel()
}
@Test
fun observeDeviceMonitoringDialogRequests() = runBlockingTest {
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
val broadcastDispatcher = mock<BroadcastDispatcher>()
// Return a fake broadcastFlow that emits 3 fake events when collected.
val broadcastFlow = flowOf(Unit, Unit, Unit)
whenever(
broadcastDispatcher.broadcastFlow(
any(),
nullable(),
anyInt(),
nullable(),
)
)
.thenAnswer { broadcastFlow }
// Increment nDialogRequests whenever a request to show the dialog is made by the
// FooterActionsInteractor.
var nDialogRequests = 0
whenever(qsSecurityFooterUtils.showDeviceMonitoringDialog(any(), nullable())).then {
nDialogRequests++
}
val underTest =
utils.footerActionsViewModel(
footerActionsInteractor =
utils.footerActionsInteractor(
qsSecurityFooterUtils = qsSecurityFooterUtils,
broadcastDispatcher = broadcastDispatcher,
),
)
val job = launch {
underTest.observeDeviceMonitoringDialogRequests(quickSettingsContext = mock())
}
advanceUntilIdle()
assertThat(nDialogRequests).isEqualTo(3)
job.cancel()
}
@Test
fun isVisible() {
val underTest = utils.footerActionsViewModel()
assertThat(underTest.isVisible.value).isTrue()
underTest.onVisibilityChangeRequested(visible = false)
assertThat(underTest.isVisible.value).isFalse()
underTest.onVisibilityChangeRequested(visible = true)
assertThat(underTest.isVisible.value).isTrue()
}
}