作者:才华横溢的段老师 蓝田大营

一、背景

Android 从 N 开始不允许以 file:// 的方式通过 Intent 在两个 App 之间分享文件,取而代之的是通过 FileProvider 生成 content://Uri 。如果在 Android N 以上的版本继续使用 file:// 的方式分享文件,则系统会直接抛出异常,导致 App 出现 Crash ,同时会报以下错误日志:

FATAL EXCEPTION: mainProcess: com.inthecheesefactory.lab.intent_fileprovider, PID: 28905android.os.FileUriExposedException: file:///storage/emulated/0/.../xxx/xxx.jpg exposed beyond app through ClipData.Item.getUri()at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)at android.net.Uri.checkFileUriExposed(Uri.java:2346)at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)

当然如果工程的 targetSDK 小于24,暂时还不会遇到这个问题,一旦升级到24及以上,则会立即出现上述问题,所以提早做好预防很有必要,否则等到线上曝出大量的 bug 就很被动了。

二、关于 FileProvider

官方对于 FileProvider 的解释为:FileProvider 是一个特殊的 ContentProvider 子类,通过 content://Uri 代替 file://Uri 实现不同 App 间的文件安全共享。

当通过包含 Content URI 的 Intent 共享文件时,需要申请临时的读写权限,可以通过 Intent.setFlags() 方法实现。

而 file://Uri 方式需要申请长期有效的文件读写权限,直到这个权限被手动改变为止,这是极其不安全的做法。因此 Android 从 N 版本开始禁止通过 file://Uri 在不同 App 之间共享文件。

三、FileProvider 的使用流程

完成整个文件共享的流程,需要配置以下5点:

  1. 定义一个 FileProvider
  2. 指定有效的文件
  3. 为文件生成有效的 Content URI
  4. 申请临时的读写权限
  5. 发送 Content URI 至其他的 App

1. 定义 FileProvider

FileProvider 已经把文件生成 Content URI 的工作帮我们做掉了,因此我们只需要在 AndroidManifest.xml 文件中配置 <provider> 元素并提供相应的属性。

重要的属性包括以下四个:

  • 设置 android:name 为android.support.v4.content.FileProvider,这是固定的,不需要手动更改;
  • 设置 android:authorities 为 application id + .provider ;
  • 设置 android:exported 为 false ,表示 FileProvider 不是公开的;
  • 设置 android:grantUriPermissions 为 true 表示允许临时读写文件。

此处需要特别说明的是

  1. android:authorities 最好是 application id 而不能直接用包名硬编码,因为 Android 系统要求 android:authorities 对于每个 App 而言必须是唯一的。
  2. 假如 FileProvider 用在 SDK 中,多个 App 都在调用同一个 SDK,而 SDK 中的 android:authorities 为硬编码,那么 App 之间的 authorities 就会出现冲突,会报 Install shows error in console: INSTALL FAILED CONFLICTING PROVIDER 的错误。
  3. 如果 SDK 的 android:authorities 是 application id,那么 authorities 会和宿主 App 的 application id 保持一致,就不会出现 authorities 冲突的问题。
  4. 在 Java 代码中调用 getPackageName() 返回的是 application id ,而非 package name ,要验证这一点也很容易,在 build.gradle 文件中定义和包名不同的 application id ,打印代码中 getPackageName() 的返回值,就会发现返回值是 build.gradle 中自定义的 application id ,而非 package name
  5. 关于 package name 和 application id 的区别可以参考 ApplicationId 与 PackageName 的区别

以下是一个简单的示例:

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

需要说明的是 ${applicationId} 是占位符,Gradle 会替换成我们在 build.gralde 中定义的 applicationId "com.domain.example",如果 build.gradle 文件中没有定义,那么 application id的默认值是 App 的 package name。

2. 指定有效的文件

在生成 Content URI 之前你还需要提前指定文件目录,通常的做法是在 res 目录下新建一个 xml 文件夹,然后创建一个 xml 文件,在此文件中指定共享文件的路径和名字,示例如下:

<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-path name="my_images" path="images/"/>...
</paths>

其中 name 属性和 path 属性必填, name 表示共享文件的名字, path 代表文件路径。

  • external-path 代表文件位于手机外部存储空间,访问效果如同 Environment.getExternalStorageDirectory();
  • files-path 代表文件位于手机内部存储空间,访问效果如同 getFilesDir();
  • cache-path 代表文件位于手机内部缓存空间,访问效果如同 getCacheDir()。

xml 文件创建完成后,还需要在 manifest 文件的 <provider> 元素下完成相应的配置,假定 xml 文件命名为 file_paths.xml ,示例如下:

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

3. 为共享文件生成 Content URI

文件配置完成后还需要生成可以被其他 App 访问的 Content URI,可以直接调用 FileProvider 提供的 getUriForFile(File file) 方法,顾名思义,传入文件名称就可以得到相应的 Content URI 。需要访问该文件的 App 可以通过 ContentResolver.openFileDescriptor 得到一个 ParcelFileDescriptor 对象。

假定你想要共享一个图片文件,文件存放的位置为手机内部存储空间下的 images 文件夹,图片文件名字为 default_name.jpg ,那么生成 Content URI 方式如下:

File imagePath = new File(getContext().getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.provider", newFile);

最后生成的 Content URI 为

content://com.domain.example.provider/images/default_image.jpg.

4. 申请临时读写文件权限

上文已经提到 FileProvider 可以申请临时读写文件权限,以增强安全性,所以 Content URI 生成完成后,还需要申请临时访问权限。

通常直接通过 intent.setFlags 即可完成,具体的权限名称为:Intent.FLAG_GRANT_READ_URI_PERMISSION 和 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。

5. 发送 Content URI 至其他的 App

万事已备,只需要发送出去即可,通常都会使用 startActivityForResult 方法发送,可以在 onActivityResult 中获取其他 App 的处理结果,完成整个操作闭环。

三、实用场景——手机照相

在 Android N 之前的版本调用相机获取图片可以用如下代码实现:

// 设置照片需要存储的位置
photoPath = FileUtil.getImageFile().getPath()
Intent intent = new Intent();// 指定开启系统相机的Action
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.addCategory(Intent.CATEGORY_DEFAULT);// 把文件地址转换成Uri格式
Uri uri = Uri.parse("file://" + photoPath);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
activity.startActivityForResult(intent, requestCode);

如果要想在 Android N 及以上版本上不会出错,则必须将 file:// 形式替换成 content:// ,具体的代码如下:

Intent intent = new Intent();
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);// 系统版本大于N的统一用FileProvider处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {// 将文件转换成content://Uri的形式Uri photoURI = FileProvider.getUriForFile(activity,activity.getPackageName()+ ".provider",new File(photoPath));// 申请临时访问权限intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
} else {intent.addCategory(Intent.CATEGORY_DEFAULT);Uri uri = Uri.parse("file://" + photoPath);intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}
activity.startActivityForResult(intent, requestCode);

需要注意的是 getPackageName() 返回值是 application id,关于 application id 上文已经解释过,此处不再重复。

实用场景——微信朋友圈多图分享

微信官方不支持朋友圈直接多图分享,Android 之前的版本由于没有强制限制 file:// 的使用,所以可以通过访问微信包名的方式实现朋友圈多图分享,但是Android N 之后这种“曲线救国”的方式就不行了。

先来看一下之前如何通过访问包名实现朋友圈多图分享,代码如下:

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareToTimeLineUI"));
intent.setAction("android.intent.action.SEND_MULTIPLE");// List存储多张图片地址
ArrayList<Uri> localArrayList = new ArrayList<>();
for (int i = 0, size = localPicsList.size(); i < size; i++) {localArrayList.add(Uri.parse("file:///" + localPicsList.get(i)));
}intent.putParcelableArrayListExtra("android.intent.extra.STREAM", localArrayList);
intent.setType("image/*");
intent.putExtra("Kdescription", desc);
context.startActivity(intent);

这种方式可以直接绕过微信官方 SDK 实现多图分享,无需手动选择图片,唯一的问题就是没有分享结果的回调,也就是说无法判断是否分享成功,这在大部分情况下依然是一种可以接受的方案。

但是如果 targetSDK 大于等于24,那么这项功能就无效了,原因就是 Android N 不允许 file://Uri 的方式在不同的 App 间共享文件,但是如果换成 FileProvider 的方式,经试验发现依然是无效的,所以在 Android N 上无法实现朋友圈直接多图分享。

原文地址: https://zhuanlan.zhihu.com/p/26139355

FileProvider 在 Android N 上的应用相关推荐

  1. android-解决 Android N 上 报错:android.os.FileUriExposedException

    解决 Android N 上 安装Apk时报错:android.os.FileUriExposedException: file:///storage/emulated/0/Download/appN ...

  2. 红橙Darren视频笔记 热更新 bsdiff bspatch 在Android设备上的应用 架构篇1完结篇

    概述 当时红橙的视频讲解就差不多90分钟,但是真正自己做出来热更新的demo还是花了八九个晚上,期间遇到各种各样的问题,什么叫台上一分钟 台下十年功是深有体会了. 本节会涉及一部分NDK的知识 推荐阅 ...

  3. Android头像上传实战模拟

    Android头像上传实战模拟 在开发中头像的上传应该是必不可少的,话不多说上效果图! 点击头像弹出PopupWindow分别三个按钮. <?xml version="1.0" ...

  4. uni-app.04.发布成H5后,uni.chooseImage方法在android WebView上无法使用

    发布成H5后,uni.chooseImage方法在android WebView上无法使用 引言 解决方案 特别注意 引言 经过三个星期的折腾,uni-app的编码阶段宣告结束,正式进入到测试阶段.由 ...

  5. 在Android手机上使用PaddleMobile实现图像分类

    原文博客:Doi技术团队 链接地址:https://blog.doiduoyi.com/authors/1584446358138 初心:记录优秀的Doi技术团队学习经历 前言 现在越来越多的手机要使 ...

  6. 在Android手机上使用MACE实现图像分类

    原文博客:Doi技术团队 链接地址:https://blog.doiduoyi.com/authors/1584446358138 初心:记录优秀的Doi技术团队学习经历 前言 在之前笔者有介绍过&l ...

  7. 奥比中光Gemini 3D双目结构光深度相机在Android平台上深度数据噪点非常多的问题

    相机:Gemini 3D双目结构光深度相机 环境:Android7.1 软件:SDK中的java demo下的depthforopenni2 问题: 在Android样例depthforopenni2 ...

  8. Gemini 3D双目结构光深度相机在Android平台上深度数据噪点非常多的问题

    相机:Gemini 3D双目结构光深度相机 环境:Android7.1 软件:SDK中的java demo下的depthforopenni2 问题: 在Android样例depthforopenni2 ...

  9. 设置android启动器,教程:在任意 Android 设备上安装 HTC 专属桌面启动器

    HTC 手机的标志性 Sense UI 几乎是伴随着 Android 系统成长起来的,BlinkFeed 作为 Sense 5 的新特性进入了人们的视野之中.经历了几代更新,BlinkFeed 受到更 ...

最新文章

  1. 忽悠神经网络指南:教你如何把深度学习模型骗得七荤八素
  2. OpenCV使用VideoWriter和VideoCapture的实例(附完整代码)
  3. Navigation Drawer介绍
  4. 红橙Darren视频笔记 OKHttp基本使用 对http框架进行封装 链式调用
  5. Kaggle新手入门之路(完结)
  6. Codeforces Round #222 (Div. 2): C. Maze(BFS)
  7. 数据库基础 MySQL
  8. eeglab加载显示脑电数据,eeglab简单操作
  9. Win10鼠标单击经常变双击
  10. 路由交换的一些常见知识点总结
  11. 蓝桥杯算法训练——调和数列问题
  12. 2022年中国大学排行榜出炉~
  13. 修改360企业版杀毒软件备注名的方法
  14. 杨过最后达到了独孤求败的什么境界, 其实金庸在书中已说明
  15. 共享虚拟机是什么意思_苹果笔记本电脑MacBook双系统or虚拟机选择指南
  16. python判断字符串合法,详解Python判定IP地址合法性的三种方法 python中判断一个字符串是否是IP地址...
  17. Pycharm安装matplotlib
  18. 木马伪装“刷单任务” 劫持QQ语音暗中盗号
  19. 系统黑科技之流氓软件免疫让电脑像免疫新冠一样免疫流氓软件
  20. Nelder-Mead算法在Matlab中的实现

热门文章

  1. Spring 源码阅读 之 Spring框架加载
  2. 爱上经典之《蜗牛与黄鹂鸟》
  3. tf.nn.in_top_k的用法
  4. 用C语言扩展Python的功能的实例
  5. stringstream用法总结
  6. 【Python】if else 一行写完
  7. [云炬创业基础笔记]第二章创业者测试2
  8. [云炬创业基础笔记]第六章商业模式测试24
  9. [云炬python3玩转机器学习]5-2最小二乘法a和b推导
  10. 年前整理的Css规范