diff options
| author | 2021-01-11 21:48:22 +0000 | |
|---|---|---|
| committer | 2021-01-11 21:48:22 +0000 | |
| commit | b1e9252d657bcc70c824eef8af86747efd9c9fa2 (patch) | |
| tree | ef5a23e6b6794faa96d8a336859997933d514b87 | |
| parent | 19d37240a51eafe10f7f2dc4c8f638588df52d19 (diff) | |
| parent | cb189d4630b9425da6c04f47fbfb42a875b4d782 (diff) | |
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.
8 files changed, 507 insertions, 108 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index e56274aa4fd2..5fbc94822d4e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -102,6 +102,7 @@ import java.lang.annotation.RetentionPolicy; 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 class Notification implements Parcelable */ 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 class Notification implements Parcelable */ 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 class Notification implements Parcelable 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 class Notification implements Parcelable */ 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 @@ public class Notification implements Parcelable .viewType(StandardTemplateParams.VIEW_TYPE_HEADS_UP) .fillTextsFrom(this) .setMaxRemoteInputHistory(1); - return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), - p, + return applyStandardTemplateWithActions(getHeadsUpBaseLayoutResource(), p, null /* result */); } @@ -6596,11 +6683,7 @@ public class Notification implements Parcelable */ @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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable } 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable } 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable * @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 @@ public class Notification implements Parcelable 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 69ca28a38fb6..9db7ca0cad6b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14630,6 +14630,34 @@ public final class Settings { 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 220a31c12f4e..8dafc5db6178 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -75,6 +75,8 @@ import android.view.RemotableViewMethod; 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 @@ public class RemoteViews implements Parcelable, Filter { 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 @@ public class RemoteViews implements Parcelable, Filter { } /** + * 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 @@ public class RemoteViews implements Parcelable, Filter { 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 @@ public class RemoteViews implements Parcelable, Filter { } /** + * 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 @@ public class RemoteViews implements Parcelable, Filter { 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 000638475a10..41be36bc2b04 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 c326a40f7324..3ae21311ea49 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 2df9684e0e6f..31b4edd4e6e8 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 c1c1d65fb9d7..e22f2649d540 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -217,6 +217,7 @@ public class SettingsBackupTest { 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 @@ public class SettingsBackupTest { 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 28ee9358737e..97201f5c9a34 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 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { /** 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 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { 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); |