Android7.0,解决 FileUriExposedException

问题现象

我们在开发 app 时避免不了需要添加应用内升级功能。当 app 启动时,如果检测到最新版本,将 apk 安装包从服务器下载下来,执行安装。

安装apk的代码一般写法如下,网上随处可以搜到
public static void installApk(Context context, File file) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri data = Uri.fromFile(file);
    intent.setDataAndType(data, "application/vnd.android.package-archive");
    context.startActivity(intent);
}
1
public static void installApk(Context context, File file) {
2
    Intent intent = new Intent(Intent.ACTION_VIEW);
3
    Uri data = Uri.fromFile(file);
4
    intent.setDataAndType(data, "application/vnd.android.package-archive");
5
    context.startActivity(intent);
6
}

然而,当我们在Android7.0手机中执行时,会发现报如下错误日志
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.apk exposed beyond app through Intent.getData()
  at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
  at android.net.Uri.checkFileUriExposed(Uri.java:2346)
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
  at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
  at android.app.Activity.startActivityForResult(Activity.java:4224)
  at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
  at android.app.Activity.startActivityForResult(Activity.java:4183)
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
  at android.app.Activity.startActivity(Activity.java:4507)
  at android.app.Activity.startActivity(Activity.java:4475)
 
1
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/net.csdn.blog.ruancoder/cache/test.apk exposed beyond app through Intent.getData()
2
  at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
3
  at android.net.Uri.checkFileUriExposed(Uri.java:2346)
4
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
5
  at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
6
  at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
7
  at android.app.Activity.startActivityForResult(Activity.java:4224)
8
  at android.support.v4.app.BaseFragmentActivityJB.startActivityForResult(BaseFragmentActivityJB.java:50)
9
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:79)
10
  at android.app.Activity.startActivityForResult(Activity.java:4183)
11
  at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:859)
12
  at android.app.Activity.startActivity(Activity.java:4507)
13
  at android.app.Activity.startActivity(Activity.java:4475)

原因

我们来看一下FileUriExposedException官方文档
当应用程序将文件以【file://】形式的Uri公开到另一个应用程序时抛出的异常

不鼓励这种曝光方式,因为接收的app可能无法访问你所共享的路径。例如,接收app可能未请求运行时权限Manifest.permission.READ_EXTERNAL_STORAGE ,或者平台可能跨用户配置文件边界[user profile boundaries]共享Uri。
相反,应用程序应使用【content://】形式的Uris,以便平台可以扩展接收应用程序的临时权限以访问资源。

仅针对 Build.VERSION_CODES.N 或更高版本的应用程序抛出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但强烈建议不要这样做。
x
 
1
当应用程序将文件以【file://】形式的Uri公开到另一个应用程序时抛出的异常
2
3
不鼓励这种曝光方式,因为接收的app可能无法访问你所共享的路径。例如,接收app可能未请求运行时权限Manifest.permission.READ_EXTERNAL_STORAGE ,或者平台可能跨用户配置文件边界[user profile boundaries]共享Uri。
4
相反,应用程序应使用【content://】形式的Uris,以便平台可以扩展接收应用程序的临时权限以访问资源。
5
6
仅针对 Build.VERSION_CODES.N 或更高版本的应用程序抛出此操作。 早期SDK版本的app可以以【file://】形式的Uri共享文件,但强烈建议不要这样做。

FileProvider官方文档:


解决方案

1、声明FileProvider
首先在清单文件中声明FileProvider。
<manifest>
    <application>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>
1
<manifest>
2
    <application>
3
        <provider
4
            android:name="android.support.v4.content.FileProvider"
5
            android:authorities="${applicationId}.fileprovider"
6
            android:exported="false"
7
            android:grantUriPermissions="true">
8
            <meta-data
9
                android:name="android.support.FILE_PROVIDER_PATHS"
10
                android:resource="@xml/file_paths" />
11
        </provider>
12
    </application>
13
</manifest>

其中
  • android:name 是固定写法。
  • android:authorities 可自定义,是用来标识该 provider 的唯一标识,建议结合包名来保证 authority 的唯一性。
  • android:exported 必须设置成 false,否则运行时会报错 java.lang.SecurityException: Provider must not be exported 。
  • android:grantUriPermissions 用来控制共享文件的访问权限。

<meta-data>节点中的 android:resource 指定了共享文件的路径,此处的 file_paths 即是该 Provider 对外提供文件的目录的配置文件,存放在 res/xml/ 下。

2、添加 file_paths.xml 文件
文件格式如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <root-path
            name="root"
            path=""/>
        <files-path
            name="files"
            path=""/>
        <cache-path
            name="cache"
            path=""/>
        <external-path
            name="external"
            path=""/>
        <external-files-path
            name="external_file_path"
            path=""/>
        <external-cache-path
            name="external_cache_path"
            path=""/>
    </paths>
</resources>
26
 
1
<?xml version="1.0" encoding="utf-8"?>
2
<resources>
3
    <paths>
4
        <root-path
5
            name="root"
6
            path=""/>
7
        <files-path
8
            name="files"
9
            path=""/>
10
        <cache-path
11
            name="cache"
12
            path=""/>
13
        <external-path
14
            name="external"
15
            path=""/>
16
        <external-files-path
17
            name="external_file_path"
18
            path=""/>
19
        <external-cache-path
20
            name="external_cache_path"
21
            path=""/>
22
    </paths>
23
</resources>

其中根元素<paths>是固定的,内部元素可以是以下节点:
  • files-path  对应 getFilesDir()
  • cache-path 对应getCacheDir()
  • external-path 对应Environment.getExternalStorageDirectory()
  • external-files-path 对应getExternalFilesDir()
  • external-cache-path 对应getExternalCacheDir()

比如,我们将下载的apk文件存放到 sdcard 中的 Android/data/<package>/cache/download 中,file_paths.xml 文件如下:
<external-cache-path name="cache_download" path="download"/>
x
 
1
<external-cache-path name="cache_download" path="download"/>
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
x
1
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);

在Java代码中使用FileProvider

案例1:安装指定路径路径的apk

public static void installApk(Context context, File file) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri data;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //判断版本大于等于7.0
        data = FileProvider.getUriForFile(context, context.getPackageName( )+ ".fileprovider", file);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //给目标应用一个临时授权
    } else {
        data = Uri.fromFile(file);
    }
    intent.setDataAndType(data, "application/vnd.android.package-archive");
    context.startActivity(intent);
}
1
public static void installApk(Context context, File file) {
2
    Intent intent = new Intent(Intent.ACTION_VIEW);
3
    Uri data;
4
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //判断版本大于等于7.0
5
        data = FileProvider.getUriForFile(context, context.getPackageName( )+ ".fileprovider", file);
6
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //给目标应用一个临时授权
7
    } else {
8
        data = Uri.fromFile(file);
9
    }
10
    intent.setDataAndType(data, "application/vnd.android.package-archive");
11
    context.startActivity(intent);
12
}

案例2:获取拍照后指定路径的文件

去拍照并指定保存文件的位置:
private void showCamera(File file) {
	Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
	Uri uri;
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
		uri = FileProvider.getUriForFile(mContext, mContext.getPackageName()+".fileprovider", file);
	} else {
		uri = Uri.fromFile(file);
	}
	intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
	startActivityForResult(intent, REQUEST_CAMERA);
}
x
1
private void showCamera(File file) {
2
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
3
    Uri uri;
4
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
5
        uri = FileProvider.getUriForFile(mContext, mContext.getPackageName()+".fileprovider", file);
6
    } else {
7
        uri = Uri.fromFile(file);
8
    }
9
    intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
10
    startActivityForResult(intent, REQUEST_CAMERA);
11
}
处理拍照结果:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
	super.onActivityResult(requestCode, resultCode, data);
	if(requestCode == REQUEST_CAMERA && resultCode == Activity.RESULT_OK) {
		if (mTmpFile != null) {
			//文件还是你 showCamera 时传过去的文件
		}
	}
}
x
 
1
@Override
2
public void onActivityResult(int requestCode, int resultCode, Intent data) {
3
    super.onActivityResult(requestCode, resultCode, data);
4
    if(requestCode == REQUEST_CAMERA && resultCode == Activity.RESULT_OK) {
5
        if (mTmpFile != null) {
6
            //文件还是你 showCamera 时传过去的文件
7
        }
8
    }
9
}
2018-8-28

猜你喜欢

转载自www.cnblogs.com/baiqiantao/p/9548146.html