CSDN博客之星投票请移驾:http://vote.blog.csdn.net/blogstaritem/blogstar2013/weidi1989
本文主要介绍本应用的控制层具体实现。如需了解项目结构与框架,请移步之前系列文章:
Android之基于XMPP协议即时通讯软件(一)
Android之基于XMPP协议即时通讯软件(二)
另外,本项目已经升级到V1.0.1,已同步到开源中国代码托管:http://git.oschina.net/way/XMPP
今后更新也只会在此处同步,不会再打包上传到csdn,敬请悉知!
之前给大家介绍过,该小应用采用的是MVC设计模式,所以今天就跟大家分享一下控制层的具体实现。控制层担当一个非常重要的角色,既要处理界面传递过来的任务:点击发送消息、切换在线状态等,又要处理服务器发送过来的消息:有好友上线、收到新消息、保持长连接、掉线自动连接等。概括的说,总共分为以下四步:
①.实例化对象,作一些参数配置。
②.开始连接服务器,实现登陆。
③.注册各种事件监听,比如联系人动态变化、各种消息状态监听、开启长连接任务、掉线自动连接等。
④.用户主动退出,注销登录,断开连接。
第一步很简单,当用户启动该应用时,即启动本应用关健服务,并与界面Activity完成绑定,同时完成xmpp的参数配置,我这里是放在类的静态块里面完成的:
static { registerSmackProviders(); } // 做一些基本的配置 static void registerSmackProviders() { ProviderManager pm = ProviderManager.getInstance(); // add IQ handling pm.addIQProvider("query", "http://jabber.org/protocol/disco#info", new DiscoverInfoProvider()); // add delayed delivery notifications pm.addExtensionProvider("delay", "urn:xmpp:delay", new DelayInfoProvider()); pm.addExtensionProvider("x", "jabber:x:delay", new DelayInfoProvider()); // add carbons and forwarding pm.addExtensionProvider("forwarded", Forwarded.NAMESPACE, new Forwarded.Provider()); pm.addExtensionProvider("sent", Carbon.NAMESPACE, new Carbon.Provider()); pm.addExtensionProvider("received", Carbon.NAMESPACE, new Carbon.Provider()); // add delivery receipts pm.addExtensionProvider(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceipt.Provider()); pm.addExtensionProvider(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceiptRequest.Provider()); // add XMPP Ping (XEP-0199) pm.addIQProvider("ping", "urn:xmpp:ping", new PingProvider()); ServiceDiscoveryManager.setIdentityName(XMPP_IDENTITY_NAME); ServiceDiscoveryManager.setIdentityType(XMPP_IDENTITY_TYPE); }
public boolean login(String account, String password) throws XXException {// 登陆实现 try { if (mXMPPConnection.isConnected()) {// 首先判断是否还连接着服务器,需要先断开 try { mXMPPConnection.disconnect(); } catch (Exception e) { L.d("conn.disconnect() failed: " + e); } } SmackConfiguration.setPacketReplyTimeout(PACKET_TIMEOUT);// 设置超时时间 SmackConfiguration.setKeepAliveInterval(-1); SmackConfiguration.setDefaultPingInterval(0); registerRosterListener();// 监听联系人动态变化 mXMPPConnection.connect(); if (!mXMPPConnection.isConnected()) { throw new XXException("SMACK connect failed without exception!"); } mXMPPConnection.addConnectionListener(new ConnectionListener() { public void connectionClosedOnError(Exception e) { mService.postConnectionFailed(e.getMessage());// 连接关闭时,动态反馈给服务 } public void connectionClosed() { } public void reconnectingIn(int seconds) { } public void reconnectionFailed(Exception e) { } public void reconnectionSuccessful() { } }); initServiceDiscovery();// 与服务器交互消息监听,发送消息需要回执,判断是否发送成功 // SMACK auto-logins if we were authenticated before if (!mXMPPConnection.isAuthenticated()) { String ressource = PreferenceUtils.getPrefString(mService, PreferenceConstants.RESSOURCE, XMPP_IDENTITY_NAME); mXMPPConnection.login(account, password, ressource); } setStatusFromConfig();// 更新在线状态 } catch (XMPPException e) { throw new XXException(e.getLocalizedMessage(), e.getWrappedThrowable()); } catch (Exception e) { // actually we just care for IllegalState or NullPointer or XMPPEx. L.e(SmackImpl.class, "login(): " + Log.getStackTraceString(e)); throw new XXException(e.getLocalizedMessage(), e.getCause()); } registerAllListener();// 注册监听其他的事件,比如新消息 return mXMPPConnection.isAuthenticated(); }
第三步比较关健,登陆成功后,我们就必须要监听服务器的各种消息状态变化,以及要维持自身的一个稳定性,即保持长连接和掉线自动重连。下面是注册所有监听的函数:
private void registerAllListener() { // actually, authenticated must be true now, or an exception must have // been thrown. if (isAuthenticated()) { registerMessageListener();// 注册新消息监听 registerMessageSendFailureListener();// 注册消息发送失败监听 registerPongListener();// 注册服务器回应ping消息监听 sendOfflineMessages();// 发送离线消息 if (mService == null) { mXMPPConnection.disconnect(); return; } // we need to "ping" the service to let it know we are actually // connected, even when no roster entries will come in mService.rosterChanged(); } }
private void registerRosterListener() { mRoster = mXMPPConnection.getRoster(); mRosterListener = new RosterListener() { private boolean isFristRoter; @Override public void presenceChanged(Presence presence) {// 联系人状态改变,比如在线或离开、隐身之类 L.i("presenceChanged(" + presence.getFrom() + "): " + presence); String jabberID = getJabberID(presence.getFrom()); RosterEntry rosterEntry = mRoster.getEntry(jabberID); updateRosterEntryInDB(rosterEntry);// 更新联系人数据库 mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } @Override public void entriesUpdated(Collection<String> entries) {// 更新数据库,第一次登陆 // TODO Auto-generated method stub L.i("entriesUpdated(" + entries + ")"); for (String entry : entries) { RosterEntry rosterEntry = mRoster.getEntry(entry); updateRosterEntryInDB(rosterEntry); } mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } @Override public void entriesDeleted(Collection<String> entries) {// 有好友删除时, L.i("entriesDeleted(" + entries + ")"); for (String entry : entries) { deleteRosterEntryFromDB(entry); } mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } @Override public void entriesAdded(Collection<String> entries) {// 有人添加好友时,我这里没有弹出对话框确认,直接添加到数据库 L.i("entriesAdded(" + entries + ")"); ContentValues[] cvs = new ContentValues[entries.size()]; int i = 0; for (String entry : entries) { RosterEntry rosterEntry = mRoster.getEntry(entry); cvs[i++] = getContentValuesForRosterEntry(rosterEntry); } mContentResolver.bulkInsert(RosterProvider.CONTENT_URI, cvs); if (isFristRoter) { isFristRoter = false; mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } } }; mRoster.addRosterListener(mRosterListener); }
private void registerMessageListener() { // do not register multiple packet listeners if (mPacketListener != null) mXMPPConnection.removePacketListener(mPacketListener); PacketTypeFilter filter = new PacketTypeFilter(Message.class); mPacketListener = new PacketListener() { public void processPacket(Packet packet) { try { if (packet instanceof Message) {// 如果是消息类型 Message msg = (Message) packet; String chatMessage = msg.getBody(); // try to extract a carbon Carbon cc = CarbonManager.getCarbon(msg); if (cc != null && cc.getDirection() == Carbon.Direction.received) {// 收到的消息 L.d("carbon: " + cc.toXML()); msg = (Message) cc.getForwarded() .getForwardedPacket(); chatMessage = msg.getBody(); // fall through } else if (cc != null && cc.getDirection() == Carbon.Direction.sent) {// 如果是自己发送的消息,则添加到数据库后直接返回 L.d("carbon: " + cc.toXML()); msg = (Message) cc.getForwarded() .getForwardedPacket(); chatMessage = msg.getBody(); if (chatMessage == null) return; String fromJID = getJabberID(msg.getTo()); addChatMessageToDB(ChatConstants.OUTGOING, fromJID, chatMessage, ChatConstants.DS_SENT_OR_READ, System.currentTimeMillis(), msg.getPacketID()); // always return after adding return;// 记得要返回 } if (chatMessage == null) { return;// 如果消息为空,直接返回了 } if (msg.getType() == Message.Type.error) { chatMessage = "<Error> " + chatMessage;// 错误的消息类型 } long ts;// 消息时间戳 DelayInfo timestamp = (DelayInfo) msg.getExtension( "delay", "urn:xmpp:delay"); if (timestamp == null) timestamp = (DelayInfo) msg.getExtension("x", "jabber:x:delay"); if (timestamp != null) ts = timestamp.getStamp().getTime(); else ts = System.currentTimeMillis(); String fromJID = getJabberID(msg.getFrom());// 消息来自对象 addChatMessageToDB(ChatConstants.INCOMING, fromJID, chatMessage, ChatConstants.DS_NEW, ts, msg.getPacketID());// 存入数据库,并标记为新消息DS_NEW mService.newMessage(fromJID, chatMessage);// 通知service,处理是否需要显示通知栏, } } catch (Exception e) { // SMACK silently discards exceptions dropped from // processPacket :( L.e("failed to process packet:"); e.printStackTrace(); } } }; mXMPPConnection.addPacketListener(mPacketListener, filter);// 这是最关健的了,少了这句,前面的都是白费功夫 }
从连上服务器完成登录15分钟后,闹钟响起,开始给服务器发送一条ping消息(随机生成一唯一ID),同时启动超时闹钟(本应用是30+3秒),如果服务器在30+3秒内回复了一条pong消息(与之前发送的ping消息ID相同),代表与服务器任然保持连接,则取消超时闹钟,完成一次ping-pong过程。如果在30+3秒内服务器未响应,或者回复的pong消息与之前发送的ping消息ID不一致,则认为与服务器已经断开。此时,将此消息反馈给界面,同时启动重连任务。实现长连接。
关健代码如下:
/***************** start 处理ping服务器消息 ***********************/ private void registerPongListener() { // reset ping expectation on new connection mPingID = null;// 初始化ping的id if (mPongListener != null) mXMPPConnection.removePacketListener(mPongListener);// 先移除之前监听对象 mPongListener = new PacketListener() { @Override public void processPacket(Packet packet) { if (packet == null) return; if (packet.getPacketID().equals(mPingID)) {// 如果服务器返回的消息为ping服务器时的消息,说明没有掉线 L.i(String.format( "Ping: server latency %1.3fs", (System.currentTimeMillis() - mPingTimestamp) / 1000.)); mPingID = null; ((AlarmManager) mService .getSystemService(Context.ALARM_SERVICE)) .cancel(mPongTimeoutAlarmPendIntent);// 取消超时闹钟 } } }; mXMPPConnection.addPacketListener(mPongListener, new PacketTypeFilter( IQ.class));// 正式开始监听 mPingAlarmPendIntent = PendingIntent.getBroadcast( mService.getApplicationContext(), 0, mPingAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);// 定时ping服务器,以此来确定是否掉线 mPongTimeoutAlarmPendIntent = PendingIntent.getBroadcast( mService.getApplicationContext(), 0, mPongTimeoutAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);// 超时闹钟 mService.registerReceiver(mPingAlarmReceiver, new IntentFilter( PING_ALARM));// 注册定时ping服务器广播接收者 mService.registerReceiver(mPongTimeoutAlarmReceiver, new IntentFilter( PONG_TIMEOUT_ALARM));// 注册连接超时广播接收者 ((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE)) .setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, AlarmManager.INTERVAL_FIFTEEN_MINUTES, mPingAlarmPendIntent);// 15分钟ping以此服务器 } /** * BroadcastReceiver to trigger reconnect on pong timeout. */ private class PongTimeoutAlarmReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent i) { L.d("Ping: timeout for " + mPingID); mService.postConnectionFailed(XXService.PONG_TIMEOUT); //logout();// 超时就断开连接 } } /** * BroadcastReceiver to trigger sending pings to the server */ private class PingAlarmReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent i) { if (mXMPPConnection.isAuthenticated()) { sendServerPing();// 收到ping服务器的闹钟,即ping一下服务器 } else L.d("Ping: alarm received, but not connected to server."); } } public void sendServerPing() { if (mPingID != null) {// 此时说明上一次ping服务器还未回应,直接返回,直到连接超时 L.d("Ping: requested, but still waiting for " + mPingID); return; // a ping is still on its way } Ping ping = new Ping(); ping.setType(Type.GET); ping.setTo(PreferenceUtils.getPrefString(mService, PreferenceConstants.Server, PreferenceConstants.GMAIL_SERVER)); mPingID = ping.getPacketID();// 此id其实是随机生成,但是唯一的 mPingTimestamp = System.currentTimeMillis(); L.d("Ping: sending ping " + mPingID); mXMPPConnection.sendPacket(ping);// 发送ping消息 // register ping timeout handler: PACKET_TIMEOUT(30s) + 3s ((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE)).set( AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + PACKET_TIMEOUT + 3000, mPongTimeoutAlarmPendIntent);// 此时需要启动超时判断的闹钟了,时间间隔为30+3秒 }
public void postConnectionFailed(final String reason) { mMainHandler.post(new Runnable() { public void run() { connectionFailed(reason); } }); } private void connectionFailed(String reason) { L.i(XXService.class, "connectionFailed: " + reason); mConnectedState = DISCONNECTED;// 更新当前连接状态 if (mSmackable != null) mSmackable.setStatusOffline();// 将所有联系人标记为离线 if (TextUtils.equals(reason, LOGOUT)) {// 如果是手动退出 ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) .cancel(mPAlarmIntent); return; } // 回调 if (mConnectionStatusCallback != null) { mConnectionStatusCallback.connectionStatusChanged(mConnectedState, reason); if (mIsFirstLoginAction)// 如果是第一次登录,就算登录失败也不需要继续 return; } // 无网络连接时,直接返回 if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) { ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) .cancel(mPAlarmIntent); return; } String account = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.ACCOUNT, ""); String password = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.PASSWORD, ""); // 无保存的帐号密码时,也直接返回 if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) { L.d("account = null || password = null"); return; } // 如果不是手动退出并且需要重新连接,则开启重连闹钟 if (PreferenceUtils.getPrefBoolean(this, PreferenceConstants.AUTO_RECONNECT, true)) { L.d("connectionFailed(): registering reconnect in " + mReconnectTimeout + "s"); ((AlarmManager) getSystemService(Context.ALARM_SERVICE)).set( AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + mReconnectTimeout * 1000, mPAlarmIntent); mReconnectTimeout = mReconnectTimeout * 2; if (mReconnectTimeout > RECONNECT_MAXIMUM) mReconnectTimeout = RECONNECT_MAXIMUM; } else { ((AlarmManager) getSystemService(Context.ALARM_SERVICE)) .cancel(mPAlarmIntent); } }
// 自动重连广播接收者 private class ReconnectAlarmReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent i) { L.d("Alarm received."); if (!PreferenceUtils.getPrefBoolean(XXService.this, PreferenceConstants.AUTO_RECONNECT, true)) { return; } if (mConnectedState != DISCONNECTED) { L.d("Reconnect attempt aborted: we are connected again!"); return; } String account = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.ACCOUNT, ""); String password = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.PASSWORD, ""); if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) { L.d("account = null || password = null"); return; } Login(account, password); } }
public void onNetChange() { if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) {// 如果是网络断开,不作处理 connectionFailed(NETWORK_ERROR); return; } if (isAuthenticated())// 如果已经连接上,直接返回 return; String account = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.ACCOUNT, ""); String password = PreferenceUtils.getPrefString(XXService.this, PreferenceConstants.PASSWORD, ""); if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password))// 如果没有帐号,也直接返回 return; if (!PreferenceUtils.getPrefBoolean(this, PreferenceConstants.AUTO_RECONNECT, true))// 不需要重连 return; Login(account, password);// 重连 }
<action android:name="android.intent.action.USER_PRESENT" />
⑤.实现服务在前台运行:这个我在之前的文章中有介绍过:Android之后台服务判断本应用Activity是否处于栈顶,这里就不在赘述了。
第四步是用户主动退出,注销登陆,这个好像没有多少需要介绍的,无法是释放掉一些资源,关闭一些服务等等,也无需多说。看看代码即可:
// 退出 public boolean logout() { // mIsNeedReConnection = false;// 手动退出就不需要重连闹钟了 boolean isLogout = false; if (mConnectingThread != null) { synchronized (mConnectingThread) { try { mConnectingThread.interrupt(); mConnectingThread.join(50); } catch (InterruptedException e) { L.e("doDisconnect: failed catching connecting thread"); } finally { mConnectingThread = null; } } } if (mSmackable != null) { isLogout = mSmackable.logout(); mSmackable = null; } connectionFailed(LOGOUT);// 手动退出 return isLogout; }
好了,整个控制层大概就讲到这里,总结一下:
重要的是第三步:注册监听和长连接的处理,其中长连接处理也是最为关键和麻烦的。
文章比较长,其实也花了我几个小时的时间,首先感谢你看到了文章末尾,由于个人水平限制,难免会有一些失误或者不准确的地方,欢迎大家批评指出。