请求运行时权限

每个 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 后,请请求权限。用户会看到一个系统权限对话框,他们可以在其中选择是否向您的应用授予特定权限。

为此,请使用包含在 AndroidX 库中的RequestPermission 合同,其中您允许系统为您管理权限请求代码。由于使用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>

后台位置

如果应用中的某个功能会持续地与其他用户共享位置或使用Geofencing 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 时,系统会确定何时安全地终止这些进程。通常情况下,系统会等待您的应用在后台运行较长时间而不是在前台运行。

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

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

如果您的应用的目标 API 级别为 Android 11(API 级别 30)或更高,并且几个月未被使用,则系统会通过自动重置用户已授予您的应用的敏感运行时权限来保护用户数据。在有关应用休眠的指南中了解更多信息。

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

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

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

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

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

adb shell install -g PATH_TO_APK_FILE

其他资源

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

要了解有关请求权限的更多信息,请查看权限示例

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