重點(diǎn)分析了APP層關(guān)心的問(wèn)題,也可直接跳過(guò)分析,僅看黃色標(biāo)注的結(jié)論部分。(簡(jiǎn)書(shū)居然不支持HTML!)
可能遇到的坑
- 為啥我的應(yīng)用在A(yíng)ndroid O上發(fā)不出來(lái)通知了?
- 為啥我把上面的問(wèn)題解決了,但設(shè)置通知的震動(dòng)、聲音、呼吸燈都不起作用?。?/li>
- 為啥我把上面的問(wèn)題都解決了,但通知聲音關(guān)不了???
- 為啥我把上面的問(wèn)題全都解決了,但想換個(gè)個(gè)性點(diǎn)的通知鈴聲換不了啊?
細(xì)看源碼來(lái)填坑
1. 一號(hào)坑: 為何不能發(fā)出通知?
當(dāng)發(fā)不出通知時(shí),往往伴隨著如下Log輸出,有時(shí)還會(huì)伴隨有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)
參見(jiàn):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;
}
系統(tǒng)打印的E級(jí)Log,以及toast均出自此處(toast僅在eng\userDebug固件中才會(huì)執(zhí)行);原因是由于channel為空;那何時(shí)會(huì)為空呢?
參見(jiàn):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;
}
如上判斷,當(dāng)你的 targetSdk >= 26 時(shí),系統(tǒng)是不會(huì)給你添加默認(rèn)Channel的,反之低版本則會(huì)默認(rèn)添加;
即使是targetSdk < 26,只要你的 compileSdk >= 26 ,也是可以設(shè)置Channel的,同樣也會(huì)生效。
另外,通常NotificationChannel是在程序初始化時(shí)就已經(jīng)創(chuàng)建并注冊(cè)了,千萬(wàn)不要每次發(fā)通知的時(shí)候都去重新創(chuàng)建一次,沒(méi)有任何意義。
結(jié)論:
當(dāng)應(yīng)用在A(yíng)ndroid O上發(fā)不出通知時(shí),請(qǐng)先確認(rèn)下 targetSdk 是否為26及以上,是否忘記傳入已經(jīng)創(chuàng)建過(guò)的 ChannelId 了。
如果你的TargetSDK是26以下,且構(gòu)建通知時(shí)也沒(méi)傳入 ChannelId,那么這篇文章討論的所有問(wèn)題,你應(yīng)該都不會(huì)遇到;在A(yíng)ndroid O設(shè)備上,你APP通知的表現(xiàn)應(yīng)該會(huì)和以前一模一樣。
2. 二號(hào)坑: 為何震動(dòng)、聲音、呼吸燈不起作用?
當(dāng)增加了通知通道后,通知是出來(lái)了,卻發(fā)現(xiàn)通知的震動(dòng)、聲音、呼吸燈這些屬性,實(shí)際表現(xiàn)跟你期望的可能不一樣。
此時(shí),有木有發(fā)現(xiàn)NotificationChannel里也有一整套設(shè)置通知屬性的方法!
// 傳入?yún)?shù):通道ID,通道名字,通道優(yōu)先級(jí)(類(lèi)似曾經(jīng)的 builder.setPriority())
NotificationChannel channel =
new NotificationChannel(NOTIFICATION_CHANNELID, name, NotificationManager.IMPORTANCE_HIGH);
// 配置通知渠道的屬性
channel.setDescription(description);
// 設(shè)置通知出現(xiàn)時(shí)聲音,默認(rèn)通知是有聲音的
channel.setSound(null, null);
// 設(shè)置通知出現(xiàn)時(shí)的閃燈(如果 android 設(shè)備支持的話(huà))
channel.enableLights(true);
channel.setLightColor(Color.RED);
// 設(shè)置通知出現(xiàn)時(shí)的震動(dòng)(如果 android 設(shè)備支持的話(huà))
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
//最后在 notificationManager 中創(chuàng)建該通知渠道
mNotificationManager.createNotificationChannel(channel);
當(dāng)APP創(chuàng)建了Channel,并傳入了ChannelId,系統(tǒng)就可能只會(huì)讀取該Channel中的屬性;而以前在Build時(shí)設(shè)置的屬性全都無(wú)效了。這里說(shuō)“可能”,而不是“一定”,就是因?yàn)樾枰獫M(mǎn)足如下條件:
參見(jiàn):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;
}
如上判斷,當(dāng) ChannelId 存在且非默認(rèn)值(應(yīng)用添加的均為非默認(rèn)值,默認(rèn)值只能由系統(tǒng)添加)時(shí),mPreChannelsNotification 為false,則部分通知屬性會(huì)采用 NotificationChannel 里設(shè)置的參數(shù),而非Notification Build時(shí)設(shè)置的參數(shù)。涉及參數(shù)有:通知聲音、呼吸燈、震動(dòng)、優(yōu)先級(jí)。
結(jié)論:
當(dāng)設(shè)置的通知震動(dòng)、聲音、呼吸燈不起作用時(shí),請(qǐng)先確認(rèn)你是否創(chuàng)建了 NotificationChannel,并在構(gòu)建通知時(shí)傳入了該ChannelId。如果是的話(huà),你需要將以前在notification build時(shí)設(shè)置的這些參數(shù),轉(zhuǎn)移到 notificationChannel 中,方可生效。
另外,這里沒(méi)有強(qiáng)調(diào)對(duì) targetSdk 的判斷,是因?yàn)樗谶@里不重要。 當(dāng) targetSdk < 26 時(shí),應(yīng)用也可以設(shè)置Channel;而當(dāng) targetSdk >= 26 時(shí),應(yīng)用必須設(shè)置Channel;這兩種情況下系統(tǒng)均會(huì)讀取 notificationChannel 中設(shè)置的屬性。
3. 三號(hào)坑:
通知聲音不能關(guān)閉、通知鈴聲不能更改,以及震動(dòng)、呼吸燈、優(yōu)先級(jí)這些屬性在Channel中更改無(wú)效,都屬于同一類(lèi)問(wèn)題。于是四號(hào)坑就在這里一并填了吧。
此類(lèi)問(wèn)題是在A(yíng)PP創(chuàng)建 NotificationChannel 時(shí),就已經(jīng)確定下來(lái)了。即:
mNotificationManager.createNotificationChannel(channel);
參見(jiàn):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創(chuàng)建的 Channel 最終是在NMS(通知服務(wù))中完成初始化并注冊(cè)的;如上述邏輯片段,系統(tǒng)首先會(huì)判斷此 ChannelId 是否已經(jīng)存在,如果存在的話(huà),撈出來(lái)繼續(xù)用!??!
你可以更新的屬性也只有通道Name和Description,另外也可以把通道優(yōu)先級(jí)往低了調(diào),前提是用戶(hù)沒(méi)有手動(dòng)更改過(guò)。不難看出,上面說(shuō)的聲音、震動(dòng)、呼吸燈這些屬性是沒(méi)法改了。。。
聰明的你一定想到辦法了,那我可以先把這個(gè)ChannelId的通知通道刪了,在創(chuàng)建個(gè)相同ChannelId的。其實(shí)開(kāi)始我也是這么想的,不過(guò)智慧的谷歌工程師,把這條路堵死了。當(dāng)你調(diào)用 deleteNotificationChannel() 刪除通知通道時(shí),其實(shí)系統(tǒng)里除了給這個(gè)通道打個(gè) “deleted” 的標(biāo)簽外,啥也沒(méi)干。。。當(dāng)你再次創(chuàng)建相同 ChannelId 的通道時(shí),它只是把舊的那個(gè)撈出來(lái),去掉 “deleted” 標(biāo)簽繼續(xù)用。
此刻,你應(yīng)該發(fā)現(xiàn)了一個(gè)“小漏洞”,那我可以創(chuàng)建個(gè)新的ChannelId,不就可以了。答案是肯定的,當(dāng)然可以了。不過(guò)就是,系統(tǒng)會(huì)把你刪除通道的這個(gè)行為記錄下來(lái),用小字兒在你APP的通知設(shè)置頁(yè)面顯示出來(lái) —— "n categories deleted"。
如果想徹底刪除已經(jīng)創(chuàng)建注冊(cè)的Channel,只有清除應(yīng)用數(shù)據(jù)或者卸載應(yīng)用。
Android官方是這么解釋這個(gè)設(shè)計(jì)的:NotificationChannel 就像是開(kāi)發(fā)者送給用戶(hù)的一個(gè)精美禮物,一旦送出去,控制權(quán)就在用戶(hù)那里了。即使用戶(hù)把通知鈴聲設(shè)置成《江南style》,你可以知道,但不可以更改。
結(jié)論:
剛適配Android O時(shí),發(fā)現(xiàn)通知聲音關(guān)不掉;主要是因?yàn)锳ndroid在 NotificationChannel 中將聲音設(shè)置成默認(rèn)開(kāi)啟了,而已經(jīng)設(shè)置的 Channel 屬性又不能更改,所以無(wú)論如何調(diào)試也不會(huì)生效。其它屬性原理與此類(lèi)似。
若要新的 Channel 屬性生效,只有三個(gè)辦法:更換ChannelId、清除應(yīng)用數(shù)據(jù)、卸載應(yīng)用
4. 補(bǔ)充Channel importance levels:
提到通知的聲音、震動(dòng)屬性,不得不提下通知“優(yōu)先級(jí)”這個(gè)參數(shù),也叫通知“重要性”。
Android O之前,叫通知“優(yōu)先級(jí)”,通過(guò)在Build時(shí),setPriority() 設(shè)置,共分為5檔(-2 ~ 2);
默認(rèn)值:Notification.PRIORITY_DEFAULT
Android O之后,叫通知“重要性”,通過(guò)NotificationChannel的 setImportance() 設(shè)置,也是5檔(0 ~ 4);
默認(rèn)值:NotificationManager.IMPORTANCE_DEFAULT
即使你設(shè)置了通知聲音、震動(dòng)這些屬性,其“重要性”也必須滿(mǎn)足下表對(duì)應(yīng)的檔位:
| 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 |
如果遇到通知聲音、振動(dòng)、呼吸燈提醒異常的情況,也可通過(guò)檢索如下event log判斷是否設(shè)置成功:
06-17 14:26:57.250 2848 2848 I notification_alert: [0|***|915|null|10110,1,1,1]
最后三位參數(shù)分別是:buzz(振動(dòng)), beep(響鈴), blink(呼吸燈)
最后的總結(jié)
Android每次版本升級(jí),均會(huì)對(duì)通知中心作出較大改動(dòng)。Android O 引入的通知通道,相比于L時(shí)引入的通知分組,對(duì)APP的影響更大,系統(tǒng)的態(tài)度也更加強(qiáng)硬。另一方面也體現(xiàn)了Android非常重視用戶(hù)的選擇權(quán),杜絕無(wú)意義的通知打擾,希望將這些權(quán)限完全掌控在用戶(hù)手中。
由于國(guó)內(nèi)各大ROM定制廠(chǎng)商,雖然升級(jí)到了Android O,但由于其UI及交互,與原生差異較大;這部分邏輯往往是殘缺的。要么廢棄了通知通道功能,要么屏蔽了通知通道的設(shè)置頁(yè)面。就像當(dāng)年Android L的通知分組和通知回復(fù)那樣,并不是所有的國(guó)內(nèi)定制ROM都支持的。
所以對(duì)那些比較看重通知場(chǎng)景的應(yīng)用(如信息提醒類(lèi)),最穩(wěn)妥的做法或許是:
不適配Android O,保持TargetSDK在26以下
適配Android O,自己實(shí)現(xiàn)震動(dòng)、鈴聲;如微信、QQ
適配Android O,每次更新“聲音、振動(dòng)、呼吸燈、重要性”屬性時(shí),創(chuàng)建新的channelId
【參考材料】
NotificationChannel API 文檔:API NotificationChannel
NotificationChannel 設(shè)計(jì)說(shuō)明:Channels in Android O
NotificationChannel 視頻介紹(Youtube):Notification Updates in Android Oreo
NotificationChannel 視頻介紹(YouKu):Notification Updates in Android Oreo