目录
- 简介
- 查看危险权限
- 请求方式
- 权限的演变
- 项目地址
- 优秀权限申请库
简介
不知道你对上面的安装场景是否还有印象,想当年智能机刚刚流行起来的时候,安装应用时,总会有一个界面上展示着安装应用所需要的权限,点击继续就代表你已经同意了以上所有权限(此界面跟以前稍微有所不同,以前在Android6.0(API级别23)之前,在安装界面权限不能操作,界面大概就是这样的,懂我意思就行😊),可能一个功能很单一的App,也要请求一屏都滑不完的权限列表,用来收集一些用户的隐私信息,所以在Android6.0(API级别23)以上,一些危险权限就不能在安装应用的一股脑全部允许了,需要在App的场景用到的地方动态的去申请,区别于之前的安装完成就拥有所有权限,动态申请可以让用户的隐身得到更好的保护;因为用户是参与其中的,并有权选择同意或拒绝。对用户而言是交互上的变化,对Android开发来说,是一种新的代码逻辑处理,下面我们就说一下如何在代码中实现运行时权限的申请。
查看危险权限
在实现运行时权限之前,我们还要做一件事,就是要先知道,哪些权限是危险权限,需要动态的申请。在官方的开发者文档中有告诉我们如何查看危险权限;查看危险权限
连接上手机,在终端输入以下命令,就可以看到以组列出的危险权限
adb shell pm list permissions -d -g
➜ ~ adb shell pm list permissions -d -g
Dangerous Permissions:
group:com.google.android.gms.permission.CAR_INFORMATION
permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
permission:com.google.android.gms.permission.CAR_MILEAGE
permission:com.google.android.gms.permission.CAR_FUEL
group:android.permission-group.CONTACTS
group:android.permission-group.PHONE
group:android.permission-group.CALENDAR
group:android.permission-group.CALL_LOG
group:android.permission-group.CAMERA
...
上面列出的是一组一组的危险权限,为什么是组,先按下不表,在后面的演变中会再说;这样就知道哪些是危险权限了,在应用中使用到时,就要想着使用运行时权限了。
请求方式
先说之前的请求方式
知道了危险权限有哪些,接下来就是请求了;请求方式很简单,在清单文件注册是少不了的。接着在调用功能之前,进行运行时权限的请求和判断,我们以打电话为例,来写一段代码:
//在Mainfest文件中添加需要用到的权限
<uses-permission android:name="android.permission.CALL_PHONE" />
public void callPhone(View view) {
//请求权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
//权限第一次被拒绝后,第二次申请时shouldShowRequestPermissionRationale方法会返回true
if (ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.CALL_PHONE)) {
DialogUtils.showDialog(this, new DialogClickListener() {
@Override
public void ok() {
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},CALL_PHONE_REQUEST_CODE);
}
});
}else {
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CALL_PHONE},CALL_PHONE_REQUEST_CODE);
}
}else {
call();
}
}
private void call() {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel://18568655367"));
startActivity(intent);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case CALL_PHONE_REQUEST_CODE:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
}else {
Toast.makeText(MainActivity.this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
申请权限
在点击事件中先调用ContextCompat.checkSelfPermission()
方法检查是否有电话权限,该方法的返回值是PERMISSION_GRANTED
或 PERMISSION_DENIED
分别表示已授权和未授权;根据返回值判断如果没有授权就调用ActivityCompat.requestPermissions()
方法发起权限请求,其中的参数CALL_PHONE_REQUEST_CODE
是自定义的一个请求码,在下面的onRequestPermissionsResult
回调方法中会用到。作用和startActivityForResult
方法中的requestCode
类似。如果判断有权限就直接打电话。
接着就会弹出一个弹框,显示应用要请求的权限,用户可以选择允许
或拒绝
,选择的结果会在onRequestPermissionsResult
回调方法中处理。
处理权限
在onRequestPermissionsResult
回调方法中刚才提到的requestCode
就要用上了,因为考虑到在一个页面中可能会有不同权限的请和逻辑处理,所以可以用requestCode
很好的区分;在方法中判断如果code
相等,判断grantResults
数组的长度,grantResults
数组就是我们上面请求传入new String[]{Manifest.permission.CALL_PHONE}
数组,因为我们传入的只有一个权限,所以获取第一个,判断是否已经授权,如果授权调用call()
方法拨打电话,其他情况就是权限被拒绝,给出相应的提示。
以上就是请求运行时权限的过程,上面只是请求一个权限,如果请求多个权限,只需在数组中继续添加权限,对应的在onRequestPermissionsResult
回调方法处理就行。
ActivityCompat.shouldShowRequestPermissionRationale()方法
请求权限代码中,在判断是否有某一权限之后,接着调用了ActivityCompat.shouldShowRequestPermissionRationale()
方法的判断,这个方法在用户拒绝了一次权限以后,再次申请此权限,该方法会返回true
,用意是当用户不理解为什么申请此权限而拒绝时,应用可以给出一个解释,让用户知道,我要申请此权限的用途,在用户明白之后再次申请权限可以提升同意此权限的概率。
再说说AndroidX包提供的请求方式
接着我们再说说另一种方式,这种方式是AndroidX包中提供的一种请求方式,和上面的方法基本上差不多,少了回调部分,下面还是以打电话为例:
private ActivityResultLauncher<String> requestPermissionLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
call();
} else {
Toast.makeText(this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
}
});
}
@RequiresApi(api = Build.VERSION_CODES.M)
public void secondCall(View view) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
call();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.CALL_PHONE)) {
DialogUtils.showDialog(this, new DialogClickListener() {
@Override
public void ok() {
requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE);
}
});
} else {
requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE);
}
}
private void call() {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel://12345678"));
startActivity(intent);
}
创建registerForActivityResult并处理
和前面的不同,AndroidX中提供的请求方式,不需要重写onRequestPermissionsResult
方法,而是将处理结果放在了registerForActivityResult
方法中处理,根据isGranted
字段判断权限是否已授权。
申请权限
申请权限和前面判断的类似,registerForActivityResult
方法返回一个ActivityResultLauncher
对象,利用ActivityResultLauncher.launch()
方法,进行权限的申请。
多权限申请
上面只是一个权限的申请,如果有多个权限怎么办呢,需要替换一下类型
// 1、将String替换成String[]
private ActivityResultLauncher<String[]> requestPermissionLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
// 2、将RequestPermission替换成RequestMultiplePermissions
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), map -> {
// 3、isGranted的类型由boolean变成map,map的键值对是<String,Boolean>
//String对应的是权限,Boolean对应的是是否授权,需要判断处理
if (map.size() > 0
&& map.get(Manifest.permission.CALL_PHONE)
&& map.get(Manifest.permission.CAMERA)
) {
call();
} else {
Toast.makeText(this,"电话权限被拒绝",Toast.LENGTH_SHORT).show();
}
});
}
@RequiresApi(api = Build.VERSION_CODES.M)
public void secondCall(View view) {
//4、检测权限也需要判断多个,用&&符号
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
) {
call();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.CALL_PHONE)) {
DialogUtils.showDialog(this, new DialogClickListener() {
@Override
public void ok() {
// 5、launch方法中参数由String变成String[]
requestPermissionLauncher.launch(new String[]{Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA});
}
});
} else {
requestPermissionLauncher.launch(new String[]{Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA});
}
}
替换部分有5个点
- 将String替换成
String[]
- 将RequestPermission替换成
RequestMultiplePermissions
- isGranted的类型由boolean变成
map
,map
的键值对是<String,Boolean>
;String对应的是权限,Boolean对应的是是否授权,对判断也需要变更。 - 检测权限也需要判断多个,用
&&
符号 - launch方法中参数由String变成
String[]
注意
在使用AndroidX包申请运行时权限时,可能会遇到这样一个bug
java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
查了一下这个错误说是因为requestCode
不能为负数或者大于65536;讲道理,我们这种方式在代码中并没有用到requestCode
,那为什么有这种问题呢?坦白的说:我不知道。我在网上找到了一个答案解决了这个问题。
在build.gradle
文件里面添加以下两个依赖:
implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
implementation 'androidx.fragment:fragment:1.3.0-alpha08'
添加这两个依赖就能解决问题了。如果有哪位小伙伴知道问题的原因,还请告知,感谢😊
权限的演变
知道怎么请求权限以后,再聊一聊Android6.0到Android11这几个版本中针对运行时权限做了哪些修改,也让我们在开发中更好的去做适配。
如果tragetSdkVersion<=21
在build.gradle
文件里面targetSdkVersion
表示应用最高兼容到的Android版本,就是使用API最高的版本。上面说了在Android6.0开始才有的运行时权限,6.0对应的API是23
,那如果我们把项目的targetSdkVersion
改成21
,这时候还没有运行时权限,如果在手机上运行会是什么样呢?
文章开头你看到的图片就是targetSdkVersion
改成21
的场景,所有的权限都在安装时自动授权,安装好即获得清单文件中的全部权限,但是在继续安装的时候系统会有提示框:
而且页面在手机上也不是全屏,下方会有全屏展示的的按钮,点击以后可能会全屏,也可能会闪退(不同品牌手机上安装,展示的界面可能不一样,上图仅做参考)。
如果这时候有了升级包,新包的版本是6.0以上的API,你把app升级了,调用用到危险权限的功能,也是可以调用的,因为在5.0的时候,已经把权限全部都给了,所以不需要重新申请。但是如果卸载重新安装或把权限在应用信息页面关闭,如果使用到这一危险权限程序就会报异常,需要进行运行时权限处理。
权限弹框的变化
首先申请权限的弹框框,在开发中不可自定义,由系统提供。在官方的文档中也有说明 在Android6.0、7.0时候的权限弹框,只要请求就可以弹出,不管拒绝多少次;在弹框上有一个“不再提示”的小的选择框,只有在拒绝权限时选中了此选项,下次再请求就不会再弹出权限弹框了。
在Android8.0、9.0、10的系统上,差别不大,也是只要请求就会展示权限申请弹框;不同点在于弹框的选择上,在这些系统版本上,第一次请求权限时,没有“不在提示”的选项,只有“允许”和“拒绝”,但是如果第一请求该运行时权限,用户拒绝了,那么从第二次申请开始,在弹框中就会显示三个选项,分别是“允许”、“拒绝”、“拒绝并不再提示”;如果用户选择了第三个,之后这个权限就会一直不被授权,也无法调起权限弹框,只能在应用的详情页面,手动授权。
在Android11系统上,又有了新的变化;在权限弹框中同样没有“不再提示”的小的选择框,但是在权限被拒绝后,也不会有“拒绝并不再提示”这样的选项,如果连续拒绝两次之后,就不会再显示权限弹框,权限会返回被拒绝,同样后续也只能在应用的详情页面,手动授权。相当于此刻系统帮我们勾选了“不再提示”的选项;所以可以利用好上面提到的shouldShowRequestPermissionRationale
方法,在用户第一次拒绝了申请后,要向用户解释明白为什么要申请此权限。
Tips:因为手上只有6.0、7.0和10.0系统的手机,所以弹框第二次变化的情况,不是很确定,如果描述有误,还请大佬指正,我会在第一时间修改。另外国内手机厂商众多,定制的UI系统也各有千秋,在展示形式和文字描述上可能有所不同,但大致就是上面说的这些情况,不影响我们的适配和代码逻辑判断。
权限组
在上面命令行中查看危险权限的时候,我们知道危险权限都是以组的形式的存在的,比如READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAGE
两者同属于STORAGE
组,在Android8.0以前(Api < 25),如果我们申请READ_EXTERNAL_STORAGE
权限,系统则会将STORAGE
组的权限都给我们,也就是说我们可以同时得到READ_EXTERNAL_STORAGE
和WRITE_EXTERNAL_STORAGE
两个权限(两个权限在清单文件中都有声明),拿到权限后可以进行读写操作;但是从Android8.0开始(Api >= 25)有变化了,当我们申请其中一个权限时,系统只会给当前申请的权限,并不会将整个组都给到我们;如果后续再申请组中的另一个权限,系统会立马同意,中间不会通知用户,也就是说不会有弹框提示。
那这情况对我们开发有什么影响呢?举个例子,如果我们在代码中申请了READ_EXTERNAL_STORAGE
权限,在权限同意之后,对内存进行读写操作,如果是运行在8.0系统之前的手机上,那么是没有问题,可以正常的使用。如果是在8.0或更高系统的手机上,就行不通了。因为申请哪个权限就只给哪一个,不会将权限组都给。
解决办法:在申请危险权限时,可以将整个权限组的权限一起申请,这样用户体验方面也只是给出一次弹框,同时也可以兼容到所有的Android版本。最后别忘记在清单文件中声明要申请的权限。
项目地址
项目中有上面示例的所有代码,还有一个根据郭霖大佬的视频简单封装的一个BaseActivity,可以简化调用的流程。
优秀权限申请库
Google可能自己也感觉,权限申请的步骤有些繁琐,所以就封装了一个类,大家感兴趣可以看看 EasyPermissions
郭霖大佬的PermissionX