Android6.0适配
从Android 6.0 MarshMallow开始,Android支持动态权限管理,即有些权限需要在使用到的时候动态申请。
这些权限会关系到用户的隐私或影响到其他应用的运行,这些危险权限,谷歌还做了一个权限组,以分组的形式来呈现:
[图片上传失败...(image-3f71a8-1642080911915)]
由于运行权限机制的出现,变得我们需要对新开发的应用去做适配,当然有人会问,那我之前开发的老应用不就完蛋了,是不是运行到6.0系统上就会发生各种崩溃?
其实不会的,谷歌做了良好的适配:
当你的应用targetSdkVersion小于23的时候,就算你运行在Android6.0系统上,它也会默认采用以前的权限管理机制,也就是一刀切。当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,它才会采用新的这套权限管理机制。
所以如果你想逃开这个“麻烦”,只要把targetSdkVersion的版本设置为低于23就可以了,不过不建议采用这种方案。
当你的targetSdkVersion大于等于23的时候且在Andorid6.0(M)系统上,若不做适配,程序则会崩溃,报错信息如下:
01-16 02:23:46.181 E/CrashHandler Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA
java.lang.SecurityException: Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA
说了这么多,那么来看下怎么进行Android6.0(M)的权限管理适配吧,其实很简单,只需要记住下面几个API方法就可以:(API23之后提供,support-v4包下也有提供)
int checkSelfPermission(String permission) 用来检测应用是否已经具有权限
void requestPermissions(String[] permissions, int requestCode) 进行请求单个或多个权限
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 请求权限结果回调
[图片上传失败...(image-193991-1642080911916)]
以评测教师端app修改头像功能模块为例,该操作需要拍照和读写sd卡权限
下面来段代码示例(当用户点击照相机时)
//判断当前系统是否高于或等于6.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//通过调用support-v4包提供的方法
if (ContextCompat.checkSelfPermission(getActivity(), android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
//具有拍照权限和读写SD卡权限,直接调用相机
//具体调用代码
takePhoto();
} else {
//不具有拍照权限和读写SD卡权限,需要进行权限申请;在Fragment中申请权限,不可以通过ActivityCompat.requestPermissions调用,直接使用Fragment的requestPermissions方法,否则会回调到Activity的onRequestPermissionsResult
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_CAMERA_CODE);
}
} else {
//当前系统小于6.0,直接调用拍照
takePhoto();
}
private void takePhoto() {
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u = Uri.fromFile(f);
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
}
REQUEST_PERMISSION_CAMERA_CODE
是个标识码,类似Intent跳转的REQUEST_CODE的,然后我们就可以在onRequestPermissionsResult进行权限申请的回调处理:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CAMERA_CODE) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.CAMERA) || !ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)) {
////如果用户选择了不再提示系统授权弹框,则自定义对话框引导用户进行授权
showMissingPermissionDialog();
return;
}
if (grantResults.length >= 2) {
int cameraResult = grantResults[0];
int storageResult = grantResults[1];
if (cameraResult == PackageManager.PERMISSION_GRANTED && storageResult == PackageManager.PERMISSION_GRANTED) {
takePhoto();
}
}
}
}
private void showMissingPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.help);
builder.setMessage(R.string.string_help_text);
// 关闭对话框
builder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startAppSettings();
}
});
builder.show();
}
// 启动应用的设置
private void startAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getActivity().getPackageName()));
startActivity(intent);
}
[图片上传失败...(image-909967-1642080911916)]
注:shouldShowRequestPermissionRationale方法:当app完全没有机会被授权的时候,比如用户在权限对话框中选择了"不再显示”,调用shouldShowRequestPermissionRationale() 则返回false,这种情况下,我们可以通过自定义对话框引导用户进行授权,如下图
[图片上传失败...(image-30ea9-1642080911916)]
Android7.0适配
在Android7.0版本上,Android系统强制执行了StrictMode API 政策,禁止向你的应用外公开File://URI。如果一项包含文件File://URI类型的Intent离开你的应用,应用失败,并出现FileUriExposedException异常,比如系统相机拍照,裁剪照片
经测试,File api在应用内读取文件存储依然可以继续使用,应用间(主要指调用部分系统应用)进行共享会直接报错。由于评测教师端裁剪照片功能通过自定义的ClipHeaderActivity类来实现,属于应用内,所以不会报错
在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u = Uri.fromFile(f);
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
应对策略:若要在应用间共享文件,可以发送 content:// URI类型的Uri,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider类
使用FileProvider
使用FileProvider的大致步骤如下:
第一步:在manifest清单文件中注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${FILE_PROVIDER}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
注:${FILE_PROVIDER}的值定义在主工程app目录下的build.gradle文件内,不同渠道的app需要配置不同的值,否则会和多渠道打包冲突
productFlavors {
ningxia {
applicationId "com.lanxum.diagnosis.teacher.nx"
resValue "string", "app_name", "全优学教师-宁夏正式版"
buildConfigField "String", "PREFIX_URL", "\"http://218.95.173.132\""
buildConfigField "int", "VERSION", "2"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider"]
}
ningxiatest {
applicationId "com.lanxum.diagnosis.teacher.nxtest"
resValue "string", "app_name", "全优学教师-宁夏测试版"
buildConfigField "String", "PREFIX_URL", "\"http://223.202.64.196:8084\""
buildConfigField "int", "VERSION", "2"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider"]
}
huanggang {
applicationId "com.lanxum.diagnosis.teacher.hg"
resValue "string", "app_name", "全优学教师-黄冈版"
buildConfigField "String", "PREFIX_URL", "\"http://221.235.188.44:51311\""
buildConfigField "int", "VERSION", "1"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider"]
}
}
第二步:指定共享的目录
为了指定共享的目录我们需要在资源(res)目录下创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest注册的provider所引用的resource保持一致即可)的资源文件,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="camera_photos" />
</paths>
</resources>
<files-path/>代表的根目录: Context.getFilesDir()
<external-path/>代表的根目录: Environment.getExternalStorageDirectory()
<cache-path/>代表的根目录: getCacheDir()
注:上述代码中path="",是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="pictures",
那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
第三步:使用FileProvider
上述准备工作做完之后,现在我们就可以使用FileProvider了。
还是以调用系统相机拍照为例,我们需要将上述拍照代码修改为如下:
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
u = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER, f);
} else {
u = Uri.fromFile(f);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
以上内容均为参考以下链接,上述示例代码我也只在模拟器上测试过,尚未在真机上测试,可能还存在不兼容和待优化的地方,想要了解更详细具体的内容,可以直接查看以下链接地址
参考链接:
http://www.jianshu.com/p/56b9fb319310
http://www.jianshu.com/p/a37f4827079a
http://www.jianshu.com/p/feb6dd8d2212
http://www.jianshu.com/p/d3a998ec04ad
https://www.aswifter.com/2015/11/04/android-6-permission/