今天來聊聊Android 7.0 FileUriExposedException異常窖式,以及它的使用方法和使用場景
一 描述
- 問題
對于面向 Android 7.0 的應(yīng)用,Android 框架執(zhí)行的 StrictModeAPI 政策禁止在您的應(yīng)用外部公開 file:// URI动壤。如果一項包含文件 URI 的 intent 離開您的應(yīng)用萝喘,則應(yīng)用出現(xiàn)故障,并出現(xiàn) FileUriExposedException異常 - 解決方案
要在應(yīng)用間共享文件琼懊,您應(yīng)發(fā)送一項 content://URI阁簸,并授予 URI 臨時訪問權(quán)限。進行此授權(quán)的最簡單方式除了將targetSdkVersion改成24以下哼丈,就是使用 FileProvider類
官網(wǎng)對FileProvider描述:
FileProvider是ContentProvider的一個特殊子類启妹,它通過創(chuàng)建內(nèi)容來實現(xiàn)與應(yīng)用程序相關(guān)聯(lián)的文件的安全共享:// Uri用于文件,而不是文件:/// Uri醉旦。
內(nèi)容URI允許您使用臨時訪問權(quán)限來授予讀取和寫入訪問權(quán)限饶米。當您創(chuàng)建包含內(nèi)容URI的Intent時桨啃,為了將內(nèi)容URI發(fā)送到客戶端應(yīng)用程序,還可以調(diào)用Intent.setFlags()來添加權(quán)限咙崎。只要接收活動的堆棧處于活動狀態(tài)优幸,客戶端應(yīng)用程序就可以使用這些權(quán)限。對于要訪問服務(wù)的意圖褪猛,只要服務(wù)正在運行,權(quán)限就可用羹饰。
相比之下伊滋,為了控制對文件的訪問:/// Uri你必須修改底層文件的文件系統(tǒng)權(quán)限。您提供的權(quán)限可用于任何應(yīng)用程序队秩,并在您更改之前保持有效笑旺。這種訪問水平基本上是不安全的。
內(nèi)容URI提供的增加文件訪問安全級別使FileProvider成為Android安全基礎(chǔ)架構(gòu)的關(guān)鍵部分馍资。
二 如何使用FileProvider
我們先看如何使用FileProvider筒主,官網(wǎng)也有詳細說明:https://developer.android.com/reference/android/support/v4/content/FileProvider.html
1. 定義FileProvider
由于FileProvider的默認功能,包括內(nèi)容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”方式命名,增加辨別性建钥,系統(tǒng)唯一
android:exproted 要求必須為false藤韵,為true則會報安全異常
android:grantUriPermissions 是否允許為文件設(shè)置臨時權(quán)限 “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節(jié)點</small>
在paths節(jié)點內(nèi)部支持以下幾個子節(jié)點,分別為:
<root-path/> 代表設(shè)備的根目錄new File("/")
<files-path/> 代表該文件files/的應(yīng)用程序的內(nèi)部存儲區(qū)的子目錄镐依,等同于context.getFilesDir()
<cache-path/> 代表應(yīng)用程序的內(nèi)部存儲區(qū)域的緩存子目錄的文件匹涮,等同于context.getCacheDir()
<external-path/> 代表在外部存儲區(qū)根目錄的文件,等同于Environment.getExternalStorageDirectory()
<external-files-path> 代表應(yīng)用程序的外部存儲區(qū)根目錄的文件槐壳,等同于Context.getExternalFilesDir(String) /Context.getExternalFilesDir(null)
<external-cache-path> 代表應(yīng)用程序的外部緩存區(qū)根目錄的文件然低,等同于Context.getExternalCacheDir()
file_paths用來指定Uri共享和真實路徑的映射關(guān)系,name屬性的值可以自定義宏粤,path屬性的值表示共享的具體位置脚翘,設(shè)置為空,就表示共享整個SD卡绍哎,也可指定對應(yīng)的SDcard下的文件目錄来农,根據(jù)需求自行定義
3. 獲得content uri
使用getUriForFile()將file:// 轉(zhuǎn)換成 content://
Uri fileUri = FileProvider.getUriForFile(this, "包名.fileprovider", file);
4. 臨時讀寫權(quán)限授權(quán)
需要對接收應(yīng)用設(shè)置讀權(quán)限或?qū)憴?quán)限亦或讀寫均設(shè)置:
FLAG_GRANT_READ_URI_PERMISSION:讀權(quán)限
FLAG_GRANT_WRITE_URI_PERMISSION:寫權(quán)限
授權(quán)方式:
- 使用Intent.addFlags或setFlags,該方式授權(quán)的有效期限崇堰,權(quán)限截止于該 App 所處的堆棧被銷毀自動回收(APP銷毀)沃于,主要用于針對intent.setData涩咖,setDataAndType以及setClipData相關(guān)方式傳遞uri
2 使用grantUriPermission(String toPackage, Uri uri, int modeFlags)來進行授權(quán),該方式授權(quán)的有效期限繁莹,從授權(quán)一刻開始檩互,手動調(diào)用 Context.revokeUriPermission() 方法或者設(shè)備重啟才截止
三 使用場景
a. 相機拍照
Android 7.0之前我們這樣拍照,沒有什么問題(忽略6.0權(quán)限問題):
private static final int REQUEST_TAKE_PHOTO = 0X11;
private Uri imageUri ;
private void takePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//判斷是否有相機應(yīng)用
if (takePictureIntent.resolveActivity(getActivity().getPackageManager()) != null) {
//獲取存儲路徑 沒有則創(chuàng)建
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或者以上的原生系統(tǒng)運行咨演,發(fā)現(xiàn)應(yīng)用直接停止運行闸昨,如文章開頭所說拋出了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)
接下來根據(jù)官網(wǎng)的解決辦法,如第二步所說配置好 FileProvider薄风,更改拍照方法:
private void takePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//判斷是否有相機應(yīng)用
if (takePictureIntent.resolveActivity(getActivity().getPackageManager()) != null) {
//獲取存儲路徑 沒有則創(chuàng)建
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);
//添加權(quán)限 這一句表示對目標應(yīng)用臨時授權(quán)該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);
//添加權(quán)限 這一句表示對目標應(yīng)用臨時授權(quán)該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)循诉,但是切記需要進行授權(quán)和取消授權(quán),否則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);
}
大概使用就這么多撇他,望多多指教茄猫。