今天來聊聊Android 7.0 FileUriExposedException異常,以及它的使用方法和使用場景
一 描述
- 問題
對于面向 Android 7.0 的應用,Android 框架執行的 StrictModeAPI 政策禁止在您的應用外部公開 file:// URI。如果一項包含文件 URI 的 intent 離開您的應用,則應用出現故障,并出現 FileUriExposedException異常 - 解決方案
要在應用間共享文件,您應發送一項 content://URI,并授予 URI 臨時訪問權限。進行此授權的最簡單方式除了將targetSdkVersion改成24以下,就是使用 FileProvider類
官網對FileProvider描述:
FileProvider是ContentProvider的一個特殊子類,它通過創建內容來實現與應用程序相關聯的文件的安全共享:// Uri用于文件,而不是文件:/// Uri。
內容URI允許您使用臨時訪問權限來授予讀取和寫入訪問權限。當您創建包含內容URI的Intent時,為了將內容URI發送到客戶端應用程序,還可以調用Intent.setFlags()來添加權限。只要接收活動的堆棧處于活動狀態,客戶端應用程序就可以使用這些權限。對于要訪問服務的意圖,只要服務正在運行,權限就可用。
相比之下,為了控制對文件的訪問:/// Uri你必須修改底層文件的文件系統權限。您提供的權限可用于任何應用程序,并在您更改之前保持有效。這種訪問水平基本上是不安全的。
內容URI提供的增加文件訪問安全級別使FileProvider成為Android安全基礎架構的關鍵部分。
二 如何使用FileProvider
我們先看如何使用FileProvider,官網也有詳細說明:https://developer.android.com/reference/android/support/v4/content/FileProvider.html
1. 定義FileProvider
由于FileProvider的默認功能,包括內容URI代的文件,你不需要在代碼中定義一個子類。我們在manifest中聲明provider
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="包名.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
...
</application>
</manifest>
android:name 【固定值】 FileProvider的包名+類名
android:authorities 【自定義】 推薦以包名+”.fileprovider”方式命名,增加辨別性,系統唯一
android:exproted 要求必須為false,為true則會報安全異常
android:grantUriPermissions 是否允許為文件設置臨時權限 “true”
android:resource="@xml/file_paths"就是我們的共享路徑配置的xml文件
2 . 配置file_paths
FileProvider只能生成你事先指定的 content URI,file_paths配置如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external"
path=""/>
<external-path
name="my_images"
path="Android/data/包名/files/Pictures/"/>
<external-path
name="images"
path="Pictures/"/>
</paths>
<small>注意: 注: XML文件是你可以指定你要共享的目錄的唯一途徑,你不能以編程方式添加一個目錄,至少配置一個external-path節點</small>
在paths節點內部支持以下幾個子節點,分別為:
<root-path/> 代表設備的根目錄new File("/")
<files-path/> 代表該文件files/的應用程序的內部存儲區的子目錄,等同于context.getFilesDir()
<cache-path/> 代表應用程序的內部存儲區域的緩存子目錄的文件,等同于context.getCacheDir()
<external-path/> 代表在外部存儲區根目錄的文件,等同于Environment.getExternalStorageDirectory()
<external-files-path> 代表應用程序的外部存儲區根目錄的文件,等同于Context.getExternalFilesDir(String) /Context.getExternalFilesDir(null)
<external-cache-path> 代表應用程序的外部緩存區根目錄的文件,等同于Context.getExternalCacheDir()
file_paths用來指定Uri共享和真實路徑的映射關系,name屬性的值可以自定義,path屬性的值表示共享的具體位置,設置為空,就表示共享整個SD卡,也可指定對應的SDcard下的文件目錄,根據需求自行定義
3. 獲得content uri
使用getUriForFile()將file:// 轉換成 content://
Uri fileUri = FileProvider.getUriForFile(this, "包名.fileprovider", file);
4. 臨時讀寫權限授權
需要對接收應用設置讀權限或寫權限亦或讀寫均設置:
FLAG_GRANT_READ_URI_PERMISSION:讀權限
FLAG_GRANT_WRITE_URI_PERMISSION:寫權限
授權方式:
- 使用Intent.addFlags或setFlags,該方式授權的有效期限,權限截止于該 App 所處的堆棧被銷毀自動回收(APP銷毀),主要用于針對intent.setData,setDataAndType以及setClipData相關方式傳遞uri
2 使用grantUriPermission(String toPackage, Uri uri, int modeFlags)來進行授權,該方式授權的有效期限,從授權一刻開始,手動調用 Context.revokeUriPermission() 方法或者設備重啟才截止
三 使用場景
a. 相機拍照
Android 7.0之前我們這樣拍照,沒有什么問題(忽略6.0權限問題):
private static final int REQUEST_TAKE_PHOTO = 0X11;
private Uri imageUri ;
private void takePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//判斷是否有相機應用
if (takePictureIntent.resolveActivity(getActivity().getPackageManager()) != null) {
//獲取存儲路徑 沒有則創建
File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (!directory.exists()) {
if (!directory.mkdir()) {
return;
}
}
File file = new File(directory.getAbsolutePath(), new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".jpeg");
imageUri = Uri.fromFile(file);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(takePictureIntent, TAKE_PHOTO);
} else {
ToastUtil.showShort(getString(R.string.TakePhoto_Error));
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_TAKE_PHOTO) {
// 通知圖庫更新
getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, imageUri ));
}
}
如果我們使用Android 7.0或者以上的原生系統運行,發現應用直接停止運行,如文章開頭所說拋出了android.os.FileUriExposedException:
android.os.FileUriExposedException:
file:///storage/emulated/0/Pictures/20170723-201847.jpeg exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
接下來根據官網的解決辦法,如第二步所說配置好 FileProvider,更改拍照方法:
private void takePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//判斷是否有相機應用
if (takePictureIntent.resolveActivity(getActivity().getPackageManager()) != null) {
//獲取存儲路徑 沒有則創建
File directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
if (!directory.exists()) {
if (!directory.mkdir()) {
return;
}
}
File file = new File(directory.getAbsolutePath(), new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".jpeg");
Uri uri = imageUri = Uri.fromFile(file);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//兼容7.0
uri = FileProvider.getUriForFile(getApplication(), "包名.fileprovider", file);
//添加權限 這一句表示對目標應用臨時授權該Uri所代表的文件
takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(takePictureIntent, TAKE_PHOTO);
} else {
ToastUtil.showShort(getString(R.string.TakePhoto_Error));
}
}
添加了版本判斷,并使用 FileProvider.getUriForFile()獲得content Uri,方法主要更改如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//兼容7.0
uri = FileProvider.getUriForFile(getApplication(), "包名.fileprovider", file);
//添加權限 這一句表示對目標應用臨時授權該Uri所代表的文件
takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
當然也可以不用判斷版本,直接使用FileProvider.getUriForFile(getApplication(), "包名.fileprovider", file)獲得Uri替換Uri.fromFile(file),但是切記需要進行授權和取消授權,否則4.4以下會報Permission Denial
b. 圖片裁剪
/**
* @param activity 當前activity
* @param orgUri 剪裁原圖的Uri
* @param desUri 剪裁后的圖片的Uri
* @param aspectX X方向的比例
* @param aspectY Y方向的比例
* @param width 剪裁圖片的寬度
* @param height 剪裁圖片高度
* @param requestCode 剪裁圖片的請求碼
*/
public static void cropImageUri(Activity activity, Uri orgUri, Uri desUri, int aspectX, int aspectY, int width, int height, int requestCode) {
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(orgUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", aspectX);
intent.putExtra("aspectY", aspectY);
intent.putExtra("outputX", width);
intent.putExtra("outputY", height);
intent.putExtra("scale", true);
//將剪切的圖片保存到目標Uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
intent.putExtra("return-data", false);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true);
activity.startActivityForResult(intent, requestCode);
}
c. 安裝apk
// 安裝Apk
public void installApk(Context context) {
File file = new File(Environment.getExternalStorageDirectory(), "app.apk");
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.fromFile(file);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, "包名.fileprovider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
大概使用就這么多,望多多指教。