我在 Android SDK 上寫了一個簡單的程式 (姑且把她叫 Follow Me),來測試 Android 的 XMPP Services (Google Talk) & Google Map 整合,我希望可以透過 Google Talk,來追蹤 Contact Lists 現在的位置及互相通訊。例如可以透過 FollowMe ,我就可以很順利的開車跟在領導車的朋友後面,因為 FollwMe 會在地圖上追蹤及顯示共同出遊的車子(包含領導車及自己)的位置,一路順暢的共同到達目的地。畫面如下圖,我 (soccercheng) 在追蹤 [email protected] 的行蹤。
要寫這個程式,必須要對下圖中,Application Framework 的一些項目進行了解,如:
如果只看 Android 提供的 Google APIs & Services 文件,很難看的懂他在寫甚麼,MapView 還好,XMPP 就不好懂,所以我把在測試 XMPP 的經驗和大家分享一下:
首先,我們必須讓 GPhone 的 XMPP 掛載上 Google Talk (This means U have to apply an Google Talk account),在 Dev Tools --> XMPP Settings (如上圖),用你的帳號讓登入 Google Talk。
接下來,介紹一下 Android XMPP Programming 的基本概念:
Listen to XMPP Notifications
(建議你先看一下 Anatomy of an Android Application,不然你會看不懂)
XMPP Services 是透過 Notification 的方式,來發佈消息,所已入你的程式想知道 XMPP Services 的訊息,就必須掛載 IntentReceiver & IntentFilter,文件中並未記載 XMPP Services 到底發布了哪些 Notifications,相關的定義在 com.google.android.xmppService.XmppConstants 中,不過文件上並沒有這個class 的說明,我是直接在 eclipse 看這個 XmppConstants 的說明,XmppConstants 最主要定義了三個:
另外,我有定義了兩個 Action,讓我的程式透過 XMPP Service ,來告知我是否有人跟我要我現在位置,會是跟我回報他的位置:
註冊 & 掛載 IntentReceiver & IntentFilter
註冊方式基本上有兩種,一種是在 AndroidManifest.xml 中定義,或是寫程式向系統註冊,兩者最主要的差異是:
public class FollowMe extends MapActivity
implements View.OnClickListener {
....
/** Called when the activity is becoming visible to the user. */
/* For example, you can register an IntentReceiver in onStart()
* to monitor for changes that impact your UI, and unregister it in onStop()
* when the user an no longer see what you are displaying.
* The onStart() and onStop() methods can be called multiple times,
* as the activity becomes visible and hidden to the user.
*/
@Override
protected void onStart() {
super.onStart();
// Register XMPP Data MessageReciver
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(XmppDataMessageReceiver.ACTION_REQUEST_LOCATION);
intentFilter.addAction(XmppDataMessageReceiver.ACTION_REPORT_LOCATION);
intentFilter.addAction(XmppDataMessageReceiver.ACTION_NEW_CHAT);
intentFilter.addAction(...ACTION_XMPP_CONNECTION_STATE...);
intentFilter.addAction(...ACTION_ROSTER_OR_PRESENCE_CHANGED...);
....
this.registerReceiver(xmppDataMessageReceiver, intentFilter);
}
@Override
protected void onStop() {
super.onStop();
// unregister XMPP Data MessageReciver
this.unregisterReceiver(xmppDataMessageReceiver);
}
....
}
在 IntentReceiver 中接收 Notification
public class XmppDataMessageReceiver extends IntentReceiver {
....
@Override
public void onReceiveIntent(Context context, Intent intent) {
String action = intent.getAction();
if(action.equals(ACTION_REQUEST_LOCATION)) {
Bundle bundle = intent.getExtras();
if(bundle != null) {
String requester = bundle.getString("requester");
//showMessage(context, requester);
if(requester != null)
activity.reportLocation(requester);
}
}else if(action.equals(ACTION_REPORT_LOCATION)) {
Bundle bundle = intent.getExtras();
if(bundle != null) {
String reporter = bundle.getString("reporter");
int latitude = Integer.parseInt(bundle.getString("latitude"));
int longitude = Integer.parseInt(bundle.getString("longitude"));
activity.setUserLocation(reporter, latitude, longitude);
}
}else if(action.equals(ACTION_XMPP_CONNECTION_STATE)) {
Bundle bundle = intent.getExtras();
int state = bundle.getInteger("state").intValue();
if(state == com.google.android.xmppService.ConnectionState.REQUESTED_ROSTER) {
// Get the base URI for IM contacts.
try {
ContentURI myPerson = new ContentURI("content://im/contacts");
// Query for this record.
Cursor cur = activity.managedQuery(myPerson, null, null, null);
String[] names = cur.getColumnNames();
// TODO There is no document for IM Contact operation,
// 2 hard for me to hacking it
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else if(action.equals(ACTION_ROSTER_OR_PRESENCE_CHANGED)) {
//showMessage(context, ACTION_ROSTER_OR_PRESENCE_CHANGED);
} else if(action.equals(ACTION_NEW_CHAT)) {
//showMessage(context, ACTION_NEW_CHAT);
}
}
....
}
Reporting Location
public class XmppDataMessageReceiver extends IntentReceiver {
....
private Intent getRequestIntentToSend() {
Intent intent = new Intent(XmppDataMessageReceiver.ACTION_REQUEST_LOCATION);
intent.putExtra("requester", userName);
return intent;
}
private Intent getResponseIntentToSend() {
Intent intent = new Intent(XmppDataMessageReceiver.ACTION_REPORT_LOCATION);
intent.putExtra("reporter", userName);
//Bundle bundle = intent.getExtras();
int i = locationIndex++ % 19;
intent.putExtra("latitude", Integer.toString(locations[i][0]));
intent.putExtra("longitude", Integer.toString(locations[i][1]));
//intent.putExtra("location", new Point(locations[i][0], locations[i][1]));
return intent;
}
public void reportLocation(String requester) {
Intent intent = getResponseIntentToSend();
if (mXmppSession == null) {
showMessage(getText(R.string.xmpp_service_not_connected));
return;
}
try {
mXmppSession.sendDataMessage(requester, intent);
} catch (DeadObjectException ex) {
showMessage(getText(R.string.found_stale_xmpp_service));
mXmppSession = null;
bindXmppService();
}
}
....
}
Binding to XMPP Service
如何 bind to XMPP Service,在現在接露的 document 中找不到,還好在 SDK 的 sample 中,有兩個應用 (XmppDataMessageReceiver.java &XmppDataMessageSender.java),有用到一些 undocumented classes,其中接露了不少秘密。
public class XmppDataMessageReceiver extends IntentReceiver {
....
private void bindXmppService() {
bindService((new Intent()).setComponent(
com.google.android.xmppService.XmppConstants.XMPP_SERVICE_COMPONENT),
null, mConnection, 0);
}
private IXmppSession mXmppSession = null;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the XmppService has been
// established, giving us the service object we can use to
// interact with the service. We are communicating with our
// service through an IDL interface, so get a client-side
// representation of that from the raw service object.
IXmppService xmppService = IXmppService.Stub.asInterface(service);
// Increment the event monitor reference count. When application UI
// is alive and listening for the various XMPP event intent
// broadcast (i.e. incoming chat, muc invitation, subscription invitation),
// call this so XmppService will send the appropriate intent broadcast
// instead of posting a titlebar notification.
try {
xmppService.incrementEventMonitorRef();
} catch (DeadObjectException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
}
try {
mXmppSession = xmppService.getDefaultSession();
if (mXmppSession == null) {
// this should not happen.
showMessage(getText(R.string.xmpp_session_not_found));
return;
}
//if()
mXmppSession.requestRosterAndSendInitialPresence();
userName = mXmppSession.getUsername();
} catch (DeadObjectException ex) {
//Log.e(LOG_TAG, "caught " + ex);
showMessage(getText(R.string.found_stale_xmpp_service));
return;
}
okButton.setEnabled(true);
}
public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mXmppSession = null;
okButton.setEnabled(false);
}
};
....
}