1.引子
在换到Android手机之前,对Android系统的印象是这系统app的跑马场,app可以任意索取各种权限,随意窃取各种隐私,换手机后才知道Android系统对权限的管理已有很大的改观,索取的每个危险权限都需要提示用户,当然Android只是尽可能提示用户,还是存在着用户不同意就不给用的情况。
权限管理的改进给开发者增加了一定工作量,申请危险权限不再是简单的在AndroidManifest.xml添加一行代码的事,而是需要提示用户进行动态申请。了解动态申请之后,觉得没多复杂,但不了解时,只知道在网上拷贝现成代码片段,碰到需要申请新的权限就不知道从何下手。我曾经粘贴了几段动态申请权限代码,就是因为不了解原理,只会复制粘贴,代码虽然能够运行,但无比臃肿。
为此通过此文梳理Android权限,以便掌握Android权限管理,拒绝做Copy工程师。
2.权限的作用
权限的目的是保护Android用户的隐私。作为一个可以任意定制的开源系统,权限真是用户隐私最后的屏障,Android系统要求,在默认情况下,任何应用程序无权执行会对其他应用程序、操作系统或用户产生不利影响的任何操作。包括读写用户的私人数据(例如联系人或电子邮件),读写其他应用程序的文件,访问网络等等。要执行这些操作,Android应用必须获得对应的权限。
Android有上百种权限,可分为三大类,一类是普通权限,一类是签名权限,还有一类是危险权限。
其中普通权限是指那些不会威胁到用户安全和隐私的权限,例如设置闹钟权限。授予app普通权限只需要在AndroidManifest.xml文件里声明,系统会自动授权,不需要用户确认,用户也无法做撤销操作。
第二种是签名权限,这类权限同样是在应用安装时由系统授予,不过并不是无条件授予,想使用权限的应用程序必须与定义该权限的应用程序使用相同的证书才行,例如应用A使用证书A签名,定义了一个签名权限,应用B想使用这个权限,必须由证书A签名。签名权限使用的场景如一个公司有多个app,为了能让这些app能够相互调用,但不希望被外部app调用,就可以通过自定义一个签名调用权限实现。
最后再来看第三类危险权限,危险权限之所以被危险,是因为这类权限涉及到用户个人敏感数据资源,或者影响用户存储的数据或者其他应用程序的数据,例如读取用户联系人,打开麦克风等,江湖上一直有传言,目前许多app会偷偷打开麦克风窃听用户对话,然后根据对话内容精准推送广告,这类传言像是都市传说,因为具有神秘性能广泛传播,要知道,原版Android系统中,应用索取危险权限必须需要向用户提示,在用户许可之前,应用是无法获取权限的。不过Android系统毕竟是开源的,将原生Android权限管理功能修改倒是有可能虽然用户没同意,但app仍可以获得权限,这话题就在本文之外了,个人认为有其他更好的方法来窃取隐私,窃听对话是成本较高的那种,用起来不值得。
如前所述,权限有上百种,一一记住不现实,其实我们只要知道哪些是危险权限,危险权限之外都是普通权限。
危险权限总共9个分组,24个权限,如果我们申请的权限不在这个表中,那就说明是普通权限,而权限分组意思是组内的这些权限同属于一个组,用户同意授权一个权限组,则组内的所有权限都会授予,比如,应用被授予READ_EXTERNAL_STORAGE权限之后,如果再申请WRITE_EXTERNAL_STORAGE权限,系统会立即授予该权限,不再向用户提示。
3.申请权限步骤和示例
- 普通权限申请很简单,只需要在AndroidManifest.xml文件申请即可,例如申请设置闹铃的权限
<uses-permission android:name="android.permission.SET_ALARM" />
- 签名权限申请类似,只需要修饰protectionLevel成signature即可,例如设置一个自定义权限成签名权限,代码如下,其他应用申请这个权限时必须具有相同的签名才能授予
<permission android:name="com.test.permission.act" android:protectionLevel="signature"/>
- 申请危险权限重点说一下,一方面和普通权限一样,仍需要在AndroidManifest.xml文件声明,另一方面,还需要额外动态申请。下面以申请存储读写权限来作为例子说明。
-
在AndroidManifest文件中声明,这一步不可少
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
在应用启动代码中检查权限,确认应用是否已经拥有读写存储的权限。这部分是在代码中实现了,所以叫动态申请。Android中使用
ContextCompat.checkSelfPermission()
检查权限的授权情况,这个方法第一个参数是上下文Context,第二个参数是所要申请权限的名称,名称当然不能随便写阿猫阿狗,而是有特定名称,建议直接调Android在Manifest.permission
中已定义好的名称,比如现在要申请的读外部存储权限是Manifest.permission.WRITE_EXTERNAL_STORAGE
。该方法返回一个int类型的PERMISSION_GRANTED(同意)
或者PERMISSION_DENIED`(拒绝)
。一般来说,程序初次安装后,所申请的权限的时候都是处于PERMISSION_DENIED
状态,需要提出申请,用户同意之后不需要再次申请,除非卸载应用重新安装或者到系统设置里把权限给关了; -
接下来是申请权限,申请使用的方法
requestPermissions()
,该方法传入参数有三个,第一个是Activity、 第二个是需要请求授权的权限字符串数组,可以把想申请的权限名称都列入其中,第三个是识别权限请求的请求代码,在权限申请的回调函数onRequestPermissionsResult()
用得着,该值必须大于或等于0,其作用在回调函数里再详细说明,这里只要理解成是一个自定义的标记。需要注意的是,申请权限是异步处理,也就是说,代码不会阻塞在等用户点击,而是用户点击后再回调onRequestPermissionsResult()
。以上两个步骤的代码如下:
-
//检查应用是否已经拥有权限
if(ActivityCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
// 说明没有该权限,就需要申请权限
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}else {
//已经拥有权限,做进一步操作
}
- 接下来是覆写申请权限回调函数
onRequestPermissionsResult()
,对权限申请做后续处理,该回调函数有三个返回结果,第一个requestCode
,这个参数就是在requestPermissions()
提到的识别权限的请求代码,作用就是一个回执单号,根据这个单号我们能确定是我们申请权限的处理结果,也就是说,在申请权限时传入这个参数,在处理结束后,回调函数原封不动给回来,这样我们就知道是不是我们的处理结果;第二个参数是权限字符串数组,也就是我们在requestPermissions()
填的权限字符串数组;第三个参数用户响应的数组,用户的对每个申请权限的批复情况就是在这个数组里,这个数组的个数是和申请权限数组是一一对应的。代码如下:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
//通过requestCode来识别是否同一个请求
if (requestCode == 1){
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
//用户同意执行的操作
}else{
//用户不同意不同意执行的操作,例如提示用户不同意不给用。。。
}
}
}
以上如果申请单个权限,如果申请多个权限又怎么操作呢?基本步骤和申请单个权限类似,但因为是多个权限,还需要做一下额外处理,例如怎么判断是否每个权限都授权了。下面以多申请一个录音权限来说明
- 同样,首先在AndroidManifest.xml做声明
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
- 准备工作是创建一个权限字符串数组和声明一个权限列表list,其中列表用于判断每个权限的处理情况
String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE}; List<String> mPermissionList = new ArrayList<>();
- 判断每个权限授权情况,未同意的列入申请名单,然后申请权限
private void myRequestPermission() {
mPermissionList.clear();
//逐个判断权限是否已经通过
for (int i = 0; i < permissions.length; i++) {
if (ContextCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
//如果未授权,则添加到列表中
mPermissionList.add(permissions[i]);
}
}
//申请权限,如果列表不为空,说明有权限需要申请
if (mPermissionList.size() > 0) {
//有权限没有通过,需要申请
ActivityCompat.requestPermissions(this, permissionsList.toArray(new String[permissionsList.size()]), 1);
}else{
//已有权限,做进一步操作
}
}
- 覆写请求权限回调的方法,做后续处理
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
int resultLength = grantResults.length;
//说明回调成功了,权限授权被允许
if(resultLength > 0){
for(int grantCode : grantResults){
if(grantCode == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show();
}
}
}
break;
default:
break;
}
}
4.总结
经过了解Android权限后,可以看出申请权限过程不是很复杂,普通权限比较简单,只需要在AndroidManifest.xml中声明即可,危险权限不仅需要在AndroidManifest.xml声明,同时还需要在代码中动态申请,首先是使用checkSelfPermission()
检查是否某个危险权限是否已授权,如果没有授权,则requestPermissions()
申请授权,记得这是异步处理,代码不会阻塞在等待用户授权位置,接下来就是处理授权情况,系统回调onRequestPermissionsResult
通知我们处理结果,我们根据这个结果做进一步处理。同时申请多个权限稍微复杂一点,需要处理每个权限的处理情况。