Android 适配——动态权限管理

https://rainmonth.github.io/posts/A180427.html

Android 动态权限管理

最新

Android Q分区存储权限变更及适配

Android 6.0(Marshmallow, 软棉花糖,Api 23),权限分为普通权限和许可权限,许可权限分类归组,该组一个权限被许可后,其他的权限均可使用。

基本介绍

  • 普通权限

    只需在xml申请即可,使用方法和6.0之前的一样。应用安装后会默认获得许可。

  • 许可权限

    具体权限分组情况可以使用以下shell命令查看

    1
    adb shell pm list permissions -d -g

    同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,app也有READ_CONTACTS和GET_ACCOUNTS了

  • 相关方法

    1. ContextCompat.checkSelfPermission()

      检查应用是否拥有该权限,若已授权,返回值为PERMISSION_GRANTED,否则返回PERMISSION_DENIED。

    2. ActivityCompat.requestPermissions()

      该方法在M之前版本调用,OnRequestPermissionsResultCallback 直接被调用,带着正确的 PERMISSION_GRANTED或者 PERMISSION_DENIED。

    3. AppCompatActivity.onRequestPermissionsResult()

      该方法类似于Activity的OnActivityResult()的回调方法,主要接收请求授权的返回值。

  • 需运行时动态申请的权限组

    日历 android.permission-group.CALENDAR

    相机 android.permission-group.CAMERA

    联系人 android.permission-group.CONTACTS

    定位 android.permission-group.LOCATION

    耳机 android.permission-group.MICROPHONE

    电话 android.permission-group.PHONE

    传感器 android.permission-group.SENSORS

    短信 android.permission-group.SMS

    存储 android.permission-group.STORAGE

权限适配解决方案

基本方案(原生方法)

原生方案就是采用android系统提供的权限相关的方法来动态的做权限适配处理,是其他解决方案的基础

我这里来测试读取联系人和打电话这两个6.0之后要在运行时赋予的权限。

MainActivity.java文件内容如下:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package com.rainmonth.myapplication;

import android.Manifest;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Build;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

// 权限允许对应的Map
private Map<Integer, Runnable> allowPermissionRunnableMap = new HashMap<>();
// 权限拒绝对应的Map
private Map<Integer, Runnable> denyPermissionRunnableMap = new HashMap<>();

private Button btCallPhone;
private Button btContact;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

btCallPhone = (Button) findViewById(R.id.btn_call_phones);
btContact = (Button) findViewById(R.id.btn_contacts);

btCallPhone.setOnClickListener(this);
btContact.setOnClickListener(this);
}

/**
* 请求权限
*
* @param requestCode 请求授权的id 唯一标识即可
* @param requestPermission 请求的权限
* @param allowRunnable 同意授权后的操作
* @param denyRunnable 禁止权限后的操作
*/
protected void requestPermission(int requestCode, String requestPermission, Runnable allowRunnable, Runnable denyRunnable) {
if (allowRunnable == null) {
throw new IllegalArgumentException("allowableRunnable == null");
}

allowPermissionRunnableMap.put(requestCode, allowRunnable);
if (denyRunnable != null) {
denyPermissionRunnableMap.put(requestCode, denyRunnable);
}

//版本判断,6.0以上才予以处理
if (Build.VERSION.SDK_INT >= 23) {
//检查是否拥有权限
int checkCallPhonePermission = ContextCompat.checkSelfPermission(getApplicationContext(), requestPermission);
if (checkCallPhonePermission != PackageManager.PERMISSION_GRANTED) {
//弹出对话框提示用户赋予该权限
ActivityCompat.requestPermissions(this, new String[]{requestPermission}, requestCode);
} else {
allowRunnable.run();
}
} else {
allowRunnable.run();
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// 其实还可以根据requestCode进行区分处理(不同的权限申请,采用不同的处理方式)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Runnable allowRun = allowPermissionRunnableMap.get(requestCode);
allowRun.run();
} else {
Runnable disallowRun = denyPermissionRunnableMap.get(requestCode);
disallowRun.run();
}
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_call_phones:
requestPermission(1, Manifest.permission.CALL_PHONE, new Runnable() {
@Override
public void run() {
callPhone();
}
}, new Runnable() {
@Override
public void run() {
callPhoneDenied();
}
});
break;
case R.id.btn_contacts:
requestPermission(2, Manifest.permission.READ_CONTACTS, new Runnable() {
@Override
public void run() {
readContact();
}
}, new Runnable() {
@Override
public void run() {
readContactDenied();
}
});
break;
}
}

/**
* 模拟赋予电话权限后打电话的方法
*/
private void callPhone() {
Toast.makeText(MainActivity.this, "CALL_PHONE OK", Toast.LENGTH_SHORT).show();
}

/**
* 模拟拒绝赋予电话权限后处理的方法,一般用来做街面上的兼容处理,以提醒用户去设置相关的权限
*/
private void callPhoneDenied() {
Toast.makeText(MainActivity.this, "CALL_PHONE Denied", Toast.LENGTH_SHORT).show();
}

/**
* 模拟赋予联系权限后的方法
*/
private void readContact() {
ContentResolver cr = getContentResolver();
String str[] = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.PHOTO_ID};
Cursor cur = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, str, null, null, null);
int count = cur.getCount();
cur.close();
Toast.makeText(MainActivity.this, String.format("发现%s条", count), Toast.LENGTH_SHORT).show();
}

/**
* 模拟拒绝赋予联系人权限后处理的方法,一般用来做街面上的兼容处理,以提醒用户去设置相关的权限
*/
private void readContactDenied() {
Toast.makeText(MainActivity.this, "Contact Denied", Toast.LENGTH_SHORT).show();
}
}

activity_main.xml文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.rainmonth.myapplication.MainActivity">

<Button
android:id="@+id/btn_contacts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contacts" />

<Button
android:id="@+id/btn_call_phones"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Call PHones" />

</LinearLayout>

权限申请结果的处理主要在onRequestPermissionsResult中,而处理权限申请所用到的两个HashMap则是在requestPermissions就传递过去了。根据requestCode来在map中查找对应的Runnable对象来处理。

RxJava + RxPermissions方案

在了解了RxJava后,若果你看到上述代码就会觉得上面的基本方案肯定能有一种RxJava式的封装来解决(基本方案中其处理核心就是利用系统提供的onRequestPermissionsResult这个回调方法,RxJava可以很好的解决回调问题),没错,RxPermissions就应运而生了。

RxPermissions的源码还没时间具体分析,先看看它的使用方式。

  1. 引入

    添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // RxJava 版本小于2.0
    dependencies {
    compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.0@aar'
    }

    // RxJava 版本2.0以上
    dependencies {
    compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.8.2@aar'
    }
  2. 直接申请权限(以Manifest.permission.CAMERA为例)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    RxPermissions.getInstance(this)
    .request(Manifest.permission.CAMERA)
    .subscribe(new Action1<Boolean>() {
    @Override
    public void call(Boolean granted) {
    if (granted) { // 在android 6.0之前会默认返回true
    // 已经获取权限
    String jpgPath = getCacheDir() + "test.jpg";
    takePhotoByPath(jpgPath, 2);
    } else {
    // 未获取权限
    Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
    }
    }
    });
  3. 条件触发获取权限(可以结合RxBinding使用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    RxView.clicks(findViewById(R.id.request_permission))
    .compose(RxPermissions.getInstance(this).ensure(Manifest.permission.CAMERA))
    .subscribe(new Action1<Boolean>() {
    @Override
    public void call(Boolean granted) {

    if (granted) { // 在android 6.0之前会默认返回true
    // 已经获取权限
    String jpgPath = getCacheDir() + "test.jpg";
    takePhotoByPath(jpgPath, 2);
    } else {
    // 未获取权限
    Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
    }
    }
    });
  4. 同时请求多个权限(合并结果)

    以同时申请拍照和录音权限,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    RxPermissions.getInstance(MainActivity.this).request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
    .subscribe(new Action1<Boolean>() {
    @Override
    public void call(Boolean granted) {
    if (granted) { // 在android 6.0之前会默认返回true
    // 已经获取权限
    String jpgPath = getCacheDir() + "test.jpg";
    takePhotoByPath(jpgPath, 2);
    } else {
    // 未获取权限
    Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
    }
    }
    });

    上面这种请求权限方式,只有所有的权限请求都同意之后,才会返回true,有一个为false,则返回false

  5. 同时获取多个权限(分别获取结果)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    RxPermissions.getInstance(MainActivity.this).requestEach(Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO)
    .subscribe(new Action1<Permission>() {
    @Override
    public void call(Permission permission) {
    if (permission.name.equals(Manifest.permission.CAMERA)) {
    if (permission.granted) {
    String jpgPath = getCacheDir() + "test.jpg";
    takePhotoByPath(jpgPath, 2);
    } else {
    // 未获取权限
    Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
    }

    } else if (permission.name.equals(Manifest.permission.RECORD_AUDIO)) {

    }
    }
    });

RxJava的这种链式写法很好的应用在权限请求上了,哈哈!

其他问题

  • 在部分手机Android 5.0上直接使用ContextWrapper的checkSelfPermission方法会抛出异常(NoSuchMethodException)这个时候记得将其替换成ContextCompat的checkSelfPermission方法
  • API 23之前的机子调用checkSelfPermission的返回值一直是0,也就是PERMISSION.GRANTED(不管用户是否已经赋予该权限),解决方法就是调用support包下面的PermissionChecker的checkSelfPermission方法

其他与权限申请相关的项目

PermissionsDispatcher

使用标注的方式,动态生成类处理运行时权限,目前还不支持嵌套Fragment。

Grant

简化运行时权限处理

android-RuntimePermissions

Google官方的例子