寫在前面
看了下上一篇文章的寫作日期晶默,轉(zhuǎn)眼之間已經(jīng)過去了大半個(gè)月了……一個(gè)國(guó)慶小長(zhǎng)假下來也是放松了不少臀防,不過學(xué)習(xí)也不能過于疏忽了箱锐。以前偷懶沒有看適配6.0 和 7.0的東西,最近在下也是正式的拋棄了大三時(shí)買的ip6埋同,入了一臺(tái)小米mix2州叠。mix2是基于Android 7.1的系統(tǒng)的,自己平時(shí)也喜歡用自己的手機(jī)調(diào)試應(yīng)用凶赁,各種沒有適配導(dǎo)致的崩潰自然也是免不了的咧栗。以前沒看權(quán)限適配一是因?yàn)?.0以上的系統(tǒng)覆蓋率不是很高,二也是因?yàn)槟菚r(shí)候要看的東西很多虱肄,這東西不是很急迫致板,現(xiàn)在各個(gè)廠商的手機(jī)出廠系統(tǒng)都是6.0以上的了,也是時(shí)候看一下了咏窿。本文包括以下內(nèi)容:
- Android 6.0 動(dòng)態(tài)權(quán)限申請(qǐng)
- RxPermission
- Android 7.0 File Uri 導(dǎo)致的崩潰以及如何適配
Android 6.0 動(dòng)態(tài)權(quán)限申請(qǐng)
這里先推薦一波官方的文檔:https://developer.android.com/training/permissions/requesting.html#perm-request斟或,文檔講的還是比較詳盡的,愛自己折騰的同學(xué)(比如我)看到這應(yīng)該say goodbye了
扯犢子時(shí)間到此為止集嵌,繼續(xù)正文萝挤。系統(tǒng)權(quán)限分為幾個(gè)保護(hù)級(jí)別御毅,這里需要了解的是兩個(gè)保護(hù)級(jí)別是正常權(quán)限和危險(xiǎn)權(quán)限。正常權(quán)限只要應(yīng)用聲明了怜珍,系統(tǒng)就會(huì)自動(dòng)給應(yīng)用該權(quán)限端蛆。而危險(xiǎn)權(quán)限在Android 6.0 及以上時(shí),則需要通過動(dòng)態(tài)申請(qǐng)來獲取權(quán)限酥泛。當(dāng)然今豆,考慮到一些兼容性問題,項(xiàng)目的 targetSdkVersion <= 22 時(shí)柔袁,并不需要?jiǎng)討B(tài)申請(qǐng)權(quán)限呆躲。但適配是早晚要去適配的。捶索。歼秽。躲也躲不掉,還是先了解下為妙情组。任何權(quán)限都可以屬于一個(gè)權(quán)限組燥筷,危險(xiǎn)權(quán)限也有自己的組別,當(dāng)你請(qǐng)求了某組權(quán)限中的某個(gè)權(quán)限成功院崇,那么該組的其他權(quán)限系統(tǒng)也將授予肆氓。比如申請(qǐng)了 STORAGE 權(quán)限組的 READ_EXTERNAL_STORAGE 權(quán)限,那么該組的 WRITE_EXTERNAL_STORAGE 權(quán)限在使用時(shí)就無需申請(qǐng)了底瓣。接下來放一張危險(xiǎn)權(quán)限組及危險(xiǎn)權(quán)限的 截圖
谢揪,markdown 制表還是挺麻煩的……偷個(gè)懶
動(dòng)態(tài)權(quán)限申請(qǐng)實(shí)操
前面簡(jiǎn)介寫完了,下面開始實(shí)操捐凭,申請(qǐng)權(quán)限主要分為以下幾個(gè)步驟:
- 檢查是否擁有權(quán)限
- 如果以前用戶拒絕過拨扶,提示
- 申請(qǐng)權(quán)限
- 在回調(diào)中查看是否申請(qǐng)成功
首先是檢查和申請(qǐng)權(quán)限,雖然步驟是以上所述茁肠,但是代碼比較簡(jiǎn)單患民,就不一一拆開了,代碼中的注釋都比較詳細(xì)垦梆。
int permission = ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.CAMERA);
if (permission == PackageManager.PERMISSION_GRANTED) {
// 有此權(quán)限
Toast.makeText(MainActivity.this, "已經(jīng)具有該權(quán)限匹颤,無需再申請(qǐng)", Toast.LENGTH_SHORT).show();
} else {
// 無此權(quán)限,申請(qǐng)權(quán)限
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.CAMERA)) {
// 如果之前請(qǐng)求過權(quán)限但用戶拒絕了請(qǐng)求
Toast.makeText(MainActivity.this, "請(qǐng)求相機(jī)權(quán)限,將用于拍照", Toast.LENGTH_SHORT).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}, 500);
} else {
// 請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
// 回調(diào)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CAMERA:
// 如果請(qǐng)求被取消了托猩,result數(shù)組將是空的
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 成功申請(qǐng)?jiān)摍?quán)限
Toast.makeText(this, "成功申請(qǐng)相機(jī)權(quán)限", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "您已拒絕該權(quán)限印蓖,無法使用相機(jī)功能 GG,請(qǐng)手動(dòng)打開該權(quán)限", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
上面的 shouldShowRequestPermissionRationale
方法會(huì)返回一個(gè)布爾值京腥,這個(gè)方法返回值的規(guī)則如下:
- 如果之前請(qǐng)求過此權(quán)限但用戶拒絕了請(qǐng)求赦肃,將返回 true
- 如果用戶在過去拒絕了請(qǐng)求,并在權(quán)限請(qǐng)求系統(tǒng)對(duì)話框中選擇了Don't ask again,此方法將返回false他宛。
- 如果設(shè)備規(guī)范禁止應(yīng)用具有該權(quán)限船侧,返回false
我這個(gè)代碼的基本流程和文檔中是一致的,后來在測(cè)試的時(shí)候發(fā)現(xiàn)第一次請(qǐng)求也會(huì)走false那個(gè)選項(xiàng)堕汞,如果我拒絕了這個(gè)權(quán)限勺爱,后續(xù)就不會(huì)再有權(quán)限申請(qǐng)彈窗了晃琳,在申請(qǐng)權(quán)限的時(shí)候系統(tǒng)會(huì)自動(dòng)拒絕讯检。所以我覺得正確的做法應(yīng)該是在回調(diào)中判斷權(quán)限被拒絕時(shí)使用shouldShowRequestPermissionRationale
方法,如果返回false則打開設(shè)置界面讓用戶去打開權(quán)限卫旱。當(dāng)然人灼,由于國(guó)產(chǎn)手機(jī)的各種系統(tǒng)定制。顾翼。投放。打開權(quán)限設(shè)置界面可能并不是一個(gè)非常輕松的過程……需要適配……這里就不做這件事了。
RxPermission
上面的權(quán)限申請(qǐng)相對(duì)來說還是比較繁瑣的适贸,接下來介紹一下三方RxPermission灸芳,從Rx這倆字你應(yīng)該可以看出來,這玩意是需要依賴RxJava的拜姿。不過對(duì)于現(xiàn)在的應(yīng)用開發(fā)來說烙样,RxJava,Okhttp幾乎都是標(biāo)配了蕊肥,所以說我覺得問題不大谒获,依賴如下:
// rxpermission
compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
// rxjava
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
// rxandroid
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
申請(qǐng)權(quán)限代碼如下:
RxPermissions rxPermissions = new RxPermissions(MainActivity.this);
rxPermissions.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.subscribe(new Observer<Boolean>() {
@Override
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
}
@Override
public void onNext(@io.reactivex.annotations.NonNull Boolean isGranted) {
if (isGranted) {
Toast.makeText(MainActivity.this, "權(quán)限申請(qǐng)成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "權(quán)限申請(qǐng)失敗,用戶拒絕了此權(quán)限", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(@io.reactivex.annotations.NonNull Throwable e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "權(quán)限申請(qǐng)失敗壁却,代碼異常", Toast.LENGTH_SHORT).show();
}
@Override
public void onComplete() {
}
});
當(dāng)然了批狱,如果你希望在權(quán)限被拒絕時(shí)進(jìn)行進(jìn)一步的操作,可以使用如下代碼:
rxPermissions
.requestEach(Manifest.permission.CAMERA,
Manifest.permission.READ_PHONE_STATE)
.subscribe(permission -> { // will emit 2 Permission objects
if (permission.granted) {
// `permission.name` is granted !
} else if (permission.shouldShowRequestPermissionRationale)
// Denied permission without ask never again
} else {
// Denied permission with ask never again
// Need to go to the settings
}
});
恩展东,是的赔硫,下面一段代碼是從倉(cāng)庫(kù)上復(fù)制來的。盐肃。卦停。好了,不要在意這些細(xì)節(jié)恼蓬,接著看一下關(guān)于File uri 在 Android N 上引發(fā)異常的適配惊完。
7.0 適配 File Uri
這里拿拍照作為例子,平時(shí)我們調(diào)用系統(tǒng)相機(jī)實(shí)現(xiàn)拍照功能代碼如下:
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Toast.makeText(MainActivity.this, "SD卡狀態(tài)異常", Toast.LENGTH_SHORT).show();
return;
}
long dateTaken = System.currentTimeMillis();
// 圖像名稱
CharSequence fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken);
// 圖像路徑
String path = Environment.getExternalStorageDirectory().toString() +
File.separator + "forpermission" + File.separator + fileName + ".jpg";
File imageFile = new File(path);
if (!imageFile.getParentFile().exists()) {
imageFile.getParentFile().mkdirs();
}
if (!imageFile.exists()) try {
imageFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
// 根據(jù)文件解析出文件對(duì)應(yīng)的Uri
Uri uri = Uri.fromFile(imageFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
// 判斷是否有 Activity 能處理 intent
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_TAKE_PHOTO);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode == RESULT_OK){
if(requestCode == REQUEST_TAKE_PHOTO){
Toast.makeText(this, "拍照成功", Toast.LENGTH_SHORT).show();
}
}
}
在 7.0 以前運(yùn)行這段代碼是沒問題的处硬,那么在 7.0 之后呢小槐?
拋出了 FileUriExposedException 異常,恩,直接crash凿跳,心里相想必有一萬頭草泥馬奔騰而過吧件豌。解釋官網(wǎng)上也有,Android 7.0 行為變更控嗜,中這段話描述了原因:
行茧彤,你說用什么就用什么。接下來先詳細(xì)的記錄一下操作過程疆栏,之后再解釋一下細(xì)節(jié)曾掂。首先在清單文件中聲明:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.xiasuhuei321.studyforpermission.takePhotoN"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
接下來新建xml包并新建file_paths xml文件:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path
name="root"
path="."/>
<external-files-path
name="camera_photo"
path="/storage/emulated/0/forpermission/" />
</paths>
拍照代碼:
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Toast.makeText(MainActivity.this, "SD卡狀態(tài)異常", Toast.LENGTH_SHORT).show();
return;
}
long dateTaken = System.currentTimeMillis();
// 圖像名稱
CharSequence fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken);
// 圖像路徑
String path = Environment.getExternalStorageDirectory().toString() +
File.separator + "forpermission" + File.separator + fileName + ".jpg";
File imageFile = new File(path);
if (!imageFile.getParentFile().exists()) {
imageFile.getParentFile().mkdirs();
}
if (!imageFile.exists()) try {
imageFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
// 根據(jù)文件解析出文件對(duì)應(yīng)的Uri
Uri uri = FileProvider.getUriForFile(MainActivity.this,
"com.xiasuhuei321.studyforpermission.takePhotoN", imageFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
// 判斷是否有 Activity 能處理 intent
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_TAKE_PHOTO);
}
恩,留個(gè)小坑壁顶,操作流程寫了珠洗,余下的具體解析待填。
好了若专,決定不填了许蓖,就是這么任性,哈哈哈哈哈哈哈调衰。