Android 运行时权限终极总结

目录

一:基础&起因

二:基本使用

2.1 Activity当中的一般用法

2.2 Fragement中的用法

三:注意事项和打电话例子

3.1 注意事项

3.2 相关API

3.3 使用原生API的零时权限例子

四:常见轮子/推荐

4.1 PermissionsDispatcher

4.2  AndroidUtilCode中的权限类(本人惯用)

4.3 RxPermissions

4.4 自己手撸一个零时权限框架推荐学习SoulPermission

4.5 总结&建议


一:基础&起因

安卓系统权限分为两类:

  • 正常权限不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。
  • 危险权限会授予应用访问用户机密数据的权限。如果您需要危险权限,用户必须明确批准您的应用使用这些权限。

Android 6.0在我们原有的AndroidManifest.xml声明权限的基础上,又新增了运行时权限动态检测。上述的危险权限分为9组如下:

//日历;摄像头;通讯录;地理位置;麦克风;电话;身体传感器;短信;存储空间


group:android.permission-group.CONTACTS  //联系人分组
  permission:android.permission.WRITE_CONTACTS //写入联系人
  permission:android.permission.GET_ACCOUNTS   //访问GMail账户列表
  permission:android.permission.READ_CONTACTS  //读取联系人

group:android.permission-group.PHONE  //电话分组
  permission:android.permission.READ_CALL_LOG  //读取通话记录
  permission:android.permission.READ_PHONE_STATE //读取手机状态
  permission:android.permission.CALL_PHONE  //直接拨打电话
  permission:android.permission.WRITE_CALL_LOG //写入通话记录
  permission:android.permission.USE_SIP //SIP(会话初始协议)不是很了解
permission:android.permission.PROCESS_OUTGOING_CALLS //应用程序监听、控制、取消呼出电话的权限 
permission:com.android.voicemail.permission.ADD_VOICEMAIL //应用程序添加系统中的语音邮件

group:android.permission-group.CALENDAR //日历分组
  permission:android.permission.READ_CALENDAR //读取日历
  permission:android.permission.WRITE_CALENDAR //更改日历

group:android.permission-group.CAMERA //摄像分组
  permission:android.permission.CAMERA //打开摄像

group:android.permission-group.SENSORS //传感器
  permission:android.permission.BODY_SENSORS //允许该应用存取监测您身体状况的传感器所收集的数据,例如您的心率

group:android.permission-group.LOCATION //位置信息
  permission:android.permission.ACCESS_FINE_LOCATION  //通过WiFi或移动基站的方式获取用户错略的经纬度信息,定位精度大概误差在30~1500米    
  permission:android.permission.ACCESS_COARSE_LOCATION //通过GPS芯片接收卫星的定位信息,定位精度达10米以内

group:android.permission-group.STORAGE //文件存储分组
  permission:android.permission.READ_EXTERNAL_STORAGE //读取文件权限
  permission:android.permission.WRITE_EXTERNAL_STORAGE //写入文件权限

group:android.permission-group.MICROPHONE //麦克风分组
  permission:android.permission.RECORD_AUDIO //程序录制音频

group:android.permission-group.SMS //短信分组
  permission:android.permission.READ_SMS //读取短信
  permission:android.permission.RECEIVE_WAP_PUSH //允许程序监控将收到WAP PUSH信息
  permission:android.permission.RECEIVE_MMS //允许一个程序监控将收到MMS彩信
  permission:android.permission.RECEIVE_SMS //允许程序监控一个将收到短信息
  permission:android.permission.SEND_SMS //发送短信
  permission:android.permission.READ_CELL_BROADCASTS //读取广播 需要具体实例

Android 运行时权限终极总结_第1张图片

 


二:基本使用

2.1 Activity当中的一般用法

       注:onRequestPermissionsResult()方法为使用requestPermissions()方法后的回调

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
   
    chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{ Manifest.permission. WRITE_EXTERNAL_STORAGE }, 1);
            } else {
                openAlbum();
            }
        }
    });
}

private void openAlbum() {
    Intent intent = new Intent("android.intent.action.GET_CONTENT");
    intent.setType("image/*");
    startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case 1:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openAlbum();
            } else {
                Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
            }
            break;
        default:
    }
}

2.2 Fragement中的用法

        我们在Fragment中申请时不是使用AcyivityCompat而是使用Fragment本身,接着类似于Activity,在Fragement中的onRequestPermissionsResult()方法中进行回调。代码如下:

MyFragment.this.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},  1);

注:fragement中无法回调onRequestPermissionsResult()方法:

        在Fragment中申请权限时发现Fragment中不会回调onRequestPermissionsResult方法,如果我们在Activity中重写onRequestPermissionsResult方法你会发现它会回调到Activity中的onRequestPermissionsResult方法,所以我们需要在Activity中的onRequestPermissionsResult方法中进行处理让它把改事件传递到我们的fragment中:

// Activity中
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
  {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults);
       // 获取到Activity下的Fragment
       List fragments = getSupportFragmentManager().getFragments();
       if (fragments == null)
       {
           return;
       }
       // 查找在Fragment中onRequestPermissionsResult方法并调用
       for (Fragment fragment : fragments)
       {
           if (fragment != null)
           {
               // 这里就会调用我们Fragment中的onRequestPermissionsResult方法
               fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
           }
       }
   }

        之后在我们申请权限的Fragment中重写onRequestPermissionsResult方法时,通过在Activity中遍历所有的Fragment中调用onRequestPermissionsResult方法,就是我们Fragment中重写的onRequestPermissionsResult方法:

// Fragment 中
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull String[] permissions,@NonNull int[] grantResults)
{
    // TODO 写我们自己的处理逻辑
}

 


三:注意事项和打电话例子

3.1 注意事项

  1. 在 Android 5.1(API 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在安装时要求用户授予权限。(沿用之前的权限系统)
  2. 即使在安装时已经授予应用所有权限,在Android 6.0之后依然可以通过 "Setting" 来关闭已经授予的权限。
  3. 在请求权限时,系统只告诉用户应用需要的权限组,而不告知具体权限。
  4. 如果在未检查授权的情况下,直接使用危险权限,会导致程序Crash。
  5. 使用 v4 包中的 ContextCompat 处理权限(v13 包中的FragmentCompat),不需要考虑版本问题。

3.2 相关API

  • int checkSelfPermission()

检查应用是否有指定权限。返回值为 PackageManager.PERMISSION_GRANTED 表示有权限, PackageManager.PERMISSION_DENIED 表示无权限。

  • void requestPermissions()

请求指定权限,可以是多个,以数组的方式。

  • boolean shouldShowRequestPermissionRationale()

如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。

  • void onRequestPermissionsResult()

请求权限的结果回调。

3.3 使用原生API的零时权限例子

因为以上列举的相关API都是在 API 23 才有的,为了适配低版本,官方提供了 v4 v13 兼容包。我们可以直接使用兼容包中的方法进行权限处理。

步骤(以拨打电话为例)

  • 还是和以前一样,先在清单文件中申请所需要的权限。

  • 在使用到拨打电话的地方,进行权限检查

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
        != PackageManager.PERMISSION_GRANTED) {
    // 应用没有授予拨打电话权限,请求权限
    requestCameraPermission();
} else {
    // 应用被授予拨打电话权限 PackageManager.PERMISSION_GRANTED
    makeCall();
}
  • 如果有权限,直接拨打电话,至此结束
  • 如果没有权限,则请求权限

ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CALLPHONE);
  • 在请求权限过程中可以使用shouldShowRequestPermissionRationale()检查是否被拒绝过,如果被拒绝过,可以给用户一个详细解释。

if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
    // 向用户详细解释申请该权限的原因
    new AlertDialog.Builder(this)
            .setCancelable(false)
            .setMessage("拨打电话需要使用电话权限,如果不授予权限会导致该功能无法正常使用")
            .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    ActivityCompat.requestPermissions(
                            OriginalActivity.this,
                            new String[]{Manifest.permission.CALL_PHONE},
                            REQUEST_CALLPHONE
                    );
                }
            })
            .setNegativeButton("不给", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            })
            .show();
} 
  • 处理授权结果回调

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

    if (requestCode == REQUEST_CALLPHONE) {
        if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 授予权限,拨打电话
            makeCall();
        } else {
            Toast.makeText(this, "请求权限被拒绝", Toast.LENGTH_SHORT).show();
        }
    } else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

四:常见轮子/推荐

 

4.1 PermissionsDispatcher

        github star 9.1k;

        在处理运行时权限的时候,虽然官方提供了兼容包不再需要做版本检查,但处理起来依然使代码很杂乱。现在已经出现了很多处理运行时权限的开源库,这里给大家推荐 PermissionsDispatcher。该库在GitHub同比获得 star 最多。而且使用 apt 技术,在编译时期动态生成xxxxPermissionsDispatcher模板代码,效率很高!

API 简介

该库使用 apt 技术,自然使用的就是注解。

注解 是否必须 作用
@RuntimePermissions 标记Activity/Fragment,则注解解释器会生成对应类的代码
@NeedsPermission 标记需要授权才能执行的方法
@OnShowRationale   对应shouldShowRequestPermissionRationale(),当应用之前请求过此权限但用户拒绝了请求,再次请求时调用
@OnPermissionDenied   当请求权限遭拒绝时调用
@OnNeverAskAgain   当用户勾选不再提示,并拒绝权限时,再次请求时调用

步骤(以使用相机为例)

  • 还是在清单文件中声明使用的权限

  • 配置依赖 PermissionsDispatcher,这里不再赘述
  • 代码示例

@RuntimePermissions
public class PermissionsDispatcherActivity extends AppCompatActivity {

    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView = (ImageView) findViewById(R.id.imageView);

        findViewById(R.id.btn_camera).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PermissionsDispatcherActivityPermissionsDispatcher.takePhotoWithCheck(PermissionsDispatcherActivity.this);
            }
        });

    }

    @NeedsPermission(Manifest.permission.CAMERA)
    void takePhoto() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 启动系统相机
        startActivityForResult(intent, 100);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) { // 如果返回数据
            if (requestCode == 100) { // 判断请求码是否为REQUEST_CAMERA,如果是代表是这个页面传过去的,需要进行获取
                Bundle bundle = data.getExtras(); // 从data中取出传递回来缩略图的信息,图片质量差,适合传递小图片
                Bitmap bitmap = (Bitmap) bundle.get("data"); // 将data中的信息流解析为Bitmap类型
                imageView.setImageBitmap(bitmap);// 显示图片
            }
        }
    }

    @OnShowRationale(Manifest.permission.CAMERA)
    void showRationaleForRecord(final PermissionRequest request) {
        new AlertDialog.Builder(this)
                .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.proceed();
                    }
                })
                .setNegativeButton("不给", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        request.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage("拍照需要相机权限,应用将要申请使用相机权限")
                .show();
    }

    @OnPermissionDenied(Manifest.permission.CAMERA)
    void showCameraDenied() {
        Toast.makeText(getApplicationContext(), "权限被拒绝", Toast.LENGTH_LONG).show();
    }

    @OnNeverAskAgain(Manifest.permission.CAMERA)
    void onRCameraNeverAskAgain() {
        new AlertDialog.Builder(this)
                .setPositiveButton("好的", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // 打开系统应用设置
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        intent.addCategory(Intent.CATEGORY_DEFAULT);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                        dialog.cancel();
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.cancel();
                    }
                })
                .setCancelable(false)
                .setMessage("您已经禁止了相机权限,是否现在去开启")
                .show();
    }

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

使用注意

  • 注解的方法不能是private
  • 在同一 Activity/Fragment 中可以多次使用以上注解,但是同一组权限处理中注解的value的值应该相同。
  • AS 中可以配合 PermissionsDispatcher plugin 插件一起使用。

4.2  AndroidUtilCode中的权限类(本人惯用)

       详情可见作者文章:终于等到你--权限工具类

       封装一个PermissionHelper.java 类,方便使用:

import android.app.Activity;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;

import com.blankj.utilcode.constant.PermissionConstants;
import com.blankj.utilcode.util.ActivityUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.PermissionUtils;

import java.util.List;


public class PermissionHelper {

    public static void requestCamera(final OnPermissionGrantedListener listener) {
        request(listener, PermissionConstants.CAMERA);
    }

    public static void requestStorage(final OnPermissionGrantedListener listener) {
        request(listener, PermissionConstants.STORAGE);
    }

    public static void requestPhone(final OnPermissionGrantedListener listener) {
        request(listener, PermissionConstants.PHONE);
    }

    public static void requestPhone(final OnPermissionGrantedListener grantedListener,
                                    final OnPermissionDeniedListener deniedListener) {
        request(grantedListener, deniedListener, PermissionConstants.PHONE);
    }

    public static void requestSms(final OnPermissionGrantedListener listener) {
        request(listener, PermissionConstants.SMS);
    }

    public static void requestLocation(final OnPermissionGrantedListener listener) {
        request(listener, PermissionConstants.LOCATION);
    }

    private static void request(final OnPermissionGrantedListener grantedListener,
                                final @PermissionConstants.Permission String... permissions) {
        request(grantedListener, null, permissions);
    }

    private static void request(final OnPermissionGrantedListener grantedListener,
                                final OnPermissionDeniedListener deniedListener,
                                final @PermissionConstants.Permission String... permissions) {
        PermissionUtils.permission(permissions)
                .rationale(new PermissionUtils.OnRationaleListener() {
                    @Override
                    public void rationale(ShouldRequest shouldRequest) {
                        shouldRequest.again(true);
                    }
                })
                .callback(new PermissionUtils.FullCallback() {
                    @Override
                    public void onGranted(List permissionsGranted) {
                        if (grantedListener != null) {
                            grantedListener.onPermissionGranted();
                        }
                        LogUtils.d(permissionsGranted);
                    }

                    @Override
                    public void onDenied(List permissionsDeniedForever, List permissionsDenied) {
                        if (!permissionsDeniedForever.isEmpty()) {
                            showOpenAppSettingDialog();
                        }
                        if (deniedListener != null) {
                            deniedListener.onPermissionDenied();
                        }
                        LogUtils.d(permissionsDeniedForever, permissionsDenied);
                    }
                })
                .request();
    }

    public interface OnPermissionGrantedListener {
        void onPermissionGranted();
    }

    public interface OnPermissionDeniedListener {
        void onPermissionDenied();
    }


    //权限拒绝后(加了不再询问),再要权限的时候需要打开系统设置
    public static void showOpenAppSettingDialog() {
        Activity topActivity = ActivityUtils.getTopActivity();
        if (topActivity == null || topActivity.isFinishing()) return;
        new AlertDialog.Builder(topActivity)
                .setTitle(android.R.string.dialog_alert_title)
                .setMessage("We need some of the permissions you rejected or the system failed to apply failed, please manually set to the page authorize, otherwise the function can\\'t be used normally!")
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        PermissionUtils.launchAppDetailsSettings();
                    }
                })
                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                })
                .setCancelable(false)
                .create()
                .show();
    }
}

使用举例:

PermissionHelper.requestCamera(new PermissionHelper.OnPermissionGrantedListener() {
                    @SuppressLint("MissingPermission")
                    @Override
                    public void onPermissionGranted() {
                        RxDialogChooseImage dialogChooseImage = new RxDialogChooseImage(mContext, TITLE);
                        dialogChooseImage.show();
                    }
                });

4.3 RxPermissions

github star  8.6k的零时权限处理框架,业内熟知,推荐使用。


4.4 自己手撸一个零时权限框架推荐学习SoulPermission

这也许是Android一句话权限适配的更优解决方案

源码中,作者主要

  1. 优雅的避掉onPermissionResult:用一个没有界面的Fragment去完成我们权限请求的操作

  2. 舍去Activity:使用Application中的ActivityLifecycleCallbacks,使用它的 registerActivityLifecycleCallbacks,感知Activity声明周期变化,获取到当前应用栈顶的Activity。Application中ActivityLifecycleCallbacks的原理分析请看这篇文章;

  3. 最后避掉Application(免初始化):参考Lifecycle组件的初始化,通过自定义ContentProvider来完成库的初始化;

通过阅读学习,完全可以自己手撸仿写一个属于自己的零时权限框架!


4.5 总结&建议

  1. 请求权限显示的是标准Android对话框,我们不能自定义。
  2. targetSdkVersion 设置为 22 或更低版本只是权宜之计。作为App开发者,需要尽快适配新权限机制。
  3. 在某个功能模块严重依赖某些权限的情况下,为了减少程序中出现过多权限检查,可以在该模块入口处统一检查,如果没有授予相应权限,则不提供该模块使用。

你可能感兴趣的:(Android)