请求运行时权限

每个 Android 应用都在访问权受限的沙盒中运行。如果您的应用需要使用自身沙盒以外的资源或信息,您可以声明运行时权限,并设置一项权限请求来获取所需的访问权限。以下步骤是权限使用工作流的一部分。

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

如果您没有声明任何危险权限,或者您的应用安装在搭载 Android 5.1(API 级别 22)或更低版本的设备上,则系统会自动授予相应的权限,您无需完成本页剩下的任何步骤。

基本原则

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

  • 当用户开始与需要相关权限的功能互动时,在具体使用情境下请求权限。
  • 不要阻止用户使用应用。始终提供选项供用户取消指导界面流程,例如说明请求权限理由的流程。
  • 如果用户拒绝或撤消某项功能所需的权限,请适当降级您的应用以便让用户可以继续使用您的应用(可能通过停用需要权限的功能来实现)。
  • 不要对系统行为做任何假设。例如,假设某些权限会出现在同一个权限组中。权限组的作用只是在应用请求密切相关的多个权限时,帮助系统尽可能减少向用户显示的系统对话框数量。

请求权限的工作流

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

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

  1. 在应用的清单文件中,声明应用可能需要请求的权限
  2. 设计应用的用户体验,使应用中的特定操作与特定运行时权限相关联。告知用户哪些操作可能会要求他们向您的应用授予访问其私人数据的权限。
  3. 等待用户调用应用中需要访问特定用户私人数据的任务或操作。届时,您的应用可以请求访问相应数据所需的运行时权限。
  4. 检查用户是否已授予应用所需的运行时权限。如果已授权,那么您的应用可以访问用户私人数据。如果没有,请继续执行下一步。

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

  5. 检查您的应用是否应向用户显示理由,说明您的应用需要用户授予特定运行时权限的原因。如果系统确定您的应用不应显示理由,请继续直接执行下一步,无需显示界面元素。

    不过,如果系统确定您的应用应该显示一个理由,请在界面元素中向用户显示理由,明确说明您的应用试图访问哪些数据,以及应用获得运行时权限后可为用户提供哪些好处。用户确认理由后,请继续执行下一步。

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

  7. 检查用户的响应,他们可能会选择同意或拒绝授予运行时权限。

  8. 如果用户向您的应用授予相应权限,您就可以访问用户私人数据。如果用户拒绝授予该权限,请适当降低应用体验,使应用在未获得受该权限保护的信息时也能向用户提供功能。

图 1 说明了与此过程相关的工作流和决策组:

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

确定应用是否已获得权限

如需检查用户是否已向您的应用授予特定权限,请将该权限传入 ContextCompat.checkSelfPermission() 方法。根据您的应用是否具有相应权限,此方法会返回 PERMISSION_GRANTEDPERMISSION_DENIED

说明您的应用为何需要获取权限

系统在您调用 requestPermissions() 时显示的权限对话框将说明应用需要哪些权限,但不会解释为何需要这些权限。在某些情况下,用户可能会感到困惑。因此,最好在调用 requestPermissions() 之前向用户解释应用需要相应权限的原因。

研究表明,如果用户知道应用需要权限的原因(例如需要权限来支持应用的核心功能或投放广告),他们会更容易接受权限请求。因此,如果您仅使用归入权限组的一小部分 API 调用,明确列出您使用哪些权限以及使用原因会非常有用。例如,如果您仅使用粗略位置信息,请在应用说明或应用的帮助文章中告知用户。

在特定条件下,让用户实时了解应用在访问敏感数据也是很有帮助的。例如,如果您要使用摄像头或麦克风,最好在应用中的某个位置或在通知栏中(如果应用正在后台运行)使用通知图标通知用户,以免看上去像是在偷偷收集数据。

最后,如果您需要请求权限以便在应用中运行某项功能,但用户不清楚原因,则需要找到一种方法让用户知道您为什么需要最敏感的权限。

如果 ContextCompat.checkSelfPermission() 方法返回 PERMISSION_DENIED,请调用 shouldShowRequestPermissionRationale()。如果此方法返回 true,请向用户显示指导界面,在此界面中说明用户希望启用的功能为何需要特定权限。

此外,如果您的应用请求与位置信息、麦克风或相机相关的权限,请考虑说明该应用需要访问这些信息的原因

请求权限

用户查看指导界面后或者 shouldShowRequestPermissionRationale() 的返回值表明您不需要显示指导界面后,您可以请求权限。用户会看到系统权限对话框,并可在其中选择是否向您的应用授予特定权限。

为此,请使用 AndroidX 库中包含的 RequestPermission 协定类,您可在其中允许系统替您管理权限请求代码。由于使用 RequestPermission 协定类可简化逻辑,因此建议您尽量使用这种解决方案。不过,如果需要,您也可以在权限请求过程中自行管理请求代码,并将该请求代码添加到权限回调逻辑中。

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

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

然后,您可以使用以下某个类:

以下步骤显示了如何使用 RequestPermission 协定类。使用 RequestMultiplePermissions 协定类的流程基本与此相同。

  1. 在 activity 或 fragment 的初始化逻辑中,将 ActivityResultCallback 的实现传入对 registerForActivityResult() 的调用。ActivityResultCallback 定义应用如何处理用户对权限请求的响应。

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

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

    调用 launch() 之后,系统会显示系统权限对话框。当用户做出选择后,系统会异步调用您在上一步中定义的 ActivityResultCallback 实现。

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

    此外,系统权限对话框中的文本会提及与您请求的权限关联的权限组。此权限分组是为了让系统易于使用,您的应用不应依赖于特定权限组之内或之外的权限。

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

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>

后台位置信息

如果应用中的某项功能会不断与其他用户分享位置信息或使用 Geofencing API,则该应用需要后台位置信息访问权限。以下是此类情况的几个示例:

  • 在家庭位置信息分享应用中,某项功能可让用户与家庭成员持续分享位置信息。
  • 在 IoT 应用中,某项功能可让用户配置自己的家居设备,使其在用户离家时关机并在用户回家时重新开机。

除了前台位置信息部分所述的情况之外,如果应用在任何其他情况下访问设备的当前位置信息,系统就会认为应用需要使用后台位置信息。后台位置信息精确度与前台位置信息精确度相同,具体取决于应用声明的位置信息权限。

在 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>

处理权限请求遭拒情况

如果用户拒绝了权限请求,应用应该帮助用户了解拒绝授予权限的影响。具体而言,应用应该让用户知道因缺少权限而无法使用哪些功能。在处理这种情况时,请牢记以下最佳做法:

  • 引导用户的注意力。在应用界面中突出显示因为应用没有获得必要的权限而受限的功能所在的具体部分。以下示例说明了您可以采取的做法:

    • 在原本用于显示该功能的结果或数据的位置显示一条消息。
    • 显示一个包含错误图标并带有相应错误颜色的不同按钮。
  • 内容要具体。显示的消息不要空泛,而要指出因为应用没有获得必要的权限而无法使用的具体功能。

  • 不要阻止界面显示。换言之,不要显示全屏警告消息,让用户根本无法继续使用您的应用。

与此同时,您的应用还应尊重用户拒绝授予权限的决定。从 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 参考文档页面

单次授权

对话框里有三个按钮,名为“仅限这一次”的选项是其中的第二个按钮。
图 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