前言:大家平時在開發的過程中是否會遇到這種情況:很多產品體驗上的細節,特別是涉及到技術相關的細節,產品與設計可能並不會給出詳細的解決方案,甚至可能並不太關注這方面的體驗細節。例如,應用的快取清理機制該怎麼實現?許可權申請的時機應該放在哪?使用者沒有給予應用必要的許可權該怎麼處理......這種時候,作為一個開發人員,特別是對自家產品的使用體驗有追求的開發人員,其實完全可以充當一回產品,從產品的角度出發去思考,該怎樣在技術實現的細節上,讓自家的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
以上的手機系統上才能返回準確的結果。
ImplApi24
類中areNotificationEnable()
的實現如下:
/**
* 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();
}
}複製程式碼
ImplKitKat
類中areNotificationEnable()
的實現如下:
public static boolean areNotificationsEnabled(Context context) {
AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
try {
Class<?> 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(appOps, value, uid, pkg)
== AppOpsManager.MODE_ALLOWED);
} catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException |
InvocationTargetException | IllegalAccessException | RuntimeException e) {
return true;
}
}複製程式碼
這種反射的方式實際上就是通過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. 總結
本文涉及到的知識點可能並不深奧,更多的是想向大家展示一下,假如開發人員從產品的角度去提升應用的體驗,可以從什麼角度去切入。如果能給大家帶來一些啟發就好了。看完文章後,不凡思考一下,自己的應用在許可權提示上的體驗是否做到最好了呢?如果還有可以改善的地方,那麼趕緊根據自家應用的實際情況,改善一下許可權提示的體驗吧。相信我,做了這件事,你的使用者會感激你的。