Fully custom view notifications now receive minimal decoration when targeting S.
* The level of decoration for fully custom notifications can be customized with FULLY_CUSTOM_VIEW_NOTIF_DECORATION
* The level of decoration for decorated notifications can be customized with DECORATED_CUSTOM_VIEW_NOTIF_DECORATION
* Adds some helpful logic when "backporting" S logic so that we can see what UNDO notifications will really look like.
Note: this patch now includes code that used to be in separate reviews: I700472b9c8fba2563b447f570cab7dd60a0a3984 and I55e12843a3edc6e9a78e5b8c848400a8f9228d1c
Test: Manual
Bug: 173522761
Change-Id: I7aba1aec2ba3023857735754c000977c4491b774
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 297b9ff..587a7cc 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -102,6 +102,7 @@
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -645,6 +646,11 @@
*/
public static final int FLAG_IMMEDIATE_FGS_DISPLAY = 0x00002000;
+ private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
+ BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
+ DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
+ MessagingStyle.class);
+
/** @hide */
@IntDef({FLAG_SHOW_LIGHTS, FLAG_ONGOING_EVENT, FLAG_INSISTENT, FLAG_ONLY_ALERT_ONCE,
FLAG_AUTO_CANCEL, FLAG_NO_CLEAR, FLAG_FOREGROUND_SERVICE, FLAG_HIGH_PRIORITY,
@@ -5098,7 +5104,7 @@
final int max = ex.getInt(EXTRA_PROGRESS_MAX, 0);
final int progress = ex.getInt(EXTRA_PROGRESS, 0);
final boolean ind = ex.getBoolean(EXTRA_PROGRESS_INDETERMINATE);
- if (p.hasProgress && (max != 0 || ind)) {
+ if (!p.mHideProgress && (max != 0 || ind)) {
contentView.setViewVisibility(com.android.internal.R.id.progress, View.VISIBLE);
contentView.setProgressBar(
R.id.progress, max, progress, ind);
@@ -5426,7 +5432,7 @@
int N = nonContextualActions.size();
boolean emphazisedMode = mN.fullScreenIntent != null;
big.setBoolean(R.id.actions, "setEmphasizedMode", emphazisedMode);
- if (N > 0) {
+ if (N > 0 && !p.mHideActions) {
big.setViewVisibility(R.id.actions_container, View.VISIBLE);
big.setViewVisibility(R.id.actions, View.VISIBLE);
big.setViewLayoutMarginDimen(R.id.notification_action_list_margin_target,
@@ -5519,6 +5525,71 @@
return createContentView(false /* increasedheight */ );
}
+ // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps,
+ // a use case that is not supported by the Compat Framework library. Workarounds to resolve
+ // the change's state in NotificationManagerService were very complex. While it's possible
+ // apps can detect the change, it's most likely that the changes will simply result in
+ // visual regressions.
+ @SuppressWarnings("AndroidFrameworkCompatChange")
+ private boolean fullyCustomViewRequiresDecoration(boolean fromStyle) {
+ // Custom views which come from a platform style class are safe, and thus do not need to
+ // be wrapped. Any subclass of those styles has the opportunity to make arbitrary
+ // changes to the RemoteViews, and thus can't be trusted as a fully vetted view.
+ if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) {
+ return false;
+ }
+ final ContentResolver contentResolver = mContext.getContentResolver();
+ final int decorationType = DevFlags.getFullyCustomViewNotifDecoration(contentResolver);
+ return decorationType != DevFlags.DECORATION_NONE
+ && (DevFlags.shouldBackportSNotifRules(contentResolver)
+ || mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S);
+ }
+
+ private RemoteViews minimallyDecoratedContentView(@NonNull RemoteViews customContent) {
+ int decorationType =
+ DevFlags.getFullyCustomViewNotifDecoration(mContext.getContentResolver());
+ StandardTemplateParams p = mParams.reset()
+ .viewType(StandardTemplateParams.VIEW_TYPE_NORMAL)
+ .decorationType(decorationType)
+ .fillTextsFrom(this);
+ TemplateBindResult result = new TemplateBindResult();
+ RemoteViews standard = applyStandardTemplate(getBaseLayoutResource(), p, result);
+ buildCustomContentIntoTemplate(mContext, standard, customContent,
+ p, result, decorationType);
+ return standard;
+ }
+
+ private RemoteViews minimallyDecoratedBigContentView(@NonNull RemoteViews customContent) {
+ int decorationType =
+ DevFlags.getFullyCustomViewNotifDecoration(mContext.getContentResolver());
+ StandardTemplateParams p = mParams.reset()
+ .viewType(StandardTemplateParams.VIEW_TYPE_BIG)
+ .decorationType(decorationType)
+ .fillTextsFrom(this);
+ TemplateBindResult result = new TemplateBindResult();
+ RemoteViews standard = applyStandardTemplateWithActions(getBigBaseLayoutResource(),
+ p, result);
+ buildCustomContentIntoTemplate(mContext, standard, customContent,
+ p, result, decorationType);
+ return standard;
+ }
+
+ private RemoteViews minimallyDecoratedHeadsUpContentView(
+ @NonNull RemoteViews customContent) {
+ int decorationType =
+ DevFlags.getFullyCustomViewNotifDecoration(mContext.getContentResolver());
+ StandardTemplateParams p = mParams.reset()
+ .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP)
+ .decorationType(decorationType)
+ .fillTextsFrom(this);
+ TemplateBindResult result = new TemplateBindResult();
+ RemoteViews standard = applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(),
+ p, result);
+ buildCustomContentIntoTemplate(mContext, standard, customContent,
+ p, result, decorationType);
+ return standard;
+ }
+
/**
* Construct a RemoteViews for the smaller content view.
*
@@ -5531,11 +5602,13 @@
*/
public RemoteViews createContentView(boolean increasedHeight) {
if (mN.contentView != null && useExistingRemoteView()) {
- return mN.contentView;
+ return fullyCustomViewRequiresDecoration(false /* fromStyle */)
+ ? minimallyDecoratedContentView(mN.contentView) : mN.contentView;
} else if (mStyle != null) {
final RemoteViews styleView = mStyle.makeContentView(increasedHeight);
if (styleView != null) {
- return styleView;
+ return fullyCustomViewRequiresDecoration(true /* fromStyle */)
+ ? minimallyDecoratedContentView(styleView) : styleView;
}
}
StandardTemplateParams p = mParams.reset()
@@ -5555,18 +5628,30 @@
public RemoteViews createBigContentView() {
RemoteViews result = null;
if (mN.bigContentView != null && useExistingRemoteView()) {
- return mN.bigContentView;
+ return fullyCustomViewRequiresDecoration(false /* fromStyle */)
+ ? minimallyDecoratedBigContentView(mN.bigContentView) : mN.bigContentView;
}
if (mStyle != null) {
result = mStyle.makeBigContentView();
hideLine1Text(result);
+ if (fullyCustomViewRequiresDecoration(true /* fromStyle */)) {
+ result = minimallyDecoratedBigContentView(result);
+ }
}
- if (result == null && bigContentViewRequired()) {
- StandardTemplateParams p = mParams.reset()
- .viewType(StandardTemplateParams.VIEW_TYPE_BIG)
- .fillTextsFrom(this);
- result = applyStandardTemplateWithActions(getBigBaseLayoutResource(), p,
- null /* result */);
+ if (result == null) {
+ if (bigContentViewRequired()) {
+ StandardTemplateParams p = mParams.reset()
+ .viewType(StandardTemplateParams.VIEW_TYPE_BIG)
+ .fillTextsFrom(this);
+ result = applyStandardTemplateWithActions(getBigBaseLayoutResource(), p,
+ null /* result */);
+ } else if (DevFlags.shouldBackportSNotifRules(mContext.getContentResolver())
+ && useExistingRemoteView()
+ && fullyCustomViewRequiresDecoration(false /* fromStyle */)) {
+ // This "backport" logic is a special case to handle the UNDO style of notif
+ // so that we can see what that will look like when the app targets S.
+ result = minimallyDecoratedBigContentView(mN.contentView);
+ }
}
makeHeaderExpanded(result);
return result;
@@ -5660,11 +5745,14 @@
*/
public RemoteViews createHeadsUpContentView(boolean increasedHeight) {
if (mN.headsUpContentView != null && useExistingRemoteView()) {
- return mN.headsUpContentView;
+ return fullyCustomViewRequiresDecoration(false /* fromStyle */)
+ ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView)
+ : mN.headsUpContentView;
} else if (mStyle != null) {
final RemoteViews styleView = mStyle.makeHeadsUpContentView(increasedHeight);
if (styleView != null) {
- return styleView;
+ return fullyCustomViewRequiresDecoration(true /* fromStyle */)
+ ? minimallyDecoratedHeadsUpContentView(styleView) : styleView;
}
} else if (mActions.size() == 0) {
return null;
@@ -5676,8 +5764,7 @@
.viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP)
.fillTextsFrom(this)
.setMaxRemoteInputHistory(1);
- return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(),
- p,
+ return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), p,
null /* result */);
}
@@ -6595,11 +6682,7 @@
*/
@SystemApi
public static Class<? extends Style> getNotificationStyleClass(String templateClass) {
- Class<? extends Style>[] classes = new Class[] {
- BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
- DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
- MessagingStyle.class };
- for (Class<? extends Style> innerClass : classes) {
+ for (Class<? extends Style> innerClass : PLATFORM_STYLE_CLASSES) {
if (templateClass.equals(innerClass.getName())) {
return innerClass;
}
@@ -6607,6 +6690,57 @@
return null;
}
+ private static void buildCustomContentIntoTemplate(@NonNull Context context,
+ @NonNull RemoteViews template, @Nullable RemoteViews customContent,
+ @NonNull StandardTemplateParams p, @NonNull TemplateBindResult result,
+ int decorationType) {
+ int childIndex = -1;
+ if (customContent != null) {
+ // Need to clone customContent before adding, because otherwise it can no longer be
+ // parceled independently of remoteViews.
+ customContent = customContent.clone();
+ if (p.mHeaderless) {
+ if (decorationType <= DevFlags.DECORATION_PARTIAL) {
+ template.removeAllViewsExceptId(R.id.notification_top_line_container,
+ R.id.notification_main_column);
+ }
+ if (decorationType != DevFlags.DECORATION_FULL_COMPATIBLE) {
+ // Change the max content size from 60dp (the compatible size) to 48dp
+ // (the constrained size). This is done by increasing the minimum margin
+ // (implemented as top/bottom margins) and decreasing the extra margin
+ // (implemented as the height of shrinkable top/bottom views in the column).
+ template.setViewLayoutMarginDimen(
+ R.id.notification_headerless_view_column,
+ RemoteViews.MARGIN_TOP,
+ R.dimen.notification_headerless_margin_constrained_minimum);
+ template.setViewLayoutMarginDimen(
+ R.id.notification_headerless_view_column,
+ RemoteViews.MARGIN_BOTTOM,
+ R.dimen.notification_headerless_margin_constrained_minimum);
+ template.setViewLayoutHeightDimen(
+ R.id.notification_headerless_margin_extra_top,
+ R.dimen.notification_headerless_margin_constrained_extra);
+ template.setViewLayoutHeightDimen(
+ R.id.notification_headerless_margin_extra_bottom,
+ R.dimen.notification_headerless_margin_constrained_extra);
+ }
+ } else {
+ // also update the end margin to account for the large icon or expander
+ Resources resources = context.getResources();
+ result.mTitleMarginSet.applyToView(template, R.id.notification_main_column,
+ resources.getDimension(R.dimen.notification_content_margin_end)
+ / resources.getDisplayMetrics().density);
+ }
+ template.removeAllViewsExceptId(R.id.notification_main_column, R.id.progress);
+ template.addView(R.id.notification_main_column, customContent, 0 /* index */);
+ template.addFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED);
+ childIndex = 0;
+ }
+ template.setIntTag(R.id.notification_main_column,
+ com.android.internal.R.id.notification_custom_view_index_tag,
+ childIndex);
+ }
+
/**
* An object that can apply a rich notification style to a {@link Notification.Builder}
* object.
@@ -7812,7 +7946,7 @@
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(isCollapsed ? StandardTemplateParams.VIEW_TYPE_NORMAL
: StandardTemplateParams.VIEW_TYPE_BIG)
- .hasProgress(false)
+ .hideProgress(true)
.title(conversationTitle)
.text(null)
.hideLargeIcon(hideRightIcons || isOneToOne)
@@ -8627,7 +8761,8 @@
private RemoteViews makeMediaContentView() {
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_NORMAL)
- .hasProgress(false).fillTextsFrom(mBuilder);
+ .hideProgress(true)
+ .fillTextsFrom(mBuilder);
RemoteViews view = mBuilder.applyStandardTemplate(
R.layout.notification_template_material_media, p,
null /* result */);
@@ -8676,7 +8811,8 @@
}
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_BIG)
- .hasProgress(false).fillTextsFrom(mBuilder);
+ .hideProgress(true)
+ .fillTextsFrom(mBuilder);
RemoteViews big = mBuilder.applyStandardTemplate(
R.layout.notification_template_material_big_media, p , null /* result */);
@@ -8770,29 +8906,39 @@
RemoteViews headsUpContentView = mBuilder.mN.headsUpContentView == null
? mBuilder.mN.contentView
: mBuilder.mN.headsUpContentView;
+ if (headsUpContentView == null) {
+ return null; // no custom view; use the default behavior
+ }
if (mBuilder.mActions.size() == 0) {
return makeStandardTemplateWithCustomContent(headsUpContentView);
}
+ int decorationType = getDecorationType();
TemplateBindResult result = new TemplateBindResult();
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP)
- .hasCustomContent(headsUpContentView != null)
+ .decorationType(decorationType)
.fillTextsFrom(mBuilder);
RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions(
mBuilder.getHeadsUpBaseLayoutResource(), p, result);
- buildIntoRemoteViewContent(remoteViews, headsUpContentView, result, true);
+ buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, headsUpContentView,
+ p, result, decorationType);
return remoteViews;
}
private RemoteViews makeStandardTemplateWithCustomContent(RemoteViews customContent) {
+ if (customContent == null) {
+ return null; // no custom view; use the default behavior
+ }
+ int decorationType = getDecorationType();
TemplateBindResult result = new TemplateBindResult();
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_NORMAL)
- .hasCustomContent(customContent != null)
+ .decorationType(decorationType)
.fillTextsFrom(mBuilder);
RemoteViews remoteViews = mBuilder.applyStandardTemplate(
mBuilder.getBaseLayoutResource(), p, result);
- buildIntoRemoteViewContent(remoteViews, customContent, result, true);
+ buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, customContent,
+ p, result, decorationType);
return remoteViews;
}
@@ -8800,38 +8946,37 @@
RemoteViews bigContentView = mBuilder.mN.bigContentView == null
? mBuilder.mN.contentView
: mBuilder.mN.bigContentView;
+ if (bigContentView == null) {
+ return null; // no custom view; use the default behavior
+ }
+ int decorationType = getDecorationType();
TemplateBindResult result = new TemplateBindResult();
StandardTemplateParams p = mBuilder.mParams.reset()
.viewType(StandardTemplateParams.VIEW_TYPE_BIG)
- .hasCustomContent(bigContentView != null)
+ .decorationType(decorationType)
.fillTextsFrom(mBuilder);
RemoteViews remoteViews = mBuilder.applyStandardTemplateWithActions(
mBuilder.getBigBaseLayoutResource(), p, result);
- buildIntoRemoteViewContent(remoteViews, bigContentView, result, false);
+ buildCustomContentIntoTemplate(mBuilder.mContext, remoteViews, bigContentView,
+ p, result, decorationType);
return remoteViews;
}
- private void buildIntoRemoteViewContent(RemoteViews remoteViews,
- RemoteViews customContent, TemplateBindResult result, boolean headerless) {
- int childIndex = -1;
- if (customContent != null) {
- // Need to clone customContent before adding, because otherwise it can no longer be
- // parceled independently of remoteViews.
- customContent = customContent.clone();
- remoteViews.removeAllViewsExceptId(R.id.notification_main_column, R.id.progress);
- remoteViews.addView(R.id.notification_main_column, customContent, 0 /* index */);
- remoteViews.addFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED);
- childIndex = 0;
- }
- remoteViews.setIntTag(R.id.notification_main_column,
- com.android.internal.R.id.notification_custom_view_index_tag,
- childIndex);
- if (!headerless) {
- // also update the end margin to account for the large icon or expander
- Resources resources = mBuilder.mContext.getResources();
- result.mTitleMarginSet.applyToView(remoteViews, R.id.notification_main_column,
- resources.getDimension(R.dimen.notification_content_margin_end)
- / resources.getDisplayMetrics().density);
+ // This code is executed on behalf of other apps' notifications, sometimes even by 3p apps,
+ // a use case that is not supported by the Compat Framework library. Workarounds to resolve
+ // the change's state in NotificationManagerService were very complex. While it's possible
+ // apps can detect the change, it's most likely that the changes will simply result in
+ // visual regressions.
+ @SuppressWarnings("AndroidFrameworkCompatChange")
+ private int getDecorationType() {
+ ContentResolver contentResolver = mBuilder.mContext.getContentResolver();
+ if (mBuilder.mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S
+ || DevFlags.shouldBackportSNotifRules(contentResolver)) {
+ return DevFlags.getDecoratedCustomViewNotifDecoration(contentResolver);
+ } else {
+ // For apps that don't target S, this decoration provides the closest behavior to R,
+ // but doesn't fit with the design guidelines for S.
+ return DevFlags.DECORATION_FULL_COMPATIBLE;
}
}
@@ -11158,8 +11303,9 @@
int mViewType = VIEW_TYPE_UNSPECIFIED;
boolean mHeaderless;
- boolean mHasCustomContent;
- boolean hasProgress = true;
+ boolean mHideTitle;
+ boolean mHideActions;
+ boolean mHideProgress;
CharSequence title;
CharSequence text;
CharSequence headerTextSecondary;
@@ -11172,8 +11318,9 @@
final StandardTemplateParams reset() {
mViewType = VIEW_TYPE_UNSPECIFIED;
mHeaderless = false;
- mHasCustomContent = false;
- hasProgress = true;
+ mHideTitle = false;
+ mHideActions = false;
+ mHideProgress = false;
title = null;
text = null;
summaryText = null;
@@ -11185,9 +11332,7 @@
}
final boolean hasTitle() {
- // We hide the title when the notification is a decorated custom view so that decorated
- // custom views always have to include their own title.
- return !TextUtils.isEmpty(title) && !mHasCustomContent;
+ return !TextUtils.isEmpty(title) && !mHideTitle;
}
final StandardTemplateParams viewType(int viewType) {
@@ -11200,13 +11345,18 @@
return this;
}
- final StandardTemplateParams hasProgress(boolean hasProgress) {
- this.hasProgress = hasProgress;
+ final StandardTemplateParams hideActions(boolean hideActions) {
+ this.mHideActions = hideActions;
return this;
}
- final StandardTemplateParams hasCustomContent(boolean hasCustomContent) {
- this.mHasCustomContent = hasCustomContent;
+ final StandardTemplateParams hideProgress(boolean hideProgress) {
+ this.mHideProgress = hideProgress;
+ return this;
+ }
+
+ final StandardTemplateParams hideTitle(boolean hideTitle) {
+ this.mHideTitle = hideTitle;
return this;
}
@@ -11262,6 +11412,16 @@
this.maxRemoteInputHistory = maxRemoteInputHistory;
return this;
}
+
+ public StandardTemplateParams decorationType(int decorationType) {
+ boolean hideTitle = decorationType <= DevFlags.DECORATION_FULL_COMPATIBLE;
+ boolean hideOtherFields = decorationType <= DevFlags.DECORATION_MINIMAL;
+ hideTitle(hideTitle);
+ hideLargeIcon(hideOtherFields);
+ hideProgress(hideOtherFields);
+ hideActions(hideOtherFields);
+ return this;
+ }
}
/**
@@ -11271,7 +11431,67 @@
* @hide
*/
public static class DevFlags {
+
+ /**
+ * Notifications will not be decorated. The custom content will be shown as-is.
+ *
+ * <p>NOTE: This option is not available for notifications with DecoratedCustomViewStyle,
+ * as that API contract includes decorations that this does not provide.
+ */
+ public static final int DECORATION_NONE = 0;
+
+ /**
+ * Notifications will be minimally decorated with ONLY an icon and expander as follows:
+ * <li>A large icon is never shown.
+ * <li>A progress bar is never shown.
+ * <li>The expanded and heads up states do not show actions, even if provided.
+ * <li>The collapsed state gives the app's custom content 48dp of vertical space.
+ * <li>The collapsed state does NOT include the top line of views,
+ * like the alerted icon or work profile badge.
+ *
+ * <p>NOTE: This option is not available for notifications with DecoratedCustomViewStyle,
+ * as that API contract includes decorations that this does not provide.
+ */
+ public static final int DECORATION_MINIMAL = 1;
+
+ /**
+ * Notifications will be partially decorated with AT LEAST an icon and expander as follows:
+ * <li>A large icon is shown if provided.
+ * <li>A progress bar is shown if provided and enough space remains below the content.
+ * <li>Actions are shown in the expanded and heads up states.
+ * <li>The collapsed state gives the app's custom content 48dp of vertical space.
+ * <li>The collapsed state does NOT include the top line of views,
+ * like the alerted icon or work profile badge.
+ */
+ public static final int DECORATION_PARTIAL = 2;
+
+ /**
+ * Notifications will be fully decorated as follows:
+ * <li>A large icon is shown if provided.
+ * <li>A progress bar is shown if provided and enough space remains below the content.
+ * <li>Actions are shown in the expanded and heads up states.
+ * <li>The collapsed state gives the app's custom content 40dp of vertical space.
+ * <li>The collapsed state DOES include the top line of views,
+ * like the alerted icon or work profile badge.
+ * <li>The collapsed state's top line views will never show the title text.
+ */
+ public static final int DECORATION_FULL_COMPATIBLE = 3;
+
+ /**
+ * Notifications will be fully decorated as follows:
+ * <li>A large icon is shown if provided.
+ * <li>A progress bar is shown if provided and enough space remains below the content.
+ * <li>Actions are shown in the expanded and heads up states.
+ * <li>The collapsed state gives the app's custom content 20dp of vertical space.
+ * <li>The collapsed state DOES include the top line of views
+ * like the alerted icon or work profile badge.
+ * <li>The collapsed state's top line views will contain the title text if provided.
+ */
+ public static final int DECORATION_FULL_CONSTRAINED = 4;
+
private static final boolean DEFAULT_BACKPORT_S_NOTIF_RULES = false;
+ private static final int DEFAULT_FULLY_CUSTOM_DECORATION = DECORATION_MINIMAL;
+ private static final int DEFAULT_DECORATED_DECORATION = DECORATION_PARTIAL;
/**
* Used by unit tests to force that this class returns its default values, which is required
@@ -11291,5 +11511,37 @@
return Settings.Global.getInt(contentResolver, Settings.Global.BACKPORT_S_NOTIF_RULES,
DEFAULT_BACKPORT_S_NOTIF_RULES ? 1 : 0) == 1;
}
+
+ /**
+ * @return the decoration type to be applied to notifications with fully custom view.
+ * @hide
+ */
+ public static int getFullyCustomViewNotifDecoration(
+ @NonNull ContentResolver contentResolver) {
+ if (sForceDefaults) {
+ return DEFAULT_FULLY_CUSTOM_DECORATION;
+ }
+ final int decoration = Settings.Global.getInt(contentResolver,
+ Settings.Global.FULLY_CUSTOM_VIEW_NOTIF_DECORATION,
+ DEFAULT_FULLY_CUSTOM_DECORATION);
+ // clamp to a valid decoration value
+ return Math.max(DECORATION_NONE, Math.min(decoration, DECORATION_FULL_CONSTRAINED));
+ }
+
+ /**
+ * @return the decoration type to be applied to notifications with DecoratedCustomViewStyle.
+ * @hide
+ */
+ public static int getDecoratedCustomViewNotifDecoration(
+ @NonNull ContentResolver contentResolver) {
+ if (sForceDefaults) {
+ return DEFAULT_DECORATED_DECORATION;
+ }
+ final int decoration = Settings.Global.getInt(contentResolver,
+ Settings.Global.DECORATED_CUSTOM_VIEW_NOTIF_DECORATION,
+ DEFAULT_DECORATED_DECORATION);
+ // clamp to a valid decoration value (and don't allow decoration to be NONE or MINIMAL)
+ return Math.max(DECORATION_PARTIAL, Math.min(decoration, DECORATION_FULL_CONSTRAINED));
+ }
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 69ca28a..9db7ca0 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -14630,6 +14630,34 @@
public static final String BACKPORT_S_NOTIF_RULES = "backport_s_notif_rules";
/**
+ * The decoration to put on fully custom views that target S.
+ *
+ * <p>Values are:
+ * <br>0: DECORATION_NONE: no decorations.
+ * <br>1: DECORATION_MINIMAL: most minimal template; just the icon and the expander.
+ * <br>2: DECORATION_PARTIAL: basic template without the top line.
+ * <br>3: DECORATION_FULL_COMPATIBLE: basic template with the top line; 40dp of height.
+ * <br>4: DECORATION_FULL_CONSTRAINED: basic template with the top line; 28dp of height.
+ * <p>See {@link android.app.Notification.DevFlags} for more details.
+ * @hide
+ */
+ public static final String FULLY_CUSTOM_VIEW_NOTIF_DECORATION =
+ "fully_custom_view_notif_decoration";
+
+ /**
+ * The decoration to put on decorated custom views that target S.
+ *
+ * <p>Values are:
+ * <br>2: DECORATION_PARTIAL: basic template without the top line.
+ * <br>3: DECORATION_FULL_COMPATIBLE: basic template with the top line; 40dp of height.
+ * <br>4: DECORATION_FULL_CONSTRAINED: basic template with the top line; 28dp of height.
+ * <p>See {@link android.app.Notification.DevFlags} for more details.
+ * @hide
+ */
+ public static final String DECORATED_CUSTOM_VIEW_NOTIF_DECORATION =
+ "decorated_custom_view_notif_decoration";
+
+ /**
* Block untrusted touches mode.
*
* Can be one of:
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index 0006384..a045e15 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -45,103 +45,6 @@
android:padding="@dimen/notification_icon_circle_padding"
/>
- <LinearLayout
- android:id="@+id/notification_headerless_view_column"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_marginBottom="@dimen/notification_headerless_margin_minimum"
- android:layout_marginTop="@dimen/notification_headerless_margin_minimum"
- android:orientation="vertical"
- >
-
- <!--
- This invisible FrameLayout is here as a collapsible padding. Having a layout_weight=1 is
- what causes this view (and it's counterpart at the opposite end) to collapse before the
- actual content views do.
- This pair of 10dp collapsible paddings (plus the 16dp fixed margins) allow us to support
- headerless notifications of 1-3 lines (where each line is 20dp tall) where the 1-line
- variant is 56dp and the 2- and 3-line variants are both 76dp.
- -->
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_headerless_margin_extra"
- android:layout_weight="1"
- />
-
- <!-- extends ViewGroup -->
- <NotificationTopLineView
- android:id="@+id/notification_top_line"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/notification_headerless_line_height"
- android:layout_marginEnd="@dimen/notification_heading_margin_end"
- android:layout_marginStart="@dimen/notification_content_margin_start"
- android:clipChildren="false"
- android:theme="@style/Theme.DeviceDefault.Notification"
- >
-
- <!--
- NOTE: The notification_top_line_views layout contains the app_name_text.
- In order to include the title view at the beginning, the Notification.Builder
- has logic to hide that view whenever this title view is to be visible.
- -->
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/notification_header_separating_margin"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:singleLine="true"
- android:textAlignment="viewStart"
- android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
- />
-
- <include layout="@layout/notification_top_line_views" />
-
- </NotificationTopLineView>
-
- <LinearLayout
- android:id="@+id/notification_main_column"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/notification_heading_margin_end"
- android:layout_marginStart="@dimen/notification_content_margin_start"
- android:orientation="vertical"
- >
-
- <include
- layout="@layout/notification_template_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_headerless_line_height"
- android:layout_marginTop="0dp"
- />
-
- <include
- layout="@layout/notification_template_progress"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_headerless_line_height"
- />
-
- </LinearLayout>
-
- <!--
- This invisible FrameLayout is here as a collapsible padding. Having a layout_weight=1 is
- what causes this view (and it's counterpart at the opposite end) to collapse before the
- actual content views do.
- This pair of 10dp collapsible paddings (plus the 16dp fixed margins) allow us to support
- headerless notifications of 1-3 lines (where each line is 20dp tall) where the 1-line
- variant is 56dp and the 2- and 3-line variants are both 76dp.
- -->
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_headerless_margin_extra"
- android:layout_weight="1"
- />
-
- </LinearLayout>
-
<ImageView
android:id="@+id/right_icon"
android:layout_width="@dimen/notification_right_icon_size"
@@ -181,4 +84,116 @@
</FrameLayout>
+ <LinearLayout
+ android:id="@+id/notification_headerless_view_column"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="@dimen/notification_headerless_margin_minimum"
+ android:layout_marginTop="@dimen/notification_headerless_margin_minimum"
+ android:orientation="vertical"
+ >
+
+ <!--
+ This invisible FrameLayout is here as a collapsible padding. Having a layout_weight=1 is
+ what causes this view (and it's counterpart at the opposite end) to collapse before the
+ actual content views do.
+ This pair of 10dp collapsible paddings (plus the 16dp fixed margins) allow us to support
+ headerless notifications of 1-3 lines (where each line is 20dp tall) where the 1-line
+ variant is 56dp and the 2- and 3-line variants are both 76dp.
+ -->
+ <FrameLayout
+ android:id="@+id/notification_headerless_margin_extra_top"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_headerless_margin_extra"
+ android:layout_weight="1"
+ />
+
+ <!--
+ Because RemoteViews doesn't allow us to remove specific views, this container allows us
+ to remove the NotificationTopLineView without removing other padding items.
+ -->
+ <FrameLayout
+ android:id="@+id/notification_top_line_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ >
+
+ <!-- extends ViewGroup -->
+ <NotificationTopLineView
+ android:id="@+id/notification_top_line"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/notification_headerless_line_height"
+ android:layout_marginEnd="@dimen/notification_heading_margin_end"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
+ android:clipChildren="false"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ >
+
+ <!--
+ NOTE: The notification_top_line_views layout contains the app_name_text.
+ In order to include the title view at the beginning, the Notification.Builder
+ has logic to hide that view whenever this title view is to be visible.
+ -->
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
+ />
+
+ <include layout="@layout/notification_top_line_views" />
+
+ </NotificationTopLineView>
+
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/notification_main_column"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/notification_heading_margin_end"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
+ android:orientation="vertical"
+ >
+
+ <include
+ layout="@layout/notification_template_text"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_headerless_line_height"
+ android:layout_marginTop="0dp"
+ />
+
+ <include
+ layout="@layout/notification_template_progress"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_headerless_line_height"
+ />
+
+ </LinearLayout>
+
+ <!--
+ This invisible FrameLayout is here as a collapsible padding. Having a layout_weight=1 is
+ what causes this view (and it's counterpart at the opposite end) to collapse before the
+ actual content views do.
+ This pair of 10dp collapsible paddings (plus the 16dp fixed margins) allow us to support
+ headerless notifications of 1-3 lines (where each line is 20dp tall) where the 1-line
+ variant is 56dp and the 2- and 3-line variants are both 76dp.
+ -->
+ <FrameLayout
+ android:id="@+id/notification_headerless_margin_extra_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_headerless_margin_extra"
+ android:layout_weight="1"
+ />
+
+ </LinearLayout>
+
</com.android.internal.widget.NotificationMaxHeightFrameLayout>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 5b19a48..fbcf2cb 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -301,12 +301,18 @@
<!-- The top padding for the notification expand button. -->
<dimen name="notification_expand_button_padding_top">1dp</dimen>
- <!-- minimum vertical margin for the headerless notification content -->
+ <!-- minimum vertical margin for the headerless notification content, when cap = 60dp -->
<dimen name="notification_headerless_margin_minimum">8dp</dimen>
- <!-- extra vertical margin for the headerless notification content -->
+ <!-- extra vertical margin for the headerless notification content, when cap = 60dp -->
<dimen name="notification_headerless_margin_extra">10dp</dimen>
+ <!-- minimum vertical margin for the headerless notification content, when cap = 48dp -->
+ <dimen name="notification_headerless_margin_constrained_minimum">14dp</dimen>
+
+ <!-- extra vertical margin for the headerless notification content, when cap = 48dp -->
+ <dimen name="notification_headerless_margin_constrained_extra">4dp</dimen>
+
<!-- The height of each of the 1 or 2 lines in the headerless notification template -->
<dimen name="notification_headerless_line_height">20sp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fa66451..beb0059 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2876,6 +2876,9 @@
<java-symbol type="id" name="alternate_expand_target" />
<java-symbol type="id" name="notification_header" />
<java-symbol type="id" name="notification_top_line" />
+ <java-symbol type="id" name="notification_top_line_container" />
+ <java-symbol type="id" name="notification_headerless_margin_extra_top" />
+ <java-symbol type="id" name="notification_headerless_margin_extra_bottom" />
<java-symbol type="id" name="time_divider" />
<java-symbol type="id" name="header_text_divider" />
<java-symbol type="id" name="header_text_secondary_divider" />
@@ -2897,6 +2900,8 @@
<java-symbol type="dimen" name="notification_header_icon_size" />
<java-symbol type="dimen" name="notification_header_app_name_margin_start" />
<java-symbol type="dimen" name="notification_header_separating_margin" />
+ <java-symbol type="dimen" name="notification_headerless_margin_constrained_minimum" />
+ <java-symbol type="dimen" name="notification_headerless_margin_constrained_extra" />
<java-symbol type="string" name="default_notification_channel_label" />
<java-symbol type="string" name="importance_from_user" />
<java-symbol type="string" name="importance_from_person" />
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index c1c1d65..e22f264 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -217,6 +217,7 @@
Settings.Global.DEBUG_APP,
Settings.Global.DEBUG_VIEW_ATTRIBUTES,
Settings.Global.DEBUG_VIEW_ATTRIBUTES_APPLICATION_PACKAGE,
+ Settings.Global.DECORATED_CUSTOM_VIEW_NOTIF_DECORATION,
Settings.Global.DEFAULT_DNS_SERVER,
Settings.Global.DEFAULT_INSTALL_LOCATION,
Settings.Global.DEFAULT_RESTRICT_BACKGROUND_DATA,
@@ -285,6 +286,7 @@
Settings.Global.WIFI_ON_WHEN_PROXY_DISCONNECTED,
Settings.Global.FSTRIM_MANDATORY_INTERVAL,
Settings.Global.FOREGROUND_SERVICE_STARTS_LOGGING_ENABLED,
+ Settings.Global.FULLY_CUSTOM_VIEW_NOTIF_DECORATION,
Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST,
Settings.Global.GLOBAL_HTTP_PROXY_HOST,
Settings.Global.GLOBAL_HTTP_PROXY_PAC,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 28ee935..97201f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -134,12 +134,14 @@
/** Shows or hides feedback indicator */
@Override
public void showFeedbackIcon(boolean show, Pair<Integer, Integer> resIds) {
- mFeedbackIcon.setVisibility(show ? View.VISIBLE : View.GONE);
- if (show) {
- if (mFeedbackIcon instanceof ImageButton) {
- ((ImageButton) mFeedbackIcon).setImageResource(resIds.first);
+ if (mFeedbackIcon != null) {
+ mFeedbackIcon.setVisibility(show ? View.VISIBLE : View.GONE);
+ if (show) {
+ if (mFeedbackIcon instanceof ImageButton) {
+ ((ImageButton) mFeedbackIcon).setImageResource(resIds.first);
+ }
+ mFeedbackIcon.setContentDescription(mView.getContext().getString(resIds.second));
}
- mFeedbackIcon.setContentDescription(mView.getContext().getString(resIds.second));
}
}
@@ -263,7 +265,9 @@
mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon);
mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_EXPANDER,
mExpandButton);
- mTransformationHelper.addViewTransformingToSimilar(mWorkProfileImage);
+ if (mWorkProfileImage != null) {
+ mTransformationHelper.addViewTransformingToSimilar(mWorkProfileImage);
+ }
if (mIsLowPriority && mHeaderText != null) {
mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
mHeaderText);