1. 目标
此文章的最终目标:让童鞋们了解6.0动态权限的逻辑,以及学会封装动态权限申请,实现'一句话申请权限'
。
2. 前言
在android6.0之前,开发者无需在java代码里进行危险权限
的申请,只需在AndroidManifest.xml文件里进行权限声明即可,但从android6.0开始,google为了用户的安全考虑,加入了动态权限机制,危险权限
必须在代码里申请,当然,AndroidManifest.xml文件里一样还是要进行声明,别忘了这一点哦。(ps:加入了动态权限,虽然对开发者而言是麻烦了一些,但是对整体的android环境有一定的改善,流氓软件不能像以前那样流氓了。)
3. 什么是危险权限
危险权限,也可以理解成较敏感权限,有哪些呢,请看下表:
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
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
permission:android.permission.ACCESS_COARSE_LOCATION
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
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
很明显可以看到,一个group下面带有一个或多个permission,这是什么情况呢?
其实是这样的,google把相类似的权限都分到了对应的组里,这样有什么好处? 仅仅是为了好看、方便记忆?
不不,好处是:属于同一组的权限不用重复授权。例如,如果用户之前已经授权过WRITE_CONTACTS
权限的话,当你申请获取READ_CONTACTS
的时候,不需要等待用户同意授权,而是直接返回授权成功。
当然,既然是以分组的形式进行授权,那么系统的授权dialog
上面提示的也会是具体的组名,而不是单个权限名。
4. 怎么申请危险权限
4.1 先看看相关的API:
// 第二个参数可以从`Manifest.permission`里取值
// 第三个参数会在`onRequestPermissionsResult`方法里回调回来
ActivityCompat.requestPermissions(final Activity activity,
final String[] permissions, final int requestCode)
调用此方法后,如果targetSdkVersion版本小于26,或者用户已经授权过对应组的权限,会直接回调activity的onRequestPermissionsResult
方法,否则会弹出系统对话框询问用户是否允许该权限组,当用户点了拒绝或者同意权限,也都同意会回调activity的onRequestPermissionsResult
方法。
4.2 那么,重点逻辑处理似乎就是在onRequestPermissionsResult
方法里了。
- 先来看看该方法的参数:
// 1. requestCode即调用ActivityCompat.requestPermissions时填的requestCode。
// 2. permissions即调用ActivityCompat.requestPermissions时填的permissions。
// 注意如果targetSdkVersion版本小于26,则permissions是空数组
// 3. grantResults即权限授权结果。
// 注意如果targetSdkVersion版本小于26,则grantResults是空数组
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
- 所以,只要grantResults数组的size > 0 并且 其中一个为“拒绝授权”,则为有权限被拒绝了
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒绝权限
// 有权限被决绝,执行拒绝权限后的逻辑,例如退出界面
// xxx
return;
}
}
// 执行同意权限后的逻辑
// xxx
- 应用权限设置界面跳转提醒
权限拒绝有两种情况:1. 不勾选“不再提醒”; 2.勾选“不再提醒”。
如果用户勾选了“不再提醒”,那么再次申请权限的时候,系统不会弹出授权dialog,此时我们的app应该给予提示,否则用户很有可能会感觉莫名其妙:怎么进来这个界面后又退出来了。
那么问题来了,怎么判断有没有勾选“不再提醒”
,android提供了一个API:
// 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
// 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
// 3:如果用户同意了权限,此方法返回false
boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(Activity , String[]])
所以,只要在判断出是拒绝权限的地方,判断出shouldShowRequestPermissionRationale返回false,即为需要提醒跳转。下面贴下代码:
// 循环判断权限,只要有一个拒绝了,则回调onReject()。 全部允许时才回调onAllow()
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒绝权限
// 对于 ActivityCompat.shouldShowRequestPermissionRationale
// 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
// 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
// 3:如果用户同意了权限,此方法返回false
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
// 拒绝选了"不再提醒",一般提示跳转到权限设置页面,
// 并在dialog cancel的时候,执行onReject();
showTipDialog(permissions[i]);
} else {
// 有权限被决绝,执行拒绝权限后的逻辑
onReject();
}
return;
}
}
// 执行同意权限后的逻辑
onAllow();
5. 动态请求封装,实现'一句话申请权限'
其实原理很简单,就是用一个PermissionBaseActivity父类来处理权限的申请和判断,然后利用‘延迟实现’的思想,通过回调的方式,把具体的同意或拒绝授权后的业务逻辑交给子类来实现。还是直接贴代码吧:
public class PermissionBaseActivity extends AppCompatActivity {
protected final String TAG = getClass().getSimpleName().replace("Activity", "Act");
protected TipDialog tipDialog;
private SparseArray listenerMap = new SparseArray<>();
/**
* 权限请求结果监听者
*/
public interface OnPermissionResultListener {
/**
* 权限被允许
*/
void onAllow();
/**
* 权限被拒绝
*/
void onReject();
}
/**
* 镜像权限申请
* @param onPermissionResultListener 申请权限结果回调
*/
public void checkPermissions(final String[] permissions, OnPermissionResultListener onPermissionResultListener) {
if (Build.VERSION.SDK_INT < 23 || permissions.length == 0) {// android6.0已下不需要申请,直接为"同意"
if (onPermissionResultListener != null)
onPermissionResultListener.onAllow();
} else {
int size = listenerMap.size();
if (onPermissionResultListener != null) {
listenerMap.put(size, onPermissionResultListener);
}
ActivityCompat.requestPermissions(this, permissions, size);
}
}
/**
* 显示提示"跳转到应用权限设置界面"的dialog
* @param permission 具体的某个权限,用于展示dialog的内容文字。
*/
private void showTipDialog(String permission, final OnPermissionResultListener onPermissionResultListener) {
if (tipDialog == null) {
tipDialog = new TipDialog(this);
}
// 确定按钮
tipDialog.findViewById(R.id.btn_confirm).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tipDialog.cancel();
toAppDetailSetting();
}
});
// 取消按钮
tipDialog.findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tipDialog.cancel();
}
});
tipDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
onPermissionResultListener.onReject();
}
});
tipDialog.setTipText(PermissionUtil.getTip(permission));
tipDialog.show();
}
/**
* 跳转系统的App应用详情页
*/
protected void toAppDetailSetting() {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(localIntent);
}
@Override
protected void onDestroy() {
if (tipDialog != null) {
tipDialog.cancel();
tipDialog = null;
}
listenerMap.clear();
listenerMap = null;
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
OnPermissionResultListener onPermissionResultListener = listenerMap.get(requestCode);
if (onPermissionResultListener != null) {
listenerMap.remove(requestCode);
// 循环判断权限,只要有一个拒绝了,则回调onReject()。 全部允许时才回调onAllow()
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {// 拒绝权限
// 对于 ActivityCompat.shouldShowRequestPermissionRationale
// 1:用户拒绝了该权限,没有勾选"不再提醒",此方法将返回true。
// 2:用户拒绝了该权限,有勾选"不再提醒",此方法将返回 false。
// 3:如果用户同意了权限,此方法返回false
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
// 拒绝选了"不再提醒",一般提示跳转到权限设置页面
showTipDialog(permissions[i], onPermissionResultListener);
} else {
onPermissionResultListener.onReject();
}
return;
}
}
onPermissionResultListener.onAllow();
}
}
}
其中用了一个listenerMap
来维护多个请求,避免如果同时有多个权限申请请求,onRequestPermissionsResult会乱套的。
同时用此SparseArray的size作为requestCode,方便处理完授权判断后,从map里移除listener,减少内存占用。
- 子类使用:
checkPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, new OnPermissionResultListener() {
@Override
public void onAllow() {
Log.d(TAG, "onAllow: ");
}
@Override
public void onReject() {
Log.d(TAG, "onReject: ");
}
});
是不是很简洁咧,理解原理后,稍微封装下就好,也不用去引用什么第三方框架了。
6. 额外说一下,责任单一原则
大家都知道,写android app的时候,基本上都有一个BaseActivity,但是有些童鞋会把很多逻辑都一起写在BaseActivity这个类里,感觉像大杂烩一样,很不美观,后期有问题了或者需要优化了,定位不好定位。
建议,遵循责任单一原则:
权限相关的逻辑就写在PermissionBaseActivity里。
Mvp相关的绑定逻辑,就写在MvpBaseActivity里。等等
最后再去处理继承关系就好了。
觉得有用同学,点赞鼓励下呗。 个人公众号『Grade桂』,欢迎关注