Merge "Ensure text contrast" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
0b02b77b7a
@@ -5286,7 +5286,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
|
||||
@@ -5296,7 +5296,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) {
|
||||
@@ -5323,13 +5323,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));
|
||||
@@ -5581,9 +5574,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) {
|
||||
@@ -5604,8 +5596,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) {
|
||||
@@ -5846,7 +5838,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);
|
||||
@@ -5858,7 +5850,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())
|
||||
@@ -5866,7 +5858,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);
|
||||
}
|
||||
}
|
||||
@@ -6280,9 +6272,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) {
|
||||
@@ -6307,8 +6299,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,
|
||||
@@ -6385,72 +6377,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7586,8 +7518,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);
|
||||
}
|
||||
@@ -8207,6 +8139,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());
|
||||
@@ -8215,11 +8154,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);
|
||||
@@ -8230,6 +8171,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;
|
||||
@@ -8441,7 +8396,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",
|
||||
@@ -8587,7 +8542,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;
|
||||
@@ -8695,6 +8650,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
|
||||
@@ -8788,15 +8752,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}.
|
||||
@@ -9011,7 +8966,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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.annotation.Nullable;
|
||||
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 @@ 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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user