这也许是Android一句话许可权适配的最优 解决方案 现状 关于执行时的许可权不用多说,这个概念已经很久,近期工信部在强推SDK26,我这边做了一些适配工作,其中有一项就是执行时许可权,今天将对执行时许可权提供一个更优雅的解决方案,如果你还不了解执行时许可权,请移步。 (以直接呼叫打电话功能为例) 首先我们专案中可能会有这么一个方法: /** * 拨打指定电话 */ 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 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等。 虽然也能实现我们的功能,但无论自己适配还是现有开源库方案大体上都会或多或少有以下几个问题: 1、每个页面都要重写onPermissionResult方法、维护requestCode、或者第三方库封装的onPermissionResult方法,如果专案庞大,适配到每个业务点会非常繁琐 2、许可权申请还区分Activity和Fragment,又要分别处理 3、每个许可权都要写大量的if else程式码去做版本判断,判断新老机型分别处理 基于第一个业务繁琐的问题,很多应用选择适配许可权的时候,把所用到的敏感许可权放在一个特定的页面去申请,比如欢迎页(某知名音乐播放器等),如果授权不成功,则会直接无法进入应用,这样虽然省事,但是使用者体验不好,我在应用一开启,提示需要电话许可权,使用者会很疑惑。 这样其实就违背了“执行时授权”的初衷,Google希望我们在真正呼叫的该功能的时候去请求,这样许可权请求和使用者的目的是一致的,也更容易授予许可权成功。 那么能不能做到如下几个点呢? 1、不需要Activity和Fragment作为载体、不需要去重写onPermissionResult。 2、去除版本判断。只需要在一个工具类中把某个方法(如打电话)适配,然后全域性呼叫,做到真正的执行时请求。 3、一行程式码完成从许可权检查、请求、到最终完成后做事情。 答案当然是有,下面是我们今天的主角: SoulPermission SoulPermission应运而生。 https://github.com/soulqw/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(); } }); } 解决问题: 1、解耦Activity和Fragment、不再需要Context、不再需要onPermissionResult 2、内部涵盖版本判断,一行程式码解决许可权相关操作,无需在呼叫业务方写许可权适配程式码,继而实现真正呼叫时请求的“真执行时许可权” 3、接入成本低,零入侵,仅需要在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也做了相容,可以通过AppOpps 检查许可权,内部将许可权名称做了相应的对映,它的大体流程就是下图: (这个检查结果不一定准确,但是即使不准确,也预设成功(A),保证我们回拨能往下走,不会阻塞流程,其他自己实现了许可权系统的手机,如vivo,魅族等也是走此方法,最终走他们自己的许可权申请流程) 基于对于新老手机版本做了控制,在许可权拒绝里面很多处理也是又可以提取的部分,我们可以把回拨再次封装一下,进一步减少重复程式码: 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); } }); 我们再来看看效果: 主要源代码分析 1、优雅的避掉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 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 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怎么来呢? 2、再舍去Activity 当然是使用Application中的ActivityLifecycleCallbacks,使用它的registerActivityLifecycleCallbacks,感知Activity宣告周期变化,获取到当前应用栈顶的Activity,这样我们就不需要自己手动传入了。 public class PermissionActivityLifecycle implements Application.ActivityLifecycleCallbacks { WeakReference 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初始化即可。 3、能否更简便一点? 最后避掉Application(免初始化): 我们可以自定义ContentProvider来完成库的初始化,我们可以参考Lifecycle元件的初始化: http://chaosleong.github.io/2017/05/27/How-Lifecycle-aware-Components-actually-works/ //lifeCycle定义的初始化Provider public class LifecycleRuntimeTrojanProvider extends ContentProvider { @Override public boolean onCreate() { LifecycleDispatcher.init(getContext()); ProcessLifecycleOwner.init(getContext()); return true; } } 和它的Manifest档案: android:name=android.arch.lifecycle.LifecycleRuntimeTrojanProvider android:authorities=${applicationId}.lifecycle-trojan android:exported=false android:multiprocess=true /> 参照它的实现给我们提供了一个很好的思路,我们可以自定义Provider去初始化一些库或者其他的内容,现在我们写一个自己的initContentProvider: public class InitProvider extends ContentProvider { @Override public boolean onCreate() { //初始化我们的库 SoulPermission.getInstance().autoInit((Application) getContext()); return true; } //...... } 在库的AndroidManifest档案中宣告: android:name=.permission.InitProvider android:multiprocess=true android:exported=false/> 至于为什么这个Context就是Application,我们可以参考ActivityThread中的对ContentProvider的初始化: public void handleInstallProvider(ProviderInfo info) { //即我们的应用的Application installContentProviders(mInitialApplication, Arrays.asList(info)); } 至此,我们许可权申请流程就跟Activity、Fragment、乃至Context都没有关系了。 4. 去除if&else、涵盖版本判断: 虽然我们完成了对执行时许可权的申请流程,但是毕竟只针对6.0以上机型,如果上面流程还想一句话完成的话,那我们还得相容老的机型,so,我们需要做在方法内做一个版本判断: 首先判断系统版本 public static boolean isOldPermissionSystem(Context context) { int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; return android.os.Build.VERSION.SDK_INT } 然后是检查许可权: 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); ... default: break; } return true; } boolean checkOp(int op) { if (Build.VERSION.SDK_INT 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); } } 至此,我们的三个主要需求的源代码分析基本完成,如果有啥疑问和细节上的实现,可以自行阅读源代码即可。 总结: SoulPerission很好的适配了真执行时许可权、除了上述三个个主要功能以外还提供以下功能: 支援多项许可权同时请求支援检查通知许可权支援系统许可权页面跳转支援debug模式https://github.com/soulqw/SoulPermission 想学习更多Android知识,或者获取相关资料请关注我,并私信回复【资料】。 有面试资源系统整理分享,Java语言进阶和Kotlin语言与Android相关技术核心,APP开发框架知识, 360°Android App全方位效能优化。Android前沿技术,高阶UI、Gradle、RxJava、小程式、Hybrid、 移动架构师专题专案实战环节、React Native、等技术教程!架构师课程、NDK模组开发、 Flutter等全方面的 Android高阶实践技术讲解。还有线上答疑