Android NDK开发详解用户位置信息之创建和监控地理围栏
)
地理围栏可以感知用户当前的位置以及用户与其可能关注的地点之间的距离。如需标记关注地点,请指定其经纬度。如需调整与关注地点的距离,请添加半径。经纬度加半径可以定义一个地理围栏,围绕关注地点创建一个圆形区域,即围栏。
您可以有多个活跃的地理围栏,但需遵循以下限制:针对每个设备用户,每个应用的地理围栏数不超过 100 个。对于每个地理围栏,您可以要求位置信息服务向您发送进入和离开事件,或者您也可以指定在地理围栏区域内等待或停留多长时间会触发事件。您可指定以毫秒为单位的有效期限,以限制任何一个地理围栏的持续时间。当地理围栏过期后,位置信息服务会自动将其移除。
本课介绍了如何添加和移除地理围栏,以及如何使用 BroadcastReceiver 监听地理围栏 transition 事件。
设置地理围栏监控
请求设置地理围栏监控的第一步就是请求必要的权限。为了使用地理围栏,您的应用必须请求以下权限:
ACCESS_FINE_LOCATION
如果应用以 Android 10(API 级别 29)或更高版本为目标平台,还需请求 ACCESS_BACKGROUND_LOCATION 权限
如需了解详情,请参阅有关如何请求位置信息权限的指南。
若要使用 BroadcastReceiver 监听地理围栏 transition 事件,请添加一个元素来指定服务名称。此元素必须是 元素的子元素:
<application
android:allowBackup="true">
...
<receiver android:name=".GeofenceBroadcastReceiver"/>
<application/>
如需访问地理位置 API,您需要创建地理围栏客户端的实例。了解如何连接客户端:
Kotlin
lateinit var geofencingClient: GeofencingClient
override fun onCreate(savedInstanceState: Bundle?) {
// ...
geofencingClient = LocationServices.getGeofencingClient(this)
}
Java
private GeofencingClient geofencingClient;
@Override
public void onCreate(Bundle savedInstanceState) {
// ...
geofencingClient = LocationServices.getGeofencingClient(this);
}
创建和添加地理围栏
您的应用需要创建和添加地理围栏,即使用地理位置 API 的 builder 类来创建地理围栏对象,再使用 convenience 类来添加这些对象。此外,为了处理发生地理围栏 transition 事件时从位置信息服务发送的 intent,您可以按照本部分中所述的方式定义 PendingIntent。
注意:在单用户设备上,每个应用的地理围栏数不能超过 100 个。对于多用户设备,针对每个设备用户,每个应用的地理围栏数不能超过 100 个。
创建地理围栏对象
首先,使用 Geofence.Builder 创建地理围栏,为地理围栏设置所需的半径、持续时间和 transition 事件类型。例如,填充列表对象:
Kotlin
geofenceList.add(Geofence.Builder()
// Set the request ID of the geofence. This is a string to identify this
// geofence.
.setRequestId(entry.key)
// Set the circular region of this geofence.
.setCircularRegion(
entry.value.latitude,
entry.value.longitude,
Constants.GEOFENCE_RADIUS_IN_METERS
)
// Set the expiration duration of the geofence. This geofence gets automatically
// removed after this period of time.
.setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
// Set the transition types of interest. Alerts are only generated for these
// transition. We track entry and exit transitions in this sample.
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
// Create the geofence.
.build())
Java
geofenceList.add(new Geofence.Builder()
// Set the request ID of the geofence. This is a string to identify this
// geofence.
.setRequestId(entry.getKey())
.setCircularRegion(
entry.getValue().latitude,
entry.getValue().longitude,
Constants.GEOFENCE_RADIUS_IN_METERS
)
.setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
Geofence.GEOFENCE_TRANSITION_EXIT)
.build());
本例从常量文件中提取数据。在实际操作中,应用可能会根据用户的位置动态创建地理围栏。
指定地理围栏和初始触发器
以下代码段使用 GeofencingRequest 类及其嵌套的 GeofencingRequestBuilder 类指定要监控的地理围栏并设置如何触发相关的地理围栏事件:
Kotlin
private fun getGeofencingRequest(): GeofencingRequest {
return GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofences(geofenceList)
}.build()
}
Java
private GeofencingRequest getGeofencingRequest() {
GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
builder.addGeofences(geofenceList);
return builder.build();
}
本示例展示了两个地理围栏触发器的使用。当设备进入地理围栏时触发 GEOFENCE_TRANSITION_ENTER transition 事件,当设备离开地理围栏时触发 GEOFENCE_TRANSITION_EXIT transition 事件。指定 INITIAL_TRIGGER_ENTER 将告知位置信息服务在设备进入地理围栏后应触发 GEOFENCE_TRANSITION_ENTER。
在许多情况下,可能使用 INITIAL_TRIGGER_DWELL 更可取,因为该触发器仅在用户在地理围栏内停留所定义的持续时间后才会触发事件。这种方法可以帮助减少因设备在短暂进入和离开地理围栏时收到大量通知而导致的“垃圾提醒”。另一种从地理围栏获得最佳结果的策略是将最小半径设置为 100 米。这有助于提升典型 Wi-Fi 网络下的定位准确度并降低设备功耗。
定义适用于地理围栏 transition 事件的广播接收器
位置信息服务发送的 Intent 可以在您的应用中触发各种操作,但是不应让它启动 activity 或 fragment,因为组件应该仅在响应用户操作时才对用户可见。在许多情况下,BroadcastReceiver 是处理地理围栏 transition 事件的理想方式。BroadcastReceiver 在事件发生时(例如进入或离开地理围栏时)获得更新,并可以启动长时间运行的后台工作。
以下代码段展示了如何定义启动 BroadcastReceiver 的 PendingIntent:
Kotlin
class MainActivity : AppCompatActivity() {
// ...
private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
// addGeofences() and removeGeofences().
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
Java
public class MainActivity extends AppCompatActivity {
// ...
private PendingIntent getGeofencePendingIntent() {
// Reuse the PendingIntent if we already have it.
if (geofencePendingIntent != null) {
return geofencePendingIntent;
}
Intent intent = new Intent(this, GeofenceBroadcastReceiver.class);
// We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
// calling addGeofences() and removeGeofences().
geofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.
FLAG_UPDATE_CURRENT);
return geofencePendingIntent;
}
添加地理围栏
如需添加地理围栏,请使用 GeofencingClient.addGeofences() 方法。提供 GeofencingRequest 对象和 PendingIntent。以下代码段展示了如何处理结果:
Kotlin
geofencingClient?.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
addOnSuccessListener {
// Geofences added
// ...
}
addOnFailureListener {
// Failed to add geofences
// ...
}
}
Java
geofencingClient.addGeofences(getGeofencingRequest(), getGeofencePendingIntent())
.addOnSuccessListener(this, new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
// Geofences added
// ...
}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// Failed to add geofences
// ...
}
});
处理地理围栏 transition 事件
当位置信息服务检测到用户进入或离开地理围栏时,它会发送您在添加地理围栏的请求中包含的 PendingIntent 中所含的 Intent。诸如 GeofenceBroadcastReceiver 等广播接收器会发现 Intent 被调用,然后从该 intent 中获取地理围栏事件,借此确定地理围栏 transition 事件的类型以及所触发的是哪一个已定义的地理围栏。广播接收器可以指示应用开始执行后台工作,或者视需要发送通知作为输出。
注意:在 Android 8.0(API 级别 26)及更高版本中,如果应用在后台运行时监测到地理围栏,设备会每隔几分钟响应一次地理围栏事件。如需了解如何使应用适应这些响应限制,请参阅后台位置信息限制。
以下代码段展示了如何定义在发生地理围栏 transition 事件时发布通知的 BroadcastReceiver。当用户点击通知时,将会显示该应用的主要 Activity:
Kotlin
class GeofenceBroadcastReceiver : BroadcastReceiver() {
// ...
override fun onReceive(context: Context?, intent: Intent?) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.errorCode)
Log.e(TAG, errorMessage)
return
}
// Get the transition type.
val geofenceTransition = geofencingEvent.geofenceTransition
// Test that the reported transition was of interest.
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER |
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
// Get the geofences that were triggered. A single event can trigger
// multiple geofences.
val triggeringGeofences = geofencingEvent.triggeringGeofences
// Get the transition details as a String.
val geofenceTransitionDetails = getGeofenceTransitionDetails(
this,
geofenceTransition,
triggeringGeofences
)
// Send notification and log the transition details.
sendNotification(geofenceTransitionDetails)
Log.i(TAG, geofenceTransitionDetails)
} else {
// Log the error.
Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
geofenceTransition))
}
}
}
Java
public class GeofenceBroadcastReceiver extends BroadcastReceiver {
// ...
protected void onReceive(Context context, Intent intent) {
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
if (geofencingEvent.hasError()) {
String errorMessage = GeofenceStatusCodes
.getStatusCodeString(geofencingEvent.getErrorCode());
Log.e(TAG, errorMessage);
return;
}
// Get the transition type.
int geofenceTransition = geofencingEvent.getGeofenceTransition();
// Test that the reported transition was of interest.
if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {
// Get the geofences that were triggered. A single event can trigger
// multiple geofences.
List<Geofence> triggeringGeofences = geofencingEvent.getTriggeringGeofences();
// Get the transition details as a String.
String geofenceTransitionDetails = getGeofenceTransitionDetails(
this,
geofenceTransition,
triggeringGeofences
);
// Send notification and log the transition details.
sendNotification(geofenceTransitionDetails);
Log.i(TAG, geofenceTransitionDetails);
} else {
// Log the error.
Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
geofenceTransition));
}
}
}
BroadcastReceiver 在通过 PendingIntent 检测到地理围栏 transition 事件后,会获取该事件的类型,并测试其是否属于应用用于触发通知的某一种事件,在此情况下为 GEOFENCE_TRANSITION_ENTER 或 GEOFENCE_TRANSITION_EXIT。然后,该服务会发送通知并记录地理围栏 transition 事件的详细信息。
停止监控地理围栏
在不需要时停止监控地理围栏,有助于节省设备的电量和 CPU 周期。您可以在用于添加和移除地理围栏的主 activity 中停止监控地理围栏。移除地理围栏会立即停止对它的监控。该 API 提供两种方法来移除地理围栏:通过请求 ID 移除地理围栏,或者移除与指定 PendingIntent 关联的地理围栏。
以下代码段通过 PendingIntent 移除地理围栏,以后当设备进入或离开先前添加的地理围栏时,将不会再发送任何通知:
Kotlin
geofencingClient?.removeGeofences(geofencePendingIntent)?.run {
addOnSuccessListener {
// Geofences removed
// ...
}
addOnFailureListener {
// Failed to remove geofences
// ...
}
}
Java
geofencingClient.removeGeofences(getGeofencePendingIntent())
.addOnSuccessListener(this, new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
// Geofences removed
// ...
}
})
.addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// Failed to remove geofences
// ...
}
});
您可以将地理围栏与其他位置感知功能(例如定期位置信息更新)结合使用。如需了解详情,请参阅本课程中的其他课程。
使用地理围栏最佳做法
本部分介绍关于将地理围栏与 Android 的地理位置 API 结合使用的建议。
降低功耗
您可以使用以下技巧来优化使用地理围栏功能的应用的功耗:
将通知响应时间设置为较高的值。这样做可以增加地理围栏提醒的延迟时间,从而降低功耗。例如,如果将响应时间值设置为 5 分钟,应用就会每 5 分钟才检查一次进入或离开提醒。设置较低的值并不一定意味着用户会在该时间段内收到通知。例如,如果您将该值设置为 5 秒,用户可能需要比这长一点的时间才会收到提醒。
对于家中或工作场所等用户需要度过大量时间的位置,请使用较大的地理围栏半径。虽然使用较大的半径并不会直接降低功耗,但会降低应用检查进入或离开事件的频率,从而实际起到降低总体功耗的作用。
为地理围栏选择最佳半径
为了达到最佳效果,地理围栏的最小半径应设置在 100 到 150 米之间。当 Wi-Fi 可用时,定位准确度通常在 20 到 50 米之间。当有室内位置时,准确度可精确到 5 米。除非您知道地理围栏内有室内位置,否则请假定 Wi-Fi 网络下的定位准确度为 50 米左右。
当无法使用 Wi-Fi 定位时(例如在乡村地区行驶时),定位准确度会降低。准确度可以低至几百米至几千米。在这种情况下,应该使用较大的半径来创建地理围栏。
向用户说明您的应用为何使用地理围栏
由于您的应用会在您使用地理围栏时在后台访问位置信息,因此请考虑应用可为用户带来哪些好处。向用户清楚说明为何您的应用需要这样的权限,帮助他们加深理解并提高透明度。
如需详细了解与位置信息访问权限(包括地理围栏)相关的最佳做法,请参阅隐私设置最佳做法页面。
使用“dwell”transition 类型来减少不必要的提醒
如果您在短暂驶过地理围栏时收到大量提醒,减少这种提醒的最佳方法是使用 GEOFENCE_TRANSITION_DWELL 而非 GEOFENCE_TRANSITION_ENTER transition 类型。这样,只有当用户在地理围栏内停留的时间达到指定的一段时间后,才会发送停留提醒。您可以通过设置停留延迟时间来选择持续时间。
仅在需要时才重新注册地理围栏
已注册的地理围栏保存在 com.google.android.gms 软件包持有的 com.google.process.location 进程中。应用无需执行任何操作来处理以下事件,因为系统会在这些事件后恢复地理围栏:
Google Play 服务升级。
Google Play 服务因资源限制被系统终止并重启。
定位进程崩溃。
如果应用在以下事件发生后仍然需要地理围栏,则必须重新注册地理围栏,因为系统在这些情况下无法恢复地理围栏:
设备重启。应用应监听设备的启动完成操作,然后重新注册所需的地理围栏。
应用被卸载并重新安装。
应用数据被清除。
Google Play 服务数据被清除。
应用收到 GEOFENCE_NOT_AVAILABLE 提醒。这通常发生在 NLP(Android 的网络位置信息提供程序)被停用后。
地理围栏进入事件问题排查
如果设备进入地理围栏时未触发地理围栏(未触发 GEOFENCE_TRANSITION_ENTER 提醒),请首先确保已按照本指南中的说明正确注册了地理围栏。
以下是提醒没有正常工作的一些可能原因:
无法提供地理围栏内的准确定位,或者地理围栏太小。在大多数设备上,地理围栏服务仅使用网络定位来触发地理围栏。该服务使用此方法的原因在于,网络定位的功耗更低,获取离散位置所需的时间更少,最重要的是,它可以在室内使用。
设备关闭了 Wi-Fi。启用 Wi-Fi 可以大幅提高定位准确度,因此如果关闭 Wi-Fi,您的应用可能永远不会收到地理围栏提醒,具体情况取决于地理围栏半径、设备机型或 Android 版本等若干设置。自 Android 4.3(API 级别 18)起,我们添加了“仅 Wi-Fi 扫描模式”功能,该功能可让用户在停用 Wi-Fi 后仍可获得良好的网络定位。最好是提示用户并向其提供启用 Wi-Fi 或仅 Wi-Fi 扫描模式的快捷方式(如果两者均被停用)。使用 SettingsClient 确保正确配置设备的系统设置,实现最佳的位置检测效果。
注意:如果您的应用以 Android 10(API 级别 29)或更高版本为目标平台,您无法直接调用 WifiManager.setEnabled(),除非您的应用为系统应用或设备政策控制器 (DPC)。请改用设置面板。
地理围栏内没有可靠的网络连接。如果没有可靠的数据连接,可能不会生成提醒。这是因为,地理围栏服务依赖于网络位置信息提供程序,而该程序需要有数据连接才能工作。
提醒可能会延迟。地理围栏服务不会持续地查询位置信息,因此接收提醒时可能会有一些延迟。延迟通常不到 2 分钟,设备移动时会更少。如果后台位置信息限制有效,平均延迟为 2 到 3 分钟左右。如果设备长时间不移动,延迟可能会增加(最长可达 6 分钟)。
其他资源
如需详细了解地理围栏,请查看以下资料:
示例
这是关于创建和监控地理围栏的示例应用。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2023-07-04。