目前在項目中遇到Android 7.0及以上系統調用相機拍照時出現崩潰的情況丽涩,分析后發(fā)現是7.0系統的適配問題引起的藐不,下面將收集到的7.0適配的相關資料整理以備忘。
Android 7.0對于文件共享權限做了進一步的限制,比如我們在調用系統相機拍照的時候經常會這樣寫:
void takePhoto(String cameraPhotoPath) {
File cameraPhoto = new File(cameraPhotoPath);
Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(cameraPhoto));
startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO);
}
這在一般情況下超全,運行沒有問題,但是當把targetSdkVersion指定成24及之上并且在api>=24的設備上運行時邓馒,就會拋出異常:
android.os.FileUriExposedException:
file:///storage/emulated/0/DCIM/IMG_20170125_144112.jpg exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8909)
7.0系統上Android不再允許app中把file://uri暴露給其他app嘶朱,包括但不局限于通過Intent或ClipData等方法。原因在于使用file://Uri會有一些風險光酣,比如:
- 文件是私有的疏遏,接收file://Uri的app無法訪問該文件
- 在Android6.0之后引入運行時權限,如果接收file://Uri的app沒有申請READ_EXTERNAL_STORAGE權限救军,在讀取文件時會引發(fā)崩潰
解決辦法如下:
首先在AndroidManifest.xml中添加provider
<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/provider_paths" />
</provider>
res/xml/provider_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>
修改調用相機拍照的代碼如下:
void takePhoto(String cameraPhotoPath) {
File cameraPhoto = new File(cameraPhotoPath);
Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri photoUri;
if (Build.VERSION.SDK_INT >= 24) {
photoUri = FileProvider.getUriForFile(
this,
getPackageName() + ".provider",
cameraPhoto);
} else {
photoUri = Uri.from(cameraPhoto);
}
takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO);
}
下面來具體講講這個FileProvider
使用content://Uri的優(yōu)點:
- 它可以控制共享文件的讀寫權限财异,只要調用Intent.setFlags()就可以設置對方app對共享文件的訪問權限,并且該權限在對方app退出后自動失效唱遭。相比之下戳寸,使用file://Uri時只能通過修改文件系統的權限來實現訪問控制,這樣的話訪問控制對所有app都生效的拷泽,不能區(qū)分app
- 它可以隱藏共享文件的真實路徑
定義FileProvider
FileProvider會隱藏共享文件的真實路徑疫鹊,將它轉換成content://Uri路徑袖瞻,因此,我們還需要設定轉換的規(guī)則拆吆。android:resource="@xml/provider_paths"這個屬性指定了規(guī)則所在的文件
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path path="." name="root" />
<files-path path="images/" name="my_images" />
<files-path path="audios/" name="my_audios" />
...
</paths>
<paths>中可以定義以下子節(jié)點
子節(jié)點 | 對應路徑 |
---|---|
files-path | Context.getFilesDir() |
cache-path | Context.getCacheDir() |
external-path | Environment.getExternalStorageDirectory() |
external-files-path | Context.getExternalFilesDir(null) |
external-cache-path | Context.getExternalCacheDir() |
file://到content://的轉換規(guī)則:
- 替換前綴:把file://替換成content://${android:authorities}
- 匹配和替換聋迎。遍歷的子節(jié)點,找到最大能匹配上文件路徑前綴的那個子節(jié)點锈拨,用path的值替換掉文件路徑里所匹配的內容
- 文件路徑剩余的部分保持不變
代碼中生成Content Uri
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "2016/pic.png");
Uri contentUri = getUriForFile(getContext(), getPackageName() + ".fileprovider", newFile);
有兩種設置文件的訪問權限:
- 調用Context.grantUriPermission(package, uri, modeFlags)砌庄。這樣設置的權限只有在手動調用Context.revokeUriPermission(uri, modeFlags)或系統重啟后才會失效
- 調用Intent.setFlags()來設置權限。權限失效的時機:接收Intent的Activity所在的stack銷毀時