请求运行时权限

每个 Android 应用都在有限访问的沙盒中运行。如果您的应用需要使用其自身沙盒之外的资源或信息,您可以 声明运行时权限 并设置提供此访问权限的权限请求。这些步骤是 使用权限的工作流程 的一部分。

如果您声明了任何 危险权限,并且您的应用安装在运行 Android 6.0(API 级别 23)或更高版本的设备上,您必须按照本指南中的步骤在运行时请求危险权限。

如果您没有声明任何危险权限,或者您的应用安装在运行 Android 5.1(API 级别 22)或更低版本的设备上,则权限将自动授予,您无需完成此页面上的任何其他步骤。

基本原则

在运行时请求权限的基本原则如下

  • 在用户开始与需要权限的功能进行交互时,在上下文中请求权限。
  • 不要阻止用户。始终提供取消教育性 UI 流程的选项,例如解释请求权限的理由的流程。
  • 如果用户拒绝或撤销功能所需的权限,请优雅地降级您的应用,以便用户可以继续使用您的应用,可能可以通过禁用需要该权限的功能。
  • 不要假设任何系统行为。例如,不要假设权限显示在同一个权限组中。权限组只是帮助系统在应用请求密切相关的权限时,尽量减少向用户展示的系统对话框数量。

请求权限的工作流程

在您在应用中声明和请求运行时权限之前,评估您的应用是否需要这样做。您可以满足应用中的许多用例,例如拍照、暂停媒体播放和显示相关广告,而无需声明任何权限。

如果您得出结论,您的应用需要声明和请求运行时权限,请完成以下步骤

  1. 在应用的清单文件中,声明应用可能需要请求的权限
  2. 设计应用的 UX,以便应用中的特定操作与特定的运行时权限相关联。让用户知道哪些操作可能需要他们授予您的应用访问私人用户数据的权限。
  3. 等待用户 在您的应用中调用需要访问特定私人用户数据的任务或操作。此时,您的应用可以请求访问该数据所需的运行时权限。
  4. 检查用户是否已授予您的应用所需的运行时权限。如果已授予,您的应用可以访问私人用户数据。否则,继续下一步。

    每次执行需要该权限的操作时,您都必须检查是否拥有该权限。

  5. 检查您的应用是否应该向用户显示理由,解释为什么您的应用需要用户授予特定的运行时权限。如果系统确定您的应用不应该显示理由,则直接继续下一步,无需显示 UI 元素。

    但是,如果系统确定您的应用应该显示理由,则在 UI 元素中向用户显示该理由。在此理由中,清楚地解释您的应用尝试访问哪些数据以及如果用户授予运行时权限,应用可以为用户提供哪些好处。在用户确认理由后,继续下一步。

  6. 请求您的应用访问私人用户数据所需的运行时权限。系统将显示运行时权限提示,例如 权限概述页面 上所示的提示。

  7. 检查用户的响应,即他们选择授予还是拒绝运行时权限。

  8. 如果用户向您的应用授予了权限,您可以访问私人用户数据。如果用户拒绝了权限,则 优雅地降低您的应用体验,以便它在没有受该权限保护的信息的情况下向用户提供功能。

图 1 说明了与该过程相关的流程和决策集。

图 1. 显示在 Android 上声明和请求运行时权限的工作流程的图表。

确定您的应用是否已获得授权

要检查用户是否已向您的应用授予特定权限,请将该权限传递给 ContextCompat.checkSelfPermission() 方法。根据您的应用是否具有该权限,该方法将返回 PERMISSION_GRANTEDPERMISSION_DENIED 之一。

解释为什么您的应用需要该权限

当您调用 requestPermissions() 时,系统显示的权限对话框会说明您的应用想要哪些权限,但不会说明原因。在某些情况下,用户可能会发现这一点令人费解。最好在调用 requestPermissions() 之前向用户解释您的应用为何需要这些权限。

研究表明,如果用户知道应用为何需要权限(例如,权限是用于支持应用的核心功能还是用于广告),那么他们会对权限请求感到更加舒适。因此,如果您仅使用权限组下部分 API 调用的功能,那么明确列出您使用哪些权限以及原因将有所帮助。例如,如果您仅使用粗略位置,请在您的应用描述中或关于您的应用的帮助文章中告知用户。

在某些情况下,实时让用户了解敏感数据访问也很有用。例如,如果您正在访问相机或麦克风,那么最好通过在您的应用中的某个位置使用通知图标,或者在通知栏中(如果应用在后台运行)来让用户知道,这样就不会让人觉得您在秘密收集数据。

最终,如果您需要请求权限才能使应用中的某些功能正常工作,但用户不清楚原因,请找到一种方法让用户了解您为何需要最敏感的权限。

如果 ContextCompat.checkSelfPermission() 方法返回 PERMISSION_DENIED,请调用 shouldShowRequestPermissionRationale()。如果此方法返回 true,请向用户显示一个教育性 UI。在此 UI 中,说明用户想要启用的功能为何需要特定权限。

此外,如果您的应用请求与位置、麦克风或相机相关的权限,请考虑 解释您的应用为何需要访问 此信息。

请求权限

在用户查看教育性 UI 或 shouldShowRequestPermissionRationale() 的返回值指示您不需要显示教育性 UI 后,请求该权限。用户会看到一个系统权限对话框,他们可以在其中选择是否向您的应用授予特定权限。

为此,请使用 RequestPermission 合同(包含在 AndroidX 库中),您可以在其中 允许系统为您管理权限请求代码。由于使用 RequestPermission 合同可以简化您的逻辑,因此在可能的情况下,它是推荐的解决方案。但是,如果需要,您也可以 自己管理请求代码 作为权限请求的一部分,并将此请求代码包含在您的权限回调逻辑中。

允许系统管理权限请求代码

要允许系统管理与权限请求关联的请求代码,请在模块的 build.gradle 文件中添加对以下库的依赖项

然后,您可以使用以下类之一

以下步骤展示了如何使用 RequestPermission 合同。对于 RequestMultiplePermissions 合同,该过程几乎相同。

  1. 在活动或片段的初始化逻辑中,将 ActivityResultCallback 的实现传递给对 registerForActivityResult() 的调用。 ActivityResultCallback 定义了您的应用如何处理用户对权限请求的响应。

    保留对 registerForActivityResult() 的返回值的引用,该返回值的类型为 ActivityResultLauncher

  2. 如果需要显示系统权限对话框,请在您在上一步中保存的 ActivityResultLauncher 实例上调用 launch() 方法。

    在调用 launch() 后,将出现系统权限对话框。当用户做出选择时,系统将异步调用您在前面步骤中定义的 ActivityResultCallback 的实现。

    注意:您的应用无法自定义调用 launch() 时出现的对话框。要向用户提供更多信息或上下文,请更改您的应用的 UI,使其更容易让用户了解您的应用中的功能为何需要特定权限。例如,您可以更改启用该功能的按钮中的文本。

    此外,系统权限对话框中的文本会引用与您请求的权限关联的 权限组。此权限分组是为系统易用性而设计的,您的应用不应依赖于权限在特定权限组内或组外。

以下代码片段展示了如何处理权限响应

Kotlin

// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of
// ActivityResultLauncher. You can use either a val, as shown in this snippet,
// or a lateinit var in your onAttach() or onCreate() method.
val requestPermissionLauncher =
    registerForActivityResult(RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Permission is granted. Continue the action or workflow in your
            // app.
        } else {
            // Explain to the user that the feature is unavailable because the
            // feature requires a permission that the user has denied. At the
            // same time, respect the user's decision. Don't link to system
            // settings in an effort to convince the user to change their
            // decision.
        }
    }

Java

// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of
// ActivityResultLauncher, as an instance variable.
private ActivityResultLauncher<String> requestPermissionLauncher =
    registerForActivityResult(new RequestPermission(), isGranted -> {
        if (isGranted) {
            // Permission is granted. Continue the action or workflow in your
            // app.
        } else {
            // Explain to the user that the feature is unavailable because the
            // feature requires a permission that the user has denied. At the
            // same time, respect the user's decision. Don't link to system
            // settings in an effort to convince the user to change their
            // decision.
        }
    });

此代码片段演示了在必要时检查权限和从用户那里请求权限的推荐流程

Kotlin

when {
    ContextCompat.checkSelfPermission(
            CONTEXT,
            Manifest.permission.REQUESTED_PERMISSION
            ) == PackageManager.PERMISSION_GRANTED -> {
        // You can use the API that requires the permission.
    }
    ActivityCompat.shouldShowRequestPermissionRationale(
            this, Manifest.permission.REQUESTED_PERMISSION) -> {
        // In an educational UI, explain to the user why your app requires this
        // permission for a specific feature to behave as expected, and what
        // features are disabled if it's declined. In this UI, include a
        // "cancel" or "no thanks" button that lets the user continue
        // using your app without granting the permission.
        showInContextUI(...)
    }
    else -> {
        // You can directly ask for the permission.
        // The registered ActivityResultCallback gets the result of this request.
        requestPermissionLauncher.launch(
                Manifest.permission.REQUESTED_PERMISSION)
    }
}

Java

if (ContextCompat.checkSelfPermission(
        CONTEXT, Manifest.permission.REQUESTED_PERMISSION) ==
        PackageManager.PERMISSION_GRANTED) {
    // You can use the API that requires the permission.
    performAction(...);
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
        this, Manifest.permission.REQUESTED_PERMISSION)) {
    // In an educational UI, explain to the user why your app requires this
    // permission for a specific feature to behave as expected, and what
    // features are disabled if it's declined. In this UI, include a
    // "cancel" or "no thanks" button that lets the user continue
    // using your app without granting the permission.
    showInContextUI(...);
} else {
    // You can directly ask for the permission.
    // The registered ActivityResultCallback gets the result of this request.
    requestPermissionLauncher.launch(
            Manifest.permission.REQUESTED_PERMISSION);
}

自己管理权限请求代码

作为 允许系统管理权限请求代码 的替代方法,您可以自己管理权限请求代码。为此,请将请求代码包含在对 requestPermissions() 的调用中。

以下代码片段演示了如何使用请求代码请求权限

Kotlin

when {
    ContextCompat.checkSelfPermission(
            CONTEXT,
            Manifest.permission.REQUESTED_PERMISSION
            ) == PackageManager.PERMISSION_GRANTED -> {
        // You can use the API that requires the permission.
        performAction(...)
    }
    ActivityCompat.shouldShowRequestPermissionRationale(
            this, Manifest.permission.REQUESTED_PERMISSION) -> {
        // In an educational UI, explain to the user why your app requires this
        // permission for a specific feature to behave as expected, and what
        // features are disabled if it's declined. In this UI, include a
        // "cancel" or "no thanks" button that lets the user continue
        // using your app without granting the permission.
        showInContextUI(...)
    }
    else -> {
        // You can directly ask for the permission.
        requestPermissions(CONTEXT,
                arrayOf(Manifest.permission.REQUESTED_PERMISSION),
                REQUEST_CODE)
    }
}

Java

if (ContextCompat.checkSelfPermission(
        CONTEXT, Manifest.permission.REQUESTED_PERMISSION) ==
        PackageManager.PERMISSION_GRANTED) {
    // You can use the API that requires the permission.
    performAction(...);
} else if (ActivityCompat.shouldShowRequestPermissionRationale(
        this, Manifest.permission.REQUESTED_PERMISSION)) {
    // In an educational UI, explain to the user why your app requires this
    // permission for a specific feature to behave as expected, and what
    // features are disabled if it's declined. In this UI, include a
    // "cancel" or "no thanks" button that lets the user continue
    // using your app without granting the permission.
    showInContextUI(...);
} else {
    // You can directly ask for the permission.
    requestPermissions(CONTEXT,
            new String[] { Manifest.permission.REQUESTED_PERMISSION },
            REQUEST_CODE);
}

在用户对系统权限对话框做出响应后,系统将调用您的应用对 onRequestPermissionsResult() 的实现。系统会传入用户对权限对话框的响应,以及您定义的请求代码,如以下代码片段所示

Kotlin

override fun onRequestPermissionsResult(requestCode: Int,
        permissions: Array<String>, grantResults: IntArray) {
    when (requestCode) {
        PERMISSION_REQUEST_CODE -> {
            // If request is cancelled, the result arrays are empty.
            if ((grantResults.isNotEmpty() &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                // Permission is granted. Continue the action or workflow
                // in your app.
            } else {
                // Explain to the user that the feature is unavailable because
                // the feature requires a permission that the user has denied.
                // At the same time, respect the user's decision. Don't link to
                // system settings in an effort to convince the user to change
                // their decision.
            }
            return
        }

        // Add other 'when' lines to check for other
        // permissions this app might request.
        else -> {
            // Ignore all other requests.
        }
    }
}

Java

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
        int[] grantResults) {
    switch (requestCode) {
        case PERMISSION_REQUEST_CODE:
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 &&
                    grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted. Continue the action or workflow
                // in your app.
            }  else {
                // Explain to the user that the feature is unavailable because
                // the feature requires a permission that the user has denied.
                // At the same time, respect the user's decision. Don't link to
                // system settings in an effort to convince the user to change
                // their decision.
            }
            return;
        }
        // Other 'case' lines to check for other
        // permissions this app might request.
    }
}

请求位置权限

当您请求位置权限时,请遵循与任何其他 运行时权限 相同的最佳实践。在位置权限方面,一个重要的区别在于系统包含多个与位置相关的权限。您请求的权限以及您请求权限的方式取决于应用用例的位置要求。

前台位置

如果您的应用包含一项仅共享或接收一次位置信息或在限定时间内共享或接收位置信息的特性,那么该特性需要前台位置访问权限。以下是一些示例

  • 在导航应用中,一项特性可以让用户获得逐向行驶路线。
  • 在消息应用中,一项特性可以让用户与其他用户共享其当前位置。

如果应用的特性在以下情况之一中访问设备的当前位置,系统会认为您的应用正在使用前台位置

  • 属于您的应用的活动可见。
  • 您的应用正在运行前台服务。当前台服务运行时,系统会通过显示持久性通知来提高用户意识。当应用被置于后台时(例如,当用户按下其设备上的“主页”按钮或关闭其设备的显示屏时),您的应用会保留访问权限。

    在 Android 10(API 级别 29)及更高版本中,您必须声明 前台服务类型location,如以下代码片段所示。在早期版本的 Android 上,建议您声明此前台服务类型。

    <!-- Recommended for Android 9 (API level 28) and lower. -->
    <!-- Required for Android 10 (API level 29) and higher. -->
    <service
        android:name="MyNavigationService"
        android:foregroundServiceType="location" ... >
        <!-- Any inner elements go here. -->
    </service>
    

当您的应用请求ACCESS_COARSE_LOCATION权限或ACCESS_FINE_LOCATION权限时,您需要声明对前台位置的访问权限,如下所示:

<manifest ... >
  <!-- Include this permission any time your app needs location information. -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

  <!-- Include only if your app benefits from precise location access. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

后台位置

如果应用中的某个功能需要持续地与其他用户共享位置或使用地理围栏 API,则该应用需要访问后台位置。以下是一些示例:

  • 在一个家庭位置共享应用中,某个功能允许用户持续地与家人共享位置。
  • 在一个物联网应用中,某个功能允许用户配置家用设备,使其在用户离开家时关闭,并在用户返回家时重新打开。

如果您的应用在前台位置部分描述的以外的其他情况下访问设备的当前位置,系统会认为您的应用正在使用后台位置。后台位置的精度与前台位置精度相同,这取决于您的应用声明的位置权限。

在 Android 10(API 级别 29)及更高版本中,您必须在应用的清单文件中声明ACCESS_BACKGROUND_LOCATION权限,以便在运行时请求访问后台位置。在早期版本的 Android 中,当您的应用获得前台位置访问权限时,它会自动获得后台位置访问权限。

<manifest ... >
  <!-- Required only when requesting background location access on
       Android 10 (API level 29) and higher. -->
  <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>

处理权限拒绝

如果用户拒绝了权限请求,您的应用应该帮助用户了解拒绝权限的后果。特别是,您的应用应该让用户知道由于缺少权限而无法使用的功能。在这样做时,请牢记以下最佳实践:

  • 引导用户的注意力。突出显示应用 UI 中由于缺少必要权限而功能受限的特定部分。以下是一些可以做的事情的示例:

    • 在功能结果或数据应该出现的位置显示一条消息。
    • 显示一个带有错误图标和颜色的不同按钮。
  • 具体说明。不要显示通用的消息。相反,要明确说明哪些功能由于缺少必要的权限而不可用。

  • 不要阻止用户界面。换句话说,不要显示全屏警告消息,阻止用户继续使用您的应用。

同时,您的应用应该尊重用户拒绝权限的决定。从 Android 11(API 级别 30)开始,如果用户在应用安装在设备上的整个生命周期中多次点击特定权限的拒绝按钮,那么如果您的应用再次请求该权限,用户将不会看到系统权限对话框。用户的操作意味着“不再询问”。在之前的版本中,用户每次请求权限时都会看到系统权限对话框,除非他们之前选择了“不再询问”复选框或选项。

如果用户多次拒绝权限请求,则这将被视为永久拒绝。非常重要的是,只有在用户需要访问特定功能时才提示用户进行权限授权,否则您可能会意外地失去重新请求权限的能力。

在某些情况下,权限可能会在没有用户采取任何操作的情况下被自动拒绝。(权限也可能被自动授予。)重要的是不要对自动行为做出任何假设。每次您的应用需要访问需要权限的功能时,请检查您的应用是否仍然被授予该权限。

为了在请求应用权限时提供最佳的用户体验,还可以参考应用权限最佳实践

在测试和调试时检查拒绝状态

为了确定应用是否被永久拒绝了权限(用于调试和测试目的),请使用以下命令:

adb shell dumpsys package PACKAGE_NAME

其中 PACKAGE_NAME 是要检查的包的名称。

命令的输出包含如下所示的部分:

...
runtime permissions:
  android.permission.POST_NOTIFICATIONS: granted=false, flags=[ USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED]
  android.permission.ACCESS_FINE_LOCATION: granted=false, flags=[ USER_SET|USER_FIXED|USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED]
  android.permission.BLUETOOTH_CONNECT: granted=false, flags=[ USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHEN_DENIED]
...

用户曾拒绝一次的权限会用 USER_SET 标记。用户曾永久拒绝(选择两次拒绝)的权限会用 USER_FIXED 标记。

为了确保测试人员在测试期间看到请求对话框,请在完成应用调试后重置这些标记。为此,请使用以下命令:

adb shell pm clear-permission-flags PACKAGE_NAME PERMISSION_NAME user-set user-fixed

PERMISSION_NAME 是要重置的权限的名称。

要查看 Android 应用权限的完整列表,请访问权限 API 参考页面

一次性权限

The option called 'Only this time' is the second of three buttons in
    the dialog.
图 2. 应用请求一次性权限时出现的系统对话框。

从 Android 11(API 级别 30)开始,每当您的应用请求与位置、麦克风或相机相关的权限时,面向用户的权限对话框中都会包含一个名为仅此一次的选项,如图 2 所示。如果用户在对话框中选择此选项,您的应用将获得一个临时的一次性权限

然后,您的应用可以在一段时间内访问相关数据,该时间取决于应用的行为和用户的操作。

  • 当您的应用活动可见时,您的应用可以访问数据。
  • 如果用户将您的应用发送到后台,您的应用可以继续在短时间内访问数据。
  • 如果您在活动可见时启动前台服务,然后用户将您的应用移至后台,您的应用可以继续访问数据,直到前台服务停止。

权限被撤销后,应用进程会终止

如果用户撤销了一次性权限,例如在系统设置中,您的应用将无法访问数据,无论您是否启动了前台服务。与任何权限一样,如果用户撤销了应用的一次性权限,应用的进程将终止。

当用户下次打开您的应用,并且应用中的某个功能请求访问位置、麦克风或相机时,系统会再次提示用户进行权限授权。

重置未使用的权限

Android 提供了几种方法将未使用的运行时权限重置为默认的拒绝状态:

删除应用访问权限

在 Android 13(API 级别 33)及更高版本中,您可以删除应用对不再需要的运行时权限的访问权限。当您更新应用时,请执行此步骤,这样用户更有可能理解您的应用为什么继续请求特定权限。这些信息有助于用户对您的应用建立信任。

要删除对运行时权限的访问权限,请将该权限的名称传递给revokeSelfPermissionOnKill()。要同时删除对一组运行时权限的访问权限,请将权限名称的集合传递给revokeSelfPermissionsOnKill()。权限删除过程是异步发生的,并且会杀死与应用的 UID 关联的所有进程。

为了让系统删除应用对权限的访问权限,所有与应用绑定的进程都必须被杀死。当您调用 API 时,系统会确定何时可以安全地杀死这些进程。通常情况下,系统会在您的应用在后台运行而不是在前台运行一段时间后才会这样做。

为了通知用户您的应用不再需要访问特定运行时权限,请在用户下次启动应用时显示一个对话框。该对话框可以包含权限列表。

自动重置未使用应用的权限

如果您的应用面向 Android 11(API 级别 30)或更高版本,并且在几个月内未被使用,系统会通过自动重置用户之前授予应用的敏感运行时权限来保护用户数据。有关详细信息,请参阅关于应用休眠的指南。

如有必要,请求成为默认处理程序

某些应用依赖于对与通话记录和短信相关的敏感用户信息的访问权限。如果您要请求与通话记录和短信相关的特定权限,并将您的应用发布到 Play 商店,那么您必须在请求这些运行时权限之前提示用户将您的应用设置为核心系统功能的默认处理程序

有关默认处理程序的更多信息,包括关于向用户显示默认处理程序提示的指南,请参阅关于仅在默认处理程序中使用的权限的指南

出于测试目的,授予所有运行时权限

为了在您在模拟器或测试设备上安装应用时自动授予所有运行时权限,请在以下代码片段中演示的 adb shell install 命令中使用 -g 选项:

adb shell install -g PATH_TO_APK_FILE

其他资源

有关权限的更多信息,请阅读以下文章:

要详细了解如何请求权限,请查看权限示例

您还可以完成此演示隐私最佳实践的 codelab