Add APIs for notification app overlays

- Can be enabled/disabled at channel and channel group levels
- An activity to launch can be added to notification

Test: atest, cts
Bug: 111236845
Change-Id: I9a4832211676cca4649d1f28e6e3e3157954d268
This commit is contained in:
Julia Reynolds
2018-10-24 09:22:38 -04:00
parent 2a7854addd
commit b6bd93d960
11 changed files with 338 additions and 101 deletions

View File

@@ -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<android.app.NotificationChannel> 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<android.app.NotificationChannelGroup> CREATOR;

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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.
*
* <p>This intent will be ignored unless this notification is posted to a channel that
* allows {@link NotificationChannel#canOverlayApps() app overlays}.</p>
*
* <p>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.</p>
*/
public Builder setAppOverlayIntent(PendingIntent intent) {
mN.mAppOverlayIntent = intent;
return this;
}
/** @removed */
@Deprecated
public Builder setChannel(String channelId) {

View File

@@ -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.
*
* <p>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}.</p>
*
* <p>Only modifiable before the channel is submitted to
* * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.</p>
* @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);
}

View File

@@ -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<NotificationChannel> 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.
*
* <p>This value will be ignored for notifications that are posted to channels that do not
* allow app overlays ({@link NotificationChannel#canOverlayApps()}.
*
* <p>Only modifiable before the channel is submitted to
* {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.</p>
* @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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);