Android Notification和許可權機制探討

來源:互聯網
上載者:User

標籤:att   har   _id   映射   comm   相容性問題   rup   版本號碼   center   

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


先看一下以下兩張圖

圖一:


看到這圖可能大家不太明確,這和我們的notification有什麼關係,我來簡介一下背景。這是發生在15年NBA季後賽期間,火箭隊對陣小牛隊,火箭隊以3:1率先,僅僅要再贏一場就能淘汰對手。這時候火箭隊的官方首席運營官發了這條官方推特。

翻譯一下就是 “一把槍指著小牛的隊標,哼哼,僅僅須要閉上你們額眼睛,立即就要結束了”。這條推特當時引起了非常多人的轉寄和評論,而且推送給了全部關注相關比賽的球迷以及媒體。

我們試想一下,你是一個小牛的球迷,輸了比賽以後本來心情就非常差。這時候手機一震,收到這條通知欄推送。你是不是會有一種強烈的被蔑視感覺。當天推特上就掀起了一陣網路爭議。不僅小牛球迷,其它中立球迷也表示這條推特諷刺意味十足,已經有侮辱對手的嫌疑了。當然了,通知欄表示我不背這個鍋,誰來背?第二天,這位首席運營官就被火箭官方開除了,並宣稱此推特僅代表前運營官個人意見與火箭隊無不論什麼關係。


圖二:





說完別人,再來說說我們自己吧。3月5日那天,群裡都在討論這條推送。本意是我們的編輯打算推一個分手相關的歌單,可是文案考慮不周全,讓人誤解。

導致非常多使用者感到莫名其妙,我們試想一下。你準備與你近期交往的對象一起吃個晚餐,出門前收到這條祝我們分手的通知。你是不是感到非常不爽呢。是的,在微博上隨手一搜就發現有非常多使用者是這樣的不爽的感覺了。當然了我拿這個對照並沒有說要炒這位編輯的魷魚噢。



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

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

一、Notification的使用

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

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

四、通知許可權問題

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

六、總結



一、Notification的使用

眼下咱們酷狗裡的通知使用主要有下面三種情境

1.訊息中心的通知

2.下載歌曲的通知

3.通過PlaybackService啟動的通知

以下簡單分析一下這三種情境的通知是怎樣實現的。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd3Vob25ncWkwMDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >

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字型的大小和顏色須要合理地配置,不然非常easy在不同系統中和其它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下的許可權流程

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd3Vob25ncWkwMDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >



鑒權(檢測許可權)這一步來說一說。

這個之前提到過的AppOpsManager

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd3Vob25ncWkwMDEy/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >


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.