1. 程式人生 > >Android7.0上拍照與選擇照片Crash問題

Android7.0上拍照與選擇照片Crash問題

在Android7.0系統上,Android 框架強制執行了 StrictMode API 政策禁止向你的應用外公開 file:// URI。 如果一項包含檔案 file:// URI型別 的 Intent 離開你的應用,應用失敗,並出現 FileUriExposedException 異常,如呼叫系統相機拍照,或裁切照片

應對策略:

若要在應用間共享檔案,可以傳送 content:// URI型別的Uri,並授予 URI 臨時訪問許可權。 進行此授權的最簡單方式是使用 FileProvider類。 如需有關許可權和共享檔案的更多資訊,請參閱共享檔案。
在Android7.0上呼叫系統相機拍照,裁切照片

一、呼叫系統相機拍照

在Android7.0之前,如果你想呼叫系統相機拍照可以通過以下程式碼來進行:

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists())file.getParentFile().mkdirs();
Uri imageUri = Uri.fromFile(file);
Intent intent = new Intent();
intent.setAction
(MediaStore.ACTION_IMAGE_CAPTURE);//設定Action為拍照 intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片儲存到指定URI startActivityForResult(intent,1006);

而在Android7.0上使用上述方式呼叫系統相拍照則會丟擲如下異常:

android.os.FileUriExposedException: file:////storage/emulated/0/temp/1474956193735.jpg exposed beyond app through Intent.getData
() at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799) at android.net.Uri.checkFileUriExposed(Uri.java:2346) at android.content.Intent.prepareToLeaveProcess(Intent.java:8933) at android.content.Intent.prepareToLeaveProcess(Intent.java:8894) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517) at android.app.Activity.startActivityForResult(Activity.java:4223) ... at android.app.Activity.startActivityForResult(Activity.java:4182)

什麼原因呢?

這是由於Android7.0執行了“StrictMode API 政策禁”的原因,這時,我們就可以用FileProvider來解決這一問題。

如何使用FileProvider

使用FileProvider的大致步驟如下:

第一步:在manifest清單檔案中註冊provider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.your.package.name"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

android:authorities表示授權列表,當有多個授權時,用分號隔開。名字填寫你的應用包名。
android:exported表示該內容提供器(ContentProvider)是否能被第三方程式元件使用。一般填false,為 true則會報安全異常。
android:grantUriPermissions這個是為是否對ContentProvider中的內容無訪問許可權的使用者提供臨時許可權。true,表示授予 URI 臨時訪問許可權。false,表示不授予。

< meta-data >中的android:resource資原始檔的路徑。

第二步:指定共享的目錄

由一步可知,為了指定共享的目錄我們需要在資源(res)目錄下建立一個xml目錄,然後建立一個名為“file_paths”(名字可以隨便起,只要和在manifest註冊的provider所引用的resource保持一致即可)的資原始檔,內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path path="" name="camera_photos" />
    </paths>
</resources>

備註:
< files-path/>代表的根目錄為: Context.getFilesDir()
< external-path/>代表的根目錄為: Environment.getExternalStorageDirectory()
< cache-path/>代表的根目錄為: getCacheDir()
上述程式碼中path=”“,是有特殊意義的,它程式碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了,如果你將path設為path=”pictures”,
那麼它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的檔案是不行的。

第三步:使用FileProvider

上述準備工作做完之後,現在我們就可以使用FileProvider了。
還是以呼叫系統相機拍照為例,我們需要將上述拍照程式碼修改為如下:

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");

if (!file.getParentFile().exists()){
   file.getParentFile().mkdirs();
}

String authorities = "com.your.package.name";
//通過FileProvider建立一個content型別的Uri
Uri imageUri = 
FileProvider.getUriForFile(context, authorities, file);

Intent intent = new Intent();
//新增這一句表示對目標應用臨時授權該Uri所代表的檔案
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//設定Action為拍照
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//將拍取的照片儲存到指定URI
startActivityForResult(intent,1006);

相比Android7.0之前的程式碼,上述程式碼中主要有以下兩處改變:

  1. 將之前Uri的scheme型別為file的Uri改成了有FileProvider建立一個content型別的Uri。
  2. 添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);來對目標應用臨時授權該Uri所代表的檔案。

因此,上述程式碼通過FileProvider的Uri getUriForFile (Context context, String authority, File file)
靜態方法來獲取Uri,該方法中authority引數就是清單檔案中註冊provider的android:authorities=”com.your.package.name”。
對Web伺服器如tomcat,IIS比較熟悉的小夥伴,都只知道為了網站內容的安全和高效,Web伺服器都支援為網站內容設定一個虛擬目錄,其實FileProvider也有異曲同工之處。
將getUriForFile方法獲取的Uri打印出來如下:

content://com.your.package.name/camera_photos/temp/1474960080319.jpg。

其中camera_photos就是res/xml/資原始檔夾下定義的file_paths.xml中paths的name。

因為上述指定的path為path=”“,所以
content://com.your.package.name/camera_photos/代表的真實路徑就是根目錄,
即:/storage/emulated/0/。

因此

content://com.your.package.name/camera_photos/temp/1474960080319.jpg

代表的真實路徑是:/storage/emulated/0/temp/1474960080319.jpg。

二、裁切照片

在Android7.0之前,你可以通過如下方法來裁切照片:

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()){
  file.getParentFile().mkdirs();
}
Uri outputUri = Uri.fromFile(file);
Uri imageUri=Uri.fromFile(new File("/storage/emulated/0/temp/1474960080319.jpg"));
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent,1008);

和拍照一樣,上述程式碼在Android7.0上同樣會引起android.os.FileUriExposedException異常,解決辦法就是上文說說的使用FileProvider。

參照拍照,將上述程式碼改為如下即可:

File file=new File(Environment.getExternalStorageDirectory(), "/temp/"+System.currentTimeMillis() + ".jpg");
if (!file.getParentFile().exists()){
 file.getParentFile().mkdirs();
}
Uri outputUri = FileProvider.getUriForFile(context, "com.your.package.name",file);
Uri imageUri=FileProvider.getUriForFile(context, "com.your.package.name", new File("/storage/emulated/0/temp/1474960080319.jpg");//通過FileProvider建立一個content型別的Uri
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("scale", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent,1008);

另外,裁切照片推薦大家使用開源工具庫TakePhoto
TakePhoto是一款在Android裝置上獲取照片(拍照或從相簿、檔案中選擇)、裁剪圖片、壓縮圖片的開源工具庫。大家可以拿來進行快速開發。