之前项目升级时都做了适配文档,之前发了 Android O 适配 ,现在将 Android N 的文档也分享一下
一、Uri适配
现在遇到的是调用系统照相机拍照摄像,照片裁剪,APK安装等需要适配。有些Uri是不需要适配的,用了FileProvider反而有问题。比如照片裁剪中保存裁剪照片的Uri详细见下文,还有就是拍照后发送广播将照片更新到图库的Uri也不能用FileProvider,如下。
File f = new File("");
Uri contentUri = Uri.fromFile(f);
mediaScanIntent.setData(contentUri);
context.sendBroadcast(mediaScanIntent);
下面介绍需要适配的Uri:
1、在Android7.0上调用系统相机
在Android7.0之前,如果你想调用系统相机拍照可以通过以下代码来进行:
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory(), "img.jpg");
Uri imageUri = Uri.fromFile(file);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, 1);
在Android7.0之后,需要使用FileProvider进行适配:
- 第一步:指定共享的目录
在xml下新建file_paths文件(名字任意)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="camera_photos" />
</paths>
</resources>
上述代码中path="",是有特殊意义的,它代码根目录,也就是说你可以向其它的应用共享根目录及其子目录下任何一个文件了,如果你将path设为path="pictures",
那么它代表着根目录下的pictures目录(eg:/storage/emulated/0/pictures),如果你向其它应用分享pictures目录范围之外的文件是不行的。
存在几种类型:
<files-path>:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;
<cache-path>:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;
<external-path>:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;
<external-files-path>:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
<external-cache-path>:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();
这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 name 和 path 两个属性。
其中,path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。注意:path 属性值不能使用具体的独立文件名,只能是目录名
而 name 属性用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。
- 第二步:在manifest清单文件中注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="包名.provider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
注:
name:四大组件,必须唯一
authorities:唯一,一般用包名+字符串 来保证唯一
grantUriPermissions:true,表示授予 URI 临时访问权限
exported:为false,为true则会报安全异常
meta-data下resource指定共享目录文件(第一步创建文件)
- 第三步:Java中使用FileProvider
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file = new File(Environment.getExternalStorageDirectory(), "img.jpg");
Uri imageUri;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
imageUri = Uri.fromFile(file);
} else {
imageUri = FileProvider.getUriForFile(context, authority, file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, 1);
跟7.0前比较主要变化的就两处:
- 将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri
- 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件
上述代码通过FileProvider的Uri getUriForFile (Context context, String authority, File file)静态方法来获取Uri.
该方法中authority参数就是manifest清单文件中注册provider的android:authorities="xxx"的值
将getUriForFile方法获取的Uri打印出来如下:
content://xxx/camera_photos/img.jpg
2、裁剪照片
在Android7.0之前,可以通过以下代码来进行:
Intent intent = new Intent("com.android.camera.action.CROP");
Uri imageUri = Uri.fromFile(imgFile); // 要裁剪的照片File
intent.setDataAndType(imageUri, "image/*");
File file = new File(Environment.getExternalStorageDirectory(), "crop.jpg");
Uri outputUri = Uri.fromFile(file); // 裁剪后的照片
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
...
startActivityForResult(intent, 1);
在Android7.0之后,需要使用FileProvider进行适配:
Intent intent = new Intent("com.android.camera.action.CROP");
Uri imageUri;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
imageUri = Uri.fromFile(imgFile);
} else {
imageUri = FileProvider.getUriForFile(context, authority, imgFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
intent.setDataAndType(imageUri, "image/*");
File file = new File(Environment.getExternalStorageDirectory(), "crop.jpg");
Uri outputUri = Uri.fromFile(file); // 裁剪后的照片
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
...
startActivityForResult(intent, 1);
跟7.0前比较主要变化的就两处:
- 将之前需要裁剪照片Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri
- 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件
注:
照片裁剪中有两个uri,需要将原始照片的uri通过FileProvider获取,裁剪后的照片还通过Uri.fromFile获取,否则会提示不能保存照片。
3、安装APK
同上,创建URI的方式不同
public static void installApk(Context context, String apkPath) {
File file = new File(apkPath);
if (file.exists()) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(FileProvider.getUriForFile(context, authority, file), type);
} else {
intent.setDataAndType(Uri.fromFile(file), type);
}
context.startActivity(intent);
} else {
Toast.makeText(context, "文件不存在", Toast.LENGTH_SHORT).show();
}
}
4、Android中引用第三方module Uri适配问题
当主app中按照上面方式适配后,在module中按上面三步适配后会报multiple error这样的错误,原因是因为provider是四大组件,相同name只能有一个,在打包合并manifest后出错。解决方案两种:
- 写个类MyFileProvider继承FileProvider,将manifest的provider的name改为MyFileProvider即可
public class MyFileProvider extends FileProvider {
}
- 不需要在module的manifest中写provider,在代码中适配时直接用主app的manifest中的provider的authorities的值即可
FileProvider.getUriForFile(context, authority, file);
即这里authority直接用app的authority