關於Android7.0相機閃退以及相簿獲取不到圖片問題
阿新 • • 發佈:2019-02-03
文件說明:關於Android7.0及以上機型調取相機閃退情況處理。
現象:
因開發中遇到需要呼叫系統相機或相簿獲取圖片,於是也沒有多思考就使用相關指定的Action去調取相機或者相簿,在開始測試時未出現問題,直到這個APK包被裝到一箇中興手機(型號A2017)手機上,於是坑就出現了:在該手機上呼叫相機時出現應用閃退,獲取相簿也有同樣的問題。於是本人有換了華為P9 Plus,問題同樣存在。因本人logcat出了問題,沒有日誌,一開始以為是6.0出現的動態獲取下許可權問題,因為這種情況在魅族MX6上也出現過,當時解決的方案是手動去設定了對應用的相機許可權,從而解決了該問題。於是在程式碼中增加了許可權獲取程式碼:
if (Build.VERSION.SDK_INT>22){
if (ContextCompat.checkSelfPermission(this,Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED){
//先判斷有沒有許可權 ,沒有就在這裡進行許可權的申請
requestPermissions(new String[]{Manifest.permission.CAMERA}, CAMERA_OK);
}else {
//說明已經獲取到攝像頭許可權了 想幹嘛幹嘛
toSelectPhotoOrOpenCamera();
}
}else {
//這個說明系統版本在6.0之下,不需要動態獲取許可權。
toSelectPhotoOrOpenCamera();
}
並重寫了許可權獲取方法:
/**
* 許可權操作結果處理
*/
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
switch (requestCode) {
case CAMERA_OK:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//使用者已授權
toSelectPhotoOrOpenCamera();
} else {
//使用者拒絕許可權
ToastUtils.show(this, "缺少相機許可權,暫時無法提供掃描功能,請嘗試在設定中開啟相機許可權!", Toast.LENGTH_LONG);
}
break;
}
}
事實證明這個坑不淺,增加以上程式碼過後,依然出現閃退,然而該程式碼在魅族MX6和OPPOA59s上則不存在該問題。
原因鎖定:
因沒有日誌可看,於是在網上查詢關於該問題的現象,終於將問題鎖定為因Android7.0對手機相關許可權回收而出現的,這也說明為什麼該種情況只在中興A2017和華為P9 plus上出現(中興華為的android版本為7.1.1和7.0,而魅族MX6和OPPOA59s的android版本則為6.0)。
原因簡述:
其實不僅是呼叫相機和相簿,只要是訪問檔案,都會出現這個錯誤,其原因是Android 7.0 做了一些系統許可權更改,為了提高私有檔案的安全性,面向 Android 7.0 或更高版本的應用私有目錄被限制訪問,此設定可防止私有檔案的元資料洩漏,如它們的大小或存在性。而此許可權更改有多重副作用,其中之一就是當傳遞軟體包網域外的 file:// URI 可能給接收器留下無法訪問的路徑。因此,嘗試傳遞 file:// URI 會觸發 FileUriExposedException。分享私有檔案內容的推薦方法是使用 FileProvider。在應用間共享檔案對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含檔案 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。要在應用間共享檔案,應傳送一項 content:// URI,並授予 URI 臨時訪問許可權。進行此授權的最簡單方式是使用 FileProvider 類。原文在這裡[Android 7.0 行為變更](https://developer.android.Google.cn/about/versions/nougat/android-7.0-changes.html)。
解決方案:
針對android版本不同,做不同的處理。即當android版本大於等於N時,則使用另一套調取相機和相簿的機制。於是產生了以下程式碼:
調取相機:
if (android.os.Build.VERSION.SDK_INT>=24) {
PhotoUtilsAbove23.openCamera(MainActivity.this);
}else {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
ContentValues values = new ContentValues();
photoUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,photoUri);
startActivityForResult(intent, SELECT_PIC_BY_TACK_PHOTO);
}else {
ToastUtils.show(MainActivity.this,"SD卡不存在",Toast.LENGTH_SHORT);
}
}
調取相簿:
if (Build.VERSION.SDK_INT >= 24) {
PhotoUtilsAbove23.openPhotos(MainActivity.this);
}else {
Intent intent = new Intent(Intent.ACTION_PICK,android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, SELECT_PIC_BY_PICK_PHOTO);
}
使用到的工具類:可以參考demo中的PhotoUtilsAbove23類。其中有兩個方法:
在7.0以上的開啟相機方式:
public static void openCamera(Context context){
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(context.getPackageManager()) != null) {//判斷是否有相機應用
// Create the File where the photo should go
try {
photoFile = createImageFile(context);//建立臨時圖片檔案
} catch (IOException ex) {
ex.printStackTrace();
}
// Continue only if the File was successfully created
if (photoFile != null) {
//FileProvider 是一個特殊的 ContentProvider 的子類,
//它使用 content:// Uri 代替了 file:/// Uri. ,更便利而且安全的為另一個app分享檔案
Uri photoURI = FileProvider.getUriForFile(context,
context.getPackageName()+".fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
((MainActivity)context).startActivityForResult(takePictureIntent, SELECT_PIC_BY_TACK_PHOTO);
}
}
}
在7.0以上的開啟相簿方式:
public static void openPhotos(Context context){
//mGalleryFile = new File(getExternalDir(), IMAGE_GALLERY_NAME);
try {
mGalleryFile=createImageFile(context);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
if (Build.VERSION.SDK_INT >= 24) {//如果大於等於7.0使用FileProvider
Uri uriForFile = FileProvider.getUriForFile(context, context.getPackageName()+".fileprovider", mGalleryFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uriForFile);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
((MainActivity)context).startActivityForResult(intent, SELECT_PIC_BY_PICK_PHOTO);
}
}
其他工具方法則不一一列舉。
在使用針對不同版本的android使用不同的調取方式後,還需要做一些配置,才能正常調起相機和相簿,其中包括Manifest.xml和res目錄下的xml資料夾。(網上很多方法,在調取相簿時使用的常規呼叫方式,確實能開啟相簿,但是開啟相簿後,選擇某一張圖片,也確實返回到了onActivityResult方法中,同時也可以去到圖片的路徑:類似於這種路徑:/storage/emulated/0/DICM/***/***.jpg,但是這個路徑無法通過BitmapFactory的decodeFile等方法拿出來一個bitmap,至於原因,因為沒有日誌,暫時不清楚,但依照網上描述,依然和7.0針對許可權的處理有關,於是我們針對7.0以上的呼叫相簿使用了另一種方式,也就是上文提到的方式)。
在manifest中配置如下:
<!-- 新增7.0針對檔案許可權的provider -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.opencameraabove23demo.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>
</provider>
在res目錄下新增xml資料夾,並增加一個對應檔案filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path name="my_images" path="Android/data/com.example.opencameraabove23demo/files/Pictures" />
</paths>
</resources>
注意:com.example.opencameraabove23demo為你的app的包名,同時需要給provider處理Uri的授權grantUriPermissions=true
接著在onActivityResult中,我們做了如下處理。
在7.0之前,我們都是使用圖片的真是路徑path來通過BitmapFactory的decode方法獲取圖片,在7.0之後,將使用Uri來獲取圖片。
/**
* 獲取選擇照片時的Uri,最終獲得drawable
*/
@SuppressWarnings("unused")
private void getPhotoUri(int requestCode, int resultCode, Intent data) {
isSelectPhoto=false;
if (requestCode == SELECT_PIC_BY_PICK_PHOTO) {//如果選擇照片成功,則獲取其Uri
if (android.os.Build.VERSION.SDK_INT>=24) {
//content://com.android.providers.media.documents/document/image%3A41837
photoUri=data.getData();
isSelectPhoto=true;
picPath=PhotoUtilsAbove23.getGalleryFile().getAbsolutePath();
drawable=PhotoUtilsAbove23.getAddressByUriAbove23(this,picPath,photoUri,isSelectPhoto);
}else {
if (data == null) {
ToastUtils.show(MainActivity.this,
"error file",Toast.LENGTH_SHORT);
return;
}
photoUri = data.getData();
if (photoUri == null) {
ToastUtils.show(MainActivity.this,
"error file",Toast.LENGTH_SHORT);
return;
}
drawable=PhotoUtilsAbove23.getAddressByUri(this,photoUri,isSelectPhoto);
}
}else if (requestCode == SELECT_PIC_BY_TACK_PHOTO) {
if (android.os.Build.VERSION.SDK_INT>=24) {
///storage/emulated/0/Android/data/cn.com.fdbank.ui/files/Pictures/JPEG_20170505_154338_738550391.jpg
//注意此處我們的圖片路徑不在是/storage/emulated/0/DICM/***/***.jpg,而是一個臨時授權的地址
picPath=PhotoUtilsAbove23.getPhotoFile().getAbsolutePath();
drawable=PhotoUtilsAbove23.getAddressByUriAbove23(this,picPath,photoUri,isSelectPhoto);
}else {
drawable=PhotoUtilsAbove23.getAddressByUri(this,photoUri,isSelectPhoto);
}
}
}
現象:
因開發中遇到需要呼叫系統相機或相簿獲取圖片,於是也沒有多思考就使用相關指定的Action去調取相機或者相簿,在開始測試時未出現問題,直到這個APK包被裝到一箇中興手機(型號A2017)手機上,於是坑就出現了:在該手機上呼叫相機時出現應用閃退,獲取相簿也有同樣的問題。於是本人有換了華為P9 Plus,問題同樣存在。因本人logcat出了問題,沒有日誌,一開始以為是6.0出現的動態獲取下許可權問題,因為這種情況在魅族MX6上也出現過,當時解決的方案是手動去設定了對應用的相機許可權,從而解決了該問題。於是在程式碼中增加了許可權獲取程式碼:
if (Build.VERSION.SDK_INT>22){
if (ContextCompat.checkSelfPermission(this,Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED){
//先判斷有沒有許可權 ,沒有就在這裡進行許可權的申請
requestPermissions(new String[]{Manifest.permission.CAMERA}, CAMERA_OK);
}else {
//說明已經獲取到攝像頭許可權了 想幹嘛幹嘛
toSelectPhotoOrOpenCamera();
}
}else {
//這個說明系統版本在6.0之下,不需要動態獲取許可權。
toSelectPhotoOrOpenCamera();
}
並重寫了許可權獲取方法:
/**
* 許可權操作結果處理
*/
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
switch (requestCode) {
case CAMERA_OK:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//使用者已授權
toSelectPhotoOrOpenCamera();
} else {
//使用者拒絕許可權
ToastUtils.show(this, "缺少相機許可權,暫時無法提供掃描功能,請嘗試在設定中開啟相機許可權!", Toast.LENGTH_LONG);
}
break;
}
}
事實證明這個坑不淺,增加以上程式碼過後,依然出現閃退,然而該程式碼在魅族MX6和OPPOA59s上則不存在該問題。
原因鎖定:
因沒有日誌可看,於是在網上查詢關於該問題的現象,終於將問題鎖定為因Android7.0對手機相關許可權回收而出現的,這也說明為什麼該種情況只在中興A2017和華為P9 plus上出現(中興華為的android版本為7.1.1和7.0,而魅族MX6和OPPOA59s的android版本則為6.0)。
原因簡述:
其實不僅是呼叫相機和相簿,只要是訪問檔案,都會出現這個錯誤,其原因是Android 7.0 做了一些系統許可權更改,為了提高私有檔案的安全性,面向 Android 7.0 或更高版本的應用私有目錄被限制訪問,此設定可防止私有檔案的元資料洩漏,如它們的大小或存在性。而此許可權更改有多重副作用,其中之一就是當傳遞軟體包網域外的 file:// URI 可能給接收器留下無法訪問的路徑。因此,嘗試傳遞 file:// URI 會觸發 FileUriExposedException。分享私有檔案內容的推薦方法是使用 FileProvider。在應用間共享檔案對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在您的應用外部公開 file:// URI。如果一項包含檔案 URI 的 intent 離開您的應用,則應用出現故障,並出現 FileUriExposedException 異常。要在應用間共享檔案,應傳送一項 content:// URI,並授予 URI 臨時訪問許可權。進行此授權的最簡單方式是使用 FileProvider 類。原文在這裡[Android 7.0 行為變更](https://developer.android.Google.cn/about/versions/nougat/android-7.0-changes.html)。
解決方案:
針對android版本不同,做不同的處理。即當android版本大於等於N時,則使用另一套調取相機和相簿的機制。於是產生了以下程式碼:
調取相機:
if (android.os.Build.VERSION.SDK_INT>=24) {
PhotoUtilsAbove23.openCamera(MainActivity.this);
}else {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
ContentValues values = new ContentValues();
photoUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,values);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT,photoUri);
startActivityForResult(intent, SELECT_PIC_BY_TACK_PHOTO);
}else {
ToastUtils.show(MainActivity.this,"SD卡不存在",Toast.LENGTH_SHORT);
}
}
調取相簿:
if (Build.VERSION.SDK_INT >= 24) {
PhotoUtilsAbove23.openPhotos(MainActivity.this);
}else {
Intent intent = new Intent(Intent.ACTION_PICK,android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, SELECT_PIC_BY_PICK_PHOTO);
}
使用到的工具類:可以參考demo中的PhotoUtilsAbove23類。其中有兩個方法:
在7.0以上的開啟相機方式:
public static void openCamera(Context context){
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (takePictureIntent.resolveActivity(context.getPackageManager()) != null) {//判斷是否有相機應用
// Create the File where the photo should go
try {
photoFile = createImageFile(context);//建立臨時圖片檔案
} catch (IOException ex) {
ex.printStackTrace();
}
// Continue only if the File was successfully created
if (photoFile != null) {
//FileProvider 是一個特殊的 ContentProvider 的子類,
//它使用 content:// Uri 代替了 file:/// Uri. ,更便利而且安全的為另一個app分享檔案
Uri photoURI = FileProvider.getUriForFile(context,
context.getPackageName()+".fileprovider",
photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
((MainActivity)context).startActivityForResult(takePictureIntent, SELECT_PIC_BY_TACK_PHOTO);
}
}
}
在7.0以上的開啟相簿方式:
public static void openPhotos(Context context){
//mGalleryFile = new File(getExternalDir(), IMAGE_GALLERY_NAME);
try {
mGalleryFile=createImageFile(context);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
if (Build.VERSION.SDK_INT >= 24) {//如果大於等於7.0使用FileProvider
Uri uriForFile = FileProvider.getUriForFile(context, context.getPackageName()+".fileprovider", mGalleryFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uriForFile);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
((MainActivity)context).startActivityForResult(intent, SELECT_PIC_BY_PICK_PHOTO);
}
}
其他工具方法則不一一列舉。
在使用針對不同版本的android使用不同的調取方式後,還需要做一些配置,才能正常調起相機和相簿,其中包括Manifest.xml和res目錄下的xml資料夾。(網上很多方法,在調取相簿時使用的常規呼叫方式,確實能開啟相簿,但是開啟相簿後,選擇某一張圖片,也確實返回到了onActivityResult方法中,同時也可以去到圖片的路徑:類似於這種路徑:/storage/emulated/0/DICM/***/***.jpg,但是這個路徑無法通過BitmapFactory的decodeFile等方法拿出來一個bitmap,至於原因,因為沒有日誌,暫時不清楚,但依照網上描述,依然和7.0針對許可權的處理有關,於是我們針對7.0以上的呼叫相簿使用了另一種方式,也就是上文提到的方式)。
在manifest中配置如下:
<!-- 新增7.0針對檔案許可權的provider -->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.opencameraabove23demo.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>
</provider>
在res目錄下新增xml資料夾,並增加一個對應檔案filepaths.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path name="my_images" path="Android/data/com.example.opencameraabove23demo/files/Pictures" />
</paths>
</resources>
注意:com.example.opencameraabove23demo為你的app的包名,同時需要給provider處理Uri的授權grantUriPermissions=true
接著在onActivityResult中,我們做了如下處理。
在7.0之前,我們都是使用圖片的真是路徑path來通過BitmapFactory的decode方法獲取圖片,在7.0之後,將使用Uri來獲取圖片。
/**
* 獲取選擇照片時的Uri,最終獲得drawable
*/
@SuppressWarnings("unused")
private void getPhotoUri(int requestCode, int resultCode, Intent data) {
isSelectPhoto=false;
if (requestCode == SELECT_PIC_BY_PICK_PHOTO) {//如果選擇照片成功,則獲取其Uri
if (android.os.Build.VERSION.SDK_INT>=24) {
//content://com.android.providers.media.documents/document/image%3A41837
photoUri=data.getData();
isSelectPhoto=true;
picPath=PhotoUtilsAbove23.getGalleryFile().getAbsolutePath();
drawable=PhotoUtilsAbove23.getAddressByUriAbove23(this,picPath,photoUri,isSelectPhoto);
}else {
if (data == null) {
ToastUtils.show(MainActivity.this,
"error file",Toast.LENGTH_SHORT);
return;
}
photoUri = data.getData();
if (photoUri == null) {
ToastUtils.show(MainActivity.this,
"error file",Toast.LENGTH_SHORT);
return;
}
drawable=PhotoUtilsAbove23.getAddressByUri(this,photoUri,isSelectPhoto);
}
}else if (requestCode == SELECT_PIC_BY_TACK_PHOTO) {
if (android.os.Build.VERSION.SDK_INT>=24) {
///storage/emulated/0/Android/data/cn.com.fdbank.ui/files/Pictures/JPEG_20170505_154338_738550391.jpg
//注意此處我們的圖片路徑不在是/storage/emulated/0/DICM/***/***.jpg,而是一個臨時授權的地址
picPath=PhotoUtilsAbove23.getPhotoFile().getAbsolutePath();
drawable=PhotoUtilsAbove23.getAddressByUriAbove23(this,picPath,photoUri,isSelectPhoto);
}else {
drawable=PhotoUtilsAbove23.getAddressByUri(this,photoUri,isSelectPhoto);
}
}
}
這裡同樣使用到了PhotoUtilsAbove23工具類中的方法,詳細情況見demo中的該工具方法。這個工具類中的一些方法我也是借鑑網上一些大神提供的方法,如有不足,請大神指點!關於這個工具類我已經放在一個文件中,這個文件有一個demo和demo說明,有問題的朋友可以通過下面連結來下載:http://download.csdn.net/detail/xiangxiang_8_8/9838317
上面的demo步驟有些複雜,其實這裡還有個操作步驟更簡單的demo,是我沒事寫的JSBridgr互動實現的一個相機及相簿功能,同樣相容7.0,懂一點android和js的人應該都能看懂,其實java這邊都是一樣的,只不過發起是在JS,關於這個JSBridge的demo的地址,可以在下面連結下載:
http://download.csdn.net/detail/xiangxiang_8_8/9885531
最後感謝我同事----科藍浩哥的耐心指導。