Android 运行时权限处理
转载请标明出处:
;
本文出自:
一、概述
随着android 6.0发布以及普及,我们开发者所要应对的主要就是新版本sdk带来的一些变化,首先关注的就是权限机制的变化。对于6.0的几个主要的变化,查看查看官网的这篇文章,其中当然包含runtime permissions。
ok,本篇文章目的之一就是对运行时权限处理的一个介绍,以及对目前权限相关的库的一些了解。
当然非常推荐阅读官网权限相关文章:
本文也是在上述文章基础上理解、实验以及封装。
二、运行时权限的变化及特点
对于6.0以下的权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装,造成了我们想要使用某个app,就要默默忍受其一些不必要的权限(比如是个app都要访问通讯录、短信等)。而在6.0以后,我们可以直接安装,当app需要我们授予不恰当的权限的时候,我们可以予以拒绝(比如:单机的象棋对战,请求访问任何权限,我都是不同意的)。当然你也可以在设置界面对每个app的权限进行查看,以及对单个权限进行授权或者解除授权。
新的权限机制更好的保护了用户的隐私,google将权限分为两类,一类是normal permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是dangerous permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等。
- normal permissions如下
access_location_extra_commands access_network_state access_notification_policy access_wifi_state bluetooth bluetooth_admin broadcast_sticky change_network_state change_wifi_multicast_state change_wifi_state disable_keyguard expand_status_bar get_package_size install_shortcut internet kill_background_processes modify_audio_settings nfc read_sync_settings read_sync_stats receive_boot_completed reorder_tasks request_install_packages set_alarm set_time_zone set_wallpaper set_wallpaper_hints transmit_ir uninstall_shortcut use_fingerprint vibrate wake_lock write_sync_settings
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- dangerous permissions:
group:android.permission-group.contacts permission:android.permission.write_contacts permission:android.permission.get_accounts permission:android.permission.read_contacts group:android.permission-group.phone permission:android.permission.read_call_log permission:android.permission.read_phone_state permission:android.permission.call_phone permission:android.permission.write_call_log permission:android.permission.use_sip permission:android.permission.process_outgoing_calls permission:com.android.voicemail.permission.add_voicemail group:android.permission-group.calendar permission:android.permission.read_calendar permission:android.permission.write_calendar group:android.permission-group.camera permission:android.permission.camera group:android.permission-group.sensors permission:android.permission.body_sensors group:android.permission-group.location permission:android.permission.access_fine_location permission:android.permission.access_coarse_location group:android.permission-group.storage permission:android.permission.read_external_storage permission:android.permission.write_external_storage group:android.permission-group.microphone permission:android.permission.record_audio group:android.permission-group.sms permission:android.permission.read_sms permission:android.permission.receive_wap_push permission:android.permission.receive_mms permission:android.permission.receive_sms permission:android.permission.send_sms permission:android.permission.read_cell_broadcasts
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
可以通过adb shell pm list permissions -d -g
进行查看。
看到上面的dangerous permissions,会发现一个问题,好像危险权限都是一组一组的,恩,没错,的确是这样的,
那么有个问题:分组对我们的权限机制有什么影响吗?
的确是有影响的,如果app运行在android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对read_contacts
已经授权了,当你的app申请write_contacts
时,系统会直接授权通过。此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。
不过需要注意的是,不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。
三、相关api
好在运行时相关的api也比较简单,所以适配起来并不会非常痛苦。
api的讲解就跟着申请权限步骤一起了:
-
在androidmanifest文件中添加需要的权限。
这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。
-
检查权限
if (contextcompat.checkselfpermission(thisactivity, manifest.permission.read_contacts) != packagemanager.permission_granted) { }else{ // }
- 1
- 2
- 3
- 4
- 5
- 6
这里涉及到一个api,
contextcompat.checkselfpermission
,主要用于检测某个权限是否已经被授予,方法返回值为packagemanager.permission_denied
或者packagemanager.permission_granted
。当返回denied就需要进行申请授权了。 -
申请授权
activitycompat.requestpermissions(thisactivity, new string[]{manifest.permission.read_contacts}, my_permissions_request_read_contacts);
- 1
- 2
- 3
该方法是异步的,第一个参数是context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestcode,主要用于回调的时候检测。可以从方法名
requestpermissions
以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。 -
处理权限申请回调
@override public void onrequestpermissionsresult(int requestcode, string permissions[], int[] grantresults) { switch (requestcode) { case my_permissions_request_read_contacts: { // if request is cancelled, the result arrays are empty. if (grantresults.length > 0 && grantresults[0] == packagemanager.permission_granted) { // permission was granted, yay! do the // contacts-related task you need to do. } else { // permission denied, boo! disable the // functionality that depends on this permission. } return; } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
ok,对于权限的申请结果,首先验证requestcode定位到你的申请,然后验证grantresults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果你同时申请两个权限,那么grantresults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~
当然,到此我们的权限申请的不走,基本介绍就如上述。不过还有个api值得提一下:
// should we show an explanation? if (activitycompat.shouldshowrequestpermissionrationale(thisactivity, manifest.permission.read_contacts)) // show an expanation to the user *asynchronously* -- don't block // this thread waiting for the user's response! after the user // sees the explanation, try again to request the permission. }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
这个api主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。
那么将上述几个步骤结合到一起就是:
// here, thisactivity is the current activity if (contextcompat.checkselfpermission(thisactivity, manifest.permission.read_contacts) != packagemanager.permission_granted) { // should we show an explanation? if (activitycompat.shouldshowrequestpermissionrationale(thisactivity, manifest.permission.read_contacts)) { // show an expanation to the user *asynchronously* -- don't block // this thread waiting for the user's response! after the user // sees the explanation, try again to request the permission. } else { // no explanation needed, we can request the permission. activitycompat.requestpermissions(thisactivity, new string[]{manifest.permission.read_contacts}, my_permissions_request_read_contacts); // my_permissions_request_read_contacts is an // app-defined int constant. the callback method gets the // result of the request. } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
四、简单的例子
这里写一个简单的例子,针对于运行时权限。相信大家在最开始接触android的时候,都利用intent实验了打电话、发短信等功能。
我们看看直接拨打电话在android 6.x的设备上权限需要如何处理。
当然代码非常简单:
package com.zhy.android160217; import android.manifest; import android.content.intent; import android.content.pm.packagemanager; import android.net.uri; import android.os.bundle; import android.support.v4.app.activitycompat; import android.support.v4.content.contextcompat; import android.support.v7.app.appcompatactivity; import android.view.view; import android.widget.toast; public class mainactivity extends appcompatactivity { private static final int my_permissions_request_call_phone = 1; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); } public void testcall(view view) { if (contextcompat.checkselfpermission(this, manifest.permission.call_phone) != packagemanager.permission_granted) { activitycompat.requestpermissions(this, new string[]{manifest.permission.call_phone}, my_permissions_request_call_phone); } else { callphone(); } } public void callphone() { intent intent = new intent(intent.action_call); uri data = uri.parse("tel:" + "10086"); intent.setdata(data); startactivity(intent); } @override public void onrequestpermissionsresult(int requestcode, string[] permissions, int[] grantresults) { if (requestcode == my_permissions_request_call_phone) { if (grantresults[0] == packagemanager.permission_granted) { callphone(); } else { // permission denied toast.maketext(mainactivity.this, "permission denied", toast.length_short).show(); } return; } super.onrequestpermissionsresult(requestcode, permissions, grantresults); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
在android 6.x上运行是,点击testcall,即会弹出授权窗口,如何你allow则直接拨打电话,如果denied则toast弹出”permission denied”。
例子很简单,不过需要注意的是,对于intent这种方式,很多情况下是不需要
授权的甚至权限都不需要的,比如:你是到拨号界面而不是直接拨打电话,就不需要去申请权限;打开系统图库去选择照片;调用系统相机app去牌照等。更多请参考consider using an intent
.
当然,上例也说明了并非所有的通过intent的方式都不需要申请权限。一般情况下,你是通过intent打开另一个app,让用户通过该app去做一些事情,你只关注结果(onactivityresult),那么权限是不需要你处理的,而是由打开的app去处理。
五、封装
(1)申请权限
虽然权限处理并不复杂,但是需要编写很多重复的代码,所以目前也有很多库对用法进行了封装,大家可以在github首页搜索:android permission
,对比了几个库的使用方式,发现https://github.com/lovedise/permissiongen这个库据我所见相比较而言使用算是比较简单的,那么就以这个库的源码为基础来讲解,大家有兴趣可以多看几个库的源码。
封装的代码很简单,正如大家的对上面的权限代码所见,没有特别复杂的地方。
对于申请权限的代码,原本的编写为:
if (contextcompat.checkselfpermission(this, manifest.permission.call_phone) != packagemanager.permission_granted) { activitycompat.requestpermissions(this, new string[]{manifest.permission.call_phone}, my_permissions_request_call_phone); } else { callphone(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
大家可以看到,对于其他的权限,其实申请的逻辑是类似的;唯一不同的肯定就是参数,那么看上面的代码,我们需要3个参数:activity|fragment
、权限字符串数组
、int型申请码
。
也就是说,我们只需要写个方法,接受这几个参数即可,然后逻辑是统一的。
public static void needpermission(fragment fragment, int requestcode, string[] permissions) { requestpermissions(fragment, requestcode, permissions); } @targetapi(value = build.version_codes.m) private static void requestpermissions(object object, int requestcode, string[] permissions) { if (!utils.isovermarshmallow()) { doexecutesuccess(object, requestcode); return; } list<string> deniedpermissions = utils.finddeniedpermissions(getactivity(object), permissions); if (deniedpermissions.size() > 0) { if (object instanceof activity) { ((activity) object).requestpermissions(deniedpermissions.toarray(new string[deniedpermissions.size()]), requestcode); } else if (object instanceof fragment) { ((fragment) object).requestpermissions(deniedpermissions.toarray(new string[deniedpermissions.size()]), requestcode); } else { throw new illegalargumentexception(object.getclass().getname() + " is not supported"); } } else { doexecutesuccess(object, requestcode); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
utils.finddeniedpermissions
其实就是check没有授权的权限。
#utils @targetapi(value = build.version_codes.m) public static list<string> finddeniedpermissions(activity activity, string... permission) { list<string> denypermissions = new arraylist<>(); for (string value : permission) { if (activity.checkselfpermission(value) != packagemanager.permission_granted) { denypermissions.add(value); } } return denypermissions; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
那么上述的逻辑就很清晰了,需要的3种参数传入,先去除已经申请的权限,然后开始申请权限。
ok,我相信上面代码,大家扫一眼就可以了解了。
(2)处理权限回调
对于回调,因为要根据是否授权去执行不同的事情,所以很多框架也需要些一连串的代码,或者和前面的申请代码耦合。不过这个框架还是比较方便的,也是我选择它来讲解的原因。
回调主要做的事情:
- 了解是否授权成功。
- 根据授权情况进行回调。
对于第一条逻辑都一样;对于第二条,因为涉及到两个分支,每个分支执行不同的方法。
对于第二条,很多框架选择将两个分支的方法在申请权限的时候进行注册,然后在回调中根据requestcode进行匹配执行,不过这样需要在针对每次申请进行对象管理。
不过这个框架采取了一种很有意思的做法,它利用注解去确定需要执行的方法,存在两个注解:
@permissionsuccess(requestcode = 100) @permissionfail(requestcode = 100)
- 1
- 2
利用反射根据授权情况+requestcode即可找到注解标注的方法,然后直接执行即可。
大致的代码为:
@override public void onrequestpermissionsresult(int requestcode, string[] permissions, int[] grantresults) { permissiongen.onrequestpermissionsresult(this, requestcode, permissions, grantresults); } private static void requestresult(object obj, int requestcode, string[] permissions, int[] grantresults) { list<string> deniedpermissions = new arraylist<>(); for (int i = 0; i < grantresults.length; i++) { if (grantresults[i] != packagemanager.permission_granted) { deniedpermissions.add(permissions[i]); } } if (deniedpermissions.size() > 0) { doexecutefail(obj, requestcode); } else { doexecutesuccess(obj, requestcode); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
首先根据grantresults进行判断成功还是失败,对于成功则:
private static void doexecutesuccess(object activity, int requestcode) { method executemethod = utils.findmethodwithrequestcode(activity.getclass(), permissionsuccess.class, requestcode); executemethod(activity, executemethod); } #utils public static <a extends annotation> method findmethodwithrequestcode(class clazz, class<a> annotation, int requestcode) { for (method method : clazz.getdeclaredmethods()) { if (method.isannotationpresent(annotation)) { if (isequalrequestcodefromanntation(method, annotation, requestcode)) { return method; } } } return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
根据注解和requestcode找到方法,然后反射执行即可。失败的逻辑类似,不贴代码了。
ok,到此我们的运行时权限相对于早起版本的变化、特点、以及如何处理和封装都介绍完了。
不过对于上面讲解的库,肯定有人会说:运行时反射会影响效率,没错,不过我已经在上述代码的基础上将运行时注解改成annotation processor的方式了,即编译时注解,这样的话,就不存在反射损失效率的问题了。本来准备fork修改,然后pr,结果写完,改动太大,估计pr是不可能通过了,所以另起项目了,也方便后面的做一些扩展和维护。
详见库:https://github.com/hongyangandroid/mpermissions.
六、mpermissions用法
对外的接口和permissiongen基本一致,因为申请只需要三个参数,抛弃了使用原本类库的单例的方式,直接一个几个静态方法,简单整洁暴力。
贴一个用法:
public class mainactivity extends appcompatactivity { private button mbtnsdcard; private static final int requect_code_sdcard = 2; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); mbtnsdcard = (button) findviewbyid(r.id.id_btn_sdcard); mbtnsdcard.setonclicklistener(new view.onclicklistener() { @override public void onclick(view v) { mpermissions.requestpermissions(mainactivity.this, requect_code_sdcard, manifest.permission.write_external_storage); } }); } @override public void onrequestpermissionsresult(int requestcode, string[] permissions, int[] grantresults) { mpermissions.onrequestpermissionsresult(this, requestcode, permissions, grantresults); super.onrequestpermissionsresult(requestcode, permissions, grantresults); } @permissiongrant(requect_code_sdcard) public void requestsdcardsuccess() { toast.maketext(this, "grant access sdcard!", toast.length_short).show(); } @permissiondenied(requect_code_sdcard) public void requestsdcardfailed() { toast.maketext(this, "deny access sdcard!", toast.length_short).show(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
是不是简单明了~~对于onrequestpermissionsresult所有的activity都是一致的,所以可以放到baseactivity中去。此外,在fragment中使用的方式一致,详见demo。
详见库:https://github.com/hongyangandroid/mpermissions.
至于为什么不直接介绍mpermission的源码,因为主要涉及到annotation processor,所以以这种方式引出,后面考虑单篇博文介绍下我目前所会的编译时注解的相关做法以及api的使用。
最后感谢permissiongen
库,祝大家元宵节快乐~!
上一篇: 单例模式(3)
下一篇: L1-039 古风排版