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

寫在前面

看了下上一篇文章的寫作日期晶默,轉(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è)懶

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

動(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è)小坑壁顶,操作流程寫了珠洗,余下的具體解析待填。
好了若专,決定不填了许蓖,就是這么任性,哈哈哈哈哈哈哈调衰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膊爪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嚎莉,更是在濱河造成了極大的恐慌米酬,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萝喘,死亡現(xiàn)場(chǎng)離奇詭異淮逻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)阁簸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門爬早,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人启妹,你說我怎么就攤上這事筛严。” “怎么了饶米?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵桨啃,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我檬输,道長(zhǎng)照瘾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任丧慈,我火速辦了婚禮析命,結(jié)果婚禮上主卫,老公的妹妹穿的比我還像新娘。我一直安慰自己鹃愤,他們只是感情好簇搅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著软吐,像睡著了一般瘩将。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凹耙,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天姿现,我揣著相機(jī)與錄音,去河邊找鬼使兔。 笑死建钥,一個(gè)胖子當(dāng)著我的面吹牛藤韵,可吹牛的內(nèi)容都是我干的虐沥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼泽艘,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼欲险!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匹涮,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤天试,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后然低,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喜每,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年雳攘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了带兜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吨灭,死狀恐怖刚照,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情喧兄,我是刑警寧澤无畔,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站吠冤,受9級(jí)特大地震影響浑彰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拯辙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一郭变、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦饵较、人聲如沸拍嵌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)横辆。三九已至,卻和暖如春茄猫,著一層夾襖步出監(jiān)牢的瞬間狈蚤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工划纽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脆侮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓勇劣,卻偏偏與公主長(zhǎng)得像靖避,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子比默,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容