4.監控位置地區
4.1 問題
需要應用程式在使用者進入或退出特定位置區域時向其提供上下文資訊。
4.2 解決方案
(API Level 9)
使用作為Google+Play/">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()確定哪些地區是任意給定事件的一部分。