欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 运行时权限处理

程序员文章站 2022-07-05 15:19:27
转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/50709663; 本文出自:【张鸿洋的博客】 一、概述 随着Android 6.0发布以及普及,我们开发者所要应对的主要就是新版本SDK带来的一些变化,首先关注的就是权限机制的变化 ......

转载请标明出处: 
; 
本文出自:

一、概述

随着android 6.0发布以及普及,我们开发者所要应对的主要就是新版本sdk带来的一些变化,首先关注的就是权限机制的变化。对于6.0的几个主要的变化,查看查看官网的这篇文章,其中当然包含runtime permissions

ok,本篇文章目的之一就是对运行时权限处理的一个介绍,以及对目前权限相关的库的一些了解。

当然非常推荐阅读官网权限相关文章:

本文也是在上述文章基础上理解、实验以及封装。

二、运行时权限的变化及特点

对于6.0以下的权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装,造成了我们想要使用某个app,就要默默忍受其一些不必要的权限(比如是个app都要访问通讯录、短信等)。而在6.0以后,我们可以直接安装,当app需要我们授予不恰当的权限的时候,我们可以予以拒绝(比如:单机的象棋对战,请求访问任何权限,我都是不同意的)。当然你也可以在设置界面对每个app的权限进行查看,以及对单个权限进行授权或者解除授权。

新的权限机制更好的保护了用户的隐私,google将权限分为两类,一类是normal permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是dangerous permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等。

  • normal permissions如下
access_location_extra_commands
access_network_state
access_notification_policy
access_wifi_state
bluetooth
bluetooth_admin
broadcast_sticky
change_network_state
change_wifi_multicast_state
change_wifi_state
disable_keyguard
expand_status_bar
get_package_size
install_shortcut
internet
kill_background_processes
modify_audio_settings
nfc
read_sync_settings
read_sync_stats
receive_boot_completed
reorder_tasks
request_install_packages
set_alarm
set_time_zone
set_wallpaper
set_wallpaper_hints
transmit_ir
uninstall_shortcut
use_fingerprint
vibrate
wake_lock
write_sync_settings
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • dangerous permissions:
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
  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
  permission:android.permission.receive_mms
  permission:android.permission.receive_sms
  permission:android.permission.send_sms
  permission:android.permission.read_cell_broadcasts
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

可以通过adb shell pm list permissions -d -g进行查看。

看到上面的dangerous permissions,会发现一个问题,好像危险权限都是一组一组的,恩,没错,的确是这样的,

那么有个问题:分组对我们的权限机制有什么影响吗?

的确是有影响的,如果app运行在android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对read_contacts已经授权了,当你的app申请write_contacts时,系统会直接授权通过。此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。

不过需要注意的是,不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。

三、相关api

好在运行时相关的api也比较简单,所以适配起来并不会非常痛苦。

api的讲解就跟着申请权限步骤一起了:

  1. 在androidmanifest文件中添加需要的权限。

    这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。

  2. 检查权限

    if (contextcompat.checkselfpermission(thisactivity,
                    manifest.permission.read_contacts)
            != packagemanager.permission_granted) {
    }else{
        //
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里涉及到一个api,contextcompat.checkselfpermission,主要用于检测某个权限是否已经被授予,方法返回值为packagemanager.permission_denied或者packagemanager.permission_granted。当返回denied就需要进行申请授权了。

  3. 申请授权

     activitycompat.requestpermissions(thisactivity,
                    new string[]{manifest.permission.read_contacts},
                    my_permissions_request_read_contacts);
    • 1
    • 2
    • 3

    该方法是异步的,第一个参数是context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestcode,主要用于回调的时候检测。可以从方法名requestpermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。

  4. 处理权限申请回调

    @override
    public void onrequestpermissionsresult(int requestcode,
            string permissions[], int[] grantresults) {
        switch (requestcode) {
            case my_permissions_request_read_contacts: {
                // if request is cancelled, the result arrays are empty.
                if (grantresults.length > 0
                    && grantresults[0] == packagemanager.permission_granted) {
    
                    // permission was granted, yay! do the
                    // contacts-related task you need to do.
    
                } else {
    
                    // permission denied, boo! disable the
                    // functionality that depends on this permission.
                }
                return;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ok,对于权限的申请结果,首先验证requestcode定位到你的申请,然后验证grantresults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果你同时申请两个权限,那么grantresults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~

当然,到此我们的权限申请的不走,基本介绍就如上述。不过还有个api值得提一下:

// should we show an explanation?
if (activitycompat.shouldshowrequestpermissionrationale(thisactivity,
        manifest.permission.read_contacts)) 
    // show an expanation to the user *asynchronously* -- don't block
    // this thread waiting for the user's response! after the user
    // sees the explanation, try again to request the permission.

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个api主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。

那么将上述几个步骤结合到一起就是:

// here, thisactivity is the current activity
if (contextcompat.checkselfpermission(thisactivity,
                manifest.permission.read_contacts)
        != packagemanager.permission_granted) {

    // should we show an explanation?
    if (activitycompat.shouldshowrequestpermissionrationale(thisactivity,
            manifest.permission.read_contacts)) {

        // show an expanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! after the user
        // sees the explanation, try again to request the permission.

    } else {

        // no explanation needed, we can request the permission.

        activitycompat.requestpermissions(thisactivity,
                new string[]{manifest.permission.read_contacts},
                my_permissions_request_read_contacts);

        // my_permissions_request_read_contacts is an
        // app-defined int constant. the callback method gets the
        // result of the request.
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

四、简单的例子

这里写一个简单的例子,针对于运行时权限。相信大家在最开始接触android的时候,都利用intent实验了打电话、发短信等功能。

我们看看直接拨打电话在android 6.x的设备上权限需要如何处理。

当然代码非常简单:

package com.zhy.android160217;

import android.manifest;
import android.content.intent;
import android.content.pm.packagemanager;
import android.net.uri;
import android.os.bundle;
import android.support.v4.app.activitycompat;
import android.support.v4.content.contextcompat;
import android.support.v7.app.appcompatactivity;
import android.view.view;
import android.widget.toast;

public class mainactivity extends appcompatactivity
{

    private static final int my_permissions_request_call_phone = 1;

    @override
    protected void oncreate(bundle savedinstancestate)
    {
        super.oncreate(savedinstancestate);
        setcontentview(r.layout.activity_main);
    }

    public void testcall(view view)
    {
        if (contextcompat.checkselfpermission(this,
                manifest.permission.call_phone)
                != packagemanager.permission_granted)
        {

            activitycompat.requestpermissions(this,
                    new string[]{manifest.permission.call_phone},
                    my_permissions_request_call_phone);
        } else
        {
            callphone();
        }
    }

    public void callphone()
    {
        intent intent = new intent(intent.action_call);
        uri data = uri.parse("tel:" + "10086");
        intent.setdata(data);
        startactivity(intent);
    }

    @override
    public void onrequestpermissionsresult(int requestcode, string[] permissions, int[] grantresults)
    {

        if (requestcode == my_permissions_request_call_phone)
        {
            if (grantresults[0] == packagemanager.permission_granted)
            {
                callphone();
            } else
            {
                // permission denied
                toast.maketext(mainactivity.this, "permission denied", toast.length_short).show();
            }
            return;
        }
        super.onrequestpermissionsresult(requestcode, permissions, grantresults);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

在android 6.x上运行是,点击testcall,即会弹出授权窗口,如何你allow则直接拨打电话,如果denied则toast弹出”permission denied”。

例子很简单,不过需要注意的是,对于intent这种方式,很多情况下是不需要授权的甚至权限都不需要的,比如:你是到拨号界面而不是直接拨打电话,就不需要去申请权限;打开系统图库去选择照片;调用系统相机app去牌照等。更多请参考consider using an intent 
.

当然,上例也说明了并非所有的通过intent的方式都不需要申请权限。一般情况下,你是通过intent打开另一个app,让用户通过该app去做一些事情,你只关注结果(onactivityresult),那么权限是不需要你处理的,而是由打开的app去处理。

五、封装

(1)申请权限

虽然权限处理并不复杂,但是需要编写很多重复的代码,所以目前也有很多库对用法进行了封装,大家可以在github首页搜索:android permission,对比了几个库的使用方式,发现https://github.com/lovedise/permissiongen这个库据我所见相比较而言使用算是比较简单的,那么就以这个库的源码为基础来讲解,大家有兴趣可以多看几个库的源码。

封装的代码很简单,正如大家的对上面的权限代码所见,没有特别复杂的地方。

对于申请权限的代码,原本的编写为:

if (contextcompat.checkselfpermission(this,
                manifest.permission.call_phone)
                != packagemanager.permission_granted)
{

    activitycompat.requestpermissions(this,
            new string[]{manifest.permission.call_phone},
            my_permissions_request_call_phone);
} else
{
    callphone();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

大家可以看到,对于其他的权限,其实申请的逻辑是类似的;唯一不同的肯定就是参数,那么看上面的代码,我们需要3个参数:activity|fragment权限字符串数组int型申请码

也就是说,我们只需要写个方法,接受这几个参数即可,然后逻辑是统一的。

public static void needpermission(fragment fragment, int requestcode, string[] permissions)
{
    requestpermissions(fragment, requestcode, permissions);
}

@targetapi(value = build.version_codes.m)
private static void requestpermissions(object object, int requestcode, string[] permissions)
{
    if (!utils.isovermarshmallow())
    {
        doexecutesuccess(object, requestcode);
        return;
    }
    list<string> deniedpermissions = utils.finddeniedpermissions(getactivity(object), permissions);

    if (deniedpermissions.size() > 0)
    {
        if (object instanceof activity)
        {
            ((activity) object).requestpermissions(deniedpermissions.toarray(new string[deniedpermissions.size()]), requestcode);
        } else if (object instanceof fragment)
        {
            ((fragment) object).requestpermissions(deniedpermissions.toarray(new string[deniedpermissions.size()]), requestcode);
        } else
        {
            throw new illegalargumentexception(object.getclass().getname() + " is not supported");
        }

    } else
    {
        doexecutesuccess(object, requestcode);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

utils.finddeniedpermissions其实就是check没有授权的权限。

#utils
@targetapi(value = build.version_codes.m)
public static list<string> finddeniedpermissions(activity activity, string... permission)
{
    list<string> denypermissions = new arraylist<>();
    for (string value : permission)
    {
        if (activity.checkselfpermission(value) != packagemanager.permission_granted)
        {
            denypermissions.add(value);
        }
    }
    return denypermissions;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

那么上述的逻辑就很清晰了,需要的3种参数传入,先去除已经申请的权限,然后开始申请权限。

ok,我相信上面代码,大家扫一眼就可以了解了。

(2)处理权限回调

对于回调,因为要根据是否授权去执行不同的事情,所以很多框架也需要些一连串的代码,或者和前面的申请代码耦合。不过这个框架还是比较方便的,也是我选择它来讲解的原因。

回调主要做的事情:

  1. 了解是否授权成功。
  2. 根据授权情况进行回调。

对于第一条逻辑都一样;对于第二条,因为涉及到两个分支,每个分支执行不同的方法。

对于第二条,很多框架选择将两个分支的方法在申请权限的时候进行注册,然后在回调中根据requestcode进行匹配执行,不过这样需要在针对每次申请进行对象管理。

不过这个框架采取了一种很有意思的做法,它利用注解去确定需要执行的方法,存在两个注解:

@permissionsuccess(requestcode = 100)
@permissionfail(requestcode = 100)
  • 1
  • 2

利用反射根据授权情况+requestcode即可找到注解标注的方法,然后直接执行即可。

大致的代码为:

@override public void onrequestpermissionsresult(int requestcode, string[] permissions,
      int[] grantresults) {
    permissiongen.onrequestpermissionsresult(this, requestcode, permissions, grantresults);
}

private static void requestresult(object obj, int requestcode, string[] permissions,
                                  int[] grantresults)
{
    list<string> deniedpermissions = new arraylist<>();
    for (int i = 0; i < grantresults.length; i++)
    {
        if (grantresults[i] != packagemanager.permission_granted)
        {
            deniedpermissions.add(permissions[i]);
        }
    }

    if (deniedpermissions.size() > 0)
    {
        doexecutefail(obj, requestcode);
    } else
    {
        doexecutesuccess(obj, requestcode);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

首先根据grantresults进行判断成功还是失败,对于成功则:

private static void doexecutesuccess(object activity, int requestcode)
{
    method executemethod = utils.findmethodwithrequestcode(activity.getclass(),
            permissionsuccess.class, requestcode);

    executemethod(activity, executemethod);
}

#utils
public static <a extends annotation> method findmethodwithrequestcode(class clazz,  class<a> annotation, int requestcode)
{
    for (method method : clazz.getdeclaredmethods())
    {
        if (method.isannotationpresent(annotation))
        {
            if (isequalrequestcodefromanntation(method, annotation, requestcode))
            {
                return method;
            }
        }
    }
    return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

根据注解和requestcode找到方法,然后反射执行即可。失败的逻辑类似,不贴代码了。

ok,到此我们的运行时权限相对于早起版本的变化、特点、以及如何处理和封装都介绍完了。

不过对于上面讲解的库,肯定有人会说:运行时反射会影响效率,没错,不过我已经在上述代码的基础上将运行时注解改成annotation processor的方式了,即编译时注解,这样的话,就不存在反射损失效率的问题了。本来准备fork修改,然后pr,结果写完,改动太大,估计pr是不可能通过了,所以另起项目了,也方便后面的做一些扩展和维护。

详见库:https://github.com/hongyangandroid/mpermissions.

六、mpermissions用法

对外的接口和permissiongen基本一致,因为申请只需要三个参数,抛弃了使用原本类库的单例的方式,直接一个几个静态方法,简单整洁暴力。

贴一个用法:

public class mainactivity extends appcompatactivity
{

    private button mbtnsdcard;
    private static final int requect_code_sdcard = 2;

    @override
    protected void oncreate(bundle savedinstancestate)
    {
        super.oncreate(savedinstancestate);
        setcontentview(r.layout.activity_main);

        mbtnsdcard = (button) findviewbyid(r.id.id_btn_sdcard);
        mbtnsdcard.setonclicklistener(new view.onclicklistener()
        {
            @override
            public void onclick(view v)
            {
                mpermissions.requestpermissions(mainactivity.this, requect_code_sdcard, manifest.permission.write_external_storage);
            }
        });
    }

    @override
    public void onrequestpermissionsresult(int requestcode, string[] permissions, int[] grantresults)
    {
        mpermissions.onrequestpermissionsresult(this, requestcode, permissions, grantresults);
        super.onrequestpermissionsresult(requestcode, permissions, grantresults);
    }


    @permissiongrant(requect_code_sdcard)
    public void requestsdcardsuccess()
    {
        toast.maketext(this, "grant access sdcard!", toast.length_short).show();
    }

    @permissiondenied(requect_code_sdcard)
    public void requestsdcardfailed()
    {
        toast.maketext(this, "deny access sdcard!", toast.length_short).show();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

是不是简单明了~~对于onrequestpermissionsresult所有的activity都是一致的,所以可以放到baseactivity中去。此外,在fragment中使用的方式一致,详见demo。

详见库:https://github.com/hongyangandroid/mpermissions.

至于为什么不直接介绍mpermission的源码,因为主要涉及到annotation processor,所以以这种方式引出,后面考虑单篇博文介绍下我目前所会的编译时注解的相关做法以及api的使用。

最后感谢permissiongen库,祝大家元宵节快乐~!


 

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lmj623565791/article/details/50709663