Add Notification channels
In this iteration:
-Every app gets a default channel that notifications will be posted
to if they don't specify a channel themselves. The default channel
inherits app-wide settings on upgrade.
-Apps can create new channels without user approval, but apps
cannot change the name of a channel once created, nor can they ever
set the importance.
- When a notification is posted:
- If the channel is marked as 'vibrates', vibration will be
applied to notifications that lack a vibration. unlike the default
notification flag, notifications will retain their custom vibration
if given
- Same with sound and lights
- A notification's importance is the min of the app and channel
importance
- A notification can bypass dnd if: either the app or channel settings
say it can
- A notification's show on lockscreen setting comes from the app first,
and the channel second if the app has no preference
Tests: in cl. also there's a cl for cts and a test app.
Change-Id: I630f99df655800b00586dcfab538d320d04fe0f0
This commit is contained in:
@@ -19,6 +19,7 @@ package android.app;
|
||||
|
||||
import android.app.ITransientNotification;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
@@ -57,6 +58,15 @@ interface INotificationManager
|
||||
int getImportance(String pkg, int uid);
|
||||
int getPackageImportance(String pkg);
|
||||
|
||||
void createNotificationChannel(String pkg, in NotificationChannel channel);
|
||||
void updateNotificationChannel(String pkg, in NotificationChannel channel);
|
||||
void updateNotificationChannelForPackage(String pkg, int uid, in NotificationChannel channel);
|
||||
NotificationChannel getNotificationChannel(String pkg, String channelId);
|
||||
NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId);
|
||||
void deleteNotificationChannel(String pkg, String channelId);
|
||||
ParceledListSlice getNotificationChannels(String pkg);
|
||||
ParceledListSlice getNotificationChannelsForPackage(String pkg, int uid);
|
||||
|
||||
// TODO: Remove this when callers have been migrated to the equivalent
|
||||
// INotificationListener method.
|
||||
StatusBarNotification[] getActiveNotifications(String callingPkg);
|
||||
|
||||
@@ -17,4 +17,3 @@
|
||||
package android.app;
|
||||
|
||||
parcelable Notification;
|
||||
parcelable Notification.Topic;
|
||||
|
||||
@@ -992,6 +992,8 @@ public class Notification implements Parcelable
|
||||
private Icon mSmallIcon;
|
||||
private Icon mLargeIcon;
|
||||
|
||||
private String mChannelId;
|
||||
|
||||
/**
|
||||
* Structure to encapsulate a named action that can be shown as part of this notification.
|
||||
* It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is
|
||||
@@ -1675,6 +1677,10 @@ public class Notification implements Parcelable
|
||||
}
|
||||
|
||||
color = parcel.readInt();
|
||||
|
||||
if (parcel.readInt() != 0) {
|
||||
mChannelId = parcel.readString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1780,6 +1786,8 @@ public class Notification implements Parcelable
|
||||
|
||||
that.color = this.color;
|
||||
|
||||
that.mChannelId = this.mChannelId;
|
||||
|
||||
if (!heavy) {
|
||||
that.lightenPayload(); // will clean out extras
|
||||
}
|
||||
@@ -2028,6 +2036,13 @@ public class Notification implements Parcelable
|
||||
}
|
||||
|
||||
parcel.writeInt(color);
|
||||
|
||||
if (mChannelId != null) {
|
||||
parcel.writeInt(1);
|
||||
parcel.writeString(mChannelId);
|
||||
} else {
|
||||
parcel.writeInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2217,6 +2232,13 @@ public class Notification implements Parcelable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the channel this notification posts to.
|
||||
*/
|
||||
public String getNotificationChannel() {
|
||||
return mChannelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The small icon representing this notification in the status bar and content view.
|
||||
*
|
||||
@@ -2405,6 +2427,14 @@ public class Notification implements Parcelable
|
||||
return mColorUtil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the channel the notification should be delivered on.
|
||||
*/
|
||||
public Builder setChannel(String channelId) {
|
||||
mN.mChannelId = channelId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a timestamp pertaining to the notification (usually the time the event occurred).
|
||||
*
|
||||
|
||||
19
core/java/android/app/NotificationChannel.aidl
Normal file
19
core/java/android/app/NotificationChannel.aidl
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2016, The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.app;
|
||||
|
||||
parcelable NotificationChannel;
|
||||
382
core/java/android/app/NotificationChannel.java
Normal file
382
core/java/android/app/NotificationChannel.java
Normal file
@@ -0,0 +1,382 @@
|
||||
package android.app;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import android.annotation.SystemApi;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A representation of settings that apply to a collection of similarly themed notifications.
|
||||
*/
|
||||
public final class NotificationChannel implements Parcelable {
|
||||
|
||||
/**
|
||||
* The id of the default channel for an app. All notifications posted without a notification
|
||||
* channel specified are posted to this channel.
|
||||
*/
|
||||
public static final String DEFAULT_CHANNEL_ID = "miscellaneous";
|
||||
|
||||
private static final String TAG_CHANNEL = "channel";
|
||||
private static final String ATT_NAME = "name";
|
||||
private static final String ATT_ID = "id";
|
||||
private static final String ATT_PRIORITY = "priority";
|
||||
private static final String ATT_VISIBILITY = "visibility";
|
||||
private static final String ATT_IMPORTANCE = "importance";
|
||||
private static final String ATT_LIGHTS = "lights";
|
||||
private static final String ATT_VIBRATION = "vibration";
|
||||
private static final String ATT_DEFAULT_RINGTONE = "ringtone";
|
||||
|
||||
private static final int DEFAULT_VISIBILITY =
|
||||
NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
|
||||
private static final int DEFAULT_IMPORTANCE =
|
||||
NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
|
||||
|
||||
private final String mId;
|
||||
private CharSequence mName;
|
||||
private int mImportance = DEFAULT_IMPORTANCE;
|
||||
private boolean mBypassDnd;
|
||||
private int mLockscreenVisibility = DEFAULT_VISIBILITY;
|
||||
private Uri mRingtone;
|
||||
private boolean mLights;
|
||||
private boolean mVibration;
|
||||
|
||||
/**
|
||||
* Creates a notification channel.
|
||||
*
|
||||
* @param id The id of the channel. Must be unique per package.
|
||||
* @param name The user visible name of the channel.
|
||||
*/
|
||||
public NotificationChannel(String id, CharSequence name) {
|
||||
this.mId = id;
|
||||
this.mName = name;
|
||||
}
|
||||
|
||||
protected NotificationChannel(Parcel in) {
|
||||
if (in.readByte() != 0) {
|
||||
mId = in.readString();
|
||||
} else {
|
||||
mId = null;
|
||||
}
|
||||
mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
||||
mImportance = in.readInt();
|
||||
mBypassDnd = in.readByte() != 0;
|
||||
mLockscreenVisibility = in.readInt();
|
||||
if (in.readByte() != 0) {
|
||||
mRingtone = Uri.CREATOR.createFromParcel(in);
|
||||
} else {
|
||||
mRingtone = null;
|
||||
}
|
||||
mLights = in.readByte() != 0;
|
||||
mVibration = in.readByte() != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
if (mId != null) {
|
||||
dest.writeByte((byte) 1);
|
||||
dest.writeString(mId);
|
||||
} else {
|
||||
dest.writeByte((byte) 0);
|
||||
}
|
||||
TextUtils.writeToParcel(mName, dest, flags);
|
||||
dest.writeInt(mImportance);
|
||||
dest.writeByte(mBypassDnd ? (byte) 1 : (byte) 0);
|
||||
dest.writeInt(mLockscreenVisibility);
|
||||
if (mRingtone != null) {
|
||||
dest.writeByte((byte) 1);
|
||||
mRingtone.writeToParcel(dest, 0);
|
||||
} else {
|
||||
dest.writeByte((byte) 0);
|
||||
}
|
||||
dest.writeByte(mLights ? (byte) 1 : (byte) 0);
|
||||
dest.writeByte(mVibration ? (byte) 1 : (byte) 0);
|
||||
}
|
||||
|
||||
// Only modifiable by users.
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void setName(CharSequence name) {
|
||||
this.mName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void setImportance(int importance) {
|
||||
this.mImportance = importance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void setBypassDnd(boolean bypassDnd) {
|
||||
this.mBypassDnd = bypassDnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void setLockscreenVisibility(int lockscreenVisibility) {
|
||||
this.mLockscreenVisibility = lockscreenVisibility;
|
||||
}
|
||||
|
||||
// Modifiable by apps.
|
||||
|
||||
/**
|
||||
* Sets the ringtone that should be played for notifications posted to this channel if
|
||||
* the notifications don't supply a ringtone.
|
||||
*/
|
||||
public void setDefaultRingtone(Uri defaultRingtone) {
|
||||
this.mRingtone = defaultRingtone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether notifications posted to this channel should display notification lights,
|
||||
* on devices that support that feature.
|
||||
*/
|
||||
public void setLights(boolean lights) {
|
||||
this.mLights = lights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether notification posted to this channel should vibrate, even if individual
|
||||
* notifications are marked as having vibration.
|
||||
*/
|
||||
public void setVibration(boolean vibration) {
|
||||
this.mVibration = vibration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of this channel.
|
||||
*/
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user visible name of this channel.
|
||||
*/
|
||||
public CharSequence getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user specified importance {e.g. @link NotificationManager#IMPORTANCE_LOW} for
|
||||
* notifications posted to this channel.
|
||||
*/
|
||||
public int getImportance() {
|
||||
return mImportance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not notifications posted to this channel can bypass the Do Not Disturb
|
||||
* {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY} mode.
|
||||
*/
|
||||
public boolean canBypassDnd() {
|
||||
return mBypassDnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification sound for this channel.
|
||||
*/
|
||||
public Uri getDefaultRingtone() {
|
||||
return mRingtone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether notifications posted to this channel trigger notification lights.
|
||||
*/
|
||||
public boolean shouldShowLights() {
|
||||
return mLights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether notifications posted to this channel always vibrate.
|
||||
*/
|
||||
public boolean shouldVibrate() {
|
||||
return mVibration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public int getLockscreenVisibility() {
|
||||
return mLockscreenVisibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void populateFromXml(XmlPullParser parser) {
|
||||
// Name and id are set in the constructor.
|
||||
setImportance(safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE));
|
||||
setBypassDnd(Notification.PRIORITY_DEFAULT
|
||||
!= safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT));
|
||||
setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
|
||||
setDefaultRingtone(safeUri(parser, ATT_DEFAULT_RINGTONE));
|
||||
setLights(safeBool(parser, ATT_LIGHTS, false));
|
||||
setVibration(safeBool(parser, ATT_VIBRATION, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public void writeXml(XmlSerializer out) throws IOException {
|
||||
out.startTag(null, TAG_CHANNEL);
|
||||
out.attribute(null, ATT_ID, getId());
|
||||
out.attribute(null, ATT_NAME, getName().toString());
|
||||
if (getImportance() != DEFAULT_IMPORTANCE) {
|
||||
out.attribute(
|
||||
null, ATT_IMPORTANCE, Integer.toString(getImportance()));
|
||||
}
|
||||
if (canBypassDnd()) {
|
||||
out.attribute(
|
||||
null, ATT_PRIORITY, Integer.toString(Notification.PRIORITY_MAX));
|
||||
}
|
||||
if (getLockscreenVisibility() != DEFAULT_VISIBILITY) {
|
||||
out.attribute(null, ATT_VISIBILITY,
|
||||
Integer.toString(getLockscreenVisibility()));
|
||||
}
|
||||
if (getDefaultRingtone() != null) {
|
||||
out.attribute(null, ATT_DEFAULT_RINGTONE, getDefaultRingtone().toString());
|
||||
}
|
||||
if (shouldShowLights()) {
|
||||
out.attribute(null, ATT_LIGHTS, Boolean.toString(shouldShowLights()));
|
||||
}
|
||||
if (shouldVibrate()) {
|
||||
out.attribute(null, ATT_VIBRATION, Boolean.toString(shouldVibrate()));
|
||||
}
|
||||
out.endTag(null, TAG_CHANNEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public JSONObject toJson() throws JSONException {
|
||||
JSONObject record = new JSONObject();
|
||||
record.put(ATT_ID, getId());
|
||||
record.put(ATT_NAME, getName());
|
||||
if (getImportance() != DEFAULT_IMPORTANCE) {
|
||||
record.put(ATT_IMPORTANCE,
|
||||
NotificationListenerService.Ranking.importanceToString(getImportance()));
|
||||
}
|
||||
if (canBypassDnd()) {
|
||||
record.put(ATT_PRIORITY, Notification.PRIORITY_MAX);
|
||||
}
|
||||
if (getLockscreenVisibility() != DEFAULT_VISIBILITY) {
|
||||
record.put(ATT_VISIBILITY, Notification.visibilityToString(getLockscreenVisibility()));
|
||||
}
|
||||
if (getDefaultRingtone() != null) {
|
||||
record.put(ATT_DEFAULT_RINGTONE, getDefaultRingtone().toString());
|
||||
}
|
||||
record.put(ATT_LIGHTS, Boolean.toString(shouldShowLights()));
|
||||
record.put(ATT_VIBRATION, Boolean.toString(shouldVibrate()));
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private static Uri safeUri(XmlPullParser parser, String att) {
|
||||
final String val = parser.getAttributeValue(null, att);
|
||||
return val == null ? null : Uri.parse(val);
|
||||
}
|
||||
|
||||
private static int safeInt(XmlPullParser parser, String att, int defValue) {
|
||||
final String val = parser.getAttributeValue(null, att);
|
||||
return tryParseInt(val, defValue);
|
||||
}
|
||||
|
||||
private static int tryParseInt(String value, int defValue) {
|
||||
if (TextUtils.isEmpty(value)) return defValue;
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
|
||||
final String value = parser.getAttributeValue(null, att);
|
||||
if (TextUtils.isEmpty(value)) return defValue;
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
|
||||
public static final Creator<NotificationChannel> CREATOR = new Creator<NotificationChannel>() {
|
||||
@Override
|
||||
public NotificationChannel createFromParcel(Parcel in) {
|
||||
return new NotificationChannel(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationChannel[] newArray(int size) {
|
||||
return new NotificationChannel[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
NotificationChannel that = (NotificationChannel) o;
|
||||
|
||||
if (mImportance != that.mImportance) return false;
|
||||
if (mBypassDnd != that.mBypassDnd) return false;
|
||||
if (mLockscreenVisibility != that.mLockscreenVisibility) return false;
|
||||
if (mLights != that.mLights) return false;
|
||||
if (mVibration != that.mVibration) return false;
|
||||
if (!mId.equals(that.mId)) return false;
|
||||
if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
|
||||
return mRingtone != null ? mRingtone.equals(
|
||||
that.mRingtone) : that.mRingtone == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NotificationChannel{" +
|
||||
"mId='" + mId + '\'' +
|
||||
", mName=" + mName +
|
||||
", mImportance=" + mImportance +
|
||||
", mBypassDnd=" + mBypassDnd +
|
||||
", mLockscreenVisibility=" + mLockscreenVisibility +
|
||||
", mRingtone='" + mRingtone + '\'' +
|
||||
", mLights=" + mLights +
|
||||
", mVibration=" + mVibration +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = mId.hashCode();
|
||||
result = 31 * result + (mName != null ? mName.hashCode() : 0);
|
||||
result = 31 * result + mImportance;
|
||||
result = 31 * result + (mBypassDnd ? 1 : 0);
|
||||
result = 31 * result + mLockscreenVisibility;
|
||||
result = 31 * result + (mRingtone != null ? mRingtone.hashCode() : 0);
|
||||
result = 31 * result + (mLights ? 1 : 0);
|
||||
result = 31 * result + (mVibration ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -377,6 +377,66 @@ public class NotificationManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification channel that notifications can be posted to.
|
||||
*/
|
||||
public void createNotificationChannel(NotificationChannel channel) {
|
||||
INotificationManager service = getService();
|
||||
try {
|
||||
service.createNotificationChannel(mContext.getPackageName(), channel);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification channel settings for a given channel id.
|
||||
*/
|
||||
public NotificationChannel getNotificationChannel(String channelId) {
|
||||
INotificationManager service = getService();
|
||||
try {
|
||||
return service.getNotificationChannel(mContext.getPackageName(), channelId);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all notification channels created by the calling app.
|
||||
*/
|
||||
public List<NotificationChannel> getNotificationChannels() {
|
||||
INotificationManager service = getService();
|
||||
try {
|
||||
return service.getNotificationChannels(mContext.getPackageName()).getList();
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings for a given channel.
|
||||
*/
|
||||
public void updateNotificationChannel(NotificationChannel channel) {
|
||||
INotificationManager service = getService();
|
||||
try {
|
||||
service.updateNotificationChannel(mContext.getPackageName(), channel);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given notification channel.
|
||||
*/
|
||||
public void deleteNotificationChannel(String channelId) {
|
||||
INotificationManager service = getService();
|
||||
try {
|
||||
service.deleteNotificationChannel(mContext.getPackageName(), channelId);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
|
||||
@@ -99,6 +99,9 @@ public abstract class NotificationRankerService extends NotificationListenerServ
|
||||
/** Autobundled summary notification was canceled because its group was unbundled */
|
||||
public static final int REASON_UNAUTOBUNDLED = 16;
|
||||
|
||||
/** Notification was canceled by the user banning the channel. */
|
||||
public static final int REASON_CHANNEL_BANNED = 17;
|
||||
|
||||
private Handler mHandler;
|
||||
|
||||
/** @hide */
|
||||
|
||||
Reference in New Issue
Block a user