创建和监控地理围栏

地理围栏将对用户当前位置的感知与对用户与可能感兴趣的位置的接近程度的感知相结合。要标记感兴趣的位置,您需要指定其纬度和经度。要调整位置的接近程度,您需要添加半径。纬度、经度和半径定义了一个地理围栏,在感兴趣的位置周围创建一个圆形区域或围栏。

您可以拥有多个活动的地理围栏,每个应用、每个设备用户最多可拥有 100 个。对于每个地理围栏,您可以要求位置服务向您发送进入和退出事件,或者您可以在地理围栏区域内指定一个等待持续时间,或停留,然后触发事件。您可以通过以毫秒为单位指定过期持续时间来限制任何地理围栏的持续时间。地理围栏过期后,位置服务会自动将其移除。

本课程将向您展示如何添加和移除地理围栏,然后使用BroadcastReceiver监听地理围栏转换。

注意:在 Wear 设备上,地理围栏 API 无法有效利用电量。我们不建议在 Wear 上使用这些 API。阅读节约电量和电池以获取更多信息。

设置地理围栏监控

请求地理围栏监控的第一步是请求必要的权限。要使用地理围栏,您的应用必须请求以下权限

要了解更多信息,请参阅有关如何请求位置权限的指南。

如果要使用BroadcastReceiver监听地理围栏转换,请添加一个指定服务名称的元素。此元素必须是 <application>元素的子元素

<application
   android:allowBackup="true">
   ...
   <receiver android:name=".GeofenceBroadcastReceiver"/>
<application/>

要访问位置 API,您需要创建 Geofencing 客户端的实例。要了解如何连接客户端

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 的构建器类来创建 Geofence 对象,并使用便捷类来添加它们,从而创建和添加地理围栏。此外,为了处理地理围栏转换发生时位置服务发送的意图,您可以定义一个PendingIntent,如本节所示。

注意:在单用户设备上,每个应用的地理围栏数量限制为 100 个。对于多用户设备,每个应用每个设备用户的地理围栏数量限制为 100 个。

创建地理围栏对象

首先,使用 Geofence.Builder创建地理围栏,设置地理围栏所需的半径、持续时间和转换类型。例如,要填充列表对象

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转换在设备进入地理围栏时触发,而 GEOFENCE_TRANSITION_EXIT转换在设备退出地理围栏时触发。指定 INITIAL_TRIGGER_ENTER告诉位置服务,如果设备已在地理围栏内,则应触发 GEOFENCE_TRANSITION_ENTER

在许多情况下,最好改为使用 INITIAL_TRIGGER_DWELL,它仅在用户在地理围栏内停留一段时间后才触发事件。这种方法有助于减少设备在短暂进入和退出地理围栏时产生的大量通知导致的“警报泛滥”。获得地理围栏最佳结果的另一种策略是设置 100 米的最小半径。这有助于考虑典型 Wi-Fi 网络的位置精度,也有助于减少设备功耗。

为地理围栏转换定义广播接收器

来自位置服务的Intent可以触发应用中的各种操作,但您不应该让它启动活动或片段,因为组件应该只在响应用户操作时才可见。在许多情况下,BroadcastReceiver是处理地理围栏转换的好方法。一个BroadcastReceiver在事件发生时(例如进入或离开地理围栏)获取更新,并且可以启动长时间运行的后台工作。

以下代码片段展示了如何定义一个启动BroadcastReceiverPendingIntent

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

处理地理围栏转换

当位置服务检测到用户已进入或退出地理围栏时,它会发送包含在您在添加地理围栏请求中包含的PendingIntent中的Intent。广播接收器(如GeofenceBroadcastReceiver)会注意到Intent被调用,然后可以从 Intent 中获取地理围栏事件,确定地理围栏转换类型,并确定哪个定义的地理围栏被触发。广播接收器可以指示应用开始执行后台工作,或者如果需要,发送通知作为输出。

注意:在 Android 8.0(API 级别 26)及更高版本中,如果应用在后台运行时正在监控地理围栏,则设备每隔几分钟就会响应一次地理围栏事件。要了解如何使您的应用适应这些响应限制,请参阅后台位置限制

以下代码片段展示了如何定义一个BroadcastReceiver,当发生地理围栏转换时,它会发布通知。当用户点击通知时,应用的主活动将出现。

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

通过PendingIntent检测到转换事件后,BroadcastReceiver获取地理围栏转换类型,并测试它是否为应用用于触发通知的事件之一——在本例中,为GEOFENCE_TRANSITION_ENTERGEOFENCE_TRANSITION_EXIT。然后,服务发送通知并记录转换详细信息。

停止地理围栏监控

在不再需要或不需要时停止地理围栏监控可以帮助节省设备的电池电量和 CPU 周期。您可以在用于添加和删除地理围栏的主活动中停止地理围栏监控;删除地理围栏会立即停止它。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 秒,则可能需要更长的时间才能收到警报)。

  • 对于用户花费大量时间的位置(例如家或工作地点),使用更大的地理围栏半径。虽然更大的半径不会直接降低功耗,但它会降低应用检查入口或出口的频率,从而有效降低整体功耗。

为您的地理围栏选择最佳半径

为了获得最佳效果,地理围栏的最小半径应设置为 100 到 150 米之间。当 Wi-Fi 可用时,位置精度通常在 20 到 50 米之间。当室内定位可用时,精度范围可以小到 5 米。除非您知道地理围栏内有室内定位可用,否则假设 Wi-Fi 定位精度约为 50 米。

当 Wi-Fi 定位不可用时(例如,当您在农村地区开车时),位置精度会下降。精度范围可以大到几百米到几公里。在这种情况下,您应该使用更大的半径创建地理围栏。

向用户解释您的应用为何使用地理围栏

因为您的应用在使用地理围栏时会在后台访问位置,请考虑您的应用如何为用户带来好处。向他们清楚地解释您的应用为什么需要此访问权限,以提高用户理解和透明度。

有关与位置访问(包括地理围栏)相关的最佳实践的更多信息,请参阅隐私最佳实践页面。

使用停留转换类型减少警报垃圾邮件

如果您在短暂经过地理围栏时收到大量警报,减少警报的最佳方法是使用 GEOFENCE_TRANSITION_DWELL类型的转换,而不是 GEOFENCE_TRANSITION_ENTER。这样,只有当用户在某个地理围栏内停留一段时间后,才会发送停留警报。您可以通过设置停留延迟来选择持续时间。

仅在需要时重新注册地理围栏

已注册的地理围栏保留在com.google.process.location进程中,该进程由com.google.android.gms包拥有。应用不需要执行任何操作来处理以下事件,因为系统会在这些事件后恢复地理围栏。

  • 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)或更高版本,则除非您的应用是系统应用或设备策略控制器 (DPC),否则您无法直接调用WifiManager.setEnabled()。相反,请使用设置面板

  • 您的地理围栏内没有可靠的网络连接。如果没有可靠的数据连接,则可能不会生成警报。这是因为地理围栏服务依赖于网络位置提供程序,而网络位置提供程序又需要数据连接。
  • 警报可能会延迟。地理围栏服务不会持续查询位置,因此在接收警报时预计会有一些延迟。通常延迟小于 2 分钟,即使设备一直在移动,延迟也更短。如果后台位置限制生效,则平均延迟约为 2-3 分钟。如果设备已静止一段时间,则延迟可能会增加(最多 6 分钟)。

其他资源

要详细了解地理围栏,请查看以下资料。

示例

示例应用,用于创建和监控地理围栏。