Android常用框架----权限管理系列框架

来源:互联网 发布:腾道数据 编辑:程序博客网 时间:2024/06/06 20:18

上篇介绍了事件总线系列框架(http://blog.csdn.net/siwenyy/article/details/75041216),接下来讲讲大家关注的权限问题。


在Android开发中,我们应该遇到过安装app到6.0系统的手机上应用突然一闪而过,查看log,导致崩溃的原因java.lang.SecurityException: Permission Denial。接下来就讲讲6.0权限适配问题。


一、权限分类

普通权限:只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。

ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

危险权限:所有危险的Android系统权限属于权限组,如果APP运行在Android 6.0 (API level 23)或者更高级别的设备中,而且targetSdkVersion>=23时,系统将会自动采用动态权限管理策略,如果你在涉及到特殊权限

操作时没有做动态权限的申请将会导致App崩溃。

注意事项:

1、必须也在Manifest中申明,否则申请时不提使用用户,直接回调开发者权限被拒绝。
2、同一个权限组的任何一个权限被授权了,这个权限组的其他权限也自动被授权。
3、申请某一个权限的时候系统弹出的Dialog是对整个权限组的说明,而不是单个权限。

group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS 写入联系人
permission:android.permission.GET_ACCOUNTS 查找设备上的帐户
permission:android.permission.READ_CONTACTS 读取联系人
 
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG 读取通话记录
permission:android.permission.READ_PHONE_STATE 读取电话状态
permission:android.permission.CALL_PHONE 拨打电话
permission:android.permission.WRITE_CALL_LOG 修改通话记录
permission:android.permission.USE_SIP sip视频服务
permission:android.permission.PROCESS_OUTGOING_CALLS 程序监视,修改或放弃拨出电话
permission:com.android.voicemail.permission.ADD_VOICEMAIL 允许应用程序添加系统中的语音邮件
 
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR 读取日历
permission:android.permission.WRITE_CALENDAR 修改日历
 
group:android.permission-group.CAMERA
permission:android.permission.CAMERA 拍照
 
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS 传感器
 
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION 通过gps获取定位权限
permission:android.permission.ACCESS_COARSE_LOCATION 通过WiFi和移动基站获取定位权限
 
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE 读取内存卡
permission:android.permission.WRITE_EXTERNAL_STORAGE 写内存卡
 
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO 录音
 
group:android.permission-group.SMS
permission:android.permission.READ_SMS 读取短信
permission:android.permission.RECEIVE_WAP_PUSH 接收wap push信息
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS 接收短信
permission:android.permission.SEND_SMS 发送短信
permission:android.permission.READ_CELL_BROADCASTS 获取小区广播

特殊权限 (分在危险权限中)
比如: 系统级别对话框:SYSTEM_ALERT_WINDOW ,修改系统设置:WRITE_SETTINGS 这2个特殊权限,我们需要在startActivityForResult里调用即可,这2个权限一般是不会用到,会用到的地方要么是黑科技或者是反用户体验的场景。

二、需了解的问题
1、为什么6.0需要权限适配 
6.0之前Android的权限都是在安装的时候授予的,6.0之后,为了简化安装流程,并且方便用户控制权限,Android允许在运行的时候动态控制权限。对于开发而言就是将targetSdkVersion设置为23,当运行在Android 6.0 +的手机上时,就会调用6.0相关的API,达到动态控制权限的目的。但是,如果仅仅是将targetSdkVersion设置为23,而在代码层面没有针对Android 6.0做适配,就可能在申请系统服务的时候,由于权限不足,引发崩溃。

2、什么权限需要动态适配

并非所有的权限都需要动态申请,Android6.0将权限分为两种,普通权限跟敏感(危险)权限,普通权限是不需要动态申请的,但是敏感权限需要动态申请。


3、怎样动态适配权限

对于敏感权限的适配有一个原则,那就是实时检查,因为权限随时可能被回收,比如用户可以在设置里面把权限给取消,但是APP并不一定知道,因此每次都需要检查,一旦没有,就需要请求,之后,根据返回结果处理后续逻辑。


三、简单使用


以打电话为例

package com.xiaolajiao.linkdemo.permissiondemo;
 
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
 
import com.xiaolajiao.linkdemo.R;
 
/**
* Created by huangy on 2017/7/13.
*/
public class CallPermission extends AppCompatActivity implements View.OnClickListener {
private String phoneNumber = "10086";
 
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
 
Button button = (Button) findViewById(R.id.btn);
button.setText("打电话");
button.setOnClickListener(this);
 
}
 
@Override
public void onClick(View view) {
//判断系统是否高于或等于6.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//第一步:检测权限
if(ContextCompat.checkSelfPermission(this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
 
if(ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.CALL_PHONE)){
//判断是否需要先弹出自定义的解释说明,需要注意的是这个方法在首次请求的时候是返回false的,
//只有当用户拒绝过一次授权后才会返回true,毕竟多一步交互会造成体验下降,只有在用户对权限产生困扰的时候才需要向用户进行解释说明。
}else {
//第二步:不具备打电话的权限,请求权限
ActivityCompat.requestPermissions(this, new String[]{ android.Manifest.permission.CALL_PHONE }, 1);
}
 
}else {
//具有打电话的权限,直接调用拨号功能
callPhone(phoneNumber);
}
}else {
//当前系统小于6.0,直接调用拨号功能
}
 
}
 
private void callPhone(String phoneNumber) {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:" + phoneNumber));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
 
}
 
//第三步:处理权限请求结果
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){//打电话权限
callPhone(phoneNumber);
}else {
//不具有相关权限,给予用户提醒,比如Toast或者对话框,让用户去系统设置-应用管理里把相关权限开启
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}

对于shouldShowRequestPermissionRationale()方法官网文档的解释


记得在清单文件中配置权限
<uses-permission android:name="android.permission.CALL_PHONE"/>

运行效果如下(华为p8 max):
点击打电话按钮后会弹出权限申请提示框
 
点击始终允许后,再次点击打电话按钮就进入拨号界面了


四、按照开源的PermissionGen思路对权限申请进行了封装后的使用

1、权限工具类
package com.xiaolajiao.linkdemo.permissiondemo;
 
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
 
import java.lang.reflect.Method;
 
/**
* 权限工具类
*
*/
public class PermissionUtil {
 
public static void needPermission(Fragment context, int reqCode, String... permissions) {
needPermission(context.getActivity(), reqCode, permissions);
}
 
public static void needPermission(Activity context, int reqCode, String... permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
//6.0以下版本不需要代码申请权限
executeSuccessResult(context, reqCode);
}
 
boolean granted = hasPermission(context, permissions);//检查权限
if (granted) {
//已获得权限
executeSuccessResult(context, reqCode);
} else {
//申请权限
ActivityCompat.requestPermissions(context, permissions, reqCode);
}
}
 
private static void executeSuccessResult(Object context, int reqCode) {
Method successMethod = getTargetMethod(context, reqCode,PermissionSuccess.class);
try {
successMethod.invoke(context);
} catch (Exception e) {
e.printStackTrace();
}
}
 
private static void executeFailResult(Object context, int reqCode) {
Method successMethod = getTargetMethod(context, reqCode,PermissionFail.class);
try {
successMethod.invoke(context);
} catch (Exception e) {
e.printStackTrace();
}
}
 
private static Method getTargetMethod(Object context, int reqCode,Class annotation) {
Method[] declaredMethods = context.getClass().getDeclaredMethods();
for (Method method : declaredMethods) {
if (!method.isAccessible()) {
method.setAccessible(true); //私有的方法必须强制
}
//判断方法上是否使用了目标注解
boolean annotationPresent = method.isAnnotationPresent(annotation);
if (annotationPresent) {
if (isTargetMethod(method,reqCode,annotation)) { //比较requestCode是否相等
return method;
}
}
}
return null;
}
private static boolean isTargetMethod(Method method, int reqCode, Class cls){
if(cls.equals(PermissionSuccess.class)){
return reqCode == method.getAnnotation(PermissionSuccess.class).requestCode();
}else if(cls.equals(PermissionFail.class)){
return reqCode == method.getAnnotation(PermissionFail.class).requestCode();
}
return false;
}
private static boolean hasPermission(Context context, String... permissions) {
for (String permission : permissions) {
int granted = ContextCompat.checkSelfPermission(context, permission);
if (granted == PackageManager.PERMISSION_DENIED) {
return false;
}
}
return true;
}
public static void onRequestPermissionsResult(Fragment context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
onRequestPermissionsResult(context, requestCode, permissions, grantResults);
}
 
public static void onRequestPermissionsResult(Activity context, int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean permissionGranted = true;
for (int grant : grantResults) {
if (grant == PackageManager.PERMISSION_DENIED) {
permissionGranted = false;
break;
}
}
if (permissionGranted) {
//获得权限
executeSuccessResult(context, requestCode);
} else {
//权限被用户拒绝
executeFailResult(context, requestCode);
}
}
}

2、权限获取成功接口
package com.xiaolajiao.linkdemo.permissiondemo;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
* 标识权限获取成功
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PermissionSuccess {
int requestCode();
}

3、权限获取失败接口
package com.xiaolajiao.linkdemo.permissiondemo;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
* 标识权限获取失败
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionFail {
int requestCode();
}

4、相机主类
package com.xiaolajiao.linkdemo.permissiondemo;
 
import android.Manifest;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
 
import com.xiaolajiao.linkdemo.R;
 
/**
* Created by huangy on 2017/7/13.
*/
 
public class PhotoPermission extends AppCompatActivity implements View.OnClickListener {
private final int PER_CAMERA = 3;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_photo);
findViewById(R.id.camera).setOnClickListener(this);
}
 
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.camera:
PermissionUtil.needPermission(this,PER_CAMERA, Manifest.permission.CAMERA);
break;
}
}
 
@PermissionSuccess(requestCode = PER_CAMERA)
private void grantPermissionSuccess(){
Toast.makeText(this,"已获照相机得权限",Toast.LENGTH_SHORT).show();
}
 
@PermissionFail(requestCode = PER_CAMERA)
private void grantPersmissionFail(){
Toast.makeText(this,"照相机权限被拒绝",Toast.LENGTH_SHORT).show();
}
 
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
PermissionUtil.onRequestPermissionsResult(this,requestCode,permissions,grantResults);
}
 
}

五、其他封装后的使用(参考文档 http://blog.csdn.net/u014099894/article/details/51896832  )

GitHub上有几个很出名,融合了EasyPermission和PermissionGen,删去了注解,使用PermissionGen的请求方式,并利用回调来处理权限申请后的逻辑。

修改后的EasyPermission的特点: 
1. 使用PermissionGen的方式申请权限 
2. 去掉了注解,只使用回调,代码更清晰,更专注于业务逻辑 
3. 必须让权限申请类实现EasyPermission.PermissionCallback接口,否则抛出异常

public class MainFragment extends Fragment implements EasyPermission.PermissionCallback {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
 
/*
像PermissionGen那样申请权限,清晰明了
*/
EasyPermission.with(this)//Activity或Fragment
.addRequestCode(RC_SMS_PERM)//RequestCode
.permissions(Manifest.permission.READ_SMS)//请求的权限
//showRequestPermissionRationale时的对话框的提示信息
.rationale(getString(R.string.rationale_sms))
.request();
}
 
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
/*
系统回调方法,传递给EasyPermission
*/
EasyPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
 
@Override
public void onPermissionGranted(int requestCode, List<String> perms) {
/*
用户授予权限,做业务逻辑
*/
Toast.makeText(getActivity(), "TODO: SMS Granted", Toast.LENGTH_SHORT).show();
}
 
@Override
public void onPermissionDenied(int requestCode, List<String> perms) {
/*
用于拒绝授予权限,提示信息
*/
Toast.makeText(getActivity(), "TODO: SMS Denied", Toast.LENGTH_SHORT).show();
 
//用户点击了不再询问,弹出对话框去Settings界面开启,这段代码根据业务需求可以添加,也可删去
EasyPermission.checkDeniedPermissionsNeverAskAgain(this, "授权啊,不授权没法用啊," + "去设置里授权大哥", perms);
}
 
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
/*
从Settings界面跳转回来,标准代码,就这么写
*/
if (requestCode == EasyPermission.SETTINGS_REQ_CODE) {
if(EasyPermission.hasPermissions(getContext(), Manifest.permission.READ_SMS)){
//已授权,处理业务逻辑
//...
}else{
Toast.makeText(getContext(),"没有权限,无法工作",Toast.LENGTH_SHORT).show();
}
}
}
}


总结:

1、首先要知道6.0版本权限模型跟原来版本是不同的,不再是统一在manifest中默认系统授权,而是有需要的时候,向系统请求授权,提高用户体验。

2、了解权限检测流程,需要注意以下两个问题:

问题一:获取权限后进入设置取消权限再回到应用的问题

如果将权限请求放在了onCreate()方法中,那么建议在onResume()中再进行一次校验,避免用户在当前页面授权后通过按Home键进入设置中取消了刚授权的权限,返回应用后由于无权限而Crash。onResume()的校验逻辑和onCreate()的请求逻辑几乎一致,但是需要判断如果在请求权限的过程中,就不要进行校验了。请求权限的过程中可以通过一个状态位在调用requestPermissions()和收到onRequestPermissionsResult()回调的地方进行标记。

问题二:勾选“不再询问”的说明

勾选了“不在询问”选项后,每次调用requestPermissions()方法,onRequestPermissionsResult()回调都会直接返回未授权,不会弹出授权对话框,此时可以通过Toast或对话框告知用户如何去设置中进行授权。

3、明白权限类型,分为normal和dangerous类型,同时,在dangerous中还需要注意一点,SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS这两个特殊授权请求方式,跟一般授权请求方式不同。

4、在判断APP是否运行在Android M上,可以用版本号来判断,可以准确点。


如果你还是不知道如何动态申请权限,请参考官方文档https://developer.android.com/training/permissions/requesting.html?hl=zh-cn

对于权限申请的封装,在网上搜了以下几个封装库,大家可以选择合适自己的库使用,也可以自行封装。


权限管理库

1. RxPermissios

Github: https://github.com/tbruyelle/RxPermissions


2. AndPermission

Github: https://github.com/yanzhenjie/AndPermission


3. PermissionHelper

Github: https://github.com/k0shk0sh/PermissionHelper


4. PermissionsDispatcher

Github: https://github.com/hotchemi/PermissionsDispatcher


5.Android_M_requestPermissions

Github: https://github.com/captain-miao/Android_M_requestPermissions 



原创粉丝点击