• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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