1 /*
2  * Copyright (C) 2019 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.external
18 
19 import android.app.PendingIntent
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.pm.ApplicationInfo
23 import android.content.pm.PackageManager
24 import android.content.pm.ServiceInfo
25 import android.graphics.drawable.Drawable
26 import android.graphics.drawable.Icon
27 import android.os.Handler
28 import android.service.quicksettings.IQSTileService
29 import android.service.quicksettings.Tile
30 import android.test.suitebuilder.annotation.SmallTest
31 import android.testing.AndroidTestingRunner
32 import android.testing.TestableLooper
33 import android.view.IWindowManager
34 import android.view.View
35 import com.android.internal.logging.MetricsLogger
36 import com.android.systemui.SysuiTestCase
37 import com.android.systemui.animation.ActivityLaunchAnimator
38 import com.android.systemui.animation.view.LaunchableFrameLayout
39 import com.android.systemui.classifier.FalsingManagerFake
40 import com.android.systemui.plugins.ActivityStarter
41 import com.android.systemui.plugins.qs.QSTile
42 import com.android.systemui.plugins.statusbar.StatusBarStateController
43 import com.android.systemui.qs.QSHost
44 import com.android.systemui.qs.QsEventLogger
45 import com.android.systemui.qs.logging.QSLogger
46 import com.android.systemui.settings.FakeDisplayTracker
47 import com.android.systemui.util.mockito.any
48 import com.android.systemui.util.mockito.eq
49 import com.android.systemui.util.mockito.nullable
50 import org.junit.Assert.assertEquals
51 import org.junit.Assert.assertFalse
52 import org.junit.Assert.assertThrows
53 import org.junit.Assert.assertTrue
54 import org.junit.Before
55 import org.junit.Test
56 import org.junit.runner.RunWith
57 import org.mockito.ArgumentMatchers.anyInt
58 import org.mockito.ArgumentMatchers.anyString
59 import org.mockito.Mock
60 import org.mockito.Mockito.clearInvocations
61 import org.mockito.Mockito.mock
62 import org.mockito.Mockito.never
63 import org.mockito.Mockito.reset
64 import org.mockito.Mockito.verify
65 import org.mockito.Mockito.`when`
66 import org.mockito.MockitoAnnotations
67 
68 @SmallTest
69 @RunWith(AndroidTestingRunner::class)
70 @TestableLooper.RunWithLooper(setAsMainLooper = true)
71 class CustomTileTest : SysuiTestCase() {
72 
73     companion object {
74         const val packageName = "test_package"
75         const val className = "test_class"
76         val componentName = ComponentName(packageName, className)
77         val TILE_SPEC = CustomTile.toSpec(componentName)
78     }
79 
80     @Mock private lateinit var tileHost: QSHost
81     @Mock private lateinit var metricsLogger: MetricsLogger
82     @Mock private lateinit var statusBarStateController: StatusBarStateController
83     @Mock private lateinit var activityStarter: ActivityStarter
84     @Mock private lateinit var qsLogger: QSLogger
85     @Mock private lateinit var tileService: IQSTileService
86     @Mock private lateinit var tileServices: TileServices
87     @Mock private lateinit var tileServiceManager: TileServiceManager
88     @Mock private lateinit var windowService: IWindowManager
89     @Mock private lateinit var packageManager: PackageManager
90     @Mock private lateinit var applicationInfo: ApplicationInfo
91     @Mock private lateinit var serviceInfo: ServiceInfo
92     @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
93     @Mock private lateinit var uiEventLogger: QsEventLogger
94 
95     private var displayTracker = FakeDisplayTracker(mContext)
96     private lateinit var customTile: CustomTile
97     private lateinit var testableLooper: TestableLooper
98     private lateinit var customTileBuilder: CustomTile.Builder
99 
100     @Before
101     fun setUp() {
102         MockitoAnnotations.initMocks(this)
103         testableLooper = TestableLooper.get(this)
104 
105         mContext.addMockSystemService("window", windowService)
106         mContext.setMockPackageManager(packageManager)
107         `when`(tileHost.context).thenReturn(mContext)
108         `when`(tileServices.getTileWrapper(any(CustomTile::class.java)))
109                 .thenReturn(tileServiceManager)
110         `when`(tileServiceManager.tileService).thenReturn(tileService)
111         `when`(packageManager.getApplicationInfo(anyString(), anyInt()))
112                 .thenReturn(applicationInfo)
113 
114         `when`(packageManager.getServiceInfo(any(ComponentName::class.java), anyInt()))
115                 .thenReturn(serviceInfo)
116         serviceInfo.applicationInfo = applicationInfo
117 
118         customTileBuilder = CustomTile.Builder(
119                 { tileHost },
120                 uiEventLogger,
121                 testableLooper.looper,
122                 Handler(testableLooper.looper),
123                 FalsingManagerFake(),
124                 metricsLogger,
125                 statusBarStateController,
126                 activityStarter,
127                 qsLogger,
128                 customTileStatePersister,
129                 tileServices,
130                 displayTracker
131         )
132 
133         customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
134         customTile.initialize()
135         testableLooper.processAllMessages()
136     }
137 
138     @Test
139     fun testCorrectUser() {
140         assertEquals(0, customTile.user)
141 
142         val userContext = mock(Context::class.java)
143         `when`(userContext.packageManager).thenReturn(packageManager)
144         `when`(userContext.userId).thenReturn(10)
145 
146         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, userContext)
147         tile.initialize()
148         testableLooper.processAllMessages()
149 
150         assertEquals(10, tile.user)
151     }
152 
153     @Test
154     fun testToggleableTileHasBooleanState() {
155         `when`(tileServiceManager.isToggleableTile).thenReturn(true)
156         customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
157         customTile.initialize()
158         testableLooper.processAllMessages()
159 
160         assertTrue(customTile.state is QSTile.BooleanState)
161         assertTrue(customTile.newTileState() is QSTile.BooleanState)
162     }
163 
164     @Test
165     fun testRegularTileHasNotBooleanState() {
166         assertFalse(customTile.state is QSTile.BooleanState)
167         assertFalse(customTile.newTileState() is QSTile.BooleanState)
168     }
169 
170     @Test
171     fun testValueUpdatedInBooleanTile() {
172         `when`(tileServiceManager.isToggleableTile).thenReturn(true)
173         customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
174         customTile.initialize()
175         testableLooper.processAllMessages()
176 
177         customTile.qsTile.icon = mock(Icon::class.java)
178         `when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java)))
179                 .thenReturn(mock(Drawable::class.java))
180 
181         val state = customTile.newTileState()
182         assertTrue(state is QSTile.BooleanState)
183 
184         customTile.qsTile.state = Tile.STATE_INACTIVE
185         customTile.handleUpdateState(state, null)
186         assertFalse((state as QSTile.BooleanState).value)
187 
188         customTile.qsTile.state = Tile.STATE_ACTIVE
189         customTile.handleUpdateState(state, null)
190         assertTrue(state.value)
191 
192         customTile.qsTile.state = Tile.STATE_UNAVAILABLE
193         customTile.handleUpdateState(state, null)
194         assertFalse(state.value)
195     }
196 
197     @Test
198     fun testNoCrashOnNullDrawable() {
199         customTile.qsTile.icon = mock(Icon::class.java)
200         `when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java)))
201                 .thenReturn(null)
202         customTile.handleUpdateState(customTile.newTileState(), null)
203     }
204 
205     @Test
206     fun testNoLoadStateTileNotActive() {
207         // Not active by default
208         testableLooper.processAllMessages()
209 
210         verify(customTileStatePersister, never()).readState(any())
211     }
212 
213     @Test
214     fun testNoPersistedStateTileNotActive() {
215         // Not active by default
216         val t = Tile().apply {
217             state = Tile.STATE_INACTIVE
218         }
219         customTile.updateTileState(t)
220         testableLooper.processAllMessages()
221 
222         verify(customTileStatePersister, never()).persistState(any(), any())
223     }
224 
225     @Test
226     fun testPersistedStateRetrieved() {
227         val state = Tile.STATE_INACTIVE
228         val label = "test_label"
229         val subtitle = "test_subtitle"
230         val contentDescription = "test_content_description"
231         val stateDescription = "test_state_description"
232 
233         val t = Tile().apply {
234             this.state = state
235             this.label = label
236             this.subtitle = subtitle
237             this.contentDescription = contentDescription
238             this.stateDescription = stateDescription
239         }
240         `when`(tileServiceManager.isActiveTile).thenReturn(true)
241         `when`(customTileStatePersister
242                 .readState(TileServiceKey(componentName, customTile.user))).thenReturn(t)
243         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
244         tile.initialize()
245         testableLooper.processAllMessages()
246 
247         // Make sure we have an icon in the tile because we don't have a default icon
248         // This should not be overridden by the retrieved tile that has null icon.
249         tile.qsTile.icon = mock(Icon::class.java)
250         `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java)))
251                 .thenReturn(mock(Drawable::class.java))
252 
253         val pi = mock(PendingIntent::class.java)
254         `when`(pi.isActivity).thenReturn(true)
255         tile.qsTile.activityLaunchForClick = pi
256 
257         tile.refreshState()
258 
259         testableLooper.processAllMessages()
260 
261         val tileState = tile.state
262 
263         assertEquals(state, tileState.state)
264         assertEquals(label, tileState.label)
265         assertEquals(subtitle, tileState.secondaryLabel)
266         assertEquals(contentDescription, tileState.contentDescription)
267         assertEquals(stateDescription, tileState.stateDescription)
268     }
269 
270     @Test
271     fun testStoreStateOnChange() {
272         val t = Tile().apply {
273             state = Tile.STATE_INACTIVE
274             label = "test_label"
275             subtitle = "test_subtitle"
276             contentDescription = "test_content_description"
277             stateDescription = "test_state_description"
278         }
279         `when`(tileServiceManager.isActiveTile).thenReturn(true)
280 
281         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
282         tile.initialize()
283         testableLooper.processAllMessages()
284 
285         tile.updateTileState(t)
286 
287         testableLooper.processAllMessages()
288 
289         verify(customTileStatePersister)
290                 .persistState(TileServiceKey(componentName, customTile.user), t)
291     }
292 
293     @Test
294     fun testAvailableBeforeInitialization() {
295         `when`(packageManager.getApplicationInfo(anyString(), anyInt()))
296                 .thenThrow(PackageManager.NameNotFoundException())
297         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
298         assertTrue(tile.isAvailable)
299     }
300 
301     @Test
302     fun testNotAvailableAfterInitializationWithoutIcon() {
303         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
304         reset(tileHost)
305         tile.initialize()
306         testableLooper.processAllMessages()
307         assertFalse(tile.isAvailable)
308         verify(tileHost).removeTile(tile.tileSpec)
309     }
310 
311     @Test
312     fun testInvalidPendingIntentDoesNotStartActivity() {
313         val pi = mock(PendingIntent::class.java)
314         `when`(pi.isActivity).thenReturn(false)
315         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
316 
317         assertThrows(IllegalArgumentException::class.java) {
318             tile.qsTile.activityLaunchForClick = pi
319         }
320 
321         tile.handleClick(mock(View::class.java))
322         testableLooper.processAllMessages()
323 
324         verify(activityStarter, never())
325             .startPendingIntentDismissingKeyguard(
326                 any(), any(), any(ActivityLaunchAnimator.Controller::class.java))
327     }
328 
329     @Test
330     fun testValidPendingIntentWithNoClickDoesNotStartActivity() {
331         val pi = mock(PendingIntent::class.java)
332         `when`(pi.isActivity).thenReturn(true)
333         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
334         tile.qsTile.activityLaunchForClick = pi
335 
336         testableLooper.processAllMessages()
337 
338         verify(activityStarter, never())
339             .startPendingIntentDismissingKeyguard(
340                 any(), any(), any(ActivityLaunchAnimator.Controller::class.java))
341     }
342 
343     @Test
344     fun testValidPendingIntentStartsActivity() {
345         val pi = mock(PendingIntent::class.java)
346         `when`(pi.isActivity).thenReturn(true)
347         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
348         tile.qsTile.activityLaunchForClick = pi
349 
350         tile.handleClick(mock(LaunchableFrameLayout::class.java))
351 
352         testableLooper.processAllMessages()
353 
354         verify(activityStarter)
355             .startPendingIntentDismissingKeyguard(
356                 eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>())
357     }
358 
359     @Test
360     fun testActiveTileListensOnceAfterCreated() {
361         `when`(tileServiceManager.isActiveTile).thenReturn(true)
362 
363         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
364         tile.initialize()
365         tile.postStale()
366         testableLooper.processAllMessages()
367 
368         verify(tileServiceManager).setBindRequested(true)
369         verify(tileService).onStartListening()
370     }
371 
372     @Test
373     fun testActiveTileDoesntListenAfterFirstTime() {
374         `when`(tileServiceManager.isActiveTile).thenReturn(true)
375 
376         val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
377         tile.initialize()
378         // Make sure we have an icon in the tile because we don't have a default icon
379         // This should not be overridden by the retrieved tile that has null icon.
380         tile.qsTile.icon = mock(Icon::class.java)
381         `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java)))
382                 .thenReturn(mock(Drawable::class.java))
383 
384         tile.postStale()
385         testableLooper.processAllMessages()
386 
387         // postStale will set it to not listening after it's done
388         verify(tileService).onStopListening()
389 
390         clearInvocations(tileServiceManager, tileService)
391 
392         tile.setListening(Any(), true)
393         testableLooper.processAllMessages()
394 
395         verify(tileServiceManager, never()).setBindRequested(true)
396         verify(tileService, never()).onStartListening()
397     }
398 }