1. 准备工作
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 开发者预览版的设备/模拟器
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 将不会跟踪所有本地更改。您可以点击 Ignore。(您所做的任何更改都不会保存到 Git 代码库中。)
如果您采用的是 Android 视图,那么在项目窗口的左上角应该会看到类似下图所示的内容。(如果您采用的是 Project 视图,那么需要展开项目才能看到这些内容。)
有两个文件夹(base
和 complete
)。它们都称为一个“模块”。
请注意,首次打开项目时,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 模拟器,请参阅在模拟器上运行。
运行初始项目
运行应用。
- 将您的 Android 设备连接到计算机或启动模拟器。(请确保设备搭载的是 Android 10 或更高版本)。
- 在工具栏中,从下拉选择器中选择
base
配置,然后点击 Run:
- 请注意,您的设备会显示以下应用:
您可能会注意到,输出屏幕中没有显示任何位置信息。这是因为您尚未添加位置信息代码。
3. 正在添加位置
概念
此 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_PROVIDER
或 GPS_PROVIDER
的位置信息更新,您必须在 Android 清单文件中分别声明 ACCESS_COARSE_LOCATION
或 ACCESS_FINE_LOCATION
权限,以请求用户的权限。如果没有这些权限,您的应用将无法在运行时请求位置信息访问权限。
当您的应用在搭载 Android 10 或更高版本的设备上使用时,这些权限涵盖了仅限一次和仅在使用该应用时允许情形。
位置
应用可以通过 com.google.android.gms.location
软件包中的类访问一组受支持的位置信息服务。
查看主类:
FusedLocationProviderClient
- 这是位置信息框架的核心组件。创建后,您可以使用它请求位置信息更新并获取最近一次的已知位置。
LocationRequest
- 这是一个数据对象,其中包含请求的服务质量参数(更新间隔、优先级和准确性)。当您请求位置信息更新时,系统会将此信息传递给
FusedLocationProviderClient
。 LocationCallback
- 这用于在设备位置发生更改或无法再确定时接收通知。系统会向其传递一个
LocationResult
,您可以从中获取Location
以将其保存在数据库中。
现在,您已经基本了解自己要做什么了,开始编写代码吧!
4. 添加位置功能
此 Codelab 重点介绍最常见的位置信息选项:仅在使用该应用时允许。
要接收位置信息更新,您的应用必须具有可见的 activity 或在前台运行的服务(会显示通知)。
权限
此 Codelab 的目的是演示如何接收位置信息更新(而不是如何请求位置信息更新),因此我们已为您编写了权限方面的代码。如果您已经了解这部分代码,可以跳过。
以下是主要的权限(在这部分无需采取任何操作):
- 声明您在
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
对象获取最新位置。之后,您需要使用本地广播(如果有效)或更新 Notification
(如果此服务作为 Service
在前台运行),将新位置告知 Activity
。
- 仔细阅读注释,了解每个部分的作用。
订阅位置信息变更
现在,您已初始化所有内容,接下来需要告知 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 中的位置信息最佳实践,您的应用已经支持 Android 10 的“使用时”位置信息设置。
运行 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 设备位置信息策略
按照此 Codelab 中所示的方式检查和请求位置信息权限,您的应用可以成功跟踪其关于设备位置的访问权限级别。
本页面列出了与位置信息权限相关的一些关键最佳实践。有关如何使用户请参阅应用权限最佳做法。
仅请求您所需的权限
仅在需要时请求权限。例如:
- 请勿在应用启动时请求位置信息权限限(除非绝对有必要)。
- 如果您的应用以 Android 10 或更高版本为目标平台,并且您有前台服务,请在清单中声明
foregroundServiceType
为"location"
。 - 请勿请求后台位置信息权限,除非您有更安全、更透明的用户位置信息访问权限中所述的有效使用情形。
在未授予权限时支持优雅降级
为了保持良好的用户体验,请在设计应用时确保它可以优雅地处理以下情况:
- 应用无法访问位置信息。
- 应用在后台运行时无法访问位置信息。
8. 恭喜
您已了解如何在 Android 中接收位置信息更新,请记住这些最佳实践!