diff --git a/api/current.txt b/api/current.txt index 1c3ee82dbba0d..2d31c355fa600 100755 --- a/api/current.txt +++ b/api/current.txt @@ -5217,6 +5217,7 @@ package android.app { ctor public Notification(android.os.Parcel); method public android.app.Notification clone(); method public int describeContents(); + method public android.app.PendingIntent getAppOverlayIntent(); method public int getBadgeIconType(); method public java.lang.String getChannelId(); method public java.lang.String getGroup(); @@ -5446,6 +5447,7 @@ package android.app { method public android.app.Notification.Style getStyle(); method public static android.app.Notification.Builder recoverBuilder(android.content.Context, android.app.Notification); method public android.app.Notification.Builder setActions(android.app.Notification.Action...); + method public android.app.Notification.Builder setAppOverlayIntent(android.app.PendingIntent); method public android.app.Notification.Builder setAutoCancel(boolean); method public android.app.Notification.Builder setBadgeIconType(int); method public android.app.Notification.Builder setCategory(java.lang.String); @@ -5663,6 +5665,7 @@ package android.app { public final class NotificationChannel implements android.os.Parcelable { ctor public NotificationChannel(java.lang.String, java.lang.CharSequence, int); method public boolean canBypassDnd(); + method public boolean canOverlayApps(); method public boolean canShowBadge(); method public int describeContents(); method public void enableLights(boolean); @@ -5678,6 +5681,7 @@ package android.app { method public android.net.Uri getSound(); method public long[] getVibrationPattern(); method public boolean hasUserSetImportance(); + method public void setAllowAppOverlay(boolean); method public void setBypassDnd(boolean); method public void setDescription(java.lang.String); method public void setGroup(java.lang.String); @@ -5697,6 +5701,7 @@ package android.app { public final class NotificationChannelGroup implements android.os.Parcelable { ctor public NotificationChannelGroup(java.lang.String, java.lang.CharSequence); + method public boolean canOverlayApps(); method public android.app.NotificationChannelGroup clone(); method public int describeContents(); method public java.util.List getChannels(); @@ -5704,6 +5709,7 @@ package android.app { method public java.lang.String getId(); method public java.lang.CharSequence getName(); method public boolean isBlocked(); + method public void setAllowAppOverlay(boolean); method public void setDescription(java.lang.String); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; diff --git a/api/system-current.txt b/api/system-current.txt index 7e510827b6ff0..27554e31f1d4a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -987,8 +987,8 @@ package android.content { field public static final java.lang.String ACTION_PRE_BOOT_COMPLETED = "android.intent.action.PRE_BOOT_COMPLETED"; field public static final java.lang.String ACTION_QUERY_PACKAGE_RESTART = "android.intent.action.QUERY_PACKAGE_RESTART"; field public static final java.lang.String ACTION_RESOLVE_INSTANT_APP_PACKAGE = "android.intent.action.RESOLVE_INSTANT_APP_PACKAGE"; - field public static final java.lang.String ACTION_REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"; field public static final java.lang.String ACTION_REVIEW_PERMISSIONS = "android.intent.action.REVIEW_PERMISSIONS"; + field public static final java.lang.String ACTION_REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"; field public static final java.lang.String ACTION_SHOW_SUSPENDED_APP_DETAILS = "android.intent.action.SHOW_SUSPENDED_APP_DETAILS"; field public static final deprecated java.lang.String ACTION_SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED"; field public static final java.lang.String ACTION_SPLIT_CONFIGURATION_CHANGED = "android.intent.action.SPLIT_CONFIGURATION_CHANGED"; diff --git a/api/test-current.txt b/api/test-current.txt index 463c9d3589851..731f61144a656 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -140,7 +140,10 @@ package android.app { } public final class NotificationChannelGroup implements android.os.Parcelable { + method public int getUserLockedFields(); + method public void lockFields(int); method public void setBlocked(boolean); + field public static final int USER_LOCKED_ALLOW_APP_OVERLAY = 2; // 0x2 } public class NotificationManager { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4f41da6e52fbc..6d464fb190c17 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1275,6 +1275,8 @@ public class Notification implements Parcelable private String mShortcutId; private CharSequence mSettingsText; + private PendingIntent mAppOverlayIntent; + /** @hide */ @IntDef(prefix = { "GROUP_ALERT_" }, value = { GROUP_ALERT_ALL, GROUP_ALERT_CHILDREN, GROUP_ALERT_SUMMARY @@ -2225,6 +2227,9 @@ public class Notification implements Parcelable } mGroupAlertBehavior = parcel.readInt(); + if (parcel.readInt() != 0) { + mAppOverlayIntent = PendingIntent.CREATOR.createFromParcel(parcel); + } } @Override @@ -2339,6 +2344,7 @@ public class Notification implements Parcelable that.mBadgeIcon = this.mBadgeIcon; that.mSettingsText = this.mSettingsText; that.mGroupAlertBehavior = this.mGroupAlertBehavior; + that.mAppOverlayIntent = this.mAppOverlayIntent; if (!heavy) { that.lightenPayload(); // will clean out extras @@ -2660,6 +2666,13 @@ public class Notification implements Parcelable parcel.writeInt(mGroupAlertBehavior); + if (mAppOverlayIntent != null) { + parcel.writeInt(1); + mAppOverlayIntent.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + // mUsesStandardHeader is not written because it should be recomputed in listeners } @@ -3072,6 +3085,14 @@ public class Notification implements Parcelable return mGroupAlertBehavior; } + /** + * Returns the intent that will be used to display app content in a floating window over the + * existing foreground activity. + */ + public PendingIntent getAppOverlayIntent() { + return mAppOverlayIntent; + } + /** * The small icon representing this notification in the status bar and content view. * @@ -3406,6 +3427,23 @@ public class Notification implements Parcelable return this; } + /** + * Sets the intent that will be used to display app content in a floating window + * over the existing foreground activity. + * + *

This intent will be ignored unless this notification is posted to a channel that + * allows {@link NotificationChannel#canOverlayApps() app overlays}.

+ * + *

Notifications with a valid and allowed app overlay intent will be displayed as + * floating windows outside of the notification shade on unlocked devices. When a user + * interacts with one of these windows, this app overlay intent will be invoked and + * displayed.

+ */ + public Builder setAppOverlayIntent(PendingIntent intent) { + mN.mAppOverlayIntent = intent; + return this; + } + /** @removed */ @Deprecated public Builder setChannel(String channelId) { diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 9f93e1765da3c..41ceaafa56a93 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -15,6 +15,8 @@ */ package android.app; +import static android.app.NotificationManager.IMPORTANCE_HIGH; + import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; @@ -41,6 +43,7 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; +import java.util.Objects; /** * A representation of settings that apply to a collection of similarly themed notifications. @@ -81,6 +84,7 @@ public final class NotificationChannel implements Parcelable { private static final String ATT_FG_SERVICE_SHOWN = "fgservice"; private static final String ATT_GROUP = "group"; private static final String ATT_BLOCKABLE_SYSTEM = "blockable_system"; + private static final String ATT_ALLOW_APP_OVERLAY = "app_overlay"; private static final String DELIMITER = ","; /** @@ -113,6 +117,11 @@ public final class NotificationChannel implements Parcelable { */ public static final int USER_LOCKED_SHOW_BADGE = 0x00000080; + /** + * @hide + */ + public static final int USER_LOCKED_ALLOW_APP_OVERLAY = 0x00000100; + /** * @hide */ @@ -124,6 +133,7 @@ public final class NotificationChannel implements Parcelable { USER_LOCKED_VIBRATION, USER_LOCKED_SOUND, USER_LOCKED_SHOW_BADGE, + USER_LOCKED_ALLOW_APP_OVERLAY }; private static final int DEFAULT_LIGHT_COLOR = 0; @@ -133,6 +143,7 @@ public final class NotificationChannel implements Parcelable { NotificationManager.IMPORTANCE_UNSPECIFIED; private static final boolean DEFAULT_DELETED = false; private static final boolean DEFAULT_SHOW_BADGE = true; + private static final boolean DEFAULT_ALLOW_APP_OVERLAY = true; @UnsupportedAppUsage private final String mId; @@ -156,6 +167,7 @@ public final class NotificationChannel implements Parcelable { private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT; // If this is a blockable system notification channel. private boolean mBlockableSystem = false; + private boolean mAllowAppOverlay = DEFAULT_ALLOW_APP_OVERLAY; /** * Creates a notification channel. @@ -217,6 +229,7 @@ public final class NotificationChannel implements Parcelable { mAudioAttributes = in.readInt() > 0 ? AudioAttributes.CREATOR.createFromParcel(in) : null; mLightColor = in.readInt(); mBlockableSystem = in.readBoolean(); + mAllowAppOverlay = in.readBoolean(); } @Override @@ -269,6 +282,7 @@ public final class NotificationChannel implements Parcelable { } dest.writeInt(mLightColor); dest.writeBoolean(mBlockableSystem); + dest.writeBoolean(mAllowAppOverlay); } /** @@ -460,6 +474,22 @@ public final class NotificationChannel implements Parcelable { this.mLockscreenVisibility = lockscreenVisibility; } + /** + * Sets whether notifications posted to this channel can appear outside of the notification + * shade, floating over other apps' content. + * + *

This value will be ignored for channels that aren't allowed to pop on screen (that is, + * channels whose {@link #getImportance() importance} is < + * {@link NotificationManager#IMPORTANCE_HIGH}.

+ * + *

Only modifiable before the channel is submitted to + * * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.

+ * @see Notification#getAppOverlayIntent() + */ + public void setAllowAppOverlay(boolean allowAppOverlay) { + mAllowAppOverlay = allowAppOverlay; + } + /** * Returns the id of this channel. */ @@ -572,6 +602,22 @@ public final class NotificationChannel implements Parcelable { return mGroup; } + /** + * Returns whether notifications posted to this channel can display outside of the notification + * shade, in a floating window on top of other apps. + */ + public boolean canOverlayApps() { + return isAppOverlayAllowed() && getImportance() >= IMPORTANCE_HIGH; + } + + /** + * Like {@link #canOverlayApps()}, but only checks the permission, not the importance. + * @hide + */ + public boolean isAppOverlayAllowed() { + return mAllowAppOverlay; + } + /** * @hide */ @@ -605,6 +651,7 @@ public final class NotificationChannel implements Parcelable { /** * Returns whether the user has chosen the importance of this channel, either to affirm the * initial selection from the app, or changed it to be higher or lower. + * @see #getImportance() */ public boolean hasUserSetImportance() { return (mUserLockedFields & USER_LOCKED_IMPORTANCE) != 0; @@ -652,6 +699,7 @@ public final class NotificationChannel implements Parcelable { lockFields(safeInt(parser, ATT_USER_LOCKED, 0)); setFgServiceShown(safeBool(parser, ATT_FG_SERVICE_SHOWN, false)); setBlockableSystem(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false)); + setAllowAppOverlay(safeBool(parser, ATT_ALLOW_APP_OVERLAY, DEFAULT_ALLOW_APP_OVERLAY)); } @Nullable @@ -770,6 +818,9 @@ public final class NotificationChannel implements Parcelable { if (isBlockableSystem()) { out.attribute(null, ATT_BLOCKABLE_SYSTEM, Boolean.toString(isBlockableSystem())); } + if (canOverlayApps() != DEFAULT_ALLOW_APP_OVERLAY) { + out.attribute(null, ATT_ALLOW_APP_OVERLAY, Boolean.toString(canOverlayApps())); + } out.endTag(null, TAG_CHANNEL); } @@ -812,6 +863,7 @@ public final class NotificationChannel implements Parcelable { record.put(ATT_DELETED, Boolean.toString(isDeleted())); record.put(ATT_GROUP, getGroup()); record.put(ATT_BLOCKABLE_SYSTEM, isBlockableSystem()); + record.put(ATT_ALLOW_APP_OVERLAY, canOverlayApps()); return record; } @@ -899,58 +951,36 @@ public final class NotificationChannel implements Parcelable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - NotificationChannel that = (NotificationChannel) o; - - if (getImportance() != that.getImportance()) return false; - if (mBypassDnd != that.mBypassDnd) return false; - if (getLockscreenVisibility() != that.getLockscreenVisibility()) return false; - if (mLights != that.mLights) return false; - if (getLightColor() != that.getLightColor()) return false; - if (getUserLockedFields() != that.getUserLockedFields()) return false; - if (mVibrationEnabled != that.mVibrationEnabled) return false; - if (mShowBadge != that.mShowBadge) return false; - if (isDeleted() != that.isDeleted()) return false; - if (isBlockableSystem() != that.isBlockableSystem()) return false; - if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; - if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { - return false; - } - if (getDescription() != null ? !getDescription().equals(that.getDescription()) - : that.getDescription() != null) { - return false; - } - if (getSound() != null ? !getSound().equals(that.getSound()) : that.getSound() != null) { - return false; - } - if (!Arrays.equals(mVibration, that.mVibration)) return false; - if (getGroup() != null ? !getGroup().equals(that.getGroup()) : that.getGroup() != null) { - return false; - } - return getAudioAttributes() != null ? getAudioAttributes().equals(that.getAudioAttributes()) - : that.getAudioAttributes() == null; - + return getImportance() == that.getImportance() && + mBypassDnd == that.mBypassDnd && + getLockscreenVisibility() == that.getLockscreenVisibility() && + mLights == that.mLights && + getLightColor() == that.getLightColor() && + getUserLockedFields() == that.getUserLockedFields() && + isFgServiceShown() == that.isFgServiceShown() && + mVibrationEnabled == that.mVibrationEnabled && + mShowBadge == that.mShowBadge && + isDeleted() == that.isDeleted() && + isBlockableSystem() == that.isBlockableSystem() && + mAllowAppOverlay == that.mAllowAppOverlay && + Objects.equals(getId(), that.getId()) && + Objects.equals(getName(), that.getName()) && + Objects.equals(mDesc, that.mDesc) && + Objects.equals(getSound(), that.getSound()) && + Arrays.equals(mVibration, that.mVibration) && + Objects.equals(getGroup(), that.getGroup()) && + Objects.equals(getAudioAttributes(), that.getAudioAttributes()); } @Override public int hashCode() { - int result = getId() != null ? getId().hashCode() : 0; - result = 31 * result + (getName() != null ? getName().hashCode() : 0); - result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0); - result = 31 * result + getImportance(); - result = 31 * result + (mBypassDnd ? 1 : 0); - result = 31 * result + getLockscreenVisibility(); - result = 31 * result + (getSound() != null ? getSound().hashCode() : 0); - result = 31 * result + (mLights ? 1 : 0); - result = 31 * result + getLightColor(); + int result = Objects.hash(getId(), getName(), mDesc, getImportance(), mBypassDnd, + getLockscreenVisibility(), getSound(), mLights, getLightColor(), + getUserLockedFields(), + isFgServiceShown(), mVibrationEnabled, mShowBadge, isDeleted(), getGroup(), + getAudioAttributes(), isBlockableSystem(), mAllowAppOverlay); result = 31 * result + Arrays.hashCode(mVibration); - result = 31 * result + getUserLockedFields(); - result = 31 * result + (mVibrationEnabled ? 1 : 0); - result = 31 * result + (mShowBadge ? 1 : 0); - result = 31 * result + (isDeleted() ? 1 : 0); - result = 31 * result + (getGroup() != null ? getGroup().hashCode() : 0); - result = 31 * result + (getAudioAttributes() != null ? getAudioAttributes().hashCode() : 0); - result = 31 * result + (isBlockableSystem() ? 1 : 0); return result; } @@ -976,6 +1006,7 @@ public final class NotificationChannel implements Parcelable { + ", mGroup='" + mGroup + '\'' + ", mAudioAttributes=" + mAudioAttributes + ", mBlockableSystem=" + mBlockableSystem + + ", mAllowAppOverlay=" + mAllowAppOverlay + '}'; pw.println(prefix + output); } @@ -1001,6 +1032,7 @@ public final class NotificationChannel implements Parcelable { + ", mGroup='" + mGroup + '\'' + ", mAudioAttributes=" + mAudioAttributes + ", mBlockableSystem=" + mBlockableSystem + + ", mAllowAppOverlay=" + mAllowAppOverlay + '}'; } @@ -1034,6 +1066,7 @@ public final class NotificationChannel implements Parcelable { mAudioAttributes.writeToProto(proto, NotificationChannelProto.AUDIO_ATTRIBUTES); } proto.write(NotificationChannelProto.IS_BLOCKABLE_SYSTEM, mBlockableSystem); + proto.write(NotificationChannelProto.ALLOW_APP_OVERLAY, mAllowAppOverlay); proto.end(token); } diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java index 17c5cba3ed2cd..2322a42c93d57 100644 --- a/core/java/android/app/NotificationChannelGroup.java +++ b/core/java/android/app/NotificationChannelGroup.java @@ -32,6 +32,7 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A grouping of related notification channels. e.g., channels that all belong to a single account. @@ -49,13 +50,33 @@ public final class NotificationChannelGroup implements Parcelable { private static final String ATT_DESC = "desc"; private static final String ATT_ID = "id"; private static final String ATT_BLOCKED = "blocked"; + private static final String ATT_ALLOW_APP_OVERLAY = "app_overlay"; + private static final String ATT_USER_LOCKED = "locked"; + private static final boolean DEFAULT_ALLOW_APP_OVERLAY = true; + + /** + * @hide + */ + public static final int USER_LOCKED_BLOCKED_STATE = 0x00000001; + /** + * @hide + */ + @TestApi + public static final int USER_LOCKED_ALLOW_APP_OVERLAY = 0x00000002; + + /** + * @see #getId() + */ @UnsupportedAppUsage private final String mId; private CharSequence mName; private String mDescription; private boolean mBlocked; private List mChannels = new ArrayList<>(); + private boolean mAllowAppOverlay = DEFAULT_ALLOW_APP_OVERLAY; + // Bitwise representation of fields that have been changed by the user + private int mUserLockedFields; /** * Creates a notification channel group. @@ -89,6 +110,8 @@ public final class NotificationChannelGroup implements Parcelable { } in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader()); mBlocked = in.readBoolean(); + mAllowAppOverlay = in.readBoolean(); + mUserLockedFields = in.readInt(); } private String getTrimmedString(String input) { @@ -115,6 +138,8 @@ public final class NotificationChannelGroup implements Parcelable { } dest.writeParcelableList(mChannels, flags); dest.writeBoolean(mBlocked); + dest.writeBoolean(mAllowAppOverlay); + dest.writeInt(mUserLockedFields); } /** @@ -155,6 +180,15 @@ public final class NotificationChannelGroup implements Parcelable { return mBlocked; } + /** + * Returns whether notifications posted to this channel group can display outside of the + * notification shade, in a floating window on top of other apps. These may additionally be + * blocked at the notification channel level, see {@link NotificationChannel#canOverlayApps()}. + */ + public boolean canOverlayApps() { + return mAllowAppOverlay; + } + /** * Sets the user visible description of this group. * @@ -165,6 +199,21 @@ public final class NotificationChannelGroup implements Parcelable { mDescription = getTrimmedString(description); } + /** + * Sets whether notifications posted to this channel group can appear outside of the + * notification shade, floating over other apps' content. + * + *

This value will be ignored for notifications that are posted to channels that do not + * allow app overlays ({@link NotificationChannel#canOverlayApps()}. + * + *

Only modifiable before the channel is submitted to + * {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.

+ * @see Notification#getAppOverlayIntent() + */ + public void setAllowAppOverlay(boolean allowAppOverlay) { + mAllowAppOverlay = allowAppOverlay; + } + /** * @hide */ @@ -187,6 +236,29 @@ public final class NotificationChannelGroup implements Parcelable { mChannels = channels; } + /** + * @hide + */ + @TestApi + public void lockFields(int field) { + mUserLockedFields |= field; + } + + /** + * @hide + */ + public void unlockFields(int field) { + mUserLockedFields &= ~field; + } + + /** + * @hide + */ + @TestApi + public int getUserLockedFields() { + return mUserLockedFields; + } + /** * @hide */ @@ -194,6 +266,7 @@ public final class NotificationChannelGroup implements Parcelable { // Name, id, and importance are set in the constructor. setDescription(parser.getAttributeValue(null, ATT_DESC)); setBlocked(safeBool(parser, ATT_BLOCKED, false)); + setAllowAppOverlay(safeBool(parser, ATT_ALLOW_APP_OVERLAY, DEFAULT_ALLOW_APP_OVERLAY)); } private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { @@ -216,6 +289,10 @@ public final class NotificationChannelGroup implements Parcelable { out.attribute(null, ATT_DESC, getDescription().toString()); } out.attribute(null, ATT_BLOCKED, Boolean.toString(isBlocked())); + if (canOverlayApps() != DEFAULT_ALLOW_APP_OVERLAY) { + out.attribute(null, ATT_ALLOW_APP_OVERLAY, Boolean.toString(canOverlayApps())); + } + out.attribute(null, ATT_USER_LOCKED, Integer.toString(mUserLockedFields)); out.endTag(null, TAG_GROUP); } @@ -230,6 +307,8 @@ public final class NotificationChannelGroup implements Parcelable { record.put(ATT_NAME, getName()); record.put(ATT_DESC, getDescription()); record.put(ATT_BLOCKED, isBlocked()); + record.put(ATT_ALLOW_APP_OVERLAY, canOverlayApps()); + record.put(ATT_USER_LOCKED, mUserLockedFields); return record; } @@ -255,30 +334,20 @@ public final class NotificationChannelGroup implements Parcelable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - NotificationChannelGroup that = (NotificationChannelGroup) o; - - if (isBlocked() != that.isBlocked()) return false; - if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false; - if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { - return false; - } - if (getDescription() != null ? !getDescription().equals(that.getDescription()) - : that.getDescription() != null) { - return false; - } - return getChannels() != null ? getChannels().equals(that.getChannels()) - : that.getChannels() == null; + return isBlocked() == that.isBlocked() && + mAllowAppOverlay == that.mAllowAppOverlay && + mUserLockedFields == that.mUserLockedFields && + Objects.equals(getId(), that.getId()) && + Objects.equals(getName(), that.getName()) && + Objects.equals(getDescription(), that.getDescription()) && + Objects.equals(getChannels(), that.getChannels()); } @Override public int hashCode() { - int result = getId() != null ? getId().hashCode() : 0; - result = 31 * result + (getName() != null ? getName().hashCode() : 0); - result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0); - result = 31 * result + (isBlocked() ? 1 : 0); - result = 31 * result + (getChannels() != null ? getChannels().hashCode() : 0); - return result; + return Objects.hash(getId(), getName(), getDescription(), isBlocked(), getChannels(), + mAllowAppOverlay, mUserLockedFields); } @Override @@ -287,6 +356,8 @@ public final class NotificationChannelGroup implements Parcelable { cloned.setDescription(getDescription()); cloned.setBlocked(isBlocked()); cloned.setChannels(getChannels()); + cloned.setAllowAppOverlay(canOverlayApps()); + cloned.lockFields(mUserLockedFields); return cloned; } @@ -298,6 +369,8 @@ public final class NotificationChannelGroup implements Parcelable { + ", mDescription=" + (!TextUtils.isEmpty(mDescription) ? "hasDescription " : "") + ", mBlocked=" + mBlocked + ", mChannels=" + mChannels + + ", mAllowAppOverlay=" + mAllowAppOverlay + + ", mUserLockedFields=" + mUserLockedFields + '}'; } @@ -312,7 +385,7 @@ public final class NotificationChannelGroup implements Parcelable { for (NotificationChannel channel : mChannels) { channel.writeToProto(proto, NotificationChannelGroupProto.CHANNELS); } - + proto.write(NotificationChannelGroupProto.ALLOW_APP_OVERLAY, mAllowAppOverlay); proto.end(token); } } diff --git a/core/proto/android/app/notification_channel.proto b/core/proto/android/app/notification_channel.proto index 75cc18bf7a07d..435d32f59a356 100644 --- a/core/proto/android/app/notification_channel.proto +++ b/core/proto/android/app/notification_channel.proto @@ -57,4 +57,7 @@ message NotificationChannelProto { // If this is a blockable system notification channel. optional bool is_blockable_system = 17; optional bool fg_service_shown = 18; + // Default is true. + // Allows the notifications to appear outside of the shade in floating windows + optional bool allow_app_overlay = 19; } diff --git a/core/proto/android/app/notification_channel_group.proto b/core/proto/android/app/notification_channel_group.proto index 4fb27b0d6e4a6..6d6ceb2f7cbed 100644 --- a/core/proto/android/app/notification_channel_group.proto +++ b/core/proto/android/app/notification_channel_group.proto @@ -36,4 +36,7 @@ message NotificationChannelGroupProto { optional string description = 3; optional bool is_blocked = 4; repeated NotificationChannelProto channels = 5; + // Default is true. + // Allows the notifications to appear outside of the shade in floating windows + optional bool allow_app_overlay = 6; } diff --git a/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java b/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java index 29ee920d4dde4..ca3daad8d1090 100644 --- a/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java +++ b/packages/ExtServices/src/android/ext/services/notification/ChannelImpressions.java @@ -37,6 +37,8 @@ public final class ChannelImpressions implements Parcelable { static final String ATT_DISMISSALS = "dismisses"; static final String ATT_VIEWS = "views"; static final String ATT_STREAK = "streak"; + static final String ATT_SENT = "sent"; + static final String ATT_INTERRUPTIVE = "interruptive"; private int mDismissals = 0; private int mViews = 0; diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 3a0ab77c13136..8fce5e3acdc22 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -515,8 +515,20 @@ public class PreferencesHelper implements RankingConfig { if (oldGroup != null) { group.setChannels(oldGroup.getChannels()); + // apps can't update the blocked status or app overlay permission if (fromTargetApp) { group.setBlocked(oldGroup.isBlocked()); + group.setAllowAppOverlay(oldGroup.canOverlayApps()); + group.unlockFields(group.getUserLockedFields()); + group.lockFields(oldGroup.getUserLockedFields()); + } else { + // but the system can + if (group.isBlocked() != oldGroup.isBlocked()) { + group.lockFields(NotificationChannelGroup.USER_LOCKED_BLOCKED_STATE); + } + if (group.canOverlayApps() != oldGroup.canOverlayApps()) { + group.lockFields(NotificationChannelGroup.USER_LOCKED_ALLOW_APP_OVERLAY); + } } } r.groups.put(group.getId(), group); @@ -1071,6 +1083,9 @@ public class PreferencesHelper implements RankingConfig { if (original.canShowBadge() != update.canShowBadge()) { update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); } + if (original.isAppOverlayAllowed() != update.isAppOverlayAllowed()) { + update.lockFields(NotificationChannel.USER_LOCKED_ALLOW_APP_OVERLAY); + } } public void dump(PrintWriter pw, String prefix, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 79998a5bf7541..3fe381b0abe20 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -280,7 +280,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); assertTrue(mHelper.getIsAppImportanceLocked(PKG_N_MR1, UID_N_MR1)); - assertEquals(channel1, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); + assertEquals(channel1, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); compareChannels(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); @@ -348,7 +349,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertEquals(IMPORTANCE_NONE, mHelper.getImportance(PKG_O, UID_O)); assertTrue(mHelper.canShowBadge(PKG_N_MR1, UID_N_MR1)); - assertEquals(channel1, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); + assertEquals(channel1, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); compareChannels(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); compareChannels(channel3, @@ -487,7 +489,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); NotificationChannel channel2 = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + new NotificationChannel("id2", "name2", IMPORTANCE_HIGH); NotificationChannel channel3 = new NotificationChannel("id3", "name3", IMPORTANCE_LOW); channel3.setGroup(ncg.getId()); @@ -500,7 +502,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId()); mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg.getId()); - assertEquals(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); + assertEquals(channel2, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, true, channel1.getId(), channel2.getId(), channel3.getId(), NotificationChannel.DEFAULT_CHANNEL_ID); @@ -516,8 +519,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertNull(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false)); assertNull(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), false)); assertNull(mHelper.getNotificationChannelGroup(ncg.getId(), PKG_N_MR1, UID_N_MR1)); - //assertEquals(ncg2, mHelper.getNotificationChannelGroup(ncg2.getId(), PKG_N_MR1, UID_N_MR1)); - assertEquals(channel2, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); + assertEquals(channel2, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2.getId(), false)); } @Test @@ -799,14 +802,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCreateChannel_CannotChangeHiddenFields() throws Exception { + public void testCreateChannel_CannotChangeHiddenFields() { final NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + new NotificationChannel("id2", "name2", IMPORTANCE_HIGH); channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); channel.enableLights(true); channel.setBypassDnd(true); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); channel.setShowBadge(true); + channel.setAllowAppOverlay(false); int lockMask = 0; for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; @@ -823,19 +827,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertFalse(savedChannel.canBypassDnd()); assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); + assertEquals(channel.canOverlayApps(), savedChannel.canOverlayApps()); verify(mHandler, never()).requestSort(); } @Test - public void testCreateChannel_CannotChangeHiddenFieldsAssistant() throws Exception { + public void testCreateChannel_CannotChangeHiddenFieldsAssistant() { final NotificationChannel channel = - new NotificationChannel("id2", "name2", IMPORTANCE_LOW); + new NotificationChannel("id2", "name2", IMPORTANCE_HIGH); channel.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes); channel.enableLights(true); channel.setBypassDnd(true); channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET); channel.setShowBadge(true); + channel.setAllowAppOverlay(false); int lockMask = 0; for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { lockMask |= NotificationChannel.LOCKABLE_FIELDS[i]; @@ -852,10 +858,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertFalse(savedChannel.canBypassDnd()); assertFalse(Notification.VISIBILITY_SECRET == savedChannel.getLockscreenVisibility()); assertEquals(channel.canShowBadge(), savedChannel.canShowBadge()); + assertEquals(channel.canOverlayApps(), savedChannel.canOverlayApps()); } @Test - public void testClearLockedFields() throws Exception { + public void testClearLockedFields() { final NotificationChannel channel = getChannel(); mHelper.clearLockedFields(channel); assertEquals(0, channel.getUserLockedFields()); @@ -867,7 +874,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testLockFields_soundAndVibration() throws Exception { + public void testLockFields_soundAndVibration() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); final NotificationChannel update1 = getChannel(); @@ -891,7 +898,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testLockFields_vibrationAndLights() throws Exception { + public void testLockFields_vibrationAndLights() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); final NotificationChannel update1 = getChannel(); @@ -911,7 +918,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testLockFields_lightsAndImportance() throws Exception { + public void testLockFields_lightsAndImportance() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); final NotificationChannel update1 = getChannel(); @@ -931,7 +938,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testLockFields_visibilityAndDndAndBadge() throws Exception { + public void testLockFields_visibilityAndDndAndBadge() { mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); assertEquals(0, mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel().getId(), false) @@ -962,6 +969,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { .getUserLockedFields()); } + @Test + public void testLockFields_appOverlay() { + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel(), true, false); + assertEquals(0, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, getChannel().getId(), false) + .getUserLockedFields()); + + final NotificationChannel update = getChannel(); + update.setAllowAppOverlay(false); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, update, true); + assertEquals(NotificationChannel.USER_LOCKED_ALLOW_APP_OVERLAY, + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, update.getId(), false) + .getUserLockedFields()); + } + @Test public void testDeleteNonExistentChannel() throws Exception { mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, "does not exist"); @@ -1255,21 +1277,24 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted, true); - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nonGroupedNonDeletedChannel, true, false); + mHelper.createNotificationChannel( + PKG_N_MR1, UID_N_MR1, nonGroupedNonDeletedChannel, true, false); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedAndDeleted, true, false); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedButNotDeleted, true, false); mHelper.deleteNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, deleted.getId()); assertNull(mHelper.getNotificationChannelGroup(deleted.getId(), PKG_N_MR1, UID_N_MR1)); - assertNotNull(mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1)); + assertNotNull( + mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1)); - assertNull(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedAndDeleted.getId(), false)); - compareChannels(groupedAndDeleted, - mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedAndDeleted.getId(), true)); + assertNull(mHelper.getNotificationChannel( + PKG_N_MR1, UID_N_MR1, groupedAndDeleted.getId(), false)); + compareChannels(groupedAndDeleted, mHelper.getNotificationChannel( + PKG_N_MR1, UID_N_MR1, groupedAndDeleted.getId(), true)); - compareChannels(groupedButNotDeleted, - mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, groupedButNotDeleted.getId(), false)); + compareChannels(groupedButNotDeleted, mHelper.getNotificationChannel( + PKG_N_MR1, UID_N_MR1, groupedButNotDeleted.getId(), false)); compareChannels(nonGroupedNonDeletedChannel, mHelper.getNotificationChannel( PKG_N_MR1, UID_N_MR1, nonGroupedNonDeletedChannel.getId(), false)); @@ -1381,15 +1406,49 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCreateGroup() throws Exception { + public void testCreateGroup() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); - assertEquals(ncg, mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1).iterator().next()); + assertEquals(ncg, + mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1).iterator().next()); verify(mHandler, never()).requestSort(); } @Test - public void testCannotCreateChannel_badGroup() throws Exception { + public void testUpdateGroup_fromSystem_appOverlay() { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + + // from system, allowed + NotificationChannelGroup update = ncg.clone(); + update.setAllowAppOverlay(false); + + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, update, false); + NotificationChannelGroup updated = + mHelper.getNotificationChannelGroup("group1", PKG_N_MR1, UID_N_MR1); + assertFalse(updated.canOverlayApps()); + assertEquals(NotificationChannelGroup.USER_LOCKED_ALLOW_APP_OVERLAY, + updated.getUserLockedFields()); + } + + @Test + public void testUpdateGroup_fromApp_appOverlay() { + NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + + // from app, not allowed + NotificationChannelGroup update = new NotificationChannelGroup("group1", "name1"); + update.setAllowAppOverlay(false); + + mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); + NotificationChannelGroup updated = + mHelper.getNotificationChannelGroup("group1", PKG_N_MR1, UID_N_MR1); + assertTrue(updated.canOverlayApps()); + assertEquals(0, updated.getUserLockedFields()); + } + + @Test + public void testCannotCreateChannel_badGroup() { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); channel1.setGroup("garbage"); @@ -1401,7 +1460,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCannotCreateChannel_goodGroup() throws Exception { + public void testCannotCreateChannel_goodGroup() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); NotificationChannel channel1 = @@ -1409,12 +1468,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel1.setGroup(ncg.getId()); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false); - assertEquals(ncg.getId(), - mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false).getGroup()); + assertEquals(ncg.getId(), mHelper.getNotificationChannel( + PKG_N_MR1, UID_N_MR1, channel1.getId(), false).getGroup()); } @Test - public void testGetChannelGroups() throws Exception { + public void testGetChannelGroups() { NotificationChannelGroup unused = new NotificationChannelGroup("unused", "s"); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, unused, true); NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); @@ -1465,7 +1524,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testGetChannelGroups_noSideEffects() throws Exception { + public void testGetChannelGroups_noSideEffects() { NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, ncg, true); @@ -1516,10 +1575,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCreateChannel_updateName() throws Exception { + public void testCreateChannel_updateName() { NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false); - NotificationChannel actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); + NotificationChannel actual = + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertEquals("hello", actual.getName()); nc = new NotificationChannel("id", "goodbye", IMPORTANCE_HIGH); @@ -1533,12 +1593,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCreateChannel_addToGroup() throws Exception { + public void testCreateChannel_addToGroup() { NotificationChannelGroup group = new NotificationChannelGroup("group", ""); mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, group, true); NotificationChannel nc = new NotificationChannel("id", "hello", IMPORTANCE_DEFAULT); mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, nc, true, false); - NotificationChannel actual = mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); + NotificationChannel actual = + mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", false); assertNull(actual.getGroup()); nc = new NotificationChannel("id", "hello", IMPORTANCE_HIGH);