当前位置:   article > 正文

在Android上优雅的申请权限_android 申请权限

android 申请权限

简介

对于权限,每个android开发者应该很熟悉了,对于targetSDK大于23的时候需要对某些敏感权限进行动态申请,比如获取通讯录权限、相机权限、定位权限等。
在android 6.0中也同时添加了权限组的概念,若用户同意组内的某一个权限,那么系统默认app可以使用组内的所有权限,无需再次申请。
这里贴一张权限组的图片:

 

申请权限API

先介绍一下android 6.0以上动态申请权限的流程,申请权限,用户可以点击拒绝,再次申请的时候可以选择不再提醒。
下面说介绍一下运行时申请权限需要用到的API,代码示例使用kotlin实现

  • 在Manifest中注册
	<uses-permission android:name="android.permission.XXX"/>
  • 检查用户是否同意了某个权限
  1. // (API) int checkSelfPermission (Context context, String permission)
  2. ContextCompat.checkSelfPermission(context, Manifest.permission.XXX) != PackageManager.PERMISSION_GRANTED
  • 申请权限
  1. // (API) void requestPermissions (Activity activity, String[] permissions, int requestCode)
  2. requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_CODE_CALL_PHONE)
  • 请求结果回调
  1. // (API) void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)
  2. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  3. }
  • 是否需要向用户解释请求权限的目的
  1. // (API) boolean shouldShowRequestPermissionRationale (Activity activity, String permission)
  2. ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)
情况返回值
第一次打开App时false
上次弹出权限点击了禁止(但没有勾选“下次不在询问”)true
上次选择禁止并勾选“下次不在询问 ”false

注:如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don't ask again 选项,此方法将返回 false。如果设备规范禁止应用具有该权限,此方法也会返回 false。

单一权限申请交互流程

我们做移动端需要直接与用户交互,需要多考虑如何根用户交互才能达到最好的体验。下面我结合google samples中动态申请权限示例android-RuntimePermissions
github.com/googlesampl…
以及动态申请权限框架easypermissions
github.com/googlesampl…
来对交互上做一个总结。

首先说明,Android不建议App直接进行拨打电话这种敏感操作,建议跳转至拨号界面,并将电话号码传入拨号界面中,这里仅作参考案例,下面每中情况都是用户从用户第一次申请权限开始(权限询问状态)

  • 直接允许权限。(动态图见原文)

  • 拒绝之后再次申请允许(动态图见原文)

  • 不再提醒之后引导至设置界面面(动态图见原文)

话不多说,上代码。

  1. /**
  2. * 创建伴生对象,提供静态变量
  3. */
  4. companion object {
  5. const val TAG = "MainActivity"
  6. const val REQUEST_CODE_CALL_PHONE = 1
  7. }
  8. ...
  9. // 这里进行调用requestPermmission()进行拨号前的权限请求
  10. ...
  11. private fun callPhone() {
  12. val intent = Intent(Intent.ACTION_CALL)
  13. val data = Uri.parse("tel:9898123456789")
  14. intent.data = data
  15. startActivity(intent)
  16. }
  17. /**
  18. * 提示用户申请权限说明
  19. */
  20. @TargetApi(Build.VERSION_CODES.M)
  21. fun showPermissionRationale(rationale: String) {
  22. Snackbar.make(view, rationale,
  23. Snackbar.LENGTH_INDEFINITE)
  24. .setAction("确定") {
  25. requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
  26. }.setDuration(3000)
  27. .show()
  28. }
  29. /**
  30. * 用户点击拨打电话按钮,先进行申请权限
  31. */
  32. private fun requestPermmission(context: Context) {
  33. // 判断是否需要运行时申请权限
  34. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
  35. // 判断是否需要对用户进行提醒,用户点击过拒绝&&没有勾选不再提醒时进行提示
  36. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
  37. // 给用于予以权限解释, 对于已经拒绝过的情况,先提示申请理由,再进行申请
  38. showPermissionRationale("需要打开电话权限直接进行拨打电话,方便您的操作")
  39. } else {
  40. // 无需说明理由的情况下,直接进行申请。如第一次使用该功能(第一次申请权限),用户拒绝权限并勾选了不再提醒
  41. // 将引导跳转设置操作放在请求结果回调中处理
  42. requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
  43. }
  44. } else {
  45. // 拥有权限直接进行功能调用
  46. callPhone()
  47. }
  48. }
  49. /**
  50. * 权限申请回调
  51. */
  52. @TargetApi(Build.VERSION_CODES.M)
  53. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  54. // 根据requestCode判断是那个权限请求的回调
  55. if (requestCode == REQUEST_PERMISSION_CODE_CALL_PHONE) {
  56. // 判断用户是否同意了请求
  57. if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  58. callPhone()
  59. } else {
  60. // 未同意的情况
  61. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
  62. // 给用于予以权限解释, 对于已经拒绝过的情况,先提示申请理由,再进行申请
  63. showPermissionRationale("需要打开电话权限直接进行拨打电话,方便您的操作")
  64. } else {
  65. // 用户勾选了不再提醒,引导用户进入设置界面进行开启权限
  66. Snackbar.make(view, "需要打开权限才能使用该功能,您也可以前往设置->应用。。。开启权限",
  67. Snackbar.LENGTH_INDEFINITE)
  68. .setAction("确定") {
  69. val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
  70. intent.data = Uri.parse("package:$packageName")
  71. startActivityForResult(intent,REQUEST_SETTINGS_CODE)
  72. }
  73. .show()
  74. }
  75. }
  76. } else {
  77. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  78. }
  79. }
  80. public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  81. super.onActivityResult(requestCode, resultCode, data)
  82. if (requestCode == REQUEST_SETTINGS_CODE) {
  83. Toast.makeText(this, "再次判断是否同意了权限,再进行自定义处理",
  84. Toast.LENGTH_LONG).show()
  85. }
  86. }
  87. }
  88. 复制代码

EasyPermissions使用及存在问题

上面介绍了单一权限的申请,简单的一个申请代码量其实已经不小了,对于某一个功能需要多个权限更是需要复杂的逻辑判断。google给我们推出了一个权限申请的开源框架,下面围绕着EasyPermission进行说明。
使用方法不介绍了,看一下demo就可以了,网上也有很多的文章这里引用前人的总结。

blog.csdn.net/hexingen/ar…

我在使用的时候发现了有这样一个问题,使用版本是pub.devrel:easypermissions:2.0.0,在demo中使用多个权限申请的时候同意一个,拒绝一个,没有勾选不在提醒。这个时候,第二次申请权限,在提示用户使用权限时候点击取消,会弹出跳转到设置手动开启的弹框。这个做法是不合适的,用户并没有点击不在提醒,可以在app内部引导用户授权,肯定是哪里的逻辑有问题。先贴图(动态图见原文)

 

从最后的设置界面也可以看出,app并没有拒绝某些权限,还处于询问状态。
为了了解为什么出现这样的异常情况,那就跟我一起read the XXXX source code吧。
先说结论,在提示用户点击取消的时候会进入下面方法

  1. @Override
  2. public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
  3. Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
  4. // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
  5. // This will display a dialog directing them to enable the permission in app settings.
  6. if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
  7. new AppSettingsDialog.Builder(this).build().show();
  8. }
  9. }

在判断EasyPermissions.somePermissionPermanentlyDenied()的时候判断出了问题,弹出了dialog(这里的对话框使用Activity实现的)

EasyPermissions源码分析

这里我会跟着demo使用的思路,对源码进行阅读。建议下载源码,上面有链接
在点击两个权限的按钮之后调用如下方法

  1. @AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
  2. public void locationAndContactsTask() {
  3. if (hasLocationAndContactsPermissions()) {
  4. // 如果有权限,toast
  5. Toast.makeText(this, "TODO: Location and Contacts things", Toast.LENGTH_LONG).show();
  6. } else {
  7. // 没有权限,进行申请权限,交由EasyPermission类管理
  8. EasyPermissions.requestPermissions(
  9. this,
  10. getString(R.string.rationale_location_contacts),
  11. RC_LOCATION_CONTACTS_PERM,
  12. LOCATION_AND_CONTACTS);
  13. }
  14. }

按照使用的思路梳理,先不管注解部分。跟进EasyPermissions.requestPermissions

  1. /**
  2. * 请求多个权限,如果系统需要就弹出权限说明
  3. *
  4. * @param host context
  5. * @param rationale 想用户说明为什么需要这些权限
  6. * @param requestCode 请求码用于onRequestPermissionsResult回调中确定是哪一次申请
  7. * @param perms 具体需要的权限
  8. */
  9. public static void requestPermissions(
  10. @NonNull Activity host, @NonNull String rationale,
  11. int requestCode, @Size(min = 1) @NonNull String... perms) {
  12. requestPermissions(
  13. new PermissionRequest.Builder(host, requestCode, perms)
  14. .setRationale(rationale)
  15. .build());
  16. }

很明显,调用了内部的requestPermissions()方法,继续跟

  1. public static void requestPermissions(
  2. @NonNull Fragment host, @NonNull String rationale,
  3. int requestCode, @Size(min = 1) @NonNull String... perms) {
  4. requestPermissions(
  5. new PermissionRequest.Builder(host, requestCode, perms)
  6. .setRationale(rationale)
  7. .build());
  8. }

构建者Builder模式创建了一个PermissionRequest.Builder对象,传入真正的requestPermissions()方法,跟吧

  1. public static void requestPermissions(PermissionRequest request) {
  2. // 在请求权限之前检查是否已经包含了这些权限
  3. if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
  4. // 已经存在了权限,给权限状态数组赋值PERMISSION_GRANTED,并进入请求完成部分。不进行这条处理分支的分析,自己看一下吧
  5. notifyAlreadyHasPermissions(
  6. request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
  7. return;
  8. }
  9. // 通过helper类来辅助调用系统api申请权限
  10. request.getHelper().requestPermissions(
  11. request.getRationale(),
  12. request.getPositiveButtonText(),
  13. request.getNegativeButtonText(),
  14. request.getTheme(),
  15. request.getRequestCode(),
  16. request.getPerms());
  17. }

requestPermissions()方法

  1. public void requestPermissions(@NonNull String rationale,
  2. @NonNull String positiveButton,
  3. @NonNull String negativeButton,
  4. @StyleRes int theme,
  5. int requestCode,
  6. @NonNull String... perms) {
  7. // 这里遍历调用系统api ,shouldShowRequestPermissionRationale,是否需要提示用户申请说明
  8. if (shouldShowRationale(perms)) {
  9. showRequestPermissionRationale(
  10. rationale, positiveButton, negativeButton, theme, requestCode, perms);
  11. } else {
  12. // 抽象方法,其实就是在不同的子类里调用系统api
  13. // ActivityCompat.requestPermissions(getHost(), perms, requestCode);方法
  14. directRequestPermissions(requestCode, perms);
  15. }
  16. }

到这里,第一次的请求流程已经结束,与用户交互,按我们上面gif的演示,对一个权限允许,一个权限拒绝。
这时候回到Activity中的回调onRequestPermissionsResult方法中

  1. @Override
  2. public void onRequestPermissionsResult(int requestCode,
  3. @NonNull String[] permissions,
  4. @NonNull int[] grantResults) {
  5. super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  6. // 交给EasyPermissions类进行处理事件
  7. EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
  8. }

跟进去!

  1. public static void onRequestPermissionsResult(int requestCode,
  2. @NonNull String[] permissions,
  3. @NonNull int[] grantResults,
  4. @NonNull Object... receivers) {
  5. // 创建两个list用于收集请求权限的结果
  6. List<String> granted = new ArrayList<>();
  7. List<String> denied = new ArrayList<>();
  8. for (int i = 0; i < permissions.length; i++) {
  9. String perm = permissions[i];
  10. if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
  11. granted.add(perm);
  12. } else {
  13. denied.add(perm);
  14. }
  15. }
  16. // 遍历
  17. for (Object object : receivers) {
  18. // 如果有某个权限被同意了,回调到Activity中的onPermissionsGranted方法
  19. if (!granted.isEmpty()) {
  20. if (object instanceof PermissionCallbacks) {
  21. ((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted);
  22. }
  23. }
  24. // 如果有某个权限被拒绝了,回调到Activity中的onPermissionsDenied方法
  25. if (!denied.isEmpty()) {
  26. if (object instanceof PermissionCallbacks) {
  27. ((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied);
  28. }
  29. }
  30. // 如果请求的权限都被同意了,进入我们被@AfterPermissionGranted注解的方法,这里对注解的使用不进行详细分析了。
  31. if (!granted.isEmpty() && denied.isEmpty()) {
  32. runAnnotatedMethods(object, requestCode);
  33. }
  34. }
  35. }

我们对权限一个允许一个拒绝,所以会回调onPermissionsGrantedonPermissionsDenied。在demo中的onPermissionsDenied方法进行了处理

  1. @Override
  2. public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
  3. Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
  4. // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
  5. // This will display a dialog directing them to enable the permission in app settings.
  6. if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
  7. new AppSettingsDialog.Builder(this).build().show();
  8. }
  9. }

做了一个判断,`EasyPermissions.somePermissionPermanentlyDenied,这里回调传入的是一个list,我们来继续分析。跟进去,一直跟!

  1. public static boolean somePermissionPermanentlyDenied(@NonNull Activity host,
  2. @NonNull List<String> deniedPermissions) {
  3. return PermissionHelper.newInstance(host)
  4. .somePermissionPermanentlyDenied(deniedPermissions);
  5. }

又进入了helper辅助类

  1. public boolean somePermissionPermanentlyDenied(@NonNull List<String> perms) {
  2. for (String deniedPermission : perms) {
  3. if (permissionPermanentlyDenied(deniedPermission)) {
  4. return true;
  5. }
  6. }
  7. return false;
  8. }

循环遍历了每一权限。有一个是true就返回true。继续跟!

  1. public boolean permissionPermanentlyDenied(@NonNull String perms) {
  2. // 返回了shouldShowRequestPermissionRationale的非值,就是系统API shouldShowRequestPermissionRationale的非值
  3. return !shouldShowRequestPermissionRationale(perms);
  4. }

这里并没有过滤掉用户已经同意的权限,正常的交互不会进入new AppSettingsDialog.Builder(this).build().show();,但是在Rationale弹框点击取消的时候会出问题,我们看一下关于权限说明的rationale弹框的具体实现。

从demo申请权限requestPermissions方法中,调用的showRequestPermissionRationale方法。在ActivityPermissionHelper类中找到具体的实现

  1. @Override
  2. public void showRequestPermissionRationale(@NonNull String rationale,
  3. @NonNull String positiveButton,
  4. @NonNull String negativeButton,
  5. @StyleRes int theme,
  6. int requestCode,
  7. @NonNull String... perms) {
  8. FragmentManager fm = getHost().getFragmentManager();
  9. // Check if fragment is already showing
  10. Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
  11. if (fragment instanceof RationaleDialogFragment) {
  12. Log.d(TAG, "Found existing fragment, not showing rationale.");
  13. return;
  14. }
  15. // 创建了一个DialogFragment并显示出来
  16. RationaleDialogFragment
  17. .newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
  18. .showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
  19. }

查看RationaleDialogFragment类,里面代码不多,找到取消按钮的实现。

  1. @NonNull
  2. @Override
  3. public Dialog onCreateDialog(Bundle savedInstanceState) {
  4. // Rationale dialog should not be cancelable
  5. setCancelable(false);
  6. // 创建listener
  7. RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
  8. RationaleDialogClickListener clickListener =
  9. new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);
  10. // 将listener传入dialog中
  11. return config.createFrameworkDialog(getActivity(), clickListener);
  12. }

查看RationaleDialogClickListener代码

  1. @Override
  2. public void onClick(DialogInterface dialog, int which) {
  3. int requestCode = mConfig.requestCode;
  4. if (which == Dialog.BUTTON_POSITIVE) { // 点击确定
  5. String[] permissions = mConfig.permissions;
  6. if (mRationaleCallbacks != null) {
  7. mRationaleCallbacks.onRationaleAccepted(requestCode);
  8. }
  9. if (mHost instanceof Fragment) {
  10. PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
  11. } else if (mHost instanceof Activity) {
  12. PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
  13. } else {
  14. throw new RuntimeException("Host must be an Activity or Fragment!");
  15. }
  16. } else { // 点击取消
  17. if (mRationaleCallbacks != null) {
  18. mRationaleCallbacks.onRationaleDenied(requestCode);
  19. }
  20. // 调用下面方法
  21. notifyPermissionDenied();
  22. }
  23. }
  24. private void notifyPermissionDenied() {
  25. if (mCallbacks != null) {
  26. // 这里回调了Activity的onPermissionsDenied()方法,传入两个权限
  27. // 不同与用户点击拒绝,用户点击拒绝的时候,此处仅传递了一个拒绝的权限,而这里将用于已经允许的权限和拒绝的权限都传入到里面去。
  28. mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions));
  29. }
  30. }

接下来在执行somePermissionPermanentlyDenied()判断的时候,已经被允许的权限在内部调用系统APIshouldShowRequestPermissionRationale是否需要说明的时候返回的是false,在easyPermission中被认为是用户勾选了不再提醒,所以导致出了问题。

至此,问题找到了,我们该如何处理呢?我们可以在onPermissionsDenied方法先对已经拥有的权限做一个筛选,将没有通过用户同意的权限塞入somePermissionPermanentlyDenied中,即可解决问题。当然,也可以改内部代码,重新编译打包放到工程内。

EasyPermissions中的巧妙设计

既然代码都分析到这里了,就继续说说EasyPermissions中设计比较巧妙的点吧。如果细心看代码,会发现在工程里rationale的弹框是用DialogFragment实现的,而AppsettingDialog是在AppSettingsDialogHolderActivity(一个空的Activity)上通过AppSettingsDialog类中内部完成的AlertDialog的创建和显示(AppSettingsDialog并不是一个dialog,只是一个辅助类)。

  1. public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {
  2. ...
  3. }
  1. public class AppSettingsDialog implements Parcelable {
  2. ...
  3. }
  1. public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
  2. ...
  3. }

真正的去往设置的dialog是在AppSettingsDialog中创建的

  1. AlertDialog showDialog(DialogInterface.OnClickListener positiveListener,
  2. DialogInterface.OnClickListener negativeListener) {
  3. AlertDialog.Builder builder;
  4. if (mThemeResId > 0) {
  5. builder = new AlertDialog.Builder(mContext, mThemeResId);
  6. } else {
  7. builder = new AlertDialog.Builder(mContext);
  8. }
  9. return builder
  10. .setCancelable(false)
  11. .setTitle(mTitle)
  12. .setMessage(mRationale)
  13. .setPositiveButton(mPositiveButtonText, positiveListener)
  14. .setNegativeButton(mNegativeButtonText, negativeListener)
  15. .show();
  16. }

为什么要创建一个单独的Activity来承载dialog呢?我的理解是这样来处理,可以统一了我们自己工程中onActivityResult方法,在跳转设置的dialog上无论点击确定和取消,都会涉及到Activity的跳转,都会回调到onActivityResult ()方法,执行统一的用户给予权限或拒绝权限的处理。

总结

参考google samples,个人认为最友好的申请权限流程应该是

  1. 用户点击功能按钮(如扫一扫),直接申请需要权限(摄像头权限),调用系统弹框进行与用户交互。
  2. 用户拒绝,那么弹框提示用户我们需要权限的理由,用户点击同意,再次调用系统弹框申请权限。
  3. 用户再次拒绝(已经点击了不再提醒),提示用户使用该功能必须获取权限,引导用户去设置界面手动开启。


作者:aTaller
链接:https://juejin.cn/post/6844903717670486030
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号