在 Android 中使用 Kotlin 接收位置資訊更新

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 檔案專案:

下載 zip

匯入專案

開啟 Android Studio,從歡迎畫面中選取 [Open an existing Android Studio project] (開啟現有的 Android Studio 專案),然後開啟專案目錄。

專案載入後,系統也可能會顯示快訊,通知您 Git 不會追蹤所有本機變更。您可以按一下 [忽略]。(系統不會將任何變更推送回 Git 存放區。)

如果您開啟的是 Android 檢視模式,應該在專案視窗的左上方看到下圖。(如果您在「專案」檢視畫面中,必須展開專案才能查看相同內容)。

有兩個資料夾 (basecomplete),每個資料夾都稱為「模組」。

請注意,由於 Android Studio 首次會在背景編譯專案,因此可能需要幾秒鐘的處理時間。在這段期間,Android Studio 底部的狀態列會顯示下列訊息:

請等待 Android Studio 完成索引建立和專案建立程序,然後再變更程式碼。如此一來,Android Studio 就能擷取所有必要的元件。

如果系統顯示「語言變更以便重新載入並生效?」或類似內容,請選取 []。

瞭解範例專案

你已完成設定並在應用程式中要求位置資訊。請將「base」模組設為起點。在每個步驟中,將程式碼加進 base 模組。完成這個程式碼研究室後,base 模組中的程式碼應與 complete 模組的內容相符。「complete」模組可用於檢查作業,或者在您遇到問題時參考。

重要元素包括:

  • MainActivity:允許使用者透過使用者介面存取裝置的位置資訊
  • LocationService:可訂閱和取消訂閱位置變更的服務,並在使用者離開應用程式的活動時,將應用程式做為前景 (僅透過通知) 宣傳。在這裡新增地點代碼。
  • Util:為 Location 類別新增擴充功能函式,並將位置資訊儲存至 SharedPreferences (簡化的資料層)。

模擬器設定

如需瞭解如何設定 Android Emulator,請參閱在模擬器上執行一文。

執行起始方案

執行應用程式。

  1. 將 Android 裝置連接到電腦或啟動模擬器。(請確認裝置搭載的是 Android 10 以上版本)。
  2. 在工具列的下拉式選取器中,選取 base 設定,然後按一下 [Run]


  1. 請注意,您的裝置會顯示下列應用程式:


您可能會發現輸出畫面中未顯示任何位置資訊。原因在於您尚未新增地點代碼。

概念

本程式碼研究室的重點在於如何接收位置更新,最終支援 Android 10 和 Android 11。

不過,在開始編寫程式碼之前,建議您先瞭解基本概念。

位置資訊存取權的類型

從程式碼研究室開始時,您可能會記得四個不同的位置存取權選項。請查看其意義:

  • 僅在使用該應用程式時允許
  • 大多數應用程式都建議使用這個選項。也稱為「使用中」或「僅限前景」存取,這個選項是在 Android 10 中加入,讓開發人員可以在應用程式使用期間擷取位置。如果符合下列任一情況,系統就會判定應用程式處於啟用狀態:
  • 顯示活動。
  • 前景服務正在執行中,且持續發出通知。
  • 僅執行一次
  • 在 Android 11 中,這個行為與僅在使用應用程式時允許相同,但在限定時間內。詳情請參閱一次性權限
  • 拒絕
  • 這個選項會禁止您存取位置資訊。
  • 一律允許
  • 這個選項可讓你隨時存取位置資訊,不過在 Android 10 以上版本中必須取得額外權限。此外,您必須確認自己具備有效用途,且遵守位置政策。在這個程式碼研究室中,您將無法涵蓋這個選項,因為這是罕見的使用案例。不過,如果您有有效的應用實例,並想瞭解如何妥善處理全天位置 (包括在背景存取位置資訊),請參閱 LocationUpdatesBackgroundKotlin 範例

服務、前景服務和繫結

如要徹底支援「僅在使用應用程式時允許」位置資訊更新,您必須考量使用者離開應用程式的時機。如果希望繼續收到這類更新,您必須建立前景 Service,並與 Notification 建立關聯。

此外,如果您想在應用程式可見同時讓使用者離開應用程式時,使用相同的 Service 要求位置更新,則必須對該 Service 繫結/繫結至 UI 元素。

由於這個程式碼研究室只用於接收位置更新資訊,因此您可以在 ForegroundOnlyLocationService.kt 類別中找到所有需要的程式碼。您可以瀏覽這門課程和 MainActivity.kt,瞭解兩者如何搭配運作。

如需詳細資訊,請參閱服務總覽繫結服務總覽

權限

如要透過 NETWORK_PROVIDERGPS_PROVIDER 接收位置更新資訊,您必須分別在 Android 資訊清單檔案中宣告 ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION 權限,藉此要求取得使用者的授權。如果沒有這些權限,應用程式就無法在執行階段要求位置資訊存取權。

這些權限涵蓋「僅限使用一次」和「僅在使用應用程式時允許」一節,而且您的應用程式使用的是搭載 Android 10 以上版本的裝置。

位置

您的應用程式可以透過 com.google.android.gms.location 套件中的課程,取得一組支援的定位服務。

查看主要課程:

  • FusedLocationProviderClient
  • 這是位置架構的核心元件。建立位置後,您可以使用它來要求位置更新並取得最近已知的位置。
  • LocationRequest
  • 這個資料物件包含要求的服務品質參數 (更新、優先順序和精確度的間隔)。您要求提供位置資訊時,系統會將這項資訊傳送到 FusedLocationProviderClient
  • LocationCallback
  • 這項資訊會在裝置位置變更,或已無法確認時收到通知。系統會傳送這個 LocationResult 給您的 Location,供您儲存在資料庫中。

您現在已經瞭解自己的操作基本知識,不妨先從程式碼著手!

本程式碼研究室著重在最常用的定位選項:「僅在使用應用程式時允許」。

如要接收位置更新,您的應用程式必須處於可見的活動或在前景執行 (有通知)。

權限

本程式碼研究室的目的是說明如何接收位置更新,而不是要求取得位置存取權,所以您已撰寫了根據權限設定的程式碼。如果已經知道,請略過此步驟。

以下是權限重點摘要 (此部分無須採取任何動作):

  1. 請在「AndroidManifest.xml.
    」中說明您有哪些權限。
  2. 在嘗試存取位置資訊之前,請先檢查使用者是否已授予您的應用程式權限。如果應用程式尚未取得權限,請要求存取權。
  3. 處理使用者的權限選擇。(您可以在 MainActivity.kt 中查看這個代碼)。

當您在 AndroidManifest.xmlMainActivity.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

  1. base 模組的 ForegroundOnlyLocationService.kt 檔案中搜尋 TODO: Step 1.3, Create a LocationRequest
  2. 在留言後加入下列程式碼。

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
}
  1. 請仔細閱讀各個註解,瞭解每個註解的運作方式。

初始化 LocationCallback

  1. base 模組的 ForegroundOnlyLocationService.kt 檔案中搜尋 TODO: Step 1.4, Initialize the LocationCallback
  2. 在留言後加入下列程式碼。
// 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.")
       }
   }
}

您在這裡建立的 LocationCallbackFusedLocationProviderClient 會在有可用的位置更新時呼叫的回呼。

在您的回呼中,首先會使用 LocationResult 物件取得最新的位置。之後,您可以使用本機廣播 (如啟用) 通知Activity 新位置,或更新Notification 如果此服務是在前景執行,Service

  1. 請仔細閱讀留言,以瞭解每個部分的用途。

訂閱位置變更

完成所有初始化作業之後,您必須將 FusedLocationProviderClient 告知對方您要接收更新。

  1. base 模組的 ForegroundOnlyLocationService.kt 檔案中搜尋 Step 1.5, Subscribe to location changes
  2. 在留言後加入下列程式碼。
// TODO: Step 1.5, Subscribe to location changes.
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())

requestLocationUpdates() 呼叫可讓 FusedLocationProviderClient 知道您想要接收位置更新資訊。

您可能注意到您先前定義的 LocationRequestLocationCallback。可讓 FusedLocationProviderClient 瞭解您要求的品質等級參數,以及更新時應呼叫哪些服務。最後,Looper 物件會指定回呼的執行緒。

此外,您可能也會注意到這段程式碼位於 try/catch 陳述式中。您的應用程式必須取得 SecurityException 的位置資訊存取權限,因此這種方法需要執行封鎖。

取消訂閱地點變更

如果應用程式不再需要存取位置資訊,就必須取消訂閱位置資訊更新。

  1. base 模組的 ForegroundOnlyLocationService.kt 檔案中搜尋 TODO: Step 1.6, Unsubscribe to location changes
  2. 在留言後加入下列程式碼。
// 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

  1. base 模組的 build.gradle 檔案中搜尋 TODO: Step 2.1, Target SDK 10
  2. 進行下列變更:
  1. compileSdkVersion 設為 29
  2. buildToolsVersion 設為 "29.0.3"
  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

  1. base 模組中,在 build.gradle 檔案中搜尋 TODO: Step 2.1, Target SDK
  2. 進行下列變更:
  1. compileSdkVersion"android-R"
  2. 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 中接收位置資訊更新,同時參考最佳做法!

瞭解詳情