Merge "Resolve Notification.Colors once, instead of lazily." into sc-dev

This commit is contained in:
Jeff DeCew
2021-05-25 16:44:59 +00:00
committed by Android (Google) Code Review
5 changed files with 388 additions and 233 deletions

View File

@@ -22,7 +22,6 @@ import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
import static java.util.Objects.requireNonNull;
import android.annotation.AttrRes;
import android.annotation.ColorInt;
import android.annotation.ColorRes;
import android.annotation.DimenRes;
@@ -36,6 +35,7 @@ import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.StringRes;
import android.annotation.StyleableRes;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -3749,20 +3749,12 @@ public class Notification implements Parcelable
private boolean mIsLegacy;
private boolean mIsLegacyInitialized;
/**
* Caches a contrast-enhanced version of {@link #mCachedContrastColorIsFor}.
*/
private int mCachedContrastColor = COLOR_INVALID;
private int mCachedContrastColorIsFor = COLOR_INVALID;
/**
* Caches an instance of StandardTemplateParams. Note that this may have been used before,
* so make sure to call {@link StandardTemplateParams#reset()} before using it.
*/
StandardTemplateParams mParams = new StandardTemplateParams();
private int mTextColorsAreForBackground = COLOR_INVALID;
private int mPrimaryTextColor = COLOR_INVALID;
private int mSecondaryTextColor = COLOR_INVALID;
Colors mColors = new Colors();
private boolean mTintActionButtons;
private boolean mInNightMode;
@@ -5019,7 +5011,7 @@ public class Notification implements Parcelable
contentView.setDrawableTint(
R.id.phishing_alert,
false /* targetBackground */,
getErrorColor(p),
getColors(p).getErrorColor(),
PorterDuff.Mode.SRC_ATOP);
}
@@ -5066,8 +5058,8 @@ public class Notification implements Parcelable
contentView.setDrawableTint(
R.id.alerted_icon,
false /* targetBackground */,
getHeaderIconColor(p),
PorterDuff.Mode.SRC_ATOP);
getColors(p).getSecondaryTextColor(),
PorterDuff.Mode.SRC_IN);
}
/**
@@ -5195,8 +5187,7 @@ public class Notification implements Parcelable
*/
@VisibleForTesting
public @ColorInt int getPrimaryTextColor(StandardTemplateParams p) {
ensureColors(p);
return mPrimaryTextColor;
return getColors(p).getPrimaryTextColor();
}
/**
@@ -5206,8 +5197,7 @@ public class Notification implements Parcelable
*/
@VisibleForTesting
public @ColorInt int getSecondaryTextColor(StandardTemplateParams p) {
ensureColors(p);
return mSecondaryTextColor;
return getColors(p).getSecondaryTextColor();
}
private void setTextViewColorSecondary(RemoteViews contentView, @IdRes int id,
@@ -5215,29 +5205,9 @@ public class Notification implements Parcelable
contentView.setTextColor(id, getSecondaryTextColor(p));
}
private void ensureColors(StandardTemplateParams p) {
int backgroundColor = getUnresolvedBackgroundColor(p);
if (mPrimaryTextColor == COLOR_INVALID
|| mSecondaryTextColor == COLOR_INVALID
|| mTextColorsAreForBackground != backgroundColor) {
mTextColorsAreForBackground = backgroundColor;
int defaultPrimaryTextColor = ContrastColorUtil.resolvePrimaryColor(mContext,
backgroundColor, mInNightMode);
int defaultSecondaryTextColor = ContrastColorUtil.resolveSecondaryColor(mContext,
backgroundColor, mInNightMode);
boolean colorized = backgroundColor != COLOR_DEFAULT;
if (colorized) {
mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast(
defaultPrimaryTextColor, backgroundColor, 4.5);
mSecondaryTextColor = ContrastColorUtil.findAlphaToMeetContrast(
defaultSecondaryTextColor, backgroundColor, 4.5);
} else {
mPrimaryTextColor = obtainThemeColor(R.attr.textColorPrimary,
defaultPrimaryTextColor);
mSecondaryTextColor = obtainThemeColor(R.attr.textColorSecondary,
defaultSecondaryTextColor);
}
}
private Colors getColors(StandardTemplateParams p) {
mColors.resolvePalette(mContext, mN.color, isBackgroundColorized(p), mInNightMode);
return mColors;
}
private void updateBackgroundColor(RemoteViews contentView,
@@ -5262,7 +5232,7 @@ public class Notification implements Parcelable
contentView.setProgressBar(R.id.progress, max, progress, ind);
contentView.setProgressBackgroundTintList(R.id.progress,
mContext.getColorStateList(R.color.notification_progress_background_color));
ColorStateList progressTint = ColorStateList.valueOf(getAccentColor(p));
ColorStateList progressTint = ColorStateList.valueOf(getPrimaryAccentColor(p));
contentView.setProgressTintList(R.id.progress, progressTint);
contentView.setProgressIndeterminateTintList(R.id.progress, progressTint);
return true;
@@ -5396,13 +5366,13 @@ public class Notification implements Parcelable
private void bindExpandButton(RemoteViews contentView, StandardTemplateParams p) {
// set default colors
int textColor = getPrimaryTextColor(p);
int pillColor = getProtectionColor(p);
int pillColor = getColors(p).getProtectionColor();
contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor);
contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor);
// Use different highlighted colors for conversations' unread count
if (p.mHighlightExpander) {
textColor = getBackgroundColor(p);
pillColor = getAccentColor(p);
pillColor = getPrimaryAccentColor(p);
}
contentView.setInt(R.id.expand_button, "setHighlightTextColor", textColor);
contentView.setInt(R.id.expand_button, "setHighlightPillColor", pillColor);
@@ -5715,7 +5685,7 @@ public class Notification implements Parcelable
showSpinner ? View.VISIBLE : View.GONE);
big.setProgressIndeterminateTintList(
R.id.notification_material_reply_progress,
ColorStateList.valueOf(getAccentColor(p)));
ColorStateList.valueOf(getPrimaryAccentColor(p)));
if (replyText.length > 1 && !TextUtils.isEmpty(replyText[1].getText())
&& p.maxRemoteInputHistory > 1) {
@@ -6110,7 +6080,7 @@ public class Notification implements Parcelable
// change the background bgColor
CharSequence title = action.title;
ColorStateList[] outResultColor = new ColorStateList[1];
int background = getSecondaryAccentColor(p);
int background = getColors(p).getSecondaryAccentColor();
if (isLegacy()) {
title = ContrastColorUtil.clearColorSpans(title);
} else {
@@ -6270,7 +6240,7 @@ public class Notification implements Parcelable
if (largeIcon != null && isLegacy()
&& getColorUtil().isGrayscaleIcon(mContext, largeIcon)) {
// resolve color will fall back to the default when legacy
int color = getContrastColor(p);
int color = getSmallIconColor(p);
contentView.setInt(R.id.icon, "setOriginalIconColor", color);
}
}
@@ -6286,14 +6256,7 @@ public class Notification implements Parcelable
*/
private @ColorInt int getStandardActionColor(Notification.StandardTemplateParams p) {
return mTintActionButtons || isBackgroundColorized(p)
? getAccentColor(p) : getNeutralColor(p);
}
/**
* Gets a neutral color that can be used for icons or similar that should not stand out.
*/
private @ColorInt int getHeaderIconColor(StandardTemplateParams p) {
return isBackgroundColorized(p) ? getSecondaryTextColor(p) : getNeutralColor(p);
? getPrimaryAccentColor(p) : getSecondaryTextColor(p);
}
/**
@@ -6301,134 +6264,12 @@ public class Notification implements Parcelable
* is the primary text color, otherwise it's the contrast-adjusted app-provided color.
*/
private @ColorInt int getSmallIconColor(StandardTemplateParams p) {
return getContrastColor(p);
return getColors(p).getContrastColor();
}
/**
* Gets the accent color for colored UI elements. If we're tinting with the theme
* accent, this is the theme accent color, otherwise this would be identical to
* {@link #getSmallIconColor(StandardTemplateParams)}.
*/
private @ColorInt int getAccentColor(StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
return getPrimaryTextColor(p);
}
int color = obtainThemeColor(R.attr.colorAccent, COLOR_INVALID);
if (color != COLOR_INVALID) {
return color;
}
return getContrastColor(p);
}
/**
* Gets the secondary accent color for colored UI elements. If we're tinting with the theme
* accent, this is the theme accent color, otherwise this would be identical to
* {@link #getSmallIconColor(StandardTemplateParams)}.
*/
private @ColorInt int getSecondaryAccentColor(StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
return getSecondaryTextColor(p);
}
int color = obtainThemeColor(R.attr.colorAccentSecondary, COLOR_INVALID);
if (color != COLOR_INVALID) {
return color;
}
return getContrastColor(p);
}
/**
* Gets the "surface protection" color from the theme, or a variant of the normal background
* color when colorized, or when not using theme color tints.
*/
private @ColorInt int getProtectionColor(StandardTemplateParams p) {
if (!isBackgroundColorized(p)) {
int color = obtainThemeColor(R.attr.colorBackgroundFloating, COLOR_INVALID);
if (color != COLOR_INVALID) {
return color;
}
}
// TODO(b/181048615): What color should we use for the expander pill when colorized
return ColorUtils.blendARGB(getPrimaryTextColor(p), getBackgroundColor(p), 0.8f);
}
/**
* Gets the theme's error color, or the primary text color for colorized notifications.
*/
private @ColorInt int getErrorColor(StandardTemplateParams p) {
if (!isBackgroundColorized(p)) {
int color = obtainThemeColor(R.attr.colorError, COLOR_INVALID);
if (color != COLOR_INVALID) {
return color;
}
}
return getPrimaryTextColor(p);
}
/**
* Gets the theme's background color
*/
private @ColorInt int getDefaultBackgroundColor() {
return obtainThemeColor(R.attr.colorSurface,
mInNightMode ? Color.BLACK : Color.WHITE);
}
/**
* Gets the contrast-adjusted version of the color provided by the app.
*/
private @ColorInt int getContrastColor(StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
return getPrimaryTextColor(p);
}
int rawColor = getRawColor(p);
if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
return mCachedContrastColor;
}
int color;
// TODO: Maybe use getBackgroundColor(p) instead -- but doing so could break the cache
int background = getDefaultBackgroundColor();
if (rawColor == COLOR_DEFAULT) {
ensureColors(p);
color = obtainThemeColor(R.attr.colorAccent, COLOR_INVALID);
if (color == COLOR_INVALID) {
color = ContrastColorUtil.resolveDefaultColor(mContext, background,
mInNightMode);
}
} else {
color = ContrastColorUtil.resolveContrastColor(mContext, rawColor,
background, mInNightMode);
}
if (Color.alpha(color) < 255) {
// alpha doesn't go well for color filters, so let's blend it manually
color = ContrastColorUtil.compositeColors(color, background);
}
mCachedContrastColorIsFor = rawColor;
return mCachedContrastColor = color;
}
/**
* Return the raw color of this Notification, which doesn't necessarily satisfy contrast.
*
* @see #getContrastColor(StandardTemplateParams) for the contrasted color
* @param p the template params to inflate this with
*/
private @ColorInt int getRawColor(StandardTemplateParams p) {
return mN.color;
}
/**
* Gets a neutral palette color; this is a contrast-satisfied version of the default color.
* @param p the template params to inflate this with
*/
private @ColorInt int getNeutralColor(StandardTemplateParams p) {
int background = getBackgroundColor(p);
int neutralColor = ContrastColorUtil.resolveDefaultColor(mContext, background,
mInNightMode);
if (Color.alpha(neutralColor) < 255) {
// alpha doesn't go well for color filters, so let's blend it manually
neutralColor = ContrastColorUtil.compositeColors(neutralColor, background);
}
return neutralColor;
/** @return the theme's accent color for colored UI elements. */
private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) {
return getColors(p).getPrimaryAccentColor();
}
/**
@@ -6573,23 +6414,6 @@ public class Notification implements Parcelable
return mN;
}
/**
* Returns the color for the given Theme.DeviceDefault.DayNight attribute, or
* defValue if that could not be completed
*/
private @ColorInt int obtainThemeColor(@AttrRes int attrRes, @ColorInt int defaultColor) {
Resources.Theme theme = mContext.getTheme();
if (theme == null) {
// Running unit tests with mocked context
return defaultColor;
}
theme = new ContextThemeWrapper(mContext, R.style.Theme_DeviceDefault_DayNight)
.getTheme();
try (TypedArray ta = theme.obtainStyledAttributes(new int[]{attrRes})) {
return ta == null ? defaultColor : ta.getColor(0, defaultColor);
}
}
/**
* Apply this Builder to an existing {@link Notification} object.
*
@@ -6700,24 +6524,8 @@ public class Notification implements Parcelable
return R.layout.notification_material_action_tombstone;
}
/**
* Gets the background color, with {@link #COLOR_DEFAULT} being a valid return value,
* which must be resolved by the caller before being used.
*/
private @ColorInt int getUnresolvedBackgroundColor(StandardTemplateParams p) {
return isBackgroundColorized(p) ? getRawColor(p) : COLOR_DEFAULT;
}
/**
* Same as {@link #getUnresolvedBackgroundColor(StandardTemplateParams)} except that it
* also resolves the default color to the background.
*/
private @ColorInt int getBackgroundColor(StandardTemplateParams p) {
int backgroundColor = getUnresolvedBackgroundColor(p);
if (backgroundColor == COLOR_DEFAULT) {
backgroundColor = getDefaultBackgroundColor();
}
return backgroundColor;
return getColors(p).getBackgroundColor();
}
private boolean textColorsNeedInversion() {
@@ -9099,8 +8907,7 @@ public class Notification implements Parcelable
container.setDrawableTint(buttonId, false, tintColor,
PorterDuff.Mode.SRC_ATOP);
int rippleAlpha = Color.alpha(mBuilder.obtainThemeColor(
android.R.attr.colorControlHighlight, COLOR_DEFAULT));
int rippleAlpha = mBuilder.getColors(p).getRippleAlpha();
int rippleColor = Color.argb(rippleAlpha, Color.red(tintColor), Color.green(tintColor),
Color.blue(tintColor));
container.setRippleDrawableColor(buttonId, ColorStateList.valueOf(rippleColor));
@@ -12431,4 +12238,219 @@ public class Notification implements Parcelable
return this;
}
}
/**
* A utility which stores and calculates the palette of colors used to color notifications.
* @hide
*/
@VisibleForTesting
public static class Colors {
private int mPaletteIsForRawColor = COLOR_INVALID;
private boolean mPaletteIsForColorized = false;
private boolean mPaletteIsForNightMode = false;
// The following colors are the palette
private int mBackgroundColor = COLOR_INVALID;
private int mProtectionColor = COLOR_INVALID;
private int mPrimaryTextColor = COLOR_INVALID;
private int mSecondaryTextColor = COLOR_INVALID;
private int mPrimaryAccentColor = COLOR_INVALID;
private int mSecondaryAccentColor = COLOR_INVALID;
private int mErrorColor = COLOR_INVALID;
private int mContrastColor = COLOR_INVALID;
private int mRippleAlpha = 0x33;
/**
* A utility for obtaining a TypedArray of the given DayNight-styled attributes, which
* returns null when the context is a mock with no theme.
*
* NOTE: Calling this method is expensive, as creating a new ContextThemeWrapper
* instances can allocate as much as 5MB of memory, so its important to call this method
* only when necessary, getting as many attributes as possible from each call.
*
* @see Resources.Theme#obtainStyledAttributes(int[])
*/
@Nullable
private static TypedArray obtainDayNightAttributes(@NonNull Context ctx,
@NonNull @StyleableRes int[] attrs) {
// when testing, the mock context may have no theme
if (ctx.getTheme() == null) {
return null;
}
Resources.Theme theme = new ContextThemeWrapper(ctx,
R.style.Theme_DeviceDefault_DayNight).getTheme();
return theme.obtainStyledAttributes(attrs);
}
/** A null-safe wrapper of TypedArray.getColor because mocks return null */
private static @ColorInt int getColor(@Nullable TypedArray ta, int index,
@ColorInt int defValue) {
return ta == null ? defValue : ta.getColor(index, defValue);
}
/**
* Resolve the palette. If the inputs have not changed, this will be a no-op.
* This does not handle invalidating the resolved colors when the context itself changes,
* because that case does not happen in the current notification inflation pipeline; we will
* recreate a new builder (and thus a new palette) when reinflating notifications for a new
* theme (admittedly, we do the same for night mode, but that's easy to check).
*
* @param ctx the builder context.
* @param rawColor the notification's color; may be COLOR_DEFAULT, but may never have alpha.
* @param isColorized whether the notification is colorized.
* @param nightMode whether the UI is in night mode.
*/
public void resolvePalette(Context ctx, int rawColor,
boolean isColorized, boolean nightMode) {
if (mPaletteIsForRawColor == rawColor
&& mPaletteIsForColorized == isColorized
&& mPaletteIsForNightMode == nightMode) {
return;
}
mPaletteIsForRawColor = rawColor;
mPaletteIsForColorized = isColorized;
mPaletteIsForNightMode = nightMode;
if (isColorized) {
if (rawColor == COLOR_DEFAULT) {
int[] attrs = {R.attr.colorAccentTertiary};
try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) {
mBackgroundColor = getColor(ta, 0, Color.WHITE);
}
} else {
mBackgroundColor = rawColor;
}
mProtectionColor = COLOR_INVALID; // filled in at the end
mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast(
ContrastColorUtil.resolvePrimaryColor(ctx, mBackgroundColor, nightMode),
mBackgroundColor, 4.5);
mSecondaryTextColor = ContrastColorUtil.findAlphaToMeetContrast(
ContrastColorUtil.resolveSecondaryColor(ctx, mBackgroundColor, nightMode),
mBackgroundColor, 4.5);
mContrastColor = mPrimaryTextColor;
mPrimaryAccentColor = mPrimaryTextColor;
mSecondaryAccentColor = mSecondaryTextColor;
mErrorColor = mPrimaryTextColor;
mRippleAlpha = 0x33;
} else {
int[] attrs = {
R.attr.colorBackground,
R.attr.colorBackgroundFloating,
R.attr.textColorPrimary,
R.attr.textColorSecondary,
R.attr.colorAccent,
R.attr.colorAccentSecondary,
R.attr.colorError,
R.attr.colorControlHighlight
};
try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) {
mBackgroundColor = getColor(ta, 0, nightMode ? Color.BLACK : Color.WHITE);
mProtectionColor = getColor(ta, 1, COLOR_INVALID);
mPrimaryTextColor = getColor(ta, 2, COLOR_INVALID);
mSecondaryTextColor = getColor(ta, 3, COLOR_INVALID);
mPrimaryAccentColor = getColor(ta, 4, COLOR_INVALID);
mSecondaryAccentColor = getColor(ta, 5, COLOR_INVALID);
mErrorColor = getColor(ta, 6, COLOR_INVALID);
mRippleAlpha = Color.alpha(getColor(ta, 7, 0x33ffffff));
}
mContrastColor = calculateContrastColor(ctx, rawColor, mPrimaryAccentColor,
mBackgroundColor, nightMode);
// make sure every color has a valid value
if (mPrimaryTextColor == COLOR_INVALID) {
mPrimaryTextColor = ContrastColorUtil.resolvePrimaryColor(
ctx, mBackgroundColor, nightMode);
}
if (mSecondaryTextColor == COLOR_INVALID) {
mSecondaryTextColor = ContrastColorUtil.resolveSecondaryColor(
ctx, mBackgroundColor, nightMode);
}
if (mPrimaryAccentColor == COLOR_INVALID) {
mPrimaryAccentColor = mContrastColor;
}
if (mSecondaryAccentColor == COLOR_INVALID) {
mSecondaryAccentColor = mContrastColor;
}
if (mErrorColor == COLOR_INVALID) {
mErrorColor = mPrimaryTextColor;
}
}
// make sure every color has a valid value
if (mProtectionColor == COLOR_INVALID) {
mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.8f);
}
}
/** calculates the contrast color for the non-colorized notifications */
private static @ColorInt int calculateContrastColor(Context ctx, @ColorInt int rawColor,
@ColorInt int accentColor, @ColorInt int backgroundColor, boolean nightMode) {
int color;
if (rawColor == COLOR_DEFAULT) {
color = accentColor;
if (color == COLOR_INVALID) {
color = ContrastColorUtil.resolveDefaultColor(ctx, backgroundColor, nightMode);
}
} else {
color = ContrastColorUtil.resolveContrastColor(ctx, rawColor, backgroundColor,
nightMode);
}
return flattenAlpha(color, backgroundColor);
}
/** remove any alpha by manually blending it with the given background. */
private static @ColorInt int flattenAlpha(@ColorInt int color, @ColorInt int background) {
return Color.alpha(color) == 0xff ? color
: ContrastColorUtil.compositeColors(color, background);
}
/** @return the notification's background color */
public @ColorInt int getBackgroundColor() {
return mBackgroundColor;
}
/**
* @return the "surface protection" color from the theme,
* or a variant of the normal background color when colorized.
*/
public @ColorInt int getProtectionColor() {
return mProtectionColor;
}
/** @return the color for the most prominent text */
public @ColorInt int getPrimaryTextColor() {
return mPrimaryTextColor;
}
/** @return the color for less prominent text */
public @ColorInt int getSecondaryTextColor() {
return mSecondaryTextColor;
}
/** @return the theme's accent color for colored UI elements. */
public @ColorInt int getPrimaryAccentColor() {
return mPrimaryAccentColor;
}
/** @return the theme's secondary accent color for colored UI elements. */
public @ColorInt int getSecondaryAccentColor() {
return mSecondaryAccentColor;
}
/**
* @return the contrast-adjusted version of the color provided by the app, or the
* primary text color when colorized.
*/
public @ColorInt int getContrastColor() {
return mContrastColor;
}
/** @return the theme's error color, or the primary text color when colorized. */
public @ColorInt int getErrorColor() {
return mErrorColor;
}
/** @return the alpha component of the current theme's control highlight color. */
public int getRippleAlpha() {
return mRippleAlpha;
}
}
}

View File

@@ -595,7 +595,9 @@ public class ContrastColorUtil {
if (backgroundColor == Notification.COLOR_DEFAULT) {
return !defaultBackgroundIsDark;
}
return ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
// Color contrast ratio luminance midpoint, X: 1.05 / (X + 0.05) = (X + 0.05) / 0.05
// Solved as X = sqrt(.05 * 1.05) - 0.05 = 0.17912878474
return ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.17912878474;
}
public static double calculateLuminance(int backgroundColor) {
@@ -619,6 +621,7 @@ public class ContrastColorUtil {
}
public static boolean isColorLight(int backgroundColor) {
// TODO(b/188947832): Use 0.17912878474 instead of 0.5 to ensure better contrast
return calculateLuminance(backgroundColor) > 0.5f;
}

View File

@@ -17,14 +17,6 @@
NOTE: You might also want to edit: packages/SystemUI/res/values-night/colors.xml
-->
<resources>
<!-- The primary text color if the text is on top of a dark background.
This is also affects colorized notifications with dark backgrounds. -->
<color name="notification_primary_text_color_dark">#ddffffff</color>
<!-- The secondary text color if the text is on top of a dark background. -->
<color name="notification_secondary_text_color_dark">#b2ffffff</color>
<color name="notification_default_color_dark">#ddffffff</color>
<color name="notification_primary_text_color_current">@color/notification_primary_text_color_dark</color>
<color name="notification_secondary_text_color_current">@color/notification_secondary_text_color_dark</color>

View File

@@ -41,6 +41,7 @@ android_test {
"frameworks-core-util-lib",
"mockwebserver",
"guava",
"androidx.core_core",
"androidx.test.espresso.core",
"androidx.test.ext.junit",
"androidx.test.runner",

View File

@@ -16,6 +16,14 @@
package android.app;
import static androidx.core.graphics.ColorUtils.calculateContrast;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
@@ -27,10 +35,10 @@ import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.LocusId;
import android.content.res.Configuration;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.media.session.MediaSession;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -325,13 +333,142 @@ public class NotificationTest {
assertNull(clone.getLocusId());
}
private Notification.Builder getMediaNotification() {
MediaSession session = new MediaSession(mContext, "test");
return new Notification.Builder(mContext, "color")
.setSmallIcon(com.android.internal.R.drawable.emergency_icon)
.setContentTitle("Title")
.setContentText("Text")
.setStyle(new Notification.MediaStyle().setMediaSession(session.getSessionToken()));
@Test
public void testColors_ensureColors_dayMode_producesValidPalette() {
Notification.Colors c = new Notification.Colors();
boolean colorized = false;
boolean nightMode = false;
resolveColorsInNightMode(nightMode, c, Color.BLUE, colorized);
assertValid(c);
}
@Test
public void testColors_ensureColors_nightMode_producesValidPalette() {
Notification.Colors c = new Notification.Colors();
boolean colorized = false;
boolean nightMode = true;
resolveColorsInNightMode(nightMode, c, Color.BLUE, colorized);
assertValid(c);
}
@Test
public void testColors_ensureColors_colorized_producesValidPalette_default() {
validateColorizedPaletteForColor(Notification.COLOR_DEFAULT);
}
@Test
public void testColors_ensureColors_colorized_producesValidPalette_blue() {
validateColorizedPaletteForColor(Color.BLUE);
}
@Test
public void testColors_ensureColors_colorized_producesValidPalette_red() {
validateColorizedPaletteForColor(Color.RED);
}
@Test
public void testColors_ensureColors_colorized_producesValidPalette_white() {
validateColorizedPaletteForColor(Color.WHITE);
}
@Test
public void testColors_ensureColors_colorized_producesValidPalette_black() {
validateColorizedPaletteForColor(Color.BLACK);
}
public void validateColorizedPaletteForColor(int rawColor) {
Notification.Colors cDay = new Notification.Colors();
Notification.Colors cNight = new Notification.Colors();
boolean colorized = true;
resolveColorsInNightMode(false, cDay, rawColor, colorized);
resolveColorsInNightMode(true, cNight, rawColor, colorized);
if (rawColor != Notification.COLOR_DEFAULT) {
assertEquals(rawColor, cDay.getBackgroundColor());
assertEquals(rawColor, cNight.getBackgroundColor());
}
assertValid(cDay);
assertValid(cNight);
if (rawColor != Notification.COLOR_DEFAULT) {
// When a color is provided, night mode should have no effect on the notification
assertEquals(cDay.getBackgroundColor(), cNight.getBackgroundColor());
assertEquals(cDay.getPrimaryTextColor(), cNight.getPrimaryTextColor());
assertEquals(cDay.getSecondaryTextColor(), cNight.getSecondaryTextColor());
assertEquals(cDay.getPrimaryAccentColor(), cNight.getPrimaryAccentColor());
assertEquals(cDay.getSecondaryAccentColor(), cNight.getSecondaryAccentColor());
assertEquals(cDay.getProtectionColor(), cNight.getProtectionColor());
assertEquals(cDay.getContrastColor(), cNight.getContrastColor());
assertEquals(cDay.getRippleAlpha(), cNight.getRippleAlpha());
}
}
private void assertValid(Notification.Colors c) {
// Assert that all colors are populated
assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getProtectionColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getPrimaryTextColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getSecondaryTextColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getPrimaryAccentColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getSecondaryAccentColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getErrorColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getContrastColor()).isNotEqualTo(Notification.COLOR_INVALID);
assertThat(c.getRippleAlpha()).isAtLeast(0x00);
assertThat(c.getRippleAlpha()).isAtMost(0xff);
// Assert that various colors have sufficient contrast
assertContrastIsAtLeast(c.getPrimaryTextColor(), c.getBackgroundColor(), 4.5);
assertContrastIsAtLeast(c.getSecondaryTextColor(), c.getBackgroundColor(), 4.5);
assertContrastIsAtLeast(c.getPrimaryAccentColor(), c.getBackgroundColor(), 4.5);
assertContrastIsAtLeast(c.getErrorColor(), c.getBackgroundColor(), 4.5);
assertContrastIsAtLeast(c.getContrastColor(), c.getBackgroundColor(), 4.5);
// This accent color is only used for emphasized buttons
assertContrastIsAtLeast(c.getSecondaryAccentColor(), c.getBackgroundColor(), 1);
}
private void assertContrastIsAtLeast(int foreground, int background, double minContrast) {
try {
assertThat(calculateContrast(foreground, background)).isAtLeast(minContrast);
} catch (AssertionError e) {
throw new AssertionError(
String.format("Insufficient contrast: foreground=#%08x background=#%08x",
foreground, background), e);
}
}
private void resolveColorsInNightMode(boolean nightMode, Notification.Colors c, int rawColor,
boolean colorized) {
runInNightMode(nightMode,
() -> c.resolvePalette(mContext, rawColor, colorized, nightMode));
}
private void runInNightMode(boolean nightMode, Runnable task) {
final String initialNightMode = changeNightMode(nightMode);
try {
Configuration currentConfig = mContext.getResources().getConfiguration();
boolean isNightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
assertEquals(nightMode, isNightMode);
task.run();
} finally {
runShellCommand("cmd uimode night " + initialNightMode);
}
}
// Change the night mode and return the previous mode
private String changeNightMode(boolean nightMode) {
final String nightModeText = runShellCommand("cmd uimode night");
final String[] nightModeSplit = nightModeText.split(":");
if (nightModeSplit.length != 2) {
fail("Failed to get initial night mode value from " + nightModeText);
}
String previousMode = nightModeSplit[1].trim();
runShellCommand("cmd uimode night " + (nightMode ? "yes" : "no"));
return previousMode;
}
/**