首页 > 代码库 > 【Android Developers Training】 106. 创建并检测地理围栏

【Android Developers Training】 106. 创建并检测地理围栏

注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接:http://developer.android.com/training/location/geofencing.html


地理围栏可以将用户当前地点信息和周围的地点信息相结合,它其实是用户接近潜在的感兴趣的地点的程度。要标记一个感兴趣的地点,你需要指定它的经纬度。要调整接近的位置,你还需要添加一个半径。经纬度和半径加起来就成为了一个地理围栏。你可以同一时间有多个激活的地理围栏。

定位服务将一个地理围栏看做是一块面积而不是点和距离。这就可以当用户进入或离开地理围栏时检测到。对于每一个地理围栏,你可以让定位服务向你发送进入事件或离开事件或者都发送。你还可以限制地理围栏的持续时间,方法是定义一个有效期(以毫秒为单位)。当地理围栏过期后,定位服务会自动移除它。


一). 请求地理围栏监测

请求地理围栏监测的第一步是申请必需的权限。要使用地理围栏,你的应用必须申请ACCESS_FINE_LOCATION。要申请这一权限,将下列元素添加为<manifest>标签的子标签:

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

检查Google Play服务

位置服务是Google Play服务APK的其中一部分。由于用户设备的状态时难以预料的,你应该一直在你尝试连接定位服务之前,检查APK是否已经安装。要检查APK是否安装,可以调用GooglePlayServicesUtil.isGooglePlayServicesAvailable(),它会返回一个整形的结果码,其含义可以参阅:ConnectionResult。如果你遇到了一个错误,可以调用GooglePlayServicesUtil.getErrorDialog(),来获取一个本地的对话框,引导用户执行正确地行为,之后将这一对话框显示在一个DialogFragment上。这一对话框可能允许用户解决当前的问题,此时Google Play服务会发回一个结果到你的activity中。要处理这一结果,需要覆写onActivityResult()方法。

Note:

要使你的应用可以兼容1.6及以后版本的系统,显示DialogFragment的activity必须是FragmentActivity的子类,而非Activity。使用FragmentActivity还可以允许你调用getSupportFragmentManager()方法来显示DialogFragment

由于你一直需要在你的代码多个地方检查Google Play服务,所以应该定义一个方法将检查行为进行封装,之后在每次连接尝试之前进行检查。下面的代码片段包含了检查Google Play服务所需要的代码:

public class MainActivity extends FragmentActivity {
    ...
    // Global constants
    /*
     * Define a request code to send to Google Play services
     * This code is returned in Activity.onActivityResult
     */
    private final static int
            CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;
    ...
    // Define a DialogFragment that displays the error dialog
    public static class ErrorDialogFragment extends DialogFragment {
        // Global field to contain the error dialog
        private Dialog mDialog;
        ...
        // Default constructor. Sets the dialog field to null
        public ErrorDialogFragment() {
            super();
            mDialog = null;
        }
        ...
        // Set the dialog to display
        public void setDialog(Dialog dialog) {
            mDialog = dialog;
        }
        ...
        // Return a Dialog to the DialogFragment.
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return mDialog;
        }
        ...
    }
    ...
    /*
     * Handle results returned to the FragmentActivity
     * by Google Play services
     */
     @Override
    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        // Decide what to do based on the original request code
        switch (requestCode) {
            ...
            case CONNECTION_FAILURE_RESOLUTION_REQUEST :
            /*
             * If the result code is Activity.RESULT_OK, try
             * to connect again
             */
                switch (resultCode) {
                    ...
                    case Activity.RESULT_OK :
                    /*
                     * Try the request again
                     */
                    ...
                    break;
                }
            ...
        }
        ...
    }
    ...
    private boolean servicesConnected() {
        // Check that Google Play services is available
        int resultCode =
                GooglePlayServicesUtil.
                        isGooglePlayServicesAvailable(this);
        // If Google Play services is available
        if (ConnectionResult.SUCCESS == resultCode) {
            // In debug mode, log the status
            Log.d("Geofence Detection",
                    "Google Play services is available.");
            // Continue
            return true;
        // Google Play services was not available for some reason
        } else {
            // Get the error code
            int errorCode = connectionResult.getErrorCode();
            // Get the error dialog from Google Play services
            Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
                    errorCode,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);

            // If Google Play services can provide an error dialog
            if (errorDialog != null) {
                // Create a new DialogFragment for the error dialog
                ErrorDialogFragment errorFragment =
                        new ErrorDialogFragment();
                // Set the dialog in the DialogFragment
                errorFragment.setDialog(errorDialog);
                // Show the error dialog in the DialogFragment
                errorFragment.show(
                        getSupportFragmentManager(),
                        "Geofence Detection");
            }
        }
    }
    ...
}

在后续章节的代码片段中,都会调用这一方法来验证是否可获取Google Play服务。

要使用地理围栏,首先定义你想要监测的地理围栏。虽然你经常要将地理围栏信息保存到一个本地的数据库或者从网络上下载下来,你需要将一个地理围栏发送给定位服务作为一个Geofence的实例(通过Geofence.Builder创建的)。每一个对象包含下列信息:

经纬度和半径:

给地理围栏定义一个圆形区域。使用经纬度标记一个感兴趣的地点,并且使用半径来调整当用户具体该地点多近后地理围栏会被检测到。半径越大,用户接近地理围栏时,激活它的可能性就越高。例如,如果一个应用提供了一个大半径的地理围栏,当用户回家时可以自动打开房间里的灯。由于半径设的太大,很有可能用户离开之后灯还是亮着的。

有效期:

设置地理围栏的有效期。一旦超过了有效期,定位服务将会删除该地理围栏。在大多数情况下,你应该指定一个有效期,但你也可能希望为用户的屋子或者工作地点的地理围栏长期保留。

过度类型:

当用户进入了地理围栏的范围(“进入”)以及当用于离开了此范围(“离开”),定位服务可以检测到这两个类型之一,或者两者都检测到。

地理围栏ID:

一个和地理围栏一起保存的字符串。你应该让这个值保持唯一,所以你可以使用它从定位服务中移除一个地理围栏。

定义一个地理围栏存储

一个地理围栏应用需要读写地理围栏数据以持久化数据。你不应该使用Geofence对象来做这件事情;相反的,使用诸如数据库等存储技术来保存相关的数据是比较好的。

作为一个存储数据的例子,下面的代码片段定义了两个类,它们使用应用的SharedPreferences实例持久化数据。类SimpleGeofence,是一个类似于数据库记录的类,它以一个“稀疏”的形式保存一个单一的Geofence对象。类SimpleGeofenceStore类似于一个数据库,它向SharedPreferences实例读写SimpleGeofence数据。

public class MainActivity extends FragmentActivity {
    ...
    /**
     * A single Geofence object, defined by its center and radius.
     */
    public class SimpleGeofence {
            // Instance variables
            private final String mId;
            private final double mLatitude;
            private final double mLongitude;
            private final float mRadius;
            private long mExpirationDuration;
            private int mTransitionType;

        /**
         * @param geofenceId The Geofence‘s request ID
         * @param latitude Latitude of the Geofence‘s center.
         * @param longitude Longitude of the Geofence‘s center.
         * @param radius Radius of the geofence circle.
         * @param expiration Geofence expiration duration
         * @param transition Type of Geofence transition.
         */
        public SimpleGeofence(
                String geofenceId,
                double latitude,
                double longitude,
                float radius,
                long expiration,
                int transition) {
            // Set the instance fields from the constructor
            this.mId = geofenceId;
            this.mLatitude = latitude;
            this.mLongitude = longitude;
            this.mRadius = radius;
            this.mExpirationDuration = expiration;
            this.mTransitionType = transition;
        }
        // Instance field getters
        public String getId() {
            return mId;
        }
        public double getLatitude() {
            return mLatitude;
        }
        public double getLongitude() {
            return mLongitude;
        }
        public float getRadius() {
            return mRadius;
        }
        public long getExpirationDuration() {
            return mExpirationDuration;
        }
        public int getTransitionType() {
            return mTransitionType;
        }
        /**
         * Creates a Location Services Geofence object from a
         * SimpleGeofence.
         *
         * @return A Geofence object
         */
        public Geofence toGeofence() {
            // Build a new Geofence object
            return new Geofence.Builder()
                    .setRequestId(getId())
                    .setTransitionTypes(mTransitionType)
                    .setCircularRegion(
                            getLatitude(), getLongitude(), getRadius())
                    .setExpirationDuration(mExpirationDuration)
                    .build();
        }
    }
    ...
    /**
     * Storage for geofence values, implemented in SharedPreferences.
     */
    public class SimpleGeofenceStore {
        // Keys for flattened geofences stored in SharedPreferences
        public static final String KEY_LATITUDE =
                "com.example.android.geofence.KEY_LATITUDE";
        public static final String KEY_LONGITUDE =
                "com.example.android.geofence.KEY_LONGITUDE";
        public static final String KEY_RADIUS =
                "com.example.android.geofence.KEY_RADIUS";
        public static final String KEY_EXPIRATION_DURATION =
                "com.example.android.geofence.KEY_EXPIRATION_DURATION";
        public static final String KEY_TRANSITION_TYPE =
                "com.example.android.geofence.KEY_TRANSITION_TYPE";
        // The prefix for flattened geofence keys
        public static final String KEY_PREFIX =
                "com.example.android.geofence.KEY";
        /*
         * Invalid values, used to test geofence storage when
         * retrieving geofences
         */
        public static final long INVALID_LONG_VALUE = http://www.mamicode.com/-999l;
        public static final float INVALID_FLOAT_VALUE = http://www.mamicode.com/-999.0f;
        public static final int INVALID_INT_VALUE = http://www.mamicode.com/-999;
        // The SharedPreferences object in which geofences are stored
        private final SharedPreferences mPrefs;
        // The name of the SharedPreferences
        private static final String SHARED_PREFERENCES =
                "SharedPreferences";
        // Create the SharedPreferences storage with private access only
        public SimpleGeofenceStore(Context context) {
            mPrefs =
                    context.getSharedPreferences(
                            SHARED_PREFERENCES,
                            Context.MODE_PRIVATE);
        }
        /**
         * Returns a stored geofence by its id, or returns null
         * if it‘s not found.
         *
         * @param id The ID of a stored geofence
         * @return A geofence defined by its center and radius. See
         */
        public SimpleGeofence getGeofence(String id) {
            /*
             * Get the latitude for the geofence identified by id, or
             * INVALID_FLOAT_VALUE if it doesn‘t exist
             */
            double lat = mPrefs.getFloat(
                    getGeofenceFieldKey(id, KEY_LATITUDE),
                    INVALID_FLOAT_VALUE);
            /*
             * Get the longitude for the geofence identified by id, or
             * INVALID_FLOAT_VALUE if it doesn‘t exist
             */
            double lng = mPrefs.getFloat(
                    getGeofenceFieldKey(id, KEY_LONGITUDE),
                    INVALID_FLOAT_VALUE);
            /*
             * Get the radius for the geofence identified by id, or
             * INVALID_FLOAT_VALUE if it doesn‘t exist
             */
            float radius = mPrefs.getFloat(
                    getGeofenceFieldKey(id, KEY_RADIUS),
                    INVALID_FLOAT_VALUE);
            /*
             * Get the expiration duration for the geofence identified
             * by id, or INVALID_LONG_VALUE if it doesn‘t exist
             */
            long expirationDuration = mPrefs.getLong(
                    getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION),
                    INVALID_LONG_VALUE);
            /*
             * Get the transition type for the geofence identified by
             * id, or INVALID_INT_VALUE if it doesn‘t exist
             */
            int transitionType = mPrefs.getInt(
                    getGeofenceFieldKey(id, KEY_TRANSITION_TYPE),
                    INVALID_INT_VALUE);
            // If none of the values is incorrect, return the object
            if (
                lat != GeofenceUtils.INVALID_FLOAT_VALUE &&
                lng != GeofenceUtils.INVALID_FLOAT_VALUE &&
                radius != GeofenceUtils.INVALID_FLOAT_VALUE &&
                expirationDuration !=
                        GeofenceUtils.INVALID_LONG_VALUE &&
                transitionType != GeofenceUtils.INVALID_INT_VALUE) {

                // Return a true Geofence object
                return new SimpleGeofence(
                        id, lat, lng, radius, expirationDuration,
                        transitionType);
            // Otherwise, return null.
            } else {
                return null;
            }
        }
        /**
         * Save a geofence.
         * @param geofence The SimpleGeofence containing the
         * values you want to save in SharedPreferences
         */
        public void setGeofence(String id, SimpleGeofence geofence) {
            /*
             * Get a SharedPreferences editor instance. Among other
             * things, SharedPreferences ensures that updates are atomic
             * and non-concurrent
             */
            Editor editor = mPrefs.edit();
            // Write the Geofence values to SharedPreferences
            editor.putFloat(
                    getGeofenceFieldKey(id, KEY_LATITUDE),
                    (float) geofence.getLatitude());
            editor.putFloat(
                    getGeofenceFieldKey(id, KEY_LONGITUDE),
                    (float) geofence.getLongitude());
            editor.putFloat(
                    getGeofenceFieldKey(id, KEY_RADIUS),
                    geofence.getRadius());
            editor.putLong(
                    getGeofenceFieldKey(id, KEY_EXPIRATION_DURATION),
                    geofence.getExpirationDuration());
            editor.putInt(
                    getGeofenceFieldKey(id, KEY_TRANSITION_TYPE),
                    geofence.getTransitionType());
            // Commit the changes
            editor.commit();
        }
        public void clearGeofence(String id) {
            /*
             * Remove a flattened geofence object from storage by
             * removing all of its keys
             */
            Editor editor = mPrefs.edit();
            editor.remove(getGeofenceFieldKey(id, KEY_LATITUDE));
            editor.remove(getGeofenceFieldKey(id, KEY_LONGITUDE));
            editor.remove(getGeofenceFieldKey(id, KEY_RADIUS));
            editor.remove(getGeofenceFieldKey(id,
                    KEY_EXPIRATION_DURATION));
            editor.remove(getGeofenceFieldKey(id, KEY_TRANSITION_TYPE));
            editor.commit();
        }
        /**
         * Given a Geofence object‘s ID and the name of a field
         * (for example, KEY_LATITUDE), return the key name of the
         * object‘s values in SharedPreferences.
         *
         * @param id The ID of a Geofence object
         * @param fieldName The field represented by the key
         * @return The full key name of a value in SharedPreferences
         */
        private String getGeofenceFieldKey(String id,
                String fieldName) {
            return KEY_PREFIX + "_" + id + "_" + fieldName;
        }
    }
    ...
}

创建地理围栏对象

下面的代码片段使用SimpleGeofenceSimpleGeofenceStore类从UI中获取地理围栏数据,把这些对象存储在一个SimpleGeofenceStore对象中,之后创建Geofence对象:

public class MainActivity extends FragmentActivity {
    ...
    /*
     * Use to set an expiration time for a geofence. After this amount
     * of time Location Services will stop tracking the geofence.
     */
    private static final long SECONDS_PER_HOUR = 60;
    private static final long MILLISECONDS_PER_SECOND = 1000;
    private static final long GEOFENCE_EXPIRATION_IN_HOURS = 12;
    private static final long GEOFENCE_EXPIRATION_TIME =
            GEOFENCE_EXPIRATION_IN_HOURS *
            SECONDS_PER_HOUR *
            MILLISECONDS_PER_SECOND;
    ...
    /*
     * Handles to UI views containing geofence data
     */
    // Handle to geofence 1 latitude in the UI
    private EditText mLatitude1;
    // Handle to geofence 1 longitude in the UI
    private EditText mLongitude1;
    // Handle to geofence 1 radius in the UI
    private EditText mRadius1;
    // Handle to geofence 2 latitude in the UI
    private EditText mLatitude2;
    // Handle to geofence 2 longitude in the UI
    private EditText mLongitude2;
    // Handle to geofence 2 radius in the UI
    private EditText mRadius2;
    /*
     * Internal geofence objects for geofence 1 and 2
     */
    private SimpleGeofence mUIGeofence1;
    private SimpleGeofence mUIGeofence2;
    ...
    // Internal List of Geofence objects
    List<Geofence> mGeofenceList;
    // Persistent storage for geofences
    private SimpleGeofenceStore mGeofenceStorage;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Instantiate a new geofence storage area
        mGeofenceStorage = new SimpleGeofenceStore(this);

        // Instantiate the current List of geofences
        mCurrentGeofences = new ArrayList<Geofence>();
    }
    ...
    /**
     * Get the geofence parameters for each geofence from the UI
     * and add them to a List.
     */
    public void createGeofences() {
        /*
         * Create an internal object to store the data. Set its
         * ID to "1". This is a "flattened" object that contains
         * a set of strings
         */
        mUIGeofence1 = new SimpleGeofence(
                "1",
                Double.valueOf(mLatitude1.getText().toString()),
                Double.valueOf(mLongitude1.getText().toString()),
                Float.valueOf(mRadius1.getText().toString()),
                GEOFENCE_EXPIRATION_TIME,
                // This geofence records only entry transitions
                Geofence.GEOFENCE_TRANSITION_ENTER);
        // Store this flat version
        mGeofenceStorage.setGeofence("1", mUIGeofence1);
        // Create another internal object. Set its ID to "2"
        mUIGeofence2 = new SimpleGeofence(
                "2",
                Double.valueOf(mLatitude2.getText().toString()),
                Double.valueOf(mLongitude2.getText().toString()),
                Float.valueOf(mRadius2.getText().toString()),
                GEOFENCE_EXPIRATION_TIME,
                // This geofence records both entry and exit transitions
                Geofence.GEOFENCE_TRANSITION_ENTER |
                Geofence.GEOFENCE_TRANSITION_EXIT);
        // Store this flat version
        mGeofenceStorage.setGeofence(2, mUIGeofence2);
        mGeofenceList.add(mUIGeofence1.toGeofence());
        mGeofenceList.add(mUIGeofence2.toGeofence());
    }
    ...
}

除了你希望监测的存储Geofence对象的List,你还需要向定位服务提供一个Intent,当监测到地理围栏转换的时候会将它发送给你的应用。

为地理围栏转换定义一个Intent

从定位服务发送的Intent可以激活你应用中的多个行为,但是你不应该让它启动一个activity或者fragment,因为组件只有在用户行为的出发条件下变的向用户可见才行。在很多情况下,用一个IntentService来处理intent是一个不错的方式。一个IntentService可以发布一个通知,在后台执行一个长时间运作的任务,将intent发送给其它服务,或者发送一个广播intent。下面的代码片段展示了如何定义一个PendingIntent来启动一个IntentService:

public class MainActivity extends FragmentActivity {
    ...
    /*
     * Create a PendingIntent that triggers an IntentService in your
     * app when a geofence transition occurs.
     */
    private PendingIntent getTransitionPendingIntent() {
        // Create an explicit Intent
        Intent intent = new Intent(this,
                ReceiveTransitionsIntentService.class);
        /*
         * Return the PendingIntent
         */
        return PendingIntent.getService(
                this,
                0,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }
    ...
}

要向定位服务请求监测地理围栏,所需的代码现在你已经都有了。

发送监测请求

发送监测请求需要两种异步操作。第一种操作为请求获取一个定位客户端,第二个操作使用客户端发送请求。在这两个情况中,定位服务会在它完成了操作后调用一个回调函数。要处理这些操作的最佳方法是将这些函数调用串联起来。下面的代码片段将演示如何设置一个acitvity,定义方法,并以正确地顺序调用他们。

首先,修改activity类定义来实现必要的回调接口。添加下列接口:

ConnectionCallbacks

当一个定位客户端连接或者断开连接后,定位服务需要调用的方法。

OnConnectionFailedListener

当尝试连接定位客户端失败或发生错误后,定位服务需要调用的方法。

OnAddGeofencesResultListener

一旦添加了地理围栏,定位服务调用的方法。

例如:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
}

开始请求过程

接下来,定义一个方法,它通过连接定位服务来开始请求的过程。通过设置一个全局变量来标记它是一个添加地理围栏的请求。这将允许你使用ConnectionCallbacks.onConnected()这一回调函数来添加地理围栏或者移除它们,这些细节将在下面的章节展开。

为了防止竞争场景的发生(比如你的应用在第一个请求结束之前又发出了第二个请求),定义一个布尔变量,用来标记当前请求的状态:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    // Holds the location client
    private LocationClient mLocationClient;
    // Stores the PendingIntent used to request geofence monitoring
    private PendingIntent mGeofenceRequestIntent;
    // Defines the allowable request types.
    public enum REQUEST_TYPE = {ADD}
    private REQUEST_TYPE mRequestType;
    // Flag that indicates if a request is underway.
    private boolean mInProgress;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Start with the request flag set to false
        mInProgress = false;
        ...
    }
    ...
    /**
     * Start a request for geofence monitoring by calling
     * LocationClient.connect().
     */
    public void addGeofences() {
        // Start a request to add geofences
        mRequestType = ADD;
        /*
         * Test for Google Play services after setting the request type.
         * If Google Play services isn‘t present, the proper request
         * can be restarted.
         */
        if (!servicesConnected()) {
            return;
        }
        /*
         * Create a new location client object. Since the current
         * activity class implements ConnectionCallbacks and
         * OnConnectionFailedListener, pass the current activity object
         * as the listener for both parameters
         */
        mLocationClient = new LocationClient(this, this, this)
        // If a request is not already underway
        if (!mInProgress) {
            // Indicate that a request is underway
            mInProgress = true;
            // Request a connection from the client to Location Services
            mLocationClient.connect();
        } else {
            /*
             * A request is already underway. You can handle
             * this situation by disconnecting the client,
             * re-setting the flag, and then re-trying the
             * request.
             */
        }
    }
    ...
}

发送请求来添加地理围栏

在你的ConnectionCallbacks.onConnected()实现中,调用, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">LocationClient.addGeofences()。注意,如果连接失败了,onConnected()不会被调用,请求被中止。

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /*
     * Provide the implementation of ConnectionCallbacks.onConnected()
     * Once the connection is available, send a request to add the
     * Geofences
     */
    @Override
    private void onConnected(Bundle dataBundle) {
        ...
        switch (mRequestType) {
            case ADD :
                // Get the PendingIntent for the request
                mTransitionPendingIntent =
                        getTransitionPendingIntent();
                // Send a request to add the current geofences
                mLocationClient.addGeofences(
                        mCurrentGeofences, pendingIntent, this);
            ...
        }
    }
    ...
}

注意, android.app.PendingIntent, com.google.android.gms.location.LocationClient.OnAddGeofencesResultListener)">addGeofences()会迅速返回,但是请求的状态在定位服务调用onAddGeofencesResult()之前是不定的。一旦这一方法被调用,你就能够确定请求是否成功。

检查定位服务返回的结果

当定位服务调用了你的回调函数onAddGeofencesResult()的实现,这就代表请求完成了,之后检查传入的状态码。如果请求成功,那么你所请求的地理围栏将被激活。否则,地理围栏不会被激活,你需要继续尝试请求或者报告错误。例如:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
        ...
    /*
     * Provide the implementation of
     * OnAddGeofencesResultListener.onAddGeofencesResult.
     * Handle the result of adding the geofences
     *
     */
    @Override
    public void onAddGeofencesResult(
            int statusCode, String[] geofenceRequestIds) {
        // If adding the geofences was successful
        if (LocationStatusCodes.SUCCESS == statusCode) {
            /*
             * Handle successful addition of geofences here.
             * You can send out a broadcast intent or update the UI.
             * geofences into the Intent‘s extended data.
             */
        } else {
        // If adding the geofences failed
            /*
             * Report errors here.
             * You can log the error using Log.e() or update
             * the UI.
             */
        }
        // Turn off the in progress flag and disconnect the client
        mInProgress = false;
        mLocationClient.disconnect();
    }
    ...
}

处理连接中断

在有些情况下,定位服务可能会在你调用了disconnect()之前就中断连接了。要处理这种情况,需要实现onDisconnected()方法。在这个方法中,设置请求标识,以表明当前没有进行中的请求,并将客户端移除:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /*
     * Implement ConnectionCallbacks.onDisconnected()
     * Called by Location Services once the location client is
     * disconnected.
     */
    @Override
    public void onDisconnected() {
        // Turn off the request flag
        mInProgress = false;
        // Destroy the current location client
        mLocationClient = null;
    }
    ...
}

处理连接错误

除了处理定位服务的常规回调函数外,你还需要提供一个回调函数,该函数会在连接错误发生的时候被定为服务调用。该回调函数可以重用DialogFragment类(你在检查Google Play服务时所定义的类)。同时它也可以重用当用户与错误对话框交互时,接收任何由Google Play服务返回的结果的onActivityResult()函数。下面的代码片段展示了该回调函数的一个例子:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    // Implementation of OnConnectionFailedListener.onConnectionFailed
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        // Turn off the request flag
        mInProgress = false;
        /*
         * If the error has a resolution, start a Google Play services
         * activity to resolve it.
         */
        if (connectionResult.hasResolution()) {
            try {
                connectionResult.startResolutionForResult(
                        this,
                        CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } catch (SendIntentException e) {
                // Log the error
                e.printStackTrace();
            }
        // If no resolution is available, display an error dialog
        } else {
            // Get the error code
            int errorCode = connectionResult.getErrorCode();
            // Get the error dialog from Google Play services
            Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
                    errorCode,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            // If Google Play services can provide an error dialog
            if (errorDialog != null) {
                // Create a new DialogFragment for the error dialog
                ErrorDialogFragment errorFragment =
                        new ErrorDialogFragment();
                // Set the dialog in the DialogFragment
                errorFragment.setDialog(errorDialog);
                // Show the error dialog in the DialogFragment
                errorFragment.show(
                        getSupportFragmentManager(),
                        "Geofence Detection");
            }
        }
    }
    ...
}

二). 处理地理围栏转换

当定位服务检测到了用户进入或者离开了一个地理围栏,它会发送一个Intent,该Intent来自于你请求添加地理围栏时所用到的PendingIntent。

定义一个IntentService

下面的代码片段展示了当一个地理围栏转换发生的时候,如何定义一个IntentService。当用户点击通知时,显示应用的主activity:

public class ReceiveTransitionsIntentService extends IntentService {
    ...
    /**
     * Sets an identifier for the service
     */
    public ReceiveTransitionsIntentService() {
        super("ReceiveTransitionsIntentService");
    }
    /**
     * Handles incoming intents
     *@param intent The Intent sent by Location Services. This
     * Intent is provided
     * to Location Services (inside a PendingIntent) when you call
     * addGeofences()
     */
    @Override
    protected void onHandleIntent(Intent intent) {
        // First check for errors
        if (LocationClient.hasError(intent)) {
            // Get the error code with a static method
            int errorCode = LocationClient.getErrorCode(intent);
            // Log the error
            Log.e("ReceiveTransitionsIntentService",
                    "Location Services error: " +
                    Integer.toString(errorCode));
            /*
             * You can also send the error code to an Activity or
             * Fragment with a broadcast Intent
             */
        /*
         * If there‘s no error, get the transition type and the IDs
         * of the geofence or geofences that triggered the transition
         */
        } else {
            // Get the type of transition (entry or exit)
            int transitionType =
                    LocationClient.getGeofenceTransition(intent);
            // Test that a valid transition was reported
            if (
                (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER)
                 ||
                (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT)
               ) {
                List <Geofence> triggerList =
                        getTriggeringGeofences(intent);

                String[] triggerIds = new String[geofenceList.size()];

                for (int i = 0; i < triggerIds.length; i++) {
                    // Store the Id of each geofence
                    triggerIds[i] = triggerList.get(i).getRequestId();
                }
                /*
                 * At this point, you can store the IDs for further use
                 * display them, or display the details associated with
                 * them.
                 */
            }
        // An invalid transition was reported
        } else {
            Log.e("ReceiveTransitionsIntentService",
                    "Geofence transition error: " +
                    Integer.toString()transitionType));
        }
    }
    ...
}

在清单列表中声明IntentService

要在系统中使用IntentService,在应用清单文件中添加一个<service>标签,例如:

<service
    android:name="com.example.android.location.ReceiveTransitionsIntentService"
    android:label="@string/app_name"
    android:exported="false">
</service>

注意,你不需要为该服务指定intent过滤器,因为它仅会接收显式的intent。如何创建地理围栏转换intent,可以阅读:Send the monitoring request。


停止地理围栏监控

要停止地理围栏监控,你需要将它们移除。你可以通过一个PendingIntent将所有地理围栏全部移除,或者只移除一部分。过程与添加地理围栏类似。首先需要为移除请求获取定位客户端,然后使用客户端提出申请。

定位服务在完成移除后所调用的回调函数在LocationClient.OnRemoveGeofencesResultListener接口中被定义。将该接口声明为你的类定义的一部分,之后添加其两个方法的定义:

onRemoveGeofencesByPendingIntentResult()

当定位服务使用函数removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)移除了所有地理围栏后被调用。

onRemoveGeofencesByRequestIdsResult(List<String>, LocationClient.OnRemoveGeofencesResultListener)

当定位服务使用函数, com.google.android.gms.location.LocationClient.OnRemoveGeofencesResultListener)">removeGeofences(List<String>, LocationClient.OnRemoveGeofencesResultListener)将给定ID所对应的部分地理围栏移除后被调用。

下面给出这些方法的使用样例:

移除所有地理围栏

由于移除地理围栏会使用一些添加地理围栏时所使用的方法,我们从定义另一个请求类型开始:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    // Enum type for controlling the type of removal requested
    public enum REQUEST_TYPE = {ADD, REMOVE_INTENT}
    ...
}

通过获取定位服务的连接开始移除请求。如果连接失败了,onConnected()不会被调用,请求中止。下面的代码片段展示了如何开始请求:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /**
     * Start a request to remove geofences by calling
     * LocationClient.connect()
     */
    public void removeGeofences(PendingIntent requestIntent) {
        // Record the type of removal request
        mRequestType = REMOVE_INTENT;
        /*
         * Test for Google Play services after setting the request type.
         * If Google Play services isn‘t present, the request can be
         * restarted.
         */
        if (!servicesConnected()) {
            return;
        }
        // Store the PendingIntent
        mGeofenceRequestIntent = requestIntent;
        /*
         * Create a new location client object. Since the current
         * activity class implements ConnectionCallbacks and
         * OnConnectionFailedListener, pass the current activity object
         * as the listener for both parameters
         */
        mLocationClient = new LocationClient(this, this, this);
        // If a request is not already underway
        if (!mInProgress) {
            // Indicate that a request is underway
            mInProgress = true;
            // Request a connection from the client to Location Services
            mLocationClient.connect();
        } else {
            /*
             * A request is already underway. You can handle
             * this situation by disconnecting the client,
             * re-setting the flag, and then re-trying the
             * request.
             */
        }
    }
    ...
}

当定位服务调用了回调函数指明连接已建立,那么就发出移除所有地理围栏的请求。再发出请求后记得关闭连接。例如:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /**
     * Once the connection is available, send a request to remove the
     * Geofences. The method signature used depends on which type of
     * remove request was originally received.
     */
    private void onConnected(Bundle dataBundle) {
        /*
         * Choose what to do based on the request type set in
         * removeGeofences
         */
        switch (mRequestType) {
            ...
            case REMOVE_INTENT :
                mLocationClient.removeGeofences(
                        mGeofenceRequestIntent, this);
                break;
            ...
        }
    }
    ...
}

虽然对removeGeofences(PendingIntent, LocationClient.OnRemoveGeofencesResultListener)的调用后,服务端会马上返回,但移除请求的结果在定位服务调用onRemoveGeofencesByPendingIntentResult()之前是不定的。下面的代码片段展示了如何定义这一方法:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /**
     * When the request to remove geofences by PendingIntent returns,
     * handle the result.
     *
     *@param statusCode the code returned by Location Services
     *@param requestIntent The Intent used to request the removal.
     */
    @Override
    public void onRemoveGeofencesByPendingIntentResult(int statusCode,
            PendingIntent requestIntent) {
        // If removing the geofences was successful
        if (statusCode == LocationStatusCodes.SUCCESS) {
            /*
             * Handle successful removal of geofences here.
             * You can send out a broadcast intent or update the UI.
             * geofences into the Intent‘s extended data.
             */
        } else {
        // If adding the geocodes failed
            /*
             * Report errors here.
             * You can log the error using Log.e() or update
             * the UI.
             */
        }
        /*
         * Disconnect the location client regardless of the
         * request status, and indicate that a request is no
         * longer in progress
         */
        mInProgress = false;
        mLocationClient.disconnect();
    }
    ...
}

移除单个地理围栏

移除单个地理围栏或者部分地理围栏的过程同删除全部地理围栏相似。要指定你想要移除的地理围栏,需要把地理围栏的ID添加到一个String的List对象中。将这个List传递给removeGeofences,该方法之后便开始移除。

通过添加一个移除地理围栏请求类型的list,然后添加一个全局变量来存储地理围栏的list:

    ...
    // Enum type for controlling the type of removal requested
    public enum REQUEST_TYPE = {ADD, REMOVE_INTENT, REMOVE_LIST}
    // Store the list of geofence Ids to remove
    String<List> mGeofencesToRemove;

之后定义你想要移除的地理围栏list。例如,在下面的例子中,要移除的Geofence的ID为“1”:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
        List<String> listOfGeofences =
                Collections.singletonList("1");
        removeGeofences(listOfGeofences);
    ...
}

下面的代码片段定义了removeGeofences()方法:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /**
     * Start a request to remove monitoring by
     * calling LocationClient.connect()
     *
     */
    public void removeGeofences(List<String> geofenceIds) {
        // If Google Play services is unavailable, exit
        // Record the type of removal request
        mRequestType = REMOVE_LIST;
        /*
         * Test for Google Play services after setting the request type.
         * If Google Play services isn‘t present, the request can be
         * restarted.
         */
        if (!servicesConnected()) {
            return;
        }
        // Store the list of geofences to remove
        mGeofencesToRemove = geofenceIds;
        /*
         * Create a new location client object. Since the current
         * activity class implements ConnectionCallbacks and
         * OnConnectionFailedListener, pass the current activity object
         * as the listener for both parameters
         */
        mLocationClient = new LocationClient(this, this, this);
        // If a request is not already underway
        if (!mInProgress) {
            // Indicate that a request is underway
            mInProgress = true;
            // Request a connection from the client to Location Services
            mLocationClient.connect();
        } else {
            /*
             * A request is already underway. You can handle
             * this situation by disconnecting the client,
             * re-setting the flag, and then re-trying the
             * request.
             */
        }
    }
    ...
}

当定位服务激活了回调函数表明这个链接已经建立以后,发出该请求来移除列表中的地理围栏。在发出请求之后关闭连接。例如:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    private void onConnected(Bundle dataBundle) {
        ...
        switch (mRequestType) {
        ...
        // If removeGeofencesById was called
            case REMOVE_LIST :
                mLocationClient.removeGeofences(
                        mGeofencesToRemove, this);
                break;
        ...
        }
        ...
    }
    ...
} 

定义onRemoveGeofencesByRequestIdsResult()的实现。定位服务会激活该回调函数来指出这个移除地理围栏的请求已经完成。在该方法中,检查传入的状态码然后采取对应的措施:

public class MainActivity extends FragmentActivity implements
        ConnectionCallbacks,
        OnConnectionFailedListener,
        OnAddGeofencesResultListener {
    ...
    /**
     * When the request to remove geofences by IDs returns, handle the
     * result.
     *
     * @param statusCode The code returned by Location Services
     * @param geofenceRequestIds The IDs removed
     */
    @Override
    public void onRemoveGeofencesByRequestIdsResult(
            int statusCode, String[] geofenceRequestIds) {
        // If removing the geocodes was successful
        if (LocationStatusCodes.SUCCESS == statusCode) {
            /*
             * Handle successful removal of geofences here.
             * You can send out a broadcast intent or update the UI.
             * geofences into the Intent‘s extended data.
             */
        } else {
        // If removing the geofences failed
            /*
             * Report errors here.
             * You can log the error using Log.e() or update
             * the UI.
             */
        }
        // Indicate that a request is no longer in progress
        mInProgress = false;
        // Disconnect the location client
        mLocationClient.disconnect();
    }
    ...
}

你可以将地理围栏和其它地点感知的功能结合起来,比如定期的地点更新或者行为认知等,这些会在该系列课程中的后续课程中展开。

在下一节课程中,会向你展示请求和接收activity更新。在定期的间隔中,定位服务可以给你发送有关用户当前物理行为的信息。基于这一信息,你可以改变你的应用行为,例如,如果你检测到用户在步行而不在开车,你可以增加定期更新的间隔。