注:本文翻译自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 = -999l
;
public
static
final
float
INVALID_FLOAT_VALUE = -999.0f
;
public
static
final
int
INVALID_INT_VALUE = -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;
}
}
...
}
创建地理围栏对象
下面的代码片段使用 SimpleGeofence 和 SimpleGeofenceStore 类从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类定义来实现必要的回调接口。添加下列接口:
当一个定位客户端连接或者断开连接后,定位服务需要调用的方法。
当尝试连接定位客户端失败或发生错误后,定位服务需要调用的方法。
一旦添加了地理围栏,定位服务调用的方法。
例如:
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()
实现中,调用
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
);
...
}
}
...
}
注意
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)
当定位服务使用函数 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更新。在定期的间隔中,定位服务可以给你发送有关用户当前物理行为的信息。基于这一信息,你可以改变你的应用行为,例如,如果你检测到用户在步行而不在开车,你可以增加定期更新的间隔。

