1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.systemui.qs
15 
16 import android.graphics.Rect
17 import android.testing.AndroidTestingRunner
18 import android.testing.TestableContext
19 import android.testing.TestableLooper
20 import android.testing.TestableLooper.RunWithLooper
21 import android.testing.ViewUtils
22 import android.view.ContextThemeWrapper
23 import android.view.View
24 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
25 import android.view.accessibility.AccessibilityNodeInfo
26 import android.widget.FrameLayout
27 import android.widget.LinearLayout
28 import androidx.test.filters.SmallTest
29 import com.android.systemui.R
30 import com.android.systemui.SysuiTestCase
31 import com.android.systemui.plugins.qs.QSTile
32 import com.android.systemui.plugins.qs.QSTileView
33 import com.android.systemui.qs.QSPanelControllerBase.TileRecord
34 import com.android.systemui.qs.logging.QSLogger
35 import com.android.systemui.qs.tileimpl.QSIconViewImpl
36 import com.android.systemui.qs.tileimpl.QSTileViewImpl
37 import com.google.common.truth.Truth.assertThat
38 import org.junit.After
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.Mockito.mock
44 import org.mockito.Mockito.never
45 import org.mockito.Mockito.verify
46 import org.mockito.MockitoAnnotations
47 
48 @RunWith(AndroidTestingRunner::class)
49 @RunWithLooper
50 @SmallTest
51 class QSPanelTest : SysuiTestCase() {
52 
53     @Mock private lateinit var qsLogger: QSLogger
54 
55     private lateinit var testableLooper: TestableLooper
56     private lateinit var qsPanel: QSPanel
57 
58     private lateinit var footer: View
59 
60     private val themedContext = TestableContext(
61             ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
62     )
63 
64     @Before
65     @Throws(Exception::class)
66     fun setup() {
67         MockitoAnnotations.initMocks(this)
68         testableLooper = TestableLooper.get(this)
69         // Apply only the values of the theme that are not defined
70 
71         testableLooper.runWithLooper {
72             qsPanel = QSPanel(themedContext, null)
73             qsPanel.mUsingMediaPlayer = true
74 
75             qsPanel.initialize(qsLogger)
76             // QSPanel inflates a footer inside of it, mocking it here
77             footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
78             qsPanel.addView(footer, MATCH_PARENT, 100)
79             qsPanel.onFinishInflate()
80             // Provides a parent with non-zero size for QSPanel
81             ViewUtils.attachView(qsPanel)
82         }
83     }
84 
85     @After
86     fun tearDown() {
87         ViewUtils.detachView(qsPanel)
88     }
89 
90     @Test
91     fun testHasCollapseAccessibilityAction() {
92         val info = AccessibilityNodeInfo(qsPanel)
93         qsPanel.onInitializeAccessibilityNodeInfo(info)
94 
95         assertThat(info.actions and AccessibilityNodeInfo.ACTION_COLLAPSE).isNotEqualTo(0)
96         assertThat(info.actions and AccessibilityNodeInfo.ACTION_EXPAND).isEqualTo(0)
97     }
98 
99     @Test
100     fun testCollapseActionCallsRunnable() {
101         val mockRunnable = mock(Runnable::class.java)
102         qsPanel.setCollapseExpandAction(mockRunnable)
103 
104         qsPanel.performAccessibilityAction(AccessibilityNodeInfo.ACTION_COLLAPSE, null)
105         verify(mockRunnable).run()
106     }
107 
108     @Test
109     fun testTilesFooterVisibleRTLLandscapeMedia() {
110         qsPanel.layoutDirection = View.LAYOUT_DIRECTION_RTL
111         // We need at least a tile so the layout has a height
112         qsPanel.tileLayout?.addTile(
113                 QSPanelControllerBase.TileRecord(
114                     mock(QSTile::class.java),
115                     QSTileViewImpl(themedContext, QSIconViewImpl(themedContext))
116                 )
117             )
118 
119         val mediaView = FrameLayout(themedContext)
120         mediaView.addView(View(themedContext), MATCH_PARENT, 800)
121 
122         qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
123         qsPanel.measure(
124             /* width */ View.MeasureSpec.makeMeasureSpec(3000, View.MeasureSpec.EXACTLY),
125             /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)
126         )
127         qsPanel.layout(0, 0, qsPanel.measuredWidth, qsPanel.measuredHeight)
128 
129         val tiles = qsPanel.tileLayout as View
130         // Tiles are effectively to the right of media
131         assertThat(mediaView isLeftOf tiles)
132         assertThat(tiles.isVisibleToUser).isTrue()
133 
134         assertThat(mediaView isLeftOf footer)
135         assertThat(footer.isVisibleToUser).isTrue()
136     }
137 
138     @Test
139     fun testTilesFooterVisibleLandscapeMedia() {
140         qsPanel.layoutDirection = View.LAYOUT_DIRECTION_LTR
141         // We need at least a tile so the layout has a height
142         qsPanel.tileLayout?.addTile(
143             QSPanelControllerBase.TileRecord(
144                 mock(QSTile::class.java),
145                 QSTileViewImpl(themedContext, QSIconViewImpl(themedContext))
146             )
147         )
148 
149         val mediaView = FrameLayout(themedContext)
150         mediaView.addView(View(themedContext), MATCH_PARENT, 800)
151 
152         qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
153         qsPanel.measure(
154             /* width */ View.MeasureSpec.makeMeasureSpec(3000, View.MeasureSpec.EXACTLY),
155             /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)
156         )
157         qsPanel.layout(0, 0, qsPanel.measuredWidth, qsPanel.measuredHeight)
158 
159         val tiles = qsPanel.tileLayout as View
160         // Tiles are effectively to the left of media
161         assertThat(tiles isLeftOf mediaView)
162         assertThat(tiles.isVisibleToUser).isTrue()
163 
164         assertThat(footer isLeftOf mediaView)
165         assertThat(footer.isVisibleToUser).isTrue()
166     }
167 
168     @Test
169     fun testBottomPadding() {
170         val padding = 10
171         themedContext.orCreateTestableResources.addOverride(
172                 R.dimen.qs_panel_padding_bottom,
173                 padding
174         )
175         qsPanel.updatePadding()
176         assertThat(qsPanel.paddingBottom).isEqualTo(padding)
177     }
178 
179     @Test
180     fun testTopPadding() {
181         val padding = 10
182         val paddingCombined = 100
183         themedContext.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
184         themedContext.orCreateTestableResources.addOverride(
185                 R.dimen.qs_panel_padding_top,
186                 paddingCombined
187         )
188 
189         qsPanel.updatePadding()
190         assertThat(qsPanel.paddingTop).isEqualTo(paddingCombined)
191     }
192 
193     @Test
194     fun testSetSquishinessFraction_noCrash() {
195         qsPanel.addView(qsPanel.mTileLayout as View, 0)
196         qsPanel.addView(FrameLayout(context))
197         qsPanel.setSquishinessFraction(0.5f)
198     }
199 
200     @Test
201     fun testSplitShade_CollapseAccessibilityActionNotAnnounced() {
202         qsPanel.setCanCollapse(false)
203         val accessibilityInfo = mock(AccessibilityNodeInfo::class.java)
204         qsPanel.onInitializeAccessibilityNodeInfo(accessibilityInfo)
205 
206         val actionCollapse = AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE
207         verify(accessibilityInfo, never()).addAction(actionCollapse)
208     }
209 
210     @Test
211     fun addTile_callbackAdded() {
212         val tile = mock(QSTile::class.java)
213         val tileView = mock(QSTileView::class.java)
214 
215         val record = TileRecord(tile, tileView)
216 
217         qsPanel.addTile(record)
218 
219         verify(tile).addCallback(record.callback)
220     }
221 
222     private infix fun View.isLeftOf(other: View): Boolean {
223         val rect = Rect()
224         getBoundsOnScreen(rect)
225         val thisRight = rect.right
226 
227         other.getBoundsOnScreen(rect)
228 
229         return thisRight <= rect.left
230     }
231 }
232