1. 简介
您将学到什么
- 为什么隐私对用户来说越来越重要。
- 过去几个版本中 Android 隐私的最佳实践。
- 如何将隐私最佳实践集成到现有应用中,使其更注重隐私保护。
您将构建什么
在此 codelab 中,您将从一个示例应用开始,该应用允许用户保存他们的照片回忆。
它将以以下屏幕启动
- 权限屏幕 - 在进入主屏幕之前,要求用户授予所有权限的屏幕。
- 主屏幕 - 显示用户所有现有照片日志的屏幕,并允许他们添加新的照片日志。
- 添加日志屏幕 - 允许用户创建新照片日志的屏幕。在这里,用户可以浏览其图库中的现有照片,使用相机拍摄新照片,并将当前城市添加到照片日志中。
- 相机屏幕 - 允许用户拍照并保存到照片日志的屏幕。
该应用正在运行,但它有许多隐私缺陷,我们将一起改进!
随着您在 codelab 中的进展,您将:
- ...了解为什么隐私对您的应用很重要
- ...了解 Android 的隐私功能和关键最佳实践。
- ...通过以下操作,了解如何在现有应用中实现这些最佳实践:
- 根据上下文请求权限
- 减少应用的位置访问
- 使用照片选择器及其他存储改进
- 使用数据访问审计 API
完成后,您将拥有一个:
- ...实现上述隐私最佳实践的应用。
- ...注重隐私保护,并通过谨慎处理用户私密数据来保护用户,从而增强用户体验的应用。
您需要什么
加分项
- 熟悉以下架构组件:ViewModel 和应用架构指南中建议的架构。
- 熟悉 Jetpack Compose。有关 Compose 的介绍,请查看 Jetpack Compose 教程。
2. 为什么隐私很重要?
研究表明,人们对自己的隐私持谨慎态度。皮尤研究中心进行的一项调查发现,84% 的美国人认为他们对公司和应用收集的数据几乎没有控制权。他们主要感到沮丧的是,除了直接使用之外,他们不知道自己的数据发生了什么。例如,他们担心数据被用于其他目的,例如为定向广告创建配置文件,甚至出售给其他方。而且一旦数据泄露,似乎就没有办法将其删除。
这种对隐私的担忧已经显著影响了人们对使用哪些服务或应用的决定。事实上,同一项皮尤研究中心的研究发现,超过一半(52%)的美国成年人因为隐私问题(例如担心收集了多少关于他们的数据)而决定不使用某个产品或服务。
因此,增强和展示您的应用的隐私对于改善用户体验至关重要,研究表明这也很可能帮助您扩大用户群。
我们将在本 codelab 中介绍的许多功能和最佳实践都直接与减少您的应用访问的数据量或增强用户对其私人数据的控制感有关。这两项增强都直接解决了我们之前在研究中看到的用户的担忧。
3. 设置您的环境
为了让您尽快开始,我们为您准备了一个入门项目。在此步骤中,您将下载整个 codelab 的代码(包括入门项目),然后在模拟器或设备上运行入门应用。
如果您已安装 Git,只需运行以下命令。要检查 Git 是否已安装,请在终端或命令行中键入 git –version
并验证其是否正确执行。
git clone https://github.com/android/privacy-codelab
如果您没有 Git,可以单击此链接下载本 codelab 的所有代码。
设置 codelab
- 在 Android Studio 中打开
PhotoLog_Start
目录中的项目。 - 在运行 Android 12 (S) 或更高版本的设备或模拟器上运行
PhotoLog_Start
运行配置。
您应该会看到一个屏幕,要求您授予权限才能运行该应用!这意味着您已成功设置环境。
4. 最佳实践:根据上下文请求权限
许多人都知道运行时权限对于解锁许多对提供出色用户体验很重要的关键功能至关重要,但您是否知道应用何时以及如何请求权限也会对用户体验产生显著影响?
让我们来看看 PhotoLog_Start
应用如何请求权限,以向您展示为什么它没有最佳的权限模型
- 启动后,用户会立即收到权限提示,要求他们授予多个权限。这可能会让用户感到困惑,最坏的情况下可能导致他们对我们的应用失去信任或卸载它!
- 在授予所有权限之前,应用不允许用户继续。用户在启动时可能不够信任我们的应用,无法授予对所有这些敏感信息的访问权限。
正如您可能猜到的那样,上面的列表代表了我们将共同进行的改进,以改善应用的权限请求过程!让我们深入了解它。
我们可以看到 Android 的最佳实践建议指出,我们应该在用户首次开始与某个功能交互时,根据上下文请求权限。这是因为如果应用请求权限以启用用户已经在交互的功能,请求就不会让用户感到惊讶。这会带来更好的用户体验。在 PhotoLog 应用中,我们应该等到用户首次点击相机或位置按钮时才请求权限。
首先,让我们删除强制用户在进入主页之前批准所有权限的权限屏幕。此逻辑目前在 MainActivity.kt
中定义,因此我们导航到那里
val startNavigation = if (permissionManager.hasAllPermissions) { Screens.Home.route } else { Screens.Permissions.route }
它会在允许用户进入主页之前检查用户是否已授予所有权限。如前所述,这不符合我们用户体验的最佳实践。让我们将其更改为以下代码,使用户无需授予所有权限即可与我们的应用交互
val startNavigation = Screens.Home.route
现在我们不再需要权限屏幕了,我们也可以从 NavHost 中删除这一行
composable(Screens.Permissions.route) { PermissionScreen(navController) }
接下来,从 Screens 类中删除这一行
object Permissions : Screens("permissions")
最后,我们也可以删除 PermissionsScreen.kt 文件。
现在,删除并重新安装您的应用,这是重置先前授予的权限的一种方法!您现在应该能够立即进入主屏幕,但是当您在“添加日志”屏幕中按下相机或位置按钮时,什么也不会发生,因为应用不再有向用户请求权限的逻辑。让我们来修复它。
添加请求相机权限的逻辑
我们将从相机权限开始。根据我们在请求权限文档中看到的代码示例,我们首先需要注册权限回调,以便使用 RequestPermission()
契约。
让我们评估一下我们需要的逻辑
- 如果用户接受权限,我们将需要向 viewModel 注册权限,如果用户尚未达到已添加照片数量的限制,则导航到相机屏幕。
- 如果用户拒绝权限,我们可以通知他们该功能不起作用,因为权限已被拒绝。
要执行此逻辑,我们可以将此代码块添加到:// TODO: 步骤 1. 注册 ActivityResult 以请求相机权限
val requestCameraPermission = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { viewModel.onPermissionChange(CAMERA, isGranted) canAddPhoto { navController.navigate(Screens.Camera.route) } } else { coroutineScope.launch { snackbarHostState.showSnackbar("Camera currently disabled due to denied permission.") } } }
现在我们需要在导航到相机屏幕之前验证应用是否具有相机权限,如果用户尚未授予权限,则请求权限。为了实现此逻辑,我们可以添加以下代码块到:// TODO: 步骤 2. 在导航到相机屏幕之前检查并请求相机权限
canAddPhoto { when { state.hasCameraAccess -> navController.navigate(Screens.Camera.route) // TODO: Step 4. Trigger rationale screen for Camera if needed else -> requestCameraPermission.launch(CAMERA) } }
现在,再次尝试运行应用程序,然后点击“添加日志”屏幕中的相机图标。您应该会看到一个请求相机权限的对话框。恭喜!这比在用户甚至没有试用过应用程序之前就要求他们批准所有权限要好得多,对吗?
但我们能做得更好吗?是的!我们可以检查系统是否建议我们显示一个理由,解释为什么我们的应用需要访问相机。这有助于潜在地提高权限的选择加入率,并保留您的应用在更合适的时机再次请求权限的能力。
为此,让我们构建一个理由屏幕,解释为什么我们的应用需要访问用户的相机。通过将以下代码块添加到:// TODO: 步骤 3. 添加相机权限的解释对话框
来完成此操作
var showExplanationDialogForCameraPermission by remember { mutableStateOf(false) } if (showExplanationDialogForCameraPermission) { CameraExplanationDialog( onConfirm = { requestCameraPermission.launch(CAMERA) showExplanationDialogForCameraPermission = false }, onDismiss = { showExplanationDialogForCameraPermission = false }, ) }
既然我们有了对话框本身,我们只需检查在请求相机权限之前是否应该显示理由。我们通过调用 ActivityCompat 的 shouldShowRequestPermissionRationale()
API 来实现。如果它返回 true,我们只需将 showExplanationDialogForCameraPermission
也设置为 true 即可显示解释对话框。
让我们在 state.hasCameraAccess
情况和 else
情况之间添加以下代码块,或在说明中先前添加以下 TODO 的位置:// TODO: 步骤 4. 添加相机权限的解释对话框
ActivityCompat.shouldShowRequestPermissionRationale(context.getActivity(), CAMERA) -> showExplanationDialogForCameraPermission = true
您的相机按钮的完整逻辑现在应该如下所示
canAddPhoto { when { state.hasCameraAccess -> navController.navigate(Screens.Camera.route) ActivityCompat.shouldShowRequestPermissionRationale(context.getActivity(), CAMERA) -> showExplanationDialogForCameraPermission = true else -> requestCameraPermission.launch(CAMERA) } }
恭喜!我们已经按照 Android 的所有最佳实践处理了相机权限!现在,删除并重新安装应用程序,然后尝试从“添加日志”页面按下相机按钮。如果您拒绝权限,应用程序不会阻止您使用其他功能,例如打开相册。
然而,下次您在拒绝权限后点击相机图标时,您应该会看到我们刚刚添加的解释提示!*请注意,系统权限提示仅在用户点击解释提示上的“继续”后才显示,如果用户点击“暂不”,我们则允许他们使用应用程序,而不会受到进一步干扰。这有助于应用程序避免用户进一步拒绝权限,并保留我们在不同时间(当用户可能更愿意授予权限时)再次请求权限的能力。
- 注意:
shouldShowRequestPermissionRationale()
API 的确切行为是一个内部实现细节,可能会发生变化。
添加请求位置权限的逻辑
现在,让我们对位置执行相同的操作。我们可以首先通过添加以下代码块来注册位置权限的 ActivityResult:// TODO: 步骤 5. 注册 ActivityResult 以请求位置权限
val requestLocationPermissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { viewModel.onPermissionChange(ACCESS_COARSE_LOCATION, isGranted) viewModel.onPermissionChange(ACCESS_FINE_LOCATION, isGranted) viewModel.fetchLocation() } else { coroutineScope.launch { snackbarHostState.showSnackbar("Location currently disabled due to denied permission.") } } }
之后,我们可以继续为位置权限添加解释对话框,方法是添加以下代码块:// TODO: 步骤 6. 添加位置权限的解释对话框
var showExplanationDialogForLocationPermission by remember { mutableStateOf(false) } if (showExplanationDialogForLocationPermission) { LocationExplanationDialog( onConfirm = { // TODO: Step 10. Change location request to only request COARSE location. requestLocationPermissions.launch(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)) showExplanationDialogForLocationPermission = false }, onDismiss = { showExplanationDialogForLocationPermission = false }, ) }
接下来,让我们继续检查、解释(如有必要)并请求位置权限。如果权限被授予,我们可以获取位置并填充照片日志。让我们继续添加以下代码块:// TODO: 步骤 7. 检查、请求并解释位置权限
when { state.hasLocationAccess -> viewModel.fetchLocation() ActivityCompat.shouldShowRequestPermissionRationale(context.getActivity(), ACCESS_COARSE_LOCATION) || ActivityCompat.shouldShowRequestPermissionRationale( context.getActivity(), ACCESS_FINE_LOCATION) -> showExplanationDialogForLocationPermission = true else -> requestLocationPermissions.launch(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)) }
至此,我们已经完成了此 codelab 的权限部分!继续重置您的应用程序,查看结果。
我们如何改善用户体验以及这对您的应用的好处总结
- 根据上下文(当用户与功能交互时)请求权限,而不是在应用启动后立即请求 → 减少混淆和用户流失。
- 创建解释屏幕,向用户解释我们的应用为什么需要访问权限 → 增加用户透明度。
- 利用 shouldShowRequestPermissionRationale() API 来确定系统何时认为您的应用应该显示解释屏幕 → 提高权限接受率并降低永久拒绝权限的可能性。
5. 最佳实践:减少您的应用对位置的访问
位置是最敏感的权限之一,这就是为什么 Android 在隐私仪表板中特别强调它的原因。
快速回顾一下,在 Android 12 中,我们为用户提供了额外的位置控制。当应用请求位置访问时,用户现在可以选择共享不那么精确的位置数据,通过选择大概位置而不是精确位置。
大概位置为应用提供了用户在 3 平方公里范围内的估计位置,这对于您的许多应用功能来说应该足够精确。我们鼓励所有需要位置访问权限的开发者审查您的用例,并且仅在用户积极参与需要其精确位置的功能时才请求 ACCESS_FINE_LOCATION
。
将粗略位置估计范围可视化为以加利福尼亚州洛杉矶市中心为中心。
对于我们的 PhotoLog 应用来说,大概位置访问绝对足够了,因为我们只需要用户的城市来提醒他们“回忆”来自哪里。然而,该应用目前正在向用户请求 ACCESS_COARSE_LOCATION
和 ACCESS_FINE_LOCATION
。让我们改变这一点。
首先,我们需要编辑位置的活动结果,并将 ActivityResultContracts.RequestPermission()
函数作为参数提供,而不是 ActivityResultContracts.RequestMultiplePermissions()
,以反映我们只请求 ACCESS_COARSE_LOCATION
的事实。
让我们用以下代码块替换当前的 requestLocationsPermissions 对象(由 // TODO: 步骤 8. 将活动结果更改为仅请求粗略位置
表示)
val requestLocationPermissions = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { viewModel.onPermissionChange(ACCESS_COARSE_LOCATION, isGranted) } }
接下来,我们将更改 launch()
方法,使其仅请求 ACCESS_COARSE_LOCATION
,而不是同时请求两种位置权限。
让我们将
requestLocationPermissions.launch(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION))
...更改为
requestLocationPermissions.launch(ACCESS_COARSE_LOCATION)
PhotoLog 中有两个 launch()
方法的实例需要更改,一个在 LocationExplanationDialog
的 onConfirm()
逻辑中,由 // TODO: 步骤 9. 将位置请求更改为仅请求粗略位置
表示,另一个在“位置”列表项中,由 // TODO: 步骤 10. 将位置请求更改为仅请求粗略位置
表示
最后,既然我们不再为 PhotoLog 请求 ACCESS_FINE_LOCATION
权限,那么让我们从 AddLogViewModel.kt 中的 onPermissionChange() 方法中删除此部分。
Manifest.permission.ACCESS_FINE_LOCATION -> { uiState = uiState.copy(hasLocationAccess = isGranted) }
并且不要忘记同时从应用的清单中删除 ACCESS_FINE_LOCATION
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
现在我们已经完成了 codelab 的位置部分!继续卸载/重新安装您的应用程序,看看结果!
6. 最佳实践:最小化存储权限的使用
应用通常需要处理设备上存储的照片。为了让用户选择所需的图像和视频,这些应用通常会实现自己的文件选择器,这要求应用请求广泛的存储访问权限。用户不喜欢授予对所有图片的访问权限,开发者也不喜欢维护独立的文件选择器。
Android 13 引入了照片选择器:一种为用户提供选择媒体文件的方式,而无需授予应用访问用户整个媒体库的权限。在 Google Play 系统更新的帮助下,它也已向后移植到 Android 11 和 12。
对于我们的 PhotoLog 应用中的功能,我们将使用 PickMultipleVisualMedia
ActivityResultContract。它将在设备上存在时使用 Android 照片选择器,并在较旧的设备上依赖于 ACTION_OPEN_DOCUMENT
Intent。
首先,让我们在 AddLogScreen
文件中注册我们的 ActivityResultContract。为此,请在此行之后添加以下代码块:// TODO: 步骤 11. 注册 ActivityResult 以启动照片选择器
val pickImage = rememberLauncherForActivityResult( PickMultipleVisualMedia(MAX_LOG_PHOTOS_LIMIT), viewModel::onPhotoPickerSelect )
注意:此处的 MAX_LOG_PHOTOS_LIMIT
表示我们选择在将照片添加到日志时设置的最大照片限制(在这种情况下,为 3)。
现在我们需要用 Android 照片选择器替换应用中原有的内部选择器。在以下代码块之后添加以下代码:// TODO: 步骤 12. 替换下面的行,显示我们的内部 UI,改为启动 Android 照片选择器
pickImage.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
通过添加这两行代码,我们现在获得了一种无需权限即可访问设备照片的方式,它提供了更好的用户体验,并且不需要代码维护!
由于 PhotoLog 不再需要访问照片的旧照片网格和存储权限,我们现在应该删除所有包含我们旧照片网格的代码,从清单中的存储权限条目到其背后的逻辑,因为它在我们的应用程序中不再需要。
7. 推荐:在调试版本中使用数据访问审计 API
您的应用是否庞大,功能和协作者众多(或者您预计未来会如此!),这使得难以追踪应用正在访问哪些用户数据?您是否知道,即使数据访问来自曾使用过但现在仅存在于您的应用中的 API 或 SDK,您的应用仍需对数据访问负责?
我们理解追踪您的应用及其所有包含的 SDK 和其他依赖项访问私密数据的所有位置非常困难。因此,为了帮助您更透明地了解您的应用及其依赖项如何访问用户的私密数据,Android 11 引入了数据访问审计。此 API 允许开发者在发生以下事件之一时执行特定操作,例如打印到日志文件:
- 您的应用代码访问私人数据。
- 依赖库或 SDK 中的代码访问私人数据。
首先,让我们回顾一下 Android 上数据访问审计 API 的基本工作原理。要采用数据访问审计,我们将注册 AppOpsManager.OnOpNotedCallback
的实例(需要以 Android 11+ 为目标)。
我们还需要覆盖回调中的三个方法,系统将在应用以不同方式访问用户数据时调用这些方法。它们是:
onNoted()
- 当应用调用访问用户数据的同步(双向绑定)API 时调用。这些通常是不需要回调的 API 调用。onAsyncNoted()
- 当应用调用访问用户数据的异步(单向绑定)API 时调用。这些通常是需要回调的 API 调用,并且数据访问发生在回调被调用时。onSelfNoted()
- 不太可能发生,例如当应用程序将其自己的 UID 传递给 noteOp() 时。
现在让我们确定这些方法中哪一个适用于 PhotoLog 应用的数据访问。PhotoLog 主要在两个地方访问用户数据,一次是在我们激活相机时,另一次是在我们访问用户位置时。这两个都是异步 API 调用,因为它们都相对资源密集,因此我们预计当访问相应的用户数据时,系统会调用 onAsyncNoted()
。
让我们看看如何为 PhotoLog 采用数据访问审计 API!
首先,我们需要创建一个 AppOpsManager.OnOpNotedCallback()
实例,并覆盖上述三个方法。
对于对象中的所有三个方法,让我们继续记录访问私有用户数据的特定操作。此操作将包含有关访问了哪种用户数据的更多信息。此外,由于我们期望在我们的应用程序访问相机和位置信息时调用 onAsyncNoted()
,让我们做一些特别的事情,记录位置访问的地图表情符号和相机访问的相机表情符号。为此,我们可以添加以下代码块到:// TODO: 步骤 1. 创建数据访问审计监听器对象
@RequiresApi(Build.VERSION_CODES.R) object DataAccessAuditListener : AppOpsManager.OnOpNotedCallback() { // For the purposes of this codelab, we are just logging to console, // but you can also integrate other logging and reporting systems here to track // your app's private data access. override fun onNoted(op: SyncNotedAppOp) { Log.d("DataAccessAuditListener","Sync Private Data Accessed: ${op.op}") } override fun onSelfNoted(op: SyncNotedAppOp) { Log.d("DataAccessAuditListener","Self Private Data accessed: ${op.op}") } override fun onAsyncNoted(asyncNotedAppOp: AsyncNotedAppOp) { var emoji = when (asyncNotedAppOp.op) { OPSTR_COARSE_LOCATION -> "\uD83D\uDDFA" OPSTR_CAMERA -> "\uD83D\uDCF8" else -> "?" } Log.d("DataAccessAuditListener", "Async Private Data ($emoji) Accessed: ${asyncNotedAppOp.op}") } }
然后我们需要实现我们刚刚创建的回调逻辑。为了获得最佳结果,我们希望尽早完成此操作,因为系统只有在注册回调之后才会开始跟踪数据访问。要注册回调,我们可以添加以下代码块到:// TODO: 步骤 2. 注册数据访问审计回调
。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val appOpsManager = getSystemService(AppOpsManager::class.java) as AppOpsManager appOpsManager.setOnOpNotedCallback(mainExecutor, DataAccessAuditListener) }
8. 总结
让我们回顾一下我们所涵盖的内容!我们...
- ...探讨了隐私对您的应用为何重要。
- ...了解了 Android 的隐私功能。
- ...通过以下方式,实施了许多应用的关键隐私最佳实践:
- 根据上下文请求权限
- 减少应用的位置访问
- 使用照片选择器及其他存储改进
- 使用数据访问审计 API
- ...在现有应用中实施了这些最佳实践,以增强其隐私性。
我们希望您喜欢我们改进 PhotoLog 隐私和用户体验的旅程,并在其中学到许多概念!
查找我们的参考代码(可选)
如果您还没有,可以在 PhotoLog_End
文件夹中查看 codelab 的解决方案代码。如果您严格按照本 codelab 的说明操作,PhotoLog_Start
文件夹中的代码应该与 PhotoLog_End
文件夹中的代码相同。
了解更多
就这些了!要了解有关我们上面介绍的最佳实践的更多信息,请查看 Android 隐私登陆页面。