评论

收藏

[Android] Android系统编程入门系列之应用级文件在应用程序间的共享

移动开发 移动开发 发布于:2021-09-29 21:50 | 阅读数:225 | 评论:0

在上篇文章了解到应用级文件只能被其所创建的应用程序所访问,那么其他应用程序是不是就无论如何都无法访问了呢?肯定不是的,只要文件经过其创建的应用程序授权,还是可以被其他应用程序所访问的。这也就是应用级文件的共享。
系统只允许共享包含实际数据的纯文件类型,而不推荐共享包含文件的目录类型。
对于文件的访问可以使用java.io.File系统文件类,但是如果想将该文件分享出去,则需要借助android.net.Uri路径定位符类。Uri类是Android系统下自定义的路径定位符规则,其符合Java中定义的java.net.URI标准资源定位符规范,并新增了getPathSegments()获取分割路径列表的方法和getQueryParameter(String key)获取指定的额外参数方法等。
通常思路是将要分享的File文件对象转换成特定的Uri路径定位符对象。在Android7即API 24版本及以后的版本中,系统对Uri增加了授权处理,这将在后文详细解释。
之后将该Uri对象可以作android.os.Bundle数据对象通过android.content.Intent意图类传递。
最终在接收到意图的应用程序中,可以收到Uri路径并对其做不同处理,即可访问该路径下的文件,针对Uri中的内容不同,一般需要做不同的转换处理,这将在后文详细解释。
文件分享方
目标版本级别小于24的应用程序
在Android7即API 24版本以前,应用程序内的任何File对象,都可以通过静态方法Uri.fromFile(File file)将参数 file 直接转换为Uri对象。
得到的Uri对象可直接操作赋值给Intent意图对象的setData(Uri data)方法中的参数 data ,或putExtra(String key, Parcelable value)方法中的参数 value,从而分享传递出去。
目标版本级别大于等于24的应用程序
从Android7即API 24版本开始,应用程序内部共享文件,仍然可以使用Uri.fromFile(File file)方法,但是如果想在应用程序中对得到的Uri对象直接传递给其他应用程序使用,会抛出android.os.FileUriExposedException异常。因此,在使用androidx依赖包的应用程序中可以借助androidx.core.content.FileProvider文件分享提供类,在使用support-v4依赖包的应用程序中可以借助android.support.v4.content.FileProvider文件分享提供类,获取相关Uri对象,并为其授权,之后才可在不同应用程序间共享访问。
对于FileProvider的使用,要先做准备工作。
首先需要在应用程序的清单文件中注册。在<application></application>标签中增加<provider></provider>标签。
在该标签中指定属性android:name="androidx.core.content.FileProvider"以绑定文件分享提供类;
另外在该标签中指定属性android:authorities="xxx",这里的属性值xxx是应用程序所在系统内唯一的,其定义规则通常建议以应用程序包名为前缀的域名,且在后文代码中有使用;
同时在改标签中指定属性android:grantUriPermissions="true",属性值为 true 时,表示允许对得到的Uri所定位的File文件授予临时读写权限。
最后在改标签中增加<meta-data/>标签,以指定所使用的数据信息。在该数据标签中,必须定义其属性为android:name="android.support.FILE_PROVIDER_PATHS",同时定义属性android:resource="@xml/file_paths" ,这里的属性值@xml/file_paths是指向定义的资源文件file_paths.xml
在自定义的资源目录 res 下创建子目录 xml,该目录中创建文件 file_paths.xml 。定义<paths></paths>根标签以指定要分享的文件路径。在根标签中可以根据要分享的文件所属路径不同而使用不同类型的标签名作为子标签插入,子标签中包含有name和path两个属性,分别设置该路径的别名和子目录。其中子标签可用类型与代码中获取路径的对应关系可参考下表。
代码获取路径子标签名context.getFilesDir()<files-path>context.getCacheDir()<cache-path>Environment.getExternalStorageDirectory()<external-path>context.getExternalMediaDirs()<external-media-path>context.getExternalFilesDir()<external-file-path>context.getExternalCacheDir()<external-cache-path>在清单文件中注册FileProvider之后,就可以在代码获取Uri的地方,与低版本中获取Uri的方法Uri.fromFile(File file)所不同的是,替换为FileProvider的静态方法getUriForFile(Context context, String authority, File file)。参数 context 是当前应用程序所在的上下文环境;参数 authority 则是在清单文件中注册FileProvider时,属性android:authorities所表示的值;参数 file 依然是应用程序中要分享的文件对象。
在使用FileProvider.getUriForFile(Context context, String authority, File file)获取到Uri之后,需要授权给要使用该路径的应用程序,在能获取到上下文环境Context对象的地方,调用grantUriPermission (String toPackage, Uri uri, int modeFlags)方法授权。参数 toPackage 即为要授权使用该路径的应用程序所在包名;参数 uri 是上文获取到的路径定位符对象;参数 modeFlags 是用来表示权限类型的标志位,其值目前包括四种:Intent.FLAG_GRANT_READ_URI_PERMISSION=1的读权限,Intent.FLAG_GRANT_WRITE_URI_PERMISSION=2的写权限,Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION=4表示长久授权,和Intent.FLAG_GRANT_PREFIX_URI_PERMISSION=8对匹配参数 uri 的前缀的所有路径定位符均授权。
在对Uri对象授权之后,也就可以像老版本一样将其封装到Intent意图对象中发送给其他应用了。
文件接收方
一般地,在分享文件的接收方,如果只是对接收到的文件内容做读写处理,可以借助java.io.FileDescriptor文件描述类,使用官方推荐的简单方式操作。
在接收方收到的Intent意图对象调用getData()方法,得到要接收的文件所对应的Uri路径定位符对象。
在可以使用上下文环境Context对象的位置,调用其getContentResolver()方法,获取上下文环境中的ContentResolver内容解析类对象。
借助ContentResolver对象的openFileDescriptor (Uri uri, String mode)方法,返回android.os.ParcelFileDescriptor系统封装的文件描述类对象。其中参数 uri 就是上文接收到的路径定位符对象;参数 mode 则标记了文件的打开方式,包括只读权限"r",只写权限"w",追加写入权限"wa",读写权限"rw"等。
在得到ParcelFileDescriptor对象后,调用其getFileDescriptor()方法,可以获得FileDescriptor文件描述对象。
最终通过得到的文件描述对象,创建FileInputStream(FileDescriptor fdObj)构造方法或FileOutputStream(FileDescriptor fdObj)构造方法获取文件输入流对象或文件输出流对象,以此操作文件内容。
还有些特殊需求,对于分享文件接收方来说,是要对收到的文件获取其所在目录并做进一步操作,也就是需要对得到的Uri路径定位符对象转换为File文件对象。这方面官方并没有像上文那样简单统一的调用方式,而需要开发者针对不同的版本做单独处理。
目标版本级别小于29的应用程序
在Android10即API 29之前的版本中,网上有一堆代码可参考,其思路就是查询系统数据库中不同媒体文件是否与Uri对象所指向位置匹配,进而确定该Uri对象指向的媒体文件目录,在应用程序内借助上下文环境获取其媒体文件所在目录File对象,从而匹配要查询的Uri路径定位符对象。
目标版本级别大于等于29的应用程序
从Android10及API 29版本开始,由于应用程序仅可访问自己分区存储内部的文件,对于收到的分享文件路径定位符Uri对象,也只能先使用上文官方推荐的读取文件方式,将Uri对象所指向的文件先保存一份到自己应用程序内部的私有目录下,再将这份私有目录下的保存文件转为File对象使用。