当前位置: 移动技术网 > 移动技术>移动开发>Android > Android 运行时权限处理

Android 运行时权限处理

2018年08月22日  | 移动技术网移动技术  | 我要评论

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

一、概述

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

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

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

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

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

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

新的权限机制更好的保护了用户的隐私,google将权限分为两类,一类是,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是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去牌照等。更多请参考.

当然,上例也说明了并非所有的通过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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网