一、动态权限的开始
Android系统目前已经更新到8.0(O),自从Android6.0(M)开始,权限适配问题成为我们开发适配工作之一。在6.0之前,应用申请权限只需要将权限在AndroidManifest中注册声明即可,用户安装应用时会有一个权限列表,只有同意这些权限后,我们才能安装成功,这就使用户不得不忍受一些无理的权限要求(访问通讯录、访问短信等);在6.0之后,Google出去安全隐私考虑,引入运行时权限,可见官网:运行时权限
将系统权限分为两类,一类是Normal permissions,这类权限在一般不涉及用户隐私,是不需要用户授权,用户安装app时默认授予,另一类是dangerous permissions,这类权限一般涉及到用户的隐私,需要用户先进行授权。
危险权限是分组的,在Android6.0的机器上,基于授权机制,如果用户申请了某个危险权限,假如app已经申请过同组的其他危险权限,那么系统会立即授权,而不需要用户点击,比如,你已经申请过 permission:android.permission.READ_EXTERNAL_STORAGE权限,那么当你再次申请permission:android.permission.WRITE_EXTERNAL_STORAGE权限时,系统会自动授权,而不再由用户确认。(对于危险权限,我们在授权时最好用到一个申请一个,不要依赖这个机制,因为8.0对此机制已经修改,后面在说)
- Dangerous Permissions权限列表:
https://developer.android.com/guide/topics/permissions/requesting.html?hl=zh-cn
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
二、动态权限的检测和申请
由于运行时权限的出现,我们必须对新的app做适配,当然,我们不必担心之前的app,Google对此做了兼容。当我们的应用targetSdkVersion小于23时,即使是装在6.0的手机上,也会依然使用6.0之前的权限规则,当targetSdkVersion大于23时,我们才使用新的这套运行时权限规则。
权限检测:
(这里我就不说第三方权限检测库的使用了.)
/*
* 1.检查权限是否存在
* 2.(1)若权限存在:直接打开app
* (2)若权限不存在,请求改权限
* 3.重写onRequestPermissionsResult()方法,判断是权限返回结果,在做处理
*/
private void requestPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
//有权限:打开相机
AlbumBlock.doActionImageCapture(this);
} else {
//无权限:申请权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
/*打开相机*/
public static void doActionImageCapture(Activity activity) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = getOutputMediaFile(activity, false);
if (file == null) {
ToastUtil.makeToast(activity, "无法保存图片");
return;
}
Uri imgUri = Uri.fromFile(file);
if (imgUri != null) {
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
} else {
ToastUtil.makeToast(activity, "SD卡不可用,相机照片无法存储!");
}
}
三、权限申请的回调
当我们申请权限后,需要对用户操作的结果进行处理。一般我们会重写onRequestPermissionsResult()方法。在这里需要注意的是,一旦当第二次拒绝权限时,会有一个是否询问的勾选按钮,一旦用户选择了不在询问,那么ActivityCompat.requestPermissions()方法将不在起作用,这个时候ActivityCompat.shouldShowRequestPermissionRationale()方法会返回false,我们可以通过这个判断进行一些自己的处理。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length == 0) {
return;
}
switch (requestCode) {
case REQUEST_CAMERA:
for (int i = 0; i < permissions.length; i++) {
String perm = permissions[i];
//依次判断权限
if (Manifest.permission.CAMERA.equals(perm)) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
//打开相机
AlbumBlock.doActionImageCapture(this);
} else {
//拒绝
/*如果用户选择了拒绝,并且选择了不在提醒,那么这个方法会返回false,这样我们就可以做一些自己的
* 提醒,避免一些不好的体验。这个时候requestPermissions()是不起作用的,所以我们需要告诉用户怎么打开权限。
* */
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, perm)) {
showRequstPermissionDialog();
}
}
}
}
break;
}
private void showRequstPermissionDialog() {
new AlertDialogUtil(this)
.setTitle("相机权限未开启")
.setMessage("请在设置中开启相机权限")
.setNegativeButton("暂不", null)
.setPositiveButton("去设置", new AlertDialogUtil.OnClickListener() {
@Override
public void onClick(DialogInterface dialog) {
try {
/*
* 当然在这里我们可以针对不同的手机品牌,跳转到不同设置界面。
* 后面会给出相应的工具类。
*/
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
} catch (Exception e) {
}
}
}).show();
}
四、7.0权限的更改及FileProvider的使用
我们知道Android6.0引入了运行时权限"Runtime Permissions",那么Android7.0对于权限这块,则增加了"StrictMode API 政策"。详见官网:https://developer.android.com/about/versions/nougat/android-7.0-changes.html#perm
(在这里推荐一篇介绍适配7.0的博客:http://www.jianshu.com/p/56b9fb319310)7.0之后,如果一项包含文件 file:// URI类型 的 Intent 离开了我们应用,那么我们的APP会出故障,并出现 FileUriExposedException 异常,例如调用系统相机拍照或者裁剪照片。对于这个问题,可根据官网给出的方法去解决:
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最
简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
。
7.0之前打开相机的方式可参照上面的代码,这个方式在7.0系统的手机由于"StrictMode API 政策禁"会报错,所以我们需要通过 FileProvider来解决这个问题。下面是具体使用步骤(参考上面那篇博客所写):
第一步:在AndroidManifest 功能清单文件中注册Provider###
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.ff.app.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
第二步:指定共享目录
我们需要再res目录下创建一个xml目录,然后创建一个file_paths(这个名字只要和你注册时使用的一致就行)文件。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="files_path" path="images/" /> //相当 Context.getFilesDir() + path, name是分享url的一部分
<cache-path name="cache_path" path="path/" /> //getCacheDir()
<external-path name="external_path" path="ffapp/images/" /> //Environment.getExternalStorageDirectory()
<external-files-path name="external_files" path="path/" />//getExternalFilesDir(String) Context.getExternalFilesDir(null)
<external-cache-path name="external_cache" path="images/" /> //Context.getExternalCacheDir()
</paths>
上述代码中path="",是有特殊意义的,它代表根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="images/",
那么它代表着根目录下的images下的目录(eg:/storage/emulated/0/images/),如果你向其它应用分享images下目录范围之外的文件是不行的。
第三步:使用FileProvider
/*打开相机*/
public static void doActionImageCapture(Activity activity) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = getOutputMediaFile(activity, false);
if (file == null) {
ToastUtil.makeToast(activity, "无法保存图片");
return;
}
Uri imgUri;
/*这里就是对FileProvider的使用*/
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
imgUri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileprovider", file);
//将存储图片的Uri读写权限授权给拍照工具应用
List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
//添加这一句表示对目标应用临时授权该Uri所代表的文件
activity.grantUriPermission(packageName, imgUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
} else {
imgUri = Uri.fromFile(file);
}
if (imgUri != null) {
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
activity.startActivityForResult(intent, REQUEST_CODE_ACTION_IMAGE_CAPTURE);
} else {
ToastUtil.makeToast(activity, "SD卡不可用,相机照片无法存储!");
}
}
在上面代码中我们判断了系统版本,如果是7.0之后的,我们则将Uri转换为一个content类型的Uri,并且对目标应用(相机)临时授权该Uri所代表的文件。getUriForFile()方法的中authority参数就是我们注册时 android:authorities="com.ff.app.fileprovider"指定的值。这里使用activity.getPackageName()是为了便于模块的抽离。
好了这就是7.0的FileProvider简单使用。
五、8.0权限的更改
在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会"错误"地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。(在说6.0权限时我们说到了这个机制,并说过不要依赖,这里8.0已经修复了)
对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
这意味着,我们应用中用到的所有权限必须一个一个的声明,举个例子:在8.0之前,我们在申请 Manifest.permission.WRITE_EXTERNAL_STORAGE的时候,系统会自动对同一个权限组的Manifest.permission.READ_EXTERNAL_STORAGE进行授权,而我们不需要再去申请,但是放到8.0及之后的系统上,如果app中同时用到这两个权限而我们只申请了一个的话,系统会报错。
对于开发者来说,我们需要明确的知道我们的应用涉及到了哪些危险权限,并且在使用时对于其进行授权申请。
Android 8.0 行为变更
六、开发中遇到的问题
关于权限的适配暂时说到这里,下面说一个开发中遇到的奇怪问题,暂时没有解决。在开发内部员工OA的app时用到了扫一扫的功能,一般扫一扫肯定需要相机权限,所以按照权限的适配去判断后,发现下面这段代码总是返回有权限,无论权限是授权的还是禁止的。(原因暂时不确定)
private void requestPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
//有权限:打开相机
AlbumBlock.doActionImageCapture(this);
} else {
//无权限:申请权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
那这样检测不就没有用吗,的确是的。后来看了很多资料,觉得可能是Android系统过于碎片化的原因,那么怎么去解决呢?首先我们需要换一种思路了,既然是调用相机,那么我们看看相机是否可用不就行了?所以在检测相机权限的时候我们在增加一个方法。
private void requestPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
if(isCameraCanUse()){
//有权限:打开相机
AlbumBlock.doActionImageCapture(this);
}else{
//去设置界面:下面会提供一个去大部分主流手机系统的设置界面的工具类
}
} else {
//无权限:申请权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
/**
* 测试当前摄像头能否被使用
*
* @return
*/
public static boolean isCameraCanUse() {
boolean canUse = true;
Camera mCamera = null;
try {
mCamera = Camera.open(0);
mCamera.setDisplayOrientation(90);
} catch (Exception e) {
canUse = false;
}
if (canUse) {
mCamera.release();
mCamera = null;
}
//Timber.v("isCameraCanuse="+canUse);
return canUse;
}
问题解决了,这里提供一个博主提供的工具类,感觉是一个厉害的工具类。
这里贴出网址:https://github.com/SenhLinsh/Utils-Everywhere里面有一个OSUtils可以判断各大手机品牌的系统,还有一个AppUtils中有个方法,这列贴出来,配合使用。。
/**
* 跳转: 「权限设置」界面
* <p>
* 根据各大厂商的不同定制而跳转至其权限设置
* 目前已测试成功机型: 小米V7V8V9, 华为, 三星, 锤子, 魅族; 测试失败: OPPO
*
* @return 成功跳转权限设置, 返回 true; 没有适配该厂商或不能跳转, 则自动默认跳转设置界面, 并返回 false
*/
public static Intent getPermissionSettingIntent(Activity activity) {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
OSUtils.ROM romType = OSUtils.getRomType();
switch (romType) {
case EMUI: // 华为
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"));
break;
case Flyme: // 魅族
intent.setAction("com.meizu.safe.security.SHOW_APPSEC");
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.putExtra("packageName", activity.getPackageName());
break;
case MIUI: // 小米
String rom = getMiuiVersion();
if ("V6".equals(rom) || "V7".equals(rom)) {
intent.setAction("miui.intent.action.APP_PERM_EDITOR");
intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
intent.putExtra("extra_pkgname", activity.getPackageName());
} else if ("V8".equals(rom) || "V9".equals(rom)) {
intent.setAction("miui.intent.action.APP_PERM_EDITOR");
intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
intent.putExtra("extra_pkgname", activity.getPackageName());
} else {
intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + activity.getPackageName()));
}
break;
case Sony: // 索尼
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity"));
break;
case ColorOS: // OPPO
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.PermissionManagerActivity"));
break;
case EUI: // 乐视
intent.putExtra("packageName", activity.getPackageName());
intent.setComponent(new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps"));
break;
case LG: // LG
intent.setAction("android.intent.action.MAIN");
intent.putExtra("packageName", activity.getPackageName());
ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
intent.setComponent(comp);
break;
case SamSung: // 三星
case SmartisanOS: // 锤子
intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + activity.getPackageName()));
break;
default:
intent.setAction(Settings.ACTION_SETTINGS);
break;
}
return intent;
}
/**
* 获取 MIUI 版本号
*/
private static String getMiuiVersion() {
String propName = "ro.miui.ui.version.name";
String line;
BufferedReader input = null;
try {
Process p = Runtime.getRuntime().exec("getprop " + propName);
input = new BufferedReader(
new InputStreamReader(p.getInputStream()), 1024);
line = input.readLine();
input.close();
} catch (IOException ex) {
ex.printStackTrace();
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return line;
}
这个问题暂时这么解决吧,能力不够还得继续学习!!
关于代码:传送门