创建和监控地理围栏

地理围栏可以感知用户当前的位置以及用户与其可能关注的地点之间的距离。要标记关注地点,请指定其经纬度。要调整与关注地点的邻近度,请添加半径。经纬度加半径可以定义一个地理围栏,即围绕关注地点创建一个圆形区域(即围栏)。

您可以有多个活动地理围栏,但需遵循以下限制:针对每个设备用户,每个应用的地理围栏数不超过 100 个。对于每个地理围栏,您可以要求位置信息服务向您发送进入和离开事件,或者您也可以指定在地理围栏区域内等待或停留多长时间会触发事件。您可以通过指定以毫秒为单位的有效期限,限制任何一个地理围栏的持续时间。当地理围栏过期后,位置信息服务会自动将其移除。

本课介绍如何添加和移除地理围栏,以及如何使用 BroadcastReceiver 监听地理围栏 transition 事件。

设置地理围栏监控

请求设置地理围栏监控的第一步就是请求必要的权限。为了使用地理围栏,您的应用必须请求 ACCESS_FINE_LOCATION 权限。如果您的应用面向 Android 10(API 级别 29)或更高版本,您还必须请求 ACCESS_BACKGROUND_LOCATION 权限。

为了请求所需的权限,您必须将相关权限作为 <manifest> 元素的子元素添加到应用清单中:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <!-- Required if your app targets Android 10 (API level 29) or higher -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    

若要使用 BroadcastReceiver 来监听地理围栏 transition 事件,请添加一个元素来指定服务名称。该元素必须是 <application> 元素的子元素:

    <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 在事件发生时(例如进入或离开地理围栏时)获得更新,并可以启动长时间运行的后台工作。

以下代码段展示了如何定义启动 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
                    // ...
                }
            });
    

处理地理围栏 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 = GeofenceErrorMessages.getErrorString(this,
                        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 = GeofenceErrorMessages.getErrorString(this,
                        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_ENTERGEOFENCE_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 提醒),请首先确保已按照本指南中的说明正确注册了地理围栏。

以下是提醒没有正常工作的一些可能原因:

  • 无法提供地理围栏内的准确定位,或者地理围栏太小。 在大多数设备上,地理围栏服务仅使用网络定位来触发地理围栏。该服务使用此方法的原因在于,网络定位的功耗更低,获取离散位置所需的时间更少,最重要的是,它可以在室内使用。
  • 设备关闭了 WLAN。 启用 WLAN 可以大幅提高定位准确性,因此如果关闭 WLAN,您的应用可能永远不会收到地理围栏提醒,这取决于地理围栏半径、设备机型或 Android 版本等设置。自 Android 4.3(API 级别 18)起,我们添加了“仅 WLAN 扫描模式”功能,该功能可让用户在停用 WLAN 后仍可获得良好的网络定位。一种很好的做法是,提示用户并向其提供启用 WLAN 或仅 WLAN 扫描模式的快捷方式(如果两者均被停用)。使用 SettingsClient 确保已针对实现最佳的位置检测,正确配置了设备的系统设置。

    注意:如果您的应用面向 Android 10(API 级别 29)或更高版本,则除非应用是系统应用或设备政策控制器 (DPC),否则您将无法直接调用 WifiManager.setEnabled()。请改用设置面板

  • 地理围栏内没有可靠的网络连接。 如果没有可靠的数据连接,则可能不会生成提醒。这是因为,地理围栏服务依赖于网络位置提供程序,而该程序需要有数据连接才能工作。
  • 提醒可能会延迟。 地理围栏服务不会持续地查询位置,因此接收提醒时可能会有一些延迟。延迟通常少于 2 分钟,设备移动时会更少。如果后台获取位置信息限制有效,则平均延迟为 2 到 3 分钟左右。如果设备长时间不移动,则延迟可能会增加(最长可达 6 分钟)。