Android Notification和許可權機制探討

來源:互聯網
上載者:User

標籤:本地   升級   版本   update   ide   標識   view   nes   開啟   

最近為了在部門內做一次小型的技術分享,深入瞭解了一下Notification的實現原理,以及android的許可權機制。在此做個記錄,文章可能比較長,沒耐心的話就直接看題綱吧。


先看一下下面兩張圖

圖一:


看到這圖可能大家不太明白,這和我們的notification有什麼關係,我來簡單介紹一下背景,這是發生在15年NBA季後賽期間,火箭隊對陣小牛隊,火箭隊以3:1領先,只要再贏一場就能淘汰對手,這時候火箭隊的官方首席運營官發了這條官方推特。翻譯一下就是 “一把槍指著小牛的隊標,哼哼,只需要閉上你們額眼睛,馬上就要結束了”。這條推特當時引起了很多人的轉寄和評論,並且推送給了所有關注相關比賽的球迷以及媒體。我們試想一下,你是一個小牛的球迷,輸了比賽以後本來心情就很差,這時候手機一震,收到這條通知欄推送,你是不是會有一種強烈的被蔑視感覺。當天推特上就掀起了一陣網路爭議,不僅小牛球迷,其他中立球迷也表示這條推特諷刺意味十足,已經有侮辱對手的嫌疑了。當然了,通知欄表示我不背這個鍋,誰來背?第二天,這位首席運營官就被火箭官方開除了,並宣稱此推特僅代表前運營官個人意見與火箭隊無任何關係。


圖二:





說完別人,再來說說我們自己吧。3月5日那天,群裡都在討論這條推送,本意是我們的編輯打算推一個分手相關的歌單,但是文案考慮不周全,讓人誤解。導致很多使用者感到莫名其妙,我們試想一下,你準備與你最近交往的對象一起吃個晚餐,出門前收到這條祝我們分手的通知,你是不是感到很不爽呢。是的,在微博上隨手一搜就發現有很多使用者是這種不爽的感覺了。當然了我拿這個對比並沒有說要炒這位編輯的魷魚噢。



好像偏題很遠了,說這麼多其實就是想說明一件事,應用程式的通知是非常重要的一環,處理的不好很可能給使用者帶來不好的印象,輕則吐槽,重則直接卸載。

好了好了,言歸正傳,我先列一下題綱吧

一、Notification的使用

二、Notification跨進程通訊的源碼分析

三、優雅地設計通知(7.0)

四、通知許可權問題

五、安卓的許可權機制(6.0)

六、總結



一、Notification的使用

目前咱們酷狗裡的通知使用主要有以下三種情境

1.訊息中心的通知

2.下載歌曲的通知

3.通過PlaybackService啟動的通知

下面簡單分析一下這三種情境的通知是如何?的。

第一種是使用系統布局產生的普通通知樣式


NotificationManagerCompat manager = NotificationManagerCompat.from(this);NotificationCompat.Builder builder = new NotificationCompat.Builder(this);builder.setContentTitle()  [1].setLargeIcon()          [2].setContentText()     [3].setNumber()            [4] .setSmallIcon()          [5].setWhen()               [6].setContentIntent(pendingIntent);manager.notify(tag, id, builder.build());


第二種是使用自訂的布局產生的通知樣式

NotificationManagerCompat manager = NotificationManagerCompat.from(this);NotificationCompat.Builder builder = new NotificationCompat.Builder(this);builder.setWhen().setSmallIcon().setLargeIcon().setContentIntent(pendingIntent);RemoteViews remoteView = new RemoteViews(getPackageName,  R.layout.custom);remoteView.setTextViewText(R.id.tv_title,  “通知標題”);remoteView.setImageViewResource(R.id.iv_icon,  R.drawable.icon);remoteView.setOnClickPendingIntent(R.id.iv_icon, pendingIntent);builder.setContent(remoteView);manager.notify(tag, id, builder.build());      RemoteViews不支援自訂View等複雜View

這兩點的共性就是都是先初始化NotificationManagerCompat,和NotificationCompat.Builder, 再經過一系列builder設值後通過manager.notify去發送通知,不同點是普通通知直接設定介面元素的值,而自訂通知是構造了一個remoteView的自訂布局,把它設定給builder的content。自訂通知呢有一點需要注意就是,這個自訂的布局裡的TextView字型的大小和顏色需要合理地配置,不然很容易在不同系統中和其他app的通知展示方式不一樣,導致使用者通知欄因為這個而顯得不美觀,甚至很突兀。那麼,官方也是有給我們提供這樣的解決方案:

Android 5.0之前可用:android:style/TextAppearance.StatusBar.EventContent.Title    // 通知標題樣式  android:style/TextAppearance.StatusBar.EventContent             // 通知內容樣式  Android 5.0及更高版本:  android:style/TextAppearance.Material.Notification.Title         // 通知標題樣式  android:style/TextAppearance.Material.Notification                  // 通知內容樣式

當然了這麼處理的話應該能解決絕大部分手機的通知文字樣式問題,但還是有一些被最佳化或者說改造過的系統,仍然不相容這樣的通知樣式,這時候就需要通過build()一個預設通知,然後再去擷取當前系統通知的文字和顏色的方式了。這種方式,可以來看看我們代碼中如何?的。

public SystemNotification getSystemText() {        mSystemNotification = new SystemNotification();        try {            NotificationCompat.Builder builder = new NotificationCompat.Builder(this);            builder.setContentTitle("SLNOTIFICATION_TITLE")                   .setContentText("SLNOTIFICATION_TEXT")                   .setSmallIcon(R.drawable.comm_ic_notification)                   .build();            LinearLayout group = new LinearLayout(this);            RemoteViews tempView = builder.getNotification().contentView;            ViewGroup event = (ViewGroup) tempView.apply(this, group);            recurseGroup(event);            group.removeAllViews();        } catch (Exception e) {            mSystemNotification.titleColor = Color.BLACK;            mSystemNotification.titleSize = 32;            mSystemNotification.contentColor = Color.BLACK;            mSystemNotification.contentSize = 24;        }        return mSystemNotification;    }    private boolean recurseGroup(ViewGroup gp) {        for (int i = 0; i < gp.getChildCount(); i++) {            View v = gp.getChildAt(i);            if (v instanceof TextView) {                final TextView text = (TextView) v;                final String szText = text.getText().toString();                if ("SLNOTIFICATION_TITLE".equals(szText)) {                    mSystemNotification.titleColor = text.getTextColors().getDefaultColor();                    mSystemNotification.titleSize = text.getTextSize();//                    return true;                }                if ("SLNOTIFICATION_TEXT".equals(szText)) {                    mSystemNotification.contentColor = text.getTextColors().getDefaultColor();                    mSystemNotification.contentSize = text.getTextSize();//                    return true;                }            }//            if (v instanceof ImageView) {//                final ImageView image = (ImageView) v;//                if (image.getBackground().getConstantState().equals(getResources().getDrawable(R.drawable.comm_ic_notification))) {//                    mSystemNotification.iconWidth = image.getWidth();//                    mSystemNotification.iconHeight = image.getHeight();//                }//            }            if (v instanceof ViewGroup) {// 如果是ViewGroup 遍曆搜尋                recurseGroup((ViewGroup) gp.getChildAt(i));            }        }        return false;    }


至於具體如何?的發送通知,我們待會再繼續分析。


而第三種通知比較特殊,是用service.startForground(notification)的方式產生的通知。

我們酷狗啟動的時候就會在通知欄產生一個可以控制播放的通知,這個通知就是playbackdservice在啟動的時候產生的。

Notification notification = new Notification();notification.icon = R.drawable.icon;notification.flags = mFlag;notification.contentView = mContentView;notification.contentIntent = pendingIntentmService.startForeground(id, notification);

這個方法的註解是這樣的:Make this service run in the foreground, supplying the ongoing notification to be shown to the user while in this state.By default services are background, meaning that if the system needs to kill them to reclaim more memory (such as to display a large page in a web browser), they can be killed without too much harm.  You can set this flag if killing your service would be disruptive to the user, such as if your service is performing background music playback, so the user would notice if their music stopped playing.

二、Notification跨進程通訊的源碼分析


我們的進程是如何將通知數據傳遞給系統進程的?系統進程又是如何拿到我們進程的資源去繪製通知欄介面的?關鍵在於RemoteViews

那我們得好好瞭解一下這裡的RemoteViews的工作原理,先看一張流程圖


我來解釋一下這張圖,本地進程和系統通知欄所在的系統進程是通過Binder來傳輸的。Notification內部本身有RemoteViews變數,當notification.Builder執行build()方法的時候,會把通知相關的資料及View操作等都通過一系列的addAction的方法存在RemoteViews裡。在notificationManager真正執行notify()的時候,本地進程通過getService拿到binder對象,再產生NotificationManagerService的執行個體,service通過調用enqueueNotificationWithTag()方法將notification,pkgName,tag,id等等展示通知需要的資料都傳遞到系統進程。系統進程通過迴圈調用RemoteViews裡的apply()方法,去擷取到之前的view操作並執行,而系統進程要拿到本地進程的資源,則是通過context.createApplicationContext()先拿到和本地進程基本一樣的context,然後再通過getResource(資源id)去擷取資源。這樣就很好地解釋了remoteViews是如何跨進程通訊的。這裡我們要再跟進一下RemoteViews的源碼,來驗證這段流程。
搞清楚了這點呢,我們再來看看之前一直存在於我們崩潰樹當中的這個崩潰,量還不小。

大致意思就是系統無法建立notification,因為通過資源id0x7f021b02找不到需要展示的通知icon,也就展示不了通知。而擷取資源的方式,剛才我們講到是通過context.createApplicationContext()拿到context,官方給出的解釋是:Return a new Context object for the given application name. This Context is the same as what the named application gets when it is launched, containing the same resources and class loader. Each call to this method returns a new instance of a Context object; Context objects are not shared, however they share common state (Resources, ClassLoader, etc) so the Context instance itself is fairly lightweight.然後用context去getResource來擷取資源,可以設想一下,因為這個資源id是在之前的build()操作的時候就已經把它傳遞到系統進程了,這時候如果本地進程覆蓋升級後更換了資源地圖表,這時候系統進程再執行getResource的話,用舊的資源id,當然就找不到資源了。目前我們酷狗的解決辦法就是固化這一部分資源id,這樣不論發多少新版本,通知欄需要的這些個資源都是同樣的資源id,怎麼拿都不會拿不到了。

瞭解了remoteViews的跨進程通訊這一塊,咱們再繼續跟進一下到底notification.notify(),經曆了哪些具體的過程。下面還是有一張圖、
這張圖清晰地描述了,通知的notify方法是如何觸發到系統更新通知欄介面的,源碼跟進講解。主要是以下幾個類NotificationManagerNotificationManagerServiceNotificationListenerServiceBaseStatusBarPhoneStatusBarStatusBarViewHeadsUpManager
三、優雅地設計通知
這裡有一張通知介面的對比圖,上面的是7.0之前的系統通知欄布局,下面的是7.0的最新系統通知欄布局。具體變化圖裡已經表現的很清晰了。當然了,現在很多手機廠商也都在嘗試使用自己定製的通知欄樣式。這也就使得我們在做自訂通知的時候會遇到很多阻礙,很顯然,因為廠商會自己來繪製通知樣式,所以我們的程式要自訂通知的時候,很可能就和系統的樣式差別很大,導致很醜的現象。只有當我們的通知本身就很特殊,不需要跟隨系統的其他通知樣式來展示時,才比較適合自訂布局,目前酷狗裡的下載通知就有這樣的問題。



說到介面布局我想起來剛開始做通知的時候,遇到的一個小問題,這裡也講一下。為什麼左上方的smallIcon看不到,是一團灰色呢。

其實是從sdk21開始,Google要求,所有應用程式的通知欄表徵圖,應該只使用alpha圖層來進行繪製,而不應該包括RGB圖層。通俗地說,就是我們的通知欄表徵圖不要帶顏色就可以了。

原來如此,怪不得我之前在酷狗裡看見這樣的代碼感到很好奇卻不知道原因。

if (SystemUtils.getSdkInt() >= 21) {    setSmallIcon(com.kugou.common.R.drawable.stat_notify_musicplayer_for5);} else {    setSmallIcon(com.kugou.common.R.drawable.stat_notify_musicplayer);}

下面我們再來看看我認為比較優雅的使用通知的方式。

1、進行中的通知
2、監聽清除事件的通知
3、不同優先順序的通知
4、系統懸浮窗和鎖屏的通知
5、不同Style樣式的通知
6、Group通知
7、可以直接回複的通知

尤其是最後兩點是7.0安卓系統專屬的。

https://material.io/guidelines/patterns/notifications.html#notifications-behavior

這個網站是Google推出的設計平台有關通知這一塊的設計吧。

這一部分我們可以來看看我寫的demo吧。


四、通知許可權問題

說到通知欄就不得不提通知欄許可權問題,之前我們的歌單推送功能,產品找到我說曝光量比點擊量大很多,從技術上是什麼原因導致使用者收到以後並不去點擊呢。

因為之前的邏輯是只要執行了notify()方法就認為通知曝光了,這裡設計是有問題的,因為很可能使用者已經把我們程式的通知給禁止掉了。

那我們怎麼知道自己的代理程式更新許可權被禁止了呢?如果被禁止了又該怎麼辦呢?

帶著這兩個問題,我開始查閱資料了。

1、API24開始系統就提供了現成的方法來擷取通知許可權

NotificationManagerCompat.from(this).areNotificationEnable();

2、還有一種方式就是通過反射擷取

/**     * 通過反射擷取通知的開關狀態     * @param context     * @return     */    public static boolean isNotificationEnabled(Context context){        AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);        ApplicationInfo appInfo = context.getApplicationInfo();        String pkg = context.getApplicationContext().getPackageName();        int uid = appInfo.uid;        Class appOpsClass = null; /* Context.APP_OPS_MANAGER */        try {            appOpsClass = Class.forName(AppOpsManager.class.getName());            Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);            Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);            int value = (int)opPostNotificationValue.get(Integer.class);            return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);        } catch (Exception e) {            e.printStackTrace();        }        return true;    }
這種方式呢實質就是通過AppOpsManager和AppOpsService去擷取位於/data/system/目錄下的檔案Appops.xml裡的資料。這一塊的流程我們待會再仔細描述一下。

好的,現在我們已經知道使用者的許可權了,如果確實被使用者禁止了,我們有以下三個處理方式

1、最友好的方式當然是給使用者一個彈窗,對我們為什麼需要通知作一下闡述,然後引導使用者去開啟許可權。

2、最不友好的方式就是通過AppOpsManager.setMode()方法去修改使用者的許可權

3、通過自己設計一個懸浮窗來替代系統的通知

第2種方式呢,在實踐的時候發現執行就會拋出異常,下面是異常資訊

SecurityException  java.lang.SecurityException: uid 10835 does not have android.permission.UPDATE_APP_OPS_STATS.

非系統應用都沒有修改許可權的許可權。如何知道是不是系統應用呢,就是這個uid了。看來Google已經把這條路給堵上了。

第3種方式應該是目前比較普遍的做法了,產品希望一定要展示,那就只能這麼繞彎子了。這裡直接看項目裡的OverlayUtils吧。不過懸浮窗又涉及到另一個懸浮窗的許可權,需要使用者開啟才行,這麼看來還是傾向於第一種讓使用者自己來決定吧。

這裡記錄一下,我測試了兩款手機

華為:開啟了通知,無論有沒有開啟懸浮窗許可權,都能彈出懸浮窗

   關閉了通知,需要開啟懸浮窗許可權才能彈出懸浮窗

小米:懸浮窗只受懸浮窗許可權控制,和是否開啟通知沒有關係


說完通知欄我們再看看,為什麼我把通知欄許可權禁止後,程式的Toast提示也都顯示不了了。

查閱源碼後發現 Toast裡也用到了NotificationManagerService。在Toast執行show()方法後,走到enqueueToast的時候有這麼一段代碼

if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {    if (!isSystemToast) {        Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");        return;    }}
原來如此,這裡也用到了檢測通知許可權的方法noteNotificationOp()。

Toast也被notification影響了,可是我們的程式裡Toast無處不在,因為通知許可權導致toast彈不出影響挺大的。那我們找找看替代方案吧,其實和通知類似。前面幾種就不說了。

其實也是用WindowManager 懸浮窗,只不過先通過系統toast拿到布局來用,這樣顯示效果就和系統toast一樣了。


五、安卓的許可權機制(6.0)

這裡說到安卓的許可權,我就想講講我還在實習的時候遇到的一個相關問題。balabalabala

當targetSdkVersion值為23以下(也就是android 6.0)的時候,許可權是在程式安裝的時候便詢問了使用者,並配置好。
但是當targetSdkVersion值為23或23以上的時候,許可權是當使用的時候才會詢問使用者,如果代碼不變的情況下,直接使用使用危險許可權,程式會直接崩潰
java.lang.SecurityException: Permission Denial
目前酷狗為21,暫時還不會出現這個問題
官方已經提供了一套流程來配合app與使用者之間的許可權互動。

那targetSdkVersion是否該提升?官方說當使用者裝置與targerSdkVersion一致的時候,程式運行效率會提高,因為會少處理很多相容性問題,有待考證。

我們來看看6.0下的許可權流程

左圖是標準流程,右圖是使用者操作了不再提示




鑒權(檢測許可權)這一步來說一說。這個之前提到過的AppOpsManager



Setting UI通過AppOpsManager與AppOpsService互動,給使用者提供入口管理各個app的操作。
 
AppOpsService具體處理使用者的各項設定,使用者的設定項儲存在 /data/system/appops.xml檔案中。
AppOpsService也會被注入到各個相關的系統服務中,進行許可權操作的檢驗。
 
各個許可權操作對應的系統服務(比如定位相關的Location Service,Audio相關的Audio Service等)中注入AppOpsService的判斷。如果使用者做了相應的設定,那麼這些系統服務就要做出相應的處理。
(比如,LocationManagerSerivce的定位相關介面在實現時,會有判斷調用該介面的app是否被使用者佈建成禁止該操作,如果有該設定,就不會繼續進行定位。)


那這個appops.xml檔案長啥樣呢,我們來看看


op標籤中
n標識許可權的opCode,
t表示時間戳記,
m標識許可權值mode,有三種
1.MODE_ALLOWED = 0;
2.MODE_IGNORED = 1;
3.MODE_ERRORED = 2;

如果沒有m值,則為預設值,每種許可權都有一種對應預設值,在AppOpsManager.sOpDefaultMode數組中,這是一個int數組,下標代表opCode,內容代表預設許可權值。其他屬性可以參考writeState方法一一對應


六、總結

一、通知的選擇
1.不依賴系統版本都要顯示同樣UI的可以使用自訂通知(例如酷狗播放通知)
2.需要與安卓系統版本UI保持一致的使用系統通知(例如酷狗訊息通知)
3.當你需要保護你的Service不被系統優先Kill掉,可以用service.startForeground(notification)
二、做通知欄拓展的時候儘可能考慮7.0的通知欄特性(因為這些都是官方針對人性化使用者體驗設計的)
三、當需要跨進程使用View的時候可以考慮RemoteViews
四、當通知許可權受阻,考慮使用替代方案(懸浮窗等)
五、建立完善的許可權詢問機制(針對targetSdkVersion,提高效率且提升使用者體驗)



Android Notification和許可權機制探討

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.