以下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对象中.