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