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