1. 程式人生 > >Android O適配---NotificationChannel

Android O適配---NotificationChannel

一,直觀來看

android-o上對通知做了更細粒度的管理。根據app的業務場景將通知分類。使用者可以在設定中選擇接收自己感興趣的通知。同時也可以遮蔽不喜歡的通知。先上圖看看新特性。

入口:手機桌面長按(android 原生8.0)app圖示,右上角出現提示“應用資訊”,點選進入應用詳情。如下圖一:我們可以看見”應用通知“


圖一

點選應用通知進入應用通知管理詳情頁,如下圖二中類別所示:本次測試建立了三種NotificationChannel:下載,聊天,推送。每種NotificationChannel設定了不同的屬性,如:重要性,是否振動等等


圖二

點選圖二中的下載NotificationChannel,進入NotificationChannel詳情頁,如圖三,可以看到下載NotificationChannel的詳細資訊


圖三

總之,android-o上對通知的管理更加細粒度,通過”管道“對通知分類,使用者可以根據自己對通知的喜好選擇接收特定管道的通知。

二,從原始碼分析

一,關鍵類


1.NotificationManagerService

NotificationManagerService繼承SystemService,是系統級別的service.

NotificationManagerService在init初始化的時候會載入/data/system/notification_policy.xml中的通知資訊到記憶體資料結構,該檔案中儲存所有package註冊的所有NotificationChannel。如下內容為xml檔案部分內容。可以看到不同package下建立的channel以及channel相關屬性。

</package>
<package name="com.android.providers.downloads" show_badge="true" uid="1010013">
<channel id="active" name="下載中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="complete" name="下載完成" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="waiting" name="等待中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="insufficient_space" name="空間不足" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
</package>
<package name="com.android.providers.downloads" show_badge="true" uid="10013">
<channel id="active" name="下載中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="complete" name="下載完成" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="waiting" name="等待中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
<channel id="insufficient_space" name="空間不足" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" />
</package>

NotificationManagerService主要職能是對通知的管理和排程。具體實現不列舉了,本次只討論android-O上的新特性

2.NotificationChannel

android-o上app傳送的每個通知必須依附於一個channel.即,每個notification物件必須傳送到指定的NotificationChannel.如果找不到channel,就會報錯,如下

NotificationService: No Channel found for pkg=com.miui.securitycore,

channelId=testNotificationChannel, id=10000, tag=null, opPkg=com.miui.securitycore,

callingUid=1000, userId=10, incomingUserId=10,

notificationUid=1001000, notification=Notification(channel=testNotificationChannel pri=0

contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x2

color=0x00000000 vis=PRIVATE)

發通知:

1.建立Notification物件

        /**
         * Constructs a new Builder with the defaults:
         *
         * @param context
         *            A {@link Context} that will be used by the Builder to construct the
         *            RemoteViews. The Context will not be held past the lifetime of this Builder
         *            object.
         * @param channelId
         *            The constructed Notification will be posted on this
         *            {@link NotificationChannel}. To use a NotificationChannel, it must first be
         *            created using {@link NotificationManager#createNotificationChannel}.
         */
        public Builder(Context context, String channelId) {
            this(context, (Notification) null);
            mN.mChannelId = channelId;
        }

2.建立NotificationChannel物件(不必每次建立)

    /**
     * Creates a notification channel.
     *
     * @param id The id of the channel. Must be unique per package. The value may be truncated if
     *           it is too long.
     * @param name The user visible name of the channel. You can rename this channel when the system
     *             locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
     *             broadcast. The recommended maximum length is 40 characters; the value may be
     *             truncated if it is too long.
     * @param importance The importance of the channel. This controls how interruptive notifications
     *                   posted to this channel are.
     */
    public NotificationChannel(String id, CharSequence name, @Importance int importance) {
        this.mId = getTrimmedString(id);
        this.mName = name != null ? getTrimmedString(name.toString()) : null;
        this.mImportance = importance;
    }


3.傳送

    /**
     * @hide
     */
    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        // Fix the notification as best we can.
        Notification.addFieldsFromContext(mContext, notification);
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }
        }
        fixLegacySmallIcon(notification, pkg);
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, user.getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }


總結:1.channel有很多屬性可以設定。channel相當一個大的類別,該類別的通知具有相同特徵,如振動,閃燈等等

    @Override
    public String toString() {
        return "NotificationChannel{" +
                "mId='" + mId + '\'' +
                ", mName=" + mName +
                ", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") +
                ", mImportance=" + mImportance +
                ", mBypassDnd=" + mBypassDnd +
                ", mLockscreenVisibility=" + mLockscreenVisibility +
                ", mSound=" + mSound +
                ", mLights=" + mLights +
                ", mLightColor=" + mLightColor +
                ", mVibration=" + Arrays.toString(mVibration) +
                ", mUserLockedFields=" + mUserLockedFields +
                ", mVibrationEnabled=" + mVibrationEnabled +
                ", mShowBadge=" + mShowBadge +
                ", mDeleted=" + mDeleted +
                ", mGroup='" + mGroup + '\'' +
                ", mAudioAttributes=" + mAudioAttributes +
                ", mBlockableSystem=" + mBlockableSystem +
                '}';
    }

2.建立notification時候需要關聯channel,沒有channel會報錯。

    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) {
        if (DBG) {
            Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
                    + " notification=" + notification);
        }
        checkCallerIsSystemOrSameApp(pkg);

        final int userId = ActivityManager.handleIncomingUser(callingPid,
                callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
        final UserHandle user = new UserHandle(userId);

        if (pkg == null || notification == null) {
            throw new IllegalArgumentException("null not allowed: pkg=" + pkg
                    + " id=" + id + " notification=" + notification);
        }

        // The system can post notifications for any package, let us resolve that.
        final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);

        // Fix the notification as best we can.
        try {
            final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
                    pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                    (userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
            Notification.addFieldsFromContext(ai, notification);
        } catch (NameNotFoundException e) {
            Slog.e(TAG, "Cannot create a context for sending app", e);
            return;
        }

        mUsageStats.registerEnqueuedByApp(pkg);

        // 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;
        }

        final StatusBarNotification n = new StatusBarNotification(
                pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                user, null, System.currentTimeMillis());
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);

        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r)) {
            return;
        }

        // Whitelist pending intents.
        if (notification.allPendingIntents != null) {
            final int intentCount = notification.allPendingIntents.size();
            if (intentCount > 0) {
                final ActivityManagerInternal am = LocalServices
                        .getService(ActivityManagerInternal.class);
                final long duration = LocalServices.getService(
                        DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
                for (int i = 0; i < intentCount; i++) {
                    PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                    if (pendingIntent != null) {
                        am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
                                WHITELIST_TOKEN, duration);
                    }
                }
            }
        }

        mHandler.post(new EnqueueNotificationRunnable(userId, r));
    }

三,多使用者下的NotificationChannel

NotificationChannel不同使用者空間下是通過uid來區分的,所以在多使用者下系統在尋找channel時是以pkg + "|" + uid為key查詢的。具體過程見原始碼。
<package name="com.miui.securitycore" show_badge="true" uid="1000">
<channel id="com_miui_securitycore_channel_id" name="com_miui_securitycore_channel_name" importance="4" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" vibration_enabled="true" show_badge="true" />
</package>

<package name="com.miui.securitycore" show_badge="true" uid="1001000">
<channel id="com_miui_securitycore_channel_id" name="com_miui_securitycore_channel_name" importance="4" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" vibration_enabled="true" show_badge="true" />
</package>