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