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