Android 运行时权限浅谈

关于Android 运行时权限

Android 开发常常遇到的一个问题就是在Android APP 安装的过程中,会向户请求一大堆权限,不同意不会让你安装,当然我是从来不会看的直接安装,我相信你也是,所以不知不觉中,也许有些敏感权限就这样被授予了,(比如我突然收到某个从未注册的平台的推广短信),为了 解决这个问题 Android M 推出了运行时权限,敏感权限在真正使用的时候会想用户提示,用户的安全性和隐私得到保护,仅仅需要做一些适配工作,今天这里我们来解决两个问题 :

  • 运行时权限有啥变化
  • 我们应该怎么样去适配运行时权限

首先我们看看官网上对6.0 权限变化的解释:

  • 如果设备运行的是 Android 6.0(API 级别 23)或更高版本,并且应用的 targetSdkVersion 是 23 或更高版本,则应用在运行时向用户请求权限。用户可随时调用权限,因此应用在每次运行时均需检查自身是否具备所需的权限。
  • 如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在用户安装应用时要求用户授予权限。如果将新权限添加到更新的应用版本,系统会在用户更新应用时要求授予该权限。用户一旦安装应用,他们撤销权限的唯一方式是卸载应用。

谷歌将新的权限分为两类,一类是正常权限,比如联网,震动一类的,这类权限跟之前一样,清单文件声明后直接授予,另一类,是危险权限,譬如:读取联系人、相机、定位等涉及用户隐私的,需要在使用时通知用户进行授权,危险权限如下:
Android 运行时权限浅谈_第1张图片
这些权限被分为一组一组的,对于分组谷歌是这样解释的。

  • 如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。*
  • 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限

后面的demo验证上面的描述。实测下来,部分定制手机会对权限分组进行修改(比如,1加手机中 READ_PHONE_STATE,为正常权限,在手机清单文件中直接声明便可使用),所以尽量不要依赖权限组,我们在需要使用权限的时候单个请求即可。

#####大致流程:

Created with Raphaël 2.2.0 检查权限 已授予? 操作 请求权限? yes no yes

#####示例:
下面我以打电话为例,示例整个流程:
测试手机 Google Pixel 2

首先我在清单文件里面声明一项权限:

 <uses-permission android:name="android.permission.CALL_PHONE" />

然后写一个按钮点击执行如下方法:

 private void makeCall() {
        Uri uri = Uri.parse("tel:10086");
        Intent callIntent = new Intent(Intent.ACTION_CALL, uri);
        startActivity(callIntent);
    }

显然,在老的权限系统下是没有任何问题的。

但是在新的系统下在新的权限系统下(手机Android版本6.0以上,并且当前应用的targetSdk 大于22, 所以这边会细分为四种情况,后面会分别讨论验证)应用就直接崩溃了,报出了:

java.lang.SecurityException: getLine1NumberForDisplay: Neither user 10348 nor current process has android.permission.READ_PHONE_STATE or android.permission.READ_SMS.

所以此时我们在请求我们权限之前,首先检查是否拥有该权限,代码如下:

int checkResult = ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CALL_PHONE);
boolean hasPermission =checkResult== PackageManager.PERMISSION_GRANTED;

当没有权限的时候,我们再去请求权限,(注意,在Activity和Framgment里面请求方法不一样):
在Activity中:

 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE);

在Fragment中:

 requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE);

当请求此方法以后,用户会收到一个系统级的弹框(这个无法被自定义)提示用户是否要授予你当前请求的权限,如果用户点击授予,权限授予成功,拒绝则授予失败,这两种情况下就会回调 activity 或者 fragment的 onRequestPermissionsResult()方法

  @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "权限授予失败", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "权限授予成功", Toast.LENGTH_SHORT).show();
            //todo 搞事情
        }
    }

当授权成功后,我们可直接使用,当我们授权失败以后,我们再次请求权限的话,依然会有系统级的弹框提示,只是多一个"不再提示"的单选框,当选中再次拒绝的话,请求权限就会直接回调授予失败。至此,权限流程基本完成,当用户想要再次开启权限的时候,就只能去设置页的应用权限去开启,如果再次关闭,则重复上述流程。

注意点:

  • 我们请求权限入参是一个数组,意味着我们可以一次请求多项权限,系统会依次弹框,然后在权限result的回调也是一个数组,也就是我们请求的每项权限的授予结果。
  • 很多第三方厂商对系统级授权做过一些改变,可能不会有二次弹框,或者第一次请求直接就会有"不在提示"的选项(如华为 )

######细节: 其实很多时候,用户对于这类请求弹框是比较疑惑的,或者经常会习惯性的点拒绝,(至少我以前是这样),或者对于权限请求不太理解,比如很多时候,我们拍照的时候,请求了相机权限,用户可能会明白,但是如果这个时候再请求定位权限,至少到这里,用户就很困惑了,所以在用户拒绝此次权限请求而你在申请此项权限的时候你需要给用户一个解释,告知用户为什么需要这项权限,所以这里要请求一个新的api:

ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.CALL_PHONE)

在应用第一次请求权限被拒绝以后,这个方法会返回true,所以综合上面几个api,连起来请求权限流程应该是这样:

//  检查是否有权限
if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

    // 权限没有,是否需要给用户一个解释
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.READ_CONTACTS)) {

        // 给用户一个解释以后,再次请求权限 ,比如起一个对话框告诉用户为啥要这个权限。
    } else {

        // 无需解释的话我们直接请求权限

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);
    }
}

####下面在demo中验证完整流程:

首先我写两个方法,分别是读取手机状态和打电话,分别在清单文件中声明相关权限:

/**
     * 读取电话状态
     * 须 READ_PHONE_STATE
     */
    private void getPhoneStatus() {
        TelephonyManager tm = (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
        if (tm == null) {
            return;
        }
        String number = "phone " + tm.getLine1Number() + "\nime" + tm.getImei() + "\nsimSerialNumber" + tm.getSimSerialNumber();
        Snackbar.make(view, number, Snackbar.LENGTH_SHORT).show();
    }

    /**
     * 直接打电话
     * 须 CALL_PHONE
     */
    private void makeCall() {
        Uri uri = Uri.parse("tel:10086");
        Intent callIntent = new Intent(Intent.ACTION_CALL, uri);
        startActivity(callIntent);
    }

然后添加两个按钮,分别调用这个两个方法,不同的是,我在打调用打电话的时候,添加了权限检查与请求的代码:

   @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        view.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getPhoneStatus();
            }
        });

        view.findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (checkAndRequestCallPermission()) {
                    makeCall();
                }
            }
        });
    }
   @TargetApi(M)
    private boolean checkAndRequestCallPermission() {
        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.CALL_PHONE)) {
                showExplainDialog();
            } else {
                requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE_PERMISSION_CALL);
            }

            return false;
        }
        return true;
    }

    private void showExplainDialog() {
        new AlertDialog.Builder(getActivity())
                .setTitle("权限解释")
                .setMessage("因为你拒绝了,所以我们无法帮你联系移动客服了,请你点击授予")
                .setPositiveButton("我知道了", null)
                .setOnDismissListener(new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE_PERMISSION_CALL);
                    }
                }).create().show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode != REQUEST_CODE_PERMISSION_CALL) {
            return;
        }
        if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(getActivity(), "授权失败", Toast.LENGTH_SHORT).show();
        } else {
            makeCall();
        }
    }

我们来看看效果:

首先,我在未请求权限的情形下,去读取电话状态,在新的权限系统模型下,直接抛出异常,第二次,我去请求电话,就弹出了我们的权限授予弹框,在我们点击授予后,在回调中我们在执行拨打电话的操作(注意看 有Toast),如果我们再次点击按钮,则检查方法返回true,我们直接就拨打成功,
此时,我们再次点击之前导致崩溃的读取读取电话状态,直接就可以读取,这里也就验证了我们之前讲到的一点,权限分组之下,一旦某一个危险权限被授予,则请求该权限组下的任意一项权限系统会立即授予。
当然这个只是实例,实际开发当然不能这么写,还是按照谷歌的意思,按需申请,用到拍照的时候就相机,用到短信的时候,再申请短信。这样才能保证用户的目的与我们的目的一致。

现在我把应用删掉再装一次,试试拒绝的流程:

当我们第一次拒绝的时候,我们调用是否给一个解释给用户的方法返回true,此时我们自定义弹框弹出,提示用户为什么要申请这项权限,当弹框消失的时候,告知行为结束,我们再次请求权限,出现系统级弹框,如果此时我选择了不再提示并且拒绝的话,直接授权失败,以后调用request 就会直接回调授权失败。
此时我再回到设置把权限开启后又关闭,再次点击,又会回到第一次请求弹框拒绝之后的流程了,至此,新的权限系统下的请求获取流程梳理完成,目前在部分尝试定制系统下,可能会有所差异,但大致流程一致。

官网流程描述

解决方案:

这也许是一句话Android权限适配的更优解决方案
SoulPermission

兼容性探讨:

基于运行环境environment(当前Android手机)和 当前应用tragetSdk 版本 ,我们可细分四种状态:

  • environment> 23 tragetSdk >=23
  • environment> 23 tragetSdk <23
  • environment< 23 tragetSdk >=23
  • environment< 23 tragetSdk <23

我们上面流程展示了运行时权限,第一种情况的流程,我们现在同样的手机,我吧应用打targetSdk 换成22试试效果:

我们可以直接拨打电话,进入权限管理界面试图关闭权限,会提示,此项权限为旧版打造,关闭可能无法正常使用,所以我们再次拒绝以后,点击电话,也无任何响应。
至此我们可以分析为,只有当运行环境和app target版本同时满足时,我们才能走到运行时环境的流程,否则都是我们老的授权方式,所以,我们在执行流程的时候,可以先用这样一个方法判断:

 /**
     * 是否是老的权限系统
     */
    public static boolean isOldPermissionSystem(Context context) {
        int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        return android.os.Build.VERSION.SDK_INT < M || targetSdkVersion < M;
    }

所以,在老的版本里面权限是直接授予的(当然国内有些厂商,也在这基础上做了自己的权限系统,我也能同过相关的方式拿到权限状态,今天这里不做讨论),我们直接调用,在新的版本里面,我们再走我们的权限申请流程即可。


我这边写了一个一句话权限库,一行代码涵盖了权限请求、版本判断等功能:

传送门

你可能感兴趣的:(android开发笔,权限,运行时权限,Android)