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

发布时间:2020-02-15 17:15    浏览次数 :

[返回]

写在前面

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

5+ API分模块封装调用了系统各种原生能力,而部分能力需要使用到Android的permissions,以下列出了各模块(或具体API)使用的的权限:

运行时权限

基础权限

5+ App必须使用的到最小权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.INTERNET"/> 允许程序访问网络
ALL <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 允许程序读写扩展存储卡

运行时权限特点

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

Audio

调用plus.audio.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.RECORD_AUDIO"/> 允许程序录制音频
ALL <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> 允许程序修改全局音频设置

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

Camera

调用plus.camera.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.CAMERA"/> 允许程序使用照相设备
ALL <uses-feature android:name="android.hardware.camera"/> 允许程序访问照相设备

权限请求封装

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

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

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

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

Contacts

调用plus.contacts.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.GET_ACCOUNTS"/> 允许程序访问Accounts Service帐户列表
ALL <uses-permission android:name="android.permission.READ_CONTACTS"/> 允许程序读取用户联系人数据
ALL <uses-permission android:name="android.permission.WRITE_CONTACTS"/> 允许程序修改用户联系人数据

封装用法

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

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();

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

Device

调用plus.device.、plus.screen.、plus.display.、plus.networkinfo.、plus.os.*使用到的权限集

API 权限 说明
plus.device.setWakelock(); plus.device.isWakelock(); <uses-permission android:name="android.permission.WAKE_LOCK"/> 允许程序保持进程不进入休眠状态
plus.device.vibrate(); <uses-permission android:name="android.permission.VIBRATE"/> 允许程序访问振动设备
plus.device.* <uses-permission android:name="android.permission.READ_PHONE_STATE"/> 允许程序访问手机状态信息
plus.device.dail(); <uses-permission android:name="android.permission.CALL_PHONE"/> 允许程序不通过拨号界面拨打电话
plus.networkinfo.* <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 允许程序访问Wi-Fi网络状态信息
plus.networkinfo.* <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 允许程序访问有关GSM网络信息

封装分析

可以看到,在使用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以下的情况。

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

Geolocation

调用plus.geolocation.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> 允许程序访问位置信息
ALL <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 允许程序访问CellID或WiFi热点来获取位置信息
ALL <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 允许程序访问Wi-Fi网络状态信息
ALL <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 允许程序访问有关GSM网络信息
ALL <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> 允许程序改变Wi-Fi连接状态
ALL <uses-permission android:name="android.permission.READ_PHONE_STATE"/> 允许程序访问手机状态信息
ALL <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> 允许程序挂载和移除可移动存储设备
ALL <uses-permission android:name="android.permission.READ_LOGS"/> 允许程序读取系统日志文件
ALL <uses-permission android:name="android.permission.WRITE_SETTINGS"/>" 允许程序读取或写入系统设置

Messaging

调用plus.messaging.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.SEND_SMS"/> 允许程序发送SMS短信
ALL <uses-permission android:name="android.permission.READ_SMS"/> 允许程序读取短信息
ALL <uses-permission android:name="android.permission.WRITE_SMS"/> 允许程序写短信

Barcode

调用plus.barcode.*使用到的权限集

API 权限 说明
ALL <uses-permission android:name="android.permission.CAMERA"/> 允许程序使用照相设备
ALL <uses-feature android:name="android.hardware.camera"/> 允许程序访问照相设备
ALL <uses-feature android:name="android.hardware.camera.autofocus"/> 允许程序访问照相设备自动聚焦
ALL <uses-permission android:name="android.permission.FLASHLIGHT"/>" 允许程序访问闪光灯
ALL <uses-permission android:name="android.permission.VIBRATE"/> 允许程序访问振动设备

Map

调用plus.maps.*使用到的权限集

百度地图

API 权限 说明
ALL <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> 允许程序访问位置信息
ALL <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 允许程序访问CellID或WiFi热点来获取位置信息
ALL <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 允许程序访问Wi-Fi网络状态信息
ALL <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 允许程序访问有关GSM网络信息
ALL <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> 允许程序改变Wi-Fi连接状态
ALL <uses-permission android:name="android.permission.READ_PHONE_STATE"/> 允许程序访问手机状态信息
ALL <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> 允许程序挂载和移除可移动存储设备
ALL <uses-permission android:name="android.permission.READ_LOGS"/> 允许程序读取系统日志文件
ALL <uses-permission android:name="android.permission.WRITE_SETTINGS"/>" 允许程序读取或写入系统设置

高德地图

API 权限 说明
ALL <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> 允许程序访问位置信息
ALL <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 允许程序访问CellID或WiFi热点来获取位置信息
ALL <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> 允许程序访问Wi-Fi网络状态信息
ALL <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> 允许程序访问有关GSM网络信息
ALL <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> 允许程序改变Wi-Fi连接状态
ALL <uses-permission android:name="android.permission.READ_PHONE_STATE"/> 允许程序访问手机状态信息
ALL <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> 允许程序挂载和移除可移动存储设备
ALL <uses-permission android:name="android.permission.READ_LOGS"/> 允许程序读取系统日志文件
ALL <uses-permission android:name="android.permission.WRITE_SETTINGS"/>" 允许程序读取或写入系统设置

OAuth

调用plus.oauth.*使用到的权限集

微信

API 权限 说明
ALL <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> 允许程序修改全局音频设置
下一篇:没有了