在Android上优雅的申请权限

简介

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

android权限组

申请权限API

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

  • 在Manifest中注册
<uses-permission android:name="android.permission.XXX"/>
复制代码
  • 检查用户是否赞成了某个权限
// (API) int checkSelfPermission (Context context, String permission)
	ContextCompat.checkSelfPermission(context, Manifest.permission.XXX) != PackageManager.PERMISSION_GRANTED
复制代码
  • 申请权限
// (API) void requestPermissions (Activity activity, String[] permissions, int requestCode)
   requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_CODE_CALL_PHONE)

复制代码
  • 请求结果回调
// (API) void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)
	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {

    }
复制代码
  • 是否须要向用户解释请求权限的目的
// (API) boolean shouldShowRequestPermissionRationale (Activity activity, String permission)
	ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)
复制代码
状况 返回值
第一次打开App时 false
上次弹出权限点击了禁止(但没有勾选“下次不在询问”) true
上次选择禁止并勾选“下次不在询问 ” false

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

单一权限申请交互流程

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

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

  • 直接容许权限。
    数组

    直接容许权限

  • 拒绝以后再次申请容许
    微信

    拒绝以后再次申请容许

  • 再也不提醒以后引导至设置界面面
    app

    再也不提醒以后引导至设置界面面

话很少说,上代码。
框架

/** * 建立伴生对象,提供静态变量 */
    companion object {
        const val TAG = "MainActivity"
        const val REQUEST_CODE_CALL_PHONE = 1
    }
    
    ...
    // 这里进行调用requestPermmission()进行拨号前的权限请求
    ...
    
    private fun callPhone() {
        val intent = Intent(Intent.ACTION_CALL)
        val data = Uri.parse("tel:9898123456789")
        intent.data = data
        startActivity(intent)
    }

    /** * 提示用户申请权限说明 */
    @TargetApi(Build.VERSION_CODES.M)
    fun showPermissionRationale(rationale: String) {
        Snackbar.make(view, rationale,
                Snackbar.LENGTH_INDEFINITE)
                .setAction("肯定") {
                    requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
                }.setDuration(3000)
                .show()
    }


    /** * 用户点击拨打电话按钮,先进行申请权限 */
    private fun requestPermmission(context: Context) {

        // 判断是否须要运行时申请权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            // 判断是否须要对用户进行提醒,用户点击过拒绝&&没有勾选再也不提醒时进行提示
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
                // 给用于予以权限解释, 对于已经拒绝过的状况,先提示申请理由,再进行申请
                showPermissionRationale("须要打开电话权限直接进行拨打电话,方便您的操做")
            } else {
                // 无需说明理由的状况下,直接进行申请。如第一次使用该功能(第一次申请权限),用户拒绝权限并勾选了再也不提醒
                // 将引导跳转设置操做放在请求结果回调中处理
                requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQUEST_PERMISSION_CODE_CALL_PHONE)
            }
        } else {
            // 拥有权限直接进行功能调用
            callPhone()
        }
    }

    /** * 权限申请回调 */
    @TargetApi(Build.VERSION_CODES.M)
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        // 根据requestCode判断是那个权限请求的回调
        if (requestCode == REQUEST_PERMISSION_CODE_CALL_PHONE) {
            // 判断用户是否赞成了请求
            if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                callPhone()
            } else {
                // 未赞成的状况
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
                    // 给用于予以权限解释, 对于已经拒绝过的状况,先提示申请理由,再进行申请
                    showPermissionRationale("须要打开电话权限直接进行拨打电话,方便您的操做")
                } else {
                    // 用户勾选了再也不提醒,引导用户进入设置界面进行开启权限
                    Snackbar.make(view, "须要打开权限才能使用该功能,您也能够前往设置->应用。。。开启权限",
                            Snackbar.LENGTH_INDEFINITE)
                            .setAction("肯定") {
                                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                                intent.data = Uri.parse("package:$packageName")
                                startActivityForResult(intent,REQUEST_SETTINGS_CODE)
                            }
                            .show()
                }
            }
        } else {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }
    
    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_SETTINGS_CODE) {
            Toast.makeText(this, "再次判断是否赞成了权限,再进行自定义处理",
                    Toast.LENGTH_LONG).show()
        }
    }

  }

复制代码

EasyPermissions使用及存在问题

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

blog.csdn.net/hexingen/ar…

我在使用的时候发现了有这样一个问题,使用版本是pub.devrel:easypermissions:2.0.0,在demo中使用多个权限申请的时候赞成一个,拒绝一个,没有勾选不在提醒。这个时候,第二次申请权限,在提示用户使用权限时候点击取消,会弹出跳转到设置手动开启的弹框。这个作法是不合适的,用户并无点击不在提醒,能够在app内部引导用户受权,确定是哪里的逻辑有问题。先贴图

easypermissions中不合理的交互.gif

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

@Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());

        // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
        // This will display a dialog directing them to enable the permission in app settings.
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }
复制代码

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

EasyPermissions源码分析

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

@AfterPermissionGranted(RC_LOCATION_CONTACTS_PERM)
    public void locationAndContactsTask() {
        if (hasLocationAndContactsPermissions()) {
            // 若是有权限,toast
            Toast.makeText(this, "TODO: Location and Contacts things", Toast.LENGTH_LONG).show();
        } else {
            // 没有权限,进行申请权限,交由EasyPermission类管理
            EasyPermissions.requestPermissions(
                    this,
                    getString(R.string.rationale_location_contacts),
                    RC_LOCATION_CONTACTS_PERM,
                    LOCATION_AND_CONTACTS);
        }
    }
复制代码

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

/** * 请求多个权限,若是系统须要就弹出权限说明 * * @param host context * @param rationale 想用户说明为何须要这些权限 * @param requestCode 请求码用于onRequestPermissionsResult回调中肯定是哪一次申请 * @param perms 具体须要的权限 */
    public static void requestPermissions( @NonNull Activity host, @NonNull String rationale, int requestCode, @Size(min = 1) @NonNull String... perms) {
        requestPermissions(
                new PermissionRequest.Builder(host, requestCode, perms)
                        .setRationale(rationale)
                        .build());
    }
复制代码

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

public static void requestPermissions( @NonNull Fragment host, @NonNull String rationale, int requestCode, @Size(min = 1) @NonNull String... perms) {
        requestPermissions(
                new PermissionRequest.Builder(host, requestCode, perms)
                        .setRationale(rationale)
                        .build());
    }
复制代码

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

public static void requestPermissions(PermissionRequest request) {

        // 在请求权限以前检查是否已经包含了这些权限
        if (hasPermissions(request.getHelper().getContext(), request.getPerms())) {
        	// 已经存在了权限,给权限状态数组赋值PERMISSION_GRANTED,并进入请求完成部分。不进行这条处理分支的分析,本身看一下吧
			notifyAlreadyHasPermissions(
                    request.getHelper().getHost(), request.getRequestCode(), request.getPerms());
            return;
        }

        // 经过helper类来辅助调用系统api申请权限
        request.getHelper().requestPermissions(
                request.getRationale(),
                request.getPositiveButtonText(),
                request.getNegativeButtonText(),
                request.getTheme(),
                request.getRequestCode(),
                request.getPerms());
    }
复制代码

requestPermissions()方法

public void requestPermissions(@NonNull String rationale, @NonNull String positiveButton, @NonNull String negativeButton, @StyleRes int theme, int requestCode, @NonNull String... perms) {
		// 这里遍历调用系统api ,shouldShowRequestPermissionRationale,是否须要提示用户申请说明
		if (shouldShowRationale(perms)) {
            showRequestPermissionRationale(
                    rationale, positiveButton, negativeButton, theme, requestCode, perms);
        } else {
        	// 抽象方法,其实就是在不一样的子类里调用系统api
        	// ActivityCompat.requestPermissions(getHost(), perms, requestCode);方法
            directRequestPermissions(requestCode, perms);
        }
    }
复制代码

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

@Override
	public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
		super.onRequestPermissionsResult(requestCode, permissions, grantResults);

		// 交给EasyPermissions类进行处理事件
		EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
	}
复制代码

跟进去!

public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, @NonNull Object... receivers) {
        // 建立两个list用于收集请求权限的结果
        List<String> granted = new ArrayList<>();
        List<String> denied = new ArrayList<>();
        for (int i = 0; i < permissions.length; i++) {
            String perm = permissions[i];
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                granted.add(perm);
            } else {
                denied.add(perm);
            }
        }

        // 遍历
        for (Object object : receivers) {
            // 若是有某个权限被赞成了,回调到Activity中的onPermissionsGranted方法
            if (!granted.isEmpty()) {
                if (object instanceof PermissionCallbacks) {
                    ((PermissionCallbacks) object).onPermissionsGranted(requestCode, granted);
                }
            }

            // 若是有某个权限被拒绝了,回调到Activity中的onPermissionsDenied方法
            
            if (!denied.isEmpty()) {
                if (object instanceof PermissionCallbacks) {
                    ((PermissionCallbacks) object).onPermissionsDenied(requestCode, denied);
                }
            }

            // 若是请求的权限都被赞成了,进入咱们被@AfterPermissionGranted注解的方法,这里对注解的使用不进行详细分析了。
            if (!granted.isEmpty() && denied.isEmpty()) {
                runAnnotatedMethods(object, requestCode);
            }
        }
    }
复制代码

咱们对权限一个容许一个拒绝,因此会回调onPermissionsGrantedonPermissionsDenied。在demo中的onPermissionsDenied方法进行了处理

@Override
    public void onPermissionsDenied(int requestCode, @NonNull List<String> perms) {
        Log.d(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());

        // (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
        // This will display a dialog directing them to enable the permission in app settings.
        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
            new AppSettingsDialog.Builder(this).build().show();
        }
    }
复制代码

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

public static boolean somePermissionPermanentlyDenied(@NonNull Activity host, @NonNull List<String> deniedPermissions) {
        return PermissionHelper.newInstance(host)
                .somePermissionPermanentlyDenied(deniedPermissions);
    }
复制代码

又进入了helper辅助类

public boolean somePermissionPermanentlyDenied(@NonNull List<String> perms) {
        for (String deniedPermission : perms) {
            if (permissionPermanentlyDenied(deniedPermission)) {
                return true;
            }
        }

        return false;
    }
复制代码

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

public boolean permissionPermanentlyDenied(@NonNull String perms) {
    	// 返回了shouldShowRequestPermissionRationale的非值,就是系统API shouldShowRequestPermissionRationale的非值
        return !shouldShowRequestPermissionRationale(perms);
    }
复制代码

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

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

@Override
    public void showRequestPermissionRationale(@NonNull String rationale, @NonNull String positiveButton, @NonNull String negativeButton, @StyleRes int theme, int requestCode, @NonNull String... perms) {
        FragmentManager fm = getHost().getFragmentManager();

        // Check if fragment is already showing
        Fragment fragment = fm.findFragmentByTag(RationaleDialogFragment.TAG);
        if (fragment instanceof RationaleDialogFragment) {
            Log.d(TAG, "Found existing fragment, not showing rationale.");
            return;
        }
		// 建立了一个DialogFragment并显示出来
        RationaleDialogFragment
                .newInstance(positiveButton, negativeButton, rationale, theme, requestCode, perms)
                .showAllowingStateLoss(fm, RationaleDialogFragment.TAG);
    }
复制代码

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

@NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Rationale dialog should not be cancelable
        setCancelable(false);

        // 建立listener
        RationaleDialogConfig config = new RationaleDialogConfig(getArguments());
        RationaleDialogClickListener clickListener =
                new RationaleDialogClickListener(this, config, mPermissionCallbacks, mRationaleCallbacks);

        // 将listener传入dialog中
        return config.createFrameworkDialog(getActivity(), clickListener);
    }
复制代码

查看RationaleDialogClickListener代码

@Override
    public void onClick(DialogInterface dialog, int which) {
        int requestCode = mConfig.requestCode;
        if (which == Dialog.BUTTON_POSITIVE) { // 点击肯定
            String[] permissions = mConfig.permissions;
            if (mRationaleCallbacks != null) {
                mRationaleCallbacks.onRationaleAccepted(requestCode);
            }
            if (mHost instanceof Fragment) {
                PermissionHelper.newInstance((Fragment) mHost).directRequestPermissions(requestCode, permissions);
            } else if (mHost instanceof Activity) {
                PermissionHelper.newInstance((Activity) mHost).directRequestPermissions(requestCode, permissions);
            } else {
                throw new RuntimeException("Host must be an Activity or Fragment!");
            }
        } else { // 点击取消
            if (mRationaleCallbacks != null) {
                mRationaleCallbacks.onRationaleDenied(requestCode);
            }
            // 调用下面方法
            notifyPermissionDenied();
        }
    }

    private void notifyPermissionDenied() {
        if (mCallbacks != null) {
        	// 这里回调了Activity的onPermissionsDenied()方法,传入两个权限
        	// 不一样与用户点击拒绝,用户点击拒绝的时候,此处仅传递了一个拒绝的权限,而这里将用于已经容许的权限和拒绝的权限都传入到里面去。
            mCallbacks.onPermissionsDenied(mConfig.requestCode, Arrays.asList(mConfig.permissions));
        }
    }
复制代码

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

至此,问题找到了,咱们该如何处理呢?咱们能够在onPermissionsDenied方法先对已经拥有的权限作一个筛选,将没有经过用户赞成的权限塞入somePermissionPermanentlyDenied中,便可解决问题。固然,也能够改内部代码,从新编译打包放到工程内。

EasyPermissions中的巧妙设计

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

public class RationaleDialogFragmentCompat extends AppCompatDialogFragment {
	...
}
复制代码
public class AppSettingsDialog implements Parcelable {
	...
}
复制代码
public class AppSettingsDialogHolderActivity extends AppCompatActivity implements DialogInterface.OnClickListener {
	...
}
复制代码

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

AlertDialog showDialog(DialogInterface.OnClickListener positiveListener, DialogInterface.OnClickListener negativeListener) {
        AlertDialog.Builder builder;
        if (mThemeResId > 0) {
            builder = new AlertDialog.Builder(mContext, mThemeResId);
        } else {
            builder = new AlertDialog.Builder(mContext);
        }
        return builder
                .setCancelable(false)
                .setTitle(mTitle)
                .setMessage(mRationale)
                .setPositiveButton(mPositiveButtonText, positiveListener)
                .setNegativeButton(mNegativeButtonText, negativeListener)
                .show();
    }
复制代码

为何要建立一个单独的Activity来承载dialog呢?个人理解是这样来处理,能够统一了咱们本身工程中onActivityResult方法,在跳转设置的dialog上不管点击肯定和取消,都会涉及到Activity的跳转,都会回调到onActivityResult ()方法,执行统一的用户给予权限或拒绝权限的处理。

总结

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

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

关注微信公众号,最新技术干货实时推送

image
相关文章
相关标签/搜索