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