一、背景
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的路径较深,甚至是跨组件访问的,数据就只能经过层层传递后才能传递给发起者,对于大型项目而言,这无疑是致命的。
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); } }}
与传统的方法不同的在于,自定义了请求成功与失败的两个注解@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给予的回调了。
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); }}
然而,上面的例子中,和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_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、对不同权限的连续请求,不简单丢弃后发起的请求,展示自定义的等待请求弹窗,让用户在前一个请求结束后,还能再处理后一个请求,不丢失任何请求,保证用户体验。