创建通知
下面代码来自https://developer.android.google.cn/training/notify-user/channels
//默认情况下,通知的文本内容会被截断以放在一行。如果您想要更长的通知,可以使用 setStyle() 添加样式模板来启用可展开的通知。
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Much longer text that cannot fit one line...")
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("Much longer text that cannot fit one line..."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
//Android 8.0 及更高版本上提供通知,必须先通过向 createNotificationChannel() 传递 NotificationChannel 的实例在系统中注册应用的通知渠道
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name);
String description = getString(R.string.channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
//以下代码段展示了如何创建基本 Intent,以在用户点按通知时打开 Activity:
// Create an explicit intent for an Activity in your app
Intent intent = new Intent(this, AlertDetails.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Set the intent that will fire when the user taps the notification
.setContentIntent(pendingIntent)
.setAutoCancel(true);
//如需显示通知,请调用 NotificationManagerCompat.notify(),并将通知的唯一 ID 和 NotificationCompat.Builder.build() 的结果传递给它。
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// notificationId is a unique int for each notification that you must define
notificationManager.notify(notificationId, builder.build());
//如需添加操作按钮,请将 addAction() 传递给 PendingIntent 方法。
Intent snoozeIntent = new Intent(this, MyBroadcastReceiver.class);
snoozeIntent.setAction(ACTION_SNOOZE);
snoozeIntent.putExtra(EXTRA_NOTIFICATION_ID, 0);
PendingIntent snoozePendingIntent =
PendingIntent.getBroadcast(this, 0, snoozeIntent, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Hello World!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_snooze, getString(R.string.snooze),
snoozePendingIntent);
// 显示紧急消息
您的应用可能需要显示紧急的时效性消息,例如来电或响铃警报。在这些情况下,您可以将全屏 Intent 与通知关联。调用通知时,根据设备的锁定状态,用户会看到以下情况之一:
如果用户设备被锁定,会显示全屏 Activity,覆盖锁屏。
如果用户设备处于解锁状态,通知以展开形式显示,其中包含用于处理或关闭通知的选项。
紧急消息弹出屏幕,时效时间30s。
Intent fullScreenIntent = new Intent(this, CallActivity.class);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("Incoming call")
.setContentText("(919) 555-1234")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
// Use a full-screen intent only for the highest-priority alerts where you
// have an associated activity that you would like to launch after the user
// interacts with the notification. Also, if your app targets Android 10
// or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
// order for the platform to invoke this notification.
.setFullScreenIntent(fullScreenPendingIntent, true);
Notification incomingCallNotification = notificationBuilder.build();
渠道的重要性
通知发出流程
通知的类型
com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
/** 默认的收缩视图。看到时,阴影被拉下来,并在锁屏,如果没有担心内容的敏感性。
* The default, contracted view. Seen when the shade is pulled down and in the lock screen
* if there is no worry about content sensitivity.
*/
public static final int FLAG_CONTENT_VIEW_CONTRACTED = 1;
/** 展开的视图。在用户展开通知时看到。
* The expanded view. Seen when the user expands a notification.
*/
public static final int FLAG_CONTENT_VIEW_EXPANDED = 1 << 1;
/** 抬头视图 当一个高优先级的通知从顶部窥视进来时就会看到。
* The heads up view. Seen when a high priority notification peeks in from the top.
*/
public static final int FLAG_CONTENT_VIEW_HEADS_UP = 1 << 2;
/** 环境视图。当收到高优先级通知且手机休眠时出现。
* The ambient view. Seen when a high priority notification is received and the phone
* is dozing.
*/
public static final int FLAG_CONTENT_VIEW_AMBIENT = 1 << 3;
/**公众视图。这是压缩视图的一个版本,它隐藏敏感信息,如果我们确定通知的内容应该被隐藏,它将在锁屏上使用。
* The public view. This is a version of the contracted view that hides sensitive
* information and is used on the lock screen if we determine that the notification's
* content should be hidden.
*/
public static final int FLAG_CONTENT_VIEW_PUBLIC = 1 << 4;
通知监听器
NotificationEntryListener监听接口,用于监听NotificationEntryManager 发出的通知变化
/**
* Listener interface for changes sent by NotificationEntryManager.
*/
public interface NotificationEntryListener {
/**在发布新通知时调用。在这一点上,通知是“待定的”:它的观点还没有被夸大,系统的大部分假装它还不存在。
* Called when a new notification is posted. At this point, the notification is "pending": its
* views haven't been inflated yet and most of the system pretends like it doesn't exist yet.
*/
default void onPendingEntryAdded(NotificationEntry entry) {
}
// TODO: Combine this with onPreEntryUpdated into "onBeforeEntryFiltered" or similar
/**在创建新条目时,但在对用户进行筛选或显示之前调用。
* Called when a new entry is created but before it has been filtered or displayed to the user.
*/
default void onBeforeNotificationAdded(NotificationEntry entry) {
}
/**创建新条目时调用。
* Called when a new entry is created.
*/
default void onNotificationAdded(NotificationEntry entry) {
}
/** 在通知即将更新时调用
* Called when a notification is about to be updated. Notification- and ranking-derived fields
* on the entry have already been updated but the following have not yet occurred:
* (a) View binding (i.e. the associated view has not yet been updated / inflation has not yet
* been kicked off.
* (b) Notification filtering
*/
default void onPreEntryUpdated(NotificationEntry entry) {
}
/**在对通知进行任何筛选后更新通知时调用。
* Called when a notification was updated, after any filtering of notifications have occurred.
*/
default void onPostEntryUpdated(NotificationEntry entry) {
}
/** 在通知的视图首次填充时调用。
* Called when a notification's views are inflated for the first time.
*/
default void onEntryInflated(NotificationEntry entry, @InflationFlag int inflatedFlags) {
}
/**当现有通知的视图重新展开时调用(通常是由于向该通知发布了更新)。
* Called when an existing notification's views are reinflated (usually due to an update being
* posted to that notification).
*
* @param entry notification data entry that was reinflated.
*/
default void onEntryReinflated(NotificationEntry entry) {
}
/**在为通知填充视图出错时调用。
* Called when an error occurred inflating the views for a notification.
*/
default void onInflationError(StatusBarNotification notification, Exception exception) {
}
/**当通知被删除时调用(可能是因为用户刷走了通知,也可能是因为开发人员收回了通知)。
* Called when a notification has been removed (either because the user swiped it away or
* because the developer retracted it).
* @param entry notification data entry that was removed. Null if no entry existed for the
* removed key at the time of removal.
* @param visibility logging data related to the visibility of the notification at the time of
* removal, if it was removed by a user action. Null if it was not removed by
* a user action.
* @param removedByUser true if the notification was removed by a user action
*/
default void onEntryRemoved(
NotificationEntry entry,
@Nullable NotificationVisibility visibility,
boolean removedByUser) {
}
/**在通知排名更改时调用
* Called whenever notification ranking changes, in response to
* {@link NotificationListenerService#onNotificationRankingUpdate}. This is called after
* NotificationData has processed the update and notifications have been re-sorted and filtered.
*
* @param rankingMap provides access to ranking information on currently active notifications
*/
default void onNotificationRankingUpdated(RankingMap rankingMap) {
}
}
NotificationAlertingManager 构造函数中给NotificationEntryManager 加 NotificationEntryListener ,以便在通知视图首次填充(onEntryInflated) 时 感知并弹出悬浮通知。
com/android/systemui/statusbar/notification/NotificationAlertingManager.java
notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@Override
public void onEntryInflated(NotificationEntry entry, int inflatedFlags) {
showAlertingView(entry, inflatedFlags);
}
@Override
public void onPostEntryUpdated(NotificationEntry entry) {
updateAlertState(entry);
}
@Override
public void onEntryRemoved(
NotificationEntry entry,
NotificationVisibility visibility,
boolean removedByUser) {
stopAlerting(entry.key);
}
});
}
/**如果内容视图已填充,并且条目仍应保持警报,则将条目添加到相应的警报管理器。
* Adds the entry to the respective alerting manager if the content view was inflated and
* the entry should still alert.
*
* @param entry entry to add
* @param inflatedFlags flags representing content views that were inflated
*/
private void showAlertingView(NotificationEntry entry, @InflationFlag int inflatedFlags) {
if ((inflatedFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
// Possible for shouldHeadsUp to change between the inflation starting and ending.
// If it does and we no longer need to heads up, we should free the view.
if (mNotificationInterruptionStateProvider.shouldHeadsUp(entry)) {
// 展示悬浮通知
mHeadsUpManager.showNotification(entry);
// Mark as seen immediately 标记为立即看到
setNotificationShown(entry.notification);
} else {
entry.freeContentViewWhenSafe(FLAG_CONTENT_VIEW_HEADS_UP);
}
}
if ((inflatedFlags & FLAG_CONTENT_VIEW_AMBIENT) != 0) {
if (mNotificationInterruptionStateProvider.shouldPulse(entry)) {
mAmbientPulseManager.showNotification(entry);
} else {
entry.freeContentViewWhenSafe(FLAG_CONTENT_VIEW_AMBIENT);
}
}
}
展示悬浮通知
当通知为高优先级,有提示音并在应用上方显示悬浮通知,停留30s后消失。进入 HeadsUpManager.showNotification(entry) 由其父类 AlertingNotificationManager实现。
com/android/systemui/statusbar/AlertingNotificationManager.java
/**在发布新通知时调用,该通知应提醒用户并显示在屏幕上。
* Called when posting a new notification that should alert the user and appear on screen.
* Adds the notification to be managed.
* @param entry entry to show
*/
public void showNotification(@NonNull NotificationEntry entry) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "showNotification");
}
addAlertEntry(entry);
updateNotification(entry.key, true /* alert */);
entry.setInterruption();
}
/**在通知状态已更新时调用。
* Called when the notification state has been updated.
* @param key the key of the entry that was updated
* @param alert whether the notification should alert again and force reevaluation of
* removal time
*/
public void updateNotification(@NonNull String key, boolean alert) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "updateNotification");
}
AlertEntry alertEntry = mAlertEntries.get(key);
if (alertEntry == null) {
// the entry was released before this update (i.e by a listener) This can happen
// with the groupmanager
return;
}
alertEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
if (alert) {
alertEntry.updateEntry(true /* updatePostTime */);
}
}
// 标记为立即看到
private void setNotificationShown(StatusBarNotification n) {
try {
mNotificationListener.setNotificationsShown(new String[]{n.getKey()});
} catch (RuntimeException e) {
Log.d(TAG, "failed setNotificationsShown: ", e);
}
}
// NotificationListener.setNotificationsShown()在哪实现呢?
NotificationListener 继承自NotificationListenerWithPlugins,NotificationListenerWithPlugins又继承自NotificationListenerService,setNotificationShown在NotificationListenerService中实现。
android/service/notification/NotificationListenerService.java
/**
* Inform the notification manager that these notifications have been viewed by the
* user. This should only be called when there is sufficient confidence that the user is
* looking at the notifications, such as when the notifications appear on the screen due to
* an explicit user interaction.
*
* <p>The service should wait for the {@link #onListenerConnected()} event
* before performing this operation.
*
* @param keys Notifications to mark as seen.
*/
public final void setNotificationsShown(String[] keys) {
if (!isBound()) return;
try {
getNotificationInterface().setNotificationsShownFromListener(mWrapper, keys);
} catch (android.os.RemoteException ex) {
Log.v(TAG, "Unable to contact notification manager", ex);
}
}
//com/android/server/notification/NotificationManagerService.java
@Override
public void setNotificationsShownFromListener(INotificationListener token, String[] keys) {
long identity = Binder.clearCallingIdentity();
try {
synchronized (mNotificationLock) {
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
if (keys == null) {
return;
}
ArrayList<NotificationRecord> seen = new ArrayList<>();
final int n = keys.length;
for (int i = 0; i < n; i++) {
NotificationRecord r = mNotificationsByKey.get(keys[i]);
if (r == null) continue;
final int userId = r.sbn.getUserId();
if (userId != info.userid && userId != UserHandle.USER_ALL
&& !mUserProfiles.isCurrentProfile(userId)) {
throw new SecurityException("Disallowed call from listener: "
+ info.service);
}
seen.add(r);
if (!r.isSeen()) {
if (DBG) Slog.d(TAG, "Marking notification as seen " + keys[i]);
reportSeen(r);
r.setSeen();
maybeRecordInterruptionLocked(r);
}
}
if (!seen.isEmpty()) {
mAssistants.onNotificationsSeenLocked(seen);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
//android/service/notification/NotificationAssistantService.java
@Override
public void onNotificationsSeen(List<String> keys) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = keys;
mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
args).sendToTarget();
}
....
case MSG_ON_NOTIFICATIONS_SEEN: {
SomeArgs args = (SomeArgs) msg.obj;
List<String> keys = (List<String>) args.arg1;
args.recycle();
onNotificationsSeen(keys);
break;
}
....
通知构建布局
以下从分析最高级的紧急提示通知入手分析相关布局。
附:可能的重要程度等级如下所示:
- 紧急:发出提示音,并以提醒式通知的形式显示。
- 高:发出提示音。
- 中:无提示音。
- 低:无提示音,且不会在状态栏中显示。
//com/android/systemui/statusbar/notification/collection/NotificationRowBinderImpl.java
// 更新布局
private void updateNotification(
NotificationEntry entry,
PackageManager pmUser,
StatusBarNotification sbn,
ExpandableNotificationRow row) {
row.setIsLowPriority(entry.ambient);
// Extract target SDK version.
try {
ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
entry.targetSdk = info.targetSdkVersion;
} catch (PackageManager.NameNotFoundException ex) {
Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
}
row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
&& entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
// TODO: should updates to the entry be happening somewhere else?
entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
entry.setRow(row);
row.setOnActivatedListener(mPresenter);
// PRIVARY_LOW 及以上消息使用增加折叠的高度
boolean useIncreasedCollapsedHeight =
mMessagingUtil.isImportantMessaging(sbn, entry.importance);
// 全屏折叠下的使用折叠的抬头
boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight
&& !mPresenter.isPresenterFullyCollapsed();
row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
row.setEntry(entry);
// IMPORTANCE_HIGH 设置展示悬浮框
if (mNotificationInterruptionStateProvider.shouldHeadsUp(entry)) {
row.updateInflationFlag(FLAG_CONTENT_VIEW_HEADS_UP, true /* shouldInflate */);
}
// IMPORTANCE_HIGH 从休眠中唤醒 展示在屏幕上
if (mNotificationInterruptionStateProvider.shouldPulse(entry)) {
row.updateInflationFlag(FLAG_CONTENT_VIEW_AMBIENT, true /* shouldInflate */);
}
row.setNeedsRedaction(
Dependency.get(NotificationLockscreenUserManager.class).needsRedaction(entry));
// 填充布局
row.inflateViews();
// bind the click event to the content area
checkNotNull(mNotificationClicker).register(row, sbn);
}
紧急通知样式flag = FLAG_CONTENT_VIEW_HEADS_UP
com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
public static CancellationSignal apply(
boolean inflateSynchronously,
InflationProgress result,
@InflationFlag int reInflateFlags,
ArrayMap<Integer, RemoteViews> cachedContentViews,
ExpandableNotificationRow row,
boolean redactAmbient,
RemoteViews.OnClickHandler remoteViewClickHandler,
@Nullable InflationCallback callback) {
NotificationContentView privateLayout = row.getPrivateLayout();
NotificationContentView publicLayout = row.getPublicLayout();
final HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();
......
flag = FLAG_CONTENT_VIEW_HEADS_UP;
if ((reInflateFlags & flag) != 0) {
if (result.newHeadsUpView != null) {
boolean isNewView =
!canReapplyRemoteView(result.newHeadsUpView,
cachedContentViews.get(FLAG_CONTENT_VIEW_HEADS_UP));
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
result.inflatedHeadsUpView = v;
}
@Override
public RemoteViews getRemoteView() {
return result.newHeadsUpView;
}
};
applyRemoteView(inflateSynchronously, result, reInflateFlags, flag,
cachedContentViews, row, redactAmbient, isNewView, remoteViewClickHandler,
callback, privateLayout, privateLayout.getHeadsUpChild(),
privateLayout.getVisibleWrapper(
VISIBLE_TYPE_HEADSUP), runningInflations,
applyCallback);
}
}
......
@VisibleForTesting
static void applyRemoteView(
boolean inflateSynchronously,
final InflationProgress result,
final @InflationFlag int reInflateFlags,
@InflationFlag int inflationId,
final ArrayMap<Integer, RemoteViews> cachedContentViews,
final ExpandableNotificationRow row,
final boolean redactAmbient,
boolean isNewView,
RemoteViews.OnClickHandler remoteViewClickHandler,
@Nullable final InflationCallback callback,
NotificationContentView parentLayout,
View existingView,
NotificationViewWrapper existingWrapper,
final HashMap<Integer, CancellationSignal> runningInflations,
ApplyCallback applyCallback) {
RemoteViews newContentView = applyCallback.getRemoteView();
......