NotificationChannel 適配填坑指南

重點(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)妥的做法或許是:

  1. 不適配Android O,保持TargetSDK在26以下

  2. 適配Android O,自己實(shí)現(xiàn)震動(dòng)、鈴聲;如微信、QQ

  3. 適配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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一,直觀(guān)來(lái)看 android-o上對(duì)通知做了更細(xì)粒度的管理。根據(jù)app的業(yè)務(wù)場(chǎng)景將通知分類(lèi)。用戶(hù)可以在設(shè)置中選擇接...
    笑羋閱讀 4,561評(píng)論 0 2
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,045評(píng)論 25 709
  • 我關(guān)注簡(jiǎn)書(shū)時(shí)間不長(zhǎng),也就一周的時(shí)間吧,但收獲卻很多,在這里我發(fā)過(guò)自己的文章,更多的是閱讀到了很多精彩實(shí)用的分享。我...
    釋若讀書(shū)閱讀 4,411評(píng)論 2 29
  • 【為愛(ài)朗讀21天】Day14 閱讀繪本《神奇洞洞書(shū)》 小盆友對(duì)這本洞洞書(shū)是真的很喜歡,看了一遍又一遍,自己沒(méi)事的時(shí)...
    煜寶陪你讀繪本閱讀 213評(píng)論 0 0
  • 有人在拍照 有人在給拍照的人拍照 你在窗前看風(fēng)景 風(fēng)景里的人在看你 人這一輩子,咋說(shuō),總是有人在給自己愛(ài)的人默默的...
    小明士閱讀 173評(píng)論 0 2

友情鏈接更多精彩內(nèi)容