关于 FileProvider

FileProvider 实际上是 ContentProvider 的一个子类,它的作用也比较明显了,file://Uri 不给用,那么换个 Uri 为 content:// 来替代。

在官方7.0的以上的系统中,尝试传递 file://uri/ 可能会触发 FileUriExposedException

java 复制代码
private static final int REQUEST_CODE_TAKE_PHOTO = 0x110;
   private String mCurrentPhotoPath;

   public void takePhotoNoCompress(View view) {
       Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
       if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

           String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                   .format(new Date()) + ".png";
           File file = new File(Environment.getExternalStorageDirectory(), filename);
           mCurrentPhotoPath = file.getAbsolutePath();

           takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
           startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
       }
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
           mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
       }
       // else tip?

   }

此时如果我们使用 Android 7.0 或者以上的原生系统,再次运行一下,你会发现应用直接停止运行,抛出了 android.os.FileUriExposedException

Caused by: android.os.FileUriExposedException:

file:///storage/emulated/0/20170601-030254.png exposed beyond app through ClipData.Item.getUri()

at android.os.StrictMode.onFileUriExposed(StrictMode.java:1932)

at android.net.Uri.checkFileUriExposed(Uri.java:2348)

官网解释:

为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

官网解决方案:

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。

打开一个4.4的模拟器,运行上述代码,你会发现又Crash啦,抛出了:

Permission Denial~

Caused by: java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{52b029b8 1670:com.android.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052

at android.os.Parcel.readException(Parcel.java:1465)

at android.os.Parcel.readException(Parcel.java:1419)

at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)

at android.app.ActivityThread.acquireProvider(ActivityThread.java:4399)

因为低版本的系统,仅仅是把这个当成一个普通的 Provider 在使用,而我们没有授权,contentprovider 的 export 设置的也是 false;导致 Permission Denial。那么,我们是否可以将 export 设置为 true 呢?很遗憾是不能的。在 FileProvider 的内部:

java 复制代码
@Override
public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

确定了 exported 必须是 false,grantUriPermissions 必须是 true,所以唯一的办法就是授权了。context 提供了两个方法:

  • grantUriPermission(String toPackage, Uri uri, int modeFlags)
  • revokeUriPermission(Uri uri, int modeFlags);

可以看到 grantUriPermission 需要传递一个包名,就是你给哪个应用授权,但是很多时候,比如分享,我们并不知道最终用户会选择哪个 app,所以我们可以这样:

java 复制代码
List<ResolveInfo> resInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, flag);
}

常见使用场景:

1、自动安装文件

2、调用系统拍照

3、调用系统裁剪

简单总结:

总结下,使用 content:// 替代 file://,主要需要 FileProvider 的支持,而因为 FileProvider 是 ContentProvider 的子类,所以需要在 AndroidManifest.xml 中注册;而又因为需要对真实的 filepath 进行映射,所以需要编写一个 xml 文档,用于描述可使用的文件夹目录,以及通过 name 去映射该文件夹目录。

对于权限,有两种方式:

  • 方式一:Intent.addFlags,该方式主要用于针对 intent.setData,setDataAndType 以及 setClipData 相关方式传递 uri 的
  • 方式二:grantUriPermission 来进行授权
java 复制代码
/**
 * 因为低版本的系统,仅仅是把 FileProvider 当成一个普通的 Provider 在使用,而我们没有授权,
 * FileProvider 的 export 设置的也是 false,导致 Permission Denial,出现 crash。
 * 所以我们可以配合 Intent 使用:通过 setData() 方法向 intent 对象添加我们的 Content URI,
 * 然后使用 setFlags() 或者 addFlags() 方法设置读/写权限。
 * 拥有授予权限的 Content URI 后,便可以通过 startActivity() 或者 setResult() 方法启动其他应用并传递授权过的 Content URI 数据
 */
public static Intent addReadWriteFlagNougat(Intent intent) {
  if (SystemUtil.hasNougat()) {
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }
  return intent;
}

------乐于分享,共同进步,欢迎留言讨论
------Treat Warnings As Errors
------Any comments greatly appreciated
------Talking is cheap, show me the code

相关推荐
2401_857439691 小时前
SpringBoot在线教育平台:设计与实现的深度解析
java·spring boot·后端
总是学不会.1 小时前
SpringBoot项目:前后端打包与部署(使用 Maven)
java·服务器·前端·后端·maven
IT学长编程1 小时前
计算机毕业设计 视频点播系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·视频点播系统
一 乐2 小时前
英语词汇小程序小程序|英语词汇小程序系统|基于java的四六级词汇小程序设计与实现(源码+数据库+文档)
java·数据库·小程序·源码·notepad++·英语词汇
曳渔3 小时前
Java-数据结构-反射、枚举 |ू・ω・` )
java·开发语言·数据结构·算法
laocooon5238578863 小时前
java 模拟多人聊天室,服务器与客户机
java·开发语言
风槐啊3 小时前
六、Java 基础语法(下)
android·java·开发语言
苹果醋33 小时前
毕业设计_基于SpringBoot+vue的社区博客系统【源码+SQL+教程+可运行】41002
java·毕业设计·博客
冬天vs不冷3 小时前
SpringBoot基础(四):bean的多种加载方式
java·spring boot·spring
说书客啊3 小时前
计算机毕业设计 | SpringBoot+vue学生成绩管理系统教务管理系统
java·spring boot·node.js·vue·毕业设计·课程设计·教务管理系统