4.1 问题
需要应用程序在用户进入或退出特定位置区域时向其提供上下文信息。
4.2 解决方案
(API Level 9)
使用作为Google Play Services一部分提供的地理围栏功能。借助这些功能,应用程序可以围绕特定点定义圆形区域,在用户移入或离开该区域时,我们希望接收相应的回调。应用程序可以创建多个Geofence实例,并且无期限地跟踪这些实例,或者在超出到期时间后自动删除它们。
为跟踪用户到达您认为重要的位置,使用基于地区的用户位置监控可能是更加节能的方法。相比于应用程序持续跟踪用户位置以找出其何时到达给定的目标,以这种方式让服务框架跟踪位置并通知用户通常可以延长电池寿命。
要点:
此处描述的地理围栏功能是Google Play Services 库的一部分,它们在任意平台级别都不是原生SDKd的一部分。然而,目标平台为API Level 8或以后版本的应用程序以及Google Play 体系内的设备都可以使用此绘图库。
4.3 实现机制
我们将创建一个由简单Activity组成的应用程序,该Activity使用户可以围绕其当前位置设置地理围栏(参见下图),然后明确地启动或停止监控。
图 RegionMonitor的控制Activity
一旦启动监控,就会激活后台服务以响应与用户位置转移到地理围栏区域内或移出该区域相关的事件。该服务组件使用户可以直接响应这些事件,而无须应用程序的UI位于前台。
要点:
因为在此例中我们访问的是用户位置,需要在AndroidManifest.xml中请求android.permission.ACCESS_FINE_LOCATION权限。
首先查看以下清单代码,其中描述了Activity的布局。
res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<SeekBar
android:id="@+id/radius"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="1000"/>
<TextView
android:id="@+id/radius_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Set Geofence at My Location"
android:onClick="onSetGeofenceClick" />
<!-- 间隔区 -->
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Monitoring"
android:onClick="onStartMonitorClick" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Monitoring"
android:onClick="onStopMonitorClick" />
</LinearLayout>
该布局包含一个SeekBar,用户使用户滑动手指来选择所需的半径值。用户可以通过触摸最上方的按钮来锁定新的地理围栏,或者使用底部的按钮启动或停止监控。以下代码清单显示了管理地理围栏监控的Activity代码。
设置地理围栏的Activity
public class MainActivity extends Activity implements
OnSeekBarChangeListener,
GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener,
LocationClient.OnAddGeofencesResultListener,
LocationClient.OnRemoveGeofencesResultListener {
private static final String TAG = "RegionMonitorActivity";
//单个地理围栏的唯一标识符
private static final String FENCE_ID = "com.androidrecipes.FENCE";
private LocationClient mLocationClient;
private SeekBar mRadiusSlider;
private TextView mStatusText, mRadiusText;
private Geofence mCurrentFence;
private Intent mServiceIntent;
private PendingIntent mCallbackIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//连线 UI 连接
mStatusText = (TextView) findViewById(R.id.status);
mRadiusText = (TextView) findViewById(R.id.radius_text);
mRadiusSlider = (SeekBar) findViewById(R.id.radius);
mRadiusSlider.setOnSeekBarChangeListener(this);
updateRadiusDisplay();
//检查 Google Play Services是否为最新版本
switch (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this)) {
case ConnectionResult.SUCCESS:
//不执行任何操作,继续
break;
case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
Toast.makeText(this,
"Geofencing service requires an update, please open Google Play.",
Toast.LENGTH_SHORT).show();
finish();
return;
default:
Toast.makeText(this,
"Geofencing service is not available on this device.",
Toast.LENGTH_SHORT).show();
finish();
return;
}
//为Google Services创建客户端
mLocationClient = new LocationClient(this, this, this);
//创建Intent 以触发服务
mServiceIntent = new Intent(this, RegionMonitorService.class);
//为 Google Services 回调创建PendingIntent
mCallbackIntent = PendingIntent.getService(this, 0, mServiceIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
protected void onResume() {
super.onResume();
//连接所有服务
if (!mLocationClient.isConnected()
&& !mLocationClient.isConnecting()) {
mLocationClient.connect();
}
}
@Override
protected void onPause() {
super.onPause();
//不在前台时断开连接
mLocationClient.disconnect();
}
public void onSetGeofenceClick(View v) {
//通过服务和半径从UI获得最新位置
Location current = mLocationClient.getLastLocation();
int radius = mRadiusSlider.getProgress();
//使用 Builder 创建新的地理围栏
Geofence.Builder builder = new Geofence.Builder();
mCurrentFence = builder
//此地理围栏的唯一值
.setRequestId(FENCE_ID)
//大小和位置
.setCircularRegion(
current.getLatitude(),
current.getLongitude(),
radius)
//进入和离开地理围栏的事件
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER
| Geofence.GEOFENCE_TRANSITION_EXIT)
//保持活跃
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.build();
mStatusText.setText(String.format("Geofence set at %.3f, %.3f",
current.getLatitude(), current.getLongitude()) );
}
public void onStartMonitorClick(View v) {
if (mCurrentFence == null) {
Toast.makeText(this, "Geofence Not Yet Set",
Toast.LENGTH_SHORT).show();
return;
}
//添加围栏以开始跟踪
// PendingIntent将随着新的更新而被触发
ArrayList<Geofence> geofences = new ArrayList<Geofence>();
geofences.add(mCurrentFence);
mLocationClient.addGeofences(geofences, mCallbackIntent, this);
}
public void onStopMonitorClick(View v) {
//移除以停止跟踪
mLocationClient.removeGeofences(mCallbackIntent, this);
}
/** SeekBar 回调 */
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
updateRadiusDisplay();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
private void updateRadiusDisplay() {
mRadiusText.setText(mRadiusSlider.getProgress() + " meters");
}
/** Google Services 连接回调 */
@Override
public void onConnected(Bundle connectionHint) {
Log.v(TAG, "Google Services Connected");
}
@Override
public void onDisconnected() {
Log.w(TAG, "Google Services Disconnected");
}
@Override
public void onConnectionFailed(ConnectionResult result) {
Log.w(TAG, "Google Services Connection Failure");
}
/** LocationClient 回调 */
/*
* 异步地理围栏添加完成时调用
* 发生此情况时,启动监控服务
*/
@Override
public void onAddGeofencesResult(int statusCode,
String[] geofenceRequestIds) {
if (statusCode == LocationStatusCodes.SUCCESS) {
Toast.makeText(this, "Geofence Added Successfully", Toast.LENGTH_SHORT).show();
}
Intent startIntent = new Intent(mServiceIntent);
startIntent.setAction(RegionMonitorService.ACTION_INIT);
startService(mServiceIntent);
}
/*
*异步地理围栏删除完成时调用
* 调用的版本取决于是通过PendingIntent还是请求ID来请求删除
*发生此情况时,启动监控服务
*/
@Override
public void onRemoveGeofencesByPendingIntentResult(
int statusCode, PendingIntent pendingIntent) {
if (statusCode == LocationStatusCodes.SUCCESS) {
Toast.makeText(this, "Geofence Removed Successfully",
Toast.LENGTH_SHORT).show();
}
stopService(mServiceIntent);
}
@Override
public void onRemoveGeofencesByRequestIdsResult(
int statusCode, String[] geofenceRequestIds) {
if (statusCode == LocationStatusCodes.SUCCESS) {
Toast.makeText(this, "Geofence Removed Successfully",
Toast.LENGTH_SHORT).show();
}
stopService(mServiceIntent);
}
}
创建此Activity之后,第一项工作是确认Google Play Services已存在且为最新版本。如果不是最新版本,则需要鼓励用户访问Google Play网站以触发最新版本的自动更新。
完成上述工作之后,通过LocationClient实例建立与位置服务的连接。我们希望仅在前台是保持此连接,因此在onResume()和onPause()之间平衡连接调用。此连接是异步的,因此必须等待onConnected()方法完成才可以执行进一步的操作。在此例中,我们只需要在用户按下某个按钮时访问LocationClient,因此,在此方法中没有特别需要完成的工作。
提示:
异步并不一定意味着缓慢。异步方法调用并不意味着预期会花费很长时间。异步仅意味着在函数返回后我们不能立刻访问对象。在大多数情况下,这些回调仍然会在Activity完全可见之前触发很长时间。
用户选择所需的半径并点击Set Geofence按钮之后,从LocationClient获得最新的已知位置,结合选择的半径来构建地理围栏。使用Geofence.Builder创建Geofence实例,该实例用于设置地理围栏的位置、唯一标识以及我们可能需要的其他任何属性。
借助setTransitionTypes(),我们控制哪些过渡会生成通知。有两种可能的过渡值:GEOFENCE_TRANSITION_ENTER和GEOFENCE_TRANSITION_EXIT。可以对其中一个事件或两个事件请求回调,在此选择对两个事件请求回调。
正值的到期时间代表从添加围栏开始未来的某个时间,到达该时间时应该自动删除地理围栏。设置该值为NEVER_EXPIPE可以无限期地跟踪此地区,直到手动将其删除。
在未来的某个时间点,当用户点击Start Monitoring按钮时,我们将同时使用Geofence和PendingIntent调用LocationClient.addGeofences()来请求更新此地区,框架会为每个新的监控事件激活此方法。注意在我们的示例中,PendingIntent指向一个服务。该请求也是异步的,当操作完成时,我们将通过onAddGeofencesResult()收到回调。此时,一条启动命令发送到我们的后台服务。
最后,当用户点击Stop Monitoring按钮时,就会删除地理围栏,并且新的更新操作将停止。我们使用传入原始请求的相同PengInent应用删除的元素。也可以使用最初构建时分配的唯一标识符删除地理围栏。异步删除完成之后,一条停止命令会发送到后台服务。
在启动和停止的情况下,我们发送一个Intent到具有唯一动作字符串的服务,因此该服务可以区分这些请求与从位置服务收到的更新。以下清单代码显示了我们到目前讨论的此后台服务。
地区监控服务
public class RegionMonitorService extends Service {
private static final String TAG = "RegionMonitorService";
private static final int NOTE_ID = 100;
//标识启动请求与事件的唯一动作
public static final String ACTION_INIT =
"com.androidrecipes.regionmonitor.ACTION_INIT";
private NotificationManager mNoteManager;
@Override
public void onCreate() {
super.onCreate();
mNoteManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
//在服务启动时发出系统通知
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setContentTitle("Geofence Service");
builder.setContentText("Waiting for transition...");
builder.setOngoing(true);
Notification note = builder.build();
mNoteManager.notify(NOTE_ID, note);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//不做任何事,仅是启动服务
if (ACTION_INIT.equals(intent.getAction())) {
//我们不关心此服务是否意外终止
return START_NOT_STICKY;
}
if (LocationClient.hasError(intent)) {
//记录任何错误
Log.w(TAG, "Error monitoring region: "
+ LocationClient.getErrorCode(intent));
} else {
//根据新事件更新进行中的通知
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_launcher);
builder.setDefaults(Notification.DEFAULT_SOUND
| Notification.DEFAULT_LIGHTS);
builder.setOngoing(true);
int transitionType = LocationClient.getGeofenceTransition(intent);
//检查在何处进入或退出地区
if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) {
builder.setContentTitle("Geofence Transition");
builder.setContentText("Entered your Geofence");
} else if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) {
builder.setContentTitle("Geofence Transition");
builder.setContentText("Exited your Geofence");
}
Notification note = builder.build();
mNoteManager.notify(NOTE_ID, note);
}
//我们不关心此服务是否意外终止
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
//服务终止时,取消进行中的通知
mNoteManager.cancel(NOTE_ID);
}
/*我们未绑定到此服务 */
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
此服务的主要作用是从关于监控地区的位置服务接收更新,并将它们发送到状态栏中的通知,这样用户就可以看到改动。
初次创建服务时(按下按钮后发送启动命令时就会发生该操作),会创建初始通知并将其发送到状态栏。这将在第一个onStartCommand()方法调用之后发生,在该方法中查找唯一的动作字符串,而不做其他任何工作。
上述工作完成之后,第一个地区监控事件将进入此服务,再次调用onStartCommand()。第一个事件是一个过渡事件,指明关于Geofences的设备位置的初始状态。在此例中,我们检查Intent是否包含错误信息,如果这是成功的跟踪事件,我们就基于包含在其中的过渡信息构造一条更新的通知,并将更新发送到状态栏。
对于在地区监控激活时收到的每个新事件,该过程会重复进行。当用户最终返回到此Activity并按下Stop Monitoring按钮时,停止命令将造成在服务中调用onDestroy()。在此方法中,我们从状态中删除通知,向用户表明监控不再激活。
注意:
如果使用同一个PendingIntent激活了多个Geofences实例,则可以使用两一个方法LocationClient.getTriggeringGeofences()确定哪些地区是任意给定事件的一部分。
网友评论