请求运行时权限

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

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

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

基本原则

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

  • 当用户开始与需要该权限的功能互动时,在上下文中请求权限。
  • 不要阻止用户。始终提供取消教育性 UI 流程的选项,例如解释请求权限原因的流程。
  • 如果用户拒绝或撤销了某个功能所需的权限,请优雅地降级您的应用,以便用户可以继续使用您的应用,可能通过禁用需要该权限的功能来实现。
  • 不要假设任何系统行为。例如,不要假设权限出现在同一个权限组中。权限组仅仅有助于系统在应用请求密切相关的权限时,最大限度地减少向用户显示的系统对话框数量。

请求权限的工作流

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

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

  1. 在应用的 manifest 文件中,声明您的应用可能需要请求的权限
  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,即可请求权限。用户会看到一个系统权限对话框,他们可以在其中选择是否向您的应用授予特定权限。

为此,请使用 AndroidX 库中包含的 RequestPermission 契约,您可以让系统为您管理权限请求代码。由于使用 RequestPermission 契约可简化您的逻辑,因此在可能的情况下,它是推荐的解决方案。但是,如果需要,您也可以在权限请求中自行管理请求代码,并将此请求代码包含在您的权限回调逻辑中。

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

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

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

以下步骤展示了如何使用 RequestPermission 契约。RequestMultiplePermissions 契约的过程也几乎相同。

  1. 在您的 activity 或 fragment 的初始化逻辑中,将 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.
    }
}

请求位置权限

当您请求位置权限时,请遵循与其他任何运行时权限相同的最佳实践。位置权限的一个重要区别是系统包含了多个与位置相关的权限。您请求哪些权限以及如何请求它们,取决于您的应用用例对位置的要求。

前台位置信息

如果您的应用包含一个只共享或接收一次或在限定时间内共享或接收位置信息的功能,那么该功能需要前台位置访问权限。一些示例如下:

  • 在导航应用中,一项功能让用户获取逐向导航。
  • 在消息应用中,一项功能让用户与另一个用户共享其当前位置。

如果您的应用的功能在以下任一情况下访问设备的当前位置,系统会认为您的应用正在使用前台位置信息:

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

    在 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)及更高版本上,您必须在应用的 manifest 中声明 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 所示。如果用户在对话框中选择此选项,您的应用将获得一个临时的一次性权限

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

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

权限撤销时应用进程终止

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

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

重置未使用的权限

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