运行时权限
Api23开始,Android权限机制更改,有一部分权限不再是简单的在AndroidManifest.xml中声明即可。而是需要在运行时让用户去选择是否允许该项权限的操作。
那么哪些权限需要在运行时申请呢?危险权限需要这么做,而普通权限仍然和以前一样。具体的分类可以看之前的文章Runtime Permissions
普通权限一半时不会威胁安全、隐私;而危险权限一般涉及用户隐私,设备安全问题。
AndroidManifest声明权限
无论是危险权限还是普通权限都必须在AndroidManifest.xml中声明,这一步的作用是什么?主要有两方面:
- 程序安装时告知用户需要的权限,由用户决定是否安装。
- 在设置的应用选项中,点击应用查看信息可以看到应用获取的权限。由用户决定是否保留应用。
坑
在build.gradle(app)中targetSdkVersion的值低于23时,应用运行在Android6.0及以上系统时,会默认打开在AndroidManifest.xml声明的权限。
如果升级应用,修改了targetSdkVersion为23及以上,再次运行时,依旧默认允许所有AndroidManifest.xml中所用声明的权限。
危险权限导致Crash
如果在Android6.0及以上运行程序执行危险权限相关操作,没有运行时检查权限获取情况,如果没有获取会导致程序crash。例如Console输出:Caused by: java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxxxx cmp=com.android.server.telecom/.components.UserCallActivity } from ProcessRecord{cae02cf 30814:android.example.com.permissionusage/u0a154} (pid=30814, uid=10154) with revoked permission android.permission.CALL_PHONE
有时候需要对执行危险权限操作进行封装,例如打电话操作:
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel://1234567890"));
startActivity(intent);
直接写成一个方法编译器会报错,即便不去处理也可以运行。可以通过捕获上面Console输出的异常来解决编译器报错:
private void makeCall() {
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel://1234567890"));
startActivity(intent);
}catch (SecurityException e) {
e.printStackTrace();
}
}
运行时权限基础写法
单个运行时权限申请
/**
* 单个权限授权
* @param view
*/
public void btnClick(View view) {
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this, new String[]{Manifest.permission.CALL_PHONE}, CALL_REQUEST);
}else {
makeCall();
}
}
权限申请回调
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch(requestCode) {
case CALL_REQUEST:
if(grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED){
makeCall();
}else {
Snackbar.make(mContainer, "权限被拒绝了", Snackbar.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
其实当grantResults数组长度为0时,程序某个地方一定出现问题。
多个运行时权限申请
/**
* 多个权限同时授权
* @param v
*/
public void btnMorePermissions(View v) {
List permissions = new ArrayList<>();
//安全权限,无需运行时检查
if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE)
!= PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.ACCESS_NETWORK_STATE);
}
if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.CALL_PHONE);
}
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if(!permissions.isEmpty()) {
ActivityCompat.requestPermissions(
this,
permissions.toArray(new String[permissions.size()]),
MORE_PERMISSIONS_REQUEST);
}else {
doSomething();
}
}
权限申请回调
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch(requestCode) {
case MORE_PERMISSIONS_REQUEST:
if(grantResults.length > 0) {
for(int i : grantResults) {
if(i != PackageManager.PERMISSION_GRANTED) {
Snackbar.make(mContainer, "某个权限没有授权", Snackbar.LENGTH_SHORT).show();
return;
}
}
doSomething();
}else {
}
default:
break;
}
}
为什么读写外部存储属于危险权限
在Android6.0以前,读写外部存储只需要在AndroidManifest.xml中声明即可,但是由于应用的强制行为,导致用户的外部存储中文件杂乱,权限被滥用。所以将其制定为危险权限。
在外部存储目录Android/data/packgae_name属于应用私有的目录,不需要运行时申请读写外部存储危险权限,甚至不需要在AndroidManifest.xml中声明就可以读写。应用可以随意支配自身的文件存储(cache目录经常会被清理软件清理,主要文件放入files目录下)。
这样既保证了外部存储目录的整洁,又不会给应用开发者带来不必要的麻烦。具体操作可以看文章Android数据存储之File总结。而且应用卸载后,相应包名的目录也会删除。
封装
由于运行时权限申请的繁琐,所以封装饰必须的。但是申请权限的操作必须建立在Activity之上。只有在Activity中才可以弹出申请权限对话框。而且申请回调函数属于Activity的方法,所以申请权限的操作和Activity藕合度非常高。可以通过以下办法:
- 自定义一个PermissionActivity,专门用于处理申请运行时权限操作。该Activity背景透明,用户无法察觉。执行完后finish掉。
- 参照RxPermissions第三方库的实现。
- 创建一个BaseActivity去实现运行时权限申请方法,然后所有Activity继承BaseActivity,需要时调用方法即可。BaseActivity对于一个项目可以提高Activity类的扩展性,在里面实现自己的方法供子类使用。
BaseActivity
public class BaseActivity extends AppCompatActivity{
private static final int REQUEST_CODE = 1;
private PermissionListener mListener;
public void requestRuntimePermissions(String[] permissions, PermissionListener listener) {
mListener = listener;
List permissionList = new ArrayList<>();
for(String permission : permissions) {
if(ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(permission);
}
}
if(!permissionList.isEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionList.toArray(new String[permissionList.size()]),
REQUEST_CODE);
}else {
mListener.onGranted();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_CODE:
if(grantResults.length > 0) {
List deniedPermission = new ArrayList<>();
for(int i = 0; i < grantResults.length; i++) {
int grantResult = grantResults[i];
if(grantResult == PackageManager.PERMISSION_DENIED) {
deniedPermission.add(permissions[i]);
}
}
if(deniedPermission.isEmpty()) {
mListener.onGranted();
}else {
mListener.onDenied(deniedPermission);
}
}
break;
default:
break;
}
}
}
PermissionListener用于将申请结果返回给调用的Activity。让Activity去实现权限申请结果相应的操作。
public interface PermissionListener {
void onGranted();
void onDenied(List deniedPermissions);
}
最后在Activity中使用:
public class SecondActivity extends BaseActivity{
private LinearLayout mContainer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
initView();
}
private void initView() {
mContainer = (LinearLayout) findViewById(R.id.id_second_container);
}
public void btnRuntimePermission(View view) {
requestRuntimePermissions(new String[]{
Manifest.permission.CALL_PHONE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionListener() {
@Override
public void onGranted() {
Snackbar.make(mContainer, "All Permissions Granted!", Snackbar.LENGTH_SHORT).show();
}
@Override
public void onDenied(List deniedPermissions) {
StringBuilder builder = new StringBuilder(32);
int deniedCount = deniedPermissions.size();
for(int i = 0; i < deniedCount; i++) {
String[] strArray = deniedPermissions.get(i).split("\\.");
builder.append(strArray[strArray.length - 1]);
if(i == (deniedCount - 1)) {
builder.append(".");
}else {
builder.append(",");
}
}
Snackbar.make(
mContainer,
"Denied Permissions:" + builder.toString(),
Snackbar.LENGTH_SHORT
).show();
}
});
}
}
参考
Android 6.0运行时权限讲解