Android O適配---NotificationChannel
android-o上對通知做了更細粒度的管理。根據app的業務場景將通知分類。使用者可以在設定中選擇接收自己感興趣的通知。同時也可以遮蔽不喜歡的通知。先上圖看看新特性。
入口:手機桌面長按(android 原生8.0)app圖示,右上角出現提示“應用資訊”,點選進入應用詳情。如下圖一:我們可以看見”應用通知“
圖一
點選應用通知進入應用通知管理詳情頁,如下圖二中類別所示:本次測試建立了三種NotificationChannel:下載,聊天,推送。每種NotificationChannel設定了不同的屬性,如:重要性,是否振動等等
圖二
點選圖二中的下載NotificationChannel,進入NotificationChannel詳情頁,如圖三,可以看到下載NotificationChannel的詳細資訊
圖三
總之,android-o上對通知的管理更加細粒度,通過”管道“對通知分類,使用者可以根據自己對通知的喜好選擇接收特定管道的通知。
二,從原始碼分析
一,關鍵類
1.NotificationManagerService
NotificationManagerService繼承SystemService,是系統級別的service.
NotificationManagerService在init初始化的時候會載入/data/system/notification_policy.xml中的通知資訊到記憶體資料結構,該檔案中儲存所有package註冊的所有NotificationChannel。如下內容為xml檔案部分內容。可以看到不同package下建立的channel以及channel相關屬性。
</package> <package name="com.android.providers.downloads" show_badge="true" uid="1010013"> <channel id="active" name="下載中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="complete" name="下載完成" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="waiting" name="等待中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="insufficient_space" name="空間不足" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> </package> <package name="com.android.providers.downloads" show_badge="true" uid="10013"> <channel id="active" name="下載中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="complete" name="下載完成" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="waiting" name="等待中" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> <channel id="insufficient_space" name="空間不足" importance="2" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" show_badge="true" /> </package>
NotificationManagerService主要職能是對通知的管理和排程。具體實現不列舉了,本次只討論android-O上的新特性
2.NotificationChannel
android-o上app傳送的每個通知必須依附於一個channel.即,每個notification物件必須傳送到指定的NotificationChannel.如果找不到channel,就會報錯,如下
NotificationService: No Channel found for pkg=com.miui.securitycore,
channelId=testNotificationChannel, id=10000, tag=null, opPkg=com.miui.securitycore,
callingUid=1000, userId=10, incomingUserId=10,
notificationUid=1001000, notification=Notification(channel=testNotificationChannel pri=0
contentView=null vibrate=null sound=null tick defaults=0x0 flags=0x2
color=0x00000000 vis=PRIVATE)
發通知:
1.建立Notification物件
/**
* Constructs a new Builder with the defaults:
*
* @param context
* A {@link Context} that will be used by the Builder to construct the
* RemoteViews. The Context will not be held past the lifetime of this Builder
* object.
* @param channelId
* The constructed Notification will be posted on this
* {@link NotificationChannel}. To use a NotificationChannel, it must first be
* created using {@link NotificationManager#createNotificationChannel}.
*/
public Builder(Context context, String channelId) {
this(context, (Notification) null);
mN.mChannelId = channelId;
}
2.建立NotificationChannel物件(不必每次建立)
/**
* Creates a notification channel.
*
* @param id The id of the channel. Must be unique per package. The value may be truncated if
* it is too long.
* @param name The user visible name of the channel. You can rename this channel when the system
* locale changes by listening for the {@link Intent#ACTION_LOCALE_CHANGED}
* broadcast. The recommended maximum length is 40 characters; the value may be
* truncated if it is too long.
* @param importance The importance of the channel. This controls how interruptive notifications
* posted to this channel are.
*/
public NotificationChannel(String id, CharSequence name, @Importance int importance) {
this.mId = getTrimmedString(id);
this.mName = name != null ? getTrimmedString(name.toString()) : null;
this.mImportance = importance;
}
3.傳送
/**
* @hide
*/
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
INotificationManager service = getService();
String pkg = mContext.getPackageName();
// Fix the notification as best we can.
Notification.addFieldsFromContext(mContext, notification);
if (notification.sound != null) {
notification.sound = notification.sound.getCanonicalUri();
if (StrictMode.vmFileUriExposureEnabled()) {
notification.sound.checkFileUriExposed("Notification.sound");
}
}
fixLegacySmallIcon(notification, pkg);
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (notification.getSmallIcon() == null) {
throw new IllegalArgumentException("Invalid notification (no valid small icon): "
+ notification);
}
}
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
總結:1.channel有很多屬性可以設定。channel相當一個大的類別,該類別的通知具有相同特徵,如振動,閃燈等等
@Override
public String toString() {
return "NotificationChannel{" +
"mId='" + mId + '\'' +
", mName=" + mName +
", mDescription=" + (!TextUtils.isEmpty(mDesc) ? "hasDescription " : "") +
", mImportance=" + mImportance +
", mBypassDnd=" + mBypassDnd +
", mLockscreenVisibility=" + mLockscreenVisibility +
", mSound=" + mSound +
", mLights=" + mLights +
", mLightColor=" + mLightColor +
", mVibration=" + Arrays.toString(mVibration) +
", mUserLockedFields=" + mUserLockedFields +
", mVibrationEnabled=" + mVibrationEnabled +
", mShowBadge=" + mShowBadge +
", mDeleted=" + mDeleted +
", mGroup='" + mGroup + '\'' +
", mAudioAttributes=" + mAudioAttributes +
", mBlockableSystem=" + mBlockableSystem +
'}';
}
2.建立notification時候需要關聯channel,沒有channel會報錯。
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
final int callingPid, final String tag, final int id, final Notification notification,
int incomingUserId) {
if (DBG) {
Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
+ " notification=" + notification);
}
checkCallerIsSystemOrSameApp(pkg);
final int userId = ActivityManager.handleIncomingUser(callingPid,
callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
final UserHandle user = new UserHandle(userId);
if (pkg == null || notification == null) {
throw new IllegalArgumentException("null not allowed: pkg=" + pkg
+ " id=" + id + " notification=" + notification);
}
// The system can post notifications for any package, let us resolve that.
final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId);
// Fix the notification as best we can.
try {
final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser(
pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
(userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId);
Notification.addFieldsFromContext(ai, notification);
} catch (NameNotFoundException e) {
Slog.e(TAG, "Cannot create a context for sending app", e);
return;
}
mUsageStats.registerEnqueuedByApp(pkg);
// setup local book-keeping
String channelId = notification.getChannelId();
if (mIsTelevision && (new Notification.TvExtender(notification)).getChannelId() != null) {
channelId = (new Notification.TvExtender(notification)).getChannelId();
}
final NotificationChannel channel = mRankingHelper.getNotificationChannel(pkg,
notificationUid, channelId, false /* includeDeleted */);
if (channel == null) {
final String noChannelStr = "No Channel found for "
+ "pkg=" + pkg
+ ", channelId=" + channelId
+ ", id=" + id
+ ", tag=" + tag
+ ", opPkg=" + opPkg
+ ", callingUid=" + callingUid
+ ", userId=" + userId
+ ", incomingUserId=" + incomingUserId
+ ", notificationUid=" + notificationUid
+ ", notification=" + notification;
Log.e(TAG, noChannelStr);
doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
"Failed to post notification on channel \"" + channelId + "\"\n" +
"See log for more details");
return;
}
final StatusBarNotification n = new StatusBarNotification(
pkg, opPkg, id, tag, notificationUid, callingPid, notification,
user, null, System.currentTimeMillis());
final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r)) {
return;
}
// Whitelist pending intents.
if (notification.allPendingIntents != null) {
final int intentCount = notification.allPendingIntents.size();
if (intentCount > 0) {
final ActivityManagerInternal am = LocalServices
.getService(ActivityManagerInternal.class);
final long duration = LocalServices.getService(
DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
for (int i = 0; i < intentCount; i++) {
PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
if (pendingIntent != null) {
am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
WHITELIST_TOKEN, duration);
}
}
}
}
mHandler.post(new EnqueueNotificationRunnable(userId, r));
}
三,多使用者下的NotificationChannel
NotificationChannel不同使用者空間下是通過uid來區分的,所以在多使用者下系統在尋找channel時是以pkg + "|" + uid為key查詢的。具體過程見原始碼。<package name="com.miui.securitycore" show_badge="true" uid="1000">
<channel id="com_miui_securitycore_channel_id" name="com_miui_securitycore_channel_name" importance="4" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" vibration_enabled="true" show_badge="true" />
</package>
<package name="com.miui.securitycore" show_badge="true" uid="1001000">
<channel id="com_miui_securitycore_channel_id" name="com_miui_securitycore_channel_name" importance="4" sound="content://settings/system/notification_sound" usage="5" content_type="4" flags="0" vibration_enabled="true" show_badge="true" />
</package>