天天看点

Android权限适配全攻略

    • Android权限适配全攻略
      • 概述
      • 运行时权限管理机制介绍
        • 权限组
        • 权限组的作用
      • 权限适配
        • Android 60 运行时动态权限适配
        • 国产ROM自定义权限机制适配
        • 特殊权限适配
        • 不依赖Activity进行权限申请
        • Android80权限适配
      • 参考

Android权限适配全攻略

概述

在Android6.0(API 23)之前Android的权限机制太过于简单粗暴:App安装时会提示用户APP所需权限,用户同意安装后就会永久授权应用所需全部权限。其中大部分用户并不知道为什么APP需要这些权限,也不知道这些权限意味着什么。这样很容易被“别有用心”的APP利用,对用户的隐私及账户安全形成了很大的挑战。

Google也开始意识到问题的严重性并且开始收紧权限,在6.0(API 23)做了大的改动,引入了动态权限管理概念。

运行时权限管理机制介绍

对于开发者而言,在6.0之前应用申请权限只要将权限在Androidmanifest.xml中注册即可获取对应权限。在6.0之后Google将所有权限进行了整合分组,将系统权限分为几个保护级别也就是权限组。

权限组

需要了解的两个最重要保护级别是normal permissions和dangerous permissions:

normal permissions:涵盖应用需要访问数据或资源,但对用户隐私或其他应用操作风险很小的区域。例如,设置时区的权限就是正常权限。如果应用声明其需要正常权限,系统会自动向应用授予该权限。

dangerous permissions:涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。

查看dangerous permissions权限可以在perm-groups 或通过

adb shell pm list permissions -d -g

查看

总结一下就是Google将权限进行了分组,申请属于normal permissions的权限在manifest中声明即可,对于dangerous permissions 权限必须运行时动态申请。

权限组的作用

开发者可能要问了:一个规模一般的APP需要的权限最少也有7,8个,难道要一个个申请么?这开发工作量可不小啊!

Google也考虑到了对开发者的影响,上面介绍的权限组的作用也就体现出来了:

如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。

如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限。

总结一下就是开发者申请危险权限时,只要获得过属于同一组的其他权限的授权,即可立即获得系统对该权限的授权,无需重复申请。

权限适配

权限适配主要受APP运行环境及APP开发使用的

targetSdkVersion

有关,分为以下几种情况:

  • targetSdkVersion >= 23 && App运行在 >= 6.0(api 23)系统的终端上

    这种情况就会采用标准的Google**运行时动态权限机制**

  • targetSdkVersion >= 23 && App运行在 < 6.0(api 23)系统的终端上

    这种情况就会采用原权限机制即安装时获取授权机制

  • targetSdkVersion < 23 && App运行在 >= 6.0(api 23)系统的终端上

    这种情况就会采用兼容模式,即在安装时获取授权。但是用户可以在设置界面随时关闭/开启 敏感权限。APP需做好权限关闭的逻辑处理。

  • targetSdkVersion < 23 && App运行在 < 6.0(api 23)系统的终端上

    这种情况就会采用原权限机制即安装时获取授权机制

注意:部分国产ROM存在自定义权限机制导致差异,下节详述。

Android 6.0 运行时动态权限适配

适配权限按步骤可以分为四步:权限检查,权限申请,授权结果处理,权限解释。

  • 权限检查:可用的API较多,如下:
    • 使用

      Activity#checkSelfPermission(String permission)

      进行检查
    • 使用

      ContextCompat#checkSelfPermission(Context context, String permission)

      进行检查
    推荐使用

    ContextCompat#checkSelfPermission

    进行检查,这样可以不依赖Activity。

    已获授权返回

    PackageManager#PERMISSION_GRANTED

    ,未获授权返回

    PackageManager#PERMISSION_DENIED

  • 权限申请:可以同时申请多个权限,requestCode标识当前申请,会弹出系统权限申请弹窗。
    • 使用

      Activity#requestPermissions(String[] permissions, int requestCode)

    • 使用

      ActivityCompat#requestPermissions(Activity activity,String[] permissions, int requestCode)

    推荐使用

    ActivityCompat#requestPermissions

    ,会根据API版本使用最佳方式。

    我们可以注意到,无论使用哪种方式进行权限申请都需要依赖Activity,因为系统在权限处理后是需要回调到Activity。

  • 授权结果处理:权限请求会通过接口回调的方式回调到Activity:
/**
   * @param requestCode 权限申请request code
   * @param permissions 申请的权限
   * @param grantResults 授权结果
   */
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
}
           
  • 权限解释:Google提供了

    ActivityCompat#shouldShowRequestPermissionRationale

    API可以判断用户是否拒绝过当前申请权限,开发者可以弹出自定义框解释申请权限原因
public boolean shouldShowRequestPermissionRationale(permission)
{
    、APP是第一次申请这个权限的话,返回false
    、用户拒绝,勾选了不再提示的话,返回false
    、用户拒绝,但是没有勾选不再提示的话,返回true
    因此如果想在第一次就给用户提示,需要记录权限是否申请过,没有申请过的话,强制弹窗提示,而不能根据这个方法的返回值来。
    最好在第一次给用户提示,因为Google的权限申请文案本地化翻译太差劲了,会导致用户很莫名其妙。
}
           

这几步就是一个标准的权限申请处理步骤,我们可以看出步骤虽然不多,但是确实挺繁琐的。权限一多,需要写很多重复代码。这里我推荐大家一些处理的比较好的开源库,可以让权限申请更简单:

1. EasyPermissions ,Google官方示例推荐。只需关心权限处理结果即可,还可以结合注解使代码更精简。

2. PermissionsDispatcher ,封装了大部分权限申请逻辑,使用方便。

3. AndPermission ,封装的比较好,对国产ROM做了支持,兼容Android8.0权限变化,API链式调用简洁方便。

国产ROM自定义权限机制适配

相信很多开发者在机型适配或使用中会发现有些机型明明是6.0以下版本,但是也存在运行时权限管理机制。

这是因为部分国产ROM(MIUI等)在4.4以上自己使用了一套权限检测机制,在这些机型上如果我们使用上一节介绍的6.0权限适配是无法适用的,具体的表现如下:

1. 使用

ContextCompat#checkSelfPermission

无法检测出当前权限是否被授权,在API23以下manifest中注册过就会返回true,API23以上可能出现实际是被关闭但是返回true的情况。

2. 不支持动态权限申请

那我们该如何检测权限是否被正确授权?

虽然很坑爹,但是办法还是有的。

查阅资料发现国产ROM中针对权限的开关可以被

AppOpsManager#noteOp(int op)

检测出来,我们可以利用这个类来检测权限授权状态,这里我们介绍这个个新的类

AppOpsManager

/**
 * API for interacting with "application operation" tracking.
 *
 * <p>This API is not generally intended for third party application developers; most
 * features are only available to system applications.  Obtain an instance of it through
 * {@link Context#getSystemService(String) Context.getSystemService} with
 * {@link Context#APP_OPS_SERVICE Context.APP_OPS_SERVICE}.</p>
 */
public class AppOpsManager {

}
           

AppOpsManager中将权限与OP_*操作码做了一一映射,比如:

android.Manifest.permission.READ_CONTACTS

public static final int OP_READ_CONTACTS = ;
           

这些操作码与支持的权限是在不断添加的,我们可以通过观察

public static final int _NUM_OP = 62;

知道当前API版本支持的操作总数,需要做好兼容。

但是通过注释可以知道,这个类主要是给系统应用使用的,并不提供给第三方应用开发者。具体表现在类中基本所有方法都被@hide标记,开发者是无法直接使用的。那该怎么办呢?

我们搜索AppOpsManager有关的类,发现Google贴心的在v4包中提供了一个兼容类

AppOpsManagerCompat

,这个类对开发者来说是开放的,并且从API4向后兼容。

/**
     * 检测权限,小米上使用
     * 
     * @param context
     * @param permission
     * @return
     */
    private static boolean hasSelfPermissionForXiaomi(Context context, String permission) {
        // 将permission转换成OP_*
        String permissionToOp = AppOpsManagerCompat.permissionToOp(permission);
        if (permissionToOp == null) {
            // 不支持的权限,或者是normal permission
            return true;
        }
        int noteOp = AppOpsManagerCompat.noteOp(context, permissionToOp, android.os.Process.myUid(), context.getPackageName());
        // AppOpsManagerCompat 与 checkSelfPermission都检测过则表明权限被开启
        return noteOp == AppOpsManagerCompat.MODE_ALLOWED && ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
    }
           

我们使用这个方法就能对像小米这种自定义权限ROM进行权限检查了,但在测试中发现这在< API 23上一直返回true,这是什么原因呢?我们看看

AppOpsManagerCompat

是如何实现的:

private static final AppOpsManagerImpl IMPL;
    static {
        if (Build.VERSION.SDK_INT >= ) {
            IMPL = new AppOpsManager23();
        } else {
            IMPL = new AppOpsManagerImpl();
        }
    }

    private static class AppOpsManagerImpl {
        public String permissionToOp(String permission) {
            // 直接返回null,导致检测方法直接判断为成功
            return null;
        }

        public int noteOp(Context context, String op, int uid, String packageName) {
            // 直接调用也会直接返回MODE_IGNORED,不会进行检测
            return MODE_IGNORED;
        }

        public int noteProxyOp(Context context, String op, String proxiedPackageName) {
            return MODE_IGNORED;
        }
    }

    private static class AppOpsManager23 extends AppOpsManagerImpl {
        @Override
        public String permissionToOp(String permission) {
            return AppOpsManagerCompat23.permissionToOp(permission);
        }

        @Override
        public int noteOp(Context context, String op, int uid, String packageName) {
            return AppOpsManagerCompat23.noteOp(context, op, uid, packageName);
        }

        @Override
        public int noteProxyOp(Context context, String op, String proxiedPackageName) {
            return AppOpsManagerCompat23.noteProxyOp(context, op, proxiedPackageName);
        }
    }
           

我们发现

AppOpsManagerCompat

在23以下的实现类中,一些方法直接返回指定值并不会进行权限检测。

这就尴尬了,在低版本

AppOpsManagerCompat

失效,而且

AppOpsManager

方法又是@hide无法直接使用,那只能用反射去实现了:

/**
     * 通过反射调用 AppOpsManager#noteOp
     * 
     * @param context
     * @param op
     * @return
     */
    public static boolean noteOp(Context context, int op) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            if (appOpsManager != null) {
                try {
                    Method method = AppOpsManager.class.getDeclaredMethod("noteOp", Integer.TYPE, Integer.TYPE,
                                String.class);
                    int noteOp = (int) method.invoke(appOpsManager, op, Process.myUid(), context.getPackageName());
                    return noteOp == AppOpsManager.MODE_ALLOWED;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }
           

总结:

对于权限检测:

1. Build.VERSION.SDK_INT >= 23时,直接使用我们上面的

hasSelfPermissionForXiaomi

方法

2. Build.VERSION.SDK_INT < 23时,使用我们上面的反射方法

noteOp

对于权限申请:

1. Build.VERSION.SDK_INT >= 23时,使用上节介绍的标准运行时权限申请方式

2. Build.VERSION.SDK_INT < 23时,没有办法申请API,只能通过引导用户跳转权限设置页进行配置。

特殊权限适配

有几个权限其行为方式与正常权限及危险权限都不同。

SYSTEM_ALERT_WINDOW

WRITE_SETTINGS

特别敏感,因此大多数应用不应该使用它们。

SYSTEM_ALERT_WINDOW

我们接触的比较多,有些功能像桌面悬浮窗,桌面歌词等,只要是通过向WindowManager里面添加View方式的,都需要通过这个权限来实现。

这里拿

SYSTEM_ALERT_WINDOW

来讲解如何处理这种特殊权限:

// AndroidManifest中添加权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
           
// 定义权限申请request code
public static int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE= ;

public void requestPermission() {
    // 通过Settings.canDrawOverlays(this)可以检测是否包含SYSTEM_ALERT_WINDOW权限
        if (!Settings.canDrawOverlays(this)) {
            // 申请权限,跳转到系统权限设置页面
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
        }
    }

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 在onActivityResult中判断是否授权成功
    if (requestCode == ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE) {
        if (Settings.canDrawOverlays(this)) {
            // 授权成功
        }
    }
}
           

Tips: 悬浮窗实现可以使用

LayoutParams.TYPE_TOAST

替换

LayoutParams.TYPE_SYSTEM_ALERT

LayoutParams.TYPE_TOAST

的使用不需要权限。(LayoutParams.TYPE_TOAST限制就是Android 4.4 (API level 18)及以下使用TYPE_TOAST无法接收触摸事件)

“不依赖Activity”进行权限申请

我们在6.0运行时动态权限适配一节中说到了,目前API进行权限申请都需要依赖Activity,因为系统在权限处理后是需要回调到Activity做处理的。

大部分场景都是可以满足的,但是对于没有Activity或者需要在不存在具体Activity进行权限申请的情况,就有点尴尬了。

输入法就遇到了这种尴尬的情况,输入法是系统服务,键盘区展示出来的其实是个Dialog。

有个这种场景:用户在输入法上点击语言输入,输入法需要获取录音权限才能进一步操作,那如何在这种场景去申请权限呢?

这就是我们这节的重点了,如何“不依赖Activity”进行权限申请,我们的方案如下:

我们这里的“不依赖Activity”指的是不依赖具体业务Activity,申请权限时启动一个只用于权限申请的Activity来完成申请流程。

这里我结合easypermissions 实现的例子如下:

PermissionsController封装了权限请求,通过统一API进行权限申请;具体申请交由EasyPermission(可替换任何库),仅处理对权限申请结果监听回调的处理

申请权限时仅需调用,并在回调中处理结果即可

PermissionsController.get(mContext).requestPermissions(this, null, RC_RECORD_PERM, Manifest.permission
                .RECORD_AUDIO);
           
// PermissionsController.java
/**
 * <pre>
 *     author : lee
 *     e-mail : [email protected]
 *     time   : 2017/07/11
 *     desc   : 权限控制类,用于处理权限请求时机控制等
 *     version: 1.0
 * </pre>
 */


public class PermissionsController {

    private static final String TAG = "PermissionsTag";
    private static PermissionsController sInstance;
    private final Context mContext;
    // 权限回调监听
    private final SparseArray<PermissionsResultCallback> mRequestIdToCallback = new SparseArray<>();


    public PermissionsController(Context context) {
        mContext = context.getApplicationContext();
    }

    @Nonnull
    public static synchronized PermissionsController get(@Nonnull Context context) {
        if (sInstance == null) {
            sInstance = new PermissionsController(context);
        }
        return sInstance;
    }

    /**
     * 请求权限;封装了键盘和常规请求
     *
     * @param callback 申请权限回调,不能为空
     * @param activity 权限处理activity,键盘区传null,其他非空
     * @param permissionsToRequest 需要申请的权限,多个权限可组合同时申请
     */
    public synchronized void requestPermissions(@Nonnull PermissionsResultCallback callback,
                                                Activity activity,
                                                int requestId,
                                                String... permissionsToRequest) {
        List<String> deniedPermissions = getDeniedPermissions(mContext, permissionsToRequest);
        if (deniedPermissions.isEmpty()) {
            // 被拒绝的权限为空,直接回调成功
            callback.onPermissionsGranted(requestId, deniedPermissions);
            return;
        }

        String[] permissionsArray = deniedPermissions.toArray(
                new String[deniedPermissions.size()]);

        if (activity != null) {
            String rationale = activity.getString(R.string.permission_msg);
            requestPermissions(activity, rationale, requestId, permissionsArray);
        } else {
            mRequestIdToCallback.put(requestId, callback);
            PermissionsActivity.run(mContext, requestId, permissionsArray);
        }
    }

    /**
     * 常规请求方式,用于在activity中
     *
     * @param host 权限处理activity
     * @param rationale 权限申请解释文本,仅当用户拒绝首次后才能弹出
     * @param requestCode 权限申请request code
     * @param perms 需要申请的权限,多个权限可组合同时申请
     */
    public static void requestPermissions(
            @NonNull Activity host, @NonNull String rationale,
            int requestCode, @NonNull String... perms) {
        EasyPermissions.requestPermissions(host, rationale, requestCode, perms);
    }

    /**
     * 申请成功回调
     *
     * @param requestCode 权限申请request code
     * @param perms 授权成功权限
     */
    public synchronized void onPermissionsGranted(int requestCode, List<String> perms) {
        PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
        mRequestIdToCallback.remove(requestCode);
        if (permissionsResultCallback != null) {
            permissionsResultCallback.onPermissionsGranted(requestCode, perms);
        }
    }

    /**
     * 申请失败回调
     *
     * @param requestCode 权限申请request code
     * @param perms 授权失败权限
     */
    public synchronized void onPermissionsDenied(int requestCode, List<String> perms) {
        PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
        mRequestIdToCallback.remove(requestCode);
        if (permissionsResultCallback != null) {
            permissionsResultCallback.onPermissionsDenied(requestCode, perms);
        }
    }

    /**
     *  权限申请结果处理,需在activity onRequestPermissionsResult回调中调用
     * @param requestCode 权限申请request code
     * @param permissions 申请的权限
     * @param grantResults 授权结果
     * @param receivers 授权结果处理
     */
    public static void onRequestPermissionsResult(int requestCode,
                                                  @NonNull String[] permissions,
                                                  @NonNull int[] grantResults,
                                                  @NonNull Object... receivers){
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, receivers);
    }

    /**
     * 存在一些权限被永久的拒绝
     *
     * @param host
     * @param deniedPermissions
     * @return
     */
    public static boolean somePermissionPermanentlyDenied(@NonNull Activity host,
                                                          @NonNull List<String> deniedPermissions) {
        return EasyPermissions.somePermissionPermanentlyDenied(host, deniedPermissions);
    }


    /**
     * 展示跳转设置页提示窗
     *
     * @param host
     */
    public static void showAppSettingDialog(@NonNull Activity host) {
        new AppSettingsDialog.Builder(host).build().show();
    }


    /**
     * 是否已获取当前权限
     *
     * @param context
     * @param perms
     * @return
     */
    public static boolean hasPermissions(Context context, @NonNull String... perms) {
        return EasyPermissions.hasPermissions(context, perms);
    }

    /**
     * 获取未授权权限
     *
     * @param context
     * @param permissions
     * @return
     */
    public static List<String> getDeniedPermissions(Context context, String... permissions) {
        final List<String> deniedPermissions = new ArrayList<String>();
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(context, permission)
                    != PackageManager.PERMISSION_GRANTED) {
                deniedPermissions.add(permission);
            }
        }
        return deniedPermissions;
    }

    /**
     * 权限回调接口,直接继承 EasyPermissions.PermissionCallbacks
     */
    public interface PermissionsResultCallback extends EasyPermissions.PermissionCallbacks{

    }

}
           

PermissionsActivity 统一处理权限申请Activity,因为本身只是个载体,样式透明并不设置任何ContentView。

// AndroidManifest.xml
<!-- 申请权限activity -->
        <activity android:name=".permission.PermissionsActivity"
                  android:theme="@style/permissionTheme"
                  android:taskAffinity=""
                  android:configChanges="orientation|screenSize|keyboardHidden"/>
           
// styles.xml
<style name="permissionTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
        <item name="android:statusBarColor" tools:targetApi="lollipop">@android:color/transparent</item>
    </style>
           
// PermissionsActivity.java
/**
 * <pre>
 *     author : lee
 *     e-mail : [email protected]
 *     time   : 2017/07/11
 *     desc   : 请求权限的Activity
 *     version: 1.0
 * </pre>
 */


public class PermissionsActivity extends Activity implements PermissionsController.PermissionsResultCallback {

    private static final String TAG = "PermissionsActivity";
    // 需要请求的权限
    public static final String EXTRA_PERMISSION_REQUESTED_PERMISSIONS = "requested_permissions";
    // 请求权限的requestCode
    public static final String EXTRA_PERMISSION_REQUEST_CODE = "request_code";
    // 默认无效requestCode
    private static final int INVALID_REQUEST_CODE = -;

    private int mPendingRequestCode = INVALID_REQUEST_CODE;


    /**
     * 启动一个权限Activity,并立即请求权限
     *
     * @param context
     * @param requestCode
     * @param permissionStrings
     */
    public static void run(
            @NonNull Context context, int requestCode, @NonNull String... permissionStrings) {
        Intent intent = new Intent(context.getApplicationContext(), PermissionsActivity.class);
        intent.putExtra(EXTRA_PERMISSION_REQUESTED_PERMISSIONS, permissionStrings);
        intent.putExtra(EXTRA_PERMISSION_REQUEST_CODE, requestCode);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPendingRequestCode = (savedInstanceState != null)
                ? savedInstanceState.getInt(EXTRA_PERMISSION_REQUEST_CODE, INVALID_REQUEST_CODE)
                : INVALID_REQUEST_CODE;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(EXTRA_PERMISSION_REQUEST_CODE, mPendingRequestCode);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mPendingRequestCode == INVALID_REQUEST_CODE) {
            final Bundle extras = getIntent().getExtras();
            final String[] permissionsToRequest =
                    extras.getStringArray(EXTRA_PERMISSION_REQUESTED_PERMISSIONS);
            mPendingRequestCode = extras.getInt(EXTRA_PERMISSION_REQUEST_CODE);
            PermissionsController.requestPermissions(this, getString(R.string.permission_msg), mPendingRequestCode,
                    permissionsToRequest);
        }
    }

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

    @Override
    public void onPermissionsGranted(int requestCode, List<String> perms) {
        PermissionsController.get(this).onPermissionsGranted(requestCode, perms);
        finish();
    }

    @Override
    public void onPermissionsDenied(int requestCode, List<String> perms) {
        PermissionsController.get(this).onPermissionsDenied(requestCode, perms);
        if (PermissionsController.somePermissionPermanentlyDenied(this, perms)) {
            if (BuildConfig.DEBUG) {
                Log.d("PermissionsTag", "存在一些权限被永久的拒绝");
            }
            PermissionsController.showAppSettingDialog(this);
        }
        finish();
    }
}
           

以上代码还存在优化空间,但是思路就是这样。

Android8.0权限适配

我们之前在Android6.0权限申请时介绍过开发者申请危险权限时,只要获得过属于同一组的其他权限的授权,即可立即获得系统对该权限的授权,无需重复申请这一特性。

但是Android8.0对此行为发生了变化,具体表现为:系统只会授予应用明确请求的权限。然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准,不会显示弹框。

区别:

1. Android6.0在申请权限成功后,属于同一组的其他权限会自动授权,无需申请。

2. Android8.0在申请权限成功后,在使用同一组权限时还是需要进行申请操作,只是申请会被自动授权。不申请则不会获得授权。

所以为了兼容8.0的变化,开发者在申请权限时最好一同申请权限组内其他权限,做好权限检查。更多内容可参考:Android8.0运行时权限策略变化和适配方案

参考

Android权限机制与适配经验

Android 开发者必知必会的权限管理知识

Android8.0运行时权限策略变化和适配方案