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

1. 事前準備

Android 10 和 11 可讓使用者進一步控管自己的應用程式存取裝置位置資訊。

在 Android 11 上執行的應用程式要求位置資訊存取權時,使用者可選擇以下四種選項:

  • 一律允許
  • 僅在使用該應用程式時允許 (Android 10)
  • 僅限一次 (Android 11)
  • 拒絕

Android 10

6a1029175b467c77.png

Android 11

73d8cc88c5877c25.png

在本程式碼研究室中,您可以瞭解如何接收位置更新,以及如何在任何 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」檢視畫面中,必須展開專案才能看到這些項目)。

fa825dae96c5dc18.png

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

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

c2273e7835c0841a.png

請等到 Android Studio 完成為專案建立索引及建構專案後,再變更程式碼。這使 Android Studio 提取所有必要的元件。

如果畫面顯示「Reload for language changes to take effect?」或類似的提示,請選取「Yes」

瞭解範例專案

你已設定完成,可以在應用程式中要求取得位置資訊。請以 base 模組做為起點。在每個步驟中,將程式碼新增至 base 模組。完成本程式碼研究室後,base 模組中的程式碼應與 complete 模組的內容相符。complete 模組可用於檢查你的作品,在發生問題時也可以參考。

主要元件包括:

  • MainActivity:使用者可允許應用程式存取裝置位置資訊的使用者介面
  • LocationService:訂閱和取消訂閱位置變更的服務,在使用者離開應用程式活動時,主動升級為前景服務 (顯示通知)。請在這裡加入位置程式碼。
  • Util:為 Location 類別新增擴充功能函式,並將位置儲存在 SharedPreferences (簡化的資料層)。

模擬器設定

如要進一步瞭解如何設定 Android 模擬器,請參閱「在模擬器上執行」。

執行範例專案

執行應用程式。

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

99600e9d44527ab.png

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

99bf1dae46f99af3.png

您可能會注意到輸出畫面不會顯示任何位置資訊。因為您尚未加入地區代碼。

3. 正在新增位置資訊

概念

本程式碼研究室的重點,在於說明如何接收位置更新,最終支援 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 並儲存至資料庫。

您現在對自己的操作有基本的瞭解,可以開始編寫程式碼了!

4. 新增位置地圖項目

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

如要接收位置更新資訊,您的應用程式必須具有可見活動,或服務在前景執行 (同時顯示通知)。

權限

本程式碼研究室旨在說明如何接收位置更新通知,而非要求位置存取權,因此系統已為您編寫權限程式碼。如果您已瞭解內容,可以選擇略過。

以下是權限重點摘要 (這部分不需要採取任何行動):

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

        // 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

  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.getMainLooper())

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 類別。

執行 app

從 Android Studio 執行應用程式,並嘗試位置按鈕。

輸出畫面應會顯示位置資訊。這是適用於 Android 9 且功能齊全的應用程式。

2ae45c4e297e3681.png

d66089bfb532e993.png

5. 支援 Android 10

在本節中,您將新增對 Android 10 的支援。

您的應用程式已經訂閱了位置異動通知,因此您不需要執行許多工作。

事實上,您只需指定前景服務用於位置資訊用途即可。

目標 SDK 29

  1. base 模組中,在 build.gradle 檔案中搜尋 TODO: Step 2.1, Target Android 10 and then Android 11.
  2. 進行這些變更:
  3. 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」

153f70847e0ec320.png

之後,您的應用程式幾乎可以支援 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 中運作。如果您尚未接受位置的存取權限,現在應該會看到權限畫面!

6a1029175b467c77.png

c7c1d226e49a121.png

39a262b66a275f66.png

6. 支援 Android 11

在本節中,您將指定 Android 11。

好消息!除了 build.gradle 檔案以外,你不需要對任何檔案進行變更!

目標 SDK 11

  1. base 模組中,在 build.gradle 檔案中搜尋 TODO: Step 2.1, Target SDK
  2. 進行這些變更:
  3. compileSdkVersion30
  4. targetSdkVersion30

程式碼應如下所示:

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」

153f70847e0ec320.png

之後,您的應用程式就可以支援 Android 11 了!

執行 app

從 Android Studio 執行應用程式,然後嘗試點選按鈕。

所有功能應會照常運作,但現在可在 Android 11 上運作。如果您尚未接受位置的存取權限,現在應該會看到權限畫面!

73d8cc88c5877c25.png

cc98fac6e089bc4.png

7. Android 定位策略

只要按照本程式碼研究室所示的方式檢查並要求位置存取權,應用程式就能順利追蹤裝置位置的存取層級。

本頁列出幾項與位置存取權相關的重要最佳做法。進一步瞭解如何資料安全性,請參閱「應用程式權限最佳做法」。

只要求必要權限

只在需要時才要求權限。例如:

  • 除非絕對必要,否則請勿在應用程式啟動時要求位置存取權。
  • 如果應用程式指定 Android 10 以上版本,並提供前景服務,請在資訊清單中宣告 "location"foregroundServiceType
  • 除非有符合「使用者位置資訊更公開透明」一文所述的有效用途,否則請勿要求背景位置資訊存取權。

若未授予權限,則支援優雅降級功能

為維持良好的使用者體驗,設計應用程式時,請確保應用程式能夠妥善處理以下情況:

  • 您的應用程式沒有任何位置資訊存取權。
  • 您的應用程式無法在背景執行時存取位置資訊。

8. 恭喜

您已瞭解如何在 Android 中接收位置更新通知,並遵循最佳做法!

瞭解詳情