Android 运行时权限获取-PermissionsDispatcher

前段时间在写着玩时用到了一个定位的库,但却总没能成功获取到地理位置,反而换了部手机却又可以了。想到两部手机的版本定位不了的是6.0的,而另一部是4.4的,所以应该是权限没有。查看日志后也确是如此,为此就记录一下关于6.0(api 23)之后的运行时权限的获取问题。虽然这个很早就有了,但是仍有部分应用其实还是没有对此做处理的。本篇是以一个开源库PermissionsDispatcher的使用来写这篇学习笔记。
Github链接:hotchemi/PermissionsDispatcher

新旧权限获取的差异(大部分情况)

6.0以前

用户: 应用安装时会罗列出全部需要的权限,确定安装则表示同意应用获取这些权限,如果因为某个权限用户不想授予,则唯一的方法就是取消安装,最后结果是可能因为一小部分功能需要这个权限使得用户没能安装使用上该应用,这对于双方都是不愿看到的。
开发者:AndroidManifest文件中声明所需要的权限,请求授权简单,需要什么权限加个uses-permission就可以了。

6.0及其之后

用户: 用户可以在手机的设置中或者一些有权限管理的应用中对我们的应用权限进行操作,禁止他们不愿意授予的权限。结果是他们仍然可以安装使用应用而且还能,而且使用时也更放心。
开发者: 需要在需要权限的地方添加代码动态申请权限的授予,在等到用户同意时才能进入到该需要权限的功能模块。相对之前而言请求授权是复杂了些,但是对于用户确是更为友好。

权限级别

系统的权限大致可分为正常权限危险权限特殊权限

  • 正常权限:涵盖应用需要访问其沙盒外部数据或资源,但对用户隐私或其他应用操作风险很小的区域。例如,设置时区的权限就是正常权限。如果应用声明其需要正常权限,系统会自动向应用授予该权限。
  • 危险权限:涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。
  • 特殊权限:有许多权限其行为方式与正常权限及危险权限都不同。SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS 特别敏感,因此大多数应用不应该使用它们。如果某应用需要其中一种权限,必须在清单中声明该权限,并且发送请求用户授权的 intent。系统将向用户显示详细管理屏幕,以响应该 intent。

权限组

所有危险的 Android 系统权限都属于权限组。如果设备运行的是 Android 6.0(API 级别 23),并且应用的 targetSdkVersion 是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:

  • 如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。

  • 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限。

  • 任何权限都可属于一个权限组,包括正常权限和应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。
  • 如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的权限组,而不告知具体权限。

危险权限及其权限组

权限组 权限
CALENDAR
(日历)
READ_CALENDAR
WRITE_CALENDAR
CAMERA
(相机)
CAMERA
CONTACTS
(通讯录)
READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION
(地理位置)
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE
(麦克风)
RECORD_AUDIO
PHONE
(电话)
READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS
(传感器)
BODY_SENSORS
SMS
(短信)
SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE
(存储)
READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE

PermissionsDispatcher的使用

在GitHub上他们已经有一些介绍,看的懂得可以直接看上面的README文档,此处只说明我这边的实现步骤,并且以一个简单的拨打电话demo来举例,或许写法有些不一样但目的都是能最终实现权限的授予。
效果图:

依赖PermissionsDispatcher

既然我们的项目针对的用户有6.0及其以上的系统,那么就需要将我们项目中的targetSdkVersion改为23或者以上

在Project的build.gradle文件中添加一下代码

buildscript {
    repositories {
        jcenter()
        ...
    dependencies {
        ...
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        ...
    }
}

在Module的build.gradle文件中添加

apply plugin: 'android-apt'
dependencies {
    ...
    /*后面的版本查看GitHub上*/
    compile 'com.github.hotchemi:permissionsdispatcher:2.4.0'
    apt 'com.github.hotchemi:permissionsdispatcher-processor:2.4.0'
}

例子及注释

首先根据我们的例子是拨打电话,那么需要的权限是CALL_PHONE,此时我们仍然需要在AndroidManifest文件中添加权限

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

我们照常写我们的逻辑代码,写完之后就是对需要权限的地方做下申请操作。先给出我的完整代码,基本上依葫芦画瓢就可以了,使用起来很简单。具体的在代码之后再说明一下。

@RuntimePermissions
public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private Button mBtnCallPhone;
    private TextView mTvTelNum;

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

        initView();
    }

    private void initView() {
        mBtnCallPhone = (Button) findViewById(R.id.btn_callPhone);
        mBtnCallPhone.setOnClickListener(this);
        mTvTelNum = (TextView) findViewById(R.id.tv_telNum);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_callPhone:
                //发起权限申请,名字为“该Activity名+PermissionsDispatcher.调用方法名+WithCheck”
                      MainActivityPermissionsDispatcher.callPhoneWithCheck(MainActivity.this);
                callPhone();
                break;
            default:
                break;
        }
    }

    /**
     * 需要权限的方法
     */
    @NeedsPermission(Manifest.permission.CALL_PHONE)
    void callPhone(){
        String telNum = mTvTelNum.getText().toString().trim();
        Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:"+telNum));
        try {
            startActivity(intent);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 唤出权限时的提示
     * @param request 所要申请的权限
     */
    @OnShowRationale(Manifest.permission.CALL_PHONE)
    void showRationaleForCallPhone(PermissionRequest request) {
        showRationaleDialog("使用此功能需要打开拨打电话的权限", request);
    }

    /**
     * 被用户拒绝
     */
    @OnPermissionDenied(Manifest.permission.CALL_PHONE)
    void onCallPhoneDenied() {
        Toast.makeText(this,"权限未授予,功能无法使用",Toast.LENGTH_SHORT).show();
    }

    /**
     * 被拒绝并勾选不在提醒授权时,应用需提示用户未获取权限,需用户自己去设置中打开
     */
    @OnNeverAskAgain(Manifest.permission.CALL_PHONE)
    void onCallPhoneNeverAskAgain() {
        AskForPermission();
    }

    /**
     * 告知用户具体需要权限的原因
     * @param messageResId
     * @param request
     */
    private void showRationaleDialog(String messageResId, final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.proceed();//请求权限
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(@NonNull DialogInterface dialog, int which) {
                        request.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage(messageResId)
                .show();
    }

    /**
     * 被拒绝并且不再提醒,提示用户去设置界面重新打开权限
     */
    private void AskForPermission() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("当前应用缺少拨打电话权限,请去设置界面打开");
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

            }
        });
        builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                intent.setData(Uri.parse("package:" + getPackageName())); // 根据包名打开对应的设置界面
                startActivity(intent);
            }
        });
        builder.create().show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // 将权限处理采用PermissionsDispatcher的处理方式
        MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }
}

该开源库有五种注释,在上面例子中就可以看到这五种注释的意义。

  • @RuntimePermissions(必须)标记需要运行时权限判断的Activity或者Fragment类,标注在该类上。
  • @NeedsPermission(必须)标记需要权限的方法。即该方法中有需要权限授予,并是在授予后才执行。
  • @OnShowRationale请求权限时的回调,在里面一般用于说明为何需要此权限。
  • @OnPermissionDenied标记用户没有授予该权限或者获取权限被拒绝时执行的方法。
  • @OnNeverAskAgain在有该标记的方法中执行如果权限请求被用户禁止且不再提示时的处理。

在此我们再回到上面的权限分类上,危险权限写法就可以像上面例子那样,但特殊权限Manifest.permission.SYSTEM_ALERT_WINDOWManifest.permission.WRITE_SETTINGS就有点改动,但改动不大,只是把onRequestPermissionsResult中权限回调的写在onActivityResult中。

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    MainActivityPermissionsDispatcher.onActivityResult(this, requestCode);
}

再回来就是说下采用该库时调用方法的规则是你(@RuntimePermissions标注的类名)+PermissionsDispatcher.(@NeedsPermission标注的方法名)+WithCheck(@RuntimePermissions标注的对象)。如我这里是:

MainActivityPermissionsDispatcher.callPhoneWithCheck(MainActivity.this);

注意:
此处需要注意的一点是除了第一个@RuntimePermissions是在类上标注的,其余四个在方法上做标注的方法不能设为private

由于各个手机厂商的定制问题可能有着各自的权限管理方式,因此此处的“设置”部分大家可以自行考虑取舍,或者捕获一下异常,避免出现错误。


好了,到这里就可以在我们的6.0及以上的系统版本上运行我们的程序查验一下。基本上是可以了,也会发现这个库使用起来确实简单,就是在调用时如MainActivityPermissionsDispatcher之类的名字写法麻烦,而且会报错,但基本上rebuild一下就可以了。可以尝试一下,当然还有很多运行时权限获取的开源库,只是我觉得就这个最简单。萝卜青菜各有所爱,适合自己的就行。

例子demo

参考:
Android6.0运行时权限解决方案:http://www.jianshu.com/p/d4a9855e92d3
权限最佳做法
系统权限

你可能感兴趣的:(android)