1. 事前準備
Android 10 和 11 可讓使用者進一步控管自己的應用程式存取裝置位置資訊。
在 Android 11 上執行的應用程式要求位置資訊存取權時,使用者可選擇以下四種選項:
- 一律允許
- 僅在使用該應用程式時允許 (Android 10)
- 僅限一次 (Android 11)
- 拒絕
Android 10
Android 11
在本程式碼研究室中,您可以瞭解如何接收位置更新,以及如何在任何 Android 版本 (尤其是 Android 10 和 11) 上支援位置資訊。在本程式碼研究室的結尾,您可以設計出符合目前最佳做法的應用程式,以擷取位置更新通知。
必要條件
執行步驟
- 採用 Android 位置資訊的最佳做法。
- 處理前景位置存取權 (使用者在應用程式使用期間要求存取裝置位置資訊時)。
- 修改現有應用程式,加入訂閱和取消訂閱位置程式碼的支援,要求要求位置資訊存取權。
- 如要新增 Android 10 和 11 版的應用程式支援,請新增邏輯,允許在前景位置或使用期間存取位置資訊。
軟硬體需求
- Android Studio 3.4 以上版本執行程式碼
- 搭載 Android 10 和 11 開發人員預覽版的裝置/模擬器
2. 踏出第一步
複製範例專案存放區
如要盡快上手,您可以採用這項範例專案進行建構。如果您已安裝 Git,只要執行下列指令即可:
git clone https://github.com/android/codelab-while-in-use-location
您可以直接前往 GitHub 頁面。
如果您沒有 Git,可以使用 ZIP 檔案取得專案:
匯入專案
開啟 Android Studio,然後選取「Open an existing Android Studio project」。,然後開啟專案目錄。
專案載入後,系統也可能會顯示快訊,通知您 Git 不會追蹤所有本機變更。點按「忽略」。(系統不會將任何變更推送回 Git 存放區。)
如果您在「Android」檢視畫面中,專案視窗的左上角會顯示如下圖所示的項目 (在「Project」檢視畫面中,必須展開專案才能看到這些項目)。
共有兩個資料夾 (base
和 complete
)。每個變數都稱為「模組」。
請注意,由於 Android Studio 首次會在背景編譯專案,因此可能需要幾秒鐘的處理時間。在此期間,您會在 Android Studio 底部的狀態列中看到以下訊息:
請等到 Android Studio 完成為專案建立索引及建構專案後,再變更程式碼。這使 Android Studio 提取所有必要的元件。
如果畫面顯示「Reload for language changes to take effect?」或類似的提示,請選取「Yes」。
瞭解範例專案
你已設定完成,可以在應用程式中要求取得位置資訊。請以 base
模組做為起點。在每個步驟中,將程式碼新增至 base
模組。完成本程式碼研究室後,base
模組中的程式碼應與 complete
模組的內容相符。complete
模組可用於檢查你的作品,在發生問題時也可以參考。
主要元件包括:
MainActivity
:使用者可允許應用程式存取裝置位置資訊的使用者介面LocationService
:訂閱和取消訂閱位置變更的服務,在使用者離開應用程式活動時,主動升級為前景服務 (顯示通知)。請在這裡加入位置程式碼。Util
:為Location
類別新增擴充功能函式,並將位置儲存在SharedPreferences
(簡化的資料層)。
模擬器設定
如要進一步瞭解如何設定 Android 模擬器,請參閱「在模擬器上執行」。
執行範例專案
執行應用程式。
- 將 Android 裝置連接至電腦或啟動模擬器。(請確認裝置搭載 Android 10 以上版本)。
- 在工具列的下拉式選取器中選取
base
設定,然後按一下「Run」:
- 請注意,裝置上會顯示下列應用程式:
您可能會注意到輸出畫面不會顯示任何位置資訊。因為您尚未加入地區代碼。
3. 正在新增位置資訊
概念
本程式碼研究室的重點,在於說明如何接收位置更新,最終支援 Android 10 和 Android 11。
不過,開始寫程式前,建議您先複習一下基礎知識。
位置存取權類型
您可能記得在本程式碼研究室的第一步之初,曾有四種位置存取權選項。看看它們代表的意義:
- 僅在使用該應用程式時允許
- 對於大多數應用程式,建議採用這個選項。又稱為「使用期間」或「僅限前景」存取權,但這個方法已在 Android 10 中新增,讓開發人員只能在應用程式使用期間擷取位置資訊。如果應用程式符合下列任一條件,即視為使用中:
- 活動會顯示。
- 前景服務正在執行中,並顯示持續性通知。
- 僅限一次
- 已在 Android 11 中新增,與「僅在使用該應用程式時允許」相同,但有一段限制。詳情請參閱「單次授權」。
- 拒絕
- 這個選項可禁止存取位置資訊。
- 一律允許
- 這個選項一律允許存取位置資訊,但如果是 Android 10 以上版本,則需要額外權限。此外,您也必須確保自己具備有效的用途,並遵守位置政策。本程式碼研究室不會介紹這個選項,因為這是極少數的用途。不過,如果您有有效的用途,且想瞭解如何正確處理一律允許的位置 (包括在背景存取位置資訊),請參閱 LocationUpdatesBackgroundKotlin 範例。
服務、前景服務與繫結
如要完整支援「僅在使用該應用程式時允許」的位置更新訊息,您必須考量使用者離開應用程式時的情形。如果您想繼續收到該情形的更新資訊,就必須建立前景 Service
,並將其與 Notification
建立關聯。
此外,如果您想在應用程式顯示期間和使用者離開應用程式時,使用相同的 Service
要求位置更新通知,就必須將該 Service
繫結/解除繫結至 UI 元素。
由於本程式碼研究室只著重於取得位置更新,因此您可以在 ForegroundOnlyLocationService.kt
類別中找到所有需要的程式碼。您可以瀏覽該類別和 MainActivity.kt
,瞭解它們如何搭配運作。
權限
如要從 NETWORK_PROVIDER
或 GPS_PROVIDER
接收位置更新通知,您必須在 Android 資訊清單檔案中分別宣告 ACCESS_COARSE_LOCATION
或 ACCESS_FINE_LOCATION
權限,要求使用者權限。如果沒有這些權限,應用程式就無法在執行階段要求位置資訊存取權。
應用程式在搭載 Android 10 以上版本的裝置上執行時,這些權限涵蓋「僅限使用一次」和「僅在使用該應用程式時允許」的情況下。
位置
您的應用程式可以透過 com.google.android.gms.location
套件中的類別存取支援的定位服務組合。
查看主要類別:
FusedLocationProviderClient
- 這是位置資訊架構的核心要素。建立後,您可以用它來要求位置更新通知,並取得最後已知位置。
LocationRequest
- 這是資料物件,包含要求的品質參數 (更新、優先順序及準確性的間隔)。當您要求位置更新通知時,系統會將其傳遞至
FusedLocationProviderClient
。 LocationCallback
- 以便在裝置位置資訊變更或我們判斷恢復運作時接收通知。系統會傳遞
LocationResult
,取得Location
並儲存至資料庫。
您現在對自己的操作有基本的瞭解,可以開始編寫程式碼了!
4. 新增位置地圖項目
本程式碼研究室著重於最常用的位置選項:「僅在使用該應用程式時允許」。
如要接收位置更新資訊,您的應用程式必須具有可見活動,或服務在前景執行 (同時顯示通知)。
權限
本程式碼研究室旨在說明如何接收位置更新通知,而非要求位置存取權,因此系統已為您編寫權限程式碼。如果您已瞭解內容,可以選擇略過。
以下是權限重點摘要 (這部分不需要採取任何行動):
- 請在
AndroidManifest.xml
中宣告要使用的權限。 - 在嘗試存取位置資訊前,請檢查使用者是否已授予應用程式相關權限。如果您的應用程式尚未取得權限,請提出存取權要求。
- 處理使用者的權限選項。(您可以在
MainActivity.kt
中查看這個程式碼)。
在 AndroidManifest.xml
或 MainActivity.kt
中搜尋 TODO: Step 1.0, Review Permissions
時,會看到所有為權限編寫的程式碼。
詳情請參閱權限總覽。
現在,您可以開始撰寫某些位置程式碼。
查看位置更新所需的重要變數
在 base
模組中,於TODO: Step 1.1, Review variables
ForegroundOnlyLocationService.kt
檔案。
這個步驟不需要採取任何行動。您只需要查看下列程式碼區塊和註解,瞭解您用來接收位置更新的主要類別和變數。
// TODO: Step 1.1, Review variables (no changes).
// FusedLocationProviderClient - Main class for receiving location updates.
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
// LocationRequest - Requirements for the location updates, i.e., how often you
// should receive updates, the priority, etc.
private lateinit var locationRequest: LocationRequest
// LocationCallback - Called when FusedLocationProviderClient has a new Location.
private lateinit var locationCallback: LocationCallback
// Used only for local storage of the last known location. Usually, this would be saved to your
// database, but because this is a simplified sample without a full database, we only need the
// last location to create a Notification if the user navigates away from the app.
private var currentLocation: Location? = null
查看 FusedLocationProviderClient 初始化
在 base
模組中,在 ForegroundOnlyLocationService.kt
檔案中搜尋 TODO: Step 1.2, Review the FusedLocationProviderClient
。程式碼應如下所示:
// TODO: Step 1.2, Review the FusedLocationProviderClient.
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
如先前的留言所述,這是接收位置更新資訊的主要類別。系統已為您初始化變數,但建議您查看程式碼,瞭解變數的初始化方式。稍後您可以在這裡加入一些程式碼,要求位置更新通知。
初始化 LocationRequest
- 在
base
模組中,在ForegroundOnlyLocationService.kt
檔案中搜尋TODO: Step 1.3, Create a LocationRequest
。 - 在註解後方新增以下程式碼。
LocationRequest
初始化程式碼會為您的要求提供額外的優質服務參數,例如間隔、等待時間上限和優先順序。
// TODO: Step 1.3, Create a LocationRequest.
locationRequest = LocationRequest.create().apply {
// Sets the desired interval for active location updates. This interval is inexact. You
// may not receive updates at all if no location sources are available, or you may
// receive them less frequently than requested. You may also receive updates more
// frequently than requested if other applications are requesting location at a more
// frequent interval.
//
// IMPORTANT NOTE: Apps running on Android 8.0 and higher devices (regardless of
// targetSdkVersion) may receive updates less frequently than this interval when the app
// is no longer in the foreground.
interval = TimeUnit.SECONDS.toMillis(60)
// Sets the fastest rate for active location updates. This interval is exact, and your
// application will never receive updates more frequently than this value.
fastestInterval = TimeUnit.SECONDS.toMillis(30)
// Sets the maximum time when batched location updates are delivered. Updates may be
// delivered sooner than this interval.
maxWaitTime = TimeUnit.MINUTES.toMillis(2)
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
- 請仔細閱讀留言說明,瞭解各項功能的運作方式。
初始化 LocationCallback
- 在
base
模組中,在ForegroundOnlyLocationService.kt
檔案中搜尋TODO: Step 1.4, Initialize the LocationCallback
。 - 在註解後方新增以下程式碼。
// TODO: Step 1.4, Initialize the LocationCallback.
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
// Normally, you want to save a new location to a database. We are simplifying
// things a bit and just saving it as a local variable, as we only need it again
// if a Notification is created (when the user navigates away from app).
currentLocation = locationResult.lastLocation
// Notify our Activity that a new location was added. Again, if this was a
// production app, the Activity would be listening for changes to a database
// with new locations, but we are simplifying things a bit to focus on just
// learning the location side of things.
val intent = Intent(ACTION_FOREGROUND_ONLY_LOCATION_BROADCAST)
intent.putExtra(EXTRA_LOCATION, currentLocation)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
// Updates notification content if this service is running as a foreground
// service.
if (serviceRunningInForeground) {
notificationManager.notify(
NOTIFICATION_ID,
generateNotification(currentLocation))
}
}
}
您在這裡建立的 LocationCallback
是當有新位置更新時,FusedLocationProviderClient
會呼叫的回呼。
您必須先在回呼中,使用 LocationResult
物件取得最新位置。之後,您可以透過地方廣播 (如果已啟用) 通知 Activity
新的位置資訊;如果這項服務是以前景 Service
執行,請更新 Notification
。
- 請仔細閱讀留言,瞭解各個部分的作用。
訂閱地點變更
現在您已初始化所有內容,您需要讓 FusedLocationProviderClient
知道您要接收更新。
- 在
base
模組中,在ForegroundOnlyLocationService.kt
檔案中搜尋Step 1.5, Subscribe to location changes
。 - 在註解後方新增以下程式碼。
// TODO: Step 1.5, Subscribe to location changes.
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
requestLocationUpdates()
呼叫會通知 FusedLocationProviderClient
你想要接收位置更新資訊。
您可能認得先前定義的 LocationRequest
和 LocationCallback
。這些元件可讓 FusedLocationProviderClient
瞭解要求的服務品質參數,以及要求更新時應呼叫的內容。最後,Looper
物件會指定回呼的執行緒。
您可能也會注意到,這個程式碼位於 try/catch
陳述式中。使用這個方法時,由於應用程式不具備位置資訊存取權,就會發生 SecurityException
。
取消訂閱地區變更
如果應用程式不再需要位置資訊存取權,請務必取消訂閱位置資訊更新。
- 在
base
模組中,在ForegroundOnlyLocationService.kt
檔案中搜尋TODO: Step 1.6, Unsubscribe to location changes
。 - 在註解後方新增以下程式碼。
// TODO: Step 1.6, Unsubscribe to location changes.
val removeTask = fusedLocationProviderClient.removeLocationUpdates(locationCallback)
removeTask.addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "Location Callback removed.")
stopSelf()
} else {
Log.d(TAG, "Failed to remove Location Callback.")
}
}
removeLocationUpdates()
方法會設定一項工作,讓 FusedLocationProviderClient
知道您不想再收到 LocationCallback
的位置更新資訊。addOnCompleteListener()
會提供完成作業的回呼,並執行 Task
。
與上一個步驟相同,您可能已註意到這個程式碼位於 try/catch
陳述式中。使用這個方法時,由於應用程式不具備位置資訊存取權,會發生 SecurityException
您可能會好奇,何時呼叫包含訂閱/取消訂閱代碼的方法。當使用者輕觸按鈕時,會在主要類別中觸發。如果想查看,請參閱 MainActivity.kt
類別。
執行 app
從 Android Studio 執行應用程式,並嘗試位置按鈕。
輸出畫面應會顯示位置資訊。這是適用於 Android 9 且功能齊全的應用程式。
5. 支援 Android 10
在本節中,您將新增對 Android 10 的支援。
您的應用程式已經訂閱了位置異動通知,因此您不需要執行許多工作。
事實上,您只需指定前景服務用於位置資訊用途即可。
目標 SDK 29
- 在
base
模組中,在build.gradle
檔案中搜尋TODO: Step 2.1, Target Android 10 and then Android 11.
。 - 進行這些變更:
- 將
targetSdkVersion
設為29
。
程式碼應如下所示:
android {
// TODO: Step 2.1, Target Android 10 and then Android 11.
compileSdkVersion 29
defaultConfig {
applicationId "com.example.android.whileinuselocation"
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
...
}
執行完畢後,系統會要求您同步處理專案。按一下「Sync Now」。
之後,您的應用程式幾乎可以支援 Android 10 了。
新增前景服務類型
在 Android 10 中,如果需要在使用期間存取位置資訊,必須納入前景服務類型。就您的情況而言,系統會使用此功能取得位置資訊。
在 base
模組的 AndroidManifest.xml
中搜尋 TODO: 2.2, Add foreground service type
,然後將下列程式碼加進 <service>
元素:
android:foregroundServiceType="location"
程式碼應如下所示:
<application>
...
<!-- Foreground services in Android 10+ require type. -->
<!-- TODO: 2.2, Add foreground service type. -->
<service
android:name="com.example.android.whileinuselocation.ForegroundOnlyLocationService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
</application>
大功告成!您的應用程式支援在「使用期間」使用 Android 10 位置遵循 Android 位置資訊的最佳做法。
執行 app
從 Android Studio 執行應用程式,並嘗試位置按鈕。
所有功能應會照常運作,但現在可在 Android 10 中運作。如果您尚未接受位置的存取權限,現在應該會看到權限畫面!
6. 支援 Android 11
在本節中,您將指定 Android 11。
好消息!除了 build.gradle
檔案以外,你不需要對任何檔案進行變更!
目標 SDK 11
- 在
base
模組中,在build.gradle
檔案中搜尋TODO: Step 2.1, Target SDK
。 - 進行這些變更:
compileSdkVersion
至30
targetSdkVersion
至30
程式碼應如下所示:
android {
TODO: Step 2.1, Target Android 10 and then Android 11.
compileSdkVersion 30
defaultConfig {
applicationId "com.example.android.whileinuselocation"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
...
}
執行完畢後,系統會要求您同步處理專案。按一下「Sync Now」。
之後,您的應用程式就可以支援 Android 11 了!
執行 app
從 Android Studio 執行應用程式,然後嘗試點選按鈕。
所有功能應會照常運作,但現在可在 Android 11 上運作。如果您尚未接受位置的存取權限,現在應該會看到權限畫面!
7. Android 定位策略
只要按照本程式碼研究室所示的方式檢查並要求位置存取權,應用程式就能順利追蹤裝置位置的存取層級。
本頁列出幾項與位置存取權相關的重要最佳做法。進一步瞭解如何資料安全性,請參閱「應用程式權限最佳做法」。
只要求必要權限
只在需要時才要求權限。例如:
- 除非絕對必要,否則請勿在應用程式啟動時要求位置存取權。
- 如果應用程式指定 Android 10 以上版本,並提供前景服務,請在資訊清單中宣告
"location"
的foregroundServiceType
。 - 除非有符合「使用者位置資訊更公開透明」一文所述的有效用途,否則請勿要求背景位置資訊存取權。
若未授予權限,則支援優雅降級功能
為維持良好的使用者體驗,設計應用程式時,請確保應用程式能夠妥善處理以下情況:
- 您的應用程式沒有任何位置資訊存取權。
- 您的應用程式無法在背景執行時存取位置資訊。
8. 恭喜
您已瞭解如何在 Android 中接收位置更新通知,並遵循最佳做法!