Merge changes from topic "decorate_custom_notifs"
* changes:
Improve RemoteViews to simplify notification view hierarchy.
Fully custom view notifications now receive minimal decoration when targeting S.
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index e56274a..5fbc948 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 = true, prefix = { "FLAG_" }, value = {FLAG_SHOW_LIGHTS, FLAG_ONGOING_EVENT,
FLAG_INSISTENT, FLAG_ONLY_ALERT_ONCE,
@@ -5099,7 +5105,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);
@@ -5427,7 +5433,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,
@@ -5520,6 +5526,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.
*
@@ -5532,11 +5603,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()
@@ -5556,18 +5629,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;
@@ -5661,11 +5746,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;
@@ -5677,8 +5765,7 @@
.viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP)
.fillTextsFrom(this)
.setMaxRemoteInputHistory(1);
- return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(),
- p,
+ return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), p,
null /* result */);
}
@@ -6596,11 +6683,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;
}
@@ -6608,6 +6691,56 @@
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.removeFromParent(R.id.notification_top_line);
+ }
+ 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.
@@ -7813,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)
@@ -8628,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 */);
@@ -8677,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 */);
@@ -8771,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;
}
@@ -8801,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;
}
}
@@ -11159,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;
@@ -11173,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;
@@ -11186,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) {
@@ -11201,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;
}
@@ -11263,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;
+ }
}
/**
@@ -11272,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
@@ -11292,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/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 220a31c..8dafc5d 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -75,6 +75,8 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewManager;
+import android.view.ViewParent;
import android.view.ViewStub;
import android.widget.AdapterView.OnItemClickListener;
@@ -177,6 +179,7 @@
private static final int OVERRIDE_TEXT_COLORS_TAG = 20;
private static final int SET_RIPPLE_DRAWABLE_COLOR_TAG = 21;
private static final int SET_INT_TAG_TAG = 22;
+ private static final int REMOVE_FROM_PARENT_ACTION_TAG = 23;
/** @hide **/
@IntDef(prefix = "MARGIN_", value = {
@@ -1831,6 +1834,75 @@
}
/**
+ * Action to remove a view from its parent.
+ */
+ private class RemoveFromParentAction extends Action {
+
+ RemoveFromParentAction(@IdRes int viewId) {
+ this.viewId = viewId;
+ }
+
+ RemoveFromParentAction(Parcel parcel) {
+ viewId = parcel.readInt();
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(viewId);
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+ final View target = root.findViewById(viewId);
+
+ if (target == null || target == root) {
+ return;
+ }
+
+ ViewParent parent = target.getParent();
+ if (parent instanceof ViewManager) {
+ ((ViewManager) parent).removeView(target);
+ }
+ }
+
+ @Override
+ public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+ // In the async implementation, update the view tree so that subsequent calls to
+ // findViewById return the correct view.
+ root.createTree();
+ ViewTree target = root.findViewTreeById(viewId);
+
+ if (target == null || target == root) {
+ return ACTION_NOOP;
+ }
+
+ ViewTree parent = root.findViewTreeParentOf(target);
+ if (parent == null || !(parent.mRoot instanceof ViewManager)) {
+ return ACTION_NOOP;
+ }
+ final ViewManager parentVg = (ViewManager) parent.mRoot;
+
+ parent.mChildren.remove(target);
+ return new RuntimeAction() {
+ @Override
+ public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
+ throws ActionException {
+ parentVg.removeView(target.mRoot);
+ }
+ };
+ }
+
+ @Override
+ public int getActionTag() {
+ return REMOVE_FROM_PARENT_ACTION_TAG;
+ }
+
+ @Override
+ public int mergeBehavior() {
+ return MERGE_APPEND;
+ }
+ }
+
+ /**
* Helper action to set compound drawables on a TextView. Supports relative
* (s/t/e/b) or cardinal (l/t/r/b) arrangement.
*/
@@ -2537,6 +2609,8 @@
return new SetRippleDrawableColor(parcel);
case SET_INT_TAG_TAG:
return new SetIntTagAction(parcel);
+ case REMOVE_FROM_PARENT_ACTION_TAG:
+ return new RemoveFromParentAction(parcel);
default:
throw new ActionException("Tag " + tag + " not found");
}
@@ -2675,6 +2749,18 @@
}
/**
+ * Removes the {@link View} specified by the {@code viewId} from its parent {@link ViewManager}.
+ * This will do nothing if the viewId specifies the root view of this RemoteViews.
+ *
+ * @param viewId The id of the {@link View} to remove from its parent.
+ *
+ * @hide
+ */
+ public void removeFromParent(@IdRes int viewId) {
+ addAction(new RemoveFromParentAction(viewId));
+ }
+
+ /**
* Equivalent to calling {@link AdapterViewAnimator#showNext()}
*
* @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()}
@@ -4025,6 +4111,22 @@
return null;
}
+ public ViewTree findViewTreeParentOf(ViewTree child) {
+ if (mChildren == null) {
+ return null;
+ }
+ for (ViewTree tree : mChildren) {
+ if (tree == child) {
+ return this;
+ }
+ ViewTree result = tree.findViewTreeParentOf(child);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
public void replaceView(View v) {
mRoot = v;
mChildren = null;
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index 0006384..41be36b 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -45,6 +45,45 @@
android:padding="@dimen/notification_icon_circle_padding"
/>
+ <ImageView
+ android:id="@+id/right_icon"
+ android:layout_width="@dimen/notification_right_icon_size"
+ android:layout_height="@dimen/notification_right_icon_size"
+ android:layout_gravity="center_vertical|end"
+ android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
+ android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
+ android:layout_marginEnd="@dimen/notification_header_expand_icon_size"
+ android:background="@drawable/notification_large_icon_outline"
+ android:importantForAccessibility="no"
+ android:scaleType="centerCrop"
+ />
+
+ <FrameLayout
+ android:id="@+id/alternate_expand_target"
+ android:layout_width="@dimen/notification_content_margin_start"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:importantForAccessibility="no"
+ />
+
+ <FrameLayout
+ android:id="@+id/expand_button_touch_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="end">
+
+ <com.android.internal.widget.NotificationExpandButton
+ android:id="@+id/expand_button"
+ android:layout_width="@dimen/notification_header_expand_icon_size"
+ android:layout_height="@dimen/notification_header_expand_icon_size"
+ android:layout_gravity="center_vertical|end"
+ android:contentDescription="@string/expand_button_content_description_collapsed"
+ android:paddingTop="@dimen/notification_expand_button_padding_top"
+ android:scaleType="center"
+ />
+
+ </FrameLayout>
+
<LinearLayout
android:id="@+id/notification_headerless_view_column"
android:layout_width="match_parent"
@@ -64,6 +103,7 @@
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"
@@ -135,6 +175,7 @@
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"
@@ -142,43 +183,4 @@
</LinearLayout>
- <ImageView
- android:id="@+id/right_icon"
- android:layout_width="@dimen/notification_right_icon_size"
- android:layout_height="@dimen/notification_right_icon_size"
- android:layout_gravity="center_vertical|end"
- android:layout_marginTop="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginBottom="@dimen/notification_right_icon_headerless_margin"
- android:layout_marginEnd="@dimen/notification_header_expand_icon_size"
- android:background="@drawable/notification_large_icon_outline"
- android:importantForAccessibility="no"
- android:scaleType="centerCrop"
- />
-
- <FrameLayout
- android:id="@+id/alternate_expand_target"
- android:layout_width="@dimen/notification_content_margin_start"
- android:layout_height="match_parent"
- android:layout_gravity="start"
- android:importantForAccessibility="no"
- />
-
- <FrameLayout
- android:id="@+id/expand_button_touch_container"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="end">
-
- <com.android.internal.widget.NotificationExpandButton
- android:id="@+id/expand_button"
- android:layout_width="@dimen/notification_header_expand_icon_size"
- android:layout_height="@dimen/notification_header_expand_icon_size"
- android:layout_gravity="center_vertical|end"
- android:contentDescription="@string/expand_button_content_description_collapsed"
- android:paddingTop="@dimen/notification_expand_button_padding_top"
- android:scaleType="center"
- />
-
- </FrameLayout>
-
</com.android.internal.widget.NotificationMaxHeightFrameLayout>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index c326a40..3ae2131 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -305,12 +305,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 2df9684..31b4edd 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2877,6 +2877,8 @@
<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_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" />
@@ -2898,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);