Android权限请求,看这篇就够了

9,162 阅读18分钟

查看该框架的github地址

一、背景

Android app开发始终绕不开申请权限,而申请权限的代码与业务代码耦合在一起早已让开发者们深恶痛绝,于是就诞生了一些方便开发者操作的权限框架,并且不断有新的优化被提出用于解决框架的不足。然而时至今日,还是很难看到一款真正完全业务解耦,并能够处理重复和连续权限请求的框架。**本文详细分析了现有Android权限请求方式存在的痛点,并在此基础上,封装了一个便捷实用的权限请求框架,尤其在处理连续请求的设计上做足了文章。**全文阅读大约需要7分钟。

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

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

1、传统方式,直接在业务类中,调用activity或者fragment的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的路径较深,甚至是跨组件访问的,数据就只能经过层层传递后才能传递给发起者,对于大型项目而言,这无疑是致命的。

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<String> 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<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);
        }
    }
}

与传统的方法不同的在于,自定义了请求成功与失败的两个注解@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<String> 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给予的回调了。

4、使用代理fragment方式,这种方式被广泛应用到市面上的各种三方框架中,比较出名的有谷歌的easypermssions框架,其内部就是使用一个代理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<len;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);
    }
}

然而,上面的例子中,和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内部完成,完成后把请求结果回调给通知外部监听,业务方收到监听后,根据请求结果完成相应的业务逻辑。  同时,设置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_LOCATION
val rationaleType = PermissionConstants.RationaleType.STORAGE or PermissionConstants.RationaleType.LOCATION
requestPermissions(activity, permissionType, rationaleType, null)

b)框架解析

private fun getPermissionsByType(permissionType: Int): MutableList<String> {
    val permissions = mutableListOf<String>()
    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()方法,方法中完成授权结果检查,并直接把结果回调给外部,如此可以避免业务方丢失用户设置页发生的授权操作,也避免了业务方需要再手动去监听处理用户设置页的操作。

//记录当前正在发起request
private var requests = SparseArray<PermissionRequest>()

/**
 * 跳转到设置页面打开权限
 */
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同时对某个权限发起了请求,但是他们响应请求的处理逻辑是不一样的;对于不同权限的连续请求,那直接抛弃后发起的请求就更不能直接抛弃。

基于此,

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

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

具体为:

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

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

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

//记录当前正在发起request
private var requests = SparseArray<PermissionRequest>()

//请求码
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
}

//向系统发起请求时,带入该requestCode
requestPermissions(permissions.toTypedArray(), requestCode)

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

/**
 * 过滤正在请求的权限,防止相同重复请求
 * 但是保留它的callback,在请求结果回来后,一同回调
 */
private fun filterPermission(permissions: MutableList<String>, callBack: IPermissionCallBack?): MutableList<String> {
    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)
        }
    }
}

4)内部通过一个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、对不同权限的连续请求,不简单丢弃后发起的请求,展示自定义的等待请求弹窗,让用户在前一个请求结束后,还能再处理后一个请求,不丢失任何请求,保证用户体验。