1 /* <lambda>null2 * Copyright (C) 2023 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 android.graphics.cts 18 19 import android.R 20 import android.app.UiModeManager 21 import android.content.Context 22 import android.graphics.Color 23 import android.graphics.cts.R as CtsR 24 import android.provider.Settings 25 import android.util.Log 26 import android.util.Pair 27 import androidx.annotation.ColorInt 28 import androidx.core.graphics.ColorUtils 29 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 30 import com.android.compatibility.common.util.CddTest 31 import com.android.compatibility.common.util.FeatureUtil 32 import com.android.compatibility.common.util.SystemUtil.runShellCommand 33 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity 34 import com.google.common.truth.Truth.assertWithMessage 35 import com.google.ux.material.libmonet.contrast.Contrast 36 import com.google.ux.material.libmonet.hct.Hct 37 import java.io.Serializable 38 import java.util.Arrays 39 import java.util.Locale 40 import kotlin.math.abs 41 import org.junit.AfterClass 42 import org.junit.Assert 43 import org.junit.BeforeClass 44 import org.junit.FixMethodOrder 45 import org.junit.Test 46 import org.junit.runner.RunWith 47 import org.junit.runners.MethodSorters 48 import org.junit.runners.Parameterized 49 import org.xmlpull.v1.XmlPullParser 50 51 @RunWith(Parameterized::class) 52 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 53 class SystemPaletteTest( 54 private val color: String, 55 private val style: String, 56 private val expectedPalette: IntArray 57 ) { 58 @Test 59 @CddTest(requirements = ["3.8.6/C-1-4,C-1-5,C-1-6"]) 60 fun a_testThemeStyles() { 61 // THEME_CUSTOMIZATION_OVERLAY_PACKAGES is not available in Wear OS 62 if (FeatureUtil.isWatch()) return 63 64 val newSetting = assurePaletteSetting() 65 66 assertWithMessage("Invalid tonal palettes for $color $style").that(newSetting).isTrue() 67 } 68 69 @Test 70 @CddTest(requirements = ["3.8.6/C-1-4,C-1-5,C-1-6"]) 71 fun b_testShades0and1000() { 72 val context = getInstrumentation().targetContext 73 74 assurePaletteSetting(context) 75 76 Log.d(TAG, "Color: $color, Style: $style") 77 78 val allPalettes = listOf( 79 getAllAccent1Colors(context), 80 getAllAccent2Colors(context), 81 getAllAccent3Colors(context), 82 getAllNeutral1Colors(context), 83 getAllNeutral2Colors(context), 84 ) 85 86 allPalettes.forEach { palette -> 87 assertColor(palette.first(), Color.WHITE) 88 assertColor(palette.last(), Color.BLACK) 89 } 90 } 91 92 @Test 93 @CddTest(requirements = ["3.8.6/C-1-4,C-1-5,C-1-6"]) 94 fun c_testColorsMatchExpectedLuminance() { 95 val context = getInstrumentation().targetContext 96 97 assurePaletteSetting(context) 98 99 val allPalettes = listOf( 100 getAllAccent1Colors(context), 101 getAllAccent2Colors(context), 102 getAllAccent3Colors(context), 103 getAllNeutral1Colors(context), 104 getAllNeutral2Colors(context) 105 ) 106 107 val labColor = doubleArrayOf(0.0, 0.0, 0.0) 108 val expectedL = doubleArrayOf( 109 100.0, 99.0, 95.0, 90.0, 80.0, 70.0, 60.0, 49.0, 40.0, 30.0, 20.0, 10.0, 0.0) 110 111 allPalettes.forEach { palette -> 112 palette.forEachIndexed { i, paletteColor -> 113 val expectedColor = expectedL[i] 114 ColorUtils.colorToLAB(paletteColor, labColor) 115 assertWithMessage( 116 "Color ${Integer.toHexString(paletteColor)} at index $i should " + 117 "have L $expectedColor in LAB space." 118 ).that(labColor[0]).isWithin(3.0).of(expectedColor) 119 } 120 } 121 } 122 123 @Test 124 @CddTest(requirements = ["3.8.6/C-1-4,C-1-5,C-1-6"]) 125 fun d_testContrastRatio() { 126 val context = getInstrumentation().targetContext 127 128 assurePaletteSetting(context) 129 130 val atLeast4dot45 = listOf( 131 Pair(0, 500), 132 Pair(50, 600), 133 Pair(100, 600), 134 Pair(200, 700), 135 Pair(300, 800), 136 Pair(400, 900), 137 Pair(500, 1000) 138 ) 139 140 val atLeast3dot0 = listOf( 141 Pair(0, 400), 142 Pair(50, 500), 143 Pair(100, 500), 144 Pair(200, 600), 145 Pair(300, 700), 146 Pair(400, 800), 147 Pair(500, 900), 148 Pair(600, 1000) 149 ) 150 151 val allPalettes = 152 listOf( 153 getAllAccent1Colors(context), 154 getAllAccent2Colors(context), 155 getAllAccent3Colors(context), 156 getAllNeutral1Colors(context), 157 getAllNeutral2Colors(context) 158 ) 159 160 fun pairContrastCheck(palette: IntArray, shades: Pair<Int, Int>, contrastLevel: Double) { 161 val background = palette[shadeToArrayIndex(shades.first)] 162 val foreground = palette[shadeToArrayIndex(shades.second)] 163 164 val contrast = Contrast.ratioOfTones( 165 Hct.fromInt(foreground).tone, 166 Hct.fromInt(background).tone 167 ) 168 169 assertWithMessage( 170 "Shade ${shades.first} (#${Integer.toHexString(background)}) " + 171 "should have at least $contrastLevel contrast ratio against " + 172 "${shades.second} (#${Integer.toHexString(foreground)}), but had $contrast" 173 ).that(contrast).isGreaterThan(contrastLevel) 174 } 175 176 allPalettes.forEach { palette -> 177 atLeast4dot45.forEach { shades -> pairContrastCheck(palette, shades, 4.45) } 178 atLeast3dot0.forEach { shades -> pairContrastCheck(palette, shades, 3.0) } 179 } 180 } 181 182 @Test 183 fun e_testDynamicColorContrast() { 184 val context = getInstrumentation().targetContext 185 186 // Ideally this should be 3.0, but there's colorspace conversion that causes rounding 187 // errors. 188 val foregroundContrast = 2.9f 189 assurePaletteSetting(context) 190 191 val bulkTest: BulkContrastTester = BulkContrastTester.of( 192 // Colors against Surface [DARK] 193 ContrastTester.ofBackgrounds(context, 194 R.color.system_surface_dark, 195 R.color.system_surface_dim_dark, 196 R.color.system_surface_bright_dark, 197 R.color.system_surface_container_dark, 198 R.color.system_surface_container_high_dark, 199 R.color.system_surface_container_highest_dark, 200 R.color.system_surface_container_low_dark, 201 R.color.system_surface_container_lowest_dark, 202 R.color.system_surface_variant_dark 203 ).andForegrounds( 204 4.5f, 205 R.color.system_on_surface_dark, 206 R.color.system_on_surface_variant_dark, 207 R.color.system_primary_dark, 208 R.color.system_secondary_dark, 209 R.color.system_tertiary_dark, 210 R.color.system_error_dark 211 ).andForegrounds( 212 foregroundContrast, 213 R.color.system_outline_dark 214 ), 215 216 // Colors against Surface [LIGHT] 217 ContrastTester.ofBackgrounds(context, 218 R.color.system_surface_light, 219 R.color.system_surface_dim_light, 220 R.color.system_surface_bright_light, 221 R.color.system_surface_container_light, 222 R.color.system_surface_container_high_light, 223 R.color.system_surface_container_highest_light, 224 R.color.system_surface_container_low_light, 225 R.color.system_surface_container_lowest_light, 226 R.color.system_surface_variant_light 227 ).andForegrounds( 228 4.5f, 229 R.color.system_on_surface_light, 230 R.color.system_on_surface_variant_light, 231 R.color.system_primary_light, 232 R.color.system_secondary_light, 233 R.color.system_tertiary_light, 234 R.color.system_error_light 235 ).andForegrounds( 236 foregroundContrast, 237 R.color.system_outline_light 238 ), 239 240 // Colors against accents [DARK] 241 ContrastTester.ofBackgrounds( 242 context, 243 R.color.system_primary_dark 244 ).andForegrounds( 245 4.5f, 246 R.color.system_on_primary_dark 247 ), 248 249 ContrastTester.ofBackgrounds( 250 context, 251 R.color.system_primary_container_dark 252 ).andForegrounds( 253 4.5f, 254 R.color.system_on_primary_container_dark 255 ), 256 257 ContrastTester.ofBackgrounds( 258 context, 259 R.color.system_secondary_dark 260 ).andForegrounds( 261 4.5f, 262 R.color.system_on_secondary_dark 263 ), 264 265 ContrastTester.ofBackgrounds( 266 context, 267 R.color.system_secondary_container_dark 268 ).andForegrounds( 269 4.5f, 270 R.color.system_on_secondary_container_dark 271 ), 272 273 ContrastTester.ofBackgrounds( 274 context, 275 R.color.system_tertiary_dark 276 ).andForegrounds( 277 4.5f, 278 R.color.system_on_tertiary_dark 279 ), 280 281 ContrastTester.ofBackgrounds( 282 context, 283 R.color.system_tertiary_container_dark 284 ).andForegrounds( 285 4.5f, 286 R.color.system_on_tertiary_container_dark 287 ), 288 289 // Colors against accents [LIGHT] 290 ContrastTester.ofBackgrounds( 291 context, 292 R.color.system_primary_light 293 ).andForegrounds( 294 4.5f, 295 R.color.system_on_primary_light 296 ), 297 298 ContrastTester.ofBackgrounds( 299 context, 300 R.color.system_primary_container_light 301 ).andForegrounds( 302 4.5f, 303 R.color.system_on_primary_container_light 304 ), 305 306 ContrastTester.ofBackgrounds( 307 context, 308 R.color.system_secondary_light 309 ).andForegrounds( 310 4.5f, 311 R.color.system_on_secondary_light 312 ), 313 314 ContrastTester.ofBackgrounds( 315 context, 316 R.color.system_secondary_container_light 317 ).andForegrounds( 318 4.5f, 319 R.color.system_on_secondary_container_light 320 ), 321 322 ContrastTester.ofBackgrounds( 323 context, 324 R.color.system_tertiary_light 325 ).andForegrounds( 326 4.5f, 327 R.color.system_on_tertiary_light 328 ), 329 330 ContrastTester.ofBackgrounds( 331 context, 332 R.color.system_tertiary_container_light 333 ).andForegrounds( 334 4.5f, 335 R.color.system_on_tertiary_container_light 336 ), 337 338 // Colors against accents [FIXED] 339 ContrastTester.ofBackgrounds( 340 context, 341 R.color.system_primary_fixed, 342 R.color.system_primary_fixed_dim 343 ).andForegrounds( 344 4.5f, 345 R.color.system_on_primary_fixed, 346 R.color.system_on_primary_fixed_variant 347 ), 348 349 ContrastTester.ofBackgrounds( 350 context, 351 R.color.system_secondary_fixed, 352 R.color.system_secondary_fixed_dim 353 ).andForegrounds( 354 4.5f, 355 R.color.system_on_secondary_fixed, 356 R.color.system_on_secondary_fixed_variant 357 ), 358 359 ContrastTester.ofBackgrounds( 360 context, 361 R.color.system_tertiary_fixed, 362 R.color.system_tertiary_fixed_dim 363 ).andForegrounds( 364 4.5f, 365 R.color.system_on_tertiary_fixed, 366 R.color.system_on_tertiary_fixed_variant 367 ), 368 369 // Auxiliary Colors [DARK] 370 ContrastTester.ofBackgrounds( 371 context, 372 R.color.system_error_dark 373 ).andForegrounds( 374 4.5f, 375 R.color.system_on_error_dark 376 ), 377 378 ContrastTester.ofBackgrounds( 379 context, 380 R.color.system_error_container_dark 381 ).andForegrounds( 382 4.5f, 383 R.color.system_on_error_container_dark 384 ), 385 386 // Auxiliary Colors [LIGHT] 387 ContrastTester.ofBackgrounds( 388 context, 389 R.color.system_error_light 390 ).andForegrounds( 391 4.5f, 392 R.color.system_on_error_light 393 ), 394 395 ContrastTester.ofBackgrounds( 396 context, 397 R.color.system_error_container_light 398 ).andForegrounds( 399 4.5f, 400 R.color.system_on_error_container_light 401 ) 402 ) 403 bulkTest.run() 404 assertWithMessage(bulkTest.allMessages).that(bulkTest.testPassed).isTrue() 405 } 406 407 private fun getAllAccent1Colors(context: Context): IntArray { 408 return getAllResourceColors( 409 context, 410 R.color.system_accent1_0, 411 R.color.system_accent1_10, 412 R.color.system_accent1_50, 413 R.color.system_accent1_100, 414 R.color.system_accent1_200, 415 R.color.system_accent1_300, 416 R.color.system_accent1_400, 417 R.color.system_accent1_500, 418 R.color.system_accent1_600, 419 R.color.system_accent1_700, 420 R.color.system_accent1_800, 421 R.color.system_accent1_900, 422 R.color.system_accent1_1000 423 ) 424 } 425 426 private fun getAllAccent2Colors(context: Context): IntArray { 427 return getAllResourceColors( 428 context, 429 R.color.system_accent2_0, 430 R.color.system_accent2_10, 431 R.color.system_accent2_50, 432 R.color.system_accent2_100, 433 R.color.system_accent2_200, 434 R.color.system_accent2_300, 435 R.color.system_accent2_400, 436 R.color.system_accent2_500, 437 R.color.system_accent2_600, 438 R.color.system_accent2_700, 439 R.color.system_accent2_800, 440 R.color.system_accent2_900, 441 R.color.system_accent2_1000 442 ) 443 } 444 445 private fun getAllAccent3Colors(context: Context): IntArray { 446 return getAllResourceColors( 447 context, 448 R.color.system_accent3_0, 449 R.color.system_accent3_10, 450 R.color.system_accent3_50, 451 R.color.system_accent3_100, 452 R.color.system_accent3_200, 453 R.color.system_accent3_300, 454 R.color.system_accent3_400, 455 R.color.system_accent3_500, 456 R.color.system_accent3_600, 457 R.color.system_accent3_700, 458 R.color.system_accent3_800, 459 R.color.system_accent3_900, 460 R.color.system_accent3_1000 461 ) 462 } 463 464 private fun getAllNeutral1Colors(context: Context): IntArray { 465 return getAllResourceColors( 466 context, 467 R.color.system_neutral1_0, 468 R.color.system_neutral1_10, 469 R.color.system_neutral1_50, 470 R.color.system_neutral1_100, 471 R.color.system_neutral1_200, 472 R.color.system_neutral1_300, 473 R.color.system_neutral1_400, 474 R.color.system_neutral1_500, 475 R.color.system_neutral1_600, 476 R.color.system_neutral1_700, 477 R.color.system_neutral1_800, 478 R.color.system_neutral1_900, 479 R.color.system_neutral1_1000 480 ) 481 } 482 483 private fun getAllNeutral2Colors(context: Context): IntArray { 484 return getAllResourceColors( 485 context, 486 R.color.system_neutral2_0, 487 R.color.system_neutral2_10, 488 R.color.system_neutral2_50, 489 R.color.system_neutral2_100, 490 R.color.system_neutral2_200, 491 R.color.system_neutral2_300, 492 R.color.system_neutral2_400, 493 R.color.system_neutral2_500, 494 R.color.system_neutral2_600, 495 R.color.system_neutral2_700, 496 R.color.system_neutral2_800, 497 R.color.system_neutral2_900, 498 R.color.system_neutral2_1000 499 ) 500 } 501 502 private fun getAllErrorColors(context: Context): IntArray { 503 return getAllResourceColors( 504 context, 505 R.color.system_error_0, 506 R.color.system_error_10, 507 R.color.system_error_50, 508 R.color.system_error_100, 509 R.color.system_error_200, 510 R.color.system_error_300, 511 R.color.system_error_400, 512 R.color.system_error_500, 513 R.color.system_error_600, 514 R.color.system_error_700, 515 R.color.system_error_800, 516 R.color.system_error_900, 517 R.color.system_error_1000 518 ) 519 } 520 521 // Helper functions 522 523 private fun assurePaletteSetting( 524 context: Context = getInstrumentation().targetContext 525 ): Boolean { 526 if (checkExpectedPalette(context).size > 0) { 527 return setExpectedPalette(context) 528 } 529 530 return true 531 } 532 533 private fun setExpectedPalette(context: Context): Boolean { 534 // Update setting, so system colors will change 535 runWithShellPermissionIdentity { 536 Settings.Secure.putString( 537 context.contentResolver, 538 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, 539 "{\"android.theme.customization.system_palette\":\"${color}\"," + 540 "\"android.theme.customization.theme_style\":\"${style}\"}" 541 ) 542 } 543 544 return conditionTimeoutCheck({ 545 val mismatches = checkExpectedPalette(context) 546 val noMismatches = mismatches.size == 0 547 548 if (DEBUG) { 549 val setting = Settings.Secure.getString( 550 context.contentResolver, 551 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES 552 ) 553 554 Log.d( 555 TAG, 556 if (noMismatches) { 557 "Palette $setting is correctly set with colors: " + 558 intArrayToHexString(expectedPalette) 559 } else { 560 """ 561 Setting: 562 $setting 563 Mismatches [index](color, expected): 564 ${ 565 mismatches.map { (i, current, expected) -> 566 val c = if (current != null) { 567 Integer.toHexString(current) 568 } else { 569 "Null" 570 } 571 val e = if (expected != null) { 572 Integer.toHexString(expected) 573 } else { 574 "Null" 575 } 576 577 return@map "[$i]($c, $e) " 578 }.joinToString(" ") 579 } 580 """.trimIndent() 581 } 582 ) 583 } 584 585 return@conditionTimeoutCheck noMismatches 586 }) 587 } 588 589 private fun checkExpectedPalette( 590 context: Context, 591 ): MutableList<Triple<Int, Int?, Int?>> { 592 val allColors = IntArray(65) 593 System.arraycopy(getAllAccent1Colors(context), 0, allColors, 0, 13) 594 System.arraycopy(getAllAccent2Colors(context), 0, allColors, 13, 13) 595 System.arraycopy(getAllAccent3Colors(context), 0, allColors, 26, 13) 596 System.arraycopy(getAllNeutral1Colors(context), 0, allColors, 39, 13) 597 System.arraycopy(getAllNeutral2Colors(context), 0, allColors, 52, 13) 598 599 return getArraysMismatch( allColors, expectedPalette ) 600 } 601 602 /** 603 * Convert the Material shade to an array position. 604 * 605 * @param shade Shade from 0 to 1000. 606 * @return index in array 607 * @see .getAllAccent1Colors 608 * @see .getAllNeutral1Colors 609 */ 610 private fun shadeToArrayIndex(shade: Int): Int { 611 return when (shade) { 612 0 -> 0 613 10 -> 1 614 50 -> 2 615 else -> { 616 shade / 100 + 2 617 } 618 } 619 } 620 621 private fun assertColor(@ColorInt observed: Int, @ColorInt expected: Int) { 622 Assert.assertEquals( 623 "Color = ${Integer.toHexString(observed)}, " + 624 "${Integer.toHexString(expected)} expected", 625 expected, 626 observed 627 ) 628 } 629 630 private fun getAllResourceColors(context: Context, vararg resources: Int): IntArray { 631 if (resources.size != 13) throw Exception("Color palettes must be 13 in size") 632 return resources.map { resId -> context.getColor(resId) }.toIntArray() 633 } 634 635 private fun intArrayToHexString(src: IntArray): String { 636 return src.joinToString { n -> Integer.toHexString(n) } 637 } 638 639 private fun conditionTimeoutCheck( 640 evalFunction: () -> Boolean, 641 totalTimeout: Int = 15000, 642 loopTime: Int = 1000 643 ): Boolean { 644 if (totalTimeout < loopTime) throw Exception("Loop time must be smaller than total time.") 645 646 var remainingTime = totalTimeout 647 648 while (remainingTime > 0) { 649 if (evalFunction()) return true 650 651 Thread.sleep(loopTime.coerceAtMost(remainingTime).toLong()) 652 remainingTime -= loopTime 653 } 654 655 return false 656 } 657 658 private fun getArraysMismatch(a: IntArray, b: IntArray): MutableList<Triple<Int, Int?, Int?>> { 659 val len = a.size.coerceAtLeast(b.size) 660 val mismatches: MutableList<Triple<Int, Int?, Int?>> = mutableListOf() 661 662 repeat(len) { i -> 663 val valueA = if (a.size >= i + 1) a[i] else null 664 val valueB = if (b.size >= i + 1) b[i] else null 665 666 if (valueB != valueA && isColorDifferenceTooLarge(valueA, valueB, 3)){ 667 mismatches.add(Triple(i, valueA, valueB)) 668 } 669 } 670 671 return mismatches 672 } 673 674 private fun isColorDifferenceTooLarge(color1: Int?, color2: Int?, threshold: Int): Boolean { 675 if (color1 == null || color2 == null) return true 676 677 val hct1 = Hct.fromInt(color1) 678 val hct2 = Hct.fromInt(color2) 679 680 val diffTone = abs(hct1.tone - hct2.tone) 681 val diffChroma = abs(hct1.chroma - hct2.chroma) 682 val diffHue = abs(hct1.hue - hct2.hue) 683 684 return diffTone + diffChroma + diffHue > threshold 685 } 686 687 // Helper Classes 688 689 private class ContrastTester private constructor( 690 var mContext: Context, 691 vararg var mForegrounds: Int 692 ) { 693 var mBgGroups = ArrayList<Background>() 694 695 fun checkContrastLevels(): ArrayList<String> { 696 val newFailMessages = ArrayList<String>() 697 mBgGroups.forEach { background -> 698 newFailMessages.addAll(background.checkContrast(mForegrounds)) 699 } 700 701 return newFailMessages 702 } 703 704 fun andForegrounds(contrastLevel: Float, vararg res: Int): ContrastTester { 705 mBgGroups.add(Background(contrastLevel, *res)) 706 return this 707 } 708 709 private inner class Background( 710 private val mContrasLevel: Float, 711 private vararg val mEntries: Int 712 ) { 713 fun checkContrast(foregrounds: IntArray): ArrayList<String> { 714 val newFailMessages = ArrayList<String>() 715 val res = mContext.resources 716 717 foregrounds.forEach { fgRes -> 718 mEntries.forEach { bgRes -> 719 if (!checkPair(mContext, mContrasLevel, fgRes, bgRes)) { 720 val background = mContext.getColor(bgRes) 721 val foreground = mContext.getColor(fgRes) 722 val contrast = ColorUtils.calculateContrast(foreground, background) 723 val msg = "Background Color '${res.getResourceName(bgRes)}'" + 724 "(#${Integer.toHexString(mContext.getColor(bgRes))}) " + 725 "should have at least $mContrasLevel " + 726 "contrast ratio against Foreground Color '" + 727 res.getResourceName(fgRes) + 728 "' (#${Integer.toHexString(mContext.getColor(fgRes))}) " + 729 " but had $contrast" 730 731 newFailMessages.add(msg) 732 } 733 } 734 } 735 736 return newFailMessages 737 } 738 } 739 740 companion object { 741 fun ofBackgrounds(context: Context, vararg res: Int): ContrastTester { 742 return ContrastTester(context, *res) 743 } 744 745 fun checkPair(context: Context, minContrast: Float, fgRes: Int, bgRes: Int): Boolean { 746 val background = context.getColor(bgRes) 747 val foreground = context.getColor(fgRes) 748 val contrast = Contrast.ratioOfTones( 749 Hct.fromInt(foreground).tone, 750 Hct.fromInt(background).tone 751 ) 752 return contrast > minContrast 753 } 754 } 755 } 756 757 private class BulkContrastTester private constructor(vararg testsArgs: ContrastTester) { 758 private val tests = testsArgs 759 private val errorMessages: MutableList<String> = mutableListOf() 760 761 val testPassed: Boolean get() = errorMessages.isEmpty() 762 763 val allMessages: String 764 get() = 765 if (testPassed) "Test OK" else errorMessages.joinToString("\n") 766 767 fun run() { 768 errorMessages.clear() 769 tests.forEach { test -> errorMessages.addAll(test.checkContrastLevels()) } 770 } 771 772 companion object { 773 fun of(vararg testers: ContrastTester): BulkContrastTester { 774 return BulkContrastTester(*testers) 775 } 776 } 777 } 778 779 companion object { 780 private const val TAG = "SystemPaletteTest" 781 private const val DEBUG = true 782 783 private var initialContrast: Float = 0f 784 785 @JvmStatic 786 @BeforeClass 787 fun setUp() { 788 initialContrast = getInstrumentation() 789 .targetContext 790 .getSystemService(UiModeManager::class.java) 791 .contrast 792 putContrastInSettings(0f) 793 } 794 795 @JvmStatic 796 @AfterClass 797 fun tearDown() { 798 putContrastInSettings(initialContrast) 799 } 800 801 private fun putContrastInSettings(contrast: Float) { 802 runShellCommand("settings put secure contrast_level $contrast") 803 } 804 805 @Parameterized.Parameters(name = "Palette {1} with color {0}") 806 @JvmStatic 807 fun testData(): List<Array<Serializable>> { 808 val context: Context = getInstrumentation().targetContext 809 val parser: XmlPullParser = context.resources.getXml( CtsR.xml.valid_themes) 810 val dataList: MutableList<Array<Serializable>> = mutableListOf() 811 812 try { 813 parser.next() 814 parser.next() 815 parser.require(XmlPullParser.START_TAG, null, "themes") 816 while (parser.next() != XmlPullParser.END_TAG) { 817 parser.require(XmlPullParser.START_TAG, null, "theme") 818 val color = parser.getAttributeValue(null, "color") 819 while (parser.next() != XmlPullParser.END_TAG) { 820 val styleName = parser.name 821 parser.next() 822 val colors = Arrays.stream( 823 parser.text.split(",".toRegex()).dropLastWhile { it.isEmpty() } 824 .toTypedArray() 825 ) 826 .mapToInt { s: String -> 827 Color.parseColor( 828 "#$s" 829 ) 830 } 831 .toArray() 832 parser.next() 833 parser.require(XmlPullParser.END_TAG, null, styleName) 834 dataList.add( 835 arrayOf( 836 color, 837 styleName.uppercase(Locale.getDefault()), 838 colors 839 ) 840 ) 841 } 842 } 843 } catch (e: Exception) { 844 throw RuntimeException("Error parsing xml", e) 845 } 846 847 return dataList 848 } 849 } 850 } 851