重点分析了APP层关心的问题,也可直接跳过分析,仅看黄色标注的结论部分。(居然不支持HTML!)
可能遇到的坑
- 为啥我的应用在Android O上发不出来通知了?
- 为啥我把上面的问题解决了,但设置通知的震动、声音、呼吸灯都不起作用啊?
- 为啥我把上面的问题都解决了,但通知声音关不了啊?
- 为啥我把上面的问题全都解决了,但想换个个性点的通知铃声换不了啊?
细看源码来填坑
1. 一号坑: 为何不能发出通知?
当发不出通知时,往往伴随着如下Log输出,有时还会伴随有Toast提示。
E/NotificationService: No Channel found for pkg=×××, channelId=null, id=952, tag=null, opPkg=×××, callingUid=10080, userId=0, incomingUserId=0, notificationUid=10080, notification=Notification(channel=null pri=1 contentView=null vibrate=default sound=default tick defaults=0x3 flags=0x10 color=0x00000000 vis=PRIVATE)
参见:NotificationManagerService.java -> enqueueNotificationInternal()
String channelId = 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;
}
系统打印的E级Log,以及toast均出自此处(toast仅在eng\userDebug固件中才会执行);原因是由于channel为空;那何时会为空呢?
参见:RankingHelper.java -> createDefaultChannelIfNeeded()
private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException {
final int userId = UserHandle.getUserId(r.uid);
final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
// O apps should not have the default channel.
return false;
}
// Otherwise, this app should have the default channel.
return true;
}
如上判断,当你的 targetSdk >= 26 时,系统是不会给你添加默认Channel的,反之低版本则会默认添加;
即使是targetSdk < 26,只要你的 compileSdk >= 26 ,也是可以设置Channel的,同样也会生效。
另外,通常NotificationChannel是在程序初始化时就已经创建并注册了,千万不要每次发通知的时候都去重新创建一次,没有任何意义。
结论:
当应用在Android O上发不出通知时,请先确认下 targetSdk 是否为26及以上,是否忘记传入已经创建过的 ChannelId 了。
如果你的TargetSDK是26以下,且构建通知时也没传入 ChannelId,那么这篇文章讨论的所有问题,你应该都不会遇到;在Android O设备上,你APP通知的表现应该会和以前一模一样。
2. 二号坑: 为何震动、声音、呼吸灯不起作用?
当增加了通知通道后,通知是出来了,却发现通知的震动、声音、呼吸灯这些属性,实际表现跟你期望的可能不一样。
此时,有木有发现NotificationChannel里也有一整套设置通知属性的方法!
// 传入参数:通道ID,通道名字,通道优先级(类似曾经的 builder.setPriority())
NotificationChannel channel =
new NotificationChannel(NOTIFICATION_CHANNELID, name, NotificationManager.IMPORTANCE_HIGH);
// 配置通知渠道的属性
channel.setDescription(description);
// 设置通知出现时声音,默认通知是有声音的
channel.setSound(null, null);
// 设置通知出现时的闪灯(如果 android 设备支持的话)
channel.enableLights(true);
channel.setLightColor(Color.RED);
// 设置通知出现时的震动(如果 android 设备支持的话)
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
//最后在 notificationManager 中创建该通知渠道
mNotificationManager.createNotificationChannel(channel);
当APP创建了Channel,并传入了ChannelId,系统就可能只会读取该Channel中的属性;而以前在Build时设置的属性全都无效了。这里说“可能”,而不是“一定”,就是因为需要满足如下条件:
参见:NotificationRecord.java -> mPreChannelsNotification
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;
}
如上判断,当 ChannelId 存在且非默认值(应用添加的均为非默认值,默认值只能由系统添加)时,mPreChannelsNotification 为false,则部分通知属性会采用 NotificationChannel 里设置的参数,而非Notification Build时设置的参数。涉及参数有:通知声音、呼吸灯、震动、优先级。
结论:
当设置的通知震动、声音、呼吸灯不起作用时,请先确认你是否创建了 NotificationChannel,并在构建通知时传入了该ChannelId。如果是的话,你需要将以前在notification build时设置的这些参数,转移到 notificationChannel 中,方可生效。
另外,这里没有强调对 targetSdk 的判断,是因为它在这里不重要。 当 targetSdk < 26 时,应用也可以设置Channel;而当 targetSdk >= 26 时,应用必须设置Channel;这两种情况下系统均会读取 notificationChannel 中设置的属性。
3. 三号坑:
通知声音不能关闭、通知铃声不能更改,以及震动、呼吸灯、优先级这些属性在Channel中更改无效,都属于同一类问题。于是四号坑就在这里一并填了吧。
此类问题是在APP创建 NotificationChannel 时,就已经确定下来了。即:
mNotificationManager.createNotificationChannel(channel);
参见:RankingHelper.java -> createNotificationChannel()
NotificationChannel existing = r.channels.get(channel.getId());
// Keep most of the existing settings
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;
}
APP创建的 Channel 最终是在NMS(通知服务)中完成初始化并注册的;如上述逻辑片段,系统首先会判断此 ChannelId 是否已经存在,如果存在的话,捞出来继续用!!!
你可以更新的属性也只有通道Name和Description,另外也可以把通道优先级往低了调,前提是用户没有手动更改过。不难看出,上面说的声音、震动、呼吸灯这些属性是没法改了。。。
聪明的你一定想到办法了,那我可以先把这个ChannelId的通知通道删了,在创建个相同ChannelId的。其实开始我也是这么想的,不过智慧的谷歌工程师,把这条路堵死了。当你调用 deleteNotificationChannel() 删除通知通道时,其实系统里除了给这个通道打个 “deleted” 的标签外,啥也没干。。。当你再次创建相同 ChannelId 的通道时,它只是把旧的那个捞出来,去掉 “deleted” 标签继续用。
此刻,你应该发现了一个“小漏洞”,那我可以创建个新的ChannelId,不就可以了。答案是肯定的,当然可以了。不过就是,系统会把你删除通道的这个行为记录下来,用小字儿在你APP的通知设置页面显示出来 —— "n categories deleted"。
如果想彻底删除已经创建注册的Channel,只有清除应用数据或者卸载应用。
Android官方是这么解释这个设计的:NotificationChannel 就像是开发者送给用户的一个精美礼物,一旦送出去,控制权就在用户那里了。即使用户把通知铃声设置成《江南style》,你可以知道,但不可以更改。
结论:
刚适配Android O时,发现通知声音关不掉;主要是因为Android在 NotificationChannel 中将声音设置成默认开启了,而已经设置的 Channel 属性又不能更改,所以无论如何调试也不会生效。其它属性原理与此类似。
若要新的 Channel 属性生效,只有三个办法:更换ChannelId、清除应用数据、卸载应用
4. 补充Channel importance levels:
提到通知的声音、震动属性,不得不提下通知“优先级”这个参数,也叫通知“重要性”。
Android O之前,叫通知“优先级”,通过在Build时,setPriority() 设置,共分为5档(-2 ~ 2);
默认值:Notification.PRIORITY_DEFAULT
Android O之后,叫通知“重要性”,通过NotificationChannel的 setImportance() 设置,也是5档(0 ~ 4);
默认值:NotificationManager.IMPORTANCE_DEFAULT
即使你设置了通知声音、震动这些属性,其“重要性”也必须满足下表对应的档位:
Importance | Behavior | Usage | Examples |
---|---|---|---|
HIGH | Makes a sound and appears on screen | Time-critical information that the user must know, or act on, immediately | Text messages, alarms, phone calls |
DEFAULT | Makes a sound | Information that should be seen at the user’s earliest convenience, but not interrupt what they're doing | Traffic alerts, task reminders |
LOW | No sound | Notification channels that don't meet the requirements of other importance levels | New content the user has subscribed to, social network invitations |
MIN | No sound or visual interruption | Non-essential information that can wait or isn’t specifically relevant to the user | Nearby places of interest, weather, promotional content |
NONE | Don't show in the shade | Normally, Suppressing notification from package by user request | Blocked apps notification |
如果遇到通知声音、振动、呼吸灯提醒异常的情况,也可通过检索如下event log判断是否设置成功:
06-17 14:26:57.250 2848 2848 I notification_alert: [0|***|915|null|10110,1,1,1]
最后三位参数分别是:buzz(振动), beep(响铃), blink(呼吸灯)
最后的总结
Android每次版本升级,均会对通知中心作出较大改动。Android O 引入的通知通道,相比于L时引入的通知分组,对APP的影响更大,系统的态度也更加强硬。另一方面也体现了Android非常重视用户的选择权,杜绝无意义的通知打扰,希望将这些权限完全掌控在用户手中。
由于国内各大ROM定制厂商,虽然升级到了Android O,但由于其UI及交互,与原生差异较大;这部分逻辑往往是残缺的。要么废弃了通知通道功能,要么屏蔽了通知通道的设置页面。就像当年Android L的通知分组和通知回复那样,并不是所有的国内定制ROM都支持的。
所以对那些比较看重通知场景的应用(如信息提醒类),最稳妥的做法或许是:
不适配Android O,保持TargetSDK在26以下
适配Android O,自己实现震动、铃声;如微信、QQ
适配Android O,每次更新“声音、振动、呼吸灯、重要性”属性时,创建新的channelId
【参考材料】
NotificationChannel API 文档:API NotificationChannel
NotificationChannel 设计说明:Channels in Android O
NotificationChannel 视频介绍(Youtube):Notification Updates in Android Oreo
NotificationChannel 视频介绍(YouKu):Notification Updates in Android Oreo