使用 Kotlin 在 Android 中接收位置信息更新

Android 10 和 11 可让用户更好地控制应用对其设备位置信息的访问权限。

当在 Android 11 上运行的应用请求位置信息访问权限时,用户有四个选择:

  • 始终允许
  • 仅在使用该应用时允许(在 Android 10 中)
  • 仅一次(在 Android 11 中)
  • 拒绝

Android 10

Android 11

在此 Codelab 中,您将学习如何接收位置信息更新,以及如何在任何 Android 版本(尤其是 Android 10 和 11)上支持位置信息。在此 Codelab 结束时,您应该会创建一个应用,它会遵循当前检索位置信息更新的最佳实践。

前提条件

您应执行的操作

  • 遵循在 Android 中使用位置信息功能的最佳实践。
  • 处理前台位置信息权限(当用户要求您的应用在使用期间访问设备位置信息)。
  • 修改现有应用,通过添加用于订阅和退订位置信息的代码,添加对请求位置信息访问权限的支持。
  • 向应用添加对 Android 10 和 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,以打开项目目录。

项目加载完成后,您可能还会看到一条提醒,指出 Git 将不会跟踪所有本地更改。您可以点击 Ignore。(您所做的任何更改都不会保存到 Git 代码库中。)

如果您采用的是 Android 视图,那么在项目窗口的左上角应该会看到类似下图所示的内容。(如果您采用的是 Project 视图,那么需要展开项目才能看到这些内容。)

有两个文件夹(basecomplete),每个都称为一个“模块”。

请注意,首次打开项目时,Android Studio 可能需要数秒时间在后台编译项目。在此期间,您会在 Android Studio 底部的状态栏中看到以下消息:

请等待 Android Studio 将项目编入索引并构建项目,然后再更改代码。这样,Android Studio 就可以提取所有必要的组件。

如果系统显示 Reload for language changes to take effect? 或类似提示,请选择 Yes

了解初始项目

您已经完成准备工作,可以在应用中请求获取位置信息了。请使用 base 模块作为起点。在每个步骤中,请将代码添加到 base 模块。完成此 Codelab 后,base 模块中的代码应与 complete 模块的内容一致。complete 模块可用于检查您的工作,或在您遇到问题时提供参考。

关键组件包括:

  • MainActivity - 供用户允许应用访问设备位置信息的界面
  • LocationService - 订阅和退订位置信息更改,并在用户离开应用的 activity 时自行提升为前台服务(会显示通知)的服务。您需要在此处添加位置信息代码。
  • Util - 为 Location 类添加扩展函数,并将位置信息保存在 SharedPreferences(简化的数据层)中。

模拟器设置

如需了解如何设置 Android 模拟器,请参阅在模拟器上运行

运行初始项目

运行应用。

  1. 将您的 Android 设备连接到计算机或启动模拟器。(请确保设备搭载的是 Android 10 或更高版本)。
  2. 在工具栏中,从下拉选择器中选择 base 配置,然后点击 Run


  1. 请注意,您的设备会显示以下应用:


您可能会注意到,输出屏幕中没有显示任何位置信息。这是因为您尚未添加位置信息代码。

概念

此 Codelab 将重点介绍如何接收位置信息更新,并最终支持 Android 10 和 Android 11。

但是,在开始编码之前,先回顾一下基础知识。

位置信息访问权限的类型

您可能还记得,本 Codelab 开头部分提到了四种不同的位置信息访问权限选项。我们来看一下它们的含义:

  • 仅在使用该应用时允许
  • 对于大多数应用,这是推荐的选项。此选项也称为“仅在使用时访问”或“仅限前台”访问,Android 10 中新增了此选项,它允许开发者仅在应用处于活跃状态时检索位置信息。如果出现以下任一情况,应用就被视为处于活动状态:
  • 有一个 activity 可见。
  • 有一项前台服务正在运行并持续显示通知。
  • 仅限这一次
  • 在 Android 11 中添加的选项,与仅在使用该应用时允许相同,但存在时间限制。有关详情,请参阅单次授权
  • 拒绝
  • 此选项会禁止访问位置信息。
  • 始终允许
  • 此选项会始终允许访问位置信息,但对于 Android 10 及更高版本,需要额外的权限。您还必须确保自己拥有有效的用例并遵守位置信息政策。此 Codelab 不涉及此选项,因为此用例极少见。不过,如果您有有效的用例,并希望了解如何正确处理始终访问位置信息情形(包括在后台访问位置信息),请参阅 LocationUpdatesBackgroundKotlin 示例

服务、前台服务和绑定

要完全支持仅在使用该应用时允许位置信息更新,您需要考虑用户离开您的应用的情况。如果您希望在这种情况下继续接收更新,则需要创建前台 Service 并将其与 Notification 相关联。

此外,如果您希望在应用可见时和用户离开应用时使用相同的 Service 请求位置信息更新,则需要将该 Service 绑定/取消绑定到相应界面元素。

由于此 Codelab 仅重点介绍如何获取位置信息更新,因此您可以在 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

现在,您已经基本了解自己要做什么了,开始编写代码吧!

此 Codelab 重点介绍最常见的位置信息选项:仅在使用该应用时允许

要接收位置信息更新,您的应用必须具有可见的 activity 或在前台运行的服务(会显示通知)。

权限

此 Codelab 的目的是演示如何接收位置信息更新(而不是如何请求位置信息更新),因此我们已为您编写了权限方面的代码。如果您已经了解这部分代码,可以跳过。

以下是权限方面的重点知识(这部分无需执行任何操作):

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

您在此处创建的 LocationCallback 是当有新的位置信息更新时,FusedLocationProviderClient 将调用的回调。

在回调中,您首先使用 LocationResult 对象获取最新位置。之后,您需要使用本地广播(如果有效)或更新 Notification(如果此服务作为 Service 在前台运行),将新位置告知 Activity

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

之后,系统会要求您同步项目。点击 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 中的位置信息最佳实践,您的应用已经支持 Android 10 的“使用时”位置信息设置。

运行应用

从 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"
   }
...
}

之后,系统会要求您同步项目。点击 Sync Now

之后,您的应用就支持 Android 11 了。

运行应用

从 Android Studio 运行您的应用,然后尝试点击相应按钮。

所有功能都应该像以前一样工作,只不过现在可以在 Android 11 上运行了。如果您之前未接受过关于位置信息的权限请求,您现在应该会看到权限屏幕!

按照此 Codelab 中所示的方式检查和请求位置信息权限,您的应用可以成功跟踪其关于设备位置的访问权限级别。

本页面列出了与位置信息权限相关的一些关键最佳实践。要详细了解如何确保用户数据安全,请参阅应用权限最佳实践

仅请求您所需的权限

仅在需要时请求权限。例如:

  • 请勿在应用启动时请求位置信息权限限(除非绝对有必要)。
  • 如果您的应用以 Android 10 或更高版本为目标平台,并且您具有前台服务,请在清单中将 foregroundServiceType 声明为 "location"
  • 除非您拥有更安全、更透明的用户位置信息访问权限中所述的有效用例,否则请勿请求后台位置信息权限。

在未授予权限时支持优雅降级

为了保持良好的用户体验,请在设计应用时确保它可以优雅地处理以下情况:

  • 应用无法访问位置信息。
  • 应用在后台运行时无法访问位置信息。

您已了解如何在 Android 中接收位置信息更新,请记住这些最佳实践!

了解更多内容