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.drawable.Drawable
21 import android.service.quicksettings.Tile
22 import android.testing.AndroidTestingRunner
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.filters.SmallTest
30 import com.android.systemui.R
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.plugins.qs.QSIconView
33 import com.android.systemui.plugins.qs.QSTile
34 import com.google.common.truth.Truth.assertThat
35 import org.junit.Before
36 import org.junit.Test
37 import org.junit.runner.RunWith
38 import org.mockito.Mock
39 import org.mockito.MockitoAnnotations
40 
41 @RunWith(AndroidTestingRunner::class)
42 @SmallTest
43 @TestableLooper.RunWithLooper(setAsMainLooper = true)
44 class QSTileViewImplTest : SysuiTestCase() {
45 
46     @Mock
47     private lateinit var iconView: QSIconView
48     @Mock
49     private lateinit var customDrawable: Drawable
50 
51     private lateinit var tileView: FakeTileView
52     private lateinit var customDrawableView: View
53     private lateinit var chevronView: View
54 
55     @Before
56     fun setUp() {
57         MockitoAnnotations.initMocks(this)
58         context.ensureTestableResources()
59 
60         tileView = FakeTileView(context, iconView, false)
61         customDrawableView = tileView.requireViewById(R.id.customDrawable)
62         chevronView = tileView.requireViewById(R.id.chevron)
63     }
64 
65     @Test
66     fun testSecondaryLabelNotModified_unavailable() {
67         val state = QSTile.State()
68         val testString = "TEST STRING"
69         state.state = Tile.STATE_UNAVAILABLE
70         state.secondaryLabel = testString
71 
72         tileView.changeState(state)
73 
74         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
75     }
76 
77     @Test
78     fun testSecondaryLabelNotModified_booleanInactive() {
79         val state = QSTile.BooleanState()
80         val testString = "TEST STRING"
81         state.state = Tile.STATE_INACTIVE
82         state.secondaryLabel = testString
83 
84         tileView.changeState(state)
85 
86         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
87     }
88 
89     @Test
90     fun testSecondaryLabelNotModified_booleanActive() {
91         val state = QSTile.BooleanState()
92         val testString = "TEST STRING"
93         state.state = Tile.STATE_ACTIVE
94         state.secondaryLabel = testString
95 
96         tileView.changeState(state)
97 
98         assertThat(state.secondaryLabel as CharSequence).isEqualTo(testString)
99     }
100 
101     @Test
102     fun testSecondaryLabelNotModified_availableNotBoolean_inactive() {
103         val state = QSTile.State()
104         state.state = Tile.STATE_INACTIVE
105         state.secondaryLabel = ""
106 
107         tileView.changeState(state)
108 
109         assertThat(TextUtils.isEmpty(state.secondaryLabel)).isTrue()
110     }
111 
112     @Test
113     fun testSecondaryLabelNotModified_availableNotBoolean_active() {
114         val state = QSTile.State()
115         state.state = Tile.STATE_ACTIVE
116         state.secondaryLabel = ""
117 
118         tileView.changeState(state)
119 
120         assertThat(TextUtils.isEmpty(state.secondaryLabel)).isTrue()
121     }
122 
123     @Test
124     fun testSecondaryLabelDescription_unavailable_default() {
125         val state = QSTile.State()
126         state.state = Tile.STATE_UNAVAILABLE
127         state.secondaryLabel = ""
128 
129         tileView.changeState(state)
130 
131         assertThat(state.secondaryLabel as CharSequence).isEqualTo(
132             context.getString(R.string.tile_unavailable)
133         )
134     }
135 
136     @Test
137     fun testSecondaryLabelDescription_booleanInactive_default() {
138         val state = QSTile.BooleanState()
139         state.state = Tile.STATE_INACTIVE
140         state.secondaryLabel = ""
141 
142         tileView.changeState(state)
143 
144         assertThat(state.secondaryLabel as CharSequence).isEqualTo(
145             context.getString(R.string.switch_bar_off)
146         )
147     }
148 
149     @Test
150     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).isEqualTo(
158             context.getString(R.string.switch_bar_on)
159         )
160     }
161 
162     @Test
163     fun testShowCustomDrawableViewBooleanState() {
164         val state = QSTile.BooleanState()
165         state.sideViewCustomDrawable = customDrawable
166 
167         tileView.changeState(state)
168 
169         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
170         assertThat(chevronView.visibility).isEqualTo(View.GONE)
171     }
172 
173     @Test
174     fun testShowCustomDrawableViewNonBooleanState() {
175         val state = QSTile.State()
176         state.sideViewCustomDrawable = customDrawable
177 
178         tileView.changeState(state)
179 
180         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
181         assertThat(chevronView.visibility).isEqualTo(View.GONE)
182     }
183 
184     @Test
185     fun testShowCustomDrawableViewBooleanStateForceChevron() {
186         val state = QSTile.BooleanState()
187         state.sideViewCustomDrawable = customDrawable
188         state.forceExpandIcon = true
189 
190         tileView.changeState(state)
191 
192         assertThat(customDrawableView.visibility).isEqualTo(View.VISIBLE)
193         assertThat(chevronView.visibility).isEqualTo(View.GONE)
194     }
195 
196     @Test
197     fun testShowChevronNonBooleanState() {
198         val state = QSTile.State()
199 
200         tileView.changeState(state)
201 
202         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
203         assertThat(chevronView.visibility).isEqualTo(View.VISIBLE)
204     }
205 
206     @Test
207     fun testShowChevronBooleanStateForcheShow() {
208         val state = QSTile.BooleanState()
209         state.forceExpandIcon = true
210 
211         tileView.changeState(state)
212 
213         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
214         assertThat(chevronView.visibility).isEqualTo(View.VISIBLE)
215     }
216 
217     @Test
218     fun testNoImageShown() {
219         val state = QSTile.BooleanState()
220 
221         tileView.changeState(state)
222 
223         assertThat(customDrawableView.visibility).isEqualTo(View.GONE)
224         assertThat(chevronView.visibility).isEqualTo(View.GONE)
225     }
226 
227     @Test
228     fun testUseStateStringsForKnownSpec_Boolean() {
229         val state = QSTile.BooleanState()
230         val spec = "internet"
231         state.spec = spec
232 
233         val unavailableString = "${spec}_unavailable"
234         val offString = "${spec}_off"
235         val onString = "${spec}_on"
236 
237         context.orCreateTestableResources.addOverride(R.array.tile_states_internet, arrayOf(
238             unavailableString,
239             offString,
240             onString
241         ))
242 
243         // State UNAVAILABLE
244         state.secondaryLabel = ""
245         state.state = Tile.STATE_UNAVAILABLE
246         tileView.changeState(state)
247         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(unavailableString)
248 
249         // State INACTIVE
250         state.secondaryLabel = ""
251         state.state = Tile.STATE_INACTIVE
252         tileView.changeState(state)
253         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(offString)
254 
255         // State ACTIVE
256         state.secondaryLabel = ""
257         state.state = Tile.STATE_ACTIVE
258         tileView.changeState(state)
259         assertThat((tileView.secondaryLabel as TextView).text).isEqualTo(onString)
260     }
261 
262     @Test
263     fun testCollectionItemInfoHasPosition() {
264         val position = 5
265         tileView.setPosition(position)
266 
267         val info = AccessibilityNodeInfo(tileView)
268         tileView.onInitializeAccessibilityNodeInfo(info)
269 
270         assertThat(info.collectionItemInfo.rowIndex).isEqualTo(position)
271         assertThat(info.collectionItemInfo.rowSpan).isEqualTo(1)
272         assertThat(info.collectionItemInfo.columnIndex).isEqualTo(0)
273         assertThat(info.collectionItemInfo.columnSpan).isEqualTo(1)
274     }
275 
276     @Test
277     fun testCollectionItemInfoNoPosition() {
278         val info = AccessibilityNodeInfo(tileView)
279         tileView.onInitializeAccessibilityNodeInfo(info)
280 
281         assertThat(info.collectionItemInfo).isNull()
282     }
283 
284     @Test
285     fun testDisabledByPolicyInactive_usesUnavailableColors() {
286         val stateDisabledByPolicy = QSTile.State()
287         stateDisabledByPolicy.state = Tile.STATE_INACTIVE
288         stateDisabledByPolicy.disabledByPolicy = true
289 
290         val stateUnavailable = QSTile.State()
291         stateUnavailable.state = Tile.STATE_UNAVAILABLE
292 
293         tileView.changeState(stateDisabledByPolicy)
294         val colorsDisabledByPolicy = tileView.getCurrentColors()
295 
296         tileView.changeState(stateUnavailable)
297         val colorsUnavailable = tileView.getCurrentColors()
298 
299         assertThat(colorsDisabledByPolicy).containsExactlyElementsIn(colorsUnavailable)
300     }
301 
302     @Test
303     fun testDisabledByPolicyActive_usesUnavailableColors() {
304         val stateDisabledByPolicy = QSTile.State()
305         stateDisabledByPolicy.state = Tile.STATE_ACTIVE
306         stateDisabledByPolicy.disabledByPolicy = true
307 
308         val stateUnavailable = QSTile.State()
309         stateUnavailable.state = Tile.STATE_UNAVAILABLE
310 
311         tileView.changeState(stateDisabledByPolicy)
312         val colorsDisabledByPolicy = tileView.getCurrentColors()
313 
314         tileView.changeState(stateUnavailable)
315         val colorsUnavailable = tileView.getCurrentColors()
316 
317         assertThat(colorsDisabledByPolicy).containsExactlyElementsIn(colorsUnavailable)
318     }
319 
320     @Test
321     fun testDisableByPolicyThenRemoved_changesColor() {
322         val stateActive = QSTile.State()
323         stateActive.state = Tile.STATE_ACTIVE
324 
325         val stateDisabledByPolicy = stateActive.copy()
326         stateDisabledByPolicy.disabledByPolicy = true
327 
328         tileView.changeState(stateActive)
329         val activeColors = tileView.getCurrentColors()
330 
331         tileView.changeState(stateDisabledByPolicy)
332         // It has unavailable colors
333         assertThat(tileView.getCurrentColors()).isNotEqualTo(activeColors)
334 
335         // When we get back to not disabled by policy tile, it should go back to active colors
336         tileView.changeState(stateActive)
337         assertThat(tileView.getCurrentColors()).containsExactlyElementsIn(activeColors)
338     }
339 
340     @Test
341     fun testDisabledByPolicy_secondaryLabelText() {
342         val testA11yLabel = "TEST_LABEL"
343         context.orCreateTestableResources
344                 .addOverride(
345                         R.string.accessibility_tile_disabled_by_policy_action_description,
346                         testA11yLabel
347                 )
348 
349         val stateDisabledByPolicy = QSTile.State()
350         stateDisabledByPolicy.state = Tile.STATE_INACTIVE
351         stateDisabledByPolicy.disabledByPolicy = true
352 
353         tileView.changeState(stateDisabledByPolicy)
354 
355         val info = AccessibilityNodeInfo(tileView)
356         tileView.onInitializeAccessibilityNodeInfo(info)
357         assertThat(
358                 info.actionList.find {
359                         it.id == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id
360                 }?.label
361         ).isEqualTo(testA11yLabel)
362     }
363 
364     @Test
365     fun testDisabledByPolicy_unavailableInStateDescription() {
366         val state = QSTile.BooleanState()
367         val spec = "internet"
368         state.spec = spec
369         state.disabledByPolicy = true
370         state.state = Tile.STATE_INACTIVE
371 
372         val unavailableString = "${spec}_unavailable"
373         val offString = "${spec}_off"
374         val onString = "${spec}_on"
375 
376         context.orCreateTestableResources.addOverride(R.array.tile_states_internet, arrayOf(
377                 unavailableString,
378                 offString,
379                 onString
380         ))
381 
382         tileView.changeState(state)
383         assertThat(tileView.stateDescription?.contains(unavailableString)).isTrue()
384     }
385 
386     class FakeTileView(
387         context: Context,
388         icon: QSIconView,
389         collapsed: Boolean
390     ) : QSTileViewImpl(
391             ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings),
392             icon,
393             collapsed
394     ) {
395         fun changeState(state: QSTile.State) {
396             handleStateChanged(state)
397         }
398     }
399 }
400