Merge "Ensure text contrast" into udc-dev am: 0b02b77b7a
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/22245852
Change-Id: Id9a6f9e7def36ca84f195e067b2360081d1411af
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index cc9c329..7aedd30 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5286,7 +5286,7 @@
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
@@ -5296,7 +5296,7 @@
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) {
@@ -5323,13 +5323,6 @@
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));
@@ -5581,9 +5574,8 @@
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) {
@@ -5604,8 +5596,8 @@
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) {
@@ -5846,7 +5838,7 @@
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);
@@ -5858,7 +5850,7 @@
&& 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())
@@ -5866,7 +5858,7 @@
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);
}
}
@@ -6280,9 +6272,9 @@
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) {
@@ -6307,8 +6299,8 @@
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,
@@ -6385,72 +6377,12 @@
* 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));
}
/**
@@ -7586,8 +7518,8 @@
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);
}
@@ -8207,6 +8139,13 @@
@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());
@@ -8215,11 +8154,13 @@
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);
@@ -8230,6 +8171,20 @@
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;
@@ -8441,7 +8396,7 @@
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",
@@ -8587,7 +8542,7 @@
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;
@@ -8696,6 +8651,15 @@
}
/**
+ * 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
*/
@@ -8788,15 +8752,6 @@
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}.
@@ -9011,7 +8966,7 @@
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) {
diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java
index ced2722..77de272 100644
--- a/core/java/com/android/internal/util/ContrastColorUtil.java
+++ b/core/java/com/android/internal/util/ContrastColorUtil.java
@@ -40,6 +40,8 @@
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 @@
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),
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 6debbfe..c5b00c9 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -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;
@@ -66,7 +65,6 @@
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;
@@ -436,93 +434,7 @@
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() {
diff --git a/core/tests/coretests/src/com/android/internal/util/ContrastColorUtilTest.java b/core/tests/coretests/src/com/android/internal/util/ContrastColorUtilTest.java
index cfe660c..5f5bf11 100644
--- a/core/tests/coretests/src/com/android/internal/util/ContrastColorUtilTest.java
+++ b/core/tests/coretests/src/com/android/internal/util/ContrastColorUtilTest.java
@@ -20,14 +20,35 @@
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 @@
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);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
index fc9d9e8..797038d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
@@ -28,6 +28,7 @@
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 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);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 4866f73..a08aa88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -78,6 +78,7 @@
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 @@
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;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
index a537b2a..9e88ceb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -726,7 +726,7 @@
mCurrentBackgroundColor = backgroundColor;
mCurrentColorized = colorized;
- final boolean dark = Notification.Builder.isColorDark(backgroundColor);
+ final boolean dark = ContrastColorUtil.isColorDark(backgroundColor);
mCurrentTextColor = ContrastColorUtil.ensureTextContrast(
dark ? mDefaultTextColorDarkBg : mDefaultTextColor,