FileProvider高版本使用,跨进程传输文件

高版本的android对文件权限的管控抓的很严格,理论上两个应用之间的文件传递现在都应该是用FileProvider去实现,这篇博客来一起了解下它的实现原理。

首先我们要明确一点,FileProvider就是一个ContentProvider,所以需要在AndroidManifest.xml里面对它进行声明:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="me.demo.fileprovider.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_path" />
</provider>

和普通的ContentProvider不一样的是他多了一个android.support.FILE_PROVIDER_PATHS的meta-data指定了一个xml资源:

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

文件URI

这个xml的作用在于为文件生成URI,root-path、files-path、cache-path这些标签代表父路径:

root-path : File("/")
files-path : Context.getFilesDir()
cache-path : context.getCacheDir()
external-path : Environment.getExternalStorageDirectory()
external-files-path : ContextCompat.getExternalFilesDirs(context, null)[0]
external-cache-path :  ContextCompat.getExternalCacheDirs(context)[0]
external-media-path :  context.getExternalMediaDirs()[0]

path属性代表子路径,name代表为"父路径/子路径"起的名字,

<files-path name="files" path="images/" />

例如上面配置代表的就是我们为 new File(context.getFilesDir(), "images/") 这个路径起了个名字叫做files

val filesDir = File(context.getFilesDir(), "images/")
val uri = FileProvider.getUriForFile(this, "me.linjw.demo.fileprovider.provider", File(filesDir, "test.jpg"))
// uri就是把filesDir的路径转换"files",然后加上content://me.linjw.demo.fileprovider.provider
// 即 "content://me.linjw.demo.fileprovider.provider/files/test.jpg"
从FileProvider的源码里面就能看到这部分的转换逻辑:
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
 
 
...
 
 
int type;
while ((type = in.next()) != END_DOCUMENT) {
    if (type == START_TAG) {
        final String tag = in.getName();
 
 
        final String name = in.getAttributeValue(null, ATTR_NAME);
        String path = in.getAttributeValue(null, ATTR_PATH);
 
 
        File target = null;
        if (TAG_ROOT_PATH.equals(tag)) {
            target = DEVICE_ROOT;
        } else if (TAG_FILES_PATH.equals(tag)) {
            target = context.getFilesDir();
        } else if (TAG_CACHE_PATH.equals(tag)) {
            target = context.getCacheDir();
        } else if (TAG_EXTERNAL.equals(tag)) {
            target = Environment.getExternalStorageDirectory();
        } else if (TAG_EXTERNAL_FILES.equals(tag)) {
            File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
            if (externalFilesDirs.length > 0) {
                target = externalFilesDirs[0];
            }
        } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
            File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
            if (externalCacheDirs.length > 0) {
                target = externalCacheDirs[0];
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                && TAG_EXTERNAL_MEDIA.equals(tag)) {
            File[] externalMediaDirs = context.getExternalMediaDirs();
            if (externalMediaDirs.length > 0) {
                target = externalMediaDirs[0];
            }
        }
 
 
        if (target != null) {
            strat.addRoot(name, buildPath(target, path));
        }
    }
}
 
 
...
 
 
private static File buildPath(File base, String... segments) {
    File cur = base;
    for (String segment : segments) {
        if (segment != null) {
            cur = new File(cur, segment);
        }
    }
    return cur;
}

查询的时候就只需要从strat里面找到文件路径最匹配的name即可。

打开文件

有了这个uri之后我们就能通过Intent将它传给其他应用,并配置Intent.FLAG_GRANT_READ_URI_PERMISSION或者Intent.FLAG_GRANT_WRITE_URI_PERMISSION为其他应用设置读写权限:

val uri = FileProvider.getUriForFile(this, "me.linjw.demo.fileprovider.provider", file)
val intent = Intent()
intent.data = uri
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setClassName("me.linjw.demo.fileprovider.recv", "me.linjw.demo.fileprovider.recv.MainActivity")
startActivity(intent)

其他应用拿到这个uri就可以通过ContentResolver.openInputStream打开文件流:

val inputStream = intent.data?.let { contentResolver.openInputStream(it) }

或者有时候我们希望通过String传递uri的时候可以提前使用Context.grantUriPermission为指定的包名申请权限,然后接收端Uri.parse去解析出Uri来操作文件:

// 发送端
val uri = FileProvider.getUriForFile(this, "me.linjw.demo.fileprovider.provider", file)
grantUriPermission("me.linjw.demo.fileprovider.recv", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
 
 
val intent = Intent()
intent.putExtra("uri", uri.toString())
intent.setClassName("me.linjw.demo.fileprovider.recv", "me.linjw.demo.fileprovider.recv.MainActivity")
startActivity(intent)
 
 
// 接收端
val uri = Uri.parse(intent.getStringExtra("uri"))
val inputStream = contentResolver.openInputStream(uri)

Uri操作文件的原理实际上就是通过请求我们之前声明的me.linjw.demo.fileprovider.provider这个ContentProvider,让它给我们去打开文件:

// FileProvider.java
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
    // ContentProvider has already checked granted permissions
    final File file = mStrategy.getFileForUri(uri);
    final int fileMode = modeToMode(mode);
    return ParcelFileDescriptor.open(file, fileMode);
}

也就是说文件权限的校验实际上只发生在打开的阶段.其他应用虽然没有权限打开我们的文件,但是我们可以在ContentProvider里面帮它打开然后返回文件描述符,给其他应用去读写。

系统应用使用FileProvider的坑

项目中有个系统应用需要向其他应用传的文件,于是把FileProvider加上,然后发现其他应用还是没有权限。从日志里面看是说这个FileProvider并没有从UID 1000里暴露出来:

02-13 06:52:28.921  4292  4292 E AndroidRuntime: Caused by: java.lang.SecurityException: 
Permission Denial: opening provider androidx.core.content.FileProvider from ProcessRecord{806d30d 4292:me.linjw.demo.fileprovider.recv/u0a53} (pid=4292, uid=10053) that is not exported from UID 1000

由于这个UID 1000太显眼,所以尝试将系统签名去掉发现权限就正常了,实锤是系统签名的原因。

查看出现异常的时候的日志,发现了下面的打印:

02-13 06:52:28.486   863  1393 W UriGrantsManagerService: For security reasons, the system cannot issue a Uri permission grant to content://me.linjw.demo.fileprovider.provider/root/data/user/0/me.linjw.demo.fileprovider/files/test.txt [user 0]; use startActivityAsCaller() instead

在代码里面搜索关键字,发现系统应用需要在源码里面配置FileProvider的authorities:

// https://cs.android.com/android/platform/superproject/+/android-13.0.0_r29:frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java
 
 
// Bail early if system is trying to hand out permissions directly; it
// must always grant permissions on behalf of someone explicit.
final int callingAppId = UserHandle.getAppId(callingUid);
if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
    if ("com.android.settings.files".equals(grantUri.uri.getAuthority())
            || "com.android.settings.module_licenses".equals(grantUri.uri.getAuthority())) {
        // Exempted authority for
        // 1. cropping user photos and sharing a generated license html
        //    file in Settings app
        // 2. sharing a generated license html file in TvSettings app
        // 3. Sharing module license files from Settings app
    } else {
        Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
                + " grant to " + grantUri + "; use startActivityAsCaller() instead");
        return -1;
    }
}

直接传递ParcelFileDescriptor

从原理上看FileProvider实际就是打开文件的ParcelFileDescriptor传给其他应用使用,那我们能不能直接打开文件然后将ParcelFileDescriptor直接通过Intent传给其他应用呢?

val intent = Intent()
intent.putExtra("fd" , ParcelFileDescriptor.open(file, MODE_READ_ONLY))
intent.setClassName("me.demo.fileprovider.recv", "me.linjw.demo.fileprovider.recv.MainActivity")
startActivity(intent)

答案是不行:

02-15 20:27:24.200 16968 16968 E AndroidRuntime: Process: me.linjw.demo.fileprovider, PID: 16968
02-15 20:27:24.200 16968 16968 E AndroidRuntime: java.lang.RuntimeException: Not allowed to write file descriptors here                        
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.nativeWriteFileDescriptor(Native Method)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.writeFileDescriptor(Parcel.java:922)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.ParcelFileDescriptor.writeToParcel(ParcelFileDescriptor.java:1110)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.writeParcelable(Parcel.java:1953)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.writeValue(Parcel.java:1859)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.writeArrayMapInternal(Parcel.java:1024)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1620)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Bundle.writeToParcel(Bundle.java:1304)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.os.Parcel.writeBundle(Parcel.java:1093)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.content.Intent.writeToParcel(Intent.java:11123)
02-15 20:27:24.200 16968 16968 E AndroidRuntime:        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:
2298)

原因在于Instrumentation的execStartActivity启动Activity前会调用Intent.prepareToLeaveProcess最终调用到Bundle.setAllowFds(false)不允许传递ParcelFileDescriptor:

// https://cs.android.com/android/platform/superproject/+/android-13.0.0_r29:frameworks/base/core/java/android/app/Instrumentation.java
public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ...
    intent.prepareToLeaveProcess(who);
    ...
}
 
 
 
 
// https://cs.android.com/android/platform/superproject/+/android-13.0.0_r29:frameworks/base/core/java/android/content/Intent.java
public void prepareToLeaveProcess(Context context) {
    final boolean leavingPackage;
    if (mComponent != null) {
        leavingPackage = !Objects.equals(mComponent.getPackageName(), context.getPackageName());
    } else if (mPackage != null) {
        leavingPackage = !Objects.equals(mPackage, context.getPackageName());
    } else {
        leavingPackage = true;
    }
    prepareToLeaveProcess(leavingPackage);
}
 
 
/**
 * Prepare this {@link Intent} to leave an app process.
 *
 * @hide
 */
public void prepareToLeaveProcess(boolean leavingPackage) {
    setAllowFds(false);
    ...
}
 
 
public void setAllowFds(boolean allowFds) {
    if (mExtras != null) {
        mExtras.setAllowFds(allowFds);
    }
}

一开始我想通过反射去强行调用setAllowFds(true),但是发现这个方法被限制了,需要系统权限才能调用:

Accessing hidden method Landroid/os/Bundle;->setAllowFds(Z)Z (max-target-o, reflection, denied)

只能另谋出路,由于ParcelFileDescriptor实现了Parcelable,所以我们可以通过传递Binder的方式迂回的去传递:

// aidl
interface IFileDescriptorsProvider {
    ParcelFileDescriptor get();
}
 
 
// 发送端
val fileProvider = object : IFileDescriptorsProvider.Stub() {
    override fun get(): ParcelFileDescriptor {
        return ParcelFileDescriptor.open(file, MODE_READ_ONLY)
    }
}
val intent = Intent()
val bundle = Bundle().apply { putBinder("fileProvider", fileProvider) }
intent.putExtras(bundle)
intent.setClassName("me.demo.fileprovider.recv", "me.demo.fileprovider.recv.MainActivity")
startActivity(intent)
 
 
// 接收端
val text = intent.extras?.getBinder("fileProvider")?.let { it ->
    val fd = IFileDescriptorsProvider.Stub.asInterface(it).get()
    AssetFileDescriptor(fd, 0, -1)
        .createInputStream()
        .use { it.bufferedReader().readLine() }
}

也可以封装成服务端service提供接口,客户端通过aidl连接服务端后,调用服务端接口返回ParcelFileDescriptor对象去获取文件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921006.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Java】二叉树:数据海洋中灯塔式结构探秘(上)

个人主页 &#x1f339;&#xff1a;喜欢做梦 二叉树中有一个树&#xff0c;我们可以猜到他和树有关&#xff0c;那我们先了解一下什么是树&#xff0c;在来了解一下二叉树 一&#x1f35d;、树型结构 1&#x1f368;.什么是树型结构&#xff1f; 树是一种非线性的数据结构&…

网口输出的加速度传感器

一、功能概述 1.1 设备简介 本模块为了对电机、风机、水泵等旋转设备进行预测性运维而开发&#xff0c;只需一个模块&#xff0c; 就可以采集旋转设备的 3 路振动信号&#xff08;XYZ 轴&#xff09;和一路温度信号&#xff0c;防护等级 IP67 &#xff0c;能够 适应恶劣的工业…

力扣面试经典 150(上)

文章目录 数组/字符串1. 合并两个有序数组2. 移除元素3. 删除有序数组中的重复项4. 删除有序数组的重复项II5. 多数元素6. 轮转数组7. 买卖股票的最佳时机8. 买卖股票的最佳时机II9. 跳跃游戏10. 跳跃游戏II11. H 指数12. O(1)时间插入、删除和获取随机元素13. 除自身以外数组的…

浅谈 proxy

应用场景 Vue2采用的defineProperty去实现数据绑定&#xff0c;Vue3则改为Proxy&#xff0c;遇到了什么问题&#xff1f; - 在Vue2中不能检测数组和对象的变化 1. 无法检测 对象property 的添加或移除 var vm new Vue({data:{a:1} })// vm.a 是响应式的vm.b 2 // vm.b 是…

P4-1【应用数组进行程序设计】第一节——知识要点:一维数组

视频&#xff1a; P4-1【应用数组进行程序设计】第一节——知识要点&#xff1a;一维数组 项目四 应用数组进行程序设计 任务一&#xff1a;冒泡排序 知识要点&#xff1a;一维数组 目录 一、任务分析 二、必备知识与理论 三、任务实施 一、任务分析 用冒泡法对任意输入…

【数据库入门】关系型数据库入门及SQL语句的编写

1.数据库的类型&#xff1a; 数据库分为网状数据库&#xff0c;层次数据库&#xff0c;关系型数据库和非关系型数据库四种。 目前市场上比较主流的是&#xff1a;关系型数据库和非关系型数据库。 关系型数据库使用结构化查询语句&#xff08;SQL&#xff09;对关系型数据库进行…

day07(单片机高级)继电器模块绘制

目录 继电器模块绘制 原理图 布局 添加板框 布线 按tab修改线宽度 布线换层 泪滴 铺铜 铺铜的作用 铺铜的使用规范 添加丝印 步骤总结 继电器模块绘制 到淘宝找一个继电器模块 继电器模块的使用&#xff08;超详细&#xff09;_继电器模块工作原理-CSDN博客文章浏览阅读4.8w次&…

1+X应急响应(网络)病毒与木马的处置:

病毒与木马的处置&#xff1a; 病毒与木马的简介&#xff1a; 病毒和木马的排查与恢复&#xff1a;

【电路笔记 TMS320F28335DSP】时钟+看门狗+相关寄存器(功能模块使能、时钟频率配置、看门狗配置)

时钟源和主时钟&#xff08;SYSCLKOUT&#xff09; 外部晶振&#xff1a;通常使用外部晶振&#xff08;如 20 MHz&#xff09;作为主要时钟源。内部振荡器&#xff1a;还可以选择内部振荡器&#xff08;INTOSC1 和 INTOSC2&#xff09;&#xff0c;适合无需高精度外部时钟的应…

CCE-基础

背景&#xff1a; 虚拟化产生解决物理机资源浪费问题&#xff0c;云计算出现实现虚拟化资源调度和管理&#xff0c;容器出现继续压榨虚拟化技术产生的资源浪费&#xff0c;用命名空间隔离&#xff08;namespace&#xff09; 灰度升级&#xff08;升级中不影响业务&#xff09…

基于LLama_factory的Qwen2.5大模型的微调笔记

Qwen2.5大模型微调记录 LLama-facrotyQwen2.5 模型下载。huggingface 下载方式Modelscope 下载方式 数据集准备模型微调模型训练模型验证及推理模型导出 部署推理vllm 推理Sglang 推理 LLama-facroty 根据git上步骤安装即可&#xff0c;要求的软硬件都装上。 llama-factory运行…

提取图片高频信息

提取图片高频信息 示例-输入&#xff1a; 示例-输出&#xff1a; 代码实现&#xff1a; import cv2 import numpy as npdef edge_calc(image):src cv2.GaussianBlur(image, (3, 3), 0)ddepth cv2.CV_16Sgray cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)grad_x cv2.Scharr(g…

移动充储机器人“小奥”的多场景应用(上)

一、高速公路服务区应用 在高速公路服务区&#xff0c;新能源汽车的充电需求得到“小奥”机器人的及时响应。该机器人配备有储能电池和自动驾驶技术&#xff0c;能够迅速定位至指定充电点&#xff0c;为待充电的新能源汽车提供服务。得益于“小奥”的机动性&#xff0c;其服务…

怎么只提取视频中的声音?从视频中提取纯音频技巧

在数字媒体的广泛应用中&#xff0c;提取视频中的声音已成为一项常见且重要的操作。无论是为了学习、娱乐、创作还是法律用途&#xff0c;提取声音都能为我们带来诸多便利。怎么只提取视频中的声音&#xff1f;本文将详细介绍提取声音的原因、工具、方法以及注意事项。 一、为什…

Windows环境GeoServer打包Docker极速入门

目录 1.前言2.安装Docker3.准备Dockerfile4.拉取linux环境5.打包镜像6.数据挂载6.测试数据挂载7.总结 1.前言 在 Windows 环境下将 GeoServer 打包为 Docker&#xff0c;可以实现跨平台一致性、简化环境配置、快速部署与恢复&#xff0c;同时便于扩展集成和版本管理&#xff0c…

《Vue零基础入门教程》第四课: 应用实例

往期内容 《Vue零基础入门教程》第一课&#xff1a;Vue简介 《Vue零基础入门教程》第二课&#xff1a;搭建开发环境 《Vue零基础入门教程》第三课&#xff1a;起步案例 参考官方文档 https://cn.vuejs.org/api/application#create-app 示例 const {createApp} Vue// 通…

NUXT3学习日记四(路由中间件、导航守卫)

前言 在 Nuxt 3 中&#xff0c;中间件&#xff08;Middleware&#xff09;是用于在页面渲染之前或导航发生之前执行的函数。它们允许你在路由切换时执行逻辑&#xff0c;像是身份验证、重定向、权限控制、数据预加载等任务。中间件可以被全局使用&#xff0c;也可以只在特定页…

使用docker快速部署Nginx、Redis、MySQL、Tomcat以及制作镜像

文章目录 应用快速部署NginxRedisMySQLTomcat 制作镜像镜像原理基于已有容器创建使用 Dockerfile 创建镜像指令说明构建应用创建 Dockerfile 文件创建镜像 应用快速部署 Nginx docker run -d -p 80:80 nginx使用浏览器访问虚拟机地址 Redis docker pull redis docker run --…

02微服务系统与设计(D1_走出微服务误区:避免从单体到分布式单体)

目录 学习前言 一、回顾&#xff1a;从单体到微服务到 Function 二、分布式单体 分布式单体起因之一&#xff1a;通过共享库和网络客户端访问分布式能力 分布式单体起因之二&#xff1a;简单用远程调用替代进程内方法调用 分布式单体起因小结 三、引入非侵入式方案&#…

WEB攻防-通用漏洞文件上传js验证mimeuser.ini语言特性

知识点&#xff1a; 1、文件上传-前端验证 2、文件上传-黑白名单 3、文件上传-user.ini妙用 4、文件上传-php语言特性 详细点&#xff1a; 1、检测层面&#xff1a;前端&#xff0c;后端等 2、检测内容&#xff1a;文件头&#xff0c;完整型&#xff0c;二次渲染等 3、检…