1. 程式人生 > >Android7.0呼叫系統相機和裁剪

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();
        }
    }
}