NotificationChannel 的创建和使用

[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 的注意事项

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_notify

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 适配填坑指南

你可能感兴趣的:(NotificationChannel 的创建和使用)