Android 7.0及其以上系統拍照,開啟相簿,裁剪,報錯: android.os.FileUriExposedException: file:///storage/emulated/0/.....
全部程式碼:點選下載
注意:如果你原先的應用的targetSdkVersion本來就小與27。那就拍照。什麼都不修改。也不會崩潰。但是、一旦你修改了你的targetSdkVersion為27.或者28。那你的應用就會報出這些問題。。具體原因。請自行百度下targetSdkVersion的意義。
Android 7.0以上的系統。在拍照的時候。報錯:
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/XXX/files/avatar.jpg exposed beyond app through ClipData.Item.getUri()
在網上查一下就可以知道。這是Android7.0的“私有目錄被限制訪問”。具體的解釋簡書的一篇文章:
https://www.jianshu.com/p/2275bb552327
裡面講的很仔細。
然後我們知道問題的解決辦法就是通過FileProvider.可是我們應該怎麼用。在哪個地方使用。
A:FileProvider的使用步驟:
1:在資源(res)目錄下建立一個xml目錄,然後建立一個名為"file_paths"的資原始檔 (res-->new -->Directory.然後輸入xml。在xml上右擊--》new--》XML--->ValuesXmL-->輸入名字file_paths 不過會跑到values下面去。。不慌我們剪下到xml下面即可)
xml裡面的內容如下:
具體解釋:借用上面連結文章裡面的一段文字:
所以:我上面的路徑應該是:/storage/emulated/0/Android/data/com.example.zongm.testapplication/ 之所以這樣寫。是想要在應用刪除的時候。在應用裡面拍的照片也能夠被刪除掉。
2:在manifest清單檔案中註冊provider(放在application節點裡面)
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.zongm.testapplication.provider"android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>
解釋:
android:authorities="com.example.zongm.testapplication.provider" 這個值可以隨便寫。我用的是appid。這個值決定了fileProVider生成的uri的路徑。後面詳細介紹
exported:要求必須為false,為true則會報安全異常。grantUriPermissions:true,表示授予 URI 臨時訪問許可權。
然後我們就可以在程式碼中使用FileProvider了。
使用:
private Uri getImageUri() { if (isSdCardExist()) { photo_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date()); File[] dirs = ContextCompat.getExternalFilesDirs(this, null); if (dirs != null && dirs.length > 0) { File dir = dirs[0]; File file = new File(dir, photo_image); takePath = file.getAbsolutePath(); Log.e("zmm", "圖片的路徑---》" + takePath); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //FileProvider.getUriForFile();第一個引數是context. // 第二個值。比較關鍵、這個也就是我們在manifest裡面的provider裡面的 //android:authorities="com.example.zongm.testapplication.provider" //因為我用的就是AppId.所以。這裡就直接用BuildConfig.APPLICATION_ID了。 //如果你的android:authorities="test.provider"。那這裡第二個引數就應該是test.provider return FileProvider.getUriForFile(getApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", file); } else { return Uri.fromFile(file); } } } return Uri.EMPTY; }
下面通過一個例子來看看我們如何使用。程式碼註釋寫的很清楚了應該:
public class Main3Activity extends AppCompatActivity { //開啟相機的返回碼 private static final int CAMERA_REQUEST_CODE = 1; //選擇圖片的返回碼 private static final int IMAGE_REQUEST_CODE = 2; //剪下圖片的返回碼 public static final int CROP_REREQUEST_CODE = 3; private ImageView iv; //相機 public static final int REQUEST_CODE_PERMISSION_CAMERA = 100; public static final int REQUEST_CODE_PERMISSION_GALLERY = 101; //照片圖片名 private String photo_image; //截圖圖片名 private String crop_image; //拍攝的圖片的真實路徑 private String takePath; //拍攝的圖片的虛擬路徑 private Uri imageUri; private Uri cropUri; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main3); iv = findViewById(R.id.iv); } /** * 拍照 * * @param view */ public void onClickTakePhoto(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { checkPermission(REQUEST_CODE_PERMISSION_CAMERA); return; } openCamera(); } /** * 開啟系統的相機的時候。我們需要傳入一個uri。該uri就是拍攝的照片的地址。 * 也就是:cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, getImageUri()); * 這裡就用到了FileProvider */ private void openCamera() { if (isSdCardExist()) { Intent cameraIntent = new Intent( "android.media.action.IMAGE_CAPTURE"); photo_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date()) + ".jpg"; imageUri = getImageUri(photo_image); //Log.e("zmm", "圖片儲存的uri---------->" + imageUri); cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT , imageUri); //新增這一句表示對目標應用臨時授權該Uri所代表的檔案 cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); startActivityForResult(cameraIntent, CAMERA_REQUEST_CODE); } else { Toast.makeText(this, "SD卡不存在", Toast.LENGTH_SHORT).show(); } } /** * 開啟相簿 * 不需要用FileProvider * * @param view */ public void onClickOpenGallery(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { checkPermission(REQUEST_CODE_PERMISSION_GALLERY); return; } openGallery(); } private void openGallery() { Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT); galleryIntent.addCategory(Intent.CATEGORY_OPENABLE); galleryIntent.setType("image/*"); startActivityForResult(galleryIntent, IMAGE_REQUEST_CODE); } /** * @param path 原始圖片的路徑 */ public void cropPhoto(String path) { crop_image = new SimpleDateFormat("yyyy_MMdd_hhmmss").format(new Date()) + "_crop" + ".jpg"; File cropFile = createFile(crop_image); File file = new File(path); Intent intent = new Intent("com.android.camera.action.CROP"); //TODO:訪問相簿需要被限制,需要通過FileProvider建立一個content型別的Uri if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //新增這一句表示對目標應用臨時授權該Uri所代表的檔案 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //TODO:訪問相簿需要被限制,需要通過FileProvider建立一個content型別的Uri imageUri = FileProvider.getUriForFile(getApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", file); cropUri = Uri.fromFile(cropFile); //TODO:cropUri 是裁剪以後的圖片儲存的地方。也就是我們要寫入此Uri.故不需要用FileProvider //cropUri = FileProvider.getUriForFile(getApplicationContext(), // BuildConfig.APPLICATION_ID + ".provider", cropFile); } else { imageUri = Uri.fromFile(file); cropUri = Uri.fromFile(cropFile); } intent.setDataAndType(imageUri, "image/*"); intent.putExtra("crop", "true"); //設定寬高比例 intent.putExtra("aspectX", 1); intent.putExtra("aspectY", 1); //設定裁剪圖片寬高 intent.putExtra("outputX", 400); intent.putExtra("outputY", 400); intent.putExtra("scale", true); //裁剪成功以後儲存的位置 intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri); intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); intent.putExtra("noFaceDetection", true); startActivityForResult(intent, CROP_REREQUEST_CODE); } /** * 獲得一個uri。該uri就是將要拍攝的照片的uri * * @return */ private Uri getImageUri(String name) { if (isSdCardExist()) { File file = createFile(name); if (file != null) { takePath = file.getAbsolutePath(); Log.e("zmm", "圖片的路徑---》" + takePath); // 輸出是/storage/emulated/0/Android/data/com.example.zongm.testapplication/files/2018_0713_111455.jpg // 根據這個path。拿到的Uri是:content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_111455.jpg //我們可以看到真實路徑:/Android/data/com.example.zongm.testapplication這一部分被files_root替代了 //也就是我們在file_path裡面寫的<external-path // name="files_root" // path="Android/data/com.example.zongm.testapplication/" /> //其中external-path代表的是 Environment.getExternalStorageDirectory() 也就是/storage/emulated/0 //。。。。我說的有點亂。大家還是看那篇簡書文章吧。:連結:https://www.jianshu.com/p/56b9fb319310 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //FileProvider.getUriForFile();第一個引數是context. // 第二個值。比較關鍵、這個也就是我們在manifest裡面的provider裡面的 //android:authorities="com.example.zongm.testapplication.provider" //因為我用的就是AppId.所以。這裡就直接用BuildConfig.APPLICATION_ID了。 //如果你的android:authorities="test.provider"。那這裡第二個引數就應該是test.provider return FileProvider.getUriForFile(getApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", file); } else { return Uri.fromFile(file); } } } return Uri.EMPTY; } public File createFile(String name) { if (isSdCardExist()) { File[] dirs = ContextCompat.getExternalFilesDirs(this, null); if (dirs != null && dirs.length > 0) { File dir = dirs[0]; return new File(dir, name); } } return null; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { switch (requestCode) { case CAMERA_REQUEST_CODE://拍照成功並且返回 //注意這裡。可以直接用takePath。也可以直接用imageUri。 //因為Glide直接載入Uri。也可以載入地址。 //Glide.with(this) // .asBitmap() // .load(imageUri) // .into(iv); //但是。這裡載入的都是拍攝的原圖。一般我們都會根據uri。或者path.找到檔案。把bitmap取出來。然後做壓縮等其他的二次處理。 //decodeImage(imageUri);//顯示照片 //或者直接去裁剪 //這裡有個坑。就是我們如果想要根據Uri---》圖片的真實path.然後拿到File.一般的Uri. // 例如是從相簿選擇照片並且回來的圖片。我們拿到的Uri是這樣的: //content://com.android.providers.media.documents/document/image%3A732871 //或者這樣:content://media/external/images/media/694091 //這樣我們可以用ImageUtils.getPath()這個裡面的一系列方法拿到真實路徑。 //但是。如果是通過我們的FileProvider拿到的Uri.是這樣的: //content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_020952.jpg //這樣的路徑我們是用ImageUtils.getPath()這個裡面的一系列方法是拿不到真實路徑的。會報錯: //報錯資訊是:GetDataColumnFail java.lang.IllegalArgumentException: column '_data'does not exist //我們在網上查一下。就可以知道。要想拿到FileProvider得到的Uri的真實圖片路徑。需要用到反射: //這裡大家可以去查一下:這裡隨便給一個部落格地址。:https://blog.csdn.net/u010853225/article/details/80191880 // 故這裡我們不能用此方法拿到真實路徑 String path = ImageUtils.getPath(this, imageUri); cropPhoto(takePath); break; case IMAGE_REQUEST_CODE://選擇圖片成功返回 if (data != null && data.getData() != null) { imageUri = data.getData(); //直接顯示出來 //decodeImage(data.getData()); //或者去裁剪 String path = ImageUtils.getPath(this, imageUri); Log.e("zmm", "選擇的圖片的虛擬地址是------------>" + data.getData() + "--->" + path); cropPhoto(path); } break; case CROP_REREQUEST_CODE: Log.e("zmm", "裁剪以後的地址是------------>" + cropUri); decodeImage(cropUri); break; } } } /** * 根據uri拿到bitmap * * @param imageUri 這個Uri是 */ private void decodeImage(Uri imageUri) { //這樣是可以正常拿到bitmap。但是我們知道。這樣寫。很有可能會oom //Bitmap bitmap = ImageUtils.decodeUriAsBitmap(this, imageUri); //Log.e("zmm", "初始大小-------------->" + bitmap.getByteCount());//原始大小是47235072 //iv.setImageBitmap(bitmap); //所以我們一般都是把bitmap 進行一次壓縮 try { Bitmap bitmapFormUri = ImageUtils.getBitmapFormUri(this, imageUri); //Log.e("zmm", "壓縮過後------------->" + bitmapFormUri // .getByteCount());//壓縮過後2952192 iv.setImageBitmap(bitmapFormUri); } catch (IOException e) { e.printStackTrace(); } } /** * 檢查許可權 * * @param requestCode */ private void checkPermission(int requestCode) { boolean granted = PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this, Manifest.permission_group.CAMERA); if (granted) {//有許可權 if (requestCode == REQUEST_CODE_PERMISSION_CAMERA) { openCamera();//開啟相機 } else { openGallery();//開啟相簿 } return; } //沒有許可權的要去申請許可權 //注意:如果是在Fragment中申請許可權,不要使用ActivityCompat.requestPermissions, // 直接使用Fragment的requestPermissions方法,否則會回撥到Activity的onRequestPermissionsResult ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest .permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) { boolean flag = true; for (int i = 0; i < grantResults.length; i++) { if (grantResults[i] != PERMISSION_GRANTED) { flag = false; break; } } //許可權通過以後。自動回撥拍照 if (flag) { if (requestCode == REQUEST_CODE_PERMISSION_CAMERA) { openCamera();//開啟相機 } else { openGallery();//開啟相簿 } } else { Toast.makeText(this, "請開啟許可權", Toast.LENGTH_SHORT).show(); } } } /** * 檢查SD卡是否存在 */ public boolean isSdCardExist() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } }
嗯。寫了很多東西。也很雜亂。總結一下
A:在用相機拍照的時候。報錯:
Caused by: android.os.FileUriExposedException: file:///storage/emulated/0/tencent/MicroMsg/WeiXin/mmexport1530703314161.jpg exposed beyond app through Intent.getData()
說明。你需要用 FileProvider.getUriForFile()來代替原本的Uri.fromFile();
解決辦法就是參照網上的方法用FileProvider.
B:如果在過程中報錯:
GetDataColumnFail java.lang.IllegalArgumentException: column '_data'does not exist
那可能是因為你在Uri轉Path的過程中用的Uri是由FileProvider得到的Uri.
解決辦法:看看是不是可以換成普通的Uri.如果非得用FileProvider得到的Uri.。你就得用反射的方法來拿到真實路徑:具體實現請自行搜尋。
C:如果在過程中報錯:
Writing exception to parcel
java.lang.SecurityException: Permission Denial: writing android.support.v4.content.FileProvider uri content://com.example.zongm.testapplication.provider/files_root/files/2018_0713_033250_crop.jpg from pid=10330, uid=10013 requires the provider be exported, or grantUriPermission()
看看是不是在不該使用FileProvider的時候使用了FileproVider。例如裁剪的時候。輸出的路徑。
至此問題就解決了。
單曲迴圈《絕口不提!愛你》
每日語錄:
在一回首間,才忽然發現,原來,我一生的種種努力,不過只為了周遭的人對我滿意而已。為了搏得他人的稱許與微笑,我戰戰兢兢地將自己套入所有的模式所有的桎梏。走到途中才忽然發現,我只剩下一副模糊的面目,和一條不能回頭的路。---席慕容《獨白》