这也许是Android权限适配更简单的解决方案

Posted 思忆(GeorgeQin)

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这也许是Android权限适配更简单的解决方案相关的知识,希望对你有一定的参考价值。

背景

关于运行时的权限不用多说,这个概念已经很久,近期工信部在强推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;
    
//。。

以上是关于这也许是Android权限适配更简单的解决方案的主要内容,如果未能解决你的问题,请参考以下文章

Android 悬浮窗权限各机型各系统适配大全

Android 悬浮窗各机型各系统适配大全

适配:Android11存储

Android 录音和摄像头权限适配

Android 录音和摄像头权限适配

Android面试题-机型适配之痛,例如三星小米华为魅族等。