背景
关于运行时的权限不用多说,这个概念已经很久,近期工信部在强推TargetSDK26,我这边做了一些适配工作,其中有一项就是运行时权限,今天将对运行时权限提供一个更优雅的解决方案,如果你还不了解运行时权限,请移步:Android运行时权限浅谈
现状:
以直接调用打电话功能为例
首先我们项目中可能会有这么一个方法:
/** * 拨打指定电话 */ public static void makeCall(Context context, String phoneNumber) { Intent intent = new Intent(Intent.ACTION_CALL); Uri data = Uri.parse("tel:" + phoneNumber); intent.setData(data); if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); } 复制代码
那么在适配动态权限以前,在我们任意用到打电话的业务页面我们可能就是这么用:
public void makeCall() { Utils.makeCall(BeforeActivity.this, "10086"); } 复制代码
于是乎,某一天,我们应用要适配targetSdk 26,首先我们要适配的就是动态权限,所以下面的代码就会变成这样:
public void makeCall() { //6.0以下 直接即可拨打 if (android.os.Build.VERSION.SDK_INT < M) { Utils.makeCall(BeforeActivity.this, "10086"); } else { //6.0以上 if (ContextCompat.checkSelfPermission(BeforeActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(BeforeActivity.this, new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE_CALL); } else { Utils.makeCall(BeforeActivity.this, "10086"); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_CALL) { if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { Toast.makeText(BeforeActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show(); } else { Utils.makeCall(BeforeActivity.this, "10086"); } } } 复制代码
以上就是拨打电话功能新老权限版本的基本实现(还不包括shouldShowRequestPermissionRationale的部分)。 目前也有一些知名的开源库,如PermissionsDispatcher,RXPermission等。虽然也能实现我们的功能,但无论自己适配还是现有开源库方案大体上都会或多或少有以下几个问题:
现有权限库存在的问题:
- 每个页面都要重写onPermissionResult方法、维护requestCode、或者第三方库封装的onPermissionResult方法,如果项目庞大,适配到每个业务点会非常繁琐。
- 权限申请还区分Activity和Fragment,又要分别处理
- 每个权限都要写大量的if else代码去做版本判断,判断新老机型分别处理
基于第一个业务繁琐的问题,很多应用选择适配权限的时候,把所用到的敏感权限放在一个特定的页面去申请,比如欢迎页(某知名音乐播放器等),如果授权不成功,则会直接无法进入应用,这样虽然省事,但是用户体验不好,我在应用一打开,提示需要电话权限,用户会很疑惑。这样其实就违背了“运行时授权”的初衷,谷歌希望我们在真正调用的该功能的时候去请求,这样权限请求和用户的目的是一致的,也更容易授予权限成功。
那么能不能做到如下几个点呢?
对权限适配的期望:
- 基于用户体验考虑,我不希望在应用一打开就向用户索取一堆授权,异或是跳一个页面专门去授权、困扰我们宝贵的用户
- 不需要去重写onPermissionResult、甚至不需要Activity和Fragment。
- 去除版本判断。无论什么系统版本的新老手机,都是走一个方法
- 一行代码完成从权限检查、请求、到最终完我要做的事情
- 我不需要在原有项目中改太多代码
带着上述几个问题,我们今天的主角:SoulPermission应运而生。
当使用了SoulPermission以后,最直观上看,我们上面的代码就变成了这样:
public void makeCall() { SoulPermission.getInstance() .checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() { @Override public void onPermissionOk(Permission permission) { Utils.makeCall(AfterActivity.this, "10086"); } @Override public void onPermissionDenied(Permission permission) { Toast.makeText(AfterActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show(); } }); } 复制代码
SoulPermission:
优势:
- 解耦Activity和Fragment、不再需要Context、不再需要onPermissionResult
- 内部涵盖版本判断,一行代码解决权限相关操作,无需在调用业务方写权限适配代码,继而实现真正调用时请求的“真运行时权限”
- 接入成本低,零入侵,仅需要在gradle配置一行代码
工作流程:
如果我以在Android手机上要做一件事(doSomeThing),那么我最终可以有两个结果:
- A:可以做
- B:不能做
基于上述两种结果,那么SoulPermission的大致工作流程如下:
从开始到结束展示了我们上述打电话的流程,A即直接拨打,B即toast提示用户,无法继续后续操作,绿色部分流程即可选部分,即对shouldShowRequestPermissionRationale的处理,那么完整权限流程下来,我们拨打电话的代码就是这么写:
public void makeCall() { SoulPermission.getInstance() .checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() { @Override public void onPermissionOk(Permission permission) { Utils.makeCall(AfterActivity.this, "10086"); } @Override public void onPermissionDenied(Permission permission) { //绿色框中的流程 //用户第一次拒绝了权限且没有勾选"不再提示"的情况下这个值为true,此时告诉用户为什么需要这个权限。 if (permission.shouldRationale()) { new AlertDialog.Builder(AfterActivity.this) .setTitle("提示") .setMessage("如果你拒绝了权限,你将无法拨打电话,请点击授予权限") .setPositiveButton("授予", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { //用户确定以后,重新执行请求原始流程 makeCall(); } }).create().show(); } else { Toast.makeText(AfterActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show(); } } }); } 复制代码
上述便是其在满足运行时权限下的完整工作流程。那么关于版本兼容呢? 针对部分手机6.0以下手机,SoulPermission也做了兼容,可以通过AppOps 检查权限,内部将权限名称做了相应的映射,它的大体流程就是下图: (这个检查结果不一定准确,但是即使不准确,也默认成功(A),保证我们回调能往下走,不会阻塞流程,有些在6.0以下自己实现了权限系统的手机(如vivo,魅族)等也是走此A的回调,最终会走到它们自己的权限申请流程)
最佳实践:
基于对于代码中对新老系统版本做了控制,而在权限拒绝里面很多处理也是又可以提取的部分,我们可以把回调再次封装一下,进一步减少重复代码:
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener { private String rationaleMessage; private Runnable retryRunnable; /** * @param rationaleMessage 当用户首次拒绝弹框时候,根据权限不同给用户不同的文案解释 * @param retryRunnable 用户点重新授权的runnable 即重新执行原方法 */ public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) { this.rationaleMessage = rationaleMessage; this.retryRunnable = retryRunnable; } @Override public void onPermissionDenied(Permission permission) { Activity activity = SoulPermission.getInstance().getTopActivity(); if (null == activity) { return; } //绿色框中的流程 //用户第一次拒绝了权限、并且没有勾选"不再提示"这个值为true,此时告诉用户为什么需要这个权限。 if (permission.shouldRationale()) { new AlertDialog.Builder(activity) .setTitle("提示") .setMessage(rationaleMessage) .setPositiveButton("授予", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { //用户确定以后,重新执行请求原始流程 retryRunnable.run(); } }).create().show(); } else { //此时请求权限会直接报未授予,需要用户手动去权限设置页,所以弹框引导用户跳转去设置页 String permissionDesc = permission.getPermissionNameDesc(); new AlertDialog.Builder(activity) .setTitle("提示") .setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。") .setPositiveButton("去设置", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { //去设置页 SoulPermission.getInstance().goPermissionSettings(); } }).create().show(); } } } 复制代码
然后我们在App所有打电话的入口处做一次调用:
/** * 拨打指定电话 */ public static void makeCall(final Context context, final String phoneNumber) { SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckPermissionWithRationaleAdapter("如果你拒绝了权限,你将无法拨打电话,请点击授予权限", new Runnable() { @Override public void run() { //retry makeCall(context, phoneNumber); } }) { @Override public void onPermissionOk(Permission permission) { Intent intent = new Intent(Intent.ACTION_CALL); Uri data = Uri.parse("tel:" + phoneNumber); intent.setData(data); if (!(context instanceof Activity)) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); } }); } 复制代码
那么这样下来,在Activity和任何业务页面的调用就只有一行代码了:
findViewById(R.id.bt_call).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { UtilsWithPermission.makeCall(getActivity(), "10086"); } }); 复制代码
其中完全拒绝以后,SoulPermission 提供了跳转到系统权限设置页的方法,我们再来看看效果:
很多时候,其实绿色部分(shouldShowRequestPermissionRationale)其实并不一定必要,反复的弹框用户可能会厌烦,大多数情况,我们这么封装就好:
public abstract class CheckPermissionAdapter implements CheckRequestPermissionListener { @Override public void onPermissionDenied(Permission permission) { //SoulPermission提供栈顶Activity Activity activity = SoulPermission.getInstance().getTopActivity(); if (null == activity) { return; } String permissionDesc = permission.getPermissionNameDesc(); new AlertDialog.Builder(activity) .setTitle("提示") .setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。") .setPositiveButton("去设置", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { //去设置页 SoulPermission.getInstance().goPermissionSettings(); } }).create().show(); } } 复制代码
我们再写一个选择联系人的方法:
/** * 选择联系人 */ public static void chooseContact(final Activity activity, final int requestCode) { SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.READ_CONTACTS, new CheckPermissionAdapter() { @Override public void onPermissionOk(Permission permission) { activity.startActivityForResult(new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), requestCode); } }); } 复制代码
在Activity中也是一行解决问题:
findViewById(R.id.bt_choose_contact).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { UtilsWithPermission.chooseContact(AfterActivity.this, REQUEST_CODE_CONTACT); } }); 复制代码
代码细节请参考demo,我们再来看看效果:
主要功能的源码分析:
优雅的避掉onPermissionResult:
适配权限最大的痛点在于:项目业务页面繁多,如果你想实现“真运行时权限”的话就需要在业务的Activity或者Fragment中去重写权限请求回调方法,斟酌一番并且在参考了下RxPermission中对权限请求的处理,我决定用同样的方式—用一个没有界面的Fragment去完成我们权限请求的操作,下面贴上部分代码:
首先定义一个接口,用于封装权限请求的结果
public interface RequestPermissionListener { /** * 得到权限检查结果 * * @param permissions 封装权限的数组 */ void onPermissionResult(Permission[] permissions); } 复制代码
然后是我们的Fragment:
public class PermissionSupportFragment extends Fragment implements IPermissionActions { /** * 内部维护requestCode */ private static final int REQUEST_CODE = 11; /** * 传入的回调 */ private RequestPermissionListener listener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //当状态发生改变,比如设备旋转时候,Fragment不会被销毁 setRetainInstance(true); } /** * 外部请求的最终调用方法 * @param permissions 权限 * @param listener 回调 */ @TargetApi(M) @Override public void requestPermissions(String[] permissions, RequestPermissionListener listener) { requestPermissions(permissions, REQUEST_CODE); this.listener = listener; } @TargetApi(M) @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); Permission[] permissionResults = new Permission[permissions.length]; //拿到授权结果以后对结果做一些封装 if (requestCode == REQUEST_CODE) { for (int i = 0; i < permissions.length; ++i) { Permission permission = new Permission(permissions[i], grantResults[i], this.shouldShowRequestPermissionRationale(permissions[i])); permissionResults[i] = permission; } } if (listener != null && getActivity() != null && !getActivity().isDestroyed()) { listener.onPermissionResult(permissionResults); } } } 复制代码
其中Permission是我们的权限名称、授予结果、是否需要给用于一个解释的包装类:
public class Permission { private static final String TAG = Permission.class.getSimpleName(); /** * 权限名称 */ public String permissionName; /** * 授予结果 */ public int grantResult; /** * 是否需要给用户一个解释 */ public boolean shouldRationale; /** * 权限是否已经被授予 */ public boolean isGranted() { return grantResult == PackageManager.PERMISSION_GRANTED; } //。。。 } 复制代码
至此,我们已经利用自己实现的一个没有界面的Fragment封装了运行时权限相关的请求、RequestCode的维护、以及onPermissionResult的回调、在我们真正调用的时候代码是这样的:
/** * * @param activity 栈顶 Activity * @param permissionsToRequest 待请求的权限 * @param listener 回调 */ private void requestRuntimePermission(final Activity activity, final Permission[] permissionsToRequest, final CheckRequestPermissionsListener listener) { new PermissionRequester(activity) .withPermission(permissionsToRequest) .request(new RequestPermissionListener() { @Override public void onPermissionResult(Permission[] permissions) { List<Permission> refusedListAfterRequest = new LinkedList<>(); for (Permission requestResult : permissions) { if (!requestResult.isGranted()) { refusedListAfterRequest.add(requestResult); } } if (refusedListAfterRequest.size() == 0) { listener.onAllPermissionOk(permissionsToRequest); } else { listener.onPermissionDenied(PermissionTools.convert(refusedListAfterRequest)); } } }); } 复制代码
其中PermissionRequester也就是一个简单的构建者模式,其中包含了对Activity的类型判断,根据Activity类型去确定Fragment的实现:如果是FragmentActivity的实例,则使用Support包中的Fragment,否则用默认的Fragment,这样就兼容了有些应用的项目的基类不是AppComponentActivity(FragmentActivity)的情形,当然,原则上最低支持4.0,即默认Fragment的支持版本。
class PermissionFragmentFactory { private static final String FRAGMENT_TAG = "permission_fragment_tag"; static IPermissionActions create(Activity activity) { IPermissionActions action; if (activity instanceof FragmentActivity) { FragmentManager supportFragmentManager = ((FragmentActivity) activity).getSupportFragmentManager(); PermissionSupportFragment permissionSupportFragment = (PermissionSupportFragment) supportFragmentManager.findFragmentByTag(FRAGMENT_TAG); if (null == permissionSupportFragment) { permissionSupportFragment = new PermissionSupportFragment(); supportFragmentManager.beginTransaction() .add(permissionSupportFragment, FRAGMENT_TAG) .commitNowAllowingStateLoss(); } action = permissionSupportFragment; } else { android.app.FragmentManager fragmentManager = activity.getFragmentManager(); PermissionFragment permissionFragment = (PermissionFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); if (null == permissionFragment) { permissionFragment = new PermissionFragment(); activity.getFragmentManager().beginTransaction() .add(permissionFragment, FRAGMENT_TAG) .commitAllowingStateLoss(); } action = permissionFragment; } return action; } } 复制代码
至此,整个请求链已经很像最外层暴露的CheckAndRequestPermission方法了,就差一个Activity了,那么参数Activity怎么来呢?我们继续想办法。
再舍去Activity:
当然是使用Application中的ActivityLifecycleCallbacks,使用它的 registerActivityLifecycleCallbacks,感知Activity声明周期变化,获取到当前应用栈顶的Activity,这样我们就不需要自己手动传入了。
public class PermissionActivityLifecycle implements Application.ActivityLifecycleCallbacks { WeakReference<Activity> topActWeakReference; @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { //原则上只需要onResume,兼容如果在onCreate的时候做权限申请保证此时有Activity对象 topActWeakReference = new WeakReference<>(activity); } //..... @Override public void onActivityResumed(Activity activity) { topActWeakReference = new WeakReference<>(activity); } //..... } 复制代码
注册它仅仅需要一个Application:
/** * @param context Application */ private void registerLifecycle(Application context) { if (null != lifecycle) { context.unregisterActivityLifecycleCallbacks(lifecycle); } lifecycle = new PermissionActivityLifecycle(); context.registerActivityLifecycleCallbacks(lifecycle); } 复制代码
这样一来,只要调用了初始化方法registerLifecycle,我们就能提供提供栈顶Activity了
/** * 获取栈顶Activity * * @return 当前应用栈顶Activity * @throws InitException 初始化失败 * @throws ContainerStatusException Activity状态异常 */ private Activity getContainer() { // may auto init failed if (null == lifecycle || null == lifecycle.topActWeakReference) { throw new InitException(); } // activity status error if (null == lifecycle.topActWeakReference.get() || lifecycle.topActWeakReference.get().isFinishing()) { throw new ContainerStatusException(); } return lifecycle.topActWeakReference.get(); } 复制代码
结合起来回到我们之前申请权限的方法(省略了日志打印和线程的判断,如果需要再细看源码):
private void requestPermissions(final Permissions permissions, final CheckRequestPermissionsListener listener) { //check container status final Activity activity; try { activity = getContainer(); } catch (Exception e) { //activity status error do not request return; } //...... //finally request requestRuntimePermission(activity, permissions.getPermissions(), listener); } 复制代码
至此,我们已经能脱离Activity和Fragment,也无需重写onPermissionResult了,只需要一个ApplicationContext初始化即可。
能否更简便一点?
最后避掉Application(免初始化):
我们可以自定义ContentProvider来完成库的初始化,我们可以参考Lifecycle组件的初始化:
//lifeCycle定义的初始化Provider public class LifecycleRuntime*Provider extends ContentProvider { @Override public boolean onCreate() { LifecycleDispatcher.init(getContext()); ProcessLifecycleOwner.init(getContext()); return true; } } 复制代码
和它的Manifest文件:
<application> <provider android:name="android.arch.lifecycle.LifecycleRuntime*Provider" android:authorities="${applicationId}.lifecycle-*" android:exported="false" android:multiprocess="true" /> </application> 复制代码
参照它的实现给我们提供了一个很好的思路,我们可以自定义Provider去初始化一些库或者其他的内容,现在我们写一个自己的initContentProvider:
public class InitProvider extends ContentProvider { @Override public boolean onCreate() { //初始化我们的库 SoulPermission.getInstance().autoInit((Application) getContext()); return true; } //...... } 复制代码
在库的AndroidManifest文件中声明:
<application> <provider android:authorities="${applicationId}.permission.provider" android:name=".permission.InitProvider" android:multiprocess="true" android:exported="false"/> </application> 复制代码
至于为什么这个Context就是Application,我们可以参考ActivityThread中的对ContentProvider的初始化:
public void handleInstallProvider(ProviderInfo info) { //即我们的应用的Application installContentProviders(mInitialApplication, Arrays.asList(info)); } 复制代码
至此,我们权限申请流程就跟Activity、Fragment、乃至Context都没有关系了。
去除if&else、涵盖版本判断:
虽然我们完成了对运行时权限的申请流程,但是毕竟只针对6.0以上机型,如果上面流程还想一句话完成的话,那我们还得兼容老的机型,so,我们需要做在方法内做一个版本判断:
首先判断系统版本
public static boolean isOldPermissionSystem(Context context) { int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; return android.os.Build.VERSION.SDK_INT < M || targetSdkVersion < M; } 复制代码
然后是检查权限:
6.0以上当然是走系统Api:
class RunTimePermissionChecker implements PermissionChecker { private String permission; private Context context; RunTimePermissionChecker(Context context, String permission) { this.permission = permission; this.context = context; } @TargetApi(M) @Override public boolean check() { int checkResult = ContextCompat.checkSelfPermission(context, permission); return checkResult == PackageManager.PERMISSION_GRANTED; } } 复制代码
6.0以下、4.4以上通过AppOps反射获取(为了保证一致性,把权限名称参数在check方法中做了映射,把权限的String参数映射成checkOp的整形参数):
class AppOpsChecker implements PermissionChecker { private Context context; private String permission; AppOpsChecker(Context context, String permission) { this.context = context; this.permission = permission; } /** * 老的通過反射方式檢查權限狀態 * 结果可能不准确,如果返回false一定未授予 * 按需在里面添加 * 如果没匹配上或者异常都默认权限授予 * * @return 检查结果 */ @Override public boolean check() { if (null == permission) { return true; } switch (permission) { case Manifest.permission.READ_CONTACTS: return checkOp(4); case Manifest.permission.WRITE_CONTACTS: return checkOp(5); case Manifest.permission.CALL_PHONE: return checkOp(13); case Manifest.permission.READ_PHONE_STATE: return checkOp(51); case Manifest.permission.CAMERA: return checkOp(26); case Manifest.permission.READ_EXTERNAL_STORAGE: return checkOp(59); case Manifest.permission.WRITE_EXTERNAL_STORAGE: return checkOp(60); case Manifest.permission.ACCESS_FINE_LOCATION: case Manifest.permission.ACCESS_COARSE_LOCATION: return checkOp(2); default: break; } return true; } boolean checkOp(int op) { if (Build.VERSION.SDK_INT < KITKAT) { return true; } try { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); Method method = AppOpsManager.class.getDeclaredMethod("checkOp", int.class, int.class, String.class); return 0 == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); } catch (Exception e) { e.printStackTrace(); } return true; } } 复制代码
和版本判断起来就是这样:
public static PermissionChecker create(Context context, String permission) { if (PermissionTools.isOldPermissionSystem(context)) { return new AppOpsChecker(context, permission); } else { return new RunTimePermissionChecker(context, permission); } } 复制代码
再到我们最终调用的权限检测方法:
private boolean checkPermission(Context context, String permission) { return CheckerFactory.create(context, permission).check(); } 复制代码
最终我们权限库一行代码从权限检测、权限请求联合起来的操作就是这样:
/** * 多个权限的检查与申请 * 在敏感操作前,先检查权限和请求权限,当完成操作后可做后续的事情 * * @param permissions 多个权限的申请 Permissions.build(Manifest.permission.CALL_PHONE,Manifest.permission.CAMERA) * @param listener 请求之后的回调 */ public void checkAndRequestPermissions(@NonNull Permissions permissions, @NonNull final CheckRequestPermissionsListener listener) { //首先检查权限 Permission[] checkResult = checkPermissions(permissions.getPermissionsString()); //得到有多少权限被拒绝了 final Permission[] refusedPermissionList = filterRefusedPermissions(checkResult); if (refusedPermissionList.length > 0) { //是否可以请求运行时权限,即6.0以上 if (canRequestRunTimePermission()) { //请求权限,并把listener传下去,也就是我们一开始看请求流程分析中的那个方法 requestPermissions(Permissions.build(refusedPermissionList), listener); } else { //无法请求权限,本次操作失败 listener.onPermissionDenied(refusedPermissionList); } } else { //没有权限被拒绝,认为所有权限都ok,回调成功 listener.onAllPermissionOk(checkResult); } } 复制代码