The New Runtime Permission

随着Android正在被不断开发,最新已经更新到Android M,这版是完全不同的,因为这有一个可以改变一切的重大改变比如新的Runtime Permission。它是非常重要的,可能会导致在不久的将来你的APP就会有一些大的麻烦。

这就是为什么我今天决定在博客上写下这个题目的原因。你需要了解这个新的运行权限,包括如何实现它在你的代码。让我们开始吧,其实为时已晚了。

Runtime Permission

Android的权限系统一直以来是一个最大的安全问题在这些权限在安装时被允许。一旦安装,应用程序将能够访问所有东西,没有任何用户的确认究竟应用程序确实有权限授予了什么事。
也就不足为奇,为什么有那么多坏家伙试图通过这个安全漏洞来收集用户的个人数据,并用可恶的方式来使用它。
Android团队也知道这个问题。 7年过去了,权限系统终于重新设计了。在Android的6.0棉花糖,安装不再允许任何权限。相反,应用程序必须一个接一个询问用户权限在运行时。

Android 系统默认 时间服务器修改_android

请注意上面显示的该权限请求对话框不是自动启动的。开发者需要手动调用它。在开发者试图调用一些需要哪些用户尚未授予权限的一些方法的情况下,该方法可能会突然抛出一个异常,而且这个异常将导致应用程序崩溃。

Android 系统默认 时间服务器修改_应用程序_02

此外,用户还可以通过手机的 设置–> 应用程序里 随时撤销授予权限的许可。

您可能已经觉得后背发凉了吧……如果你是一名Android开发者,你会突然觉得编程逻辑完全改变了。现在你不能只是单单的调用一个函数来完成像以前一样的工作了,但你必须检查的每一个功能所需的权限,否则应用程序将就直接了当的崩溃了!

这个新的Runtime Permission会像刚才描述的那样崩溃只有当我们设置应用程序的targetSdkVersion到23或者更高,现声明应用程序已经在API级别23测试此功能,仅适用于Android6.0棉花糖系统。同样的应用程序将兼容比棉花糖老的设备。

让你的应用支持新的 Runtime Permission

现在是时候让我们的应用程序完美支持新的运行权限了。先把compileSdkVersion和targetSdkVersion设置为23再说。

android {
    compileSdkVersion 23
    ...

    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }

在这个例子中,我们尝试用一个方法来添加一个联系人:

private static final String TAG = "Contacts";
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);

    // First, set up a new raw contact.
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());

    // Next, set the name for the contact.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
                    "__DUMMY CONTACT from runtime permissions sample");
    operations.add(op.build());

    // Apply the operations.
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

以上代码需要一个WRITE_CONTACTS权限。如果没有这个权限的许可,那么这个程序将要崩溃掉。
下面用之前的旧方法在AndroidManifest.xml 中添加权限。

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

下面是我们要创建另一个函数来检查权限授予与否。如果没有,那么调用一个对话框,询问用户。否则(有权限的情况),你可以去下一个步骤,创建新的联系人。

权限分为权限组像见下表:

Android 系统默认 时间服务器修改_ide_03

如果权限组的某一个权限被授予,那么在同组的另一个权限也将被自动授予为可用。举个例子,如果WRITE_CONTACTS 是被允许的,那么应用也将允许READ_CONTACTSGET_ACCOUNTS

用于检查和请求授权的源代码分别为Activity的checkSelfPermission 方法和 requestPermissions 方法。这些方法都是在API 23中被加入的。

final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

如果权限授予,那么 insertDummyContact() 将被直接调用,否则,requestPermissions 将被调用,然后弹出一个请求权限的对话框。

Android 系统默认 时间服务器修改_应用程序_04

不管(Allow)允许还是(DENY)拒绝,Activity的onRequestPermissionsResult() 总是会被调用,我们可以从第三个参数获取结果grantResults 像这样的检查结果:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_PERMISSIONS:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

这就是Runtime Permission怎么运行的。代码相当复杂但是入股用熟了也就…让你的APP完美支持Runtime Permission,你就得这么操作相同的方法来处理这种情况。

如果你想锤墙,这是一个很好的机会。

关于操作“不在询问”(Never Ask Again)

如果用户拒绝了一个权限,那么在第二次运行程序的时候,用户会收到一个“不在询问”的对话框(Never Ask Again)以防以后再询问权限。

Android 系统默认 时间服务器修改_棉花糖_05

如果这个选项被选中,下次拒绝检查。接下来的时间我们调用requestPermissions,该对话框不会出现了。相反,它只会什么都不做。

这是相当糟糕的用户体验,如果这么操做了,就什么交互都不有了。这种情况必须要好好处理,在调用requestPermissions 方法之前我们需要阐述一下我们应用为什么需要这个权限的原因,通过shouldShowRequestPermissionRationale 方法。代码如下:

final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                                        REQUEST_CODE_ASK_PERMISSIONS);
                            }
                        });
                return;
            }
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(MainActivity.this)
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show();
}

这个说明原因的对话框会在第一次权限被询问的时候出现,也会在你标记过”不在询问“之后出现。对于后边这种情况,onRequestPermissionsResult将会调用并且返回PERMISSION_DENIED 。这样,就出现了这个说明原因的dialog。

Android 系统默认 时间服务器修改_ide_06

大功告成!

一次访问多个权限的情况multiple permissions

肯定是有一些功能,需要一个以上的权限。你可以在用上述同样的方法要求用户提供多个权限。无论如何不要忘记检查“不再询问”情况,为把每一个权限做好。
下面是修改后的代码:

final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;

private void insertDummyContactWrapper() {
    List<String> permissionsNeeded = new ArrayList<String>();

    final List<String> permissionsList = new ArrayList<String>();
    if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
        permissionsNeeded.add("GPS");
    if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
        permissionsNeeded.add("Read Contacts");
    if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
        permissionsNeeded.add("Write Contacts");

    if (permissionsList.size() > 0) {
        if (permissionsNeeded.size() > 0) {
            // Need Rationale
            String message = "You need to grant access to " + permissionsNeeded.get(0);
            for (int i = 1; i < permissionsNeeded.size(); i++)
                message = message + ", " + permissionsNeeded.get(i);
            showMessageOKCancel(message,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                        }
                    });
            return;
        }
        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
        return;
    }

    insertDummyContact();
}

private boolean addPermission(List<String> permissionsList, String permission) {
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        permissionsList.add(permission);
        // Check for Rationale Option
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }
    return true;
}

当每一个权限有了允许的结果,这些结果会触发相同的回调方法,onRequestPermissionsResult 。我使用的HashMap,使源代码看起来更整洁,更具有可读性。

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
            {
            Map<String, Integer> perms = new HashMap<String, Integer>();
            // Initial
            perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
            // Fill with results
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);
            // Check for ACCESS_FINE_LOCATION
            if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                // All Permissions Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

应用条件是灵活的,你必须自己来设置一下。在一些情况下,即便有一个权限被禁用了,只是禁用了某些功能,某些情况下,用户还是具备使用某些功能的能力的。当然了,这都看怎么设计了。

使用Support 库向前兼容 (Use Support Library to make code forward-compatible)

虽然上面的代码完美运行在Android6.0棉花糖系统上。不幸的是,它会在比Android棉花糖低的版本上崩溃,因为这些API 是 API23 版本的附加功能。

最直接的方式是直接检查API版本:

if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

但是这样代码会比较繁琐。所以建议最好使用 Support Library v4,这里边已经帮你做好了这些内容。用这些方法来替代:

ContextCompat.checkSelfPermission()

不管应用程序是不是运行于Android M上。如果权限被授予此函数将返回允许(PERMISSION_GRANTED)。否则PERMISSION_DENIED将被返回。

ActivityCompat.requestPermissions()

如果这个函数被先于棉花糖的系统调用,onRequestPermissionsResultCallback会立即调用,返回权限申请成功PERMISSION_GRANTED或PERMISSION_DENIED权限被拒绝的结果。

ActivityCompat.shouldShowRequestPermissionRationale()

如果这个函数被先于棉花糖的系统调用,他将一直返回false

用Support Library v4中的这些方法替换Activity的checkSelfPermission, requestPermissions shouldShowRequestPermissionRationale 方法,这样你的应用可以用相同的代码逻辑完美运行在各个版本的安卓系统上。但是需要注意的是,这写方法可能会需要一些参数,Context或者Activity。代码如下

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.WRITE_CONTACTS)) {
            showMessageOKCancel("You need to allow access to Contacts",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[] {Manifest.permission.WRITE_CONTACTS},
                                    REQUEST_CODE_ASK_PERMISSIONS);
                        }
                    });
            return;
        }
        ActivityCompat.requestPermissions(MainActivity.this,
                new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

这些代码也可以应用到Support Library v4中的Fragment。你可以随便移动这些逻辑到Fragment中。

三方库

可能有些朋友最喜欢的就是这些。

Permissionsdispatcher

如果允许的权限中途被吊销,会发生什么事情

权限可以随时通过手机的设置 -> 应用 -> 某个app -> 权限 里撤销。如果正在运行的应用权限吊销是会造成崩溃的。所以onResume尽量检查权限。

建议

这种更改别无选择。如果你还没有做兼容,你最好不要擅自把targetSdkVersion改成23。