通知是指 Android 在应用界面之外显示的消息,发出通知后,通知先以图标的形式在状态栏中显示。用户可以在状态栏向下滑动以打开抽屉式通知栏,然后便可在其中查看更多详细信息并对通知执行操作。展示效果如下:
APP在进行日常消息推送的过程中,因为种种原因,难免会出现推送“失误”。比如技术小哥,错把生产环境当成了测试环境,频繁给用户发送测试内容,闹了笑话……就像某著名视频类APP线上曾出现过的重大“失误”
应对此类问题,小编整理了以下几种方式(建议组合使用):
官微致歉;
推送内容审核;
通知撤回;
【1、官微致歉】
既然“失误”通知已经发出,难以避免会对用户造成伤害。建议立即在官方渠道(微博、公众号等)发布致歉声明,给用户一个“说法”。
【2、推送内容审核】
为避免出现线上重大异常,在推送通知前应进行内容审核。对通知标题、内容包含“测试”、“死亡”、“国家”等关键字的通知进行拦截,必须经二次确认后才能发出。
经内容字词审核后,建议先在测试环境进行推送,观察不同设备通知样式。比如Android通知摘要subText,可能填了错误内容,但只会在部分设备上展示,如果没有发现就推到了线上,后果不堪设想。
【3、通知撤回】
所谓通知撤回,就是在“失误”通知发出后,用户还没看到的情况下将其从通知栏中移除,用以将损失降到最低的处理方式。
从技术角度而言,展示通知我们用 NotificationManager.notify(notifyID) ,再使用 NotificationManager.cancel(notifyID) 时传入相同notifyID即可实现通知撤回。
【通知撤回原理】
知其然应知其所以然。如果读者是Android研发或对Android Framework感兴趣的话还可以更深入了解下其背后的实现原理。
我们从 NotificationManager.cancel()看起:
Step 1
public void cancel(int id)
{
cancel(null, id);
}
public void cancel(String tag, int id)
{
cancelAsUser(tag, id, new UserHandle(UserHandle.myUserId()));
}
public void cancelAsUser(String tag, int id, UserHandle user)
{
INotificationManager service = getService();
String pkg = mContext.getPackageName();
if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
try {
// Binder调用远程NotificationManagerService
service.cancelNotificationWithTag(pkg, tag, id, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
Step 2
此时由APP端通过aidl调用到了系统通知服务 NotificationManagerService(简称NMS)
@Override
public void cancelNotificationWithTag(String pkg, String tag, int id, int userId) {
// 检测是否为系统APP,或比较传入pkg是否为Binder.getCallingUid()应用,如果检测失败会抛出 SecurityException异常
checkCallerIsSystemOrSameApp(pkg);
……
// 非系统APP禁止通过NotificationManager.cancel()移除ForegroundService通知
final int mustNotHaveFlags = isCallingUidSystem() ? 0 :
(Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_AUTOGROUP_SUMMARY);
cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), pkg, tag, id, 0,
mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null);
}
Step 3
void cancelNotification(final int callingUid, final int callingPid,
final String pkg, final String tag, final int id,
final int mustHaveFlags, final int mustNotHaveFlags, final boolean sendDelete,
final int userId, final int reason, final ManagedServiceInfo listener) {
// 通知展示、移除都在mHandler中单线程处理
mHandler.post(new Runnable() {
@Override
public void run() {
……
synchronized (mNotificationLock) {
// 找到需要移除的通知
NotificationRecord r = findNotificationLocked(pkg, tag, id, userId);
// Step 2中提到的mustNotHaveFlags属性,如果想移除ForegroundService通知会被return
if ((r.getNotification().flags & mustNotHaveFlags) != 0) {
return;
}
// 都检测完成,开始移除通知啦!!
// 此方法看Step 4
boolean wasPosted = removeFromNotificationListsLocked(r);
// 此方法看Step 5
cancelNotificationLocked(r, sendDelete, reason, wasPosted, listenerName);
// 此方法看Step 6
cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName, sendDelete, null);
// 此方法看Step 7
updateLightsLocked();
}
}
});
}
Step 4
先看Step 3中removeFromNotificationListsLocked()方法,这里先将要取消的通知从展示时加入的List、Map中移除
private boolean removeFromNotificationListsLocked(NotificationRecord r) {
boolean wasPosted = false;
NotificationRecord recordInList = null;
if ((recordInList = findNotificationByListLocked(mNotificationList, r.getKey()))
!= null) {
mNotificationList.remove(recordInList);
mNotificationsByKey.remove(recordInList.sbn.getKey());
wasPosted = true;
}
while ((recordInList = findNotificationByListLocked(mEnqueuedNotifications, r.getKey()))
!= null) {
mEnqueuedNotifications.remove(recordInList);
}
return wasPosted;
}
Step 5
接着看Step 3中cancelNotificationLocked()方法
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason, boolean wasPosted, String listenerName) {
final String canceledKey = r.getKey();
// 发送deleteIntent
if (sendDelete) {
if (r.getNotification().deleteIntent != null) {
try {
r.getNotification().deleteIntent.send();
} catch (PendingIntent.CanceledException ex) {
Slog.w(TAG, "canceled PendingIntent for " + r.sbn.getPackageName(), ex);
}
}
}
// 从List中移除通知记录,wasPosted为true
if (wasPosted) {
// 展示通知必须设置smallIcon,此处不为空
if (r.getNotification().getSmallIcon() != null) {
// 更新UI,此步骤将详细解释
mListeners.notifyRemovedLocked(r.sbn, reason);
}
// 停止通知铃声
if (canceledKey.equals(mSoundNotificationKey)) {
mSoundNotificationKey = null;
final long identity = Binder.clearCallingIdentity();
try {
final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
if (player != null) {
player.stopAsync();
}
} catch (RemoteException e) {
} finally {
Binder.restoreCallingIdentity(identity);
}
}
// 停止震动
if (canceledKey.equals(mVibrateNotificationKey)) {
mVibrateNotificationKey = null;
long identity = Binder.clearCallingIdentity();
try {
mVibrator.cancel();
}
finally {
Binder.restoreCallingIdentity(identity);
}
}
// 从List中移除,并没有真正更新闪光灯
mLights.remove(canceledKey);
}
…………
」
其中更新UI步骤mListeners.notifyRemovedLocked():
mListeners为NMS内部类NotificationListeners,内部通过NMS.mHandler单线程处理;
NotificationListeners通过Binder调用NotificationListenerService(简称NLS)内部实现类NotificationListenerWrapper;
NotificationListenerWrapper通过NLS.mHandler线程执行onNotificationRemoved()方法;
NLS实则为抽象类,它真正的实现是在StatusBar中NotificationListenerWithPlugins里;
NotificationListenerWithPlugins再发送到StatusBar.mHandler中执行removeNotification()方法,实现真正的更新SystemUI操作。
Step 6
接着看Step 3中cancelGroupChildrenLocked()方法
private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid, String listenerName, boolean sendDelete, FlagChecker flagChecker) {
Notification n = r.getNotification();
if (!n.isGroupSummary()) {
// 此通知没设置GroupSummary
return;
}
…………
// 通知如果设置了GroupSummary的话,这里需要删除其包含的子通知
cancelGroupChildrenByListLocked(mNotificationList, r, callingUid, callingPid, listenerName,
sendDelete, true, flagChecker);
cancelGroupChildrenByListLocked(mEnqueuedNotifications, r, callingUid, callingPid,
listenerName, sendDelete, false, flagChecker);
}
Step 7
接着看Step 3中updateLightsLocked()方法,此步骤处理闪光灯
void updateLightsLocked()
{
NotificationRecord ledNotification = null;
// Step 5的最后mLights.remove(canceledKey)删除后,这里会找出需要闪光灯的最后一条被添加的通知
while (ledNotification == null && !mLights.isEmpty()) {
final String owner = mLights.get(mLights.size() - 1);
ledNotification = mNotificationsByKey.get(owner);
if (ledNotification == null) {
Slog.wtfStack(TAG, "LED Notification does not exist: " + owner);
mLights.remove(owner);
}
}
if (ledNotification == null || mInCall || mScreenOn) {
// 如果正在打电话 or 亮屏,关闭闪光灯
mNotificationLight.turnOff();
} else {
// 闪光灯闪烁
NotificationRecord.Light light = ledNotification.getLight();
// mNotificationPulseEnabled默认为false,若想开启可以通过调用相应ContentResolver
if (light != null && mNotificationPulseEnabled) {
mNotificationLight.setFlashing(light.color, Light.LIGHT_FLASH_TIMED,
light.onMs, light.offMs);
}
}
}
以上就是通知撤回的源码实现全部流程。
【小结】
通知撤回使用很简单,但在看过其背后的实现原理后,开发者可以了解到更多通知相关的细节(比如有哪些检查、具体流程等),才能避免“踩坑”。
如果还未能完全掌握,又担心在日常开发公司APP中出现问题的话,建议可以使用第三方成熟业务进行集成,比如 个推推送。个推目前已全面支持通知撤回,撤回速度最高可以达20万条/秒。
【个推通知撤回使用方式】
下面以Java为例简单介绍下通知撤回的具体使用方式。
sdk版本要求
客户端sdk:2.12.5.0以上
服务端os-sdk:java 4.1.0.1以上
·首先在个推开发者官网http://docs.getui.com/getui/start/devcenter/?id=doc-title-1
创建应用,并获得app id、app key、app secret等信息。
·下载Java SDK:http://docs.getui.com/download.html并导入工程
·使用通知撤回模板:
参数说明
成员和方法名类型必填默认值说明
setAppIdString是-个推APPID
setAppkeyString是-个推APPKEY
setOldTaskIdString是-指定需要撤回消息对应的taskId
setForceBoolean否false【Android】客户端没有找到对应的taskid,
是否把对应appid下所有的通知都撤回
示例说明
// STEP1:设置通知撤回模板
RevokeTemplate template = new RevokeTemplate();
template.setAppId(appId);
template.setAppkey(appKey);
// 通知推送成功都会返回TaskId,填入需要撤回的通知任务即可
template.setOldTaskId(oldTaskId);
template.setForce(force);
// STEP2:设置推送其他参数
SingleMessage message = new SingleMessage();
message.setOffline(true);
// 离线有效时间,单位为毫秒,可选
message.setOfflineExpireTime(24 * 3600 * 1000);
message.setData(template);
// STEP3:执行推送
push.pushMessageToSingle(message, target);
网友评论