Android 6.0 运行时权限请求的简单封装

写在前面

刚刚经历过春招,深深感觉到很多知识点因为不常使用很容易就变得印象模糊,导致在面试中无法逻辑清晰地说出个一二三来。由此也让我深刻体会到平时多写写博客的必要性,以往一直没有动力写博客,一方面是觉得写博客实在是一件太耗时的事,另一方面是觉得自己在日常学习上的一些体会和经历并不值得供他人借鉴。现在想来,多写写博客至少可以给自己之后温习知识点一些助力,在敲键盘的当下也可以加深自己的印象。那么,就从今天做的一个运行时权限请求的简单封装开始吧,希望自己能坚持下去多写写几篇博客。

运行时权限

运行时权限特点

自Android6.0(API23)以后,用户开始在应用运行时向其授予权限,而不是在应用安装时一并授予。这一举措一方面简化了应用的安装过程,另一方面也更好的保护了用户的隐私。当然,并非所有权限都需要在运行时再向用户动态申请,只有危险权限才需要由用户明确批准应用才能使用。有关系统权限的具体介绍可以参阅正常权限和危险权限

运行时权限请求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.
    }
}

用户响应之后,我们可以在[onRequestPermissionsResult()](https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback.html#onRequestPermissionsResult(int, java.lang.String[], int[]))方法中根据用户的响应做出相应的处理,官方文档的示例代码如下:

@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;
        }

        // other 'case' lines to check for other
        // permissions this app might request
    }
}

权限请求封装

可以看到,实际上运行时权限的处理并不复杂。那么为什么我要对运行时权限进行一次简单的封装呢?

  • 一方面,虽然运行时权限处理的代码并不复杂,但考虑到这部分逻辑可能在一个项目中多处出现,实际使用中还是可能导致在项目中出现许多重复的代码。因为运行时权限处理的整体流程是比较清晰且一致的,即检查应用是否具备该权限、根据需要请求该权限、根据用户响应做出相应后续处理,我们大可以把这个流程封装起来,传入必须的参数,其余的流程就委托给一个特定的对象去完成即可。
  • 另一方面,实际上运行时权限的处理也并不总是如想象的那么简单。举例来说,在Fragment中处理运行时权限的API和前文提到的有所不同,具体可以参见Request runtime permissions from v4.Fragment and have callback go to Fragment?的讨论。此外,之前也看到一个网友提及到在Service中动态申请运行时权限之后无法做后续处理(Service中没有对应的回调方法)的问题。针对这些问题,如果能够对运行时权限处理做一个简单的封装,统一处理这些问题,那我们在编写业务逻辑代码时就可以不用困扰于处理这方面的细节了。

当然,实际上,目前也已经有很多库对运行时权限处理做了封装,在github上搜索android permiss,就能找到很多成熟的第三方开源库了。重复造轮子并不是什么好习惯,而我还是选择了自己动手封装,主要是考虑到

  • 运行时权限处理整体上比较简单,而如果因此为项目额外引入一个第三方库可能会给项目多添加了一些实际上用不上的方法。
  • 和我个人的编码习惯有关,我习惯的是将一个功能的处理流程的完整代码放在一处,方便我后续如果需要再去看代码不用在一个文件里重复跳转。因此在运行时权限处理上,我希望在传入相关参数进行请求的同时也把相关的处理后的回调方法传入。

封装用法

先来看封装之后如何进行权限申请:

new PermissionRequest.Builder(MainActivity.this,
                        new String[]{DangerousPermission.CAMERA, DangerousPermission.CALL_PHONE})
                        .build()
                        .request();

传入Context和Perimissions参数构建PermissionRequest并调用request方法即可。
当然,通常我们都需要根据用户的响应做出相应的后续处理:

new PermissionRequest.Builder(MainActivity.this,
                        new String[]{DangerousPermission.CAMERA, DangerousPermission.CALL_PHONE})
                        .setCallBack(new PermissionRequest.CallBack() {
                            @Override
                            public void onSuccess(String permission) {
                                // 参数permission对应的权限请求被允许时回调
                            }

                            @Override
                            public void onFail(String permission) {
                                // 参数permission对应的权限请求被拒绝时回调
                            }

                            @Override
                            public void onGranted() {
                                // 所有权限请求被允许时回调
                            }
                        })
                        .build()
                        .request();

当前的使用方式就这么简单。

封装分析

可以看到,在使用Builder构建PermissionRequest时传入的权限字符串为

DangerousPermission.CAMERA

之类的字符串,而非标准的

android.Manifest.permission.CAMERA

这类的权限字符串。
这里纯粹是为了方便自己使用时不会额外对正常权限去做动态申请(虽然传入了正常权限 作为参数也没什么问题,纯粹是强迫症orz),额外使用一个类DangerousPermission存储了危险权限。

/**
 * Created by Wangzf on 2017/5/15.
 * 危险的Android系统权限汇总
 * 参见https://developer.android.com/guide/topics/security/permissions.html?hl=zh-cn#normal-dangerous
 */

public class DangerousPermission {

    // permission-group.CALENDAR
    public static final String READ_CALENDAR = permission.READ_CALENDAR;
    public static final String WRITE_CALENDAR = permission.WRITE_CALENDAR;

    // permission-group.CAMERA
    public static final String CAMERA = permission.CAMERA;

    // permission_group.CONTACTS
    public static final String READ_CONTACTS = permission.READ_CONTACTS;
    public static final String WRITE_CONTACTS = permission.WRITE_CONTACTS;
    public static final String GET_ACCOUNTS = permission.GET_ACCOUNTS;

    // permission-group.LOCATION
    public static final String ACCESS_FINE_LOCATION = permission.ACCESS_FINE_LOCATION;
    public static final String ACCESS_COARSE_LOCATION = permission.ACCESS_COARSE_LOCATION;

    // permission-group.MICROPHONE
    public static final String RECORD_AUDIO = permission.RECORD_AUDIO;

    // permission-group.PHONE
    public static final String READ_PHONE_STATE = permission.READ_PHONE_STATE;
    public static final String CALL_PHONE = permission.CALL_PHONE;
    public static final String READ_CALL_LOG = permission.READ_CALL_LOG;
    public static final String WRITE_CALL_LOG = permission.WRITE_CALL_LOG;
    public static final String ADD_VOICEMAIL = permission.ADD_VOICEMAIL;
    public static final String USE_SIP = permission.USE_SIP;
    public static final String PROCESS_OUTGOING_CALLS = permission.PROCESS_OUTGOING_CALLS;

    // permission-group.SENSORS
    public static final String BODY_SENSORS = permission.BODY_SENSORS;

    // permission-group.SMS
    public static final String SEND_SMS = permission.SEND_SMS;
    public static final String RECEIVE_SMS = permission.RECEIVE_SMS;
    public static final String READ_SMS = permission.READ_SMS;
    public static final String RECEIVE_WAP_PUSH = permission.RECEIVE_WAP_PUSH;
    public static final String RECEIVE_MMS = permission.RECEIVE_MMS;

    // permission-group.STORAGE
    public static final String READ_EXTERNAL_STORAGE = permission.READ_EXTERNAL_STORAGE;
    public static final String WRITE_EXTERNAL_STORAGE = permission.WRITE_EXTERNAL_STORAGE;
}

当前情况下,构建PermissionRequest需要的参数仅有Context、Permissions、PermissionRequest.CallBack,使用Builder模式创建PermissionRequest实例似乎有过度设计之嫌(当前仅提供了setCallBack方法),但考虑到之后可以为PermissionRequest提供额外的定制功能(比如定制在用户拒绝权限请求之后的应对策略),还是使用了Builder模式来完成对象的创建。
考虑request方法的具体实现。之前在设想里我希望能够在构建PermissionRequest时同时传入CallBack将相应的回调处理代码写在一处,但是在官方API中回调的逻辑需要在onRequestPermissionsResult()方法中实现,这也就产生了一个问题:
一方面我希望通过封装能够将onRequestPermissionsResult()等等这些API的调用隐藏起来,但是另一方面又必须在发起权限请求对应的Context中调用onRequestPermissionsResult()方法并在其中调用相应的回调方法。
一个比较直观的解决方式是将onRequestPermissionsResult()方法的处理逻辑写入一个BaseActivity中,让需要处理运行时权限的Activity都继承这个BaseActivity。
但是这种解决方式感觉不够优雅,

  • 一方面由于Java单继承的特性,我们不应该轻易就为这样单一的需求占用宝贵的继承资格。当然如果是自己为当前特定项目做的封装,在原有的BaseActivity中加入onRequestPermissionsResult()方法的处理逻辑也是可以的;但我目前所做的封装工作,目的是完成独立于当前项目的一个组件,这种解决方式并不现实。
  • 另一方面,使用BaseActivity这种方式,只能解决在Activity中处理运行时权限的封装问题,无法解决上文提及到的Fragment、Service中的问题。

因此必须寻求另外的解决方式。
在翻看文档时,我发现在support.v4.app.Fragment的requestPermissions()方法的解释中提及到:

This method may start an activity allowing the user to choose which permissions to grant and which to reject. Hence, you should be prepared that your activity may be paused and resumed.

这给了我一些启示,同样的,我们也可以新启动一个Activity来处理运行时权限!
这样一切问题都迎刃而解,我们在封装的组件中启动一个Activity来处理运行时权限,自然可以隐藏onRequestPermissionsResult()方法中的处理逻辑;同时,由于我们是在Activity中处理运行时权限,那么不管调用PermissionRequest的request方法是在什么上下文环境中(不管是Activity、Fragment或者是Service),实际上我们都是在Activity中进行的处理,那些在Fragment、Service中可能出现的问题也都不复存在。
贴上PermissionRequest的完整代码:

/**
 * Created by Wangzf on 2017/5/15.
 * 运行时权限请求处理的封装类
 */

public class PermissionRequest {

    private Context mContext;
    private String[] mDangerousPermissions;
    private CallBack mCallBack;

    //未被允许的权限列表
    private List<String> mDeniedPermissions;

    private PermissionRequest(Context context, String[] dangerousPermissions) {
        mContext = context.getApplicationContext();
        mDangerousPermissions = dangerousPermissions;
    }

    public void request() {
        checkDangerousPermissions();

        if (mDeniedPermissions.isEmpty()) {
            mCallBack.onGranted();
        } else {
            startPermissionRequest();
        }
    }

    private void checkDangerousPermissions() {
        if (mDeniedPermissions == null) {
            mDeniedPermissions = new ArrayList<>();
        } else {
            mDeniedPermissions.clear();
        }

        for (int i = 0; i < mDangerousPermissions.length; i++) {
            if (!checkDangerousPermission(mDangerousPermissions[i])) {
                mDeniedPermissions.add(mDangerousPermissions[i]);
            }
        }
    }

    private void startPermissionRequest() {
        RequestPermissionActivity.setRequestCallBack(mCallBack);

        Intent requestIntent = new Intent(mContext, RequestPermissionActivity.class);
        requestIntent.putExtra(Const.DENIED_PERMISSIONS, (Serializable) mDeniedPermissions);
        mContext.startActivity(requestIntent);
    }

    /**
     * 判断传入的权限是否已经被允许
     * @param dangerousPermission
     * @return 若该权限已经被允许,返回true;否则返回false
     */
    private boolean checkDangerousPermission(String dangerousPermission) {
        if (ContextCompat.checkSelfPermission(mContext, dangerousPermission) ==
                PackageManager.PERMISSION_GRANTED) {
            return true;
        }
        return false;
    }

    public interface CallBack {

        void onSuccess(String permission);
        void onFail(String permission);
        //所有申请的权限被允许
        void onGranted();
    }

    public static class Builder {

        private PermissionRequest mPermissionRequest;

        public Builder(Context context, String[] dangerousPermissions) {
            mPermissionRequest = new PermissionRequest(context, dangerousPermissions);
            mPermissionRequest.mCallBack = new CallBack() {
                private static final String TAG = "CallBack";

                @Override
                public void onSuccess(String permission) {
                    Log.i(TAG, "onSuccess");
                }

                @Override
                public void onFail(String permission) {
                    Log.i(TAG, "onFail");
                }

                @Override
                public void onGranted() {
                    Log.i(TAG, "onGranted");
                }
            };
        }

        public Builder setCallBack(CallBack callBack) {
            mPermissionRequest.mCallBack = callBack;

            return this;
        }

        public PermissionRequest build() {
            return mPermissionRequest;
        }
    }
}

代码都很简单,也没什么好讲的。
一个小细节是由于在创建PermissionRequest时传递了Context参数,为了防止内存泄漏,在PermissionRequest的构造方法中做了处理:

    private PermissionRequest(Context context, String[] dangerousPermissions) {
        mContext = context.getApplicationContext();
        mDangerousPermissions = dangerousPermissions;
    }

最终赋给mContext的是应用的上下文而不是传入的context。考虑到在PermissionRequest中需要使用到mContext的场景,无非是在判断应用是否拥有权限以及启动新的Activity时使用,使用应用上下文完全可以满足要求。
需要注意的是在Builder的构造方法中,给mPermissionRequest的mCallBack创建了一个默认实现,这个默认实现主要的目的在于在没有给PermissionRequest设置CallBack的情况下,RequestPermissionActivity的onRequestPermissionsResult()方法可以照常调用CallBack的对应方法,不用额外判断CallBack是否为空。
此外,由于CallBack中的具体方法最终是在RequestPermissionActivity中调用的,因此还必须解决如何将CallBack传递给RequestPermissionActivity的问题,这个问题我始终想不到什么好的解决方式,最终只能粗暴地在RequestPermissionActivity中实现一个静态的setRequestCallBack方法,在PermissionRequest中调用这个方法将CallBack传递给RequestPermissionActivity。
贴下RequestPermissionActivity的代码:

public class RequestPermissionActivity extends AppCompatActivity {

    private static PermissionRequest.CallBack mRequestCallBack;

    //未被允许的权限列表
    private List<String> mDeniedPermissions;
    private String[] mDeniedPermissionsArray;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getDatas();

        ActivityCompat.requestPermissions(RequestPermissionActivity.this, mDeniedPermissionsArray, Const.REQUEST_FIRST);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        switch (requestCode) {
            case Const.REQUEST_FIRST:
                boolean granted = true;
                for (int i = 0; i < grantResults.length; i++) {
                    if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                        mRequestCallBack.onSuccess(permissions[i]);
                    } else {
                        granted = false;
                        mRequestCallBack.onFail(permissions[i]);
                    }
                }
                if (granted) {
                    mRequestCallBack.onGranted();
                }
                finish();
                break;
            default:
        }
    }

    private void getDatas() {
        Intent requestIntent = getIntent();

        mDeniedPermissions = (List<String>) requestIntent.getSerializableExtra(Const.DENIED_PERMISSIONS);

        mDeniedPermissionsArray = new String[mDeniedPermissions.size()];
        Util.list2Array(mDeniedPermissions, mDeniedPermissionsArray);
    }

    public static void setRequestCallBack(PermissionRequest.CallBack requestCallBack) {
        mRequestCallBack = requestCallBack;
    }

}

这里使用ActivityCompat.requestPermissions方法而不是Activity的requestPermissions方法主要是为了兼容API23以下的情况。

以上是整个封装的流程,因为中间啰啰嗦嗦讲了不少封装时的思考,一个简单的问题也不知不觉说了这么多了。这个封装整体上还是很粗糙,回调接口的设计,尤其是用户响应之后相关的处理这一方面还是有待修改。记录得详细些,也是方便自己之后需要用到的时候还可以挖挖坟。
在封装的时候也参考了不少博文还有初略看了一些开源库的实现,一并感谢各位乐于在网络上分享的大神们。

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

推荐阅读更多精彩内容

  • 一、引言 随着Android6.0发布,系统增加了一些新的特性和功能。这次的发布介绍了一种新的权限机制。用户可以在...
    宇是我阅读 7,288评论 7 41
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,048评论 25 707
  • 一、概述 随着Android 7.0的发布,Android 6.0的普及速度很快就升上去了,目前Android 6...
    ListenerGao阅读 857评论 3 17
  • 过年给姑姑家拜年时候偶然看见姑姑养的淡紫色风信子,当时一下子就喜欢上她们了。于是我也决定等天气暖和一些,就去买些回...
    夜雨倾晨阅读 166评论 0 0
  • 文/沈麦冬(作者) 樱花飞舞的季节,是我们长久的约定,粉色的花瓣飘飞,灵动了记忆,却沉静成一种深刻的纪念。 漫天粉...
    启点考证阅读 657评论 0 0