IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 文件共享 -> 正文阅读

[移动开发]Android 文件共享

一、前言

????应用通常需要向其他应用提供一个或者多个文件。例如:相册应用会需要向图片编辑器提供图片文件,文件管理器应用需要让用户可以在不同的存储区域内移动和拷贝文件。发送方应用可以共享文件的唯一方式是响应接收方应用的请求。

????在任何情况下,从您的应用共享文件到其他接收方应用,唯一的安全做法就是发送一个具有临时访问权限的内容 URI。具有临时访问权限的内容 URI 之所以安全,是因为它只允许接收 URI 的应用访问,并且会自动过期。Android FileProvider 组件提供 FileProvider.getUriForFile() API 用于生产文件的内容 URI。

????在本博文中,将详细介绍如何通过 Android FileProvider 生成的内容 URI, 赋予接收应用临时访问权限,实现安全共享文件。

二、配置文件共享

????将文件从您的应用安全地共享给其他应用,您需要在您的应用中以内容 URI 的形式配置文件安全句柄
。Android FileProvider 组件根据您在 XML 资源定义的规格为文件生成内容 URI。本章节将详细介绍如何向您的应用中添加默认的 FileProvider 实现,并且指定您需要共享给其他应用的文件。

FileProvider 类是 AndroidX 和辛苦的一部分(如果项目使用support支持库,那么就是 support-v4),使用需要在项目中引入相应依赖。

2.1 定义 FileProvider

????为您的应用定义一个 FileProvider,需要在清单文件中定义一个条目(使用 ``` 标签定义),这个条目指定用于生成内容 URI 的授权(authority),以及指定应用可共享的存储目录的 XML 资源文件名称。

????定义 FileProvider,在 application 标签内部使用 <provider> 标签进行定义,使用 android:authorities 属性指定 FileProvider 生成内容 URI 的授权,授权字符串必须保证唯一。在 <provider> 标签内部,必须包含一个 <meta-data> 子标签,其中 android:name 属性值必须是 android.support.FILE_PROVIDER_PATHSandroid:resources指向一个 XML 资源文件,该 XML 资源文件指定需要共享的目录,XML 资源文件放在 res/xml 目录中,在 <meta-data> 标签中的 android:resource 属性指定 XML 文件(关于 XML 文件内容参考下一章节内容 指定共享目录)。如下示例所示:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.owen.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

注意事项:
1. 对于您自己的应用,考虑使用 应用包名.fileprovider 的方式指定授权(亦可增加其他字符),防止不同应用间出现授权冲突;
2. 必须为 FileProvider 指定共享目录。

2.2 指定共享目录

????当您在清单文件中添加了 FileProvider,您需要指定包含共享文件的存储目录。指定共享目录,首先需要创建一个 XML 资源文件,存放在 res/xml 目录下。如下示例所示:

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

    <external-path name="." path="."/>

    <external-files-path name="picture" path="picture"/>
</paths>

说明:指定 FileProvider 共享目录的 XML 文件,以 <paths> 为根节点,根节点下可包含 <files-path>(应用内部存储 files 目录)、<cache-path>(应用内部存储 caches 目录)、<external-path>(外部存储目录)、<external-files-path>(外部存储 files 目录)、<external-cache-path>(外部存储 caches 目录)、<external-media-path> (外部存储 medias 目录)节点,这些节点分别指定不同目录下的子目录,每个节点可以同时存在多个不同名称和路径的子目录。更多关于指定 FileProvider 目录的内容请参考 Android FileProvider 详解

注意事项:XML 资源文件是唯一指定你的应用需要共享的目录的途径,切勿在应用中编码使用其他没有配置的目录。

2.3 生成内容 URI

????配置好 ContentProvider 之后,就可以使用 FileProvider 生成文件的内容 URI。调用 FileProvider.getUriForFile() API ,传入<provider> 标签 android:authorities 属性声明的授权以及文件对象,即可生成内容 URI,如下代码所示:

val dbFile = File(appContext.filesDir, "db/init_data.db")
val uri = FileProvider.getUriForFile(appContext, appContext.packageName + ".owen.fileprovider", dbFile);
println(uri.toString())

????通过调用 FileProvider.getUriForFile() API 生成的内容 URI 中包含 <provider> 标签 android:authorities 属性声明的授权,文件目录(XML 中 <meta-data> 指定的目录)相对应的相对路径,是 content://<authorities>/<path> 的形式。以上示例打印出来的内容 URI 如下所示:

content://com.owen.demo.android.owen.fileprovider/db/init_data.db

注意事项:
1. FileProvider.getUriForFile() 只能对 XML 文件声明的目录及其子目录下的文件生成内容 URI;
2. FileProvider.getUriForFile() 的授权(第二个参数)必须和清单文件中 provider 定义的授权一致。

更多关于 FileProvider 相关内容请参考 Android FileProvider 详解

三、共享文件

????当设置您的应用使用内容 URI 共享文件之后,您就可以响应其他应用对这些文件的请求。要响应其他应用对这些文件的请求,其中的一个方法就是在服务端应用提供一个能够被其他应用调用的文件选择界面,这种方法允许客户端应用让用户从服务端应用中选择一个文件,并能够获取到选中文件的内容 URI。

3.1 接收文件请求

????服务端应用接收客户端应用的文件请求,并以内容 URI 进行响应,服务端应用应当提供一个文件选择 Activity。客户端应用通过调用 startActivityForResult() 并传入包含 Intent.ACTION_PICK 动作的 Intent 对象启动这个 Activity 。当客户端应用调用 startActivityForResult(),将会启动文件选择 Activity,用户在界面上选择一个文件,服务端应用就会将用户选择的文件内容 URI 返回到客户端应用。

val picFileIntent = Intent(Intent.ACTION_PICK).apply {
    type = "image/*"
}
startActivityForResult(picFileIntent, 1000)

3.2 创建文件选择 Activity

????创建一个 Acivity 类用于文件选择器,并配置文件选择 Activity,首先需要在清单文件中声明该 Activity。然后在清单配置中为 Activity 添加一个 Intent 过滤器,Intent 过滤器接收 ACTION_PICKandroid.intent.action.PICK)动作,包含 CATEGORY_DEFAULTandroid.intent.category.DEFAULT) 和 CATEGORY_OPENABLEandroid.intent.category.OPENABLE)类别,并且添加服务端应用能够为其他应用提供选取的文件媒体类型。如下示例所示:

<activity android:name=".share.FileSelectActivity">
    <!-- 配置 Intent 过滤器 -->
    <intent-filter>
        <!-- 配置接收的动作 -->
        <action android:name="android.intent.action.PICK" />

        <!-- 配置类别 -->
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.OPENABLE" />

        <!-- 配置为其他应用提供的文件媒体类型 -->
        <data android:mimeType="image/*" />
    </intent-filter>
</activity>

3.3 响应文件选择

????在文件选择 Activity 中,需要用户选择了文件之后,您的应用需要判定用户选择了哪个文件,并且为改文件生成内容 URI。从您的应用响应选择的文件给请求应用时,是通过 Intent 传递文件 URI,但是需要特别注意的是,传递的文件 URI 对请求应用必须是可读的(即有权限读取)。尤其是在搭载 Android 6.0(API Level 23)及以上系统的设备上,因为从 Android 6.0(API Level 23)开始权限机制变更,并且 READ_EXTERNAL_STORAGE 权限被归类为危险权限,而接收结果的应用(请求应用)可能没有这个权限。 考虑到这些因素,强烈建议您避免使用 Uri.fromFile() 这个方法生成文件 URI,因为这个方法生成的文件 URI 有以下弊端:

  • 不允许跨越配置文件共享文件;
  • 在Android 4.4(API Level 19)版本之前,您的应用需要有 WRITE_EXTERNAL_STORAGE权限;
  • 接收者应用需要有 READ_EXTERNAL_STORAGE 权限,但是许多共享目标(接收者应用)没有这个权限;

????不过可以考虑不使用 Uri.fromFile() 生成 URI,而是用 URI 权限来授予其他应用对特定 URI 的访问权限。URI 权限不适用于 Uri.fromFile() 生成的 file:// 类型的 URI,但适用于内容提供程序(Content Provider)相关联的 URI,FileProvider 类的 API 可以帮助生成此类 URI。URI 权限同样适用于文件存储在在发送 Intent 的应用内部存储,而不是存储在外部存储的情况。对指定文件生成内容 URI,可参考:生成内容 URI 相关章节。

3.4 授予文件访问权限

????在获取到需要共享到其他应用的文件的 URI 之后,你需要授权允许客户端应用(接收者应用)访问文件。将内容 URI 添加到 Intent 对象,调用 Intent.addFlags() 添加Intent.FLAG_GRANT_READ_URI_PERMISSION 标志为 Intent 添加权限标志,即可授予客户端应用访问权限。授予客户端应用的访问权限是临时,在接收者应用的任务栈关闭后将会自动失效。如下示例代码所示

val dbFile = File(appContext.filesDir, "db/init_data.db")
val uri = FileProvider.getUriForFile(appContext, appContext.packageName + ".owen.fileprovider", dbFile);

val resultIntent = Intent().apply {
    data = uri
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // 为 URI 授予客户端程序访问权限
}

注意事项:调用 Intent.addFlags() 是使用临时授权安全地授予文件访问权限的唯一方法。避免为内容 URI 调用 Context.grantUriPermission() 方法,因为使用该方法授予的访问权限只能通过调用 Context.revokeUriPermission() 来撤销授权。

????请不要使用 Uri.fromFile() 生成文件内容 URI,原因有几点:它要求接收者应用必须拥有 READ_EXTERNAL_STORAGE 权限;在多用户之间共享文件完全行不通;在 Android 4.4(API Level 19)以下版本,需要应用用用 WRITE_EXTERNAL_STORAGE 权限(许多分享目标并没有此权限)。但是可以使用 URI 权限来授权其他应用访问特定 URI,因为 URI 权限不使适用于使用 Uri.fromFile() 生成的file:// 类型的 URI,只适用于内容提供程序(Content Provider)相关联的 URI。因此,在文件共享中应当使用 FileProvider.

3.5 与客户端应用共享文件

????客户端应用向服务端应用请求文件,服务端应用选定文件之后,需要将结果返回给客户端应用,并设定文件访问权限。返回结果使用 Intent 进行封装,并且在服务端应用中通过 setResult() 设置返回结果。如下示例代码所示:

// 接收方应用处理返回结果
val dbFile = File(appContext.filesDir, "db/init_data.db")
val uri = FileProvider.getUriForFile(appContext, appContext.packageName + ".owen.fileprovider", dbFile);

val resultIntent = Intent().apply {
    data = uri
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // 为 URI 授予客户端程序访问权限
}
setResult(Activity.RESULT_OK, resultIntent)

3.6 请求方应用处理结果数据

????当请求的接收方应用的处理页面 Activity (例如文件选择 Activity)关闭后,系统会将包含结内容 URI 的 Intent 对象发送到请求方应用(客户端应用)。在请求方应用发起请求的 Activity 页面的 onActivityResult() 中就会接受到对应的结果。如下示例代码所示:

// 请求方应用处理接收到的结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK && requestCode == 1000) {
        data?.data?.apply {
            // TODO 获取结果 URI,进行下一步处理
        }
    }
}

3.7 请求方应用访问请求的文件

????因为内容 URI 是不包含任何文件路径的(只包含相对路径),所以客户端应用(请求方应用)是无法发现和打开服务端应用的其他文件。同理,请求方应用也无法直接通过文件的路径来访问请求的文件,但是可以在请求方应用中通过获取内容 URI 的 FileDescriptor 来访问。在请求方应用中使用 ContentResolver.openFileDescriptor() 对内容 RUI 进行解析,获取一个 ParcelFileDescriptor 对象,通过 ParcelFileDescriptor 对象获取 FileDescriptor 对象,就可以通过 FileInputStreamFileOutputStream 访问文件了。如下示例所示:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK && requestCode == 1000) {
        data?.data?.apply {
            val pfd = try {
                contentResolver.openFileDescriptor(this, "r")
            } catch (e: Exception) {
                // do something to deal with exception
                return;
            }
            val fd = pfd?.fileDescriptor
            val fis = FileInputStream(fd)
            
            // to read file
        }
    }
}

注意事项:
1. 必须进行了 URI 授权,接收方应用才有权限访问;
2. 内容太 URI 中不包含文件路径,所以无法直接访问,并且 URI 权限是临时的、仅对接收方应用有效的,接收方应用任务栈销毁时会自动失效;
3. 获取 ParcelFileDescriptor 对象是,必须根据需要设定正确的文件打开模式。

3.8 检索文件信息

????当接收方获取到请求结果(文件内容 URI)之后,在处理文件之前,需要先获取内容 URI 对应的文件的相关信息(包括文件大小、媒体类型等),在进一步对文件进行处理。

3.8.1 获取文件的媒体类型

????文件的数据类型指示客户端应用如何处理文件内容,客户端应用调用 ContentResolver.getType() 获取通过文件的内容 URI 获取文件的数据类型,返回值是媒体类型。默认情况下, FileProvider 根据文件名的后缀名确定文件的媒体类型。如下代码所示:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK && requestCode == 1000) {
        data?.data?.apply {         
            val mimeType = contentResolver.getType(this);
        }
    }
}

3.8.1 获取文件名及文件大小

????FileProvider 类具有 query() 方法的默认实现。通过调用 ContentResolverquery() 方法可以查询内容 URI 对应的文件信息,这个方法的查询结果中包含文件名和文件大小,返回结果是 Cursor 类型,文件名对应的字段是 OpenableColumns.DISPLAY_NAME(String 类型),文件大小对应的字段名是 OpenableColumns.SIZE(Long 类型)。如下代码所示:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if(resultCode == RESULT_OK && requestCode == 1000) {
        data?.data?.apply {

            contentResolver.query(this, null, null, null, null)?.use { cursor ->
                val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)

                cursor.moveToFirst()
                
                val fileName = cursor.getString(nameIndex)
                val fileSize = cursor.getLong(sizeIndex)
            }
        }
    }
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-28 07:56:17  更:2021-07-28 07:56:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/7 3:00:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码