1 /*
2  * Copyright (C) 2021 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.tiles
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.Intent
22 import android.os.Handler
23 import android.provider.Settings
24 import android.service.quicksettings.Tile
25 import android.testing.TestableLooper
26 import androidx.lifecycle.LifecycleOwner
27 import androidx.test.ext.junit.runners.AndroidJUnit4
28 import androidx.test.filters.SmallTest
29 import com.android.internal.logging.MetricsLogger
30 import com.android.systemui.res.R
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.animation.ActivityTransitionAnimator
33 import com.android.systemui.classifier.FalsingManagerFake
34 import com.android.systemui.controls.ControlsServiceInfo
35 import com.android.systemui.controls.controller.ControlInfo
36 import com.android.systemui.controls.controller.ControlsController
37 import com.android.systemui.controls.controller.StructureInfo
38 import com.android.systemui.controls.dagger.ControlsComponent
39 import com.android.systemui.controls.management.ControlsListingController
40 import com.android.systemui.controls.ui.ControlsActivity
41 import com.android.systemui.controls.ui.ControlsUiController
42 import com.android.systemui.controls.ui.SelectedItem
43 import com.android.systemui.plugins.ActivityStarter
44 import com.android.systemui.plugins.statusbar.StatusBarStateController
45 import com.android.systemui.qs.QSHost
46 import com.android.systemui.qs.QsEventLogger
47 import com.android.systemui.qs.logging.QSLogger
48 import com.android.systemui.qs.tileimpl.QSTileImpl
49 import com.android.systemui.util.mockito.any
50 import com.android.systemui.util.mockito.capture
51 import com.android.systemui.util.mockito.eq
52 import com.android.systemui.util.settings.FakeSettings
53 import com.android.systemui.util.settings.SecureSettings
54 import com.google.common.truth.Truth.assertThat
55 import org.junit.After
56 import org.junit.Before
57 import org.junit.Test
58 import org.junit.runner.RunWith
59 import org.mockito.ArgumentCaptor
60 import org.mockito.ArgumentMatchers.anyBoolean
61 import org.mockito.Captor
62 import org.mockito.Mock
63 import org.mockito.Mockito.doNothing
64 import org.mockito.Mockito.nullable
65 import org.mockito.Mockito.spy
66 import org.mockito.Mockito.verify
67 import org.mockito.Mockito.verifyZeroInteractions
68 import org.mockito.Mockito.`when`
69 import org.mockito.MockitoAnnotations
70 import java.util.Optional
71 
72 @SmallTest
73 @RunWith(AndroidJUnit4::class)
74 @TestableLooper.RunWithLooper(setAsMainLooper = true)
75 class DeviceControlsTileTest : SysuiTestCase() {
76 
77     @Mock
78     private lateinit var qsHost: QSHost
79     @Mock
80     private lateinit var metricsLogger: MetricsLogger
81     @Mock
82     private lateinit var statusBarStateController: StatusBarStateController
83     @Mock
84     private lateinit var activityStarter: ActivityStarter
85     @Mock
86     private lateinit var qsLogger: QSLogger
87     @Mock
88     private lateinit var controlsComponent: ControlsComponent
89     @Mock
90     private lateinit var controlsUiController: ControlsUiController
91     @Mock
92     private lateinit var controlsListingController: ControlsListingController
93     @Mock
94     private lateinit var controlsController: ControlsController
95     @Mock
96     private lateinit var serviceInfo: ControlsServiceInfo
97     @Mock
98     private lateinit var uiEventLogger: QsEventLogger
99     @Captor
100     private lateinit var listingCallbackCaptor:
101             ArgumentCaptor<ControlsListingController.ControlsListingCallback>
102     @Captor
103     private lateinit var intentCaptor: ArgumentCaptor<Intent>
104 
105     private lateinit var testableLooper: TestableLooper
106     private lateinit var tile: DeviceControlsTile
107 
108     private lateinit var secureSettings: SecureSettings
109     private lateinit var spiedContext: Context
110     private var featureEnabled = true
111 
112     @Before
setUpnull113     fun setUp() {
114         MockitoAnnotations.initMocks(this)
115         testableLooper = TestableLooper.get(this)
116         secureSettings = FakeSettings()
117 
118         spiedContext = spy(mContext)
119         doNothing().`when`(spiedContext).startActivity(any(Intent::class.java))
120         `when`(qsHost.context).thenReturn(spiedContext)
121         `when`(controlsComponent.isEnabled()).thenReturn(true)
122         `when`(controlsController.getPreferredSelection())
123                 .thenReturn(SelectedItem.StructureItem(
124                         StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())))
125         secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
126 
127         setupControlsComponent()
128 
129         tile = createTile()
130     }
131 
132     @After
tearDownnull133     fun tearDown() {
134         tile.destroy()
135         testableLooper.processAllMessages()
136     }
137 
setupControlsComponentnull138     private fun setupControlsComponent() {
139         `when`(controlsComponent.getControlsController()).thenAnswer {
140             if (featureEnabled) {
141                 Optional.of(controlsController)
142             } else {
143                 Optional.empty()
144             }
145         }
146 
147         `when`(controlsComponent.getControlsListingController()).thenAnswer {
148             if (featureEnabled) {
149                 Optional.of(controlsListingController)
150             } else {
151                 Optional.empty()
152             }
153         }
154 
155         `when`(controlsComponent.getControlsUiController()).thenAnswer {
156             if (featureEnabled) {
157                 Optional.of(controlsUiController)
158             } else {
159                 Optional.empty()
160             }
161         }
162 
163         `when`(controlsComponent.getTileTitleId()).thenReturn(R.string.quick_controls_title)
164         `when`(controlsComponent.getTileTitleId()).thenReturn(R.drawable.controls_icon)
165     }
166 
167     @Test
testAvailablenull168     fun testAvailable() {
169         assertThat(tile.isAvailable).isTrue()
170     }
171 
172     @Test
testNotAvailableControlsnull173     fun testNotAvailableControls() {
174         featureEnabled = false
175 
176         // Destroy previous tile
177         tile.destroy()
178         tile = createTile()
179 
180         assertThat(tile.isAvailable).isFalse()
181     }
182 
183     @Test
testObservingCallbacknull184     fun testObservingCallback() {
185         verify(controlsListingController).observe(
186                 any(LifecycleOwner::class.java),
187                 any(ControlsListingController.ControlsListingCallback::class.java)
188         )
189     }
190 
191     @Test
testLongClickIntentnull192     fun testLongClickIntent() {
193         assertThat(tile.longClickIntent).isNull()
194     }
195 
196     @Test
testDoesNotHandleLongClicknull197     fun testDoesNotHandleLongClick() {
198         assertThat(tile.state.handlesLongClick).isFalse()
199     }
200 
201     @Test
testUnavailableByDefaultnull202     fun testUnavailableByDefault() {
203         assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE)
204     }
205 
206     @Test
testStateUnavailableIfNoListingsnull207     fun testStateUnavailableIfNoListings() {
208         verify(controlsListingController).observe(
209                 any(LifecycleOwner::class.java),
210                 capture(listingCallbackCaptor)
211         )
212 
213         listingCallbackCaptor.value.onServicesUpdated(emptyList())
214         testableLooper.processAllMessages()
215 
216         assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE)
217     }
218 
219     @Test
testStateUnavailableIfNotEnablednull220     fun testStateUnavailableIfNotEnabled() {
221         verify(controlsListingController).observe(
222             any(LifecycleOwner::class.java),
223             capture(listingCallbackCaptor)
224         )
225         `when`(controlsComponent.isEnabled()).thenReturn(false)
226 
227         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
228         testableLooper.processAllMessages()
229 
230         assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE)
231     }
232 
233     @Test
testStateActiveIfListingsHasControlsFavoritednull234     fun testStateActiveIfListingsHasControlsFavorited() {
235         verify(controlsListingController).observe(
236                 any(LifecycleOwner::class.java),
237                 capture(listingCallbackCaptor)
238         )
239         `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
240         `when`(controlsController.getPreferredSelection()).thenReturn(
241             SelectedItem.StructureItem(StructureInfo(
242                 ComponentName("pkg", "cls"),
243                 "structure",
244                 listOf(ControlInfo("id", "title", "subtitle", 1))
245             ))
246         )
247 
248         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
249         testableLooper.processAllMessages()
250 
251         assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE)
252     }
253 
254     @Test
testStateInactiveIfListingsHasNoControlsFavoritednull255     fun testStateInactiveIfListingsHasNoControlsFavorited() {
256         verify(controlsListingController).observe(
257                 any(LifecycleOwner::class.java),
258                 capture(listingCallbackCaptor)
259         )
260         `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
261         `when`(controlsController.getPreferredSelection())
262                 .thenReturn(SelectedItem.StructureItem(
263                         StructureInfo(ComponentName("pkg", "cls"), "structure", listOf())))
264 
265         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
266         testableLooper.processAllMessages()
267 
268         assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE)
269     }
270 
271     @Test
testStateActiveIfPreferredIsPanelnull272     fun testStateActiveIfPreferredIsPanel() {
273         verify(controlsListingController).observe(
274                 any(LifecycleOwner::class.java),
275                 capture(listingCallbackCaptor)
276         )
277         `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
278         `when`(controlsController.getPreferredSelection())
279                 .thenReturn(SelectedItem.PanelItem("appName", ComponentName("pkg", "cls")))
280 
281         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
282         testableLooper.processAllMessages()
283 
284         assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE)
285     }
286 
287     @Test
testStateInactiveIfLockednull288     fun testStateInactiveIfLocked() {
289         verify(controlsListingController).observe(
290             any(LifecycleOwner::class.java),
291             capture(listingCallbackCaptor)
292         )
293         `when`(controlsComponent.getVisibility())
294             .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK)
295 
296         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
297         testableLooper.processAllMessages()
298 
299         assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE)
300     }
301 
302     @Test
testMoveBetweenStatesnull303     fun testMoveBetweenStates() {
304         verify(controlsListingController).observe(
305                 any(LifecycleOwner::class.java),
306                 capture(listingCallbackCaptor)
307         )
308 
309         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
310         testableLooper.processAllMessages()
311 
312         listingCallbackCaptor.value.onServicesUpdated(emptyList())
313         testableLooper.processAllMessages()
314 
315         assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE)
316     }
317 
318     @Test
handleClick_unavailable_noActivityStartednull319     fun handleClick_unavailable_noActivityStarted() {
320         tile.click(null /* view */)
321         testableLooper.processAllMessages()
322 
323         verifyZeroInteractions(activityStarter)
324     }
325 
326     @Test
handleClick_available_shownOverLockscreenWhenLockednull327     fun handleClick_available_shownOverLockscreenWhenLocked() {
328         verify(controlsListingController).observe(
329                 any(LifecycleOwner::class.java),
330                 capture(listingCallbackCaptor)
331         )
332         `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE)
333         `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java)
334         `when`(controlsController.getPreferredSelection()).thenReturn(
335             SelectedItem.StructureItem(StructureInfo(
336                     ComponentName("pkg", "cls"),
337                     "structure",
338                     listOf(ControlInfo("id", "title", "subtitle", 1))
339             ))
340         )
341 
342         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
343         testableLooper.processAllMessages()
344 
345         tile.click(null /* view */)
346         testableLooper.processAllMessages()
347 
348         verify(activityStarter).startActivity(
349                 intentCaptor.capture(),
350                 eq(true) /* dismissShade */,
351                 nullable(ActivityTransitionAnimator.Controller::class.java),
352                 eq(true) /* showOverLockscreenWhenLocked */)
353         assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME)
354     }
355 
356     @Test
handleClick_availableAfterUnlock_notShownOverLockscreenWhenLockednull357     fun handleClick_availableAfterUnlock_notShownOverLockscreenWhenLocked() {
358         verify(controlsListingController).observe(
359             any(LifecycleOwner::class.java),
360             capture(listingCallbackCaptor)
361         )
362         `when`(controlsComponent.getVisibility())
363             .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK)
364         `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java)
365         `when`(controlsController.getPreferredSelection()).thenReturn(
366             SelectedItem.StructureItem(StructureInfo(
367                 ComponentName("pkg", "cls"),
368                 "structure",
369                 listOf(ControlInfo("id", "title", "subtitle", 1))
370             ))
371         )
372 
373         listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo))
374         testableLooper.processAllMessages()
375 
376         tile.click(null /* view */)
377         testableLooper.processAllMessages()
378 
379         verify(activityStarter).startActivity(
380                 intentCaptor.capture(),
381                 anyBoolean() /* dismissShade */,
382                 nullable(ActivityTransitionAnimator.Controller::class.java),
383                 eq(false) /* showOverLockscreenWhenLocked */)
384         assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME)
385     }
386 
387     @Test
verifyTileEqualsResourceFromComponentnull388     fun verifyTileEqualsResourceFromComponent() {
389         assertThat(tile.tileLabel)
390             .isEqualTo(
391                 context.getText(
392                     controlsComponent.getTileTitleId()))
393     }
394 
395     @Test
verifyTileImageEqualsResourceFromComponentnull396     fun verifyTileImageEqualsResourceFromComponent() {
397         assertThat(tile.icon)
398             .isEqualTo(QSTileImpl.ResourceIcon.get(controlsComponent.getTileImageId()))
399     }
400 
createTilenull401     private fun createTile(): DeviceControlsTile {
402         return DeviceControlsTile(
403                 qsHost,
404                 uiEventLogger,
405                 testableLooper.looper,
406                 Handler(testableLooper.looper),
407                 FalsingManagerFake(),
408                 metricsLogger,
409                 statusBarStateController,
410                 activityStarter,
411                 qsLogger,
412                 controlsComponent
413         ).also {
414             it.initialize()
415             testableLooper.processAllMessages()
416         }
417     }
418 }
419 
420 private const val CONTROLS_ACTIVITY_CLASS_NAME = "com.android.systemui.controls.ui.ControlsActivity"
421