xref: /aosp_15_r20/frameworks/base/core/tests/coretests/src/com/android/internal/util/ContrastColorUtilTest.java (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.internal.util;
18 
19 import static androidx.core.graphics.ColorUtils.calculateContrast;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.graphics.Color;
26 import android.platform.test.annotations.IgnoreUnderRavenwood;
27 import android.platform.test.ravenwood.RavenwoodRule;
28 import android.text.Spannable;
29 import android.text.SpannableString;
30 import android.text.SpannableStringBuilder;
31 import android.text.Spanned;
32 import android.text.style.ForegroundColorSpan;
33 import android.text.style.TextAppearanceSpan;
34 
35 import androidx.test.InstrumentationRegistry;
36 import androidx.test.ext.junit.runners.AndroidJUnit4;
37 import androidx.test.filters.SmallTest;
38 
39 import com.android.internal.R;
40 
41 import org.junit.Rule;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 
45 @RunWith(AndroidJUnit4.class)
46 @IgnoreUnderRavenwood(blockedBy = Color.class)
47 public class ContrastColorUtilTest {
48     @Rule
49     public final RavenwoodRule mRavenwood = new RavenwoodRule();
50 
51     @Test
52     @SmallTest
testEnsureTextContrastAgainstDark()53     public void testEnsureTextContrastAgainstDark() {
54         int darkBg = 0xFF35302A;
55 
56         int blueContrastColor = ContrastColorUtil.ensureTextContrast(Color.BLUE, darkBg, true);
57         assertContrastIsWithinRange(blueContrastColor, darkBg, 4.5, 4.75);
58 
59         int redContrastColor = ContrastColorUtil.ensureTextContrast(Color.RED, darkBg, true);
60         assertContrastIsWithinRange(redContrastColor, darkBg, 4.5, 4.75);
61 
62         final int darkGreen = 0xff008800;
63         int greenContrastColor = ContrastColorUtil.ensureTextContrast(darkGreen, darkBg, true);
64         assertContrastIsWithinRange(greenContrastColor, darkBg, 4.5, 4.75);
65 
66         int grayContrastColor = ContrastColorUtil.ensureTextContrast(Color.DKGRAY, darkBg, true);
67         assertContrastIsWithinRange(grayContrastColor, darkBg, 4.5, 4.75);
68 
69         int selfContrastColor = ContrastColorUtil.ensureTextContrast(darkBg, darkBg, true);
70         assertContrastIsWithinRange(selfContrastColor, darkBg, 4.5, 4.75);
71     }
72 
73     @Test
74     @SmallTest
testEnsureTextContrastAgainstLight()75     public void testEnsureTextContrastAgainstLight() {
76         int lightBg = 0xFFFFF8F2;
77 
78         final int lightBlue = 0xff8888ff;
79         int blueContrastColor = ContrastColorUtil.ensureTextContrast(lightBlue, lightBg, false);
80         assertContrastIsWithinRange(blueContrastColor, lightBg, 4.5, 4.75);
81 
82         int redContrastColor = ContrastColorUtil.ensureTextContrast(Color.RED, lightBg, false);
83         assertContrastIsWithinRange(redContrastColor, lightBg, 4.5, 4.75);
84 
85         int greenContrastColor = ContrastColorUtil.ensureTextContrast(Color.GREEN, lightBg, false);
86         assertContrastIsWithinRange(greenContrastColor, lightBg, 4.5, 4.75);
87 
88         int grayContrastColor = ContrastColorUtil.ensureTextContrast(Color.LTGRAY, lightBg, false);
89         assertContrastIsWithinRange(grayContrastColor, lightBg, 4.5, 4.75);
90 
91         int selfContrastColor = ContrastColorUtil.ensureTextContrast(lightBg, lightBg, false);
92         assertContrastIsWithinRange(selfContrastColor, lightBg, 4.5, 4.75);
93     }
94 
95     @Test
testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans()96     public void testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans() {
97         Context context = InstrumentationRegistry.getContext();
98 
99         Spannable text = new SpannableString("blue text with yellow and green");
100         text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
101                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
102         text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
103                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
104         TextAppearanceSpan taSpan = new TextAppearanceSpan(context,
105                 R.style.TextAppearance_DeviceDefault_Notification_Title);
106         assertThat(taSpan.getTextColor()).isNotNull();  // it must be set to prove it is cleared.
107         text.setSpan(taSpan, 0, text.length(),
108                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
109         text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
110                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
111         Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.BLACK);
112         Object[] spans = result.getSpans(0, result.length(), Object.class);
113         assertThat(spans).hasLength(3);
114 
115         assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
116         assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
117         assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
118 
119         assertThat(result.getSpanStart(spans[1])).isEqualTo(0);
120         assertThat(result.getSpanEnd(spans[1])).isEqualTo(31);
121         assertThat(spans[1]).isNotSameInstanceAs(taSpan);  // don't mutate the existing span
122         assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
123         assertThat(((TextAppearanceSpan) spans[1]).getTextColor()).isNull();
124 
125         assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
126         assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
127         assertThat(((ForegroundColorSpan) spans[2]).getForegroundColor()).isEqualTo(Color.GREEN);
128     }
129 
130     @Test
testBuilder_ensureColorSpanContrast_partialLength_adjusted()131     public void testBuilder_ensureColorSpanContrast_partialLength_adjusted() {
132         int background = 0xFFFF0101;  // Slightly lighter red
133         CharSequence text = new SpannableStringBuilder()
134                 .append("text with ")
135                 .append("some red", new ForegroundColorSpan(Color.RED),
136                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
137         CharSequence result = ContrastColorUtil.ensureColorSpanContrast(text, background);
138 
139         // ensure the span has been updated to have > 1.3:1 contrast ratio with fill color
140         Object[] spans = ((Spannable) result).getSpans(0, result.length(), Object.class);
141         assertThat(spans).hasLength(1);
142         int foregroundColor = ((ForegroundColorSpan) spans[0]).getForegroundColor();
143         assertContrastIsWithinRange(foregroundColor, background, 3, 3.2);
144     }
145 
146     @Test
testBuilder_ensureColorSpanContrast_worksWithComplexInput()147     public void testBuilder_ensureColorSpanContrast_worksWithComplexInput() {
148         Context context = InstrumentationRegistry.getContext();
149 
150         Spannable text = new SpannableString("blue text with yellow and green and cyan");
151         text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
152                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
153         text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
154                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
155         // cyan TextAppearanceSpan
156         TextAppearanceSpan taSpan = new TextAppearanceSpan(context,
157                 R.style.TextAppearance_DeviceDefault_Notification_Title);
158         taSpan = new TextAppearanceSpan(taSpan.getFamily(), taSpan.getTextStyle(),
159                 taSpan.getTextSize(), ColorStateList.valueOf(Color.CYAN), null);
160         text.setSpan(taSpan, 36, 40,
161                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
162         text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
163                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
164         Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.GRAY);
165         Object[] spans = result.getSpans(0, result.length(), Object.class);
166         assertThat(spans).hasLength(3);
167 
168         assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
169         assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
170         assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
171 
172         assertThat(result.getSpanStart(spans[1])).isEqualTo(36);
173         assertThat(result.getSpanEnd(spans[1])).isEqualTo(40);
174         assertThat(spans[1]).isNotSameInstanceAs(taSpan);  // don't mutate the existing span
175         assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
176         ColorStateList newCyanList = ((TextAppearanceSpan) spans[1]).getTextColor();
177         assertThat(newCyanList).isNotNull();
178         assertContrastIsWithinRange(newCyanList.getDefaultColor(), Color.GRAY, 3, 3.2);
179 
180         assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
181         assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
182         int newGreen = ((ForegroundColorSpan) spans[2]).getForegroundColor();
183         assertThat(newGreen).isNotEqualTo(Color.GREEN);
184         assertContrastIsWithinRange(newGreen, Color.GRAY, 3, 3.2);
185     }
186 
assertContrastIsWithinRange(int foreground, int background, double minContrast, double maxContrast)187     public static void assertContrastIsWithinRange(int foreground, int background,
188             double minContrast, double maxContrast) {
189         assertContrastIsAtLeast(foreground, background, minContrast);
190         assertContrastIsAtMost(foreground, background, maxContrast);
191     }
192 
assertContrastIsAtLeast(int foreground, int background, double minContrast)193     public static void assertContrastIsAtLeast(int foreground, int background, double minContrast) {
194         try {
195             assertThat(calculateContrast(foreground, background)).isAtLeast(minContrast);
196         } catch (AssertionError e) {
197             throw new AssertionError(
198                     String.format("Insufficient contrast: foreground=#%08x background=#%08x",
199                             foreground, background), e);
200         }
201     }
202 
assertContrastIsAtMost(int foreground, int background, double maxContrast)203     public static void assertContrastIsAtMost(int foreground, int background, double maxContrast) {
204         try {
205             assertThat(calculateContrast(foreground, background)).isAtMost(maxContrast);
206         } catch (AssertionError e) {
207             throw new AssertionError(
208                     String.format("Excessive contrast: foreground=#%08x background=#%08x",
209                             foreground, background), e);
210         }
211     }
212 
213 }
214