Android进阶——Android6.0 动态权限详解及动态申请权限的完全攻略

引言

Android系统虽然开源,但是相对还是比较安全的,尤其是高版本的系统,这得益于Android系统自身的安全机制,其中权限管理机制一直是首要的安全概念,Android 动态权限又叫运行时权限已经面世很久了,网上很多文章都是只写了用法,不客气地说只是告诉了怎么用,具体的机制并没有很完整,让一些初学者只知其然而不知其所然,对于动态权限并没有完全掌握,于是我就想结合自己的项目经验和官方的文档,一篇文章把重要关于动态的知识点都总结出来,当然不是所有的,比如说权限组的实际操作等等。

一、Android系统权限机制概述

我们知道在Android的权限系统一直是首要的安全概念,因为这些权限在Android M(6.0)之前在AndroidManifest文件中声明之后,仅App在安装的时候被询问一次,安装成功之后运行,就可以在用户毫不知晓的情况下访问权限内的内容,毫无顾忌地收集信息(虽然现在也还是可以在一次申请之后无顾忌的使用)。而在Android M之后,app将不会在安装的时候授予权限。App不得不在运行时一个一个询问用户授予权限,系统权限被按敏感级别分组(normal、signature、dangerous、privileged、signature|privileged)并且敏感权限必须在运行的时候动态申请,并且随时可以在设置了取消已经授权的权限,又可以为两大类:普通权限和危险权限,在M手机上,对于敏感权限,需要在程序运行时进行动态申请。对于非敏感权限,即Normal Permissions,和M之前的使用相同。如果APP是在Android 5.1 或更低版本的设备上运行,或者APP的targetSdkVersion为 22 或更低时,在清单中列出了危险权限,则用户必须在安装应用时授予此权限,若不授予此权限,系统则不会安装。而APP运行在 Android 6.0 或更高版本的设备上,或者应用的目标 SDK 为 23 或更高,应用必须在清单中列出所有权限,并且它必须在运行时动态申请危险权限,用户随时可以授予或拒绝每项权限。值得注意的是:从 Android 6.0(API 级别 23)开始,用户可以随时从任意应用调用权限,即使应用面向较低的 API 级别也可以调用,所以无论APP面向哪个 API 级别,都应对应用进行测试,以验证它在缺少需要的权限时行为是否正常。

二、Android M权限机制

1、Normal级别的权限只需要在AndroidManifest中声明就好,安装时就授权,不需要每次使用时都检查权限,而且用户不能取消以上授权

这里写图片描述

2、其他级别的权限在编译在Android M(即targetSdkVersion大于等于23时候)版本时候,不仅需要在AndroidManifest中声明,还得在运行的时候需要动态申请,而且还可以随时取消授权

  • 先在AndroidManifest中声明
  • 再在运行的时候动态申请

3、Android6.0之后权限组管理

同一组的任何一个权限被授权了,其他权限也自动被授权。比如说一旦WRITE_CONTACTS被授权了,app也有READ_CONTACTS和GET_ACCOUNTS了。在api23中通过Activity的checkSelfPermission和requestPermissions来检查和请求权限的方法。(或者可以使用( compile 'com.android.support:support-v4:25.2.0')v4库中的ContextCompat.checkSelfPermission()ActivityCompat.requestPermissions()
危险权限实际上才是运行时权限主要处理的对象,这些权限可能引起隐私问题或者影响其他程序运行。Android中的危险权限可以归为以下几个分组:

这里写图片描述

三、运行时申请权限

因为6.0权限授权的改变,即使你在Manifest中加入,有些权限依然需要获得用户的手动授权,但是这一机制——运行时权限仅仅是在我们我们设置targetSdkVersion 大于等于23时且运行在M系统以上的设备上才起作用(即你想要你的app支持这一新特性你得设置compileSdkVersion 和targetSdkVersion 设为大于等于23),所以如果希望app在6.0之前的设备依然使用旧的权限系统,只需要把targetSdkVersion设置为23以下即可,还有一点使用Android studio新建项目,targetSdkVersion 会自动设置为 23,如果你还没支持新运行时权限,个人建议先把targetSdkVersion 降级到22,在M以后,敏感权限默认的值是每次询问,而且shouldShowRequestPermissionRationale()返回值机制手机系统不同。

  • 第一、在AndroidManifest声明所需的权限
  • 第二、使用相应的api方法动态申请

1、使用系统提供的API

动态权限的核心工作流程:checkSelfPermission检查是否已被授予——>requestPermissions申请权限——>自动回调onRequestPermissionsResult——shouldShowRequestPermissionRationale。无论什么框架变出花来都离不开这个基本的流程。

| API方法| 说明|
|: ------------- |:------------|
| int checkSelfPermission(@NonNull Context context, @NonNull String permission) |可用activity.checkSelfPermission或者v4包下的 ContextCompat.checkSelfPermission来检查权限是否已授权 |
| void requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) | 可用activity.requestPermissions或者v4包下的 ContextCompat.requestPermissions来进行权限申请。需要为每一个权限指定一个id,当系统返回授权结果时,应用根据id拿到授权结果。|
| void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults) | 由系统自动触发,当应用申请权限后,Activity将触发这个回调,告诉应用用户的授权结果。 |
|boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) |当应用首次申请权限时,如果用户点击拒绝,下次再申请权限,Android允许你提示用户,你为什么需要这个权限,更好引导用户是否授权,其中在Android原生系统中:如果应用之前请求过此权限但用户拒绝了请求,此方法将返回true;如果用户在过去拒绝了权限请求且在权限请求系统对话框中选择了 Don't ask again 选项将返回 false;如果第一次申请权限也返回false;如果设备规范禁止应用具有该权限,此方法也会返回 false,返回false则不在显示提示对话框,返回true则回显示对话框(但并不一定任何系统的机制都是如此,不同厂商不同的Rom机制有可能不同,以HTC 6.0和联想的系统为例,在HTC上就不一定是返回true,目前测试结果一直都是false,而联想手机上则和原生的机制一样)|

public class MainActivity extends Activity implements View.OnClickListener {

    public static final int REQUEST_PERMISSION_CALL = 100;
    private static final String CALL_PHONE = Manifest.permission.CALL_PHONE;
    private static final String TAG = "Permission";
    private Button btnCheck, btnShow, btnCall;
    private Intent callIntent;

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

    private void init() {
        initViews();
        callIntent = new Intent(Intent.ACTION_CALL);
        Uri uri = Uri.parse("tel:" + "10086");
        callIntent.setData(uri);
    }

    private void initViews() {
        btnCheck = (Button) findViewById(R.id.btn_check);
        btnShow = (Button) findViewById(R.id.btn_showtip);
        btnCall = (Button) findViewById(R.id.btn_call);
        btnCheck.setOnClickListener(this);
        btnShow.setOnClickListener(this);
        btnCall.setOnClickListener(this);
    }

    private boolean checkPermission(String permission) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this, CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
                Log.e("checkPermission", "PERMISSION_GRANTED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
                return true;
            } else {
                Log.e("checkPermission", "PERMISSION_DENIED" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
                return false;
            }
        } else {
            Log.e("checkPermission", "M以下" + ContextCompat.checkSelfPermission(this, CALL_PHONE));
            return true;
        }
    }

    private void startRequestPermision(String permission) {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == REQUEST_PERMISSION_CALL) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    startActivity(callIntent);
                } else {
                    //如果拒绝授予权限,且勾选了再也不提醒
                    if (!shouldShowRequestPermissionRationale(permissions[0])) {

                        AlertDialog.Builder builder = new AlertDialog.Builder(this);
                        builder.setTitle("说明")
                                .setMessage("需要使用电话权限,进行电话测试")
                                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        showTipGoSetting();
                                    }
                                })
                                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.cancel();
                                        return;
                                    }
                                })
                                .create()
                                .show();
                    } else {
                        showTipGoSetting();
                    }
                }
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void onClick(View v) {
        int viewId = v.getId();
        switch (viewId) {
            case R.id.btn_check:
                int isGrantd = ContextCompat.checkSelfPermission(getApplicationContext(), CALL_PHONE);
                Toast.makeText(MainActivity.this, "isGrantd" + isGrantd, Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_showtip:
                boolean isShow = ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, CALL_PHONE);
                Toast.makeText(MainActivity.this, "isShow" + isShow, Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_call:
                call();
                break;
            default:
                break;
        }
    }

    private void call() {
        if (checkPermission(CALL_PHONE)) {
            startActivity(callIntent);
        } else {
            startRequestPermision(CALL_PHONE);
        }
    }

    /**
     * 用于在用户勾选“不再提示”并且拒绝时,再次提示用户
     */
    private void showTipGoSetting() {
        new AlertDialog.Builder(this)
                .setTitle("电话权限不可用")
                .setMessage("请在-应用设置-权限-中,允许APP使用电话权限来测试")
                .setPositiveButton("立即开启", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 跳转到应用设置界面
                        goToAppSetting();
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                    }
                }).setCancelable(false).show();

    }

    /**
     * 打开Setting
     */
    private void goToAppSetting() {
        Intent intent = new Intent();
        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", getPackageName(), null);
        intent.setData(uri);
        startActivityForResult(intent, 123);
    }
}

2、使用第三方开源框架

目前用得比较多的动态权限第三方库PermissionsDispatcher,核心也是依然离不开系统的API,PermissionsDispatcher采用注解,源码很简单就不单独分析了,主要就是在编译时生成代理类(代理类名称格式为XxxxPermissionsDispatcher,其中Xxxx为@RuntimePermissions标注的Activity或Fragment类名称),在Activity/Fragment中通过代理类去完成权限的检查工作,并且把系统的权限处理回调回传到代理类内部,进而完成触发内部的回调,在效率上和官方差不多,唯一的区别在于调用的形式:由于采用代理的形式,不是直接调用系统API的checkSelfPermission来检查权限,取而代之的是代理类里的XxxWithCheck的方法(其中Xxx代表被@NeedsPermission标注的方法名);处理权限申请的结果依然是在系统的onRequestPermissionsResult方法内,但是我们必须把回调结果回传到代理类,所以还必须用代理类调用他对应的onRequestPermissionsResult方法

注解名称 是否必须 说明
@RuntimePermissions 用于表面该Activity or Fragment 使用动态代理来管理权限
@NeedsPermission 用于表明在什么时候需要权限,一般用在方法声明上
@OnShowRationale 应用首次申请权限被拒绝,再次申请权限时,给出提示信息,自动回调标注有@OnShowRationale的方法
@OnPermissionDenied 应用首次申请权限,用户拒绝,使用@OnPermissionDenied标识的方法将作为回调:
@OnNeverAskAgain 应用非首次申请权限时,授权对话框会多出一个复选框不再询问,系统自动回调标注有@OnNeverAskAgain注解的方法
  • 在Module下build.gradle引入依赖
dependencies {
  compile 'com.github.hotchemi:permissionsdispatcher:2.3.2'
  annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:2.3.2'//java8不能使用apt
}
  • 在需要动态权限的Activity或者Fragment加上注解RuntimePermissions
  • 在涉及到动态权限的方法加上注解@NeedsPermission

  • 在需要申请动态权限的方法处,使用代理类的XxxWithCheck方法开启动态权限申请的第一步

  • 重写权限处理回调方法onRequestPermissionsResult,并且通过回传到代理类内部的onRequestPermissionsResult方法

  • 然后根据各自的业务需求,使用@OnShowRationale、@OnPermissionDenied、@OnNeverAskAgain来标注对应的回调方法并处理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容