[TOC]
从 API 26 开始,没有 NotificationChannel 的通知不允许发送,并且会有 Toast 提示:No Channel found for xxx
。本文仅做为对 NotificationChannel 的使用过程研究的记录。
1, 发出通知的正确代码
当 targetSdk 大于 26 时,通知的 Builder 在创建时需要传入 channel id,而这个 channel id 在使用之前需要先创建对应的 NotificationChannel,否则也会有错误提示。
private void sendNotification(int color, String channelId) {
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId, channelId,
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription(channelId);
channel.enableLights(true);
channel.setLightColor(color);
channel.setSound(null, null);
// Register the channel with system; you can't change the importance
// or other notification behaviors after this
mNotificationManager.createNotificationChannel(channel);
builder = new Notification.Builder(this, channelId);
} else {
builder = new Notification.Builder(this)
.setPriority(Notification.PRIORITY_DEFAULT)
.setLights(color, 1000, 0)
.setSound(null, null);
}
builder.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setContentTitle(channelId)
.setContentText(channelId)
.setSmallIcon(android.R.drawable.ic_notification_overlay);
ReflectUtils.reflect(builder).method("setFlag", 0x00001000, true);
Notification notification = builder.build();
mNotificationManager.notify(0x111, notification);
}
2, NotificationChannel 的注意事项
从 NotificationManager.createNotificationChannel() 到 NotificationManagerService.createNotificationChannelsImpl() 都是正常流程,create 的关键代码在 RankingHelper.createNotificationChannel() 中,具体如下:
@Override
public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
boolean fromTargetApp) {
Preconditions.checkNotNull(pkg);
Preconditions.checkNotNull(channel);
Preconditions.checkNotNull(channel.getId());
Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
Record r = getOrCreateRecord(pkg, uid);
if (r == null) {
throw new IllegalArgumentException("Invalid package");
}
if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
}
if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
throw new IllegalArgumentException("Reserved id");
}
// 前面是各种条件检查,下面这行是关键点,先检索这个 channel 是否已经存在,以 channel id 为标志位。
NotificationChannel existing = r.channels.get(channel.getId());
// Keep most of the existing settings
// 上面这句话道出了创建相同 id NotificationChannel 的关键,保留大部分已存在的设置,只更新了 name,description 等几项
if (existing != null && fromTargetApp) {
if (existing.isDeleted()) {
existing.setDeleted(false);
// log a resurrected channel as if it's new again
MetricsLogger.action(getChannelLog(channel, pkg).setType(
MetricsProto.MetricsEvent.TYPE_OPEN));
}
existing.setName(channel.getName().toString());
existing.setDescription(channel.getDescription());
existing.setBlockableSystem(channel.isBlockableSystem());
// Apps are allowed to downgrade channel importance if the user has not changed any
// fields on this channel yet.
if (existing.getUserLockedFields() == 0 &&
channel.getImportance() < existing.getImportance()) {
existing.setImportance(channel.getImportance());
}
updateConfig();
return;
}
if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
|| channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
throw new IllegalArgumentException("Invalid importance level");
}
// Reset fields that apps aren't allowed to set.
if (fromTargetApp) {
channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
channel.setLockscreenVisibility(r.visibility);
}
clearLockedFields(channel);
if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
}
if (!r.showBadge) {
channel.setShowBadge(false);
}
// channel 未创建过,把用户创建的 channel 加入到系统的 cache 里。
r.channels.put(channel.getId(), channel);
MetricsLogger.action(getChannelLog(channel, pkg).setType(
MetricsProto.MetricsEvent.TYPE_OPEN));
}
2.1 NotificationChannel 不会重复创建
从上面的分析可以知道,NotificationChannel 一旦创建,那么能更改的东西就很少了(只有名字,描述,blocksystem,以及优先级),而 blocksystem 属性只有在系统源码里面才能使用(hide),是系统应用创建的 NotificationChannel 是否能被 block的标记(未实验),所以特定的 channel 给特定的情况使用,不要多种情况混用同一个 channel。
如果想彻底删除已经创建注册的Channel,只有清除应用数据或者卸载应用。
Android官方是这么解释这个设计的:NotificationChannel 就像是开发者送给用户的一个精美礼物,一旦送出去,控制权就在用户那里了。即使用户把通知铃声设置成《江南style》,你可以知道,但不可以更改。
2.2 NotificationChannel 的配置会覆盖 Notificaition.Builder 的配置
在使用了 NotificationChannel 之后,关于震动、声音、呼吸灯的设置都需要在 NotificationChannel 中进行设置,
private Light calculateLights() {
int defaultLightColor = mContext.getResources().getColor(
com.android.internal.R.color.config_defaultNotificationColor);
int defaultLightOn = mContext.getResources().getInteger(
com.android.internal.R.integer.config_defaultNotificationLedOn);
int defaultLightOff = mContext.getResources().getInteger(
com.android.internal.R.integer.config_defaultNotificationLedOff);
int channelLightColor = getChannel().getLightColor() != 0 ? getChannel().getLightColor()
: defaultLightColor;
Light light = getChannel().shouldShowLights() ? new Light(channelLightColor,
defaultLightOn, defaultLightOff) : null;
if (mPreChannelsNotification
&& (getChannel().getUserLockedFields()
& NotificationChannel.USER_LOCKED_LIGHTS) == 0) {
final Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0) {
light = new Light(notification.ledARGB, notification.ledOnMS,
notification.ledOffMS);
if ((notification.defaults & Notification.DEFAULT_LIGHTS) != 0) {
light = new Light(defaultLightColor, defaultLightOn,
defaultLightOff);
}
} else {
light = null;
}
}
以呼吸灯为例,系统在读取呼吸灯配置的时候先从 channel 中获取,只有在 mPreChannelsNotification
为 true 的时候才会去使用 Notification 中的设置。这里没有强调对 targetSdk 的判断,是因为它在这里不重要。 当 targetSdk < 26 时,应用也可以设置Channel;而当 targetSdk >= 26 时,应用必须设置Channel;这两种情况下系统均会读取 NotificationChannel 中设置的属性。
在 NotificationRecord 的构造方法中, mPreChannelsNotification = isPreChannelsNotification();
private boolean isPreChannelsNotification() {
try {
if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(getChannel().getId())) {
final ApplicationInfo applicationInfo =
mContext.getPackageManager().getApplicationInfoAsUser(sbn.getPackageName(),
0, UserHandle.getUserId(sbn.getUid()));
if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.O) {
return true;
}
}
} catch (NameNotFoundException e) {
Slog.e(TAG, "Can't find package", e);
}
return false;
}
3, Notification 展示的过程
Notification 展示的过程我们直接从 enqueueNotificationInternal 开始看。
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
final int callingPid, final String tag, final int id, final Notification notification,
int incomingUserId) {
// 各种条件和权限判断
...
// 未设置 NotificationChannel 时抛出的 Toast 提醒就是此处发出的。
// setup local book-keeping
String channelId = notification.getChannelId();
if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
channelId = (new Notification.TvExtender(notification)).getChannelId();
}
final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg,
notificationUid, channelId, false /* includeDeleted */);
if (channel == null) {
final String noChannelStr = "No Channel found for "
+ "pkg=" + pkg
+ ", channelId=" + channelId
+ ", id=" + id
+ ", tag=" + tag
+ ", opPkg=" + opPkg
+ ", callingUid=" + callingUid
+ ", userId=" + userId
+ ", incomingUserId=" + incomingUserId
+ ", notificationUid=" + notificationUid
+ ", notification=" + notification;
Log.e(TAG, noChannelStr);
doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
"Failed to post notification on channel \"" + channelId + "\"\n" +
"See log for more details");
return;
}
// 注意此处创建的 StatusBarNotification
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, notificationUid, callingPid, notification,
user, null, System.currentTimeMillis());
// 注意此处创建的 NotificationRecord(),2.2 中的呼吸灯即是在这里获取好参数
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
...
mHandler.post(new EnqueueNotificationRunnable(userId, r));
}
在 PostNotificationRunnable 的 run() 方法中调用了 PostNotificationRunnable,Notification 的显示以及提示方式的控制都在 PostNotificationRunnable 中,显示是 mListeners.notifyPostedLocked(n, oldSbn);
,而提示方式则(声音、震动、呼吸灯)是在 buzzBeepBlinkLocked(r);
中。
参考
NotificationChannel 适配填坑指南