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.text.Spannable;
27 import android.text.SpannableString;
28 import android.text.SpannableStringBuilder;
29 import android.text.Spanned;
30 import android.text.style.ForegroundColorSpan;
31 import android.text.style.TextAppearanceSpan;
32 
33 import androidx.test.InstrumentationRegistry;
34 import androidx.test.filters.SmallTest;
35 
36 import com.android.internal.R;
37 
38 import junit.framework.TestCase;
39 
40 import org.junit.Before;
41 import org.junit.Test;
42 
43 public class ContrastColorUtilTest extends TestCase {
44 
45     private Context mContext;
46 
47     @Before
setUp()48     public void setUp() {
49         mContext = InstrumentationRegistry.getContext();
50     }
51 
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     @SmallTest
testEnsureTextContrastAgainstLight()74     public void testEnsureTextContrastAgainstLight() {
75         int lightBg = 0xFFFFF8F2;
76 
77         final int lightBlue = 0xff8888ff;
78         int blueContrastColor = ContrastColorUtil.ensureTextContrast(lightBlue, lightBg, false);
79         assertContrastIsWithinRange(blueContrastColor, lightBg, 4.5, 4.75);
80 
81         int redContrastColor = ContrastColorUtil.ensureTextContrast(Color.RED, lightBg, false);
82         assertContrastIsWithinRange(redContrastColor, lightBg, 4.5, 4.75);
83 
84         int greenContrastColor = ContrastColorUtil.ensureTextContrast(Color.GREEN, lightBg, false);
85         assertContrastIsWithinRange(greenContrastColor, lightBg, 4.5, 4.75);
86 
87         int grayContrastColor = ContrastColorUtil.ensureTextContrast(Color.LTGRAY, lightBg, false);
88         assertContrastIsWithinRange(grayContrastColor, lightBg, 4.5, 4.75);
89 
90         int selfContrastColor = ContrastColorUtil.ensureTextContrast(lightBg, lightBg, false);
91         assertContrastIsWithinRange(selfContrastColor, lightBg, 4.5, 4.75);
92     }
93 
testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans()94     public void testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans() {
95         Spannable text = new SpannableString("blue text with yellow and green");
96         text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
97                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
98         text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
99                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
100         TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
101                 R.style.TextAppearance_DeviceDefault_Notification_Title);
102         assertThat(taSpan.getTextColor()).isNotNull();  // it must be set to prove it is cleared.
103         text.setSpan(taSpan, 0, text.length(),
104                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
105         text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
106                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
107         Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.BLACK);
108         Object[] spans = result.getSpans(0, result.length(), Object.class);
109         assertThat(spans).hasLength(3);
110 
111         assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
112         assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
113         assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
114 
115         assertThat(result.getSpanStart(spans[1])).isEqualTo(0);
116         assertThat(result.getSpanEnd(spans[1])).isEqualTo(31);
117         assertThat(spans[1]).isNotSameInstanceAs(taSpan);  // don't mutate the existing span
118         assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
119         assertThat(((TextAppearanceSpan) spans[1]).getTextColor()).isNull();
120 
121         assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
122         assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
123         assertThat(((ForegroundColorSpan) spans[2]).getForegroundColor()).isEqualTo(Color.GREEN);
124     }
125 
testBuilder_ensureColorSpanContrast_partialLength_adjusted()126     public void testBuilder_ensureColorSpanContrast_partialLength_adjusted() {
127         int background = 0xFFFF0101;  // Slightly lighter red
128         CharSequence text = new SpannableStringBuilder()
129                 .append("text with ")
130                 .append("some red", new ForegroundColorSpan(Color.RED),
131                         Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
132         CharSequence result = ContrastColorUtil.ensureColorSpanContrast(text, background);
133 
134         // ensure the span has been updated to have > 1.3:1 contrast ratio with fill color
135         Object[] spans = ((Spannable) result).getSpans(0, result.length(), Object.class);
136         assertThat(spans).hasLength(1);
137         int foregroundColor = ((ForegroundColorSpan) spans[0]).getForegroundColor();
138         assertContrastIsWithinRange(foregroundColor, background, 3, 3.2);
139     }
140 
testBuilder_ensureColorSpanContrast_worksWithComplexInput()141     public void testBuilder_ensureColorSpanContrast_worksWithComplexInput() {
142         Spannable text = new SpannableString("blue text with yellow and green and cyan");
143         text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
144                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
145         text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
146                 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
147         // cyan TextAppearanceSpan
148         TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
149                 R.style.TextAppearance_DeviceDefault_Notification_Title);
150         taSpan = new TextAppearanceSpan(taSpan.getFamily(), taSpan.getTextStyle(),
151                 taSpan.getTextSize(), ColorStateList.valueOf(Color.CYAN), null);
152         text.setSpan(taSpan, 36, 40,
153                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
154         text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
155                 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
156         Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.GRAY);
157         Object[] spans = result.getSpans(0, result.length(), Object.class);
158         assertThat(spans).hasLength(3);
159 
160         assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
161         assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
162         assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
163 
164         assertThat(result.getSpanStart(spans[1])).isEqualTo(36);
165         assertThat(result.getSpanEnd(spans[1])).isEqualTo(40);
166         assertThat(spans[1]).isNotSameInstanceAs(taSpan);  // don't mutate the existing span
167         assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
168         ColorStateList newCyanList = ((TextAppearanceSpan) spans[1]).getTextColor();
169         assertThat(newCyanList).isNotNull();
170         assertContrastIsWithinRange(newCyanList.getDefaultColor(), Color.GRAY, 3, 3.2);
171 
172         assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
173         assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
174         int newGreen = ((ForegroundColorSpan) spans[2]).getForegroundColor();
175         assertThat(newGreen).isNotEqualTo(Color.GREEN);
176         assertContrastIsWithinRange(newGreen, Color.GRAY, 3, 3.2);
177     }
178 
assertContrastIsWithinRange(int foreground, int background, double minContrast, double maxContrast)179     public static void assertContrastIsWithinRange(int foreground, int background,
180             double minContrast, double maxContrast) {
181         assertContrastIsAtLeast(foreground, background, minContrast);
182         assertContrastIsAtMost(foreground, background, maxContrast);
183     }
184 
assertContrastIsAtLeast(int foreground, int background, double minContrast)185     public static void assertContrastIsAtLeast(int foreground, int background, double minContrast) {
186         try {
187             assertThat(calculateContrast(foreground, background)).isAtLeast(minContrast);
188         } catch (AssertionError e) {
189             throw new AssertionError(
190                     String.format("Insufficient contrast: foreground=#%08x background=#%08x",
191                             foreground, background), e);
192         }
193     }
194 
assertContrastIsAtMost(int foreground, int background, double maxContrast)195     public static void assertContrastIsAtMost(int foreground, int background, double maxContrast) {
196         try {
197             assertThat(calculateContrast(foreground, background)).isAtMost(maxContrast);
198         } catch (AssertionError e) {
199             throw new AssertionError(
200                     String.format("Excessive contrast: foreground=#%08x background=#%08x",
201                             foreground, background), e);
202         }
203     }
204 
205 }
206