Android 10 和 Android 11 可讓使用者進一步控管應用程式的應用程式存取權,以及存取裝置位置資訊。
當使用者在 Android 11 上執行的應用程式要求位置存取權時,使用者有以下四個選項:
- 一律允許
- 僅在使用該應用程式時允許 (在 Android 10 中)
- 僅限一次 (僅限 Android 11)
- 拒絕
Android 10
Android 11
在這個程式碼研究室中,您將瞭解如何在 Android 版本 (尤其是 Android 10 和 11) 上接收位置更新,以及如何支援位置資訊。在程式碼研究室的結尾,您可以讓應用程式遵循目前擷取位置資訊更新的最佳做法。
事前準備
要執行的步驟
- 遵循在 Android 上提供位置資訊的最佳做法。
- 處理前景位置資訊權限 (當使用者在應用程式使用期間要求存取裝置位置資訊時)。
- 加入現有應用程式,加入可要求取得位置資訊存取權的支援功能,加入訂閱和取消訂閱的地點資訊。
- 在 Android 10 和 Android 11 上新增支援在前景位置存取位置資訊的機制,或在應用程式中使用時,就能新增支援。
軟硬體需求
- 執行程式碼的 Android Studio 3.4 或更新版本
- 執行 Android 10 和 11 開發人員預覽模式的裝置/模擬器
複製範例專案存放區
您可以從這個入門專案著手,盡快開始使用。如果您已安裝 Git,可以直接執行下列指令:
git clone https://github.com/googlecodelabs/while-in-use-location
建議您直接造訪 GitHub 頁面。
如果您沒有 Git,可以取得 ZIP 檔案專案:
匯入專案
開啟 Android Studio,從歡迎畫面中選取 [Open an existing Android Studio project] (開啟現有的 Android Studio 專案),然後開啟專案目錄。
專案載入後,系統也可能會顯示快訊,通知您 Git 不會追蹤所有本機變更。您可以按一下 [忽略]。(系統不會將任何變更推送回 Git 存放區。)
如果您開啟的是 Android 檢視模式,應該在專案視窗的左上方看到下圖。(如果您在「專案」檢視畫面中,必須展開專案才能查看相同內容)。
有兩個資料夾 (base
和 complete
),每個資料夾都稱為「模組」。
請注意,由於 Android Studio 首次會在背景編譯專案,因此可能需要幾秒鐘的處理時間。在這段期間,Android Studio 底部的狀態列會顯示下列訊息:
請等待 Android Studio 完成索引建立和專案建立程序,然後再變更程式碼。如此一來,Android Studio 就能擷取所有必要的元件。
如果系統顯示「語言變更以便重新載入並生效?」或類似內容,請選取 [是]。
瞭解範例專案
你已完成設定並在應用程式中要求位置資訊。請將「base
」模組設為起點。在每個步驟中,將程式碼加進 base
模組。完成這個程式碼研究室後,base
模組中的程式碼應與 complete
模組的內容相符。「complete
」模組可用於檢查作業,或者在您遇到問題時參考。
重要元素包括:
MainActivity
:允許使用者透過使用者介面存取裝置的位置資訊LocationService
:可訂閱和取消訂閱位置變更的服務,並在使用者離開應用程式的活動時,將應用程式做為前景 (僅透過通知) 宣傳。在這裡新增地點代碼。Util
:為Location
類別新增擴充功能函式,並將位置資訊儲存至SharedPreferences
(簡化的資料層)。
模擬器設定
如需瞭解如何設定 Android Emulator,請參閱在模擬器上執行一文。
執行起始方案
執行應用程式。
- 將 Android 裝置連接到電腦或啟動模擬器。(請確認裝置搭載的是 Android 10 以上版本)。
- 在工具列的下拉式選取器中,選取
base
設定,然後按一下 [Run]:
- 請注意,您的裝置會顯示下列應用程式:
您可能會發現輸出畫面中未顯示任何位置資訊。原因在於您尚未新增地點代碼。
概念
本程式碼研究室的重點在於如何接收位置更新,最終支援 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
,供您儲存在資料庫中。
您現在已經瞭解自己的操作基本知識,不妨先從程式碼著手!
本程式碼研究室著重在最常用的定位選項:「僅在使用應用程式時允許」。
如要接收位置更新,您的應用程式必須處於可見的活動或在前景執行 (有通知)。
權限
本程式碼研究室的目的是說明如何接收位置更新,而不是要求取得位置存取權,所以您已撰寫了根據權限設定的程式碼。如果已經知道,請略過此步驟。
以下是權限重點摘要 (此部分無須採取任何動作):
- 請在「
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().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)
if (locationResult?.lastLocation != null) {
// 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 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))
}
} else {
Log.d(TAG, "Location information isn't available.")
}
}
}
您在這裡建立的 LocationCallback
是 FusedLocationProviderClient
會在有可用的位置更新時呼叫的回呼。
在您的回呼中,首先會使用 LocationResult
物件取得最新的位置。之後,您可以使用本機廣播 (如啟用) 通知Activity
新位置,或更新Notification
如果此服務是在前景執行,Service
。
- 請仔細閱讀留言,以瞭解每個部分的用途。
訂閱位置變更
完成所有初始化作業之後,您必須將 FusedLocationProviderClient
告知對方您要接收更新。
- 在
base
模組的ForegroundOnlyLocationService.kt
檔案中搜尋Step 1.5, Subscribe to location changes
。 - 在留言後加入下列程式碼。
// TODO: Step 1.5, Subscribe to location changes.
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
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
類別。
執行應用程式
從 Android Studio 執行您的應用程式,並嘗試 [位置] 按鈕。
您應該會在輸出畫面中看到位置資訊。這是適用於 Android 9 的全功能應用程式。
本節將支援 Android 10。
您的應用程式已訂閱位置變更,因此無須進行太多操作。
實際上,您只需指定前景服務就用於定位。
目標 SDK 29
- 在
base
模組的build.gradle
檔案中搜尋TODO: Step 2.1, Target SDK 10
。 - 進行下列變更:
- 將
compileSdkVersion
設為29
。 - 將
buildToolsVersion
設為"29.0.3"
。 - 將
targetSdkVersion
設為29
。
您的程式碼應如下所示:
android {
// TODO: Step 2.1, Target Android 10.
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.example.android.whileinuselocation"
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
...
}
如果這麼做,系統會要求同步處理您的專案。按一下 [立即同步處理]。
之後,您的應用程式就已經準備好支援 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 位置的最佳做法。
執行應用程式
從 Android Studio 執行您的應用程式,並嘗試 [位置] 按鈕。
目前所有功能都和之前一樣,但現在可在 Android 10 上運作。如果您先前不接受位置存取權,系統現在應該會顯示權限畫面!
在這個部分,您會指定 Android 11。
好消息!除了 build.gradle
檔案以外,您不必修改任何檔案!
目標 SDK R
- 在
base
模組中,在build.gradle
檔案中搜尋TODO: Step 2.1, Target SDK
。 - 進行下列變更:
compileSdkVersion
至"android-R"
targetSdkVersion
至"R"
您的程式碼應如下所示:
android {
// TODO: Step 2.1, Target Android 10.
compileSdkVersion "android-R"
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.example.android.whileinuselocation"
minSdkVersion 26
targetSdkVersion "R"
versionCode 1
versionName "1.0"
}
...
}
如果這麼做,系統會要求同步處理您的專案。按一下 [立即同步處理]。
完成後,您的應用程式就可以支援 Android 11!
執行應用程式
從 Android Studio 執行您的應用程式,並嘗試按一下此按鈕。
目前所有功能都和之前一樣,但現在可在 Android 11 上運作。如果您先前不接受位置存取權,系統現在應該會顯示權限畫面!
只要按照這個程式碼研究室所示的方法檢查並要求位置存取權,您的應用程式就能成功追蹤其裝置位置的存取層級。
本頁列出與位置存取權相關的幾項重要最佳做法。如要進一步瞭解如何確保使用者安全無虞,請參閱應用程式權限最佳做法。
僅詢問所需權限
只在必要時要求權限。例如:
- 除非絕對必要,否則請勿在應用程式啟動時要求位置存取權。
- 如果您的應用程式指定 Android 10 以上版本,而且您提供了前景服務,請在資訊清單中宣告
foregroundServiceType
"location"
。 - 除非安全且更加透明地存取使用者位置資訊所述,否則請勿要求背景位置存取權。
如果未授予權限,則支援安全降級
為了維持良好的使用者體驗,請設計您的應用程式,讓應用程式能夠妥善處理下列情形:
- 您的應用程式無法存取位置資訊。
- 您的應用程式無法在背景執行時存取位置資訊。
您已經瞭解如何在 Android 中接收位置資訊更新,同時參考最佳做法!