一、背景

Android app开发始终绕不开申请权限,而申请权限的代码与业务代码耦合在一起早已让开发者们深恶痛绝,于是就诞生了一些方便开发者操作的权限框架,并且不断有新的优化被提出用于解决框架的不足。然而时至今日,还是很难看到一款真正完全业务解耦,并能够处理重复和连续权限请求的框架。

本文详细分析了现有Android权限请求方式存在的痛点,并在此基础上,封装了一个便捷实用的权限请求框架,尤其在处理连续请求的设计上做足了文章。全文阅读大约需要7分钟。

 二、市场上的Android权限请求方式

一般来说,android权限请求的方式包括传统方式、注解方式、代理Activity方式、代理fragment方式四种。它们常常因为需要相互弥补缺陷,而同时出现在同一个项目中,被各方业务穿插使用,缺乏一个统一的调用方式和回调路径,使得业务代码杂乱、不易维护。下面就一一分析它们的缺陷。


1、传统方式,直接需要申请权限的在业务代码中,调用activity或者fragment的requestPermission方法,


requestPermissions (@NonNull String[] permissions, int requestCode)

然后在activity或者fragment的onRequestPermissionResult()中方法获取请求结果。这种方式缺陷在于,每个需要申请权限的业务依附的activity或者fragment都得写一对requestPermission()和onRequestPermissionResult()方法,即便是在基类BaseActivity和BaseFragment中统一写上一套这样操作方法,供各个业务子类去复用,就像下面列出的对定位权限的请求操作,为了避免写大量重复代码,在基类BaseFragment中关于做了实现,供业务子类使用。

public class BaseFragment extends Fragment {  ...    //检查定位权限    protected boolean checkLocationPermission() {        return ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED                && ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;    }    //记录请求定位权限的bizType    private String locateBizType = null;    //请求定位权限    protected void requestLocationPermission() {        requestLocationPermission(LocateBizType.TYPE_DEFAULT);    }    //请求定位权限    protected void requestLocationPermission(String bizType) {        if (!TextUtils.isEmpty(locateBizType)) {            //防止重复请求            return;        }        locateBizType = bizType;        requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, Constants.LOCATION_REQUEST_CODE);    }    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,                                           @NonNull int[] grantResults) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults);        if (requestCode == Constants.LOCATION_REQUEST_CODE) {            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {                if (checkLocationPermission()) {                    //检查gps权限                    if (DeviceUtils.isGPSEnabled(getContext())) {                        requestLocationPermissionSucceed(locateBizType);                    } else {                        showOpenGPSSettingDialog();                        requestLocationPermissionFailed(locateBizType, "定位失败,未开启位置信息");                    }                } else {                    CommonUtil.showToast("打开定位权限失败");                    requestLocationPermissionFailed(locateBizType, "定位失败,打开定位权限失败");                }            } else {                CommonUtil.showToast("打开定位权限失败");                requestLocationPermissionFailed(locateBizType, "定位失败,打开定位权限失败");            }            locateBizType = null;        }    }    //请求定位权限成功    protected void requestLocationPermissionSucceed(String locateBizType) {        //供子类回调    }    //请求定位权限失败    protected void requestLocationPermissionFailed(String bizType, String message) {        //供子类回调    }    //开启gps定位服务设置弹窗    protected void showOpenGPSSettingDialog() {        showAlert(getString(R.string.tips_location_closed), getString(R.string.tips_open_location),                "去设置", getString(R.string.cancel), true,                (dialog, i) -> {                    DeviceUtils.gotoLocationSettings(getContext());                    dialog.dismiss();                }, (dialog, i) -> dialog.dismiss()        );    }    ...}

但是,基类方式对于输入输出闭环路径的长度却无能为力,比如需要申请权限的业务类并不直接是activity和fragment,而是一个独立的View或者供h5/rn调用的Plugin方法体中,输入输出的路径就会有很大的跨度,首先请求者requester获取activity/fragment实例,并让activity/fragment发起requestPermission()请求,framework收到请求后进行处理,并将结果回调给activity/fragment的onRequestPermissionsResult()方法,最后activity/fragment再把结果传给requester,一旦requester到达activity/fragment的路径较深,甚至是跨组件访问的,数据就只能经过层层传递后才能传递给发起者,对于大型项目而言,这无疑是致命的。

Android 实现应用通知权限案例 android获取通知权限_List

2、通过自定义注解的方式,申请权限的操作封装在一个责任类中,下面例子中类名为PermissionGen,业务方requester在申请权限时,把它依附的activity和自身类对象传入到PermissionGen中,在PermissionGen中调用activity.requestPermission()发起请求,接下来,系统响应请求的回调当然还是在activity的onRequestPermissionsResult()中给回。

public class PermissionGen {    @TargetApi(value = Build.VERSION_CODES.M)    private static void requestPermissions(Object object, int requestCode, String[] permissions, PermissionRequestListener callback) {        List deniedPermissions = Utils.findDeniedPermissions(getActivity(object), permissions);        if (deniedPermissions.size() > 0) {            if (object instanceof Activity) {                ActivityCompat.requestPermissions(((Activity) object), deniedPermissions.toArray(new String[0]), requestCode);            } else if (object instanceof Fragment) {                ((Fragment) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);            }        } else {            if (callback != null) {                callback.permissionGranted(requestCode);            } else {                doExecuteSuccess(object, requestCode);            }        }    }    private static void doExecuteSuccess(Object activity, int requestCode) {        Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(), PermissionSuccess.class, requestCode);        executeMethod(activity, executeMethod);    }    private static void doExecuteFail(Object activity, int requestCode) {        Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(), PermissionFail.class, requestCode);        executeMethod(activity, executeMethod);    }    private static void executeMethod(Object activity, Method executeMethod) {        try {            if (!executeMethod.isAccessible()) executeMethod.setAccessible(true);            executeMethod.invoke(activity);        } catch (Exception e) {            e.printStackTrace();        }    }    public static void onRequestPermissionsResult(Activity obj, int requestCode, String[] permissions, int[] grantResults) {        List 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);        }    }}

Android 实现应用通知权限案例 android获取通知权限_ide_02

与传统的方法不同的在于,自定义了请求成功与失败的两个注解@PermissionSuccess、@PermissionFail,activity通过onRequestPermissionsResult()拿到结果后,并不直接由activity把result传递给requester,而是把result再回给PermissionGen,再由PermissionGen通过反射调用注解方法把结果传递requester,完成请求操作。例如下面某个业务Activity中,

public class MainActivity extends BaseActivity {    @Override    public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {        PermissionGen.onRequestPermissionsResult(this, requestCode, permissions, grantResults);    }    @PermissionSuccess(requestCode = 200)    public void PermissionGranted() {        //处理同意权限    }    @PermissionFail(requestCode = 200)    public void PermissionDeny() {       //处理拒绝权限    }}

这种方式虽然避免了requester和宿主activity之间的层层传递,但是仍然不够,还是得依靠activity的onRequestPermissionsResult()做中转,另外使用反射也带来了一定的开销问题。

3、通过代理Actvity的方式,每次需要申请权限时,都新开一个PermissionActivity,专职于权限操作,

public static void requestPermissions(Context context, IPermissionCallBack permissionCallBack, RationaleType[] types, String[] permissions) {    if (context == null) {        Log.w(TAG, "Can't check permissions for null context");        return;    }    if (checkPermissions(context, permissions)) {        Log.w(TAG, "permissions has been granted");        permissionCallBack.onPermissionsGranted(true, null);        return;    }    PermissionActivity.start(context, permissions, types, permissionCallBack);}

外部只需要在跳转至PermissionActivity时,做好Callback回调监听(避免使用onActivityResult()传值),所有的输入输出都在这个代理activity中完成,不存在大跨度传输路径的问题。

public class PermissionActivity extends FragmentActivity implements EasyPermissions.PermissionCallbacks {    private IPermissionCallBack mCallBack;...    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        EasyPermissions.requestPermissions(new PermissionRequest.Builder(this, REQUEST_CODE_PERMISSION, requestPermissions)                .setRationale(rationale)                .setTheme(AlertDialog.THEME_DEVICE_DEFAULT_DARK)                .build());    }        @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults);        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);    }    @AfterPermissionGranted(REQUEST_CODE_PERMISSION)    public void onPermissionsAllGranted() {        Log.w(TAG, "onPermissionsAllGranted");        if (mCallBack != null) mCallBack.onPermissionsGranted(true, null);        finish();    }        @Override    public void onPermissionsDenied(int requestCode, @NonNull List perms) {        Log.w(TAG, "onPermissionsDenied");        if (mCallBack != null) mCallBack.onPermissionsGranted(false, perms);        finish();    }...}

但这种方式的缺陷也是显而易见的,只要是申请权限,就得新开一个activity,虽然可以设置activity为透明无View的UI样式,但是一方面开销过大,一方面不可避免地要污染activity返回栈(迫使requester所在activity切到后台),另一方面也要污染requester的activity/fragment生命周期。而且一旦后台activity因为不保留活动等原因被意外销毁了,就无法收到PermissionActivity给予的回调了。

Android 实现应用通知权限案例 android获取通知权限_子类_03

4、使用代理fragment方式,这种方式被广泛应用到市面上的各种三方框架中,比较出名的有[谷歌的easypermssions框架](https://github.com/googlesamples/easypermissions),其内部就是使用一个代理fragment来做权限请求的载体。代理fragment,与代理Activity类似的是,权限请求操作和数据传输可以都在内部的Fragment内部完成,外部只用设置好回调监听即可,但与代理Activity不同的在于,使用代理fragment不用跳转activity,fragment依附在requester自己的activity上,不存在污染生命周期的问题,也不用担心activity被意外销毁。下面例子中把代理fragment定义在一个PermissionHelper类中,

private static void doRequest(final Activity requestHost, final String[] permissions, boolean mIsShowDialog, final PermissionHelper.PermissionCallback callback) {    // 通过Fragment请求权限    PermissionHelper.PermissionInnerFragment innerFragment = new PermissionHelper.PermissionInnerFragment();    innerFragment.setPermissionCallback(callback);    innerFragment.setFragmentActivity((FragmentActivity) requestHost);    FragmentManager fragmentManager = ((FragmentActivity) requestHost).getSupportFragmentManager();    fragmentManager.beginTransaction()            .add(innerFragment, PERMISSION_REQUEST_TAG)            .commitAllowingStateLoss();    fragmentManager.executePendingTransactions();    innerFragment.requestPermissions(permissions, PERMISSION_REQUEST_CODE);    for (String permission : permissions) {        SharedPreferences settings = getSP();        if(settings != null){            settings.edit().putString(permission,"1").commit();        }    }}

另外,android系统在申请权限时,会提供用户“禁止后不再提示”的选项,当用户点击这个选项后,业务方再一次申请该权限时,系统就不会给向用户弹窗申请权限的请求框。然而往往进入app时,用户认为某项权限是无用的,就点击了“禁止后不再提示”,而后使用过程中,又改变了主意,是愿意同意权限的,但是这时已经没有了申请权限的机会了。甚至会出现用户点了某个ui,点击事件需要具备某个权限后才能进行响应操作,这时候就出现了无响应的情况,严重影响了用户体验。比如用户对定位权限“禁止不再提示”,然后在地图业务中,又点击定位按钮,就会无响应。相比于前面3种方式均没有对“禁止后不再提示”的操作边界进行处理,笔者公司使用的框架PermissionHelper内部设置了开放配置mIsShowDialog,供业务去设置。当请求权限时,如果外部配置mIsShowDialog为true,并且所请求的权限处于“禁止后不再提示的状态”,就会展示一个引导弹窗,引导用户去设置页开启权限,其具体逻辑如下,

List<String> deniedPermissionsList = PermissionUtils.getDeniedPermissions();String[] deniedPermissionsArr = deniedPermissionsList.toArray(new String[deniedPermissionsList.size()]);if (deniedPermissionsArr.length > 0) {    PermissionUtils.sortUnshowPermission(requestHost, deniedPermissionsArr);}if (PermissionUtils.getUnshowedPermissions().size() > 0) {    List<String> unShowPermissionsList = PermissionUtils.getUnshowedPermissions();    //如果SharePreference中已存在该permission,说明不是首次检查,可弹出自定义弹框,否则首次只弹系统弹框,不弹自定义弹框    int len = PermissionUtils.getUnshowedPermissions().size();    boolean isCanShow=false;    for (int i=0;i        String permission=PermissionUtils.getUnshowedPermissions().get(i);        SharedPreferences settings = getSP();        if(settings != null && settings.contains(permission)){            isCanShow=true;        }    }    if(mIsShowDialog && isCanShow) {        StringBuilder message = getUnShowPermissionsMessage(unShowPermissionsList);        showMessageGotoSetting(message.toString(), requestHost);    }}

Android 实现应用通知权限案例 android获取通知权限_在获取权限的时候调用方法_04

然而,上面的例子中,和easypermissions这类框架一样,也有不足的地方:

其一,每次请求权限时都需要新建fragment添加到当前页面,没有进行复用设计,在频繁请求权限的业务中会在activity上添加多个fragment实例,增加内存消耗,而且每次添加的fragment所配对的tag都一样,这样只有最后一个被添加fragment会被tag关联,其他的fragment就失去了管控;另外,如果当前activity正在请求权限过程,发生了recreate(方向、配置等发生改变导致),那么fragment也会跟着重建,这时fragment内部的callback等数据会丢失,导致无法给外部回调。

其二,虽然PermissionHelper兼容了“禁止后不再提示”的操作边界,能够引导用户去设置页开启权限,不至于发生“无响应”情况,但是对用户在设置页中发生的权限操作没有做统一监听,仍然业务方回到自己所在的activity/fragment中手动实现监听处理,这时就出现了跟传统请求方式一样,重复业务代码多,数据传输路径跨度大的问题;

其三,PermissionHelper在兼容“禁止后不再提示”的操作边界的方案中,通过SharePrefence的将的请求权限状态记录到磁盘中,一方面读取磁盘文件是非常耗时的一个操作,会加大主线程开销,同时磁盘文件有丢失的风险,比如有些app中会在设置页中提供了“清除缓存”的操作按钮,用户可以主动删除磁盘文件。一旦这样操作后,就有可能出现申请权限时“无响应”的情况;

其四,PermissionHelper无法处理重复请求和连续请求,系统在面对重复请求和连续请求时,会直接回绝后来的请求。例如,笔者曾见过有项目中,由于h5/rn在就会重复和连续像native侧请求权限,对这种情况,采用了比较极端的方式来处理这种场景,在收到请求时会把被请求的权限做一个标记,当下次h5/rn再请求这个权限时,如何是已标记过的权限,不管之前的请求成功与否,就直接进行拦截,不给予请求了。这无疑是武断的,甚至很不合理,因为只要用户点了拒绝权限,后面再想点击同意权限的机会都没有了。更进一步地,当h5/rn发起连续请求时,后发出的请求被系统丢弃后,而该权限又被标记为已进行过请求,这时就会出现,用户一次都还没有选择过要不要同意权限,就再没有选择的机会了。

public class Leoma {    ...    private ArrayList<String> checkedPermission;//询问过授权的权限,不管是否授权成功    public boolean isPermissionChecked(String permission) {        return checkedPermission.contains(permission);    }    ...}

三、设计方案

基于前文对项目中各个权限申请方式的分析,代理fragment方式,相比另外三种方式,对业务方更友好,主要表现为重复业务代码少、回调路径短、使用方便等。但是该方案如果直接在app业务中使用,也存在着前文罗列的多个缺陷,表现为开销大,封装度不够,不能处理重复请求和连续请求等。本框架的设计方案,就是在它的基础上,进行一番优化改造,解决上述缺陷,使之满足业务方更苛刻的应用场景。

1、总体上,需要所有权限相关的操作入口,都封装在PermissionUtil工具类中,具体的请求逻辑在代理PermissionFragment中完成,外部只会和PermissionUtil产生联系,PermissionFragment对外部不可见。一个完成的操作流程表现为,业务方在任何需要操作权限的地方,向PermissionUtil的入口方法发一个Requester请求,发起时将权限类型、提示文案、回调接口传入,然后PermissionUtil内部进行数据解析、权限检查后,在Requester所在的当前activity中添加代理PermissionFragment,与Framework申请交互与请求返回的逻辑都在PermissionFragment内部完成,完成后把请求结果回调给通知外部监听,业务方收到监听后,根据请求结果完成相应的业务逻辑。 

Android 实现应用通知权限案例 android获取通知权限_子类_05

同时,设置fragment的retainInstance属性为true,可以有效防止当前activity出现销毁或者重建意外情况时,fragment能给得以保留,内部数据不丢失,外部回调还能照常进行。

override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    retainInstance = true}

2、PermissionConstants常量表设计,为了让业务方编写代码尽量简单,对Manifest.permission权限类型常量表进行了映射封装,业务方申请权限时,只需要传入具体权限的类型key即可,尤其是需要一次性请求多个权限时,通过位运算合并要请求的各个权限类型为一个混合类型值,框架内部会反位运算进行类型解析,

val permissionTable = mutableMapOf(        PermissionType.WRITE_EXTERNAL_STORAGE to Manifest.permission.WRITE_EXTERNAL_STORAGE,        PermissionType.ACCESS_FINE_LOCATION to Manifest.permission.ACCESS_FINE_LOCATION,        PermissionType.ACCESS_COARSE_LOCATION to Manifest.permission.ACCESS_COARSE_LOCATION,        PermissionType.READ_PHONE_STATE to Manifest.permission.READ_PHONE_STATE,        PermissionType.READ_CONTACTS to Manifest.permission.READ_CONTACTS,        PermissionType.RECORD_AUDIO to Manifest.permission.RECORD_AUDIO,        PermissionType.CAMERA to Manifest.permission.CAMERA)class RationaleType {    companion object {        //日历        const val CALENDAR = 1 shl 0        //相机        const val CAMERA = 1 shl 1        //联系人        const val CONTACTS = 1 shl 2        //定位        const val LOCATION = 1 shl 3        //麦克风        const val MICROPHONE = 1 shl 4        //打电话        const val PHONE = 1 shl 5        //传感器        const val SENSORS = 1 shl 6        //短信        const val SMS = 1 shl 7        //数据读写        const val STORAGE = 1 shl 8        //悬浮窗        const val WINDOW = 1 shl 9    }}

举例:

a)业务方传值

val permissionType = PermissionConstants.PermissionType.WRITE_EXTERNAL_STORAGE or PermissionConstants.PermissionType.ACCESS_FINE_LOCATIONval rationaleType = PermissionConstants.RationaleType.STORAGE or PermissionConstants.RationaleType.LOCATIONrequestPermissions(activity, permissionType, rationaleType, null)

b)框架解析

private fun getPermissionsByType(permissionType: Int): MutableList {    val permissions = mutableListOf()    for ((k, v) in PermissionConstants.permissionTable) {        if (permissionType and k == k) {            permissions.add(v)        }    }    return permissions}

3、PermissionFragment复用设计,首先通过checkPermissionInner()方法,对业务方传入的权限类型进行解析,并过滤掉已经被授权过的权限,然后在业务方当前activity中添加PermissionFragment,添加时无需设置ViewGroup充当container,只是通过add(fragment, tag)设置一个与fragment关联的tag,这样添加到activity不会展示任何视图,又会完整的保留fragment的生命周期。添加PermissionFragment时,首先看当前activity是否已经添加过PermissionFragment,如果是,则复用先前的fragment,没有添加过才新创建fragment,避免重复创建。

/** * 发起请求 */private fun startRequest(activity: FragmentActivity?, permissionType: Int, rationaleType: Int? = null,                         permissionCallBack: IPermissionCallBack? = null) {    if (activity == null || activity.isFinishing) {        Log.w(TAG, "Aborting! activity is finishing when requesting permission")        return    }    var permissions = getPermissionsByType(permissionType)    sendRequestLog(permissions)    if (permissions.isEmpty()) {        Log.w(TAG, "Can't check permissions for empty size")        return    }    permissions = checkPermissionsInner(activity, permissions)    if (permissions.isEmpty()) {        Log.w(TAG, "permissions has been granted")        permissionCallBack?.onPermissionsGranted(true, permissions)        return    }    val fragmentTag = "PermissionFragment"    val fragment: PermissionFragment    val fragmentManager = activity.supportFragmentManager    if (fragmentManager.findFragmentByTag(fragmentTag) != null) {        fragment = fragmentManager.findFragmentByTag(fragmentTag) as PermissionFragment    } else {        fragment = PermissionFragment()        fragmentManager.beginTransaction().add(fragment, fragmentTag).commitAllowingStateLoss()        fragmentManager.executePendingTransactions()    }    fragment.requestPermissions(permissions, getRationalesByType(activity, rationaleType), permissionCallBack)}

4、处理“禁止后不再提示”,业务方在向框架发起权限请求时,会一并传入rationaleType字段,框架内部会rationaleType进行解析组装为对应的文案,用于当申请的权限状态为“禁止后不再提示”时,向用户展示提醒,并引导用户去系统设置页开启权限,

//申请的权限之前,已经属于"禁止后不再提示"的状态,这时候为了让用户有感知,给予弹窗rationale文案进行引导提示Log.w(logTag, "one or more permissions have been denied with no longer prompt")try {    val rationale = String.format(getString(R.string.setting_tip), request?.mRationale)    val builder = AlertDialog.Builder(mContext).setTitle("").setMessage(rationale)    builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->        startSettingActivity()        dialog.dismiss()    }    builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->        notifyObserver(requestCode, request, allGranted, permissions.toList())        dialog.dismiss()    }    builder.setCancelable(false)    builder.show()} catch (e: Exception) {    e.printStackTrace()}

通过系统提供的shouldShowRequestPermissionRationale()方法,当该方法访问true表示该权限只是被禁止,返回false表示该权限被设置为禁止后不再提示。

public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {    if (mHost != null) {        return mHost.onShouldShowRequestPermissionRationale(permission);    }    return false;}

但光靠shouldShowRequestPermissionRationale()还不够,因为如果是首次申请某个权限时就被设置了“禁止后不再提示”,这时是用户的主动行为,不会出现“无响应”,是不需要弹引导提示的。只有在已经是“禁止后不再提示“的权限”才需要展示rationale文案,这里为了不借助额外的文件数据存储,采取权限在请求前和请求后的状态结合推断的方案。如果被请求的权限在请求前和请求后,shouldShowRequestPermissionRationale()都返回false,那么就触发展示rationale文案,让用户有交互响应。弹窗中可以引导用户一键跳入设置页,对权限进行授权。

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {    super.onRequestPermissionsResult(requestCode, permissions, grantResults)    isBusy.set(false)    Log.w(logTag, "requestPermissions end")    if (requests.indexOfKey(requestCode) < 0) {        return    }    val request = requests[requestCode]    var allGranted = false    val length = grantResults.size    for (i in 0 until length) {        if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {            allGranted = false            break        }        allGranted = true    }    when {        allGranted -> {            //点击了同意权限            if (isXiaoMiBrand()) {                allGranted = doubleCheckPermissionGranted(permissions)            }            Log.w(logTag, "all permissions have been granted")            notifyObserver(requestCode, request, allGranted, permissions.toList())        }        shouldShowRationale(permissions) -> {            //点击了禁止权限            notifyObserver(requestCode, request, allGranted, permissions.toList())            Log.w(logTag, "one or more permissions have been denied")        }        request.mShouldShowRationale -> {            //点击了禁止后不再提示,本次操作后变为"禁止后不再提示"的状态,再次申请该权限时,就需要给予rationale弹窗引导            notifyObserver(requestCode, request, allGranted, permissions.toList())            Log.w(logTag, "one or more permissions have been denied")        }        TextUtils.isEmpty(request?.mRationale) -> {            //没有设置引导文案,即便属于"禁止后不再提示"的状态,也没法弹窗引导            notifyObserver(requestCode, request, allGranted, permissions.toList())        }        else -> {            //申请的权限之前,已经属于"禁止后不再提示"的状态,这时候为了让用户有感知,给予弹窗rationale文案进行引导提示            Log.w(logTag, "one or more permissions have been denied with no longer prompt")            try {                val rationale = String.format(getString(R.string.setting_tip), request?.mRationale)                val builder = AlertDialog.Builder(mContext).setTitle("").setMessage(rationale)                builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->                    startSettingActivity()                    dialog.dismiss()                }                builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->                    notifyObserver(requestCode, request, allGranted, permissions.toList())                    dialog.dismiss()                }                builder.setCancelable(false)                builder.show()            } catch (e: Exception) {                e.printStackTrace()            }        }    }}

5、处理设置页操作统一监听,首先框架内的请求表会记录当前正在请求的权限,引导弹窗通过startActivityForResult()引导用户进入系统设置页授权,当从设置页返回app时,会触发内部的onActivityResult()方法,方法中完成授权结果检查,并直接把结果回调给外部,如此可以避免业务方丢失用户设置页发生的授权操作,也避免了业务方需要再手动去监听处理用户设置页的操作。

//记录当前正在发起requestprivate var requests = SparseArray()/** * 跳转到设置页面打开权限 */private fun startSettingActivity() {    try {        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" +                mContext.packageName))        intent.addCategory(Intent.CATEGORY_DEFAULT)        startActivityForResult(intent, PermissionConstants.REQUEST_CODE_PERMISSION_SETTING)    } catch (e: Exception) {        e.printStackTrace()    }}/** * 设置页返回 */override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {    super.onActivityResult(requestCode, resultCode, data)    if (requestCode != PermissionConstants.REQUEST_CODE_PERMISSION_SETTING) {        return    }    Log.d(logTag, "the results for request permissions from system setting page")    for (i in 0 until requests.size()) {        val request = requests.valueAt(i)        val grantedAll = PermissionUtil.checkPermissions(mContext, request.mPermissions)        notifyObserver(requests.keyAt(i), request, grantedAll, request.mPermissions?.toList())    }}

6、处理重复请求和连续请求,app业务中往往存在着对同一权限或者不同权限重复发起请求的场景,尤其是h5和rn通过调用native发起申请权限,native对它们发出请求不可控,往往出现一个请求还没返回,就又发起重复请求,或者连续发起不同权限请求的情况。在前面痛点分析时已经说到,对这种情况,目前项目会直接丢弃后发起的请求。

对于重复请求来说,后发起的请求确实显得多余,直接丢弃处理看似合理,实则不然,比如某个rn和h5的混合页面中,rn和h5同时对某个权限发起了请求,但是他们响应请求的处理逻辑是不一样的;对于不同权限的连续请求,那直接抛弃后发起的请求就更不能直接抛弃。

基于此,

a)对重复请求采取“保留相同权限请求的不同回调,在一次请求结果拿到时,通通给予回调”的方案;

b)对连续请求采取“拦截后发起的请求,替代为展示自定义弹窗进行等待,待当前请求完成后,让用户再操作等待中的请求”的方案。

具体为:

i)设计一个请求结构PermissionRequest,包括请求的权限、请求的回调、引导文案、请求前的状态。

class PermissionRequest(        var mPermissions: MutableList<String>?,        var mCallBacks: MutableList?,        var mRationale: String?,        var mShouldShowRationale: Boolean)

ii)每收到一个权限请求,就会生成一个唯一的requestCode,与之关联,然后被记录到requests表中。并且该requestCode会被用于向系统申请权限的requestCode,之后要取request时,只需要凭对应的requestCode就可取到对应的request。

//记录当前正在发起requestprivate var requests = SparseArray()//请求码private var requestCode = PermissionConstants.REQUEST_CODE_PERMISSION/** * 随机生成requestCode */private fun makeRequestCode(): Int {    if (requests.size() <= 0) {        return PermissionConstants.REQUEST_CODE_PERMISSION    }    //随机生成唯一的requestCode,最多尝试10次    var requestCode: Int    var tryCount = 0    do {        requestCode = Random.nextInt(0x0000FFFF)        tryCount++    } while (requests.indexOfKey(requestCode) >= 0 && tryCount < 10)    return requestCode}//向系统发起请求时,带入该requestCoderequestPermissions(permissions.toTypedArray(), requestCode)

iii)当收到相同权限的重复请求时,通过去重操作过滤请求,但是会将其callback添加相同请求request的回调表mCallBacks中,待当前请求结果回来时,一一给予回调。

/** * 过滤正在请求的权限,防止相同重复请求 * 但是保留它的callback,在请求结果回来后,一同回调 */private fun filterPermission(permissions: MutableList<String>, callBack: IPermissionCallBack?): MutableList {    if (permissions.isEmpty()) {        return mutableListOf()    }    for (i in 0 until requests.size()) {        val request = requests.valueAt(i)        request.mPermissions?.let {            if (it.containsAll(permissions)) {                if (request.mCallBacks.isNullOrEmpty()) {                    request.mCallBacks = mutableListOf(callBack)                } else {                    request.mCallBacks!!.add(callBack)                }                return mutableListOf()            }        }    }    return permissions}/** * 对重复request,它的mCallbacks中可能有多个回调,一一执行 */private fun notifyObserver(requestCode: Int, request: PermissionRequest?, grantedAll: Boolean, permissions: List<String>?) {    requests.remove(requestCode)    val callbacks = request?.mCallBacks    callbacks?.let {        for (callback in callbacks) {            Log.d(logTag, "notifyObserver result of ${request.mRationale} is $grantedAll")            callback?.onPermissionsGranted(grantedAll, permissions)        }    }}

iv)内部通过一个isBusy字段标记框架当前是否处于忙绿状态,正在对某个权限发起请求时,就处于忙绿状态,而请求结束后恢复正常状态。如果框架当前忙绿中,又收到了其他权限的请求,就被称作是连续请求,这时会拦截后发起的请求,替代为展示自定义弹窗进行等待的方式,让用户在处理完当前请求后,还能继续选择处理下一个请求,避免丢失请求。

//是否正在请求中private var isBusy = AtomicBoolean(false)fun requestPermissions(permissions: MutableList<String>, rationale: String?, callBack: IPermissionCallBack?) {    Log.w(logTag, "requestPermissions start")    val mPermissions = filterPermission(permissions, callBack)    if (mPermissions.isEmpty()) {        return    }    this.requestCode = makeRequestCode()    requests.put(requestCode, PermissionRequest(permissions, mutableListOf(callBack), rationale, shouldShowRationale(permissions.toTypedArray())))    if (isBusy.compareAndSet(false, true)) {        requestPermissions(permissions.toTypedArray(), requestCode)    } else {        showWaitDialogIfBusy(requestCode)    }}/** * 如果fragment正在忙碌中,展示等待弹窗 */private fun showWaitDialogIfBusy(requestCode: Int) {    val request = requests[requestCode]    val permissions = request.mPermissions    if (permissions.isNullOrEmpty()) {        requests.remove(requestCode)        return    }    val rationale = String.format(getString(R.string.rationale), request?.mRationale)    val builder = AlertDialog.Builder(mContext).setTitle("").setMessage(rationale)    builder.setPositiveButton(getString(R.string.ok)) { dialog, _ ->        isBusy.set(true)        requestPermissions(permissions.toTypedArray(), requestCode)        dialog.dismiss()    }    builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->        isBusy.set(false)        notifyObserver(requestCode, request, false, permissions.toList())        dialog.dismiss()    }    builder.setCancelable(false)    builder.show()}

 四、总结

本文设计了一款服务于Android权限请求的框架,重点解决现有框架和方式的痛点。对使用者来说,具有完全业务结耦、调用方式简单、更优雅的“禁止后不再提示”处理、支持处理重复请求和连续请求。

1、申请权限具体操作放到PermissionFragment中进行,使业务方能直接在当前页完成权限操作,数据传输链路直观清晰、无污染。

2、PermissionUtil作为app所有权限操作的统一收口,使得业务方在在需要权限操作时变得方便简单,项目代码变得规范易维护。

3、对“禁止后不再提示”的权限,再次发起请求时,为了不出现无响应的用户体验,展示引导弹窗让用户有感知,并引导用户去设置页中开启权限,并在内部统一完成设置页用户操作监听,完成请求,避免无响应。

4、对相同权限的重复请求,进行合并,在一次请求结果回来后,一一回调各个请求,避免重复请求的同时还能一一给予回调。

5、对不同权限的连续请求,不简单丢弃后发起的请求,展示自定义的等待请求弹窗,让用户在前一个请求结束后,还能再处理后一个请求,不丢失任何请求,保证用户体验。