Xamarin.Android 中的权限

概述

Android 应用程序在其自己的沙盒中运行,出于安全原因,它们无法访问设备上的某些系统资源或硬件。 用户必须先显式授予应用的权限,然后才能使用这些资源。 例如,未经用户明确许可,应用程序无法访问设备上的 GPS。 如果应用未经许可尝试访问受保护的资源,则 Android 将引发 Java.Lang.SecurityException

开发应用时,应用程序开发人员会在 AndroidManifest.xml 中声明权限。 Android 有两个不同的工作流,用于获取用户对以下权限的同意:

  • 对于面向 Android 5.1(API 级别 22)或更低版本的应用,安装应用时会请求权限。 如果用户未授予权限,则不会安装应用。 安装应用后,无法撤销权限,除非卸载应用。
  • 从 Android 6.0(API 级别 23 开始),用户获得了对权限的更多控制权:只要应用安装在设备上,他们就可以授予或撤销权限。 此屏幕截图显示了 Google 通讯录应用的权限设置。 它列出了各种权限,并允许用户启用或禁用权限:

Sample Permissions screen

Android 应用必须在运行时检查,才能查看它们是否有权访问受保护的资源。 如果应用没有权限,则必须使用 Android SDK 提供的新 API 发出请求,以便用户授予权限。 权限分为两个类别:

  • 普通权限 – 这些权限对用户的安全或隐私造成的安全风险很小。 Android 6.0 会在安装时自动授予普通权限。 有关普通权限的完整列表,请参阅 Android 文档。
  • 危险权限 – 与普通权限不同,危险权限是保护用户安全或隐私的权限。 用户必须显式授予这些权限。 例如,发送和接收短信的操作就需要危险权限。

重要

权限所属的类别可能会随时间而更改。 将来的 API 级别可能会将归类为“普通”权限的权限提升为危险权限。

危险权限进一步细分为权限组。 权限组将保留逻辑上相关的权限。 当用户向某个权限组的一个成员授予权限时,Android 会自动向该组的所有成员授予权限。 例如,STORAGE 权限组同时保留 WRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGE 权限。 如果用户向 READ_EXTERNAL_STORAGE 授予权限,则同时会自动授予 WRITE_EXTERNAL_STORAGE 权限。

在请求一项或多项权限之前,最佳做法是在请求权限之前提供应用需要权限的理由。 用户了解理由后,应用便可请求用户授予权限。 通过了解理由,用户可以在希望授予权限时做出明智的决定,并了解不授予权限的后果。

检查和请求权限的整个工作流称为“运行时权限”检查,可以总结为下图所示

Run-time permission check flow chart

Android 向后移植了一些新的 API,以获取旧版 Android 的权限。 这些向后移植的 API 将自动检查设备上的 Android 版本,因此无需每次都执行 API 级别检查。

本文档将讨论如何将权限添加到 Xamarin.Android 应用程序,以及面向 Android 6.0(API 级别 23)或更高版本的应用应如何执行运行时权限检查。

注意

硬件的权限可能会影响 Google Play 筛选应用的方式。 例如,如果应用需要相机的权限,则 Google Play 将不会在未安装相机的设备上在 Google Play 商店中显示该应用。

要求

强烈建议 Xamarin.Android 项目包括 Xamarin.Android.Support.Compat NuGet 包。 此包会将特定于权限的 API 向后移植给旧版 Android,从而提供一个通用接口,而无需不断检查应用正在其上运行的 Android 版本。

请求系统权限

使用 Android 权限的第一步是在 Android 清单文件中声明权限。 无论应用面向的 API 级别如何,都必须执行此操作。

面向 Android 6.0 或更高版本的应用不能这样假定,即由于用户在过去某个时刻授予了权限,因此该权限下次将有效。 面向 Android 6.0 的应用必须始终执行运行时权限检查。 面向 Android 5.1 或更低版本的应用无需执行运行时权限检查。

注意

应用程序应仅请求所需的权限。

在清单中声明权限

使用 uses-permission 元素将权限添加到 AndroidManifest.xml。 例如,如果应用程序要定位设备的位置,则需要精确和模糊位置权限。 将以下两个元素添加到清单:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

可以使用 Visual Studio 中内置的工具支持声明权限:

  1. 双击“解决方案资源管理器”中的“属性”,然后在属性窗口中选择“Android 清单”选项卡

    Required permissions in the Android Manifest tab

  2. 如果应用程序还没有 AndroidManifest.xml,请单击“找不到 AndroidManifest.xml。单击以添加一个”,如以下所示:

    No AndroidManifest.xml message

  3. 从“所需权限”列表中选择应用程序所需的任何权限并保存

    Example CAMERA permissions selected

Xamarin.Android 将在生成时自动向调试生成添加一些权限。 这将简化应用程序的调试。 具体而言,有两个值得注意的权限:INTERNETREAD_EXTERNAL_STORAGE。 这些自动设置的权限不会显示在“所需权限”列表中。 但是,发布版本仅使用“所需权限”列表中显式设置的权限

对于面向 Android 5.1(API 级别 22)或更低版本的应用,无需执行更多操作。 对于将在 Android 6.0(API 级别 23)或更高版本上运行的应用,则应继续学习下一部分,了解如何执行运行时权限检查。

Android 6.0 中的运行时权限检查

ContextCompat.CheckSelfPermission 方法(适用于 Android 支持库)用于检查是否授予了特定权限。 此方法将返回一个 Android.Content.PM.Permission 枚举,其中包含以下两个值之一:

  • Permission.Granted – 已授予指定的权限。
  • Permission.Denied – 尚未授予指定的权限。

此代码片段示例展示了如何在活动中检查相机权限:

if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.Camera) == (int)Permission.Granted) 
{
    // We have permission, go ahead and use the camera.
} 
else 
{
    // Camera permission is not granted. If necessary display rationale & request.
}

最佳做法是通知用户为什么应用程序需要权限,以便用户可以做出明智的决策来授予权限。 例如,拍摄照片并给它们添加地理标签的应用。 用户清楚相机权限是必需的,但可能不清楚为什么应用还需要设备的位置。 理由应显示一条消息,以帮助用户了解为什么需要位置权限和相机权限。

ActivityCompat.ShouldShowRequestPermissionRationale 方法用于确定是否应向用户显示理由。 如果应显示给定权限的理由,此方法将返回 true。 此屏幕截图显示了应用程序显示的 Snackbar 示例,该示例解释了应用需要知道设备位置的原因:

Rationale for location

如果用户授予权限,则应调用 ActivityCompat.RequestPermissions(Activity activity, string[] permissions, int requestCode) 方法。 此方法需要以下参数:

  • activity – 这是请求权限的活动,由 Android 告知结果。
  • permissions – 要请求的权限列表。
  • requestCode – 一个整数值,用于将权限请求的结果与 RequestPermissions 调用匹配。 此值应大于零。

此代码片段示例展示了所讨论的两种方法。 首先,进行检查以确定是否应显示权限理由。 如果要显示理由,则显示包含理由的 Snackbar。 如果用户在 Snackbar 中单击“确定”,则应用将请求权限。 如果用户不接受理由,则应用不应继续请求权限。 如果不显示理由,活动将请求权限:

if (ActivityCompat.ShouldShowRequestPermissionRationale(this, Manifest.Permission.AccessFineLocation)) 
{
    // Provide an additional rationale to the user if the permission was not granted
    // and the user would benefit from additional context for the use of the permission.
    // For example if the user has previously denied the permission.
    Log.Info(TAG, "Displaying camera permission rationale to provide additional context.");

    var requiredPermissions = new String[] { Manifest.Permission.AccessFineLocation };
    Snackbar.Make(layout, 
                   Resource.String.permission_location_rationale,
                   Snackbar.LengthIndefinite)
            .SetAction(Resource.String.ok, 
                       new Action<View>(delegate(View obj) {
                           ActivityCompat.RequestPermissions(this, requiredPermissions, REQUEST_LOCATION);
                       }    
            )
    ).Show();
}
else 
{
    ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.Camera }, REQUEST_LOCATION);
}

即使用户已授予权限,也可调用 RequestPermission。 后续调用不是必需的,但它们为用户提供了确认(或撤销)权限的机会。 调用 RequestPermission 时,控制权将移交给操作系统,这将显示用于接受权限的 UI:

Permssion Dialog

用户完成后,Android 将通过回调方法 OnRequestPermissionResult 将结果返回到活动。 此方法是接口 ActivityCompat.IOnRequestPermissionsResultCallback 的一部分,必须由活动实现。 此接口只有一个 OnRequestPermissionsResult 方法,Android 将调用该方法以通知活动用户的选择。 如果用户已授予权限,则应用可以继续使用受保护的资源。 下面是有关如何实现 OnRequestPermissionResult 的示例:

public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
    if (requestCode == REQUEST_LOCATION) 
    {
        // Received permission result for camera permission.
        Log.Info(TAG, "Received response for Location permission request.");

        // Check if the only required permission has been granted
        if ((grantResults.Length == 1) && (grantResults[0] == Permission.Granted)) {
            // Location permission has been granted, okay to retrieve the location of the device.
            Log.Info(TAG, "Location permission has now been granted.");
            Snackbar.Make(layout, Resource.String.permission_available_camera, Snackbar.LengthShort).Show();            
        } 
        else 
        {
            Log.Info(TAG, "Location permission was NOT granted.");
            Snackbar.Make(layout, Resource.String.permissions_not_granted, Snackbar.LengthShort).Show();
        }
    } 
    else 
    {
        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

总结

本指南讨论了如何在 Android 设备中添加和检查权限。 旧版 Android 应用(API 级别 < 23)和新版 Android 应用(API 级别 > 22)之间权限的工作原理差异。 它讨论了如何在 Android 6.0 中执行运行时权限检查。