本指南介绍如何在使用 Solar2D Native 构建的应用/插件中添加对 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 为目标。只需在项目中的几个位置将数字更改为 `23`(API 级别 23 对应于 Android 6.0)。
`AndroidManifest.xml` — 将 `android:targetSdkVersion="xx"` 更改为 `android:targetSdkVersion="23"`。这将允许您在本地项目中测试您的更改。
`project.properties` — 将target=Google Inc.:Google APIs:xx
target=Google Inc.:Google APIs:23
`project.plugin.properties` — 对于
确定应用/插件需要哪些权限后,声明将按如下方式处理
如果您正在开发原生插件,则必须在 `metadata.lua` 中声明权限。这可确保自动为 Solar2D 用户导入权限。
例如,如果需要 `android.permission.SEND_SMS` 权限,则 `usesPermissions` 表必须像这样包含它
local metadata = { plugin = { manifest = { usesPermissions = { "android.permission.SEND_SMS", }, }, }, } return metadata
此外,对于 Solar2D Native 开发人员,您应该告知他们必须将这些权限显式添加到他们的 `AndroidManifest.xml` 文件中
Solar2D Native 用户必须将权限显式添加到 `AndroidManifest.xml` 中。
例如,如果需要 `android.permission.SEND_SMS` 权限,则 `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>
在 Corona 中支持运行时权限与在本机 Android 应用中支持它们非常相似。本指南是对 Google 的运行时请求权限指南的补充,并与其并行进行。
在 Corona 中,我们为开发人员提供了一个接口来处理他们的权限请求,而无需担心 API 级别或权限泄漏。
您需要的大部分功能都来自我们的 `com.ansca.corona.permissions.PermissionsServices` 类。这提供了几个 API,旨在将 Google 的权限处理与
这可以通过 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` 枚举。您的应用/插件必须能够处理这些可能的权限状态,而不会崩溃或进入不良状态。
用户已授予此权限。对应于 Google 的 `android.content.pm.PackageManager.PERMISSION_GRANTED`。当权限处于此状态时,您可以执行任何需要该权限的操作。
此权限未在应用的 `AndroidManifest.xml` 文件中列出。如果权限处于此状态,则可能适用以下情况之一
您处理 `PermissionState.MISSING` 的上下文取决于特定权限对于核心功能是强制性的,还是仅仅与
// 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)!");
此权限已被明确拒绝,或尚未请求。对应于 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()`
我们已经获得了权限(可能是因为我们不在 Android 6.0+ 上,并且我们只是在 AndroidManifest.xml
中定义了权限)。在这种情况下,我们从 Lua 线程同步调用 run()
。
应用没有权限,因此我们请求权限。这将导致应用暂停,并显示一个 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(); // ... } } );