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.tileimpl
18 
19 import android.content.Context
20 import android.graphics.Rect
21 import android.graphics.drawable.Drawable
22 import android.service.quicksettings.Tile
23 import android.testing.TestableLooper
24 import android.text.TextUtils
25 import android.view.ContextThemeWrapper
26 import android.view.View
27 import android.view.accessibility.AccessibilityNodeInfo
28 import android.widget.TextView
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SmallTest
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.haptics.qs.QSLongPressEffect
33 import com.android.systemui.haptics.qs.qsLongPressEffect
34 import com.android.systemui.plugins.qs.QSTile
35 import com.android.systemui.qs.qsTileFactory
36 import com.android.systemui.res.R
37 import com.android.systemui.testKosmos
38 import com.google.common.truth.Truth.assertThat
39 import org.junit.Before
40 import org.junit.Test
41 import org.junit.runner.RunWith
42 import org.mockito.Mock
43 import org.mockito.MockitoAnnotations
44 
45 @RunWith(AndroidJUnit4::class)
46 @SmallTest
47 @TestableLooper.RunWithLooper(setAsMainLooper = true)
48 class QSTileViewImplTest : SysuiTestCase() {
49 
50     @Mock private lateinit var customDrawable: Drawable
51 
52     private lateinit var tileView: FakeTileView
53     private lateinit var customDrawableView: View
54     private lateinit var chevronView: View
55     private val kosmos = testKosmos()
56 
57     @Before
setUpnull58     fun setUp() {
59         MockitoAnnotations.initMocks(this)
60         context.ensureTestableResources()
61 
62         tileView = FakeTileView(context, false, kosmos.qsLongPressEffect)
63         customDrawableView = tileView.requireViewById(R.id.customDrawable)
64         chevronView = tileView.requireViewById(R.id.chevron)
65     }
66 
67     @Test
testSecondaryLabelNotModified_unavailablenull68     fun testSecondaryLabelNotModified_unavailable() {
69         val state = QSTile.State()
70         val testString = "TEST STRING"
71         state.state = Tile.STATE_UNAVAILABLE
72         state.secondaryLabel = testString
73 
74         tileView.changeState(state)
75 
76         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
77     }
78 
79     @Test
testSecondaryLabelNotModified_booleanInactivenull80     fun testSecondaryLabelNotModified_booleanInactive() {
81         val state = QSTile.BooleanState()
82         val testString = "TEST STRING"
83         state.state = Tile.STATE_INACTIVE
84         state.secondaryLabel = testString
85 
86         tileView.changeState(state)
87 
88         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
89     }
90 
91     @Test
testSecondaryLabelNotModified_booleanActivenull92     fun testSecondaryLabelNotModified_booleanActive() {
93         val state = QSTile.BooleanState()
94         val testString = "TEST STRING"
95         state.state = Tile.STATE_ACTIVE
96         state.secondaryLabel = testString
97 
98         tileView.changeState(state)
99 
100         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
101     }
102 
103     @Test
testSecondaryLabelNotModified_availableNotBoolean_inactivenull104     fun testSecondaryLabelNotModified_availableNotBoolean_inactive() {
105         val state = QSTile.State()
106         state.state = Tile.STATE_INACTIVE
107         state.secondaryLabel = ""
108 
109         tileView.changeState(state)
110 
111         assertThat(TextUtils.isEmpty(state.secondaryLabel)).isTrue()
112     }
113 
114     @Test
testSecondaryLabelNotModified_availableNotBoolean_activenull115     fun testSecondaryLabelNotModified_availableNotBoolean_active() {
116         val state = QSTile.State()
117         state.state = Tile.STATE_ACTIVE
118         state.secondaryLabel = ""
119 
120         tileView.changeState(state)
121 
122         assertThat(TextUtils.isEmpty(state.secondaryLabel)).isTrue()
123     }
124 
125     @Test
testSecondaryLabelDescription_unavailable_defaultnull126     fun testSecondaryLabelDescription_unavailable_default() {
127         val state = QSTile.State()
128         state.state = Tile.STATE_UNAVAILABLE
129         state.secondaryLabel = ""
130 
131         tileView.changeState(state)
132 
133         assertThat(state.secondaryLabel as CharSequence)
134             .isEqualTo(context.getString(R.string.tile_unavailable))
135     }
136 
137     @Test
testSecondaryLabelDescription_booleanInactive_defaultnull138     fun testSecondaryLabelDescription_booleanInactive_default() {
139         val state = QSTile.BooleanState()
140         state.state = Tile.STATE_INACTIVE
141         state.secondaryLabel = ""
142 
143         tileView.changeState(state)
144 
145         assertThat(state.secondaryLabel as CharSequence)
146             .isEqualTo(context.getString(R.string.switch_bar_off))
147     }
148 
149     @Test
testSecondaryLabelDescription_booleanActive_defaultnull150     fun testSecondaryLabelDescription_booleanActive_default() {
151         val state = QSTile.BooleanState()
152         state.state = Tile.STATE_ACTIVE
153         state.secondaryLabel = ""
154 
155         tileView.changeState(state)
156 
157         assertThat(state.secondaryLabel as CharSequence)
158             .isEqualTo(context.getString(R.string.switch_bar_on))
159     }
160 
161     @Test
testShowCustomDrawableViewBooleanStatenull162     fun testShowCustomDrawableViewBooleanState() {
163         val state = QSTile.BooleanState()
164         state.sideViewCustomDrawable = customDrawable
165 
166         tileView.changeState(state)
167 
168         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
169         assertThat(chevronView.visibility).isEqualTo(View.GONE)
170     }
171 
172     @Test
testShowCustomDrawableViewNonBooleanStatenull173     fun testShowCustomDrawableViewNonBooleanState() {
174         val state = QSTile.State()
175         state.sideViewCustomDrawable = customDrawable
176 
177         tileView.changeState(state)
178 
179         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
180         assertThat(chevronView.visibility).isEqualTo(View.GONE)
181     }
182 
183     @Test
testShowCustomDrawableViewBooleanStateForceChevronnull184     fun testShowCustomDrawableViewBooleanStateForceChevron() {
185         val state = QSTile.BooleanState()
186         state.sideViewCustomDrawable = customDrawable
187         state.forceExpandIcon = true
188 
189         tileView.changeState(state)
190 
191         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
192         assertThat(chevronView.visibility).isEqualTo(View.GONE)
193     }
194 
195     @Test
testShowChevronNonBooleanStatenull196     fun testShowChevronNonBooleanState() {
197         val state = QSTile.State()
198 
199         tileView.changeState(state)
200 
201         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
202         assertThat(chevronView.visibility).isEqualTo(View.VISIBLE)
203     }
204 
205     @Test
testShowChevronBooleanStateForcheShownull206     fun testShowChevronBooleanStateForcheShow() {
207         val state = QSTile.BooleanState()
208         state.forceExpandIcon = true
209 
210         tileView.changeState(state)
211 
212         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
213         assertThat(chevronView.visibility).isEqualTo(View.VISIBLE)
214     }
215 
216     @Test
testNoImageShownnull217     fun testNoImageShown() {
218         val state = QSTile.BooleanState()
219 
220         tileView.changeState(state)
221 
222         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
223         assertThat(chevronView.visibility).isEqualTo(View.GONE)
224     }
225 
226     @Test
testUseStateStringsForKnownSpec_Booleannull227     fun testUseStateStringsForKnownSpec_Boolean() {
228         val state = QSTile.BooleanState()
229         val spec = "internet"
230         state.spec = spec
231 
232         val unavailableString = "${spec}_unavailable"
233         val offString = "${spec}_off"
234         val onString = "${spec}_on"
235 
236         context.orCreateTestableResources.addOverride(
237             R.array.tile_states_internet,
238             arrayOf(unavailableString, offString, onString)
239         )
240 
241         // State UNAVAILABLE
242         state.secondaryLabel = ""
243         state.state = Tile.STATE_UNAVAILABLE
244         tileView.changeState(state)
245         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(unavailableString)
246 
247         // State INACTIVE
248         state.secondaryLabel = ""
249         state.state = Tile.STATE_INACTIVE
250         tileView.changeState(state)
251         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(offString)
252 
253         // State ACTIVE
254         state.secondaryLabel = ""
255         state.state = Tile.STATE_ACTIVE
256         tileView.changeState(state)
257         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(onString)
258     }
259 
260     @Test
testCollectionItemInfoHasPositionnull261     fun testCollectionItemInfoHasPosition() {
262         val position = 5
263         tileView.setPosition(position)
264 
265         val info = AccessibilityNodeInfo(tileView)
266         tileView.onInitializeAccessibilityNodeInfo(info)
267 
268         assertThat(info.collectionItemInfo.rowIndex).isEqualTo(position)
269         assertThat(info.collectionItemInfo.rowSpan).isEqualTo(1)
270         assertThat(info.collectionItemInfo.columnIndex).isEqualTo(0)
271         assertThat(info.collectionItemInfo.columnSpan).isEqualTo(1)
272     }
273 
274     @Test
testCollectionItemInfoNoPositionnull275     fun testCollectionItemInfoNoPosition() {
276         val info = AccessibilityNodeInfo(tileView)
277         tileView.onInitializeAccessibilityNodeInfo(info)
278 
279         assertThat(info.collectionItemInfo).isNull()
280     }
281 
282     @Test
testDisabledByPolicyInactive_usesUnavailableColorsnull283     fun testDisabledByPolicyInactive_usesUnavailableColors() {
284         val stateDisabledByPolicy = QSTile.State()
285         stateDisabledByPolicy.state = Tile.STATE_INACTIVE
286         stateDisabledByPolicy.disabledByPolicy = true
287 
288         val stateUnavailable = QSTile.State()
289         stateUnavailable.state = Tile.STATE_UNAVAILABLE
290 
291         tileView.changeState(stateDisabledByPolicy)
292         val colorsDisabledByPolicy = tileView.getCurrentColors()
293 
294         tileView.changeState(stateUnavailable)
295         val colorsUnavailable = tileView.getCurrentColors()
296 
297         assertThat(colorsDisabledByPolicy).containsExactlyElementsIn(colorsUnavailable)
298     }
299 
300     @Test
testDisabledByPolicyActive_usesUnavailableColorsnull301     fun testDisabledByPolicyActive_usesUnavailableColors() {
302         val stateDisabledByPolicy = QSTile.State()
303         stateDisabledByPolicy.state = Tile.STATE_ACTIVE
304         stateDisabledByPolicy.disabledByPolicy = true
305 
306         val stateUnavailable = QSTile.State()
307         stateUnavailable.state = Tile.STATE_UNAVAILABLE
308 
309         tileView.changeState(stateDisabledByPolicy)
310         val colorsDisabledByPolicy = tileView.getCurrentColors()
311 
312         tileView.changeState(stateUnavailable)
313         val colorsUnavailable = tileView.getCurrentColors()
314 
315         assertThat(colorsDisabledByPolicy).containsExactlyElementsIn(colorsUnavailable)
316     }
317 
318     @Test
testDisableByPolicyThenRemoved_changesColornull319     fun testDisableByPolicyThenRemoved_changesColor() {
320         val stateActive = QSTile.State()
321         stateActive.state = Tile.STATE_ACTIVE
322 
323         val stateDisabledByPolicy = stateActive.copy()
324         stateDisabledByPolicy.disabledByPolicy = true
325 
326         tileView.changeState(stateActive)
327         val activeColors = tileView.getCurrentColors()
328 
329         tileView.changeState(stateDisabledByPolicy)
330         // It has unavailable colors
331         assertThat(tileView.getCurrentColors()).isNotEqualTo(activeColors)
332 
333         // When we get back to not disabled by policy tile, it should go back to active colors
334         tileView.changeState(stateActive)
335         assertThat(tileView.getCurrentColors()).containsExactlyElementsIn(activeColors)
336     }
337 
338     @Test
testDisabledByPolicy_secondaryLabelTextnull339     fun testDisabledByPolicy_secondaryLabelText() {
340         val testA11yLabel = "TEST_LABEL"
341         context.orCreateTestableResources.addOverride(
342             R.string.accessibility_tile_disabled_by_policy_action_description,
343             testA11yLabel
344         )
345 
346         val stateDisabledByPolicy = QSTile.State()
347         stateDisabledByPolicy.state = Tile.STATE_INACTIVE
348         stateDisabledByPolicy.disabledByPolicy = true
349 
350         tileView.changeState(stateDisabledByPolicy)
351 
352         val info = AccessibilityNodeInfo(tileView)
353         tileView.onInitializeAccessibilityNodeInfo(info)
354         assertThat(
355                 info.actionList
356                     .find { it.id == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id }
357                     ?.label
358             )
359             .isEqualTo(testA11yLabel)
360     }
361 
362     @Test
testDisabledByPolicy_unavailableInStateDescriptionnull363     fun testDisabledByPolicy_unavailableInStateDescription() {
364         val state = QSTile.BooleanState()
365         val spec = "internet"
366         state.spec = spec
367         state.disabledByPolicy = true
368         state.state = Tile.STATE_INACTIVE
369 
370         val unavailableString = "${spec}_unavailable"
371         val offString = "${spec}_off"
372         val onString = "${spec}_on"
373 
374         context.orCreateTestableResources.addOverride(
375             R.array.tile_states_internet,
376             arrayOf(unavailableString, offString, onString)
377         )
378 
379         tileView.changeState(state)
380         assertThat(tileView.stateDescription?.contains(unavailableString)).isTrue()
381     }
382 
383     @Test
onStateChange_longPressEffectActive_withInvalidDuration_doesNotInitializeEffectnull384     fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotInitializeEffect() {
385         val state = QSTile.State() // A state that handles longPress
386 
387         // GIVEN an invalid long-press effect duration
388         tileView.constantLongPressEffectDuration = -1
389 
390         // WHEN the state changes
391         tileView.changeState(state)
392 
393         // THEN the long-press effect is not initialized
394         assertThat(tileView.isLongPressEffectInitialized).isFalse()
395     }
396 
397     @Test
onStateChange_longPressEffectActive_withValidDuration_initializesEffectnull398     fun onStateChange_longPressEffectActive_withValidDuration_initializesEffect() {
399         // GIVEN a test state that handles long-press and a valid long-press effect duration
400         val state = QSTile.State()
401 
402         // WHEN the state changes
403         tileView.changeState(state)
404 
405         // THEN the long-press effect is initialized
406         assertThat(tileView.isLongPressEffectInitialized).isTrue()
407     }
408 
409     @Test
onStateChange_fromLongPress_to_noLongPress_clearsResourcesnull410     fun onStateChange_fromLongPress_to_noLongPress_clearsResources() {
411         // GIVEN a state that no longer handles long-press
412         val state = QSTile.State()
413         state.handlesLongClick = false
414 
415         // WHEN the state changes
416         tileView.changeState(state)
417 
418         // THEN the long-press effect resources are not set
419         assertThat(tileView.areLongPressEffectPropertiesSet).isFalse()
420     }
421 
422     @Test
onStateChange_fromNoLongPress_to_longPress_setsPropertiesnull423     fun onStateChange_fromNoLongPress_to_longPress_setsProperties() {
424         // GIVEN that the tile has changed to a state that does not handle long-press
425         val state = QSTile.State()
426         state.handlesLongClick = false
427         tileView.changeState(state)
428 
429         // WHEN the state changes back to handling long-press
430         state.handlesLongClick = true
431         tileView.changeState(state)
432 
433         // THEN the long-press effect resources are set
434         assertThat(tileView.areLongPressEffectPropertiesSet).isTrue()
435     }
436 
437     @Test
onStateChange_withoutLongPressEffect_fromLongPress_to_noLongPress_neverSetsPropertiesnull438     fun onStateChange_withoutLongPressEffect_fromLongPress_to_noLongPress_neverSetsProperties() {
439         // GIVEN a tile where the long-press effect is null
440         tileView = FakeTileView(context, false, null)
441 
442         // GIVEN a state that no longer handles long-press
443         val state = QSTile.State()
444         state.handlesLongClick = false
445 
446         // WHEN the state changes
447         tileView.changeState(state)
448 
449         // THEN the effect properties are not set and the effect is not initialized
450         assertThat(tileView.areLongPressEffectPropertiesSet).isFalse()
451         assertThat(tileView.isLongPressEffectInitialized).isFalse()
452     }
453 
454     @Test
onStateChange_withoutLongPressEffect_fromNoLongPress_to_longPress_neverSetsPropertiesnull455     fun onStateChange_withoutLongPressEffect_fromNoLongPress_to_longPress_neverSetsProperties() {
456         // GIVEN a tile where the long-press effect is null
457         tileView = FakeTileView(context, false, null)
458 
459         // GIVEN that the tile has changed to a state that does not handle long-press
460         val state = QSTile.State()
461         state.handlesLongClick = false
462         tileView.changeState(state)
463 
464         // WHEN the state changes back to handling long-press
465         state.handlesLongClick = true
466         tileView.changeState(state)
467 
468         // THEN the effect properties are not set and the effect is not initialized
469         assertThat(tileView.areLongPressEffectPropertiesSet).isFalse()
470         assertThat(tileView.isLongPressEffectInitialized).isFalse()
471     }
472 
473     @Test
onPrepareForLaunch_paddingForLaunchAnimationIsConfigurednull474     fun onPrepareForLaunch_paddingForLaunchAnimationIsConfigured() {
475         val startingWidth = 100
476         val startingHeight = 50
477         val deltaWidth = (QSTileViewImpl.LONG_PRESS_EFFECT_WIDTH_SCALE - 1f) * startingWidth
478         val deltaHeight = (QSTileViewImpl.LONG_PRESS_EFFECT_HEIGHT_SCALE - 1f) * startingHeight
479 
480         // GIVEN that long-press effect properties are initialized
481         tileView.initializeLongPressProperties(startingHeight, startingWidth)
482 
483         // WHEN the tile is preparing for the launch animation
484         tileView.prepareForLaunch()
485 
486         // THE animation padding corresponds to the tile's growth due to the effect
487         val padding = tileView.getPaddingForLaunchAnimation()
488         assertThat(padding)
489             .isEqualTo(
490                 Rect(
491                     -deltaWidth.toInt() / 2,
492                     -deltaHeight.toInt() / 2,
493                     deltaWidth.toInt() / 2,
494                     deltaHeight.toInt() / 2,
495                 )
496             )
497     }
498 
499     @Test
onActivityLaunchAnimationEnd_onFreshTile_longPressPropertiesAreResetnull500     fun onActivityLaunchAnimationEnd_onFreshTile_longPressPropertiesAreReset() {
501         // WHEN an activity launch animation ends on a fresh tile
502         tileView.onActivityLaunchAnimationEnd()
503 
504         // THEN the tile's long-press effect properties are reset by default
505         assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
506     }
507 
508     @Test
onUpdateLongPressEffectProperties_duringLongPressEffect_propertiesAreNotResetnull509     fun onUpdateLongPressEffectProperties_duringLongPressEffect_propertiesAreNotReset() {
510         // GIVEN a state that supports long-press
511         val state = QSTile.State()
512         tileView.changeState(state)
513 
514         // WHEN the long-press effect is updating the properties
515         tileView.updateLongPressEffectProperties(1f)
516 
517         // THEN the tile's long-press effect properties haven't reset
518         assertThat(tileView.haveLongPressPropertiesBeenReset).isFalse()
519     }
520 
521     @Test
onActivityLaunchAnimationEnd_afterLongPressEffect_longPressPropertiesAreResetnull522     fun onActivityLaunchAnimationEnd_afterLongPressEffect_longPressPropertiesAreReset() {
523         // GIVEN a state that supports long-press and the long-press effect updating
524         val state = QSTile.State()
525         tileView.changeState(state)
526         tileView.updateLongPressEffectProperties(1f)
527 
528         // WHEN an activity launch animation ends on a fresh tile
529         tileView.onActivityLaunchAnimationEnd()
530 
531         // THEN the tile's long-press effect properties are reset
532         assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
533     }
534 
535     @Test
onInit_withLongPressEffect_longPressEffectHasTileAndExpandablenull536     fun onInit_withLongPressEffect_longPressEffectHasTileAndExpandable() {
537         val tile = kosmos.qsTileFactory.createTile("Test Tile")
538         tileView.init(tile)
539 
540         assertThat(tileView.isTileAddedToLongPress).isTrue()
541         assertThat(tileView.isExpandableAddedToLongPress).isTrue()
542     }
543 
544     @Test
onInit_withoutLongPressEffect_longPressEffectDoesNotHaveTileAndExpandablenull545     fun onInit_withoutLongPressEffect_longPressEffectDoesNotHaveTileAndExpandable() {
546         tileView = FakeTileView(context, false, null)
547         val tile = kosmos.qsTileFactory.createTile("Test Tile")
548         tileView.init(tile)
549 
550         assertThat(tileView.isTileAddedToLongPress).isFalse()
551         assertThat(tileView.isExpandableAddedToLongPress).isFalse()
552     }
553 
554     class FakeTileView(
555         context: Context,
556         collapsed: Boolean,
557         private val longPressEffect: QSLongPressEffect?,
558     ) :
559         QSTileViewImpl(
560             ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings),
561             collapsed,
562             longPressEffect,
563         ) {
564         var constantLongPressEffectDuration = 500
565         val isTileAddedToLongPress: Boolean
566             get() = longPressEffect?.qsTile != null
567 
568         val isExpandableAddedToLongPress: Boolean
569             get() = longPressEffect?.expandable != null
570 
getLongPressEffectDurationnull571         override fun getLongPressEffectDuration(): Int = constantLongPressEffectDuration
572 
573         fun changeState(state: QSTile.State) {
574             handleStateChanged(state)
575         }
576     }
577 }
578