一篇搞定Android M運(yùn)行時(shí)權(quán)限

由于本人能力有限匈辱,文中若有錯(cuò)誤之處谅辣,歡迎指正凄吏。
轉(zhuǎn)載請(qǐng)注明出處:http://www.reibang.com/p/d6b3e16cc1d9

從 Android 6.0(API 23)開(kāi)始铸敏,用戶開(kāi)始在應(yīng)用運(yùn)行時(shí)向其授予權(quán)限杈帐,而不是在應(yīng)用安裝時(shí)授予髓堪。這種權(quán)限機(jī)制可以讓用戶更好的管理應(yīng)用的權(quán)限,保障用戶隱私娘荡。

系統(tǒng)權(quán)限分為兩類(lèi):
  • 正常權(quán)限不會(huì)直接給用戶隱私權(quán)帶來(lái)風(fēng)險(xiǎn)。如果您的應(yīng)用在其清單中列出了正常權(quán)限驶沼,系統(tǒng)將自動(dòng)授予該權(quán)限炮沐。
  • 危險(xiǎn)權(quán)限會(huì)授予應(yīng)用訪問(wèn)用戶機(jī)密數(shù)據(jù)的權(quán)限。如果您列出了危險(xiǎn)權(quán)限回怜,則用戶必須明確批準(zhǔn)您的應(yīng)用使用這些權(quán)限大年。
危險(xiǎn)權(quán)限及權(quán)限組

需要注意的是:

  1. 在 Android 5.1(API 22)或更低版本,并且應(yīng)用的 targetSdkVersion 是 22 或更低版本玉雾,則系統(tǒng)會(huì)在安裝時(shí)要求用戶授予權(quán)限翔试。(沿用之前的權(quán)限系統(tǒng))
  2. 即使在安裝時(shí)已經(jīng)授予應(yīng)用所有權(quán)限,在Android 6.0之后依然可以通過(guò) "Setting" 來(lái)關(guān)閉已經(jīng)授予的權(quán)限复旬。
  3. 在請(qǐng)求權(quán)限時(shí)垦缅,系統(tǒng)只告訴用戶應(yīng)用需要的權(quán)限組,而不告知具體權(quán)限驹碍。
  4. 如果在未檢查授權(quán)的情況下壁涎,直接使用危險(xiǎn)權(quán)限凡恍,會(huì)導(dǎo)致程序Crash。
  5. 使用 v4 包中的 ContextCompat 處理權(quán)限(v13 包中的FragmentCompat)怔球,不需要考慮版本問(wèn)題嚼酝。

相關(guān)API

  • int checkSelfPermission()

檢查應(yīng)用是否有指定權(quán)限。返回值為 PackageManager.PERMISSION_GRANTED 表示有權(quán)限竟坛, PackageManager.PERMISSION_DENIED 表示無(wú)權(quán)限闽巩。

  • void requestPermissions()

請(qǐng)求指定權(quán)限,可以是多個(gè)担汤,以數(shù)組的方式涎跨。

  • boolean shouldShowRequestPermissionRationale()

如果應(yīng)用之前請(qǐng)求過(guò)此權(quán)限但用戶拒絕了請(qǐng)求,此方法將返回 true漫试。

  • void onRequestPermissionsResult()

請(qǐng)求權(quán)限的結(jié)果回調(diào)六敬。

使用原生API

因?yàn)橐陨狭信e的相關(guān)API都是在 API 23 才有的,為了適配低版本驾荣,官方提供了 v4 v13 兼容包外构。我們可以直接使用兼容包中的方法進(jìn)行權(quán)限處理。

步驟(以撥打電話為例)
  • 還是和以前一樣播掷,先在清單文件中申請(qǐng)所需要的權(quán)限审编。
<uses-permission android:name="android.permission.CALL_PHONE"/>
  • 在使用到撥打電話的地方,進(jìn)行權(quán)限檢查
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
        != PackageManager.PERMISSION_GRANTED) {
    // 應(yīng)用沒(méi)有授予撥打電話權(quán)限歧匈,請(qǐng)求權(quán)限
    requestCameraPermission();
} else {
    // 應(yīng)用被授予撥打電話權(quán)限 PackageManager.PERMISSION_GRANTED
    makeCall();
}
  • 如果有權(quán)限垒酬,直接撥打電話,至此結(jié)束件炉。
  • 如果沒(méi)有權(quán)限勘究,則請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CALLPHONE);
  • 在請(qǐng)求權(quán)限過(guò)程中可以使用shouldShowRequestPermissionRationale()檢查是否被拒絕過(guò),如果被拒絕過(guò)斟冕,可以給用戶一個(gè)詳細(xì)解釋口糕。
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
    // 向用戶詳細(xì)解釋申請(qǐng)?jiān)摍?quán)限的原因
    new AlertDialog.Builder(this)
            .setCancelable(false)
            .setMessage("撥打電話需要使用電話權(quán)限,如果不授予權(quán)限會(huì)導(dǎo)致該功能無(wú)法正常使用")
            .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    ActivityCompat.requestPermissions(
                            OriginalActivity.this,
                            new String[]{Manifest.permission.CALL_PHONE},
                            REQUEST_CALLPHONE
                    );
                }
            })
            .setNegativeButton("不給", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            })
            .show();
} 
  • 處理授權(quán)結(jié)果回調(diào)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                   @NonNull int[] grantResults) {

    if (requestCode == REQUEST_CALLPHONE) {
        if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 授予權(quán)限,撥打電話
            makeCall();
        } else {
            Toast.makeText(this, "請(qǐng)求權(quán)限被拒絕", Toast.LENGTH_SHORT).show();
        }
    } else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

使用輪子

在處理運(yùn)行時(shí)權(quán)限的時(shí)候磕蛇,雖然官方提供了兼容包不再需要做版本檢查景描,但處理起來(lái)依然使代碼很雜亂。現(xiàn)在已經(jīng)出現(xiàn)了很多處理運(yùn)行時(shí)權(quán)限的開(kāi)源庫(kù)秀撇,這里給大家推薦 PermissionsDispatcher超棺。該庫(kù)在GitHub同比獲得 star 最多。而且使用 apt 技術(shù)呵燕,在編譯時(shí)期動(dòng)態(tài)生成xxxxPermissionsDispatcher模板代碼棠绘,效率很高!

API 簡(jiǎn)介

該庫(kù)使用 apt 技術(shù),自然使用的就是注解弄唧。

注解 是否必須 作用
@RuntimePermissions 標(biāo)記Activity/Fragment适肠,則注解解釋器會(huì)生成對(duì)應(yīng)類(lèi)的代碼
@NeedsPermission 標(biāo)記需要授權(quán)才能執(zhí)行的方法
@OnShowRationale 對(duì)應(yīng)shouldShowRequestPermissionRationale(),當(dāng)應(yīng)用之前請(qǐng)求過(guò)此權(quán)限但用戶拒絕了請(qǐng)求候引,再次請(qǐng)求時(shí)調(diào)用
@OnPermissionDenied 當(dāng)請(qǐng)求權(quán)限遭拒絕時(shí)調(diào)用
@OnNeverAskAgain 當(dāng)用戶勾選不再提示侯养,并拒絕權(quán)限時(shí),再次請(qǐng)求時(shí)調(diào)用
步驟(以使用相機(jī)為例)
  • 還是在清單文件中聲明使用的權(quán)限
<uses-permission android:name="android.permission.CAMERA" />
  • 配置依賴 PermissionsDispatcher澄干,這里不再贅述
  • 代碼示例
@RuntimePermissions
public class PermissionsDispatcherActivity extends AppCompatActivity {

    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView = (ImageView) findViewById(R.id.imageView);

        findViewById(R.id.btn_camera).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PermissionsDispatcherActivityPermissionsDispatcher.takePhotoWithCheck(PermissionsDispatcherActivity.this);
            }
        });

    }

    @NeedsPermission(Manifest.permission.CAMERA)
    void takePhoto() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 啟動(dòng)系統(tǒng)相機(jī)
        startActivityForResult(intent, 100);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) { // 如果返回?cái)?shù)據(jù)
            if (requestCode == 100) { // 判斷請(qǐng)求碼是否為REQUEST_CAMERA,如果是代表是這個(gè)頁(yè)面?zhèn)鬟^(guò)去的逛揩,需要進(jìn)行獲取
                Bundle bundle = data.getExtras(); // 從data中取出傳遞回來(lái)縮略圖的信息,圖片質(zhì)量差麸俘,適合傳遞小圖片
                Bitmap bitmap = (Bitmap) bundle.get("data"); // 將data中的信息流解析為Bitmap類(lèi)型
                imageView.setImageBitmap(bitmap);// 顯示圖片
            }
        }
    }

    @OnShowRationale(Manifest.permission.CAMERA)
    void showRationaleForRecord(final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.proceed();
                    }
                })
                .setNegativeButton("不給", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage("拍照需要相機(jī)權(quán)限辩稽,應(yīng)用將要申請(qǐng)使用相機(jī)權(quán)限")
                .show();
    }

    @OnPermissionDenied(Manifest.permission.CAMERA)
    void showCameraDenied() {
        Toast.makeText(getApplicationContext(), "權(quán)限被拒絕", Toast.LENGTH_LONG).show();
    }

    @OnNeverAskAgain(Manifest.permission.CAMERA)
    void onRCameraNeverAskAgain() {
        new AlertDialog.Builder(this)
                .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 打開(kāi)系統(tǒng)應(yīng)用設(shè)置
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        intent.addCategory(Intent.CATEGORY_DEFAULT);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                        dialog.cancel();
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage("您已經(jīng)禁止了相機(jī)權(quán)限,是否現(xiàn)在去開(kāi)啟")
                .show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PermissionsDispatcherActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }
}
使用注意
  • 注解的方法不能是private
  • 在同一 Activity/Fragment 中可以多次使用以上注解,但是同一組權(quán)限處理中注解的value的值應(yīng)該相同从媚。
  • AS 中可以配合 PermissionsDispatcher plugin 插件一起使用逞泄。

總結(jié)與建議

  1. 請(qǐng)求權(quán)限顯示的是標(biāo)準(zhǔn)Android對(duì)話框,我們不能自定義拜效。
  2. targetSdkVersion 設(shè)置為 22 或更低版本只是權(quán)宜之計(jì)喷众。作為App開(kāi)發(fā)者,需要盡快適配新權(quán)限機(jī)制紧憾。
  3. 在某個(gè)功能模塊嚴(yán)重依賴某些權(quán)限的情況下到千,為了減少程序中出現(xiàn)過(guò)多權(quán)限檢查,可以在該模塊入口處統(tǒng)一檢查赴穗,如果沒(méi)有授予相應(yīng)權(quán)限憔四,則不提供該模塊使用。

文中的所有代碼以上傳至github
RuntimePermissionDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末般眉,一起剝皮案震驚了整個(gè)濱河市了赵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌甸赃,老刑警劉巖柿汛,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辑奈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)已烤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)鸠窗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人胯究,你說(shuō)我怎么就攤上這事稍计。” “怎么了裕循?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵臣嚣,是天一觀的道長(zhǎng)净刮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)硅则,這世上最難降的妖魔是什么淹父? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮怎虫,結(jié)果婚禮上暑认,老公的妹妹穿的比我還像新娘。我一直安慰自己大审,他們只是感情好蘸际,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著徒扶,像睡著了一般粮彤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上姜骡,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天导坟,我揣著相機(jī)與錄音,去河邊找鬼溶浴。 笑死乍迄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的士败。 我是一名探鬼主播闯两,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谅将!你這毒婦竟也來(lái)了漾狼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饥臂,失蹤者是張志新(化名)和其女友劉穎逊躁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體隅熙,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稽煤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了囚戚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酵熙。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖驰坊,靈堂內(nèi)的尸體忽然破棺而出匾二,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布察藐,位于F島的核電站皮璧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏分飞。R本人自食惡果不足惜悴务,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浸须。 院中可真熱鬧惨寿,春花似錦、人聲如沸删窒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肌索。三九已至蕉拢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诚亚,已是汗流浹背晕换。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留站宗,地道東北人闸准。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像梢灭,于是被迫代替她去往敵國(guó)和親夷家。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理敏释,服務(wù)發(fā)現(xiàn)库快,斷路器,智...
    卡卡羅2017閱讀 134,656評(píng)論 18 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,111評(píng)論 25 707
  • LZ-Says:江湖上流傳著這樣一首詩(shī): 床前明月光,我會(huì)寫(xiě)代碼财喳;千山鳥(niǎo)飛絕察迟,我會(huì)寫(xiě)代碼;松下問(wèn)童子耳高,我會(huì)寫(xiě)代碼...
    靜心Study閱讀 904評(píng)論 0 1
  • 我連著看“高海拔之戀”和“山楂樹(shù)之戀”兩部電影扎瓶,我回憶起一段已逝去很遙遠(yuǎn)的,走在懸崖邊上的生命之戀…… 這天泌枪,參加...
    奔奔閱讀 264評(píng)論 0 1
  • 每天早上9點(diǎn)開(kāi)始上班,晚上9點(diǎn)才能下班修壕,路途遙遠(yuǎn)的11點(diǎn)才能進(jìn)了家門(mén)愈捅,這么辛苦的奔波,為啥體重不減反增慈鸠,徹底成了一...
    嘿我是俠俠閱讀 1,495評(píng)論 13 6