Android7.0呼叫系統相機和裁剪
最近將專案的targetSdkVersion升級到了26,發現呼叫系統相機的時候報了下面這個錯誤:
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg exposed beyond app through ClipData.Item.getUri()
經過排查發現是 imageUri = Uri.fromFile(outputImage);這段程式碼報了錯誤.
同樣的程式碼,為什麼sdk版本沒升級前是執行正常的,升級後就報錯了呢,經過一番資料查詢發現從Android 7.0開始,一個應用提供自身檔案給其它應用使用時,如果給出一個file://格式的URI的話,應用會丟擲FileUriExposedException。這是由於谷歌認為目標app可能不具有檔案許可權,會造成潛在的問題。
那麼怎麼解決呢?
這就需要用到FileProvider這個東西了,具體使用步驟如下:
1.在AndroidManifest.xml中新增如下程式碼
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="blog.csdn.net.mchenys.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
其中authorities可以隨意配置,通常為了確保唯一,都是包名+.fileprovider的格式.同時注意下專案中使用的時候也是要跟這裡匹配就可以了.
2.在在res目錄下新建一個xml資料夾,並且新建一個file_paths的xml檔案
<?xml version="1.0" encoding="utf-8"?>
<paths >
<!--
external-path:Environment.getExternalStorageDirectory()
name:可以隨意定義,專案中使用的時候保持一致就可以了
path:表示共享的具體路徑, .表示當前路徑,即表示整個sd卡路徑-->
<external-path name="external_files" path="."/>
</paths>
更多的節點的含義,可以參考下面這個表:
經過上面這樣的配置,其實就是將某個路徑通過別名的形式標記起來了,例如external_files在本例中就表示的是sd卡的根目錄.
3.將專案中的imageUri = Uri.fromFile(outputImage);這段程式碼修改成下面方式:
if (Build.VERSION.SDK_INT >= 24) {
String authority = this.getPackageName() + ".fileprovider";
imageUri = FileProvider.getUriForFile(this, authority, outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
4.將 uri.getPath()這段程式碼做下調整.
當呼叫系統相簿拍照完後,相片的路徑資訊已經儲存到了我們指定的imageUri .如果我們直接通過imageUri .getPath()來獲取該圖片的路徑的話,你會發現根本無法使用,通過log將imageUri 列印,同時將getScheme()、getPath()、getAuthority()得到的內容也列印,結果如下所示:
Uri:
content://blog.csdn.net.mchenys.fileprovider/external_files/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg
Scheme:content
Path:
/external_files/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg
Authority:blog.csdn.net.mchenys.fileprovider
觀察Path發現多了個external_files,敏銳的你肯定知道了原因,這個其實就是我們在步驟2中的file_paths.xml裡定義的external-path節點的name.所以這就是為什麼說imageUri .getPath()拿到的路徑是用不了的,因為sd卡中根本就不存在external_files這個資料夾,那麼如何解決呢?
方法也很簡單,當拿到path之後,通過下面的方式替換一下就可以了
String path = imageUri.getPath();
if(path.contains("external_files")){
path = path.replaceAll("/external_files",Environment.getExternalStorageDirectory().
getAbsolutePath());
}
//替換後的path就是/storage/emulated/0/Android/data/blog.csdn.net.mchenys/cache/output_image.jpg,這樣就可以正常訪問了.
Ok,上面的插曲講完之後,通過一個demo來例項下在android7.0及以上的呼叫系統相機的具體操作:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="blog.csdn.net.mchenys.MainActivity">
<Button
android:text="takephoto"
android:onClick="takePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
為了相容android4.4之前的系統,需要在AndroidManifest.xml中新增訪問SD卡的許可權
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
MainActivity.java
package blog.csdn.net.mchenys;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.RequiresApi;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
public static final int TAKE_PHOTO = 1;
private ImageView picture;
private Uri imageUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
picture = findViewById(R.id.picture);
}
@RequiresApi(api = Build.VERSION_CODES.FROYO)
public void takePhoto(View view) {
/*
getExternalCacheDir訪問的應用的私有目錄(/sdcard/Android/data/<package name>/cache)
因此不需要動態許可權申請.
*/
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
outputImage.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
//相容7.0的方式獲取uri
if (Build.VERSION.SDK_INT >= 24) {
imageUri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", outputImage);
} else {
imageUri = Uri.fromFile(outputImage);
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case TAKE_PHOTO:
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
}
}
}
}
最後,別忘了配置FileProvider,按照上文介紹的方式配置就可以了.
看到這裡,相信大夥都以為就只有這一種解決方式了,哈,下面介紹一種更吊的方式,可以直接遮蔽掉FileUriExposedException.
只需要在Activity的onCreate方法中加入下面的程式碼即可.
//遮蔽7.0中使用 Uri.fromFile爆出的FileUriExposureException
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
if (Build.VERSION.SDK_INT >=24) {
builder.detectFileUriExposure();
}
然後剩下的事情就是動態許可權申請了,因為讀取和寫入sd卡在android6.0之後需要動態申請許可權,當然如果使用的是私有目錄的話也可以不用申請許可權.
demo如下,包含呼叫系統拍照和裁剪的功能
package blog.csdn.net.mchenys;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 該類展示,遮蔽FileProvider.getUriForFile方式獲取uri的操作
* Created by mChenys on 2018/4/25.
*/
public class MainActivity2 extends AppCompatActivity {
private ImageView picture;
private Uri imageUri;
private Uri cropImgUri;
public static final int TAKE_PHOTO = 1;
public static final int CROP_PHOTO = 2;
public static final int GET_PERMISSION = 3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//遮蔽7.0中使用 Uri.fromFile爆出的FileUriExposureException
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
if (Build.VERSION.SDK_INT >= 24) {
builder.detectFileUriExposure();
}
picture = findViewById(R.id.picture);
}
public void takePhoto(View view) {
/* if (Build.VERSION.SDK_INT >= 23) {
boolean hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
if (hasPermission ) {
openCamera();
} else {
showDialog("拍照需要獲取儲存許可權", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity2.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, GET_PERMISSION);
}
});
}
} else {
openCamera();
}*/
//如果操作的是私有目錄,可以不用申請許可權
openCamera();
}
private void openCamera() {
// File outputImage = new File(Environment.getExternalStorageDirectory(), "output_image.jpg");
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
outputImage.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
imageUri = Uri.fromFile(outputImage);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
switch (requestCode) {
case TAKE_PHOTO: //處理拍照返回結果
startPhotoCrop();
break;
case CROP_PHOTO://處理裁剪返回結果
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(cropImgUri));
picture.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 開啟裁剪相片
*/
public void startPhotoCrop() {
//建立file檔案,用於儲存剪裁後的照片
// File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");
File cropImage = new File(getExternalCacheDir(), "crop_image.jpg");
try {
if (cropImage.exists()) {
cropImage.delete();
}
cropImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
cropImgUri = Uri.fromFile(cropImage);
Intent intent = new Intent("com.android.camera.action.CROP");
//設定源地址uri
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 200);
intent.putExtra("outputY", 200);
intent.putExtra("scale", true);
//設定目的地址uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImgUri);
//設定圖片格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("return-data", false);//data不需要返回,避免圖片太大異常
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, CROP_PHOTO);
}
//彈窗提示
private void showDialog(String text, DialogInterface.OnClickListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("許可權申請")
.setMessage(text)
.setPositiveButton("確定", listener)
.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == GET_PERMISSION &&
grantResults[0] == PackageManager.PERMISSION_GRANTED ) {
openCamera();
}
}
}