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