Android 7.0使用FileProvider兼容适配问题总结

我们知道Google在Android 7.0以后对文件访问的安全性做了升级,Android 7.0(targetVersion >= 24)以后不允许包含file://xxx类型的intent离开应用,否则会报异常,尤其是在调用系统相机进行拍照/录视频或者是在装apk文件的时候。所以在7.0以后,除了运行时权限申请以外,主要就是这个FileProvider的适配使用了,本文记录一下我在使用过程中遇到的一些问题总结。

首先基本的使用,在AndroidManfiest.xml中配置:

<provider
    android:name=".provider.ImageSelectorProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/out_file_paths"/>
provider>

第一个name属性,如果你的应用中只是在app中使用到了一处,这个name可以写成android.support.v4.content.FileProvider就可以了,这里我使用的时候是在一个library module当中,所以是为了防止跟其他module的重名。
具体ImageSelectorProvider实现:

public class ImageSelectorProvider extends FileProvider {
    //do nothing 避免与应用中的FileProvider重名
}

就是简单的一个继承FileProvider,什么也没干。

第二个authorities属性,这个是相当于一个授权的字符串,如果你只是使用在一个app当中,这个可以随便写死,但是如果是可以被多个app共用的,那么这个最好写成跟包名一致的前缀,这里我是在一个library module当中使用的,直接引用${applicationId},这样在被其他app依赖使用的时候,就会被替换成具体的应用包名。如果不这样做,当多个应用中包含相同authorities属性值的时候,你会发现无法安装应用,具体请查看INSTALL FAILED CONFLICTING PROVIDER问题完美解决方案

第三个exported属性,false表示我们的provider不需要对外开放。

第三个grantUriPermissions属性,true表示允许获取对文件的临时访问权限。

再往下在meta-data属性中我们要配置一个resource属性,这里resource是设置一个xml文件,放置在res/xml/文件目录下的。
out_file_paths.xml内容:


<resources>
    <paths>
        <external-path name="camera_out_file" path="TQImageSelector/CameraImage/" />
    paths>
resources>

首先这里要注意的是,这个xml文件,如果你只是在你的app当中有多个library module使用FileProvider适配,在不同module的AndroidManfiest.xml中配置的这个xml的文件名必须是不同的,否则会出现问题,如果是相同的名字的话最终这个文件会被合并掉,这个问题也是折腾了好久,所以一定要记得多个模块的话要起一个不同的文件名!

这个xml中可以配置的属性:

  • :内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;
  • :内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir()所获取的目录路径;
  • :外部存储空间根目录,等同于 Environment.getExternalStorageDirectory()所获取的目录路径;
  • :外部存储空间应用私有目录下的 files/ 目录,等同于Context.getExternalFilesDir(null)所获取的目录路径;
  • :外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()

其中每个属性下面可以配置 name 和 path 两个属性,name是相当于一个别名,随便起,不要重复就行,path就是在当前这个属性下面的共享目录,比如这里写的是external-path,path是·TQImageSelector/CameraImage/,那么就表示是在
Environment.getExternalStorageDirectory().getPath()+TQImageSelector/CameraImage/这个目录下,最终这个目录会被共享访问。如果文件是直接存放在根目录下进行共享,那么path可以写成path="."这样可以访问根目录下的所有文件。

代码中使用, 以启动相机拍照为例:

public void startCamera() {
    Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (cameraIntent.resolveActivity(getPackageManager()) != null) {
        File cameraFile = FileUtils.createCameraFile(this);
        cameraPath = cameraFile.getAbsolutePath();
        if (getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.M) {
            Uri imageUri = ImageSelectorProvider.getUriForFile(this, getPackageName()+".provider", cameraFile);
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            startActivityForResult(cameraIntent, REQUEST_CAMERA);
        } else {
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(cameraFile));
            startActivityForResult(cameraIntent, REQUEST_CAMERA);
        }
    }
}

注意这里创建了一个file,创建的这个file必须是在你前面xml文件的path中配置的目录下面,它会传递到Provider.getUriForFile()方法中。另外intent最好加上flgIntent.FLAG_GRANT_READ_URI_PERMISSION, 虽然在清单文件中也配置了。

返回结果的获取

如果你启动相机的时候,创建的保存文件完整路径是记住的,比如用全局变量保存,那么你可以直接在onActivityResult中访问这个路径,但是假如这个路径没有记住,比如启动相机录制视频我是封装到一个library库当中,文件名的创建是在library内部进行创建的,这样app在使用的时候可能拿不到这个文件名,这时可以在onActivityResult中去解析这个url来获取路径也是可以的。解析代码:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode == RESULT_OK) {
        if (requestCode == REQUEST_CODE_VIDEO_RECORD_SYS_CAMERA_TARGET_N) {
            //获取系统Camera录制的视频
            Uri uri = data.getData();
            if (uri != null) {
                //视频文件路径
                String fileName = UriUtils.getFileNameByFileProviderUri(this, uri);
                if (!TextUtils.isEmpty(fileName)) {
                    String videoPath = ROOT_DIR + "/" + RecordBySystemCamera.VIDEO_FILE_DIR_TARGET_N + fileName;
                    File file = new File(videoPath);
                 //...
                }
            }
        }
    }
}

这里用到一个方法getFileNameByFileProviderUri, 用来从FileProvider提供的uri当中解析文件名,我们把在配置FileProvider时xml中配置的path路径在代码中写成常量,然后用这个常量路径 + 解析的文件名就是完整路径。

getFileNameByFileProviderUri实现代码:

/**
 * 根据FileProvider分享的Uri获取对应的文件名
 * @param context
 * @param uri
 * @return
 */
public static String getFileNameByFileProviderUri(Context context, Uri uri) {
	String name = null;
	try {
		Cursor c = context.getContentResolver().query(uri, null, null, null, null);
		if (null != c && c.moveToFirst()) {
			int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
			name = c.getString(nameIndex);
			c.close();
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
	return name;
}

参考:
Android7.0适配心得
Android 7.0 行为变更 通过FileProvider在应用间共享文件
Android 7.0 适配中 FileProvider 部分的总结
FileProvider无法获取外置SD卡问题解决方案

你可能感兴趣的:(FileProvider,Android,Android-适配)