权限管理 -1

 blob.png

随着棉花糖的出现,一种新的权限模式添加到了安卓中,这也导致开发者需要对安卓权限管理采取稍微不同的方法了。在这个系列中,我们将从技术和用户体验的角度,探讨处理权限请求的方法。 

一般而言app权限分为两种情况:一种是app核心功能所需的权限,没有这个权限都不能正常运行;一种是次要功能所需的权限。比如,对于一个相机app来说,CAMERA权限就是核心功能的一部分,一个不能拍照的相机应用完全就是无用的。但是也存在一些额外的功能,没有它app还是可以使用,比如为相片标注位置,这是一个不错的功能,需要ACCESS_FINE_LOCATION权限,但也并不是没有它app就不能操作了。

同样,我们准备在下一个系列文章中着手一个app,但是因为某些用处它需要两个权限:RECORD_AUDIO 和 MODIFY_AUDIO_SETTINGS。为了获取这些权限,我们需要像通常那样在Manifest中声明它们:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.stylingandroid.permissions">
  <uses-permission android:name="android.permission.RECORD_AUDIO" />
  <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  <application
    android:allowBackup="false"
    android:fullBackupContent="false"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme.NoActionBar"
    tools:ignore="GoogleAppIndexingWarning">
    <activity android:name=".MainActivity" />
    <activity
      android:name=".PermissionsActivity"
      android:label="@string/title_activity_permissions"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

这是至api 1之后在安卓上声明权限的标准方法。但是,一旦我们指定targetSdkVersion为23或者之后我们还需要在运行时请求这些所需的权限。这很重要,因为已经出现了很多开发者把targetSdkVersion飙到了最新,然后发现自己的app疯狂的崩溃,这是由于他们没有实现执行运行时权限请求的代码。当你已经把一个targeting API 为23或者之后的app发布到了Google Play上,这更是一个问题,你无法立即把那个apk的targeting API替换成更早的版本。

这里需要指出的是,目前已经有几个用来简化运行时权限请求过程的库。它们的质量和用处各异,但是我觉得在使用这些库之前,理解背后的过程还是相当重要的,否则可能因为你不了解自己所用的库到底做了什么而遇到问题。这也是这个系列文章的主要写作动机。

我们需要的两个权限刚好落在了两种权限中:RECORD_AUDIO 被视为敏感权限,而 MODIFY_AUDIO_SETTINGS被视为普通权限。一个敏感权限就是一个可能危及隐私和安全的权限;而普通权限则是为了获取app域之外的资源,但是对用户隐私只有很少或者没有风险。普通权限系统自动授权,而敏感权限则需要在运行时让用户明确授予。

在这个过程中,我们需要做的第一件事就是判断我们是都被授予了相关权限。在 API 23中,为了检查某个特定的权限是否已经被授权,Context中添加了几个新的方法。但是,为了避免自己写API-level 检查,最好使用ContextCompat而不是直接使用Context:

class PermissionsChecker {
    private final Context context;
    public PermissionsChecker(Context context) {
        this.context = context;
    }
    public boolean lacksPermissions(String... permissions) {
        for (String permission : permissions) {
            if (lacksPermission(permission)) {
                return true;
            }
        }
        return false;
    }
    private boolean lacksPermission(String permission) {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED;
    }
}

这段代码其实很简单-ContextCompat的checkSelfPermission方法顾名思义就是检查权限的意思,它返回 PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。其余的代码是这个app所需的,用于检查所需的某个权限是否还未被授权。

这里还得唠叨几句ContextCompat为我们做了的事情。当运行在不支持运行时权限模式的Marshmallow以前的设备上的时候,checkSelfPermission()方法总是会返回 PackageManger.PERMISSION_GRANTED   -  老的系统版本上总是隐式的授予权限。因此我们跨系统版本也只需一个方法调用,也不用去写什么检查API-level 的代码。

你可能会好奇为什么为了这个专门写了个类,主要是后面所有的Activity中都要做这个检查,所以把这个检查的逻辑独立出来,减少重复,提高代码的可维护性。

要早在Activity中实际使用它,我们只需传入activity所需的权限列表:

public class MainActivity extends AppCompatActivity {
    private static final String\[\] PERMISSIONS = new String\[\] {Manifest.permission.RECORD_AUDIO, Manifest.permission.MODIFY_AUDIO_SETTINGS};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PermissionsChecker checker = new PermissionsChecker(this);
        if (checker.lacksPermissions(PERMISSIONS)) {
            Snackbar.make(toolbar, R.string.no_permissions, Snackbar.LENGTH_INDEFINITE).show();
        }
    .
    .
    .
    }
}

简单吧。 

在 pre-Marshamllow的设备上正常工作:

blob.png

但是我们还没有为Marshmallow和之后的设备做任何处理 - 我们只是显示一个Snackbar而已:

blob.png

对于特殊权限的请求是更复杂的东西。我们将在下篇文章探讨。

本文的源代码在 这里

作者Mark Allison。本文最初发表在 Styling Android

英文原文:https://blog.stylingandroid.com/permissions-part-1/