Android 7.0 应用之间的文件共享

For apps targeting Android 7.0, the Android framework enforces the StrictMode API policy that prohibits exposing file:// URIs outside your app. If an intent containing a file URI leaves your app, the app fails with a FileUriExposedException exception.To share files between applications, you should send a content:// URI and grant a temporary access permission on the URI. The easiest way to grant this permission is by using the FileProvider class. For more information on permissions and sharing files, see Sharing Files.

以上是官方文档给出的介绍,大致意思也就是: 对于面向 Android N 的应用,Android 框架执行的 StrictMode API 政策禁止向您的应用外公开 file:// URI。 如果一项包含文件 URI 的 Intent 离开您的应用,应用失败,并出现 FileUriExposedException 异常。

若要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。 进行此授权的最简单方式是使用 FileProvider 类。 如需有关权限和共享文件的更多信息,请参阅共享文件

1
2
3
4
5
6
7
FileProvider
public class FileProvider
extends ContentProvider

java.lang.Object
android.content.ContentProvider
android.support.v4.content.FileProvider

示例

我们先来看看以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.xw.fileproviderdemo;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private static final String TAG = "MainActivity";
private android.widget.Button btntakepic;
private android.widget.ImageView imgshow;

private static final int REQUEST_CODE_TAKE_PHOTO = 0;//拍照请求码
private static final int REQUEST_CODE_CLIP_PHOTO = 1;//裁剪请求码

private File mOutputFile;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.imgshow = (ImageView) findViewById(R.id.img_show);
this.btntakepic = (Button) findViewById(R.id.btn_take_pic);
this.btntakepic.setOnClickListener(this);
}

@Override
public void onClick(View v) {
Log.d(TAG, "=====onClick=====");
switch (v.getId()) {
case R.id.btn_take_pic:
takePhoto();
break;
default:
break;
}
}

/***
* 调用相机拍照
*/
private void takePhoto() {
Log.d(TAG, "=====takePhoto: =====");
/*+++++++针对6.0及其以上系统,读写外置存储权限的检测+++++++++*/
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String sdPath = Environment.getExternalStorageDirectory()
.getAbsolutePath();
mOutputFile = new File(sdPath, System.currentTimeMillis() + ".jpg");//拍照之后照片的路径
try {
if (!mOutputFile.exists()) {
mOutputFile.createNewFile();
}
} catch (Exception e) {
e.printStackTrace();
}
Uri uri = Uri.fromFile(mOutputFile);//指定保存拍照后文件的Uri
Log.i(TAG, "takePhoto: uri:===" + uri);
Intent newIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
newIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);//将拍取的照片保存到指定Uri
startActivityForResult(newIntent, REQUEST_CODE_TAKE_PHOTO);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "=====onActivityResult=====");
if (requestCode == REQUEST_CODE_TAKE_PHOTO) {
onTakePhotoFinished(resultCode, data);
} else if (requestCode == REQUEST_CODE_CLIP_PHOTO) {
onClipPhotoFinished(resultCode, data);
}
}

/**
* 拍照完成
*
* @param resultCode
* @param data
*/
private void onTakePhotoFinished(int resultCode, Intent data) {
Log.d(TAG, "=====onTakePhotoFinished=====");
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, "take photo canceled", Toast.LENGTH_SHORT)
.show();
return;
} else if (resultCode != RESULT_OK) {
Toast.makeText(this, "take photo failed", Toast.LENGTH_SHORT)
.show();
} else {
/*调用裁剪图片的方法进行裁剪图片*/
clipPhoto(Uri.fromFile(mOutputFile));
}
}

/**
* 裁剪照片
*
* @param uri
*/
private void clipPhoto(Uri uri) {
Log.d(TAG, "clipPhoto====>" + uri);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
// 下面这个crop=true是设置在开启的Intent中设置显示的VIEW可裁剪
intent.putExtra("crop", "true");
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE_CLIP_PHOTO);
}

/**
* 裁剪照片完成
*
* @param resultCode
* @param data
*/
private void onClipPhotoFinished(int resultCode, Intent data) {
Log.d(TAG, "=====onClipPhotoFinished=====");
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, "clip photo canceled", Toast.LENGTH_SHORT)
.show();
return;
} else if (resultCode != RESULT_OK) {
Toast.makeText(this, "take photo failed", Toast.LENGTH_SHORT)
.show();
}
Bitmap bm = BitmapFactory.decodeFile(mOutputFile.getAbsolutePath());
imgshow.setImageBitmap(bm);
}
}

以上示代码通过调用系统的照相机拍照,保存图片到sdcard,并裁剪显示到界面上。
我们通过这个示例为代表,来说明本节的内容。

复现问题

当然在Android 7.0以下,你依然可以使用上面的代码来实现拍照裁剪显示。
但是当你使用的设备是Android 7.0及其以上的时候,使用以上代码,就会抛出开篇所提到的FileUriExposedException异常

android.os.FileUriExposedException:file://…. exposed beyond app through ClipData.Item.getUri()

android_n_take_photo_error.png

android_n_take_photo_error_dialog

解决方案

定义FileProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xw.fileproviderdemo">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.xw.fileproviderdemo.takePhoto.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>

注:

  • exported:必须为false,为true会报安全异常

android_n_take_photo_export_error.png

  • grantUriPermissions:为true 表示授予该URI临时访问权限

验证可用的文件

在res/xml资源目录下创建指定的xml文件

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="images" path="."/>
</paths>

以上代码表示的是:可以访问外部存储目录更目录下的文件,因为拍照后的图片是保存在Environment.getExternalStorageDirectory()下的,
也就是,故path的值这里用.号来代表当前的路径,name的值可以自定义。

注: 节点必须包含以下一个或者多个子节点:

  • files-path 代表:Context.getFilesDir().
  • cache-path 代表:getCacheDir().
  • extenal-path 代表:Environment.getExternalStorageDirectory().
  • external-files-path 代表:Context#getExternalFilesDir(String) Context.getExternalFilesDir(null).
  • external-cache-path 代表:Context.getExternalCacheDir().

给Files生成Content URI

1
2
3
4
5
6
7
8
9
10
11
private void takePhoto() {
....
//Uri uri = Uri.fromFile(mOutputFile);//指定保存拍照后文件的Uri
/*将file uri的获取方式由fromfile改变为由FileProvider.getUriForFile获取其中Authority应与AndroidManifest定义的保持一致*/
Uri uri= FileProvider.getUriForFile(this, "com.xw.fileproviderdemo.takePhoto.provider", mOutputFile);
Log.i(TAG, "takePhoto: uri:===" + uri);
Intent newIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照
newIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);//将拍取的照片保存到指定Uri
startActivityForResult(newIntent, REQUEST_CODE_TAKE_PHOTO);
....
}

以上的代码,将getUriForFile()返回的uri打印出来是:
content://com.xw.fileproviderdemo.takePhoto.provider/image/xxxx.jpg

由此可以看出:
image:就是xml中的子节点里定义的android:name的值
com.xw.fileproviderdemo.takePhoto.provider/image对应的路径就是/storage/emulated/0
那么com.xw.fileproviderdemo.takePhoto.provider/image/xxxx.jpg对应的路径就是/storage/emulated/0/xxxx.jpg

通过以上的代码修改,可以解决takePhoto()方法中拍照时的FileUriExposedException异常
再次运行项目,可以进行拍照了,拍照完成后,接下来需要进行裁剪,BOOM……Crash…Stop !!!,项目又崩溃了,然后抛出以下的异常:

android_n_clip_pic_error.png

android_n_take_photo_error_dialog

给URI临时授权

同理,抛出的异常和上面takePhoto()一样,都是FileUriExposedException异常

将接收该uri的目的App的PackageName通过grantUriPermission()函数进行设置,授予读写权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void clipPhoto(Uri uri) {
Log.d(TAG, "clipPhoto====>" + uri);
this.grantUriPermission("com.android.camera", uri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//请求URI授权读取
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//请求URI授权写入
intent.setDataAndType(uri, "image/*");
// 下面这个crop=true是设置在开启的Intent中设置显示的VIEW可裁剪
intent.putExtra("crop", "true");
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, REQUEST_CODE_CLIP_PHOTO);
}

同样,调用的时候也将file uri的获取方式由fromfile改变为由FileProvider.getUriForFile获取:

1
2
3
/*调用裁剪图片的方法进行裁剪图片*/
//clipPhoto(Uri.fromFile(mOutputFile));
clipPhoto(FileProvider.getUriForFile(this, "com.xw.fileproviderdemo.takePhoto.provider", mOutputFile));

最后,再次运行,所有Exception都解决了,Perfect ~ !

效果如下:

android_n_take_clip_success.png

  • 以上就是简单的对Android 7.0上面权限改变之文件共享方面的概括总结,如有不足,欢迎指正,谢谢
-------------本文结束感谢您的阅读-------------
if (本文对您有用) { Pay (请随意¥打赏) } else { Commit feedback (底部评论区提交建议、反馈) } 感谢支持!
0%