Android 6.0运行时权限全解析

1. Android 6.0 运行时权限的介绍

在保护用户隐私方面:Android6.0为了更好的保护个人隐私,添加了运行时权限:分为两类,一类是Normal Permissions,这类权限不涉及个人隐私,不需要用户进行授权,比如手机震动,访问网络;一类是Dangerous Permissions,这类权限涉及个人隐私,需要用户进行授权,比如读取SD卡,访问通讯录等。

在用户操作方面:当执行敏感操作之前弹出对话框,请求权限,可以拒绝,可以同意;可以在设置页面对APP的权限进行查看,以及对单个权限进行授权或者解除授权。

2. 为什么需要配置运行时权限及解决办法

为什么:
  • 直接原因:
    当我们把targetSdkVersion设置为大于22的版本后,如果我们申请危险权限的时候,谷歌就会强制我们使用动态申请,否则会报错。
  • 根本原因:保护个人隐私,提高安全性
    在6.0之前,权限会在显示在安装页面,告知APP访问哪些权限,如果你同意它访问这些权限,就可以安装,如果不同意就不能安装这款APP.因此好多App存在着滥用权限的情况,不管你用不用得到,先把权限申请了再说。举例说明:微信要读取手机里面的短信和彩信,这是不认可的,但是如果你不同意这项权限,你是无法安装这款App的;
    在6.0之后,用户不需要在安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。举例说明:一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,我仍然可以使用这款应用的其他功能,不用像6.0之前无法安装它。
解决办法:
  • 投机取巧
    如果我们不想使用6.0或者7.0的新特性,那么我们把targetSdkVersion设置22,就可以很好的避开动态配置运行时权限。而targetSdkVersion设置为22,并不影响其在Android 6.0或7.机制上使用,因为高版本兼容低版本。
  • 光明大道
    配置运行时权限。

3. 正常权限和危险权限罗列:

Normal Permissions:
WRITE_SYNC_SETTINGS 写同步设置
WAKE_LOCK 唤醒锁
VIBRATE 颤动
USE FINGERPRINT 使用指纹
UNINSTALL_SHORTCUT 卸载快捷方式
TRANSMIT_IR 红外传输
SET_WALLPAPER_HINTS 壁纸设置提示
SET_WALLPAPER 设置壁纸
SET_TIME_ZONE 设置时区
SET_ALARM 设置闹钟
REQUEST_INSTALL_PACKAGES 请求安装包
REORDER_TASKS 重新排序的任务
RECEIVE_BOOT_COMPLETED 收到启动完成
READ_SYNC_STATS 读同步数据
NFC
MODIFY_AUDIO_SETTINGS 修改音频设置
KILL_BACKGROUND_PROCESSES 杀死后台进程
INTERNET 网络
INSTALL_SHORTCUT 安装快捷方式
GET PACKAGE SIZE 得到包的大小
EXPAND_STATUS_BAR 扩大状态栏
DISABLE_KEYGUARD 禁用键盘守卫
CHANGE_WIFI_STATE 更改无限网络状态
CHANGE_WIFI_MULTICAST_STATE 改变无限多播状态
CHANGE_NETWORK_STATE 改变网络状态
BROADCAST_STICKY 粘性广播
BLUETOOTH_ADMIN 蓝牙管理
BLUETOOTH 蓝牙技术
ACCESS_WIFI_STATE 访问无限网络状态
ACCESS_NOTIFICATION_POLICY 访问通知策略
ACCESS_NETWORK_STATE 访问网络状态
ACCESS_LOCATION_EXTRA_COMMANDS 访问地点额外的命令
Dangerous Permissions(9组24个):
group:android.permission-group.CONTACTS 通讯录方面 
permission:android.permission.WRITE_CONTACTS 写入通讯录 
permission:android.permission.GET_ACCOUNTS 访问通讯录权限 
permission:android.permission.READ_CONTACTS 读取通讯录

group:android.permission-group.PHONE 电话方面 
permission:android.permission.READ_CALL_LOG 看电话记录 
permission:android.permission.READ_PHONE_STATE 读取手机状态 
permission:android.permission.CALL_PHONE 打电话 
permission:android.permission.WRITE_CALL_LOG 编写调用日志 
permission:android.permission.USE_SIP 使用SIP 
permission:android.permission.PROCESS_OUTGOING_CALLS 过程输出调用 permission:com.android.voicemail.permission.ADD_VOICEMAIL 添加语音信箱

group:android.permission-group.CALENDAR 日历 
permission:android.permission.READ_CALENDAR 读取日历 
permission:android.permission.WRITE_CALENDAR 写入日历 

group:android.permission-group.CAMERA 照相机 
permission:android.permission.CAMERA

group:android.permission-group.SENSORS 传感器 
permission:android.permission.BODY_SENSORS 体传感器

group:android.permission-group.LOCATION 位置 
permission:android.permission.ACCESS_FINE_LOCATION 获取好位置 
permission:android.permission.ACCESS_COARSE_LOCATION 

获取粗定位group:android.permission-group.STORAGE 存储 
permission:android.permission.READ_EXTERNAL_STORAGE 读取外部存储器 permission:android.permission.WRITE_EXTERNAL_STORAGE 写外部存储器

group:android.permission-group.MICROPHONE 扩音器;麦克风 
permission:android.permission.RECORD_AUDIO 录音

group:android.permission-group.SMS 信息 
permission:android.permission.READ_SMS 读取信息 
permission:android.permission.RECEIVE_WAP_PUSH 收到WAP推送 
permission:android.permission.RECEIVE_MMS 接收彩信 
permission:android.permission.RECEIVE_SMS 收信息 
permission:android.permission.SEND_SMS 发信息 

注意:你不需要了解每个权限的作用,只要把它当成一个参照表来查看就行了。在申请权限的时候,来查下,如果在危险权限里面,那就配置运行时权限,如果不在就在注册文件里面添加一下权限声明就可以了。

4. Android 6.0的运行时权限的授权机制:

Android权限都是分组的,所以授权机制会因此受到影响,授权机制如下:

  • 当申请某个危险权限的时候,如果用户在手机后台已经对分组内的其他危险权限进行了授权,那么系统会立即授权该权限(需要申请的),不会让用户进行点击授权,比如app后台已经授权READ_EXTERNAL_STORAGE (读取外部存储器),当你申请WRITE_EXTERNAL_STORAGE (写外部存储器)的时候,系统会立即授权,不会再让点击授权。
  • 需要点击申请危险权限的时候,弹出对话框,这个对话框是不可以定制的;对话框的文本说明,是对整个组的权限说明,而非某一项的。
  • 不要对权限组过多的依赖,尽量对每一个我先权限都进行申请。

5. 在程序运行时申请权限

  • 创建活动MainActivity和布局activity_main:
<Button
android:id="@+id/bt_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拨打电话" />
  • 点击拨打电话(这个时候就需要申请拨打电话的权限--------)
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button bt_call = (Button) findViewById(R.id.bt_call);
    bt_call.setOnClickListener(this);
}

@Override
public void onClick(View view) {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
    } else {
        call();
    }
}

private void call() {
    /*为了防止崩溃,我们用异常捕获代码块捕获异常,startActivity(intent)报错,让添加安全权限**/
    try {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:10086"));
        startActivity(intent);
    } catch (SecurityException e) {
        e.printStackTrace();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                call();
            } else {
                Toast.makeText(this, "你拒绝了这个权限", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
    }
}

说明:

  • 首先判断用户是不是已经给我们授权了,借助的是ContextCompat.checkSelfPermission方法。方法接收两个参数,第一个参数是Context,第二个参数是具体的权限名Manifest.permission.CALL_PHONE,然后使用方法的返回值和PackageManager.PERMISSION_GRANTED(权限准许)做比较,相等则表示已经授权,不相等表示没有授权。
  • 已经授权就去执行拨打电话的操作,没有授权则借助ActivityCompat.requestPermissions方法向客户申请授权,接收三个参数,第一个参数是Activity的实例,第二个参数是一个String数组,我们把要申请的权限名放在数组中即可,第三个参数是请求码,只要唯一值就可以了,这里传入了1。
  • 调用完了ActivityCompat.requestPermissions方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同意或者拒绝我们的权限申请。
  • 不管选择哪个,都会调用onRequestPermissionsResult方法,授权的结果会封装在grantResults参数当中,grantResults中存放的就是我们的申请权限时数组里面的内容:new String[]{Manifest.permission.CALL_PHONE},如果grantResults的长度大于0,并且其中的权限等于PackageManager.PERMISSION_GRANTED,那就表示授权成功。
  • 那这里我们只需要判断一下授权结果就可以了,如果用户同意则调用call方法拨打电话,如果不同意就弹出一个失败的提示即可。如果我们对刚才同意的权限后悔了,只需要在设置-应用程序-权限管理里面去除我们想删除的权限即可。

6. 处理“不再询问”选项

  • 总结:首先判断是否有拨打电话的权限,如果有,直接拨打电话,如果没有该权限,则弹出提示框进行权限申请。onRequestPermissionsResult就是申请权限的回调,如果用户选择“允许”,则拨打电话,如果用户选择拒绝,就弹出Toast显示权限被拒绝。
  • 注意:如果我们选择“允许”,下一次就不会弹出权限申请提示框;如果选择“拒绝”,则下一次还会弹出权限申请提示框,只不过这一次会多出一个选项,叫做“不再询问”。如果我们勾选了“不再询问”,则下一次就不会弹出权限申请提示框,而直接调用onRequestPermissionsResult,回调结果为用户最后一次的选择,也就会弹出我们定义的Toast:“权限被拒绝”。想要再次打开该权限,则需要在设置—应用—配置应用—应用权限—电话权限中授权。
  • 处理:当用户选择了“不再询问”,每次访问该权限的API时都会失效,显然不好,所以我们需要给用户一个友好的提示。使用shouldShowRequestPermissionRationale方法,这个方法用来帮助开发者向用户解释权限的情况。
@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call();
                } else {
                    Toast.makeText(MainActivity.this, "您拒绝了拨打电话的权限,会导致相关功能无法使用", Toast.LENGTH_SHORT).show();
                    if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)){
                        AlertDialog dialog = new AlertDialog.Builder(this)
                                .setMessage("该功能需要访问电话的权限,不开启将无法正常工作!")
                                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        //再次请求权限或者其他
                                    }
                                }).create();
                        dialog.show();
                    }
                }
                break;
            default:
        }
    }

7. 多权限的申请

List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this,
        Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
    permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,
        Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
    permissionList.add(Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this,
        Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
    permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (!permissionList.isEmpty()) {
    String[] permissions = permissionList.toArray(new String[permissionList.size()]);
    ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
} else {
    requestLocation();
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0) {
                for (int result : grantResults) {
                    if (result != PackageManager.PERMISSION_GRANTED) {
                        Toast.makeText(this, "必须同意所有权限才能使用本程序", Toast.LENGTH_SHORT).show();
                        finish();
                        return;
                    }
                }
                requestLocation();
            } else {
                Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
                finish();
            }
            break;
        default:
    }
}
        

8. 封装:easypermissions

  • Activity:
public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks {

    private static final String[] CALLPHONE = {Manifest.permission.CALL_PHONE};
    private static final int CALL_PHONE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.bt_camara);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                requestCallPhone();
            }
        });
    }

    //这个方法不用动,它会回调到下面成功和失败的回调那里
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }

    public void requestCallPhone() {
        if (!hasPermission()) {
            EasyPermissions.requestPermissions(
                    this,
                    "需要开启拨打电话的权限",
                    CALL_PHONE,//请求码
                    CALLPHONE);//权限
        } else {
            call();
        }
    }

    private boolean hasPermission() {
        return EasyPermissions.hasPermissions(this, Manifest.permission.CALL_PHONE);
    }

    //拨打电话
    private void call() {
        Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:10086"));
        //检测
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        startActivity(intent);
    }

    //同意授权
    @Override
    public void onPermissionsGranted(int requestCode, List<String> perms) {
        call();
    }

    //拒绝授权
    @Override
    public void onPermissionsDenied(int requestCode, List<String> perms) {
        Toast.makeText(MainActivity.this, "您拒绝了拨打电话的权限,会导致相关功能无法使用", Toast.LENGTH_SHORT).show();
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }

    //点击拒绝授权并且选中不再提问时,再次发起请求权限回弹出是否跳转设置页面去授权的对话框;这个是授权以后的回调(由于手机定制化的原因,回调因场景而异)
    //
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == AppSettingsDialog.DEFAULT_SETTINGS_REQ_CODE) {
            call();
        }
    }
}
  • 配置文件:
<uses-permission android:name="android.permission.CALL_PHONE"/>

9. 封装:PermissionsDispatcher

9.1 封装的意义

  • 每次在需要判断权限的地方均写如上面所示的一套方法,很麻烦。
  • 封装后只需要改变传进来的我们需要判断的权限即可。

9.2 使用方法

9.2.1 步骤01

@RuntimePermissions
public class MainActivity extends AppCompatActivity {

    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewByaId(R.id.bt_button);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
              
            }
        });
    }

    
    @NeedsPermission(Manifest.permission.CALL_PHONE)
        //在获取权限的地方注释
    void call() {
        Intent intent = new Intent(Intent.ACTION_CALL);
        Uri data = Uri.parse("tel:" + "10086");
        intent.setData(data);
        try {
            startActivity(intent);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

    @OnShowRationale(Manifest.permission.CALL_PHONE)
        //提示用户为何要开启此权限
    void showWhy(final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setMessage("拨打电话需要您的同意")
                .setPositiveButton("知道了", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.proceed();//再次执行权限请求
                    }
                }).show();
    }

    @OnPermissionDenied(Manifest.permission.CALL_PHONE)
        //用户选择拒绝时的提示
    void showDenied() {
        Toast.makeText(this, "您拒绝了拨打电话的操作", Toast.LENGTH_SHORT).show();
    }

    @OnNeverAskAgain(Manifest.permission.CALL_PHONE)
        //用户选择不再询问后的提示
    void showNotAsk() {
        new AlertDialog.Builder(MainActivity.this).setMessage("该功能需要访问电话的权限,不开启将无法正常工作!")
                .setPositiveButton("去开启", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
                        startActivity(intent);
                    }
                })
                .setNegativeButton("保持禁止", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .create()
                .show();
    }
}

9.2.2 步骤02

  • Build-Make Moudle 'app'
  • 伴随着会生成MainActivityPermissionsDispatcher类
  • 重写onRequestPermissionsResult方法
  • 代码如下:
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // NOTE: delegate the permission handling to generated method
        MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
    }
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivityPermissionsDispatcher.callWithPermissionCheck(MainActivity.this);
            }
        });
    }

10. 封装Demo地址

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