android-o上对通知做了更细粒度的管理。根据app的业务场景将通知分类。用户可以在设置中选择接收自己感兴趣的通知。同时也可以屏蔽不喜欢的通知。先上图看看新特性。
入口:手机桌面长按(android 原生8.0)app图标,右上角出现提示“应用信息”,点击进入应用详情。如下图一:我们可以看见”应用通知“
点击应用通知进入应用通知管理详情页,如下图二中类别所示:本次测试创建了三种NotificationChannel:下载,聊天,推送。每种NotificationChannel设置了不同的属性,如:重要性,是否振动等等
点击图二中的下载NotificationChannel,进入NotificationChannel详情页,如图三,可以看到下载NotificationChannel的详细信息
总之,android-o上对通知的管理更加细粒度,通过”管道“对通知分类,用户可以根据自己对通知的喜好选择接收特定管道的通知。
二,从源码分析
一,关键类
1.NotificationManagerService
NotificationManagerService继承SystemService,是系统级别的service.
NotificationManagerService在init初始化的时候会加载/data/system/notification_policy.xml中的通知信息到内存数据结构,该文件中保存所有package注册的所有NotificationChannel。如下内容为xml文件部分内容。可以看到不同package下创建的channel以及channel相关属性。
2.NotificationChannel
android-o上app发送的每个通知必须依附于一个channel.即,每个notification对象必须发送到指定的NotificationChannel.如果找不到channel,就会报错,如下
NotificationService: No Channel found for pkg=com.miui.securitycore,
channelId=testNotificationChannel, id=10000, tag=null, opPkg=com.miui.securitycore,
callingUid=1000, userId=10, incomingUserId=10,
notificationUid=1001000, notification=Notification(channel=testNotificationChannel pri=0
contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x2
color=0x00000000 vis=PRIVATE)
发通知:
1.创建Notification对象
/**
* Constructs a new Builder with the defaults:
*
* @param context
* A {@link Context} that will be used by the Builder to construct the
* RemoteViews. The Context will not be held past the lifetime of this Builder
* object.
* @param channelId
* The constructed Notification will be posted on this
* {@link NotificationChannel}. To use a NotificationChannel, it must first be
* created using {@link NotificationManager#createNotificationChannel}.
*/
public Builder(Context context, String channelId) {
this(context, (Notification) null);
mN.mChannelId = channelId;
}
2.创建NotificationChannel对象(不必每次创建)
/**
* Creates a notification channel.
*
* @param id The id of the channel. Must be unique per package. The value may be truncated if
* it is too long.
* @param name The user visible name of the channel. You can rename this channel when the system
* locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
* broadcast. The recommended maximum length is 40 characters; the value may be
* truncated if it is too long.
* @param importance The importance of the channel. This controls how interruptive notifications
* posted to this channel are.
*/
public NotificationChannel(String id, CharSequence name, @Importance int importance) {
this.mId = getTrimmedString(id);
this.mName = name != null ? getTrimmedString(name.toString()) : null;
this.mImportance = importance;
}
3.发送
/**
* @hide
*/
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
INotificationManager service = getService();
String pkg = mContext.getPackageName();
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != null) {
notification.sound = notification.sound.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
notification.sound.checkFileUriExposed("Notification.sound");
}
}
fixLegacySmallIcon(notification, pkg);
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "
+ notification);
}
}
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
总结:1.channel有很多属性可以设置。channel相当一个大的类别,该类别的通知具有相同特征,如振动,闪灯等等
@Override
public String toString() {
return "NotificationChannel{" +
"mId='" + mId + '\'' +
", mName=" + mName +
", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") +
", mImportance=" + mImportance +
", mBypassDnd=" + mBypassDnd +
", mLockscreenVisibility=" + mLockscreenVisibility +
", mSound=" + mSound +
", mLights=" + mLights +
", mLightColor=" + mLightColor +
", mVibration=" + Arrays.toString(mVibration) +
", mUserLockedFields=" + mUserLockedFields +
", mVibrationEnabled=" + mVibrationEnabled +
", mShowBadge=" + mShowBadge +
", mDeleted=" + mDeleted +
", mGroup='" + mGroup + '\'' +
", mAudioAttributes=" + mAudioAttributes +
", mBlockableSystem=" + mBlockableSystem +
'}';
}
2.创建notification时候需要关联channel,没有channel会报错。
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
final int callingPid, final String tag, final int id, final Notification notification,
int incomingUserId) {
if (DBG) {
Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
+ " notification=" + notification);
}
checkCallerIsSystemOrSameApp(pkg);
final int userId = ActivityManager.handleIncomingUser(callingPid,
callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
final UserHandle user = new UserHandle(userId);
if (pkg == null || notification == null) {
throw new IllegalArgumentException("null not allowed: pkg=" + pkg
+ " id=" + id + " notification=" + notification);
}
// The system can post notifications for any package, let us resolve that.
final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);
// Fix the notification as best we can.
try {
final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
(userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, notification);
} catch (NameNotFoundException e) {
Slog.e(TAG, "Cannot create a context for sending app", e);
return;
}
mUsageStats.registerEnqueuedByApp(pkg);
// setup local book-keeping
String channelId = notification.getChannelId();
if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
channelId = (new Notification.TvExtender(notification)).getChannelId();
}
final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg,
notificationUid, channelId, false /* includeDeleted */);
if (channel == null) {
final String noChannelStr = "No Channel found for "
+ "pkg=" + pkg
+ ", channelId=" + channelId
+ ", id=" + id
+ ", tag=" + tag
+ ", opPkg=" + opPkg
+ ", callingUid=" + callingUid
+ ", userId=" + userId
+ ", incomingUserId=" + incomingUserId
+ ", notificationUid=" + notificationUid
+ ", notification=" + notification;
Log.e(TAG, noChannelStr);
doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
"Failed to post notification on channel \"" + channelId + "\"\n" +
"See log for more details");
return;
}
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, notificationUid, callingPid, notification,
user, null, System.currentTimeMillis());
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r)) {
return;
}
// Whitelist pending intents.
if (notification.allPendingIntents != null) {
final int intentCount = notification.allPendingIntents.size();
if (intentCount > 0) {
final ActivityManagerInternal am = LocalServices
.getService(ActivityManagerInternal.class);
final long duration = LocalServices.getService(
DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
for (int i = 0; i < intentCount; i++) {
PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
if (pendingIntent != null) {
am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
WHITELIST_TOKEN, duration);
}
}
}
}
mHandler.post(new EnqueueNotificationRunnable(userId, r));
}
三,多用户下的NotificationChannel
NotificationChannel不同用户空间下是通过uid来区分的,所以在多用户下系统在寻找channel时是以pkg + "|" + uid为key查找的。具体过程见源码。