Android 运行时权限支持

本指南介绍如何在使用 Solar2D Native 构建的应用/插件中添加对 Android 运行时权限的支持。

关于 Android 运行时权限

当 Android 应用程序在设备上运行时,操作系统仅提供对资源的有限访问权限。在您的应用程序(或插件)可以使用受限资源之前,您必须声明使用它们的意图。这是通过编辑您的 `AndroidManifest.xml` 或在 `metadata.lua` 中指定权限来完成的。有关更多信息,请参阅声明权限

对于在 **Android 5.1** 或更低版本上运行的应用,`AndroidManifest.xml` 文件是允许哪些权限的唯一决定因素,因为用户在安装应用时会批准它们。

对于在 **Android 6.0** 或更高版本上运行的应用,应用不能可靠地假设它具有访问危险权限的权限。

危险权限表中**未**定义的权限不需要在运行时处理,但它们仍然需要声明。从本质上讲,这意味着应用/插件不能总是假设它具有所需的权限,因此它必须优雅地处理用户的权限。本指南涵盖了如何检查权限状态、请求权限以及处理结果。

回顾
  • 如果设备运行的是 **Android 5.1** 或更低版本,则在应用安装时授予权限。无需执行运行时检查。

  • 如果设备运行的是 **Android 6.0** 或更高版本,则会在运行时请求危险权限。这要求开发者和最终用户都考虑是否应授予权限。

注意

Corona 相机示例展示了 `media.hasSource( media.Camera )` 如何被修改以向用户报告更多信息,以及您的项目应如何考虑这些信息。

以 Android 6.0 为目标

要开始支持和测试运行时权限,您必须首先以 Android 6.0 为目标。只需在项目中的几个位置将数字更改为 `23`(API 级别 23 对应于 Android 6.0)。

  1. `AndroidManifest.xml` — 将 `android:targetSdkVersion="xx"` 更改为 `android:targetSdkVersion="23"`。这将允许您在本地项目中测试您的更改。

  2. `project.properties` — 将target=Google Inc.:Google APIs:xx更改为target=Google Inc.:Google APIs:23。与上面一样,这是为了测试您的更改。

  3. `project.plugin.properties` — 对于原生开发的插件,将 `target=android-xx` 行更改为 `target=android-23`。此更改将影响插件本身。

声明权限

确定应用/插件需要哪些权限后,声明将按如下方式处理

local metadata =
{
    plugin =
    {
        manifest =
        {
            usesPermissions =
            {
                "android.permission.SEND_SMS",
            },
        },
    },
}
return metadata
重要

此外,对于 Solar2D Native 开发人员,您应该告知他们必须将这些权限显式添加到他们的 `AndroidManifest.xml` 文件中(参见下一点).

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.snazzyapp">

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

</manifest>

扩展权限 API

在 Corona 中支持运行时权限与在本机 Android 应用中支持它们非常相似。本指南是对 Google 的运行时请求权限指南的补充,并与其并行进行。

在 Corona 中,我们为开发人员提供了一个接口来处理他们的权限请求,而无需担心 API 级别或权限泄漏

您需要的大部分功能都来自我们的 `com.ansca.corona.permissions.PermissionsServices` 类。这提供了几个 API,旨在将 Google 的权限处理与跨平台Corona 处理权限的方式联系起来。它还提供了易于使用的API,用于收集有关 `AndroidManifest.xml` 文件状态以及各种权限状态的信息。

有关详细信息,请参阅下面的检查权限请求权限

检查权限

这可以通过 Google 的context.checkSelfPermission() API 来完成,但是它仅在 API 级别 23 中可用,它不能防止权限泄漏,并且它会忽略 `AndroidManifest.xml` 中未列出权限的实例。相反,我们建议使用 Corona 的 `PermissionsServices.getPermissionStateFor()`API 检查权限。这提供了所有受支持的 Android 版本上权限的状态,还可以确定所需的权限是否在 `AndroidManifest.xml` 中丢失。

// Determine the PermissionState for the our ability to send SMS messages.
PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
PermissionState sendSMSState = permissionsServices.getPermissionStateFor(PermissionsServices.Permission.SEND_SMS);

在这里,我们使用 `PermissionsServices` 实例来查询 `SEND.SMS` 权限的状态。如果我们得到 `PermissionState.GRANTED` 返回,我们知道我们可以继续调用需要此权限的代码。

权限状态

`PermissionsServices.getPermissionStateFor()` 返回一个具有以下值之一的 `PermissionState` 枚举。您的应用/插件必须能够处理这些可能的权限状态,而不会崩溃或进入不良状态。

PermissionState.GRANTED

用户已授予此权限。对应于 Google 的 `android.content.pm.PackageManager.PERMISSION_GRANTED`。当权限处于此状态时,您可以执行任何需要该权限的操作。

PermissionState.MISSING

此权限未在应用的 `AndroidManifest.xml` 文件中列出。如果权限处于此状态,则可能适用以下情况之一

  • 对于原生插件,`metadata.lua` 可能未将权限指定为必需的。
  • 故意省略该权限,因为不应使用它。

您处理 `PermissionState.MISSING` 的上下文取决于特定权限对于核心功能是强制性的,还是仅仅与非关键功能相关联。例如,QR 扫描器插件绝对需要 `Camera` 权限才能运行,如果 `Camera` 权限状态为 `PermissionState.MISSING`,您应该警告开发人员他们必须将其添加到他们的 `AndroidManifest.xml` 文件中。Corona 的 `PermissionsServices.showMissingFromManifestAlert()` API 使这变得容易

// Display a "missing from manifest" alert
PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
permissionsServices.showPermissionMissingFromManifestAlert(PermissionsServices.Permission.CAMERA, "The <Super Cool QR Scanner Plugin> requires access to the device's camera(s)!");

PermissionState.DENIED

此权限已被明确拒绝,或尚未请求。对应于 Google 的 `android.content.pm.PackageManager.PERMISSION_DENIED`。

如果权限处于此状态,则表示该权限已在项目的 `AndroidManifest.xml` 文件中指定,但 Android 操作系统已拒绝应用访问该权限。此时,您应该通过 `PermissionsServices.shouldNeverAskAgain()` 检查用户是否仍希望请求该权限。假设用户仍然希望请求此权限,则应用现在应该请求访问被拒绝的权限。

// Request permission to use external storage
PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
if (!permissionsServices.shouldNeverAskAgain(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE)) {
    // Request Write External Storage permission. We cover how to do this in the section below
}

将所有内容放在一起时,检查权限状态并处理所有可能结果的代码通常将包含此模板

// Only do our dangerous work if we have permission to use external storage
PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
PermissionState writeExternalStoragePermissionState = permissionsServices.getPermissionStateFor(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE);
switch(writeExternalStoragePermissionState) {
    case MISSING:
        // The Corona developer forgot or refused to add the permission to the AndroidManifest.xml
        break;
    case DENIED:
        // Check PermissionsServices.shouldNeverAskAgain() and try to request permission
        break;
    default:
        // Permission is granted; continue as usual
        doDangerousWork();
}

请求权限

现在您知道如何确定权限的状态,您需要使用 `permissionsServices.requestPermissions()` 请求所需的权限。继续阅读以了解 `PermissionsSettings` 和 `OnRequestPermissionsHandler`。

重要

调用 `PermissionsServices.requestPermissions()` 是异步的,它会在向用户显示权限 UI 时暂停用户的应用程序。响应将在主 UI 线程上,而不是在 Lua 线程上。如有必要,请务必使用 `CoronaRuntimeTaskDispatcher`。

使用权限设置

`PermissionsSettings` 对象用于定义您要请求的权限。例如,您可以请求单个权限

// Define that we will request permission to write to external storage
PermissionsSettings settings = new PermissionsSettings(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE);

PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
permissionsServices.requestPermissions(settings, MyOnRequestPermissionsHandler);

或者,您可以请求多个权限

// Define that we will request permission to write to external storage and access the camera
PermissionsSettings settings = new PermissionsSettings(new String[] {PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE, PermissionsServices.Permission.CAMERA});

PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
permissionsServices.requestPermissions(settings, MyOnRequestPermissionsHandler);

在这两个示例中,`MyOnRequestPermissionsHandler` 都是 `OnRequestPermissionsHandler` 的实现。在下一节中了解更多信息。

请求权限处理程序

由于 `PermissionsServices.requestPermissions()` 是异步的,因此需要一个回调来指示用户何时完成拒绝或授予请求的权限。这是通过 `CoronaActivity.OnRequestPermissionsResultHandler` 接口完成的。当用户完成后,将调用此接口中的 `onHandleRequestPermissionsResult` 方法。

这是一个基本实现

private class MyRequestPermissionsResultHandler implements CoronaActivity.OnRequestPermissionsResultHandler {
    @Override
    public void onHandleRequestPermissionsResult(CoronaActivity activity, int requestCode, String[] permissions, int[] grantResults) {
        // Clean up and unregister our request (you should always do this)
        PermissionsSettings permissionsSettings = activity.unregisterRequestPermissionsResultHandler(this);
        if (permissionsSettings != null) {
            permissionsSettings.markAsServiced();
        }

        // Use PermissionServices to ensure that we have permission to write to external storage
        PermissionsServices permissionsServices = new PermissionsServices(activity);
        if (permissionsServices.getPermissionStateFor(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE) == PermissionState.GRANTED) {
            // Handle the case where our permission was granted
        } else {
            // Handle the case where our permissions were not granted
        }
    }
}

然后,您可以按如下方式使用 `OnRequestPermissionsResultHandler`

PermissionsSettings settings = new PermissionsSettings(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE);
MyRequestPermissionsResultHandler handler = new MyRequestPermissionsResultHandler()

PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
permissionsServices.requestPermissions(settings, handler);

完整示例

综上所述,我们可以编写一个实现 `CoronaActivity.OnRequestPermissionsResultHandler` 并为我们处理权限的类。实现可能如下所示

private class MyFileIOFunctionRequestPermissionsResultHandler implements CoronaActivity.OnRequestPermissionsResultHandler {
    public void handleRun() {
        // Check for WRITE_EXTERNAL_STORAGE permission
        PermissionsServices permissionsServices = new PermissionsServices(CoronaEnvironment.getApplicationContext());
        PermissionState writeExternalStoragePermissionState = permissionsServices.getPermissionStateFor(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE);
        switch(writeExternalStoragePermissionState) {
            case MISSING:
                // The Corona developer didn't add the permission to the AndroidManifest.xml
                // As it is required for our app to function, we'll error out here
                // If the permission were not critical, we could work around it here
                permissionsServices.showPermissionMissingFromManifestAlert(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE, "My plugin requires access to the device's storage!");
                break;
            case DENIED:
                if (!permissionsServices.shouldNeverAskAgain(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE)) {
                    // Create our Permissions Settings to compare against in the handler
                    PermissionsSettings settings = new PermissionsSettings(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE);

                    // Request Write External Storage permission
                    permissionsServices.requestPermissions(settings, this);
                }
                break;
            default:
                // Permission is granted!
                run();
        }
    }

    @Override
    public void onHandleRequestPermissionsResult(CoronaActivity activity, int requestCode, String[] permissions, int[] grantResults) {
        // Clean up and unregister our request (you should always do this)
        PermissionsSettings permissionsSettings = activity.unregisterRequestPermissionsResultHandler(this);
        if (permissionsSettings != null) {
            permissionsSettings.markAsServiced();
        }

        // Check for WRITE_EXTERNAL_STORAGE permission
        PermissionsServices permissionsServices = new PermissionsServices(activity);
        if (permissionsServices.getPermissionStateFor(PermissionsServices.Permission.WRITE_EXTERNAL_STORAGE) == PermissionState.GRANTED) {
            run();
        } else {
            // We were denied permission
        }
    }

    public void run() {
        // We are sure we have the required permissions; write to storage!
    }

我们会将危险代码移到上面的 `run()` 函数中。公开的 Lua 函数可能如下所示

private class MyFileIOFunction implements NamedJavaFunction {
    @Override
    public String getName() {
        // e.g. "myPlugin.doSomethingWithExternalStorage()"
        return "doSomethingWithExternalStorage";
    }

    @Override
    public int invoke(LuaState L) {
        // NOTE: RequestPermissionsResultHandlers are NOT guaranteed to be synchronous. Here we have two different behaviors:
        // 1) If the permission is already granted, or we are on Android version < 6.0, we return immediately on the lua
        //    thread with no interruption
        // 2) If the permission needs the user to approve it (Android 6.0+), we wait and handle the request later; this must
        //    be dispatched to run on the lua thread, not the main or UI thread
        // Be sure to use a CoronaRuntimeTask if needed!
        MyFileIOFunctionRequestPermissionsResultHandler resultHandler = new MyFileIOFunctionRequestPermissionsResultHandler(L);
        resultHandler.handleRun();
        return 0;
    }
}

需要注意的是,`OnRequestPermissionsResultHandler`**不**保证是同步的。检查上面的代码,我们看到有两种不同的方法来调用 `run()`

  1. 我们已经获得了权限(可能是因为我们不在 Android 6.0+ 上,并且我们只是在 AndroidManifest.xml 中定义了权限)。在这种情况下,我们从 Lua 线程同步调用 run()

  2. 应用没有权限,因此我们请求权限。这将导致应用暂停,并显示一个 UI 提示用户接受或拒绝权限请求。此交互的结果将发送到主 UI 线程上的 OnRequestPermissionsResultHandler.onHandleRequestPermissionsResult()。应该使用 CoronaRuntimeTaskDispatcher 来确保我们在适当的时间与 Lua 交互(见下文)。

private void run() {
    // This function can be called from either the main thread, or the Lua thread; safely dispatch it
    // Note that we'll need to store the lua state we want to dispatch to elsewhere
    final CoronaRuntimeTaskDispatcher dispatcher = new CoronaRuntimeTaskDispatcher(fLuaState);
    dispatcher.send( new CoronaRuntimeTask() {
            @Override
            public void executeUsing(CoronaRuntime runtime) {
                LuaState L = runtime.getLuaState();

                // ...
            }
        } );