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:
Julia Reynolds
2016-08-16 15:00:25 -04:00
parent 3cece22080
commit b5e44b7966
22 changed files with 1344 additions and 77 deletions

View File

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

View File

@@ -17,4 +17,3 @@
package android.app;
parcelable Notification;
parcelable Notification.Topic;

View File

@@ -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).
*

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

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

View File

@@ -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
*/

View File

@@ -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 */