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