Android 动态权限管理

时间:2021-03-19 15:23:32

Android 动态权限管理

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

基本介绍

  • 普通权限

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

  • 许可权限

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

    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文件内容如下:

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文件如下

<?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. 引入

    添加依赖

    // 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为例)

    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使用)

    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. 同时请求多个权限(合并结果)

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

    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. 同时获取多个权限(分别获取结果)

    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的这种链式写法很好的应用在权限请求上了,哈哈!

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

PermissionsDispatcher

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

Grant

简化运行时权限处理

android-RuntimePermissions

Google官方的例子