1. 程式人生 > >Android 7.0及其以上系統拍照,開啟相簿,裁剪,報錯: android.os.FileUriExposedException: file:///storage/emulated/0/.....

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。例如裁剪的時候。輸出的路徑。

至此問題就解決了。

單曲迴圈《絕口不提!愛你》

每日語錄:

在一回首間,才忽然發現,原來,我一生的種種努力,不過只為了周遭的人對我滿意而已。為了搏得他人的稱許與微笑,我戰戰兢兢地將自己套入所有的模式所有的桎梏。走到途中才忽然發現,我只剩下一副模糊的面目,和一條不能回頭的路。---席慕容《獨白》