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.content.res.Configuration 17 import android.graphics.Rect 18 import android.platform.test.flag.junit.FlagsParameterization 19 import android.testing.TestableContext 20 import android.testing.TestableLooper 21 import android.testing.TestableLooper.RunWithLooper 22 import android.testing.ViewUtils 23 import android.view.ContextThemeWrapper 24 import android.view.View 25 import android.view.ViewGroup 26 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 27 import android.view.accessibility.AccessibilityNodeInfo 28 import android.widget.FrameLayout 29 import android.widget.LinearLayout 30 import androidx.test.filters.SmallTest 31 import com.android.systemui.SysuiTestCase 32 import com.android.systemui.flags.DisableSceneContainer 33 import com.android.systemui.flags.parameterizeSceneContainerFlag 34 import com.android.systemui.plugins.qs.QSTile 35 import com.android.systemui.plugins.qs.QSTileView 36 import com.android.systemui.qs.QSPanelControllerBase.TileRecord 37 import com.android.systemui.qs.logging.QSLogger 38 import com.android.systemui.qs.tileimpl.QSTileViewImpl 39 import com.android.systemui.res.R 40 import com.google.common.truth.Truth.assertThat 41 import org.junit.After 42 import org.junit.Before 43 import org.junit.Test 44 import org.junit.runner.RunWith 45 import org.mockito.Mock 46 import org.mockito.Mockito.mock 47 import org.mockito.Mockito.never 48 import org.mockito.Mockito.verify 49 import org.mockito.MockitoAnnotations 50 import platform.test.runner.parameterized.ParameterizedAndroidJunit4 51 import platform.test.runner.parameterized.Parameters 52 53 @RunWith(ParameterizedAndroidJunit4::class) 54 @RunWithLooper 55 @SmallTest 56 class QSPanelTest(flags: FlagsParameterization) : SysuiTestCase() { 57 58 init { 59 mSetFlagsRule.setFlagsParameterization(flags) 60 } 61 62 @Mock private lateinit var qsLogger: QSLogger 63 64 private lateinit var testableLooper: TestableLooper 65 private lateinit var qsPanel: QSPanel 66 67 private lateinit var footer: View 68 69 private val themedContext = 70 TestableContext(ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) 71 72 @Before 73 @Throws(Exception::class) setupnull74 fun setup() { 75 MockitoAnnotations.initMocks(this) 76 testableLooper = TestableLooper.get(this) 77 // Apply only the values of the theme that are not defined 78 79 testableLooper.runWithLooper { 80 qsPanel = QSPanel(themedContext, null) 81 qsPanel.mUsingMediaPlayer = true 82 83 qsPanel.initialize(qsLogger, true) 84 // QSPanel inflates a footer inside of it, mocking it here 85 footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } 86 qsPanel.addView(footer, MATCH_PARENT, 100) 87 qsPanel.onFinishInflate() 88 // Provides a parent with non-zero size for QSPanel 89 ViewUtils.attachView(qsPanel) 90 } 91 } 92 93 @After tearDownnull94 fun tearDown() { 95 if (qsPanel.isAttachedToWindow) { 96 ViewUtils.detachView(qsPanel) 97 } 98 } 99 100 @Test testHasCollapseAccessibilityActionnull101 fun testHasCollapseAccessibilityAction() { 102 val info = AccessibilityNodeInfo(qsPanel) 103 qsPanel.onInitializeAccessibilityNodeInfo(info) 104 105 assertThat(info.actions and AccessibilityNodeInfo.ACTION_COLLAPSE).isNotEqualTo(0) 106 assertThat(info.actions and AccessibilityNodeInfo.ACTION_EXPAND).isEqualTo(0) 107 } 108 109 @Test testCollapseActionCallsRunnablenull110 fun testCollapseActionCallsRunnable() { 111 val mockRunnable = mock(Runnable::class.java) 112 qsPanel.setCollapseExpandAction(mockRunnable) 113 114 qsPanel.performAccessibilityAction(AccessibilityNodeInfo.ACTION_COLLAPSE, null) 115 verify(mockRunnable).run() 116 } 117 118 @Test 119 @DisableSceneContainer testTilesFooterVisibleLandscapeMedianull120 fun testTilesFooterVisibleLandscapeMedia() { 121 // We need at least a tile so the layout has a height 122 qsPanel.tileLayout?.addTile( 123 QSPanelControllerBase.TileRecord( 124 mock(QSTile::class.java), 125 QSTileViewImpl(themedContext), 126 ) 127 ) 128 129 val mediaView = FrameLayout(themedContext) 130 mediaView.addView(View(themedContext), MATCH_PARENT, 800) 131 132 qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true) 133 qsPanel.measure( 134 /* width */ View.MeasureSpec.makeMeasureSpec(3000, View.MeasureSpec.EXACTLY), 135 /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), 136 ) 137 qsPanel.layout(0, 0, qsPanel.measuredWidth, qsPanel.measuredHeight) 138 139 val tiles = qsPanel.tileLayout as View 140 // Tiles are effectively to the left of media 141 assertThat(tiles isLeftOf mediaView).isTrue() 142 assertThat(tiles.isVisibleToUser).isTrue() 143 144 assertThat(footer isLeftOf mediaView).isTrue() 145 assertThat(footer.isVisibleToUser).isTrue() 146 } 147 148 @Test testBottomPaddingnull149 fun testBottomPadding() { 150 val padding = 10 151 themedContext.orCreateTestableResources.addOverride( 152 R.dimen.qs_panel_padding_bottom, 153 padding, 154 ) 155 qsPanel.updatePadding() 156 assertThat(qsPanel.paddingBottom).isEqualTo(padding) 157 } 158 159 @Test testTopPaddingnull160 fun testTopPadding() { 161 val padding = 10 162 val paddingCombined = 100 163 themedContext.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding) 164 themedContext.orCreateTestableResources.addOverride( 165 R.dimen.qs_panel_padding_top, 166 paddingCombined, 167 ) 168 169 qsPanel.updatePadding() 170 assertThat(qsPanel.paddingTop).isEqualTo(paddingCombined) 171 } 172 173 @Test testSetSquishinessFraction_noCrashnull174 fun testSetSquishinessFraction_noCrash() { 175 qsPanel.addView(qsPanel.mTileLayout as View, 0) 176 qsPanel.addView(FrameLayout(context)) 177 qsPanel.setSquishinessFraction(0.5f) 178 } 179 180 @Test testSplitShade_CollapseAccessibilityActionNotAnnouncednull181 fun testSplitShade_CollapseAccessibilityActionNotAnnounced() { 182 qsPanel.setCanCollapse(false) 183 val accessibilityInfo = mock(AccessibilityNodeInfo::class.java) 184 qsPanel.onInitializeAccessibilityNodeInfo(accessibilityInfo) 185 186 val actionCollapse = AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE 187 verify(accessibilityInfo, never()).addAction(actionCollapse) 188 } 189 190 @Test addTile_callbackAddednull191 fun addTile_callbackAdded() { 192 val tile = mock(QSTile::class.java) 193 val tileView = mock(QSTileView::class.java) 194 195 val record = TileRecord(tile, tileView) 196 197 qsPanel.addTile(record) 198 199 verify(tile).addCallback(record.callback) 200 } 201 202 @Test 203 @DisableSceneContainer initializedWithNoMedia_sceneContainerDisabled_tileLayoutParentIsAlwaysQsPanelnull204 fun initializedWithNoMedia_sceneContainerDisabled_tileLayoutParentIsAlwaysQsPanel() { 205 lateinit var panel: QSPanel 206 lateinit var tileLayout: View 207 testableLooper.runWithLooper { 208 panel = QSPanel(themedContext, null) 209 panel.mUsingMediaPlayer = true 210 211 panel.initialize(qsLogger, /* usingMediaPlayer= */ false) 212 tileLayout = panel.orCreateTileLayout as View 213 // QSPanel inflates a footer inside of it, mocking it here 214 footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } 215 panel.addView(footer, MATCH_PARENT, 100) 216 panel.onFinishInflate() 217 // Provides a parent with non-zero size for QSPanel 218 ViewUtils.attachView(panel) 219 } 220 val mockMediaHost = mock(ViewGroup::class.java) 221 222 panel.setUsingHorizontalLayout(false, mockMediaHost, true) 223 224 assertThat(tileLayout.parent).isSameInstanceAs(panel) 225 226 panel.setUsingHorizontalLayout(true, mockMediaHost, true) 227 assertThat(tileLayout.parent).isSameInstanceAs(panel) 228 229 ViewUtils.detachView(panel) 230 } 231 232 @Test 233 @DisableSceneContainer initializeWithNoMedia_mediaNeverAttachednull234 fun initializeWithNoMedia_mediaNeverAttached() { 235 lateinit var panel: QSPanel 236 testableLooper.runWithLooper { 237 panel = QSPanel(themedContext, null) 238 panel.mUsingMediaPlayer = true 239 240 panel.initialize(qsLogger, /* usingMediaPlayer= */ false) 241 panel.orCreateTileLayout as View 242 // QSPanel inflates a footer inside of it, mocking it here 243 footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } 244 panel.addView(footer, MATCH_PARENT, 100) 245 panel.onFinishInflate() 246 // Provides a parent with non-zero size for QSPanel 247 ViewUtils.attachView(panel) 248 } 249 val mockMediaHost = FrameLayout(themedContext) 250 251 panel.setUsingHorizontalLayout(false, mockMediaHost, true) 252 assertThat(mockMediaHost.parent).isNull() 253 254 panel.setUsingHorizontalLayout(true, mockMediaHost, true) 255 assertThat(mockMediaHost.parent).isNull() 256 257 ViewUtils.detachView(panel) 258 } 259 260 @Test setRowColumnLayoutnull261 fun setRowColumnLayout() { 262 qsPanel.setColumnRowLayout(/* withMedia= */ false) 263 264 assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(1) 265 assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(4) 266 267 qsPanel.setColumnRowLayout(/* withMedia= */ true) 268 269 assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(2) 270 assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(2) 271 } 272 273 @Test noPendingConfigChangesAtBeginningnull274 fun noPendingConfigChangesAtBeginning() { 275 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse() 276 } 277 278 @Test configChangesWhileDetached_pendingConfigChangesnull279 fun configChangesWhileDetached_pendingConfigChanges() { 280 ViewUtils.detachView(qsPanel) 281 282 qsPanel.onConfigurationChanged(Configuration()) 283 284 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue() 285 } 286 287 @Test configChangesWhileDetached_reattach_pendingConfigChangesnull288 fun configChangesWhileDetached_reattach_pendingConfigChanges() { 289 ViewUtils.detachView(qsPanel) 290 291 qsPanel.onConfigurationChanged(Configuration()) 292 testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) } 293 294 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue() 295 } 296 297 @Test configChangesWhileDetached_reattach_detach_pendingConfigChanges_resetnull298 fun configChangesWhileDetached_reattach_detach_pendingConfigChanges_reset() { 299 ViewUtils.detachView(qsPanel) 300 301 qsPanel.onConfigurationChanged(Configuration()) 302 303 testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) } 304 ViewUtils.detachView(qsPanel) 305 306 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse() 307 } 308 309 @Test configChangeWhileAttached_noPendingConfigChangesnull310 fun configChangeWhileAttached_noPendingConfigChanges() { 311 qsPanel.onConfigurationChanged(Configuration()) 312 313 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse() 314 } 315 316 @Test configChangeWhileAttachedWithPending_doesntResetPendingnull317 fun configChangeWhileAttachedWithPending_doesntResetPending() { 318 ViewUtils.detachView(qsPanel) 319 320 qsPanel.onConfigurationChanged(Configuration()) 321 322 testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) } 323 324 qsPanel.onConfigurationChanged(Configuration()) 325 326 assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue() 327 } 328 329 companion object { getParamsnull330 @Parameters(name = "{0}") @JvmStatic fun getParams() = parameterizeSceneContainerFlag() 331 } 332 333 private infix fun View.isLeftOf(other: View): Boolean { 334 val rect = Rect() 335 getBoundsOnScreen(rect) 336 val thisRight = rect.right 337 338 other.getBoundsOnScreen(rect) 339 340 return thisRight <= rect.left 341 } 342 } 343