Android 6.0 動(dòng)態(tài)權(quán)限申請(qǐng)與 7.0 適配 File Uri 小記

寫(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了

再見(jiàn)

扯犢子時(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è)懶

危險(xiǎn)權(quán)限組及危險(xiǎn)權(quán)限

動(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ě)了,余下的具體解析待填。
好了,決定不填了,就是這么任性,哈哈哈哈哈哈哈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容