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

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

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

<providerandroid:name="androidx.core.content.FileProvider"android:authorities="me.demo.fileprovider.provider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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 permissionsfinal 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对象去获取文件。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

国产linux系统(银河麒麟,统信uos)使用 PageOffice 动态生成word文件

PageOffice 国产版 &#xff1a;支持信创系统&#xff0c;支持银河麒麟V10和统信UOS&#xff0c;支持X86&#xff08;intel、兆芯、海光等&#xff09;、ARM&#xff08;飞腾、鲲鹏、麒麟等&#xff09;、龙芯&#xff08;LoogArch&#xff09;芯片架构。 数据区域填充文本 数…

《Python制作动态爱心粒子特效》

一、实现思路 粒子效果&#xff1a; – 使用Pygame模拟粒子运动&#xff0c;粒子会以爱心的轨迹分布并运动。爱心公式&#xff1a; 爱心的数学公式&#xff1a; x16sin 3 (t),y13cos(t)−5cos(2t)−2cos(3t)−cos(4t) 参数 t t 的范围决定爱心形状。 动态效果&#xff1a; 粒子…

[Docker-显示所有容器IP] 显示docker-compose.yml中所有容器IP的方法

本文由Markdown语法编辑器编辑完成。 1. 需求背景: 最近在启动一个服务时&#xff0c;突然发现它的一个接口&#xff0c;被另一个服务ip频繁的请求。 按理说&#xff0c;之前设置的是&#xff0c;每隔1分钟请求一次接口。但从日志来看&#xff0c;则是1秒钟请求一次&#xff…

JDK、MAVEN与IDEA的安装与配置

1.认识JDK、MAVEN与IDEA JDK 提供了编译和运行Java程序的基本环境。Maven 帮助管理项目的构建和依赖。IDEA 提供了一个强大的开发环境&#xff0c;使得编写、调试和运行Java程序更加高效。 2. 安装与环境配置 2.1 官网地址 选择你需要的版本下载&#xff1a; MAVEN下载传送…

C++标准模板库 -- map和set

序列式容器和关联式容器 在本篇文章之前&#xff0c;我们已经接触了STL中的部分容器&#xff1a;如string、vector、list、deque、array、forward_list等&#xff0c;这些容器被统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值一般…

【Xbim+C#】创建圆盘扫掠IfcSweptDiskSolid

基础回顾 https://blog.csdn.net/liqian_ken/article/details/143867404 https://blog.csdn.net/liqian_ken/article/details/114851319 效果图 代码示例 在前文基础上&#xff0c;增加一个工具方法&#xff1a; public static IfcProductDefinitionShape CreateDiskSolidSha…

Flutter踩坑记录(三)-- 更改入口执行文件

我们在flutter 中可能不习惯默认的lib/main.dart 作为入口文件&#xff0c;会修改成index.dart 或者修改main.dart的位置, 用Andorid studio开发 如果我们用Andorid studio开发&#xff0c;默认修改一下配置地址 运行项目即可。 用VSCode开发 如果我们使用VSCode开发&…

AbsPlus框架介绍2

ABSPlus框架以其集成的多功能性在市场上脱颖而出。它不仅提供美观且符合主流风格的页面设计&#xff0c;还支持灵活的流程配置&#xff0c;包括算法处理流程和页面审批流程。在众多业务系统中&#xff0c;流程管理往往是核心且复杂的挑战&#xff0c;涉及数据库设计、页面开发以…

算法.图论-习题全集(Updating)

文章目录 本节设置的意义并查集篇并查集简介以及常见技巧并查集板子(洛谷)情侣牵手问题相似的字符串组岛屿数量(并查集做法)省份数量移除最多的同行或同列石头最大的人工岛找出知晓秘密的所有专家 建图及其拓扑排序篇链式前向星建图板子课程表 本节设置的意义 主要就是为了复习…

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

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

图像处理 之 凸包和最小外围轮廓生成

“ 最小包围轮廓之美” 一起来欣赏图形之美~ 1.原始图片 男人牵着机器狗 2.轮廓提取 轮廓提取 3.最小包围轮廓 最小包围轮廓 4.凸包 凸包 5.凸包和最小包围轮廓的合照 凸包和最小包围轮廓的合照 上述图片中凸包、最小外围轮廓效果为作者实现算法生成。 图形几何之美系列&#…

Nuxt.js 应用中的 webpack:configResolved事件钩子

title: Nuxt.js 应用中的 webpack:configResolved事件钩子 date: 2024/11/21 updated: 2024/11/21 author: cmdragon excerpt: 在 Nuxt.js 项目中,webpack:configResolved 钩子允许开发者在 Webpack 配置被解析后读取和修改该配置。这一钩子在所有 Webpack 配置被合并和确…

java-贪心算法

1. 霍夫曼编码&#xff08;Huffman Coding&#xff09; 描述&#xff1a; 霍夫曼编码是一种使用变长编码表对数据进行编码的算法&#xff0c;由David A. Huffman在1952年发明。它是一种贪心算法&#xff0c;用于数据压缩。霍夫曼编码通过构建一个二叉树&#xff08;霍夫曼树&a…

推荐一款专业电脑护眼工具:CareUEyes Pro

CareUEyes Pro是一款非常好用的专业电脑护眼工具&#xff0c;软件小巧&#xff0c;界面简单&#xff0c;它可以自动过滤电脑屏幕的蓝光&#xff0c;让屏幕显示更加的不伤眼&#xff0c;更加舒适&#xff0c;有效保护你的眼睛&#xff0c;可以自定义调节屏幕的色调&#xff0c;从…

记录一下在原有的接口中增加文件上传☞@RequestPart

首先&#xff0c;咱声明一下&#xff1a; RequestBody和 MultipartFile 不可以 同时使用&#xff01;&#xff01;&#xff01; 因为这两者预期的请求内容类型不同。RequestBody 预期请求的 Content-Type 是 application/json 或 application/xml&#xff0c;而 MultipartFile …

国标GB28181视频平台EasyCVR视频融合平台H.265/H.264转码业务流程

在当今数字化、网络化的视频监控领域&#xff0c;大中型项目对于视频监控管理平台的需求日益增长&#xff0c;特别是在跨区域、多设备、高并发的复杂环境中。EasyCVR视频监控汇聚管理平台正是为了满足这些需求而设计的&#xff0c;它不仅提供了全面的管理功能&#xff0c;还支持…

JavaSrcipt 函数高级

一 原型与原型链 prototype 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象或者显示原型) 原型对象prototype中有一个属性constructor, 它指向函数对象 function a(){}console.log(typeof a,typeof Date)console.log(a.prototype, Date.prot…

蓝桥杯每日真题 - 第17天

题目&#xff1a;&#xff08;最大数字&#xff09; 题目描述&#xff08;13届 C&C B组D题&#xff09; 题目分析&#xff1a; 操作规则&#xff1a; 1号操作&#xff1a;将数字加1&#xff08;如果该数字为9&#xff0c;变为0&#xff09;。 2号操作&#xff1a;将数字…

sysbench压测DM的高可用切换测试

一、配置集群 1. 配置svc.conf [rootlocalhost dm]# cat /etc/dm_svc.conf TIME_ZONE(480) LANGUAGE(CN)DM(192.168.112.139:5236,192.168.112.140:5236) [DM] LOGIN_MODE(1) SWITCH_TIME(300) SWITCH_INTERVAL(200)二、编译sysbench 2.1 配置环境变量 [dmdba~]# vi ~/.bas…

解决vue-pdf的签章不显示问题

在使用vue-pdf 4.3.0时发现上传一般的普通pdf正常预览&#xff0c;但是上传带有红头文件的和和特殊字体的pdf无法正常内容显示&#xff0c;文字丢失问题。 1、查看控制台报错信息 2、缺少字体原因 getNumPages(url) {var loadingTask pdf.createLoadingTask({url: url,//引入…