Android6.0適配
從Android 6.0 MarshMallow開始泰佳,Android支持動態(tài)權(quán)限管理,即有些權(quán)限需要在使用到的時候動態(tài)申請。
這些權(quán)限會關(guān)系到用戶的隱私或影響到其他應(yīng)用的運行,這些危險權(quán)限,谷歌還做了一個權(quán)限組凰盔,以分組的形式來呈現(xiàn):
[圖片上傳失敗...(image-3f71a8-1642080911915)]
由于運行權(quán)限機制的出現(xiàn),變得我們需要對新開發(fā)的應(yīng)用去做適配倦春,當(dāng)然有人會問户敬,那我之前開發(fā)的老應(yīng)用不就完蛋了,是不是運行到6.0系統(tǒng)上就會發(fā)生各種崩潰睁本?
其實不會的尿庐,谷歌做了良好的適配:
當(dāng)你的應(yīng)用targetSdkVersion小于23的時候,就算你運行在Android6.0系統(tǒng)上呢堰,它也會默認(rèn)采用以前的權(quán)限管理機制抄瑟,也就是一刀切。當(dāng)你的targetSdkVersion大于等于23的時候且在Andorid6.0(M)系統(tǒng)上枉疼,它才會采用新的這套權(quán)限管理機制皮假。
所以如果你想逃開這個“麻煩”鞋拟,只要把targetSdkVersion的版本設(shè)置為低于23就可以了,不過不建議采用這種方案惹资。
當(dāng)你的targetSdkVersion大于等于23的時候且在Andorid6.0(M)系統(tǒng)上贺纲,若不做適配,程序則會崩潰褪测,報錯信息如下:
01-16 02:23:46.181 E/CrashHandler Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA
java.lang.SecurityException: Permission Denial: starting Intent { act=android.media.action.IMAGE_CAPTURE flg=0x3 cmp=com.android.camera/.Camera clip={text/uri-list U:file:///storage/emulated/0/teacher/head.jpg} (has extras) } from ProcessRecord{f7b731d 16568:com.lanxum.diagnosis.teacher.hg/u0a71} (pid=16568, uid=10071) with revoked permission android.permission.CAMERA
說了這么多猴誊,那么來看下怎么進行Android6.0(M)的權(quán)限管理適配吧,其實很簡單侮措,只需要記住下面幾個API方法就可以:(API23之后提供懈叹,support-v4包下也有提供)
int checkSelfPermission(String permission) 用來檢測應(yīng)用是否已經(jīng)具有權(quán)限
void requestPermissions(String[] permissions, int requestCode) 進行請求單個或多個權(quán)限
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 請求權(quán)限結(jié)果回調(diào)
[圖片上傳失敗...(image-193991-1642080911916)]
以評測教師端app修改頭像功能模塊為例,該操作需要拍照和讀寫sd卡權(quán)限
下面來段代碼示例(當(dāng)用戶點擊照相機時)
//判斷當(dāng)前系統(tǒng)是否高于或等于6.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//通過調(diào)用support-v4包提供的方法
if (ContextCompat.checkSelfPermission(getActivity(), android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
//具有拍照權(quán)限和讀寫SD卡權(quán)限分扎,直接調(diào)用相機
//具體調(diào)用代碼
takePhoto();
} else {
//不具有拍照權(quán)限和讀寫SD卡權(quán)限澄成,需要進行權(quán)限申請;在Fragment中申請權(quán)限笆包,不可以通過ActivityCompat.requestPermissions調(diào)用,直接使用Fragment的requestPermissions方法略荡,否則會回調(diào)到Activity的onRequestPermissionsResult
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_CAMERA_CODE);
}
} else {
//當(dāng)前系統(tǒng)小于6.0庵佣,直接調(diào)用拍照
takePhoto();
}
private void takePhoto() {
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u = Uri.fromFile(f);
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
}
REQUEST_PERMISSION_CAMERA_CODE
是個標(biāo)識碼,類似Intent跳轉(zhuǎn)的REQUEST_CODE的汛兜,然后我們就可以在onRequestPermissionsResult進行權(quán)限申請的回調(diào)處理:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_CAMERA_CODE) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.CAMERA) || !ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)) {
////如果用戶選擇了不再提示系統(tǒng)授權(quán)彈框,則自定義對話框引導(dǎo)用戶進行授權(quán)
showMissingPermissionDialog();
return;
}
if (grantResults.length >= 2) {
int cameraResult = grantResults[0];
int storageResult = grantResults[1];
if (cameraResult == PackageManager.PERMISSION_GRANTED && storageResult == PackageManager.PERMISSION_GRANTED) {
takePhoto();
}
}
}
}
private void showMissingPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.help);
builder.setMessage(R.string.string_help_text);
// 關(guān)閉對話框
builder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startAppSettings();
}
});
builder.show();
}
// 啟動應(yīng)用的設(shè)置
private void startAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getActivity().getPackageName()));
startActivity(intent);
}
[圖片上傳失敗...(image-909967-1642080911916)]
注:shouldShowRequestPermissionRationale方法:當(dāng)app完全沒有機會被授權(quán)的時候巴粪,比如用戶在權(quán)限對話框中選擇了"不再顯示”,調(diào)用shouldShowRequestPermissionRationale() 則返回false粥谬,這種情況下肛根,我們可以通過自定義對話框引導(dǎo)用戶進行授權(quán),如下圖
[圖片上傳失敗...(image-30ea9-1642080911916)]
Android7.0適配
在Android7.0版本上漏策,Android系統(tǒng)強制執(zhí)行了StrictMode API 政策派哲,禁止向你的應(yīng)用外公開File://URI。如果一項包含文件File://URI類型的Intent離開你的應(yīng)用掺喻,應(yīng)用失敗芭届,并出現(xiàn)FileUriExposedException異常,比如系統(tǒng)相機拍照感耙,裁剪照片
經(jīng)測試褂乍,F(xiàn)ile api在應(yīng)用內(nèi)讀取文件存儲依然可以繼續(xù)使用,應(yīng)用間(主要指調(diào)用部分系統(tǒng)應(yīng)用)進行共享會直接報錯即硼。由于評測教師端裁剪照片功能通過自定義的ClipHeaderActivity類來實現(xiàn)逃片,屬于應(yīng)用內(nèi),所以不會報錯
在Android7.0之前只酥,如果你想調(diào)用系統(tǒng)相機拍照可以通過以下代碼來進行:
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u = Uri.fromFile(f);
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
應(yīng)對策略:若要在應(yīng)用間共享文件褥实,可以發(fā)送 content:// URI類型的Uri呀狼,并授予 URI 臨時訪問權(quán)限。 進行此授權(quán)的最簡單方式是使用 FileProvider類
使用FileProvider
使用FileProvider的大致步驟如下:
第一步:在manifest清單文件中注冊provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${FILE_PROVIDER}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
注:${FILE_PROVIDER}的值定義在主工程app目錄下的build.gradle文件內(nèi)性锭,不同渠道的app需要配置不同的值赠潦,否則會和多渠道打包沖突
productFlavors {
ningxia {
applicationId "com.lanxum.diagnosis.teacher.nx"
resValue "string", "app_name", "全優(yōu)學(xué)教師-寧夏正式版"
buildConfigField "String", "PREFIX_URL", "\"http://218.95.173.132\""
buildConfigField "int", "VERSION", "2"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nx.takephoto.fileprovider"]
}
ningxiatest {
applicationId "com.lanxum.diagnosis.teacher.nxtest"
resValue "string", "app_name", "全優(yōu)學(xué)教師-寧夏測試版"
buildConfigField "String", "PREFIX_URL", "\"http://223.202.64.196:8084\""
buildConfigField "int", "VERSION", "2"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.nxtest.takephoto.fileprovider"]
}
huanggang {
applicationId "com.lanxum.diagnosis.teacher.hg"
resValue "string", "app_name", "全優(yōu)學(xué)教師-黃岡版"
buildConfigField "String", "PREFIX_URL", "\"http://221.235.188.44:51311\""
buildConfigField "int", "VERSION", "1"
buildConfigField "String","FILE_PROVIDER","\"com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider\""
manifestPlaceholders = [FILE_PROVIDER: "com.lanxum.diagnosis.teacher.hg.takephoto.fileprovider"]
}
}
第二步:指定共享的目錄
為了指定共享的目錄我們需要在資源(res)目錄下創(chuàng)建一個xml目錄,然后創(chuàng)建一個名為“file_paths”(名字可以隨便起草冈,只要和在manifest注冊的provider所引用的resource保持一致即可)的資源文件她奥,內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="camera_photos" />
</paths>
</resources>
<files-path/>代表的根目錄: Context.getFilesDir()
<external-path/>代表的根目錄: Environment.getExternalStorageDirectory()
<cache-path/>代表的根目錄: getCacheDir()
注:上述代碼中path="",是有特殊意義的怎棱,它代碼根目錄哩俭,也就是說你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個文件了,如果你將path設(shè)為path="pictures"拳恋,
那么它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures)凡资,如果你向其它應(yīng)用分享pictures目錄范圍之外的文件是不行的。
第三步:使用FileProvider
上述準(zhǔn)備工作做完之后谬运,現(xiàn)在我們就可以使用FileProvider了隙赁。
還是以調(diào)用系統(tǒng)相機拍照為例,我們需要將上述拍照代碼修改為如下:
//照相
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String externalStorageState = Environment.getExternalStorageState();
if (externalStorageState.equals(Environment.MEDIA_MOUNTED)) {
File dir = new File(Environment.getExternalStorageDirectory() + "/teacher");
if (!dir.exists()) dir.mkdir();
File f = new File(dir, "head.jpg");
Uri u;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
u = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER, f);
} else {
u = Uri.fromFile(f);
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, u);
getActivity().startActivityForResult(intent, RESULT_CAPTURE);
}
以上內(nèi)容均為參考以下鏈接梆暖,上述示例代碼我也只在模擬器上測試過伞访,尚未在真機上測試,可能還存在不兼容和待優(yōu)化的地方轰驳,想要了解更詳細具體的內(nèi)容厚掷,可以直接查看以下鏈接地址
參考鏈接:
http://www.reibang.com/p/56b9fb319310
http://www.reibang.com/p/a37f4827079a
http://www.reibang.com/p/feb6dd8d2212
http://www.reibang.com/p/d3a998ec04ad
https://www.aswifter.com/2015/11/04/android-6-permission/