程序猿如何從產品的角度去提升應用的體驗之Android權限優化篇
前言:大家平時在開發的過程中是否會遇到這種情況:很多產品體驗上的細節,特別是涉及到技術相關的細節,產品與設計可能并不會給出詳細的解決方案,甚至可能并不太關注這方面的體驗細節。例如,應用的緩存清理機制該怎么實現?權限申請的時機應該放在哪?用戶沒有給予應用必要的權限該怎么處理......這種時候,作為一個開發人員,特別是對自家產品的使用體驗有追求的開發人員,其實完全可以充當一回產品,從產品的角度出發去思考,該怎樣在技術實現的細節上,讓自家的APP體驗變得更好。千萬不要小瞧這些細節,一個產品的***體驗,就是由無數的細節堆砌而成的。
1. 應用通知權限的優化
眾所周知,推送對于一個APP來說是很重要的功能。推送在好的產品設計中可以有效地提高產品活躍度,增加用戶的忠誠度以及留存率。但是,用戶有可能會在無意中把應用的通知權限給禁止了,導致收不到推送(用戶主動禁止應用的通知權限除外)。例如,華為手機會在通知中心直接提示用戶是否關掉某個應用的通知權限。如果用戶,特別是小白用戶一不小心把通知權限給禁止了,導致應用收不到推送,反而可能還會把這種情況當做bug來向客服反饋(任何時候都千萬不要高估用戶對于智能手機使用的了解,尤其是你的APP的目標用戶還包括中老年人的時候)。
華為手機的推送中心
如果大家有細心觀察的話會發現,當應用的通知權限被禁止的時候,體驗好的應用會在適當的時機以及場景下出現提示,告知用戶通知在應用中起到的作用,嘗試去消除用戶的不信任和謹慎心理,并引導用戶去打開通知權限。
那么,我們怎么知道自己的應用程序通知權限被禁止了呢?如果被禁止了又該怎么辦呢?下面就來說一下解決方案。
1.1 檢測應用的通知權限狀態
檢測應用的通知權限其實比較簡單。通過查詢 官方文檔 可以發現,在support庫的API 24.0.0 版本,已經有現成的方法可以直接查詢應用的通知權限狀態:
- NotificationManagerCompat.from(this).areNotificationEnable();
但是,這就意味著應用的 compileSdkVersion 也需要與support庫的版本保持一致。如果應用目前所使用的compileSdkVersion 低于 24.0.0 或者由于某些歷史原因而不能將compileSdkVersion 升到 24.0.0以上,那么就沒有辦法檢測到應用的通知權限了嗎?
其實,辦法還是有的。通過查看系統源碼,可以發現,NotificationManagerCompat.from(this).areNotificationEnable() 這個方法在不同版本的SDK上會有不同的實現。
- /**
- * Returns whether notifications from the calling package are not blocked.
- */
- public boolean areNotificationsEnabled() {
- return IMPL.areNotificationsEnabled(mContext, mNotificationManager);
- }
IMPL是一個實現了Impl接口的實現類對象,而且在靜態代碼塊中通過判斷手機系統所使用的版本號來初始化不同的實現類:
- static { if (BuildCompat.isAtLeastN()) {
- IMPL = new ImplApi24();
- } else if (Build.VERSION.SDK_INT >= 19) {
- IMPL = new ImplKitKat();
- } else if (Build.VERSION.SDK_INT >= 14) {
- IMPL = new ImplIceCreamSandwich();
- } else {
- IMPL = new ImplBase();
- }
- SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags();
- }
當手機系統Android版本小于4.4.0的時候, areNotificationEnable()方法會默認返回true。所以,此方法只有在4.4.0以上的手機系統上才能返回準確的結果。
- /**
- * Returns whether notifications from the calling package are blocked.
- */
- public boolean areNotificationsEnabled() {
- INotificationManager service = getService();
- try { return service.areNotificationsEnabled(mContext.getPackageName());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
這種反射的方式實際上就是通過AppOpsManager和AppOpsService去獲取位于/data/system/目錄下的文件Appops.xml里的數據。所以,這個系統源碼里的方法可以單獨抽取出來作為一個通用的檢測通知權限狀態的方法。有關AppOpsManager,這里先不做展開,等下在1.4章節單獨說一下。
1.2 引導用戶跳轉到通知權限設置界面
既然已經能檢測到應用的通知權限狀態,當應用的通知權限被禁止的時候,應該出現提示告知用戶通知在應用中起到的作用,并引導用戶去打開通知權限。以下為跳轉到通知權限設置的通用方法:
- public void toNotificationSetting() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Intent intent = new Intent();
- intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS");
- intent.putExtra("app_package", this.getPackageName());
- intent.putExtra("app_uid", this.getApplicationInfo().uid);
- startActivity(intent);
- } else if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
- Intent intent = new Intent();
- intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- intent.setData(Uri.parse("package:" + this.getPackageName()));
- startActivity(intent);
- }
- }
在5.0以上,可以直接跳轉到某個應用的通知權限快捷設置界面。但是在5.0以下,暫時還沒有找到可以直接跳轉到通知權限設置界面的方法,所以目前的做法是跳轉某個應用的設置界面,在設置界面列表中應該會有通知權限管理的入口。如果你有更好的做法,歡迎在評論中指出來。這里再另外拋出一個問題,供大家思考:關于通知權限提示的方案,應該在什么時機或場景下出現好?出現提示的頻率為多少好呢?是只提示一次呢,還是只要用戶沒打開通知權限就一直提示呢?歡迎大家在留言中說出自己的看法。
1.3 當通知權限被關閉后,Toast可能無法正常工作的問題
雖然跟通知權限的優化沒什么關系,不過在這里還是要提一下。這個是在調研通知權限優化問題的時候偶然發現的坑:在大部分的機型上,當應用的通知權限被關閉后,系統的 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也就無法正常工作。
對于Android手機來說,Toast在應用中隨處可見,如果因為通知權限導致Toast不工作那么影響還是挺大的。所以,在這里建議大家尋找一下Toast的替代方案,不要在項目中直接使用系統自帶的Toast。同樣的,如果大家有什么好的解決方案,也歡迎在留言中指出來。
1.4 AppOpsManager的工作原理
既然上面提到了AppOpsManager,那么這里來簡單地介紹一下它的工作原理。AppOpsManager的工作框架圖如下:
Setting UI通過AppOpsManager與AppOpsService 交互,給用戶提供入口管理各個app的操作。
AppOpsService具體處理用戶的各項設置,用戶的設置項存儲在 /data/system/appops.xml文件中。
AppOpsService也會被注入到各個相關的系統服務中,進行權限操作的檢驗。
各個權限操作對應的系統服務(比如定位相關的Location Service,Audio相關的Audio Service等)中注入AppOpsService的判斷。如果用戶做了相應的設置,那么這些系統服務就要做出相應的處理。比如,LocationManagerSerivce的定位相關接口在實現時,會有判斷調用該接口的app是否被用戶設置成禁止該操作,如果有該設置,就不會繼續進行定位。
2. 應用權限的提示優化
由于篇幅的原因,這里就拿相機權限來舉例。假設需求如下:
在需要使用相機的場景下,先提前檢測相機權限是否打開,如果沒有打開,則嘗試申請相機權限,如果用戶還是拒絕,則出現權限提示,引導用戶去開啟相機權限。下面是微信的處理方式:
(1)6.0系統以上,先嘗試申請相機權限,用戶點擊禁止后彈出引導界面
(2)6.0系統以下,用戶點擊保持禁止后彈出引導界面
在6.0以上,我們一般可以通過系統自帶的方法來檢測某個權限是否被允許:
- ActivityCompat.checkSelfPermission(context, permission)
但是,在6.0以下,如果想檢測相機權限,卻沒有一個很好的方法。***,經過查閱各種資料,發現好像只能通過一種簡單粗暴的方式去檢測相機權限:
- /**
- * 在6.0系統以下,通過嘗試打開相機的方式判斷有無拍照權限
- *
- * @return
- */
- private boolean checkCameraPermissionUnderM() {
- boolean isCanUse = true;
- Camera mCamera = null;
- try {
- mCamera = Camera.open();
- Camera.Parameters mParameters = mCamera.getParameters();
- mCamera.setParameters(mParameters);
- } catch (Exception e) {
- isCanUse = false;
- } if (mCamera != null) {
- try {
- mCamera.release();
- } catch (Exception e) {
- e.printStackTrace(); return isCanUse;
- }
- } return isCanUse;
- }
當使用這個方法的時候,在6.0以下的手機,一般調用 Camera.open()方法,如果相機權限沒打開,會先彈出相機權限申請彈框。如果點擊允許,那么該方法就會返回true,點擊禁止則返回false。這里需要指出的是,如果你使用相機的方式是通過Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)來跳轉到系統的相機界面的話,那么即使應用的相機權限被禁止了,也還是可以正常使用相機來拍照的。這種情況下要不要做權限檢查,就看個人的看法了。
不知道大家有沒注意到,微信在6.0以上和6.0以下彈出的提示對話框有點不同。在6.0以上提供“去設置”的選項,點擊會跳轉到設置里的應用列表界面,在6.0以下僅僅是提示。這也是一個產品的細節。個人看法,因為在6.0以下,有些國產系統的權限管理根本就不在設置里面,而是需要到官方提供的手機管家類型應用里面才可以進行權限的管理。那么多的國產系統,需要適配有點困難,如果沒有很好的解決方案,那么還不如不要擅自幫用戶做決定。可以看得出微信在跳轉到權限設置界面的適配上也經過了一番考量,***選擇了這種折中的方案。
說到這里,既然權限提示優化的思路已經有了,大家也可以在自己的項目中封裝一個權限管理類,“檢查權限-被禁止-嘗試申請權限-被拒絕-彈出提示框-引導用戶去打開權限”通過一個方法一氣呵成。
3. 總結
本文涉及到的知識點可能并不深奧,更多的是想向大家展示一下,假如開發人員從產品的角度去提升應用的體驗,可以從什么角度去切入。如果能給大家帶來一些啟發就好了。看完文章后,不凡思考一下,自己的應用在權限提示上的體驗是否做到***了呢?如果還有可以改善的地方,那么趕緊根據自家應用的實際情況,改善一下權限提示的體驗吧。相信我,做了這件事,你的用戶會感激你的。






























