Ensure text contrast

Test: cts NotificationTest
Test: ContrastColorUtilTest
Test: test app - add colored spans to each style, validate view
in light and dark modes; expanded and collapsed and grouped
Fixes: 159072741
Change-Id: I590c13b85f0a3a42a9e5175be5377ace32413476

Change-Id: I987f6ad4f61336d3774d9a9839a0b54aa979c207
This commit is contained in:
Julia Reynolds
2023-03-23 16:55:57 -04:00
parent 4a3b5e4b1e
commit 86bb84b71d
7 changed files with 256 additions and 194 deletions

View File

@@ -5249,7 +5249,7 @@ public class Notification implements Parcelable
boolean hasSecondLine = showProgress;
if (p.hasTitle()) {
contentView.setViewVisibility(p.mTitleViewId, View.VISIBLE);
contentView.setTextViewText(p.mTitleViewId, processTextSpans(p.mTitle));
contentView.setTextViewText(p.mTitleViewId, ensureColorSpanContrast(p.mTitle, p));
setTextViewColorPrimary(contentView, p.mTitleViewId, p);
} else if (p.mTitleViewId != R.id.title) {
// This alternate title view ID is not cleared by resetStandardTemplate
@@ -5259,7 +5259,7 @@ public class Notification implements Parcelable
if (p.mText != null && p.mText.length() != 0
&& (!showProgress || p.mAllowTextWithProgress)) {
contentView.setViewVisibility(p.mTextViewId, View.VISIBLE);
contentView.setTextViewText(p.mTextViewId, processTextSpans(p.mText));
contentView.setTextViewText(p.mTextViewId, ensureColorSpanContrast(p.mText, p));
setTextViewColorSecondary(contentView, p.mTextViewId, p);
hasSecondLine = true;
} else if (p.mTextViewId != R.id.text) {
@@ -5286,13 +5286,6 @@ public class Notification implements Parcelable
RemoteViews.MARGIN_BOTTOM, marginDimen);
}
private CharSequence processTextSpans(CharSequence text) {
if (mInNightMode) {
return ContrastColorUtil.clearColorSpans(text);
}
return text;
}
private void setTextViewColorPrimary(RemoteViews contentView, @IdRes int id,
StandardTemplateParams p) {
contentView.setTextColor(id, getPrimaryTextColor(p));
@@ -5544,9 +5537,8 @@ public class Notification implements Parcelable
headerText = mN.extras.getCharSequence(EXTRA_INFO_TEXT);
}
if (!TextUtils.isEmpty(headerText)) {
// TODO: Remove the span entirely to only have the string with propper formating.
contentView.setTextViewText(R.id.header_text, processTextSpans(
processLegacyText(headerText)));
contentView.setTextViewText(R.id.header_text, ensureColorSpanContrast(
processLegacyText(headerText), p));
setTextViewColorSecondary(contentView, R.id.header_text, p);
contentView.setViewVisibility(R.id.header_text, View.VISIBLE);
if (hasTextToLeft) {
@@ -5567,8 +5559,8 @@ public class Notification implements Parcelable
return false;
}
if (!TextUtils.isEmpty(p.mHeaderTextSecondary)) {
contentView.setTextViewText(R.id.header_text_secondary, processTextSpans(
processLegacyText(p.mHeaderTextSecondary)));
contentView.setTextViewText(R.id.header_text_secondary, ensureColorSpanContrast(
processLegacyText(p.mHeaderTextSecondary), p));
setTextViewColorSecondary(contentView, R.id.header_text_secondary, p);
contentView.setViewVisibility(R.id.header_text_secondary, View.VISIBLE);
if (hasTextToLeft) {
@@ -5809,7 +5801,7 @@ public class Notification implements Parcelable
big.setViewVisibility(R.id.notification_material_reply_text_1_container,
View.VISIBLE);
big.setTextViewText(R.id.notification_material_reply_text_1,
processTextSpans(replyText[0].getText()));
ensureColorSpanContrast(replyText[0].getText(), p));
setTextViewColorSecondary(big, R.id.notification_material_reply_text_1, p);
big.setViewVisibility(R.id.notification_material_reply_progress,
showSpinner ? View.VISIBLE : View.GONE);
@@ -5821,7 +5813,7 @@ public class Notification implements Parcelable
&& p.maxRemoteInputHistory > 1) {
big.setViewVisibility(R.id.notification_material_reply_text_2, View.VISIBLE);
big.setTextViewText(R.id.notification_material_reply_text_2,
processTextSpans(replyText[1].getText()));
ensureColorSpanContrast(replyText[1].getText(), p));
setTextViewColorSecondary(big, R.id.notification_material_reply_text_2, p);
if (replyText.length > 2 && !TextUtils.isEmpty(replyText[2].getText())
@@ -5829,7 +5821,7 @@ public class Notification implements Parcelable
big.setViewVisibility(
R.id.notification_material_reply_text_3, View.VISIBLE);
big.setTextViewText(R.id.notification_material_reply_text_3,
processTextSpans(replyText[2].getText()));
ensureColorSpanContrast(replyText[2].getText(), p));
setTextViewColorSecondary(big, R.id.notification_material_reply_text_3, p);
}
}
@@ -6243,9 +6235,9 @@ public class Notification implements Parcelable
fullLengthColor, notifBackgroundColor);
}
// Remove full-length color spans and ensure text contrast with the button fill.
title = ensureColorSpanContrast(title, buttonFillColor);
title = ContrastColorUtil.ensureColorSpanContrast(title, buttonFillColor);
}
button.setTextViewText(R.id.action0, processTextSpans(title));
button.setTextViewText(R.id.action0, ensureColorSpanContrast(title, p));
int textColor = ContrastColorUtil.resolvePrimaryColor(mContext,
buttonFillColor, mInNightMode);
if (tombstone) {
@@ -6270,8 +6262,8 @@ public class Notification implements Parcelable
button.setIntDimen(R.id.action0, "setMinimumWidth", minWidthDimen);
}
} else {
button.setTextViewText(R.id.action0, processTextSpans(
processLegacyText(action.title)));
button.setTextViewText(R.id.action0, ensureColorSpanContrast(
action.title, p));
button.setTextColor(R.id.action0, getStandardActionColor(p));
}
// CallStyle notifications add action buttons which don't actually exist in mActions,
@@ -6348,72 +6340,12 @@ public class Notification implements Parcelable
* Ensures contrast on color spans against a background color.
* Note that any full-length color spans will be removed instead of being contrasted.
*
* @param charSequence the charSequence on which the spans are
* @param background the background color to ensure the contrast against
* @return the contrasted charSequence
* @hide
*/
@VisibleForTesting
public static CharSequence ensureColorSpanContrast(CharSequence charSequence,
int background) {
if (charSequence instanceof Spanned) {
Spanned ss = (Spanned) charSequence;
Object[] spans = ss.getSpans(0, ss.length(), Object.class);
SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
for (Object span : spans) {
Object resultSpan = span;
int spanStart = ss.getSpanStart(span);
int spanEnd = ss.getSpanEnd(span);
boolean fullLength = (spanEnd - spanStart) == charSequence.length();
if (resultSpan instanceof CharacterStyle) {
resultSpan = ((CharacterStyle) span).getUnderlying();
}
if (resultSpan instanceof TextAppearanceSpan) {
TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
ColorStateList textColor = originalSpan.getTextColor();
if (textColor != null) {
if (fullLength) {
// Let's drop the color from the span
textColor = null;
} else {
int[] colors = textColor.getColors();
int[] newColors = new int[colors.length];
for (int i = 0; i < newColors.length; i++) {
boolean isBgDark = isColorDark(background);
newColors[i] = ContrastColorUtil.ensureLargeTextContrast(
colors[i], background, isBgDark);
}
textColor = new ColorStateList(textColor.getStates().clone(),
newColors);
}
resultSpan = new TextAppearanceSpan(
originalSpan.getFamily(),
originalSpan.getTextStyle(),
originalSpan.getTextSize(),
textColor,
originalSpan.getLinkTextColor());
}
} else if (resultSpan instanceof ForegroundColorSpan) {
if (fullLength) {
resultSpan = null;
} else {
ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
int foregroundColor = originalSpan.getForegroundColor();
boolean isBgDark = isColorDark(background);
foregroundColor = ContrastColorUtil.ensureLargeTextContrast(
foregroundColor, background, isBgDark);
resultSpan = new ForegroundColorSpan(foregroundColor);
}
} else {
resultSpan = span;
}
if (resultSpan != null) {
builder.setSpan(resultSpan, spanStart, spanEnd, ss.getSpanFlags(span));
}
}
return builder;
}
return charSequence;
public CharSequence ensureColorSpanContrast(CharSequence charSequence,
StandardTemplateParams p) {
return ContrastColorUtil.ensureColorSpanContrast(charSequence, getBackgroundColor(p));
}
/**
@@ -7549,8 +7481,8 @@ public class Notification implements Parcelable
RemoteViews contentView = getStandardView(mBuilder.getBigPictureLayoutResource(),
p, null /* result */);
if (mSummaryTextSet) {
contentView.setTextViewText(R.id.text, mBuilder.processTextSpans(
mBuilder.processLegacyText(mSummaryText)));
contentView.setTextViewText(R.id.text, mBuilder.ensureColorSpanContrast(
mBuilder.processLegacyText(mSummaryText), p));
mBuilder.setTextViewColorSecondary(contentView, R.id.text, p);
contentView.setViewVisibility(R.id.text, View.VISIBLE);
}
@@ -8197,6 +8129,13 @@ public class Notification implements Parcelable
@Override
public void addExtras(Bundle extras) {
super.addExtras(extras);
addExtras(extras, false, 0);
}
/**
* @hide
*/
public void addExtras(Bundle extras, boolean ensureContrast, int backgroundColor) {
if (mUser != null) {
// For legacy usages
extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
@@ -8205,11 +8144,13 @@ public class Notification implements Parcelable
if (mConversationTitle != null) {
extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
}
if (!mMessages.isEmpty()) { extras.putParcelableArray(EXTRA_MESSAGES,
Message.getBundleArrayForMessages(mMessages));
if (!mMessages.isEmpty()) {
extras.putParcelableArray(EXTRA_MESSAGES,
getBundleArrayForMessages(mMessages, ensureContrast, backgroundColor));
}
if (!mHistoricMessages.isEmpty()) { extras.putParcelableArray(EXTRA_HISTORIC_MESSAGES,
Message.getBundleArrayForMessages(mHistoricMessages));
if (!mHistoricMessages.isEmpty()) {
extras.putParcelableArray(EXTRA_HISTORIC_MESSAGES, getBundleArrayForMessages(
mHistoricMessages, ensureContrast, backgroundColor));
}
if (mShortcutIcon != null) {
extras.putParcelable(EXTRA_CONVERSATION_ICON, mShortcutIcon);
@@ -8220,6 +8161,20 @@ public class Notification implements Parcelable
extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation);
}
private static Bundle[] getBundleArrayForMessages(List<Message> messages,
boolean ensureContrast, int backgroundColor) {
Bundle[] bundles = new Bundle[messages.size()];
final int N = messages.size();
for (int i = 0; i < N; i++) {
final Message m = messages.get(i);
if (ensureContrast) {
m.ensureColorContrast(backgroundColor);
}
bundles[i] = m.toBundle();
}
return bundles;
}
private void fixTitleAndTextExtras(Bundle extras) {
Message m = findLatestIncomingMessage();
CharSequence text = (m == null) ? null : m.mText;
@@ -8431,7 +8386,7 @@ public class Notification implements Parcelable
mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p);
}
addExtras(mBuilder.mN.extras);
addExtras(mBuilder.mN.extras, true, mBuilder.getBackgroundColor(p));
contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
mBuilder.getSmallIconColor(p));
contentView.setInt(R.id.status_bar_latest_event_content, "setSenderTextColor",
@@ -8577,7 +8532,7 @@ public class Notification implements Parcelable
static final String KEY_EXTRAS_BUNDLE = "extras";
static final String KEY_REMOTE_INPUT_HISTORY = "remote_input_history";
private final CharSequence mText;
private CharSequence mText;
private final long mTimestamp;
@Nullable
private final Person mSender;
@@ -8685,6 +8640,15 @@ public class Notification implements Parcelable
return this;
}
/**
* Updates TextAppearance spans in the message text so it has sufficient contrast
* against its background.
* @hide
*/
public void ensureColorContrast(int backgroundColor) {
mText = ContrastColorUtil.ensureColorSpanContrast(mText, backgroundColor);
}
/**
* Get the text to be used for this message, or the fallback text if a type and content
* Uri have been set
@@ -8778,15 +8742,6 @@ public class Notification implements Parcelable
return bundle;
}
static Bundle[] getBundleArrayForMessages(List<Message> messages) {
Bundle[] bundles = new Bundle[messages.size()];
final int N = messages.size();
for (int i = 0; i < N; i++) {
bundles[i] = messages.get(i).toBundle();
}
return bundles;
}
/**
* Returns a list of messages read from the given bundle list, e.g.
* {@link #EXTRA_MESSAGES} or {@link #EXTRA_HISTORIC_MESSAGES}.
@@ -9001,7 +8956,7 @@ public class Notification implements Parcelable
if (!TextUtils.isEmpty(str)) {
contentView.setViewVisibility(rowIds[i], View.VISIBLE);
contentView.setTextViewText(rowIds[i],
mBuilder.processTextSpans(mBuilder.processLegacyText(str)));
mBuilder.ensureColorSpanContrast(mBuilder.processLegacyText(str), p));
mBuilder.setTextViewColorSecondary(contentView, rowIds[i], p);
contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0);
if (first) {

View File

@@ -40,6 +40,8 @@ import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.WeakHashMap;
@@ -280,6 +282,92 @@ public class ContrastColorUtil {
return charSequence;
}
/**
* Ensures contrast on color spans against a background color.
* Note that any full-length color spans will be removed instead of being contrasted.
*
* @param charSequence the charSequence on which the spans are
* @param background the background color to ensure the contrast against
* @return the contrasted charSequence
*/
public static CharSequence ensureColorSpanContrast(CharSequence charSequence,
int background) {
if (charSequence == null) {
return charSequence;
}
if (charSequence instanceof Spanned) {
Spanned ss = (Spanned) charSequence;
Object[] spans = ss.getSpans(0, ss.length(), Object.class);
SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
for (Object span : spans) {
Object resultSpan = span;
int spanStart = ss.getSpanStart(span);
int spanEnd = ss.getSpanEnd(span);
boolean fullLength = (spanEnd - spanStart) == charSequence.length();
if (resultSpan instanceof CharacterStyle) {
resultSpan = ((CharacterStyle) span).getUnderlying();
}
if (resultSpan instanceof TextAppearanceSpan) {
TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
ColorStateList textColor = originalSpan.getTextColor();
if (textColor != null) {
if (fullLength) {
// Let's drop the color from the span
textColor = null;
} else {
int[] colors = textColor.getColors();
int[] newColors = new int[colors.length];
for (int i = 0; i < newColors.length; i++) {
boolean isBgDark = isColorDark(background);
newColors[i] = ContrastColorUtil.ensureLargeTextContrast(
colors[i], background, isBgDark);
}
textColor = new ColorStateList(textColor.getStates().clone(),
newColors);
}
resultSpan = new TextAppearanceSpan(
originalSpan.getFamily(),
originalSpan.getTextStyle(),
originalSpan.getTextSize(),
textColor,
originalSpan.getLinkTextColor());
}
} else if (resultSpan instanceof ForegroundColorSpan) {
if (fullLength) {
resultSpan = null;
} else {
ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
int foregroundColor = originalSpan.getForegroundColor();
boolean isBgDark = isColorDark(background);
foregroundColor = ContrastColorUtil.ensureLargeTextContrast(
foregroundColor, background, isBgDark);
resultSpan = new ForegroundColorSpan(foregroundColor);
}
} else {
resultSpan = span;
}
if (resultSpan != null) {
builder.setSpan(resultSpan, spanStart, spanEnd, ss.getSpanFlags(span));
}
}
return builder;
}
return charSequence;
}
/**
* Determines if the color is light or dark. Specifically, this is using the same metric as
* {@link ContrastColorUtil#resolvePrimaryColor(Context, int, boolean)} and peers so that
* the direction of color shift is consistent.
*
* @param color the color to check
* @return true if the color has higher contrast with white than black
*/
public static boolean isColorDark(int color) {
// as per shouldUseDark(), this uses the color contrast midpoint.
return calculateLuminance(color) <= 0.17912878474;
}
private int processColor(int color) {
return Color.argb(Color.alpha(color),
255 - Color.red(color),

View File

@@ -16,7 +16,6 @@
package android.app;
import static android.app.Notification.Builder.ensureColorSpanContrast;
import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_READ;
import static android.app.Notification.CarExtender.UnreadConversation.KEY_ON_REPLY;
import static android.app.Notification.CarExtender.UnreadConversation.KEY_REMOTE_INPUT;
@@ -63,11 +62,9 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.app.Notification.CallStyle;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -437,93 +434,7 @@ public class NotificationTest {
assertThat(Notification.Builder.getFullLengthSpanColor(text)).isEqualTo(expectedTextColor);
}
@Test
public void testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans() {
Spannable text = new SpannableString("blue text with yellow and green");
text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
R.style.TextAppearance_DeviceDefault_Notification_Title);
assertThat(taSpan.getTextColor()).isNotNull(); // it must be set to prove it is cleared.
text.setSpan(taSpan, 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable result = (Spannable) ensureColorSpanContrast(text, Color.BLACK);
Object[] spans = result.getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(3);
assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
assertThat(result.getSpanStart(spans[1])).isEqualTo(0);
assertThat(result.getSpanEnd(spans[1])).isEqualTo(31);
assertThat(spans[1]).isNotSameInstanceAs(taSpan); // don't mutate the existing span
assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
assertThat(((TextAppearanceSpan) spans[1]).getTextColor()).isNull();
assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
assertThat(((ForegroundColorSpan) spans[2]).getForegroundColor()).isEqualTo(Color.GREEN);
}
@Test
public void testBuilder_ensureColorSpanContrast_partialLength_adjusted() {
int background = 0xFFFF0101; // Slightly lighter red
CharSequence text = new SpannableStringBuilder()
.append("text with ")
.append("some red", new ForegroundColorSpan(Color.RED),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence result = ensureColorSpanContrast(text, background);
// ensure the span has been updated to have > 1.3:1 contrast ratio with fill color
Object[] spans = ((Spannable) result).getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(1);
int foregroundColor = ((ForegroundColorSpan) spans[0]).getForegroundColor();
assertContrastIsWithinRange(foregroundColor, background, 3, 3.2);
}
@Test
public void testBuilder_ensureColorSpanContrast_worksWithComplexInput() {
Spannable text = new SpannableString("blue text with yellow and green and cyan");
text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
// cyan TextAppearanceSpan
TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
R.style.TextAppearance_DeviceDefault_Notification_Title);
taSpan = new TextAppearanceSpan(taSpan.getFamily(), taSpan.getTextStyle(),
taSpan.getTextSize(), ColorStateList.valueOf(Color.CYAN), null);
text.setSpan(taSpan, 36, 40,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable result = (Spannable) ensureColorSpanContrast(text, Color.GRAY);
Object[] spans = result.getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(3);
assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
assertThat(result.getSpanStart(spans[1])).isEqualTo(36);
assertThat(result.getSpanEnd(spans[1])).isEqualTo(40);
assertThat(spans[1]).isNotSameInstanceAs(taSpan); // don't mutate the existing span
assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
ColorStateList newCyanList = ((TextAppearanceSpan) spans[1]).getTextColor();
assertThat(newCyanList).isNotNull();
assertContrastIsWithinRange(newCyanList.getDefaultColor(), Color.GRAY, 3, 3.2);
assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
int newGreen = ((ForegroundColorSpan) spans[2]).getForegroundColor();
assertThat(newGreen).isNotEqualTo(Color.GREEN);
assertContrastIsWithinRange(newGreen, Color.GRAY, 3, 3.2);
}
@Test
public void testBuilder_ensureButtonFillContrast_adjustsDarker() {

View File

@@ -20,14 +20,35 @@ import static androidx.core.graphics.ColorUtils.calculateContrast;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.TextAppearanceSpan;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import junit.framework.TestCase;
import org.junit.Before;
import org.junit.Test;
public class ContrastColorUtilTest extends TestCase {
private Context mContext;
@Before
public void setUp() {
mContext = InstrumentationRegistry.getContext();
}
@SmallTest
public void testEnsureTextContrastAgainstDark() {
int darkBg = 0xFF35302A;
@@ -70,6 +91,91 @@ public class ContrastColorUtilTest extends TestCase {
assertContrastIsWithinRange(selfContrastColor, lightBg, 4.5, 4.75);
}
public void testBuilder_ensureColorSpanContrast_removesAllFullLengthColorSpans() {
Spannable text = new SpannableString("blue text with yellow and green");
text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
R.style.TextAppearance_DeviceDefault_Notification_Title);
assertThat(taSpan.getTextColor()).isNotNull(); // it must be set to prove it is cleared.
text.setSpan(taSpan, 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.BLACK);
Object[] spans = result.getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(3);
assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
assertThat(result.getSpanStart(spans[1])).isEqualTo(0);
assertThat(result.getSpanEnd(spans[1])).isEqualTo(31);
assertThat(spans[1]).isNotSameInstanceAs(taSpan); // don't mutate the existing span
assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
assertThat(((TextAppearanceSpan) spans[1]).getTextColor()).isNull();
assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
assertThat(((ForegroundColorSpan) spans[2]).getForegroundColor()).isEqualTo(Color.GREEN);
}
public void testBuilder_ensureColorSpanContrast_partialLength_adjusted() {
int background = 0xFFFF0101; // Slightly lighter red
CharSequence text = new SpannableStringBuilder()
.append("text with ")
.append("some red", new ForegroundColorSpan(Color.RED),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence result = ContrastColorUtil.ensureColorSpanContrast(text, background);
// ensure the span has been updated to have > 1.3:1 contrast ratio with fill color
Object[] spans = ((Spannable) result).getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(1);
int foregroundColor = ((ForegroundColorSpan) spans[0]).getForegroundColor();
assertContrastIsWithinRange(foregroundColor, background, 3, 3.2);
}
public void testBuilder_ensureColorSpanContrast_worksWithComplexInput() {
Spannable text = new SpannableString("blue text with yellow and green and cyan");
text.setSpan(new ForegroundColorSpan(Color.YELLOW), 15, 21,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.BLUE), 0, text.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE);
// cyan TextAppearanceSpan
TextAppearanceSpan taSpan = new TextAppearanceSpan(mContext,
R.style.TextAppearance_DeviceDefault_Notification_Title);
taSpan = new TextAppearanceSpan(taSpan.getFamily(), taSpan.getTextStyle(),
taSpan.getTextSize(), ColorStateList.valueOf(Color.CYAN), null);
text.setSpan(taSpan, 36, 40,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.GREEN), 26, 31,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spannable result = (Spannable) ContrastColorUtil.ensureColorSpanContrast(text, Color.GRAY);
Object[] spans = result.getSpans(0, result.length(), Object.class);
assertThat(spans).hasLength(3);
assertThat(result.getSpanStart(spans[0])).isEqualTo(15);
assertThat(result.getSpanEnd(spans[0])).isEqualTo(21);
assertThat(((ForegroundColorSpan) spans[0]).getForegroundColor()).isEqualTo(Color.YELLOW);
assertThat(result.getSpanStart(spans[1])).isEqualTo(36);
assertThat(result.getSpanEnd(spans[1])).isEqualTo(40);
assertThat(spans[1]).isNotSameInstanceAs(taSpan); // don't mutate the existing span
assertThat(((TextAppearanceSpan) spans[1]).getFamily()).isEqualTo(taSpan.getFamily());
ColorStateList newCyanList = ((TextAppearanceSpan) spans[1]).getTextColor();
assertThat(newCyanList).isNotNull();
assertContrastIsWithinRange(newCyanList.getDefaultColor(), Color.GRAY, 3, 3.2);
assertThat(result.getSpanStart(spans[2])).isEqualTo(26);
assertThat(result.getSpanEnd(spans[2])).isEqualTo(31);
int newGreen = ((ForegroundColorSpan) spans[2]).getForegroundColor();
assertThat(newGreen).isNotEqualTo(Color.GREEN);
assertContrastIsWithinRange(newGreen, Color.GRAY, 3, 3.2);
}
public static void assertContrastIsWithinRange(int foreground, int background,
double minContrast, double maxContrast) {
assertContrastIsAtLeast(foreground, background, minContrast);

View File

@@ -28,6 +28,7 @@ import android.widget.TextView;
import androidx.annotation.ColorInt;
import com.android.internal.util.ContrastColorUtil;
import com.android.keyguard.AlphaOptimizedLinearLayout;
import com.android.systemui.R;
import com.android.systemui.statusbar.CrossFadeHelper;
@@ -109,7 +110,7 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout
public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
@Nullable View contentView) {
mTitleView.setText(title);
mTitleView.setText(title.toString());
mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE);
if (TextUtils.isEmpty(text)) {
mTextView.setVisibility(GONE);

View File

@@ -78,6 +78,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.animation.InterpolatorsAndroidX;
@@ -221,7 +222,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
final int stroke = colorized ? mContext.getResources().getDimensionPixelSize(
R.dimen.remote_input_view_text_stroke) : 0;
if (colorized) {
final boolean dark = Notification.Builder.isColorDark(backgroundColor);
final boolean dark = ContrastColorUtil.isColorDark(backgroundColor);
final int foregroundColor = dark ? Color.WHITE : Color.BLACK;
final int inverseColor = dark ? Color.BLACK : Color.WHITE;
editBgColor = backgroundColor;

View File

@@ -726,7 +726,7 @@ public class SmartReplyView extends ViewGroup {
mCurrentBackgroundColor = backgroundColor;
mCurrentColorized = colorized;
final boolean dark = Notification.Builder.isColorDark(backgroundColor);
final boolean dark = ContrastColorUtil.isColorDark(backgroundColor);
mCurrentTextColor = ContrastColorUtil.ensureTextContrast(
dark ? mDefaultTextColorDarkBg : mDefaultTextColor,