Android 隱私權程式碼研究室

1. 簡介

課程內容

  • 為何隱私權對於使用者越來越重要?
  • Android 隱私權在過去幾個版本中的最佳做法。
  • 如何將隱私權最佳做法整合至現有應用程式,以提高應用程式的隱私權保護力。

建構項目

在本程式碼研究室中,您會從使用者可以儲存其相片回憶集錦的範例應用程式開始操作。

從以下畫面開始:

  • 權限畫面 - 要求使用者必須授予所有權限才能前往主畫面。
  • 主畫面 - 顯示使用者所有相片記錄的畫面,並允許使用者新增相片記錄。
  • 新增記錄畫面 - 使用者可以建立新的相片記錄的畫面。使用者可以在相片庫中瀏覽現有相片、使用相機拍攝新相片,並將目前城市加入相片記錄中。
  • 相機畫面 - 使用者可以拍照,並將相片儲存至相片記錄的畫面。

應用程式雖然正常運作,但仍有我們可以一起改進的許多隱私缺失!

跟著本程式碼研究室的步驟操作,您將:

  • ...瞭解為何隱私權對於應用程式如此重要
  • ...認識 Android 的隱私權功能和主要最佳做法。
  • ...執行下列動作,瞭解如何在現有應用程式中實作這些最佳做法:
  • 在情境中要求權限
  • 減少應用程式的位置存取權
  • 使用相片挑選工具和其他儲存空間改善功能
  • 使用資料存取稽核 API

完成課程後,您將建構一款符合以下條件的應用程式:

  • ...實作上述隱私權的最佳做法。
  • ...提供隱私權保護,並謹慎處理私人資料,進而提升使用者體驗,藉此保護使用者。

軟硬體需求

建議條件

2. 隱私權的重要性

研究顯示,大家都會擔心自己的隱私權。Pew Research Institute 所做的問卷調查發現,84% 的美國人認為自己幾乎無法掌控公司與應用程式所收集到的資料。他們主要感到挫折的地方是不知道在直接使用後自己的資料會受到哪些處理。舉例來說,他們會擔心資料會用於其他用途,例如建立用於指定目標廣告的設定檔,甚至是販售給其他方。資料到那邊後,似乎就無法移除。

此隱私權疑慮已嚴重影響使用者決定要使用哪些服務或應用程式。事實上,Pew Research Institute 同一份研究發現,半數 (52%) 美國成人基於隱私權疑慮而決定不要使用產品或服務,例如擔心收集了多少與他們相關的資料。

因此,增進並證實應用程式的隱私權是提升應用程式使用者體驗的關鍵,而且研究也顯示,這也可能有助於拓展使用者族群。

在本程式碼研究室中,我們將介紹的許多功能和最佳做法,都有助於減少應用程式存取的資料量,或提升使用者對私人資料的掌控。上述兩項增強功能皆能直接解決使用者之前在研究中分享的疑慮。

3. 設定環境

為了方便您盡快上手,我們準備了新手範例專案。在這個步驟中,您將下載適用於整個程式碼研究室的程式碼,其中包含在模擬器或裝置上執行範例應用程式的範例專案。

如果您已安裝 Git,只要執行下列指令即可。如要檢查 Git 是否已安裝完成,請在終端機或指令列中輸入 git –version 類型,並確認其可正確執行。

git clone https://github.com/android/privacy-codelab

如果您沒有 Git,可以按一下連結,以下載這個程式碼研究室的所有程式碼:

如要設定程式碼研究室:

  1. 在 Android Studio 的 PhotoLog_Start 目錄中開啟專案。
  2. 在搭載 Android 12 (S) 以上版本的裝置或模擬器上執行 PhotoLog_Start 執行設定。

d98ce953b749b2be.png

您應會看到要求授予執行應用程式權限的畫面!這表示您已成功設定環境。

4. 最佳做法:在情境中要求權限

許多人都知道,執行階段權限是解鎖許多主要功能,藉以保有良好使用者體驗的關鍵要素,但您是否知道應用程式要求權限的時機和方式,也會對使用者體驗造成重大影響嗎?

以下是 PhotoLog_Start 應用程式要求權限的方式,藉以瞭解此方式為何沒有最佳權限模型:

  1. 就在啟動後,使用者會立即收到權限提示,要求他們授予多項權限。這樣可能會讓使用者感到困惑,對我們的應用程式失去信任,最糟糕的情況更可能解除安裝應用程式!
  2. 除非授予所有權限,否則應用程式不會讓使用者繼續執行。在剛啟動應用程式時,使用者對應用程式的信任感可能還不夠,因而不會授予此私密資訊的存取權。

您或許已經猜到,以上清單為一組改善建議,旨在改善應用程式的權限要求程序!事不宜遲,就讓我們切入正題吧。

我們可以看到 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 檔案。

現在,請刪除應用程式再重新安裝,這是重設之前所授予權限的方法之一!您現在應可立即進入主畫面,但當您在「Add Log」畫面中按下相機或位置按鈕時,系統不會執行任何動作,原因在於應用程式已無向使用者要求權限的邏輯。讓我們一起解決這個問題!

加入要求相機權限的邏輯

我們將從相機權限開始著手。根據要求權限文件所示的程式碼範例,我們會想要先註冊權限回呼,以使用 RequestPermission() 合約。

接著評估所需的邏輯:

  • 如果使用者接受權限,我們會想要註冊 viewModel 的權限,而且如果使用者尚未達到新增相片數量的限制,也會前往相機畫面。
  • 如果使用者拒絕權限要求,我們可以通知使用者由於權限遭拒,因此功能無法運作。

如要執行此邏輯,我們可以將此程式碼區塊新增至:// TODO: Step 1. Register ActivityResult to request Camera permission

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: Step 2. Check & request for Camera permission before navigating to the camera screen

canAddPhoto {
   when {
       state.hasCameraAccess -> navController.navigate(Screens.Camera.route)
       // TODO: Step 4. Trigger rationale screen for Camera if needed
       else -> requestCameraPermission.launch(CAMERA)
   }
}

現在,請嘗試再次執行應用程式,然後在「Add Log」畫面中按一下相機圖示。您應會看到一個要求相機權限的對話方塊。恭喜!比起在使用者試用應用程式前,直接要求使用者核准所有權限,此做法是不是要好得多?

但我們是否可以做得更好?是的!我們可以檢查系統是否建議我們要顯示一個合理原因,解釋應用程式為何需要存取相機。此舉有助於提高權限的選擇加入率,同時也能讓應用程式在更適宜的時間重新要求權限。

為此,接著請建構合理原因畫面,解釋應用程式為何需要存取使用者的相機。方法是將下列程式碼區塊新增至:// TODO: Step 3. Add explanation dialog for Camera permission

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: Step 4. Add explanation dialog for Camera permission

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 最佳做法,來處理相機權限!請直接刪除應用程式再重新安裝,然後在「Add Log」頁面中按下相機按鈕。如果你拒絕授予權限,應用程式並不會阻止你使用其他功能,例如開啟相簿。

不過,下次在拒絕權限後按一下相機圖示時,畫面上應會顯示剛剛新增的說明提示!*。請注意,系統權限提示只會在使用者按一下說明提示中的「continue」後出現,而且如果使用者按一下「not now」,使用者即可繼續使用應用程式。這有助於應用程式避免使用者拒絕其他權限要求,同時也能在其他情況下 (例如,使用者可能準備好授予權限時) 再次要求權限。

  • 注意:shouldShowRequestPermissionRationale() API 的確切行為為內部實作詳細資料,隨時可能變更。

加入要求位置存取權的邏輯

現在對位置權限做同樣的動作。我們先註冊位置存取權的 ActivityResult,方法是將下列程式碼區塊新增至:// TODO: Step 5. Register ActivityResult to request Location permissions

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: Step 6. Add explanation dialog for Location permissions

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: Step 7. Check, request, and explain Location permissions

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))
}

這樣就完成了本程式碼研究室的權限區段了!請嘗試重設應用程式並查看結果。

以下摘要說明我們如何改善使用者體驗,以及為應用程式帶來的好處:

  • 在情境中要求權限 (使用者與功能互動時),而非立即啟動應用程式 → 減少疑惑與使用者退出使用。
  • 建立說明畫面,向使用者說明應用程式為何需要存取權限 → 提高資訊透明度。
  • 運用 shouldShowRequestPermissionRationale() API 判斷何時系統認為應用程式應顯示說明畫面 → 提高權限接受率並降低永久權限遭拒的機率。

5. 最佳做法:減少應用程式的位置存取權

位置是最機密的權限之一,因此 Android 會在隱私資訊主頁中提及該權限。

在此快速回顧,在 Android 12 中,我們已為使用者提供位置的其他控制項。使用者現在可以明確選擇分享較不準確的位置資料給應用程式,只要在應用程式要求位置存取權時選取「大概位置」,而不要選取「精確位置」即可。

「大概位置」會向應用程式提供預估的使用者位置 (誤差在 3 平方公里內),而此精確度應足以提供許多應用程式功能。我們鼓勵其應用程式需要位置存取權的所有開發人員都要審查自己的用途。只有當使用者主動使用需要精確位置的功能時,才會要求 ACCESS_FINE_LOCATION

ea5cc51fce3f219e.png

以視覺化方式呈現加州洛杉磯市中心概略位置預估範圍的圖形。

由於我們只需要使用者的城市就能提醒使用者要「回憶」的地點,因此「大概位置」存取權肯定足以 PhotoLog 應用程式運作。不過,應用程式目前正向使用者要求 ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION。接著來改變權限。

首先,我們必須編輯位置資訊的活動結果,並提供 ActivityResultContracts.RequestPermission() 函式做為參數 (而非 ActivityResultContracts.RequestMultiplePermissions()),以反映我們只要求 ACCESS_COARSE_LOCATION

請改用下列程式碼區塊,取代目前的 requestLocationsPermissions 物件 (以 // TODO: Step 8. Change activity result to only request Coarse Location 表示):

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: Step 9. Change location request to only request COARSE location 表示),另一個位於「Location」清單項目中 (以 // TODO: Step 10. Change location request to only request COARSE location 表示)

最後,既然我們已不再要求 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" />

本程式碼研究室的位置區段現已完成!請直接解除安裝/重新安裝應用程式,即可查看結果!

6. 最佳做法:盡量避免使用儲存空間權限

應用程式常會使用裝置上儲存的相片。為了讓使用者挑選出想要的圖片和影片,這些應用程式通常會實作自己的檔案選擇器,因而需要應用程式要求廣大儲存空間存取權的權限。使用者不喜歡授予自己的所有圖片,而開發人員也不喜歡維護獨立的檔案選擇器。

Android 13 導入了相片挑選工具:此工具可讓使用者選取媒體檔案,而不必授予應用程式整個媒體庫的存取權。在 Google Play 系統更新的協助下,我們也向後移植至 Android 11 和 12。

為了在 PhotoLog 應用程式中使用此功能,我們會使用 PickMultipleVisualMedia ActivityResultContract。在裝置上出現時,它會使用 Android 相片挑選工具,並仰賴舊裝置上的 ACTION_OPEN_DOCUMENT 意圖。

首先,請在 AddLogScreen 檔案中註冊 ActivityResultContract。為此,請在這一行後加入下列程式碼區塊:// TODO: Step 11. Register ActivityResult to launch the Photo Picker

val pickImage = rememberLauncherForActivityResult(
   PickMultipleVisualMedia(MAX_LOG_PHOTOS_LIMIT),
    viewModel::onPhotoPickerSelect
)

注意:此處的 MAX_LOG_PHOTOS_LIMIT 代表將相片加入記錄時可設定的相片數量上限 (此時為 3 張)。

現在,我們要用 Android 相片挑選工具取代應用程式中的內部挑選工具。在區塊後方新增以下程式碼:// TODO: Step 12. Replace the below line showing our internal UI by launching the Android Photo Picker instead

pickImage.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))

新增了這行兩行程式碼後,便擁有無權限即可存取裝置相片的方式,提供更順暢的使用者體驗,且無需維護程式碼!

由於 PhotoLog 不再需要舊版相片格線和儲存空間權限,就能存取相片,因此我們現在應移除內含舊版相片格線的所有程式碼 (從資訊清單中的儲存空間權限項目到它背後的邏輯),因為應用程式已不再需要。

7. 建議:在偵錯版本中使用 Data Access Audit API

您是否有一個大型應用程式,有著許多功能和協作者 (或預期未來會推出!),因此難以追蹤應用程式存取哪些類型的使用者資料?您知道嗎?即使資料存取權來自於某個時候用過的 API 或 SDK,但現在只在您的應用程式內沿用,您的應用程式仍要負責的資料存取權嗎?

我們瞭解您難以追蹤應用程式存取私人資料的所有位置,包括所有內含的 SDK 和其他依附元件。因此,為了協助您更清楚掌握應用程式及其依附元件如何存取使用者的私人資料,Android 11 導入了資料存取稽核功能。這個 API 可讓開發人員在每次發生下列其中一種事件時執行特定動作,例如列印到記錄檔:

  • 應用程式的程式碼存取私人資料。
  • 獨立程式庫或 SDK 中的程式碼存取私人資料。

首先說明資料存取稽核 API 如何在 Android 上運作的基本概念。如要採用資料存取稽核,我們會註冊 AppOpsManager.OnOpNotedCallback 的執行個體 (需要針對 Android 11 以上版本)。

您也必須覆寫回呼中的三個方法,因為應用程式以不同方式存取使用者資料時,系統會叫用此方法。這兩種網址格式分別如下:

  • onNoted() - 應用程式叫用可存取使用者資料的同步 (雙向繫結) API 時呼叫。這些通常是不需要回呼的 API 呼叫。
  • onAsyncNoted() - 應用程式叫用可存取使用者資料的非同步 (單向繫結) API 時呼叫。這通常是需要回呼的 API 呼叫,且在叫用回呼時進行資料存取。
  • onSelfNoted() - 不太可能會發生,例如應用程式將其 UID 傳遞至 noteOp() 時。

現在,接著要決定哪種方法適用於 PhotoLog 應用程式的資料存取權。PhotoLog 主要會在兩處存取使用者資料,一次是啟用相機時,另一次則是存取使用者的位置資訊時。這些都是非同步 API 呼叫,因為兩者都涉及相對密集的資源,所以在存取相關使用者資料時,我們預期系統會叫用 onAsyncNoted()

以下逐步說明 PhotoLog 如何採用資料存取稽核 API!

首先,我們必須建立 ​​AppOpsManager.OnOpNotedCallback() 的執行個體,並覆寫上述三個方法。

至於物件中的所有三個方法,請繼續記錄已存取私人使用者資料的特定作業。這項作業將進一步說明存取的使用者資料類型。此外,由於應用程式存取相機和位置資訊時,我們預期會呼叫 onAsyncNoted(),因此我們來做點不一樣的事情,記錄位置存取權的地圖表情符號,以及相機存取權的相機表情符號。為此,請在這一行後加入下列程式碼區塊:// TODO: Step 1. Create Data Access Audit Listener Object

@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: Step 2. Register Data Access Audit Callback.

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 資料夾中查看程式碼研究室的解決方案程式碼。如果您已確實遵循本程式碼研究室的操作說明,PhotoLog_Start 資料夾中的程式碼應與 PhotoLog_End 資料夾中的程式碼相同。

瞭解詳情

大功告成!如要瞭解上述最佳做法的詳情,請前往 Android 隱私權到達網頁