Android 7.0 行为变更 通过FileProvider在应用间共享文件

    科技2022-07-11  116

    Android 7.0 行为变更

    为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。

    替代解决方案便是使用 FileProvider。

    FileProvider

    作为四大组件之一的 ContentProvider,一直扮演着应用间共享资源的角色。这里我们要使用到的 FileProvider,就是 ContentProvider 的一个特殊子类,帮助我们将访问受限的 file:// URI 转化为可以授权共享的 content:// URI。

    第一步,注册一个 FileProvider

    作为系统四大组件之一的 ContentProvider,其子类FileProvider,也同样需要使用 元素在 Manifest 文件中添加注册信息,并按照要求设置相关属性值。

    <application> ... <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.yourname" android:exported="false" android:grantUriPermissions="true"> ... </provider> ... </application>

    其中,android:authorities 属性值是一个由 build.gradle 文件中的 applicationId 值和自定义的名称组成的 Uri 字符串(这样写是约定俗成的)。其他属性值使用如上固定值即可。

    第二步,添加共享目录

    在 res/xml 目录下新建一个 xml 文件,用于存放应用需要共享的目录文件。这个 xml 文件的内容类似这样:

    <?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="my_images" path="images/"/> ... </paths>

    元素必须包含一到多个子元素。这些子元素用于指定共享文件的目录路径,必须是这些元素之一:

    < root-path/> 代表设备的根目录new File("/");

    < files-path> :内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;

    < cache-path>:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;

    < external-path>:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;

    < external-files-path>:外部存储空间应用私有目录下的 files/ 目录,等同于 context.getExternalFilesDirs所获取的目录路径;

    < external-cache-path>:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()

    以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 namepath 两个属性。

    其中,path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。注意:path 属性值不能使用具体的独立文件名,只能是目录名。

    name 属性用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性

    如果我们需要分享的文件位于同级别目录下不同的子目录中,就需要添加多个子元素逐一指定要分享的文件目录,或者共享他们通用的父目录也行。

    添加完共享目录后,再在 元素中使用 < meta-data> 元素将 res/xml 中的 path 文件与注册的 FileProvider 链接起来:

    <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.yourname" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/yourfilename" /> </provider>

    第三步,生成 Content URI

    在 Android 7.0 出现之前,我们通常使用 Uri.fromFile() 方法生成一个 File URI。这里,我们需要使用 FileProvider 类提供的公有静态方法 getUriForFile 生成 Content URI。比如:

    Uri contentUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".myprovider", myFile);

    需要传递三个参数。第二个参数便是 Manifest 文件中注册 FileProvider 时设置的 authorities 属性值,第三个参数为要共享的文件,并且这个文件一定位于第二步我们在 path 文件中添加的子目录里面。

    举个例子:

    String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg"; File outputFile = new File(filePath); if (!outputFile.getParentFile().exists()) { outputFile.getParentFile().mkdir(); } Uri contentUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".myprovider", outputFile);

    生成的 Content URI 是这样的:

    content://com.yifeng.samples.myprovider/my_images/1493715330339.jpg

    其中,构成 URI 的 host 部分为 元素的 authorities 属性值(applicationId + customname),path 片段 my_images 为 res/xml 文件中指定的子目录别名(真实目录名为:images)。

    第四步,授予 Content URI 访问权限

    最后调用实际功能,使用FileProvider兼容安装apk,抛出了异常(警告,没有Crash):

    注意:看别人文章,误以为系统7.0与低版本有分别,低版本需要权限。 实际上所有版本都需要授予访问权

    java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{18570a 27107:com.google.android.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004

    可以看到是权限问题~

    生成 Content URI 对象后,需要对其授权访问权限。授权方式有两种:

    方式一为Intent.addFlags,该方式主要用于针对intent.setData,setDataAndType以及setClipData 相关方式传递uri的。方式二为grantUriPermission来进行授权

    另外Android 7的设备拍照功能并没有遇到Permission Denial的问题,不需要权限,主要是因为Intent的action为ACTION_IMAGE_CAPTURE,当我们startActivity后,会辗转调用Instrumentation的execStartActivity方法,在该方法内部,会调用intent.migrateExtraStreamToClipData();方法。 该方法中包含:

    if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action) || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) { final Uri output; try { output = getParcelableExtra(MediaStore.EXTRA_OUTPUT); } catch (ClassCastException e) { return false; } if (output != null) { setClipData(ClipData.newRawUri("", output)); addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION); return true; } }

    可以看到将我们的EXTRA_OUTPUT,转为了setClipData,并直接给我们添加了WRITE和READ权限。

    第五步,提供 Content URI 给其它应用

    拥有授予权限的 Content URI 后,便可以通过 startActivity() 或者 setResult() 方法启动其他应用并传递授权过的 Content URI 数据。当然,也有其他方式提供服务。

    如果你需要一次性传递多个 URI 对象,可以使用 intent 对象提供的 setClipData() 方法,并且 setFlags() 方法设置的权限适用于所有 Content URIs。

    六、快速完成适配

    最后再编写一个辅助类,例如:

    public class FileProvider7 { public static Uri getUriForFile(Context context, File file) { Uri fileUri = null; if (Build.VERSION.SDK_INT >= 24) { fileUri = getUriForFile24(context, file); } else { fileUri = Uri.fromFile(file); } return fileUri; } public static Uri getUriForFile24(Context context, File file) { Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context, context.getPackageName() + ".android7.fileprovider", file); return fileUri; } public static void setIntentDataAndType(Context context, Intent intent, String type, File file, boolean writeAble) { if (Build.VERSION.SDK_INT >= 24) { intent.setDataAndType(getUriForFile(context, file), type); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if (writeAble) { intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } } else { intent.setDataAndType(Uri.fromFile(file), type); } } }

    使用: 拍照

    Uri fileUri = FileProvider7.getUriForFile(this, file);

    安装apk

    FileProvider7.setIntentDataAndType(this, intent, "application/vnd.android.package-archive", file, true);

    参考 https://blog.csdn.net/growing_tree/article/details/71190741 https://blog.csdn.net/lmj623565791/article/details/72859156

    Processed: 0.009, SQL: 8