Android 12.0 通知发送过程源码分析-Framework

以下NotificationManagerService简称 NMS

1. 通知的发送: NotificationManager.notify(int id, Notification notification) 开始.

源码路径: /frameworks/base/core/java/android/app/NotificationManager.java
 /**
     *发布通知以显示在状态栏中。 如果通知带有
     * 相同的 ID 已被您的应用程序发布且尚未被取消,它将被更新信息取代。
     *
     * @param id 此通知的标识符在您的系统中是唯一的应用。
     * @param notification  描述向用户显示的内容。 一定不为空。
     *        
     */
    public void notify(int id, Notification notification)
    {
        notify(null, id, notification);
    }

这里继续调用 notify(), 其中 tag = null;

源码路径: /frameworks/base/core/java/android/app/NotificationManager.java 
public void notify(String tag, int id, Notification notification)
    {
        notifyAsUser(tag, id, notification, mContext.getUser());
    }


 /**
     * @hide
     */
    @UnsupportedAppUsage
    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        INotificationManager service = getService();//获取binder对象,实现跨进程通信
        String pkg = mContext.getPackageName(); //获取发送通知应用的包名

        try {
            //跨进程调用,即调用NMS中的enqueueNotificationWithTag(),请看分析4
            //在这之前会先调用fixNotification()方法,提前做一些优化,请看分析2
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(),tag, id,fixNotification(notification), user.getIdentifier())

        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

2.  优化通知 , fixNotification(notification) 代码如下:

源码路径: /frameworks/base/core/java/android/app/NotificationManager.java
 private Notification fixNotification(Notification notification) {
        String pkg = mContext.getPackageName();
        //这里把ApplicationInfo保存到Notificaiton.extras参数中, 请看2.(1)
        Notification.addFieldsFromContext(mContext, notification);

        //如果设置了通知铃声,这里获取铃声的uri
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }

        }
        fixLegacySmallIcon(notification, pkg);//smallIcon版本兼容处理,请看2.(2)
        //Android 5.1 后要求必须设置setSmallIcon(),否则抛出异常
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }

        notification.reduceImageSizes(mContext);//按比例压缩图片,请看2.(3)
        return Builder.maybeCloneStrippedForDelivery(notification);
    }

(1) 保存ApplicationInfo 对象到通知中,addFieldsFromContext() ,源码如下:

源码路径: /frameworks/base/core/java/android/app/Notification.java 
/**
     * @hide
     */
    public static void addFieldsFromContext(Context context, Notification notification) {
        addFieldsFromContext(context.getApplicationInfo(), notification);
    }

    /**
     * @hide
     */
    public static void addFieldsFromContext(ApplicationInfo ai, Notification notification) {
        //保存ApplicationInfo对象到通知中,属性名为EXTRA_BUILDER_APPLICATION_INFO
        notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, ai);
    }

(2) smallIcon版本兼容处理 

老版本中定义的通知smallIcon为资源int型,新版本中换成Icon 类型,为了兼容旧版本,这里做了转换,即把 int 型转化成Icon型,并设置到通知中,fixLegacySmallIcon(notification, pkg)源码如下:

源码路径: /frameworks/base/core/java/android/app/NotificationManager.java
private void fixLegacySmallIcon(Notification n, String pkg) {
        if (n.getSmallIcon() == null && n.icon != 0) {

           //n.setSmallIcon(Icon icon), 而 n.icon 为 int 型,这里调用了createWithResource()转换
            n.setSmallIcon(Icon.createWithResource(pkg, n.icon));
        }
    }



源码路径: frameworks/base/graphics/java/android/graphics/drawable/Icon.java

 /**
     * 创建Icon对象
     * @param resPackage 包名
     * @param resId 资源ID
     */
    public static Icon createWithResource(String resPackage, @DrawableRes int resId) {
        if (resPackage == null) {
            throw new IllegalArgumentException("Resource package name must not be null.");
        }
        final Icon rep = new Icon(TYPE_RESOURCE);
        rep.mInt1 = resId;
        rep.mString1 = resPackage;
        return rep;
    }

(3) 压缩图片 reduceImageSizes(mContext)

源码如下:

源码路径: /frameworks/base/core/java/android/app/Notification.java
/**
     * 把图片缩小成给定的尺寸
     * @hide
     */
    void reduceImageSizes(Context context) {
        if (extras.getBoolean(EXTRA_REDUCED_IMAGES)) {
            return;
        }
        boolean isLowRam = ActivityManager.isLowRamDeviceStatic();//判断设备是否为低内存
        if (mLargeIcon != null || largeIcon != null) {
            Resources resources = context.getResources();
            Class<? extends Style> style = getNotificationStyle();
            //不管是否为低内存,maxSize=48dp,源码定义在:/frameworks/base/core/res/res/values/dimens.xml
            int maxSize = resources.getDimensionPixelSize(isLowRam
                    ? R.dimen.notification_right_icon_size_low_ram
                    : R.dimen.notification_right_icon_size);
            if (mLargeIcon != null) {
                //压缩图片
                mLargeIcon.scaleDownIfNecessary(maxSize, maxSize);
            }
            if (largeIcon != null) {
               //压缩图片
                largeIcon = Icon.scaleDownIfNecessary(largeIcon, maxSize, maxSize);
            }
        }
        //对RemotView中的图片按规定的尺寸进行压缩
        reduceImageSizesForRemoteView(contentView, context, isLowRam);
        reduceImageSizesForRemoteView(headsUpContentView, context, isLowRam);
        reduceImageSizesForRemoteView(bigContentView, context, isLowRam);
        extras.putBoolean(EXTRA_REDUCED_IMAGES, true);
    }


/**
 *对RemotView中的图片按规定的尺寸进行压缩
*/
 private void reduceImageSizesForRemoteView(RemoteViews remoteView, Context context,
            boolean isLowRam) {
        if (remoteView != null) {
            Resources resources = context.getResources();
            int maxWidth = resources.getDimensionPixelSize(isLowRam
                    ? R.dimen.notification_custom_view_max_image_width_low_ram //294dp
                    : R.dimen.notification_custom_view_max_image_width);//450dp
            int maxHeight = resources.getDimensionPixelSize(isLowRam
                    ? R.dimen.notification_custom_view_max_image_height_low_ram //208dp
                    : R.dimen.notification_custom_view_max_image_height); //284dp
            remoteView.reduceImageSizes(maxWidth, maxHeight);
        }
    }



源码路径: frameworks/base/graphics/java/android/graphics/drawable/Icon.java

/**
     * 将位图缩小到给定的最大宽度和最大高度。 缩放将以统一的方式完成
     * @param bitmap 要缩小的位图
     * @param maxWidth 允许的最大宽度
     * @param maxHeight 允许的最大高度
     *
     * @如果需要则返回缩放后的位图,如果不需要缩放则返回原始位图
     * @hide
     */
    public static Bitmap scaleDownIfNecessary(Bitmap bitmap, int maxWidth, int maxHeight) {
        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
            float scale = Math.min((float) maxWidth / bitmapWidth,
                    (float) maxHeight / bitmapHeight);
            bitmap = Bitmap.createScaledBitmap(bitmap,
                    Math.max(1, (int) (scale * bitmapWidth)),
                    Math.max(1, (int) (scale * bitmapHeight)),
                    true /* filter */);
        }
        return bitmap;
    }

以上只是列举了压缩largeIcon 的例子,Notificaiton.java中,针对通知中的各种图片都做个指定尺寸的压缩.通知前期的优化完毕,继续看通知在NMS中的处理.

3. NMS 中保存通知的一些数据结构说明

源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

    // 服务端维护的 已排序 的通知
    final ArrayList<NotificationRecord> mNotificationList = new ArrayList<>();
    
    // 服务端维护的 未排序 的通知
    final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<>();
    
    // 入队通知: 保存所有入队的通知,当通知成功发送后则移除,即该列表记录的是所有入队成功且没有被发送出去的通知
    final ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>();
   
    // 维护系统自动成组后的父通知
    final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
 
   // 服务端根据groupKey,维护着所有用户主动成组的父通知 
   final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();

4. 通知到达enqueueNotificationWithTag()@NMS

enqueueNotificationWithTag()里调用了 enqueueNotificationInternal(),所以直接从enqueueNotificationInternal()开始学习,源码如下:

(1) enqueueNotificationInternal()

 源码路径: frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

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, boolean postSilently) {
                   
                      ......

        checkRestrictedCategories(notification);//检查通知是否属于仅限系统使用的类别类型,

        // 优化通知,请看4.(2)
        try {
            fixNotification(notification, pkg, tag, id, userId);
        } catch (Exception e) {
            if (notification.isForegroundService()) {
                throw new SecurityException("Invalid FGS notification", e);
            }
            Slog.e(TAG, "Cannot fix notification", e);
            return;
        }

        // 检查setForegroundService()是否有FLAG_FOREGROUND_SERVICE权限
        
        final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification(
                notification, tag, id, pkg, userId);
        if (policy == ServiceNotificationPolicy.UPDATE_ONLY) {
            if (!isNotificationShownInternal(pkg, tag, id, userId)) {
                reportForegroundServiceUpdate(false, notification, id, pkg, userId);
                return;
            }
        }

        mUsageStats.registerEnqueuedByApp(pkg);

        //把通知封装成StatusBarNotification对象,即一条通知对应一个StatusBarNotification对象,主要面对App端
        final StatusBarNotification n = new StatusBarNotification(
                pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                user, null, System.currentTimeMillis());

        // 创建channelId,
        String channelId = notification.getChannelId();
        if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
            channelId = (new Notification.TvExtender(notification)).getChannelId();
        }
        String shortcutId = n.getShortcutId();

       //Android8.0之后就需要为通知设置Channel,这里做了判断,否则无法发送通知

        final NotificationChannel channel = mPreferencesHelper.getConversationNotificationChannel(
                pkg, notificationUid, channelId, shortcutId,
                true /* parent ok */, 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;
            Slog.e(TAG, noChannelStr);

            //获取通知的重要性
            boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
                    == NotificationManager.IMPORTANCE_NONE;

            if (!appNotificationsOff) {
                doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
                        "Failed to post notification on channel \"" + channelId + "\"\n" +
                        "See log for more details");
            }
            return;
        }
      
        //把通知封装成NotificationRecord对象,即一条通知就是一个NotificationRecord对象,主要面对Service端
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
        r.setIsAppImportanceLocked(mPreferencesHelper.getIsAppImportanceLocked(pkg, callingUid));
        r.setPostSilently(postSilently);
        r.setFlagBubbleRemoved(false);
        r.setPkgAllowedAsConvo(mMsgPkgsAllowedAsConvos.contains(pkg));

        if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
            final boolean fgServiceShown = channel.isFgServiceShown();
            if (((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0
                        || !fgServiceShown)
                    && (r.getImportance() == IMPORTANCE_MIN
                            || r.getImportance() == IMPORTANCE_NONE)) {
                //提高通知的重要性
                if (TextUtils.isEmpty(channelId)
                        || NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {
                    r.setSystemImportance(IMPORTANCE_LOW);
                } else {
                    channel.setImportance(IMPORTANCE_LOW);
                    r.setSystemImportance(IMPORTANCE_LOW);
                    if (!fgServiceShown) {
                        channel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
                        channel.setFgServiceShown(true);
                    }
                    mPreferencesHelper.updateNotificationChannel(
                            pkg, notificationUid, channel, false);
                    r.updateNotificationChannel(channel);
                }
            } else if (!fgServiceShown && !TextUtils.isEmpty(channelId)
                    && !NotificationChannel.DEFAULT_CHANNEL_ID.equals(channelId)) {
                channel.setFgServiceShown(true);
                r.updateNotificationChannel(channel);
            }
        }

        ShortcutInfo info = mShortcutHelper != null
                ? mShortcutHelper.getValidShortcutInfo(notification.getShortcutId(), pkg, user)
                : null;
        if (notification.getShortcutId() != null && info == null) {
            Slog.w(TAG, "notification " + r.getKey() + " added an invalid shortcut");
        }
        r.setShortcutInfo(info);
        r.setHasSentValidMsg(mPreferencesHelper.hasSentValidMsg(pkg, notificationUid));
        r.userDemotedAppFromConvoSpace(
                mPreferencesHelper.hasUserDemotedInvalidMsgApp(pkg, notificationUid));

       //进一步过滤不符合规定的通知,限制通知速率和通知数量,请看4.(3)
        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
                r.getSbn().getOverrideGroupKey() != null)) {
            return;
        }

        if (info != null) {
            // 缓存快捷方式
            mShortcutHelper.cacheShortcut(info, user);
        }

        // 暂时允许应用程序在启动待处理意图时执行额外的工作,
        if (notification.allPendingIntents != null) {
            final int intentCount = notification.allPendingIntents.size();
            if (intentCount > 0) {
                final long duration = LocalServices.getService(
                        DeviceIdleInternal.class).getNotificationAllowlistDuration();
                for (int i = 0; i < intentCount; i++) {
                    PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                    if (pendingIntent != null) {
                        mAmi.setPendingIntentAllowlistDuration(pendingIntent.getTarget(),
                                ALLOWLIST_TOKEN, duration,
                                TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                                REASON_NOTIFICATION_SERVICE,
                                "NotificationManagerService");
                        mAmi.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
                                ALLOWLIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER
                                        | FLAG_SERVICE_SENDER));
                    }
                }
            }
        }

        // 需要升级权限才能获得包重要性
        final long token = Binder.clearCallingIdentity();
        boolean isAppForeground;
        try {
            isAppForeground = mActivityManager.getPackageImportance(pkg) == IMPORTANCE_FOREGROUND;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
        //经过上面的一步一步过滤后,现在通知post到线程里,请看5分析
        mHandler.post(new EnqueueNotificationRunnable(userId, r, isAppForeground));
    }

  

(2) 第二次优化通知, fixNotification(notification, pkg, tag, id, userId)

 源码路径: frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
 
protected void fixNotification(Notification notification, String pkg, String tag, int id,
            int userId) throws NameNotFoundException {
        final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
                pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
       //保存ApplicationInfo对象,请看分析2.(2)
        Notification.addFieldsFromContext(ai, notification);

        //检查权限,通知是否能着色,即通知中的 setColorized(boolean)
        int canColorize = mPackageManagerClient.checkPermission(
                android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
        if (canColorize == PERMISSION_GRANTED) {
            notification.flags |= Notification.FLAG_CAN_COLORIZE;
        } else {
            notification.flags &= ~Notification.FLAG_CAN_COLORIZE;
        }

        //检查全屏通知的权限,如果在Android Q(29)及以上给通知设置了fullScreenIntent,同时还
        //需要设置android.Manifest.permission.USE_FULL_SCREEN_INTENT权限,否则通知的
        //fullScreenIntent将被系统始终为null,即无效      
        if (notification.fullScreenIntent != null && ai.targetSdkVersion >= Build.VERSION_CODES.Q) {
            int fullscreenIntentPermission = mPackageManagerClient.checkPermission(
                    android.Manifest.permission.USE_FULL_SCREEN_INTENT, pkg);
            if (fullscreenIntentPermission != PERMISSION_GRANTED) {
                //权限不足,该属性设置为null
                notification.fullScreenIntent = null;
                //fullScreenIntent无效日志
                Slog.w(TAG, "Package " + pkg +
                        ": Use of fullScreenIntent requires the USE_FULL_SCREEN_INTENT permission");
            }
        }

        // 检查 Style 样式中的action事件
        if (notification.isStyle(Notification.CallStyle.class)) {
            Notification.Builder builder =
                    Notification.Builder.recoverBuilder(getContext(), notification);
            Notification.CallStyle style = (Notification.CallStyle) builder.getStyle();
            List<Notification.Action> actions = style.getActionsListWithSystemActions();
            notification.actions = new Notification.Action[actions.size()];
            actions.toArray(notification.actions);
        }

        // 检查RemoteView中的contentView,bigcontentView,headsUpContentView等是否超过
        checkRemoteViews(pkg, tag, id, notification);
    }


/**
 * 检查RemouteView 的大小,是否超过了指定的大小
*/

 private boolean removeRemoteView(String pkg, String tag, int id, RemoteViews contentView) {
        if (contentView == null) {
            return false;
        }
        //获取当前RemoteView的大小
        final int contentViewSize = contentView.estimateMemoryUsage();
        //其中 mWarnRemoteViewsSizeBytes = 2000000 bytes , mStripRemoteViewsSizeBytes = 5000000 bytes
        if (contentViewSize > mWarnRemoteViewsSizeBytes
                && contentViewSize < mStripRemoteViewsSizeBytes) {
            Slog.w(TAG, "RemoteViews too large on pkg: " + pkg + " tag: " + tag + " id: " + id
                    + " this might be stripped in a future release");
        }
         // contentViewSize >= 5000000 bytes
        if (contentViewSize >= mStripRemoteViewsSizeBytes) {
            mUsageStats.registerImageRemoved(pkg);
            Slog.w(TAG, "Removed too large RemoteViews (" + contentViewSize + " bytes) on pkg: "
                    + pkg + " tag: " + tag + " id: " + id);
            return true;
        }
        return false;
    }

(3) 限制通知速率和通知数量: checkDisqualifyingFeatures()

 源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
涉及的源码路径:
速率的计算: /frameworks/base/services/core/java/com/android/server/notification/RateEstimator.java
保存不发送的通知:/frameworks/base/services/core/java/com/android/server/notification/NotificationUsageStats.java


 /**
     * 检查是否可以发布通知。 检查速率限制器、暂停助手和阻止。
     * 如果通知检查不合格,则返回 false,
     * 应用速率不能超过5000毫秒,通知总数不能超过50条
     */
    boolean checkDisqualifyingFeatures(int userId, int uid, int id, String tag,
            NotificationRecord r, boolean isAutogroup) {
        Notification n = r.getNotification();
        final String pkg = r.getSbn().getPackageName();
        //是否为系统通知
        final boolean isSystemNotification =
                isUidSystemOrPhone(uid) || ("android".equals(pkg));
        //是否为通知监听器
        final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg);

        // 限制除 android 之外的任何给定包的通知数量
        if (!isSystemNotification && !isNotificationFromListener) {
            final int callingUid = Binder.getCallingUid();
            if (mNotificationsByKey.get(r.getSbn().getKey()) == null
                    && isCallerInstantApp(callingUid, userId)) {
                
                 // 临时应用程序对通知有一些特殊的限制。
                 // 他们不被允许创建新的通知,但是他们被允许
                 // 更新系统创建的通知(例如前台服务通知)。
                throw new SecurityException("Instant app " + pkg
                        + " cannot create notifications");
            }

            //限制更新未完成进度通知(即:进度条通知还在更新进度,当前速度还未达到最大值)的速率,
            if (mNotificationsByKey.get(r.getSbn().getKey()) != null
                    && !r.getNotification().hasCompletedProgress()
                    && !isAutogroup) {
                //算出这条通知距离上一个通知的时间差,然后算出速率,过程请看下面文字分析
                final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                //如果这个通知的速率大于规定的最大值,其中mMaxPackageEnqueueRate=5f
                if (appEnqueueRate > mMaxPackageEnqueueRate) {
                    //把违规超速率的通知数量做好统计,保存在NotificationUsageStats.java中
                    mUsageStats.registerOverRateQuota(pkg);
                    final long now = SystemClock.elapsedRealtime();
                   //这条通知的时间-上条通知的时间 > 5000 毫秒
                    if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
                        Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
                                + ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
                        mLastOverRateLogTime = now;
                    }
                    return false;//速率不合格,直接返回false
                }
            }

            // 限制应用程序可以拥有的非前台服务 未完成通知记录的数量
            if (!n.isForegroundService()) {
                //计算应用通知的总数,该总数:发送成功的通知+发送不成功的通知
                int count = getNotificationCount(pkg, userId, id, tag);
                // 应用总通知数 >= 50 条
                if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                   //把超出总数的通知保存在NotificationUsageStats.java中
                    mUsageStats.registerOverCountQuota(pkg);
                    Slog.e(TAG, "Package has already posted or enqueued " + count
                            + " notifications.  Not showing more.  package=" + pkg);
                    return false;//通知总数不合格,直接返回false
                }
            }
        }

        // 气泡或内联回复是不可变的?
        if (n.getBubbleMetadata() != null
                && n.getBubbleMetadata().getIntent() != null
                && hasFlag(mAmi.getPendingIntentFlags(
                        n.getBubbleMetadata().getIntent().getTarget()),
                        PendingIntent.FLAG_IMMUTABLE)) {
            throw new IllegalArgumentException(r.getKey() + " Not posted."
                    + " PendingIntents attached to bubbles must be mutable");
        }

        if (n.actions != null) {
            for (Notification.Action action : n.actions) {
                if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
                        && hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
                        PendingIntent.FLAG_IMMUTABLE)) {
                    throw new IllegalArgumentException(r.getKey() + " Not posted."
                            + " PendingIntents attached to actions with remote"
                            + " inputs must be mutable");
                }
            }
        }

        if (r.getSystemGeneratedSmartActions() != null) {
            for (Notification.Action action : r.getSystemGeneratedSmartActions()) {
                if ((action.getRemoteInputs() != null || action.getDataOnlyRemoteInputs() != null)
                        && hasFlag(mAmi.getPendingIntentFlags(action.actionIntent.getTarget()),
                        PendingIntent.FLAG_IMMUTABLE)) {
                    throw new IllegalArgumentException(r.getKey() + " Not posted."
                            + " PendingIntents attached to contextual actions with remote inputs"
                            + " must be mutable");
                }
            }
        }

        if (n.isStyle(Notification.CallStyle.class)) {
            boolean isForegroundService = (n.flags & FLAG_FOREGROUND_SERVICE) != 0;
            boolean hasFullScreenIntent = n.fullScreenIntent != null;
            if (!isForegroundService && !hasFullScreenIntent) {
                throw new IllegalArgumentException(r.getKey() + " Not posted."
                        + " CallStyle notifications must either be for a foreground Service or"
                        + " use a fullScreenIntent.");
            }
        }

        // 不发送snoozed类型的通知,当用户在设置中设置了不允许显示某个应用的通知(blocked)时,不再发送
        if (mSnoozeHelper.isSnoozed(userId, pkg, r.getKey())) {
            MetricsLogger.action(r.getLogMaker()
                    .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
                    .setCategory(MetricsProto.MetricsEvent.NOTIFICATION_SNOOZED));
            mNotificationRecordLogger.log(
                    NotificationRecordLogger.NotificationEvent.NOTIFICATION_NOT_POSTED_SNOOZED,
                    r);
            if (DBG) {
                Slog.d(TAG, "Ignored enqueue for snoozed notification " + r.getKey());
            }
            mSnoozeHelper.update(userId, r);
            handleSavePolicyFile();
            return false;
        }


        // blocked apps
        if (isBlocked(r, mUsageStats)) {
            return false;
        }

        return true;
    }

5 . EnqueueNotificationRunnable@NMS

到此,通知经过优化后,最终进入到线程,下面是该线程的run() 方法 ,源码如下:

 源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java 
public void run() {
            synchronized (mNotificationLock) {
                final Long snoozeAt =
                        mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
                                r.getUser().getIdentifier(),
                                r.getSbn().getPackageName(), r.getSbn().getKey());
                final long currentTime = System.currentTimeMillis();
                if (snoozeAt.longValue() > currentTime) {
                    (new SnoozeNotificationRunnable(r.getSbn().getKey(),
                            snoozeAt.longValue() - currentTime, null)).snoozeLocked(r);
                    return;
                }

                final String contextId =
                        mSnoozeHelper.getSnoozeContextForUnpostedNotification(
                                r.getUser().getIdentifier(),
                                r.getSbn().getPackageName(), r.getSbn().getKey());
                if (contextId != null) {
                    (new SnoozeNotificationRunnable(r.getSbn().getKey(),
                            0, contextId)).snoozeLocked(r);
                    return;
                }
         
               //把通知添加到List中,即入队通知,指入队但未发送出去的通知,分析请看3
                mEnqueuedNotifications.add(r);
                scheduleTimeoutLocked(r);

                final StatusBarNotification n = r.getSbn();
                
                NotificationRecord old = mNotificationsByKey.get(n.getKey());
                //查看通知List中,是否已经存在该通知(通知的唯一标识为key),
                if (old != null) {
                    // 保留以前记录的排名信息
                    r.copyRankingInformation(old);
                }
                //表明该通知之前不存在,是一个新的通知,
                final int callingUid = n.getUid();
                final int callingPid = n.getInitialPid();
                final Notification notification = n.getNotification();
                final String pkg = n.getPackageName();
                final int id = n.getId();
                final String tag = n.getTag();

                // 更新气泡通知
                updateNotificationBubbleFlags(r, isAppForeground);

                // 处理分组通知,详细介绍请看分析9
                handleGroupedNotificationLocked(r, old, callingUid, callingPid);

                if (n.isGroup() && notification.isGroupChild()) {
                    mSnoozeHelper.repostGroupSummary(pkg, r.getUserId(), n.getGroupKey());
                }

               
                if (mAssistants.isEnabled()) {
                    mAssistants.onNotificationEnqueuedLocked(r);
                    //处理完之后,延迟post到PostNotificationRunnable线程,请看分析6
                    mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                            DELAY_FOR_ASSISTANT_TIME);
                } else {
                   //处理完之后,post到PostNotificationRunnable线程,请看分析6
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
        }
    }

上面是把通知添加到 ArrayList<NotificationRecord> mEnqueuedNotifications 列表中,该列表保存了所有待处理的通知,如果通知被取消、超时、处理完成后也会从该列表移除.

6. PostNotificationRunnable@NMS

(1) 继续分析 PostNotificationRunnable 的 run() 方法,该方法主要是通知发送前的一些处理,

 源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
 public void run() {
            synchronized (mNotificationLock) {
                try {
                    NotificationRecord r = null;
                    int N = mEnqueuedNotifications.size();
                   //遍历待处理通知列表,如果传递过来的key能在列表中存在,则把通知赋值给r,
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            r = enqueued;
                            break;
                        }
                    }
                   //如果列表中不存在该key通知,就return
                    if (r == null) {
                        return;
                    }
                   //如果用户设置了不接收该通知,也return 
                    if (isBlocked(r)) {
                        return;
                    }
                   //判断应用是否被系统限制了,即应用程序当前是否已暂停。
                    final boolean isPackageSuspended =
                            isPackagePausedOrSuspended(r.getSbn().getPackageName(), r.getUid());
                    r.setHidden(isPackageSuspended);
                    if (isPackageSuspended) {
                        //统计被限制的通知的数量
                        mUsageStats.registerSuspendedByAdmin(r);
                    }
                    NotificationRecord old = mNotificationsByKey.get(key);
                    final StatusBarNotification n = r.getSbn();
                    final Notification notification = n.getNotification();

                    if (old == null || old.getSbn().getInstanceId() == null) {
                        n.setInstanceId(mNotificationInstanceIdSequence.newInstanceId());
                    } else {
                        n.setInstanceId(old.getSbn().getInstanceId());
                    }
                    
                    //判断通知是新的,还是已存在的通知,主要是通过遍历 待处理通知列表,如果存在则返回通知在列表的位置,如果是新的通知,则返回-1
                    int index = indexOfNotificationLocked(n.getKey());
                    if (index < 0) {
                       //将新的通知添加到 mNotificaitonList 列表中,
                        mNotificationList.add(r);
                        mUsageStats.registerPostedByApp(r);
                        r.setInterruptive(isVisuallyInterruptive(null, r));
                    } else {
                       //如果已存在该通知,则更新已存在的通知,即更新通知内容,key值不变,通知排序也不变
                        old = mNotificationList.get(index);  
                        mNotificationList.set(index, r);
                        mUsageStats.registerUpdatedByApp(r, old);
                        //确保通知更新过程中前台服务标志丢失
                        notification.flags |=
                                old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                        r.isUpdate = true;
                        final boolean isInterruptive = isVisuallyInterruptive(old, r);
                        r.setTextChanged(isInterruptive);
                        r.setInterruptive(isInterruptive);
                    }
                    //把通知添加到 列表中,这个列表在后面有说明
                    mNotificationsByKey.put(n.getKey(), r);

                    //如果是前台服务通知,不管应用是否设置常驻标志,系统都会强制加上FLAG_ONGOING_EVENT(常驻通知) 和 FLAG_NO_CLEAR(用户手动无法清除) 标志,
                    if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
                        notification.flags |= FLAG_ONGOING_EVENT
                                | FLAG_NO_CLEAR;
                    }

                    mRankingHelper.extractSignals(r);
                    mRankingHelper.sort(mNotificationList);
                    final int position = mRankingHelper.indexOf(mNotificationList, r);

                    int buzzBeepBlinkLoggingCode = 0;
                    if (!r.isHidden()) {
                       //处理通知的震动,音效和呼吸灯
                        buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
                    }

                    if (notification.getSmallIcon() != null) {
                        StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
                       //*****发送通知,通知各个listeners,其中就包括了SystemUI,详情请看分析7
                        mListeners.notifyPostedLocked(r, old);
                        if ((oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup()))
                                && !isCritical(r)) {
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() { 
                                    //构建父通知
                                    mGroupHelper.onNotificationPosted(
                                            n, hasAutoGroupSummaryLocked(n));
                                }
                            });
                        } else if (oldSbn != null) {
                            final NotificationRecord finalRecord = r;
                            mHandler.post(() -> mGroupHelper.onNotificationUpdated(
                                    finalRecord.getSbn(), hasAutoGroupSummaryLocked(n)));
                        }
                    } else {
                       //由于没有设置smallIcon,通知无法发送,通知listeners移除该通知.
                        if (old != null && !old.isCanceled) {
                            mListeners.notifyRemovedLocked(r,
                                    NotificationListenerService.REASON_ERROR, r.getStats());
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationRemoved(n);
                                }
                            });
                        }
                    }

                    if (mShortcutHelper != null) {
                        mShortcutHelper.maybeListenForShortcutChangesForBubbles(r,
                                false /* isRemoved */,
                                mHandler);
                    }

                    maybeRecordInterruptionLocked(r);
                    maybeRegisterMessageSent(r);
                    maybeReportForegroundServiceUpdate(r, true);
                } finally {
                   //该通知已被处理,应该把该通知从 待处理通知列表中移除
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
    

        这里从待处理通知 ArrayList<Notification> mEnqueuednotifications 取出通知,经过一些列步骤,之后把该通知添加到列表 ArrayMap<String,NotificationRecord> mNotificationsByKey 中, 该列表保存了服务端中未排序的所有通知,用于确定该通知是更新旧通知还是新类型的通知.最后, mListeners.notifyPostedLocked(r, old); 通知各个监听通知的listeners 通知更新了, 其中 mListeners 指 NotificationListeners, 它是NotificationManagerService的内部类,下面继续分析.

7. 通知监听者,通知发生变化: mListeners.notifyPostedLocked() 

源码路径: /frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java


private NotificationListeners mListeners; 

 
public class NotificationListeners extends ManagedServices {

                      ......

 private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
                boolean notifyAllListeners) {
   try {
        StatusBarNotification sbn = r.getSbn();
        StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
        TrimCache trimCache = new TrimCache(sbn);

        //过滤部分listener,如:不可见用户,Android P 以下hidden类型的通知 
         for (final ManagedServiceInfo info : getServices()) {
             boolean sbnVisible = isVisibleToListener(sbn, r. getNotificationType(), info);
             boolean oldSbnVisible = (oldSbn != null)
                    && isVisibleToListener(oldSbn, old.getNotificationType(), info);
       //如果通知不可见,则忽略
        if (!oldSbnVisible && !sbnVisible) {
             continue;
         }
       
         if (r.isHidden() && info.targetSdkVersion < Build.VERSION_CODES.P) {
            continue;
          }

         //过滤不通知所有监听者,并且版本大于Android P 
        if (!notifyAllListeners && info.targetSdkVersion >= Build.VERSION_CODES.P) {
                        continue;
        }

        //构建通知映射表,分析请看分析8
        final NotificationRankingUpdate update = makeRankingUpdateLocked(info);

        // 移除旧以前可见,现在不可见的通知
                    if (oldSbnVisible && !sbnVisible) {
                        final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                        mHandler.post(() -> notifyRemoved(
                                info, oldSbnLightClone, update, null, REASON_USER_STOPPED));
                        continue;
                    }

                    //授权
                    final int targetUserId = (info.userid == UserHandle.USER_ALL)
                            ? UserHandle.USER_SYSTEM : info.userid;
                    updateUriPermissions(r, old, info.component.getPackageName(), targetUserId);

                    final StatusBarNotification sbnToPost = trimCache.ForListener(info);
                    //通知各个监听器,之后各个监听器就能收到通知,并对通知做处理了
                    mHandler.post(() -> notifyPosted(info, sbnToPost, update));
                }
            } catch (Exception e) {
                Slog.e(TAG, "Could not notify listeners for " + r.getKey(), e);
            }
        }

到此, 通过  mHandler.post(() -> notifyPosted(info, sbnToPost, update)) 方法将通知传递到各个监听器,其中,在发送通知给监听器之前,会对通知进行排序,然后构建通知Map, SystemUI 会根据这个map 对通知进行排序.

8. 通知发送前对通知进行排序

    /**
     * 仅对监听器可见的通知进行排序,构建通知map,
     * key = StatusBarNotification.getKey();
     * value = NotificationListenerService.Ranking
     */
    @GuardedBy("mNotificationLock")
    NotificationRankingUpdate makeRankingUpdateLocked(ManagedServiceInfo info) {
        final int N = mNotificationList.size();
        final ArrayList<NotificationListenerService.Ranking> rankings = new ArrayList<>();

        for (int i = 0; i < N; i++) {
            NotificationRecord record = mNotificationList.get(i);
            if (isInLockDownMode(record.getUser().getIdentifier())) {
                continue;
            }
            //过滤掉当前用户不可见的通知
            if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) {
                continue;
            }
            //获取通知关键字key
            final String key = record.getSbn().getKey();
            //根据每个关键字对应一个 NotificationListenerService.Ranking, 即构成通知ArrayMap
            final NotificationListenerService.Ranking ranking =
                    new NotificationListenerService.Ranking();
            //将通知的关键信息添加到ranking中
            ranking.populate(
                    key,
                    rankings.size(),
                    !record.isIntercepted(),
                    record.getPackageVisibilityOverride(),
                    record.getSuppressedVisualEffects(),
                    record.getImportance(),
                    record.getImportanceExplanation(),
                    record.getSbn().getOverrideGroupKey(),
                    record.getChannel(),
                    record.getPeopleOverride(),
                    record.getSnoozeCriteria(),
                    record.canShowBadge(),
                    record.getUserSentiment(),
                    record.isHidden(),
                    record.getLastAudiblyAlertedMs(),
                    record.getSound() != null || record.getVibration() != null,
                    record.getSystemGeneratedSmartActions(),
                    record.getSmartReplies(),
                    record.canBubble(),
                    record.isTextChanged(),
                    record.isConversation(),
                    record.getShortcutInfo(),
                    record.getRankingScore() == 0
                            ? RANKING_UNCHANGED
                            : (record.getRankingScore() > 0 ?  RANKING_PROMOTED : RANKING_DEMOTED),
                    record.getNotification().isBubbleNotification(),
                    record.getProposedImportance()
            );
            rankings.add(ranking);
        }

        return new NotificationRankingUpdate(
                rankings.toArray(new NotificationListenerService.Ranking[0]));
    }

9. 通知的分组

通知组简介

继续分析 4标题中 handleGroupedNotificationLocked() 系统处理分组的源码如下:

    /**
     * 确保分组通知得到特殊处理
     *
     * 如果新通知导致组丢失其摘要,则取消组子项。
     *
     * <p>Updates mSummaryByGroupKey.</p>
     */
    @GuardedBy("mNotificationLock")
    private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old,
            int callingUid, int callingPid) {
        StatusBarNotification sbn = r.getSbn();
        Notification n = sbn.getNotification();
        if (n.isGroupSummary() && !sbn.isAppGroup())  {
            // 没有组的通知不应该是摘要,否则自动成组可能会导致错误,分析请看9.(1)
            n.flags &= ~Notification.FLAG_GROUP_SUMMARY;
        }

        String group = sbn.getGroupKey();
        boolean isSummary = n.isGroupSummary();

        Notification oldN = old != null ? old.getSbn().getNotification() : null;
        String oldGroup = old != null ? old.getSbn().getGroupKey() : null;
        boolean oldIsSummary = old != null && oldN.isGroupSummary();
        //更新 mSummaryByGroupKey,分析请看3
        if (oldIsSummary) {
            NotificationRecord removedSummary = mSummaryByGroupKey.remove(oldGroup);
            if (removedSummary != old) {
                String removedKey =
                        removedSummary != null ? removedSummary.getKey() : "<null>";
                Slog.w(TAG, "Removed summary didn't match old notification: old=" + old.getKey() +
                        ", removed=" + removedKey);
            }
        }
        if (isSummary) {
            mSummaryByGroupKey.put(group, r);
        }

        FlagChecker childrenFlagChecker = (flags) -> {
            if ((flags & FLAG_FOREGROUND_SERVICE) != 0) {
                return false;
            }
            return true;
        };

        // 如果更新导致组摘要消失,则清除旧通知的组子项。当旧通知是摘要而新通知不是摘要时,
        // 或者当旧通知是摘要并且其groupKey发生更改时,则原来父通知下的所有子通知会被移除
        if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
            cancelGroupChildrenLocked(old, callingUid, callingPid, null, false /* sendDelete */,
                    childrenFlagChecker, REASON_APP_CANCEL, SystemClock.elapsedRealtime());
        }
    }

(1) 如果 setGroupSummary(boolean isGroupSummary)设置了Notification.FLAG_GROUP_SUMMARY这个flag,但是没有调用setGroup(String groupKey)设置对应的groupKey, 则Notification.FLAG_GROUP_SUMMARY这个flag会被去掉,否则会导致后续系统的自动成组导致出错。

10. 使用规则更新通知属性值(排序前更新)

源码路径:frameworks/base/services/core/java/com/android/server/notification/RankingConfig.java

public interface RankingConfig {

    void setImportance(String packageName, int uid, int importance);
    int getImportance(String packageName, int uid);
    void setShowBadge(String packageName, int uid, boolean showBadge);
    boolean canShowBadge(String packageName, int uid);
    boolean badgingEnabled(UserHandle userHandle);
    int getBubblePreference(String packageName, int uid);
    boolean bubblesEnabled(UserHandle userHandle);
    boolean isMediaNotificationFilteringEnabled();
    boolean isGroupBlocked(String packageName, int uid, String groupId);
    boolean canShowNotificationsOnLockscreen(int userId);
    boolean canShowPrivateNotificationsOnLockScreen(int userId);

    Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,int uid);

    void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,boolean fromTargetApp);

    ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty);

    boolean createNotificationChannel(String pkg, int uid, NotificationChannel channel, boolean fromTargetApp, boolean hasDndAccess);

    void updateNotificationChannel(String pkg, int uid, NotificationChannel channel,
            boolean fromUser);

    NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
            boolean includeDeleted);

    NotificationChannel getConversationNotificationChannel(String pkg, int uid, String channelId, String conversationId,  boolean returnParentIfNoConversationChannel,boolean includeDeleted);

    boolean deleteNotificationChannel(String pkg, int uid, String channelId);

    void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId);

    void permanentlyDeleteNotificationChannels(String pkg, int uid);

    ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,boolean includeDeleted);
}

上面是规则接口类,下面分析该接口的实现类,举例通知圆点进行说明:

源码路径: frameworks/base/services/core/java/com/android/server/notification/PreferencesHelper.java

public class PreferencesHelper implements RankingConfig {
    ......
   
    @Override
    public boolean canShowBadge(String packageName, int uid) {
        synchronized (mPackagePreferences) {
            return getOrCreatePackagePreferencesLocked(packageName, uid).showBadge;
        }
    }

    //设置某个应用的通知圆点开关,开启或者关闭
    @Override
    public void setShowBadge(String packageName, int uid, boolean showBadge) {
        synchronized (mPackagePreferences) {
            getOrCreatePackagePreferencesLocked(packageName, uid).showBadge = showBadge;
        }
        updateConfig();//更新属性配置
    }
 ......

(1) 两个方法中都调用了同一个方法 getOrCreatePackagePreferencesLocked(),


    private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg,
            @UserIdInt int userId, int uid, int importance, int priority, int visibility,
            boolean showBadge, int bubblePreference) {
        final String key = packagePreferencesKey(pkg, uid);
        PackagePreferences r = (uid == UNKNOWN_UID)
                ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId))
                : mPackagePreferences.get(key);
        if (r == null) {
            r = new PackagePreferences();
            r.pkg = pkg;
            r.uid = uid;
            r.importance = importance;
            r.priority = priority;
            r.visibility = visibility;
            r.showBadge = showBadge;
            r.bubblePreference = bubblePreference;
            if (mOemLockedApps.containsKey(r.pkg)) {
                List<String> channels = mOemLockedApps.get(r.pkg);
                if (channels == null || channels.isEmpty()) {
                    r.oemLockedImportance = true;
                } else {
                    r.oemLockedChannels = channels;
                }
            }

            try {
                createDefaultChannelIfNeededLocked(r);
            } catch (PackageManager.NameNotFoundException e) {
                Slog.e(TAG, "createDefaultChannelIfNeededLocked - Exception: " + e);
            }

            if (r.uid == UNKNOWN_UID) {
                mRestoredWithoutUids.put(unrestoredPackageKey(pkg, userId), r);
            } else {
                mPackagePreferences.put(key, r);
            }
        }
        return r;
    }

(2) 该方法返回 PackagePreferences 对象,它是PreferencesHelper.java的内部类,接着看下该对象有哪些属性:

    private static class PackagePreferences {
        String pkg;
        int uid = UNKNOWN_UID;
        int importance = DEFAULT_IMPORTANCE;//通知重要性
        int priority = DEFAULT_PRIORITY; //通知优先级
        int visibility = DEFAULT_VISIBILITY; //通知可见性
        boolean showBadge = DEFAULT_SHOW_BADGE; //通知原点
        int bubblePreference = DEFAULT_BUBBLE_PREFERENCE; //通知气泡
        int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; 

        boolean oemLockedImportance = DEFAULT_OEM_LOCKED_IMPORTANCE;
        List<String> oemLockedChannels = new ArrayList<>();
        boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;

        boolean hasSentInvalidMessage = false;
        boolean hasSentValidMessage = false;
       
        boolean userDemotedMsgApp = false;

        Delegate delegate = null;
        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
        Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>();

        public boolean isValidDelegate(String pkg, int uid) {
            return delegate != null && delegate.isAllowed(pkg, uid);
        }
    }

(3)该内部类对象保存了通知的一些属性,是通知属性的封装类,如上面两个方法中,
都用到了getOrCreatePackagePreferencesLocked(packageName, uid).showBadge 
来获取通知是否开启通知原点功能, 该方法相当于是通过 PackagePreferences.showBadge 
获取属性值,之后便可以通过PreferencesHelper 来获取通知最新的属性.

通过 设置 或者 桌面快捷方式 可以打开通知圆点功能,请求会从 设置 跨进程发送到NotificationManagerService(NMS), NMS 会通过setShowBadge()@PreferencesHelper来更新属性,并把最新属性值保存到PreferencesHelper对象中.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/763019.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue2组件传值(通信)的方式

目录 1.父传后代 ( 后代拿到了父的数据 )1. 父组件引入子组件&#xff0c;绑定数据2. 子组件直接使用父组件的数据3. 依赖注入(使用 provide/inject API)1.在祖先组件中使用 provide2.在后代组件中使用 inject 2.后代传父 &#xff08;父拿到了后代的数据&#xff09;1. 子组件…

【Qt】认识Qt界面Hello world小程序

一.认识Qt界面 1.左边栏 在编辑模式下&#xff0c;左边竖排的两个窗⼝叫做 "边栏" 。 ① 是项⽬⽂件管理窗⼝ ② 是打开⽂件列表窗⼝。 边栏⾥的窗⼝数⽬可以增加&#xff0c;边栏⼦窗⼝标题栏有⼀排⼩按钮&#xff0c;最右边的是关闭按钮&#xff0c;倒数第⼆个是 …

千元好礼等你来拿 MatrixOne最强体验官

开发者集合&#xff01;[MatrixOne最强体验官]带着丰厚的奖品走来啦&#xff01;MatrixOne 是一款高度兼容 MySQL 语法的 HTAP 数据库&#xff0c;MatrixOne Cloud (MO Cloud) 是基于 MatrixOne 内核的全托管云原生数据平台&#xff0c;具备实时 HTAP&#xff0c;多租户&#x…

Unity Shader 软粒子

Unity Shader 软粒子 前言项目Shader连连看项目渲染管线设置 鸣谢 前言 当场景有点单调的时候&#xff0c;就需要一些粒子点缀&#xff0c;此时软粒子就可以发挥作用了。 使用软粒子与未使用软粒子对比图 项目 Shader连连看 这里插播一点&#xff0c;可以用Vertex Color与…

antd(5.x) Popover 的content有个modal,关不掉了

问题描述&#xff1a; 如上图所示&#xff0c;我的提示modal 关不掉了&#xff0c;思考问题症结在handleVisibleChange const content (<div className{styles.box}>别的样式</div>{/* 链接 */}<div className{styles.linkBox}><Modaltitle{提示}open{…

deepin基于apt-mirror同步软件源及构建本地内网源

1.安装apt-mirror sudo apt install -y apt-mirror2.配置apt-mirror(/etc/apt/mirror.list) sudo cp /etc/apt/mirror.list /etc/apt/mirror.list.deepin.bak #备份配置文件 sudo gedit /etc/apt/mirror.list修改如下&#xff1a; deb [trustedyes] https://mirrors.bfsu.ed…

在线如何快速把图片变小?图片轻松修改大小的3个在线工具

随着现在图片在工作和生活中的广泛使用&#xff0c;在使用图片的时候经常会因为图片太大的问题受到影响&#xff0c;比较简单的一种处理方法可以通过压缩图片的方式来缩小图片大小&#xff0c;那么图片压缩具体该怎么来操作呢&#xff1f;下面就给大家分享几款图片在线压缩工具…

python如何求不定积分

sympy介绍 sympy库的安装非常的简单&#xff0c;利用conda命令可以快速的完成安装。 conda install sympy 接下来&#xff0c;我们将介绍利用第三方库sympy来完成积分的计算。 python求解不定积分 接下来&#xff0c;我们将介绍上述的不定积分的求解。 首先导入sympy库中的…

切片的基础知识

文章目录 ● Slice 的底层实现原理&#xff1f;● array 和 Slice 的区别&#xff1f;● 拷贝大切片一定比小切片代价大吗&#xff1f;● Slice 深拷贝和浅拷贝&#xff1f;● 零切片、空切片、nil切片&#xff1f;● Slice 的扩容机制&#xff1f;● Slice 为什么不是线程安全…

父子节点内容和个数提取

有时我们需要获得菜单的内容和个数&#xff0c;这个时候通常有父子菜单&#xff0c;那么怎么分别获取到他们呢&#xff1f;以下面的智慧物业管理系统为例&#xff0c;有7个父节点&#xff0c;每个父节点下面有子节点。如何把父节点名称和总数&#xff0c;以及子节点的名称和总数…

Golang-context理解

golang-context笔记整理 golang为何设计context&#xff1f;代码上理解原理空context类cancelCtx类.withcancelctx方法 timerCtx类valueCtx类 golang为何设计context&#xff1f; 有并发特性的语言中&#xff0c;都会有一种说法&#xff1a;创建异步线程或者携程的时候&#x…

在postman中调试supabase的API接口

文章目录 在supabase中获取API地址和key知道它的restfull风格在postman中进行的设置1、get请求调试2、post新增用户调试3、使用patch更新数据&#xff0c;不用put&#xff01;4、delete删除数据 总结 在supabase中获取API地址和key 首先登录dashboard后台&#xff0c;首页- 右…

OFDM的缺点与关键技术

子载波间干扰英文简写ICI&#xff0c;ICI可能由各种原因引起 在多径信道中&#xff0c;CP小于最大附加时延时收发系统载波频率偏差和采样偏差收发系统相对移动&#xff0c;存在多普勒频移 ICI是制约OFDM系统性能的主要重要因素之一 对频率偏差敏感----->同步技术&#xff0…

Figma-ui设计学习APP Store

Figma汉化&#xff1a;Figma 中文社区_插件组件库,软件汉化教程 - Figma.Cool 选择Chorme汉化版离线包 插件安装&#xff1a; 打开浏览器安装扩展&#xff0c;解压加载进去即可。 打开标尺&#xff0c;设置左右内边距参考线&#xff08;左21 右356&#xff09;&#xff0c;wi…

同一个excel表格,为什么在有的电脑上会显示#NAME?

一、哪些情况会产生#NAME?的报错 1.公式名称拼写错误 比如求和函数SUM&#xff0c;如果写成SUN就会提示#NAME&#xff1f;报错。 2.公式中的文本值未添加双引号 如下图&#xff1a; VLOOKUP(丙,A:B,2,0) 公式的计算结果会返回错误值#NAME?&#xff0c;这是因为公式中文本…

PostgreSQL的学习心得和知识总结(一百四十七)|深入理解PostgreSQL数据库之transaction chain的使用和实现

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

反激开关电源反馈电路相关参数选型

Vb的电压正常变化范围是&#xff1a;0-1V&#xff08;最低0V&#xff0c;由于有稳压管&#xff0c;最高不会超过1V&#xff09; Vb的电压越高&#xff0c;则输出占空比越大&#xff0c;Vb电压越低&#xff0c;则输出占空比越小 那么Va的正常变化范围应该是&#xff1a;1.4-4.…

文本生成sql模型(PipableAI/pip-sql-1.3b)

安装环境 pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers 代码 question "What are the email address, town and county of the customers who are of the least common gender?"sc…

IT专业入门——高考假期预习指南,我来做你的引路人

目录 认识IT知识体系 什么是计算机 按规模、速度和功能分类 按照其工作模式分类 硬件 操作系统 编程语言 对学习语言的一点建议 对于学python的一点看法 网络 数据结构与算法 数据库 Web开发 Web前端 Web后端 基础预习指南 技术路线学习一览 学习资源推荐 刷…

白话负载均衡、正反向代理(入门科普版)

什么是负载均衡&#xff1f;为什么需要负载均衡 从字面上理解&#xff0c;什么是负载&#xff0c;服务器承受访问量的大小是负载&#xff0c;但是单台服务器的访问性能是有限的&#xff0c;最典型的例子就是双十一、春运抢票这种&#xff0c;这时候就需要一种方案来解决这类问…