測試通知錯發到線上了怎麼辦?看這裏!

測試通知錯發到線上了怎麼辦?看這裏!

通知是指 Android 在應用界面之外顯示的消息,發出通知後,通知先以圖標的形式在狀態欄中顯示。用戶可以在狀態欄向下滑動以打開抽屜式通知欄,然後便可在其中查看更多詳細信息並對通知執行操作。展示效果如下:

 

APP在進行日常消息推送的過程中,因爲種種原因,難免會出現推送“失誤”。比如技術小哥,錯把生產環境當成了測試環境,頻繁給用戶發送測試內容,鬧了笑話……就像某著名視頻類APP線上曾出現過的重大“失誤”

 

 

應對此類問題,小編整理了以下幾種方式(建議組合使用):

  1. 官微致歉;
  2. 推送內容審覈;
  3. 通知撤回;

 

【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():

  1. mListeners爲NMS內部類NotificationListeners,內部通過NMS.mHandler單線程處理;
  2. NotificationListeners通過Binder調用NotificationListenerService(簡稱NLS)內部實現類NotificationListenerWrapper;
  3. NotificationListenerWrapper通過NLS.mHandler線程執行onNotificationRemoved()方法;
  4. NLS實則爲抽象類,它真正的實現是在StatusBar中NotificationListenerWithPlugins裏;
  5. 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 並導入工程

·使用通知撤回模板:

參數說明

成員和方法名

類型

必填

默認值

說明

setAppId

String

-

個推APPID

setAppkey

String

-

個推APPKEY

setOldTaskId

String

-

指定需要撤回消息對應的taskId

setForce

Boolean

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);

 

效果演示:

<此處有個gif演示通知撤回效果,word裏動不起來>

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章