Android6.0运行时权限(基于RxPermission开源库)

版权声明:本文为HaiyuKing原创文章,转载请注明出处!

前言

  在6.0以前的系统,都是权限一刀切的处理方式,只要用户安装,Manifest申请的权限都会被赋予,并且安装后权限也撤销不了。

  Android 6.0 采用新的权限模型,只有在需要权限的时候,才告知用户是否授权;是在runtime时候授权,而不是在原来安装的时候 ,同时默认情况下每次在运行时打开页面时候,需要先检查是否有所需要的权限申请。

  判断是否是需要运行时权限的标记就是targetSDKVersion。

  当targetSDKVersion<23的时候,仅在安装时赋予权限,使用时将不被提醒;

  当targetSDKVersion≥23的时候才会使用新的运行时权限规则。

  运行时权限未适配可能会导致应用崩溃或者在SD卡中创建目录和文件不成功。

注意:

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

解决方案:

一、将targetSDKVersion人为地降到小于23,这样就变成了还是默认使用权限,但是这种并不是Google所推荐使用的。

二、实现APP支持运行时权限

那么,哪些权限属于运行时权限呢?

权限的分组

Android中有很多权限,但并非所有的权限都是敏感权限,于是6.0系统就对权限进行了分类,一般为下述几类

  • 正常(Normal Protection)权限
  • 危险(Dangerous)权限
  • 特殊(Particular)权限
  • 其他权限(一般很少用到)

正常权限

正常权限具有如下的几个特点

  • 对用户隐私没有较大影响或者不会带来安全问题。
  • 安装后就赋予这些权限,不需要显示提醒用户,用户也不能取消这些权限。

上述的权限基本设计的是关于网络,蓝牙,时区,快捷方式等方面,只要在AndroidManifest.xml指定了这些权限,就会被授予,并且不能撤销。

注意:直接在AndroidManifest.xml文件中声明权限即可。

特殊权限

这里讲特殊权限提前讲一下,因为这个相对来说简单一些。

特殊权限,顾名思义,就是一些特别敏感的权限,在Android系统中,主要由两个

  • SYSTEM_ALERT_WINDOW(设置悬浮窗,进行一些黑科技)
  • WRITE_SETTINGS (修改系统设置)

关于上面两个特殊权限的授权,做法是使用startActivityForResult启动授权界面来完成。

注意:关于这两个特殊权限,一般不建议应用申请。

请求SYSTEM_ALERT_WINDOW

private static final int REQUEST_CODE = 1;
private  void requestAlertWindowPermission() {
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivityForResult(intent, REQUEST_CODE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE) {
        if (Settings.canDrawOverlays(this)) {
            Log.i(LOGTAG, "onActivityResult granted");
        }
    }
}

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.canDrawOverlays方法判断授权结果

请求WRITE_SETTINGS

private static final int REQUEST_CODE_WRITE_SETTINGS = 2;
private void requestWriteSettings() {
    Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS );
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
        if (Settings.System.canWrite(this)) {
            Log.i(LOGTAG, "onActivityResult write settings granted" );
        }
    }
}

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_WRITE_SETTINGS 启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.System.canWrite方法检测授权结果

危险权限

危险权限实际上才是运行时权限主要处理的对象,这些权限可能引起隐私问题或者影响其他程序运行。

Android中的危险权限可以归为以下几个分组:

权限组

权限列表

android.permission-group.CALENDAR

android.permission.READ_CALENDAR

(允许程序读取用户的日程信息)

android.permission-group.CAMERA

android.permission.CAMERA

(允许访问摄像头进行拍照)

android.permission-group.CONTACTS

android.permission.READ_CONTACTS

(允许应用访问联系人通讯录信息)

android.permission.WRITE_CONTACTS

(写入联系人,但不可读取)

android.permission.GET_ACCOUNTS

(访问GMail账户列表)

android.permission-group.LOCATION

android.permission.ACCESS_COARSE_LOCATION

(通过WiFi或移动基站的方式获取用户错略的经纬度信息,定位精度大概误差在30~1500米)

android.permission.ACCESS_FINE_LOCATION

(通过GPS芯片接收卫星的定位信息,定位精度达10米以内)

android.permission-group.MICROPHONE

android.permission.RECORD_AUDIO

(录制声音通过手机或耳机的麦克)

android.permission-group.PHONE

android.permission.READ_PHONE_STATE

(访问电话状态)

android.permission.CALL_PHONE

(允许程序从非系统拨号器里输入电话号码)

android.permission.READ_CALL_LOG

(允许应用程序读取用户的通话记录)

android.permission.WRITE_CALL_LOG

(允许一个程序写入(但不读取)用户的通话记录资料)

com.android.voicemail.permission.ADD_VOICEMAIL

(允许应用程序添加语音邮件进入系统)

android.permission.USE_SIP

(允许程序使用SIP视频服务)

android.permission.PROCESS_OUTGOING_CALLS

(允许程序监视,修改或放弃播出电话)

android.permission-group.SENSORS

android.permission.BODY_SENSORS

(允许从传感器,用户使用来衡量什么是他/她的身体内发生的事情,如心脏速率访问数据的应用程序)

android.permission-group.SMS

android.permission.SEND_SMS

(发送短信)

android.permission.RECEIVE_SMS

(接收短信)

android.permission.READ_SMS

(读取短信内容)

android.permission.RECEIVE_WAP_PUSH

(接收WAP PUSH信息)

android.permission.RECEIVE_MMS

(接收彩信)

android.permission.READ_CELL_BROADCASTS

()

android.permission-group.STORAGE

android.permission.READ_EXTERNAL_STORAGE

(允许程序读取外部存储,如SD卡读文件)

android.permission.WRITE_EXTERNAL_STORAGE

(允许程序写入外部存储,如SD卡上写文件)

 

效果图

代码分析

  • 基于Android Studio开发环境。
  • 基于RxPermission开源库。
  • 项目的最低sdk版本号(minSdkVersion)必须>=11。
  • 分为以下四种情况分析:

    A)  只有一个运行时权限申请的情况

    B)  同时请求多个权限(合并结果)的情况

    C)  同时请求多个权限(分别获取结果)的情况

    D)  条件触发获取权限(结合RxBinding使用)的情况

使用步骤

一、检查minSdkVersion值

当前项目的app/ build.gradle文件中的minSdkVersion值必须>=11

 二、添加依赖开源库

在当前项目的app/ build.gradle文件中的dependencies{}里面添加以下代码:

普通情况【这个暂时不会用】

//运行时权限
compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'

 项目中使用了RxJava2的的情况【建议使用这个--不论项目中是否使用了RxJava2】

//运行时权限
compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
compile 'io.reactivex.rxjava2:rxjava:2.0.2'

 如果想要实现条件触发获取权限(结合RxBinding使用)的情况,则还需要依赖RxBinding开源库

compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'

 例如,Demo中的依赖配置如下:

三、在AndroidManifest.xml文件中声明权限列表

<!-- ======================授权获取设备ANDROID_ID========================== -->
<!-- 访问电话状态 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 允许程序读取外部存储文件 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 允许程序写入外部存储,如SD卡上写文件 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.CAMERA" />

四、运行时权限申请代码

注意:需要import的相关类如下:

import com.jakewharton.rxbinding2.view.RxView;
import com.tbruyelle.rxpermissions2.Permission;
import com.tbruyelle.rxpermissions2.RxPermissions;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;

4.1 只有一个运行时权限申请的情况

/**只有一个运行时权限申请的情况*/
private void onePermission(){
    RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
    rxPermissions.request(Manifest.permission.READ_PHONE_STATE) //权限名称,多个权限之间逗号分隔开
            .subscribe(new Consumer<Boolean>() {
                @Override
                public void accept(Boolean granted) throws Exception {
                    Log.e(TAG, "{accept}granted=" + granted);//执行顺序——1【多个权限的情况,只有所有的权限均允许的情况下granted==true】
                    if (granted) { // 在android 6.0之前会默认返回true
                        // 已经获取权限
                        Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
                        String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根据不同的手机设备返回IMEI,MEID或者ESN码
                        Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show();
                    } else {
                        // 未获取权限
                        Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
                    }
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    Log.e(TAG,"{accept}");//可能是授权异常的情况下的处理
                }
            }, new Action() {
                @Override
                public void run() throws Exception {
                    Log.e(TAG,"{run}");//执行顺序——2
                }
            });
}

 在Activity的onCreate方法中调用即可:

打印的日志如下:

4.2 同时请求多个权限(合并结果)的情况

/**同时请求多个权限(合并结果)的情况*/
    private void MultPermission(){
        RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
        rxPermissions.request(Manifest.permission.READ_PHONE_STATE,
                Manifest.permission.READ_EXTERNAL_STORAGE)//权限名称,多个权限之间逗号分隔开
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean granted) throws Exception {
                        Log.e(TAG, "{accept}granted=" + granted);//执行顺序——1【多个权限的情况,只有所有的权限均允许的情况下granted==true】
                        if (granted) { // 在android 6.0之前会默认返回true
                            // 已经获取权限
                            Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
                        } else {
                            // 未获取权限
                            Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
                        }
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        Log.e(TAG,"{accept}");//可能是授权异常的情况下的处理
                    }
                }, new Action() {
                    @Override
                    public void run() throws Exception {
                        Log.e(TAG,"{run}");//执行顺序——2
                    }
                });
    }

 在Activity的onCreate方法中调用即可:

 

打印的日志如下:

4.3 同时请求多个权限(分别获取结果)的情况

/**同时请求多个权限(分别获取结果)的情况*/
    private void MultPermission2(){
        RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
        rxPermissions.requestEach(Manifest.permission.READ_PHONE_STATE,
                Manifest.permission.READ_EXTERNAL_STORAGE)//权限名称,多个权限之间逗号分隔开
                .subscribe(new Consumer<Permission>(){
                    @Override
                    public void accept(Permission permission) throws Exception {
                        Log.e(TAG, "{accept}permission.name=" + permission.name);
                        Log.e(TAG, "{accept}permission.granted=" + permission.granted);
                        if(permission.name.equals(Manifest.permission.READ_PHONE_STATE) && permission.granted){
                            // 已经获取权限
                            Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
                            String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根据不同的手机设备返回IMEI,MEID或者ESN码
                            Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }

  在Activity的onCreate方法中调用即可:

 

打印的日志如下:

4.4 条件触发获取权限(结合RxBinding使用)的情况

/**条件触发获取权限(结合RxBinding使用)的情况*/
    private void clickPermission(View view){
        RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
        RxView.clicks(view)
                .compose(rxPermissions.ensure(Manifest.permission.CAMERA))
                .subscribe(new Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean granted) {
                        Log.e(TAG, "{accept}granted=" + granted);//【多个权限的情况,只有所有的权限均允许的情况下granted==true】
                        if (granted) { // 在android 6.0之前会默认返回true
                            // 已经获取权限
                            Toast.makeText(MainActivity.this, "已经获取CAMERA权限", Toast.LENGTH_SHORT).show();
                        } else {
                            // 未获取权限
                            Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }

 在activity的onCreate方法中调用:

打印的日志如下:

其中,点击事件对应的控件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.why.project.runtime.MainActivity">

    <Button
        android:id="@+id/btn_getpermission"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="手动获取权限"
        android:layout_centerInParent="true"/>
</RelativeLayout>

4.5、跳转到应用权限设置界面(参考《XXPermissions》)【待完善

package com.why.project.runtime;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;

/**
 * Created by HaiyuKing
 * Used 打开应用权限设置界面
 * 参考资料:https://github.com/getActivity/XXPermissions
 */

public class PermissionSettingPage {
    private static final String MARK = Build.MANUFACTURER.toLowerCase();

    /**
     * 跳转到应用权限设置页面
     *
     * @param context 上下文对象
     * @param newTask 是否使用新的任务栈启动
     */
    static void start(Context context, boolean newTask) {

        Intent intent;
        if (MARK.contains("huawei")) {
            intent = huawei(context);
        } else if (MARK.contains("xiaomi")) {
            intent = xiaomi(context);
        } else if (MARK.contains("oppo")) {
            intent = oppo(context);
        } else if (MARK.contains("vivo")) {
            intent = vivo(context);
        } else if (MARK.contains("meizu")) {
            intent = meizu(context);
        } else {
            intent = google(context);
        }

        if (newTask) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }

        try {
            context.startActivity(intent);
        } catch (Exception e) {
            intent = google(context);
            context.startActivity(intent);
        }
    }

    private static Intent google(Context context) {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.fromParts("package", context.getPackageName(), null));
        return intent;
    }

    private static Intent huawei(Context context) {
        Intent intent = new Intent();
        intent.putExtra("packageName", context.getPackageName());
        intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"));
        if (hasIntent(context, intent)) return intent;
        intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity"));
        if (hasIntent(context, intent)) return intent;
        intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity"));
        return intent;
    }

    private static Intent xiaomi(Context context) {
        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
        intent.putExtra("extra_pkgname", context.getPackageName());
        if (hasIntent(context, intent)) return intent;

        intent.setPackage("com.miui.securitycenter");
        if (hasIntent(context, intent)) return intent;

        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
        if (hasIntent(context, intent)) return intent;

        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
        return intent;
    }

    private static Intent oppo(Context context) {
        Intent intent = new Intent();
        intent.putExtra("packageName", context.getPackageName());
        intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity");
        if (hasIntent(context, intent)) return intent;

        intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity");
        if (hasIntent(context, intent)) return intent;

        intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.PermissionAppListActivity");
        return intent;
    }

    private static Intent vivo(Context context) {
        Intent intent = new Intent();
        intent.setClassName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.FloatWindowManager");
        intent.putExtra("packagename", context.getPackageName());
        if (hasIntent(context, intent)) return intent;

        intent.setComponent(new ComponentName("com.iqoo.secure", "com.iqoo.secure.safeguard.SoftPermissionDetailActivity"));
        return intent;
    }

    private static Intent meizu(Context context) {
        Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
        intent.putExtra("packageName", context.getPackageName());
        intent.setComponent(new ComponentName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity"));
        return intent;
    }

    private static boolean hasIntent(Context context, Intent intent) {
        return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
    }
}
PermissionSettingPage.java
        findViewById(R.id.btn_opensetting).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //打开应用权限设置界面
                PermissionSettingPage.start(MainActivity.this,false);
            }
        });

混淆配置

参考资料

聊一聊 Android 6.0 的运行时权限

Android权限管理之Android 6.0运行时权限及解决办法

Android 6.0 运行时权限处理完全解析

Manifest.permission

android权限大全

RxPermissions开源库github地址

XXPermissions

项目demo下载地址

https://github.com/haiyuKing/AndroidRuntimePrivilegeDemo

posted @ 2017-05-07 10:29  HaiyuKing  阅读(10361)  评论(0编辑  收藏  举报