詳解Android7.0及以上版本拍照或者相冊選取照片包括裁剪照片時時App崩潰問題

類似問題的在網上已經有大把大把的文章,但是就我碰到的問題而言並沒有查看了哪篇文章能夠一次性解決我碰到的所有問題,所以在查看了諸多文章我的問題得以解決後,我總結一下我的解決過程,希望對有和我類似問問題的開發者們有幫助。

在手機系統升級到Android7.0後發現在頭像上傳功能出現了上述問題。而崩潰的原因就是:android.os.FileUriExposedException。
官方文檔對該錯誤的解釋,是由於出於安全考慮,Google是反對放寬私有目錄的訪問權限的,所以收起對私有文件的訪問權限是Android將來發展的趨勢。
Android7.0中嘗試傳遞 file:// URI 會觸發 FileUriExposedException,因爲在Android7.0之後Google認爲直接使用本地的根目錄即file:// URI是不安全的操作,直接訪問會拋出FileUriExposedExCeption異常,這就意味着在Android7.0以前我們訪問相機拍照存儲時,如果使用URI的方式直接存儲剪裁圖片就會造成這個異常。

下面來說解決方法:
Google爲我們提供了FileProvider類,進行一種特殊的內容提供,FileProvider時ContentProvide的子類,它使用了和內容提供器類似的機制來對數據進行保護,可以選擇性地將封裝過的Uri共享給外部,從而提高了應用的安全性。下面就讓我們看一下如何使用這個內容提供者進行數據訪問的:
1.使用FileProvider獲取Uri就會將以前的file:// URI準換成content:// URI,實現一種安全的應用間數據訪問,內容提供者作爲Android的四大組件之一,使用同樣需要在清單文件AndroidManifest.xml中進行註冊的,註冊方法如下:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.huanlego.app.provider"//這裏的值大家自己填寫即可後面會用到
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>

2.在res文件夾下面新建一個xml文件夾,然後在xml文件夾下面建立一個file_paths的xml文件作爲provider的共享文件路徑
<?xml version="1.0"encoding="utf-8"?>
<paths>
<external-pathname="camera_photos"path=""/>
<files-pathname="photos"path=""/>
</paths>
</resources>

其中name:一個引用字符串,意思就是可以隨便寫。 
path:文件夾“相對路徑”,完整路徑取決於當前的標籤類型。 
path可以爲空,表示指定目錄下的所有文件、文件夾都可以被共享。 
在這個文件中,爲每個目錄添加一個XML元素指定目錄。paths 可以添加多個子路徑:< files-path> 分享app內部的存儲;< external-path> 分享外部的存儲;< cache-path> 分享內部緩存目錄。


3.在源代碼的相關地方進行判斷

拍照:
caseR.id.bt_camera:
IntentintentFromCapture = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 判斷存儲卡是否可以用,可用進行存儲
Stringstate = Environment.getExternalStorageState();
if(state.equals(Environment.MEDIA_MOUNTED)) {
Filepath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
Filefile = new File(path,IMAGE_FILE_NAME);

if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.N) {
//下面這行代碼第二個參數要用到的就是清單文件裏剛纔我們自己填寫的authorities值
UriapkUri = FileProvider.getUriForFile(mContext,"com.huanlego.app.provider",file);
intentFromCapture.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT, apkUri);
}else{
intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT,Uri.fromFile(file));
}
}
startActivityForResult(intentFromCapture,CAMERA_REQUEST_CODE);
break;

相冊選取:我這裏使用的是第三方的PhotoPicker

caseR.id.bt_photo:
PhotoPicker.builder()
.setPhotoCount(1)
.setShowCamera(false)
.setShowGif(true)
.setPreviewEnabled(false)
.setGridColumnCount(3)
.start(HeaderCropPictureActivity.this,PhotoPicker.REQUEST_CODE);
break;

照片選取和拍攝後的回調:
/**
* 拍照選取
*/
caseCAMERA_REQUEST_CODE:
// 判斷存儲卡是否可以用,可用進行存儲
Stringstate = Environment.getExternalStorageState();
if(state.equals(Environment.MEDIA_MOUNTED)) {
Filepath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
FiletempFile = new File(path,IMAGE_FILE_NAME);
startPhotoZoom(tempFile);
} else {
ToastUtils.getInstance(getApplicationContext()).showToastShort("未找到存儲卡,無法存儲照片!");
}
break;

/**
* 相冊選取
*/
casePhotoPicker.REQUEST_CODE:
if(data!= null) {
ArrayList<String> photos =data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS);
Stringuri = photos.get(0);
if(!uri.contains("file://")) {
uri = "file://" + uri;
}
startPhotoZoom(newFile(uri));
}
break;

下面是進行的圖片裁剪
/**
* 裁剪圖片方法實現
*/
public voidstartPhotoZoom(Filefile) {
Intentintent = new Intent("com.android.camera.action.CROP");
// intent.setDataAndType(uri, "image/*");
// 設置裁剪
intent.putExtra("crop","true");
intent.putExtra("scale",true);
intent.putExtra("return-data",false);
intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
intent.putExtra("outputFormat",Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection",true);
intent.putExtra("outputX",600);
intent.putExtra("outputY",600);

if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.N) {
UriapkUri = MyFileProvider.getUriForFile(mContext,"com.huanlego.app.provider",file);
intent.setDataAndType(apkUri,"image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}else{
intent.setDataAndType(Uri.fromFile(file),"image/*");
}

startActivityForResult(intent,RESULT_REQUEST_CODE);
}

網上蒐集到的資料大多是這個流程下來的,而我按照這個流程執行的時候又出現了其他的問題

這個問題證明我的清單文件出現了問題,那麼ok我們隊清單文件的改動只是做了provider的增加,那麼問題的根源肯定是在這裏應該沒錯了。
然後繼續查詢,問題找到了


根據這位博主的文章原因及解決如下
我們項目中可能會用到其他一些第三方sdk有用到拍照功能的話,他也爲了適配android7.0也添加了這個節點,此時有些人可能就不知道如何下手了,其實很簡單我們只要重寫一個類 繼承自FileProvider,然後就按上述方法在添加一個節點就可以了;
很簡單,建一個自己的FileProvider就好了。
**
* Created by wzITger on 2017/11/21.
*/

public classMyFileProviderextendsFileProvider{
}

下面就是小小的改動:
<provider
android:name="com.huanlego.app.service.MyFileProvider"
android:authorities="com.huanlego.app.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>

這樣下來似乎就萬事大吉了,但是是這樣麼?不知道別人怎麼樣,反正我是沒有,在拍照截圖和從相冊選舉一些截圖文件或者是一些網絡圖片時一切是正常的
但是接下來,在我從相冊中選取拍攝好的照片時就又出現了新的問題:

好吧,很是無語,然後繼續找尋完美的那一刻:

FileProvider的坑(Failed to find configured root that contains)

FileProvider所支持的幾種path類型

從Android官方文檔上可以看出FileProvider提供以下幾種path類型:
<files-path path="" name="camera_photos" />
該方式提供在應用的內部存儲區的文件/子目錄的文件。它對應Context.getFilesDir返回的路徑:eg:"/data/data/com.jph.simple/files"。
<cache-path name="name" path="path" />
該方式提供在應用的內部存儲區的緩存子目錄的文件。它對應getCacheDir返回的路徑:eg:“/data/data/com.jph.simple/cache”;
<external-path name="name" path="path" />
該方式提供在外部存儲區域根目錄下的文件。它對應Environment.getExternalStorageDirectory返回的路徑:eg:"/storage/emulated/0";
<external-files-path name="name" path="path" />
該方式提供在應用的外部存儲區根目錄的下的文件。它對應Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路徑。eg:"/storage/emulated/0/Android/data/com.jph.simple/files"。
<external-cache-path name="name" path="path" />
該方式提供在應用的外部緩存區根目錄的文件。它對應Context.getExternalCacheDir()返回的路徑。eg:"/storage/emulated/0/Android/data/com.jph.simple/cache"。
以上便是Android官方文檔上介紹的FileProvider所有支持的所以path類型,這些類型在Android手機內部存儲區文件共享是可以行的通的,但對於外置SD卡是不行的,如果你想通過FileProvider.getUriForFile()獲取一個外置SD卡的Uri則會報出如下異常:
這個異常就是和我碰到的問題一樣的。那麼原因這下我們也應該徹底明瞭了,我的手機照片時默認存儲在SD卡中的嘛,三星手機可以擴展內存的哦。

接下來就是

FileProvider獲取對外置SD卡的支持。(裏面的一些細節我就不說了,想了解的可以看底下的參考來源)

然後我們就對file_paths.xml進行改動。
將FileProvider的path設置成root-path,看能否支持外置SD卡:

<?xml version="1.0"encoding="utf-8"?>
<paths>
<external-pathpath=""name="camera_photos"/>
<files-pathpath=""name="photos"/>
<root-pathname="name"path=""/>
</paths>
</resources>

就是高亮的這一行,哦了,接下來我們再來跑一跑我們的程序,ok,我的項目這下是沒有任何問題了,小磕小絆都過來了,希望這篇沒有太大技術含量只是總結他人結合己身的躺坑文章能幫助大家快速解決這樣不是問題的問題,謝謝。


以下是參考來源:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章