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