前言:大家平时在开发的过程中是否会遇到这种情况:很多产品体验上的细节,特别是涉及到技术相关的细节,产品与设计可能并不会给出详细的解决方案,甚至可能并不太关注这方面的体验细节。例如,应用的缓存清理机制该怎么实现?权限申请的时机应该放在哪?用户没有给予应用必要的权限该怎么处理......这种时候,作为一个开发人员,特别是对自家产品的使用体验有追求的开发人员,其实完全可以充当一回产品,从产品的角度出发去思考,该怎样在技术实现的细节上,让自家的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. 总结
本文涉及到的知识点可能并不深奥,更多的是想向大家展示一下,假如开发人员从产品的角度去提升应用的体验,可以从什么角度去切入。如果能给大家带来一些启发就好了。看完文章后,不凡思考一下,自己的应用在权限提示上的体验是否做到最好了呢?如果还有可以改善的地方,那么赶紧根据自家应用的实际情况,改善一下权限提示的体验吧。相信我,做了这件事,你的用户会感激你的。