寫(xiě)在前面
看了下上一篇文章的寫(xiě)作日期,轉(zhuǎn)眼之間已經(jīng)過(guò)去了大半個(gè)月了……一個(gè)國(guó)慶小長(zhǎng)假下來(lái)也是放松了不少,不過(guò)學(xué)習(xí)也不能過(guò)于疏忽了。以前偷懶沒(méi)有看適配6.0 和 7.0的東西,最近在下也是正式的拋棄了大三時(shí)買的ip6,入了一臺(tái)小米mix2。mix2是基于Android 7.1的系統(tǒng)的,自己平時(shí)也喜歡用自己的手機(jī)調(diào)試應(yīng)用,各種沒(méi)有適配導(dǎo)致的崩潰自然也是免不了的。以前沒(méi)看權(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,文檔講的還是比較詳盡的,愛(ài)自己折騰的同學(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í),則需要通過(guò)動(dòng)態(tài)申請(qǐng)來(lái)獲取權(quán)限。當(dāng)然,考慮到一些兼容性問(wèn)題,項(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í)就無(wú)需申請(qǐng)了。接下來(lái)放一張危險(xiǎn)權(quán)限組及危險(xiǎn)權(quán)限的 截圖 ,markdown 制表還是挺麻煩的……偷個(gè)懶

動(dòng)態(tài)權(quán)限申請(qǐng)實(shí)操
前面簡(jiǎn)介寫(xiě)完了,下面開(kāi)始實(shí)操,申請(qǐng)權(quán)限主要分為以下幾個(gè)步驟:
- 檢查是否擁有權(quán)限
- 如果以前用戶拒絕過(guò),提示
- 申請(qǐng)權(quán)限
- 在回調(diào)中查看是否申請(qǐng)成功
首先是檢查和申請(qǐng)權(quán)限,雖然步驟是以上所述,但是代碼比較簡(jiǎn)單,就不一一拆開(kāi)了,代碼中的注釋都比較詳細(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)限,無(wú)需再申請(qǐng)", Toast.LENGTH_SHORT).show();
} else {
// 無(wú)此權(quán)限,申請(qǐng)權(quán)限
if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.CAMERA)) {
// 如果之前請(qǐng)求過(guò)權(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)限,無(wú)法使用相機(jī)功能 GG,請(qǐng)手動(dòng)打開(kāi)該權(quán)限", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
上面的 shouldShowRequestPermissionRationale 方法會(huì)返回一個(gè)布爾值,這個(gè)方法返回值的規(guī)則如下:
- 如果之前請(qǐng)求過(guò)此權(quán)限但用戶拒絕了請(qǐng)求,將返回 true
- 如果用戶在過(guò)去拒絕了請(qǐng)求,并在權(quán)限請(qǐng)求系統(tǒng)對(duì)話框中選擇了Don't ask again,此方法將返回false。
- 如果設(shè)備規(guī)范禁止應(yīng)用具有該權(quán)限,返回false
我這個(gè)代碼的基本流程和文檔中是一致的,后來(lái)在測(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)拒絕。所以我覺(jué)得正確的做法應(yīng)該是在回調(diào)中判斷權(quán)限被拒絕時(shí)使用shouldShowRequestPermissionRationale方法,如果返回false則打開(kāi)設(shè)置界面讓用戶去打開(kāi)權(quán)限。當(dāng)然,由于國(guó)產(chǎn)手機(jī)的各種系統(tǒng)定制。。。打開(kāi)權(quán)限設(shè)置界面可能并不是一個(gè)非常輕松的過(guò)程……需要適配……這里就不做這件事了。
RxPermission
上面的權(quán)限申請(qǐng)相對(duì)來(lái)說(shuō)還是比較繁瑣的,接下來(lái)介紹一下三方RxPermission,從Rx這倆字你應(yīng)該可以看出來(lái),這玩意是需要依賴RxJava的。不過(guò)對(duì)于現(xiàn)在的應(yīng)用開(kāi)發(fā)來(lái)說(shuō),RxJava,Okhttp幾乎都是標(biāo)配了,所以說(shuō)我覺(jué)得問(wèn)題不大,依賴如下:
// 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ù)制來(lái)的。。。好了,不要在意這些細(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)行這段代碼是沒(méi)問(wèn)題的,那么在 7.0 之后呢?

拋出了 FileUriExposedException 異常,恩,直接crash,心里相想必有一萬(wàn)頭草泥馬奔騰而過(guò)吧。解釋官網(wǎng)上也有,Android 7.0 行為變更,中這段話描述了原因:

行,你說(shuō)用什么就用什么。接下來(lái)先詳細(xì)的記錄一下操作過(guò)程,之后再解釋一下細(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>
接下來(lái)新建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è)小坑,操作流程寫(xiě)了,余下的具體解析待填。
好了,決定不填了,就是這么任性,哈哈哈哈哈哈哈。