Android之基于XMPP协议即时通讯软件(三)

本文主要介绍本应用的控制层具体实现。如需了解项目结构与框架,请移步之前系列文章:

Android之基于XMPP协议即时通讯软件(一)

Android之基于XMPP协议即时通讯软件(二)


另外,本项目已经升级到V1.0.1,已同步到开源中国代码托管:http://git.oschina.net/way/XMPP

今后更新也只会在此处同步,不会再打包上传到csdn,敬请悉知!



之前给大家介绍过,该小应用采用的是MVC设计模式,所以今天就跟大家分享一下控制层的具体实现。控制层担当一个非常重要的角色,既要处理界面传递过来的任务:点击发送消息、切换在线状态等,又要处理服务器发送过来的消息:有好友上线、收到新消息、保持长连接、掉线自动连接等。概括的说,总共分为以下四步:

①.实例化对象,作一些参数配置。

②.开始连接服务器,实现登陆。

③.注册各种事件监听,比如联系人动态变化、各种消息状态监听、开启长连接任务、掉线自动连接等。

④.用户主动退出,注销登录,断开连接。


第一步很简单,当用户启动该应用时,即启动本应用关健服务,并与界面Activity完成绑定,同时完成xmpp的参数配置,我这里是放在类的静态块里面完成的:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. static {  
  2.     registerSmackProviders();  
  3. }  
  4.   
  5. // 做一些基本的配置  
  6. static void registerSmackProviders() {  
  7.     ProviderManager pm = ProviderManager.getInstance();  
  8.     // add IQ handling  
  9.     pm.addIQProvider("query""http://jabber.org/protocol/disco#info",  
  10.             new DiscoverInfoProvider());  
  11.     // add delayed delivery notifications  
  12.     pm.addExtensionProvider("delay""urn:xmpp:delay",  
  13.             new DelayInfoProvider());  
  14.     pm.addExtensionProvider("x""jabber:x:delay"new DelayInfoProvider());  
  15.     // add carbons and forwarding  
  16.     pm.addExtensionProvider("forwarded", Forwarded.NAMESPACE,  
  17.             new Forwarded.Provider());  
  18.     pm.addExtensionProvider("sent", Carbon.NAMESPACE, new Carbon.Provider());  
  19.     pm.addExtensionProvider("received", Carbon.NAMESPACE,  
  20.             new Carbon.Provider());  
  21.     // add delivery receipts  
  22.     pm.addExtensionProvider(DeliveryReceipt.ELEMENT,  
  23.             DeliveryReceipt.NAMESPACE, new DeliveryReceipt.Provider());  
  24.     pm.addExtensionProvider(DeliveryReceiptRequest.ELEMENT,  
  25.             DeliveryReceipt.NAMESPACE,  
  26.             new DeliveryReceiptRequest.Provider());  
  27.     // add XMPP Ping (XEP-0199)  
  28.     pm.addIQProvider("ping""urn:xmpp:ping"new PingProvider());  
  29.   
  30.     ServiceDiscoveryManager.setIdentityName(XMPP_IDENTITY_NAME);  
  31.     ServiceDiscoveryManager.setIdentityType(XMPP_IDENTITY_TYPE);  
  32. }  

第二步,当用户输入账号密码时,在服务中开启新线程启动连接服务器,传入参数信息(服务器、账号密码等)实现登录,同时会将登陆成功与否信息通过回调函数通知界面。也是比较简单的:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public boolean login(String account, String password) throws XXException {// 登陆实现  
  2.         try {  
  3.             if (mXMPPConnection.isConnected()) {// 首先判断是否还连接着服务器,需要先断开  
  4.                 try {  
  5.                     mXMPPConnection.disconnect();  
  6.                 } catch (Exception e) {  
  7.                     L.d("conn.disconnect() failed: " + e);  
  8.                 }  
  9.             }  
  10.             SmackConfiguration.setPacketReplyTimeout(PACKET_TIMEOUT);// 设置超时时间  
  11.             SmackConfiguration.setKeepAliveInterval(-1);  
  12.             SmackConfiguration.setDefaultPingInterval(0);  
  13.             registerRosterListener();// 监听联系人动态变化  
  14.             mXMPPConnection.connect();  
  15.             if (!mXMPPConnection.isConnected()) {  
  16.                 throw new XXException("SMACK connect failed without exception!");  
  17.             }  
  18.             mXMPPConnection.addConnectionListener(new ConnectionListener() {  
  19.                 public void connectionClosedOnError(Exception e) {  
  20.                     mService.postConnectionFailed(e.getMessage());// 连接关闭时,动态反馈给服务  
  21.                 }  
  22.   
  23.                 public void connectionClosed() {  
  24.                 }  
  25.   
  26.                 public void reconnectingIn(int seconds) {  
  27.                 }  
  28.   
  29.                 public void reconnectionFailed(Exception e) {  
  30.                 }  
  31.   
  32.                 public void reconnectionSuccessful() {  
  33.                 }  
  34.             });  
  35.             initServiceDiscovery();// 与服务器交互消息监听,发送消息需要回执,判断是否发送成功  
  36.             // SMACK auto-logins if we were authenticated before  
  37.             if (!mXMPPConnection.isAuthenticated()) {  
  38.                 String ressource = PreferenceUtils.getPrefString(mService,  
  39.                         PreferenceConstants.RESSOURCE, XMPP_IDENTITY_NAME);  
  40.                 mXMPPConnection.login(account, password, ressource);  
  41.             }  
  42.             setStatusFromConfig();// 更新在线状态  
  43.   
  44.         } catch (XMPPException e) {  
  45.             throw new XXException(e.getLocalizedMessage(),  
  46.                     e.getWrappedThrowable());  
  47.         } catch (Exception e) {  
  48.             // actually we just care for IllegalState or NullPointer or XMPPEx.  
  49.             L.e(SmackImpl.class"login(): " + Log.getStackTraceString(e));  
  50.             throw new XXException(e.getLocalizedMessage(), e.getCause());  
  51.         }  
  52.         registerAllListener();// 注册监听其他的事件,比如新消息  
  53.         return mXMPPConnection.isAuthenticated();  
  54.     }  

第三步比较关健,登陆成功后,我们就必须要监听服务器的各种消息状态变化,以及要维持自身的一个稳定性,即保持长连接和掉线自动重连。下面是注册所有监听的函数:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. private void registerAllListener() {  
  2.     // actually, authenticated must be true now, or an exception must have  
  3.     // been thrown.  
  4.     if (isAuthenticated()) {  
  5.         registerMessageListener();// 注册新消息监听  
  6.         registerMessageSendFailureListener();// 注册消息发送失败监听  
  7.         registerPongListener();// 注册服务器回应ping消息监听  
  8.         sendOfflineMessages();// 发送离线消息  
  9.         if (mService == null) {  
  10.             mXMPPConnection.disconnect();  
  11.             return;  
  12.         }  
  13.         // we need to "ping" the service to let it know we are actually  
  14.         // connected, even when no roster entries will come in  
  15.         mService.rosterChanged();  
  16.     }  
  17. }  

①.注册联系人动态变化监听 :第一次登陆时要同步本地数据库与服务器数据库的联系人,同时处理连接过程中联系人动态变化,比如说好友切换在线状态、有人申请加好友等。我这里没有将动态变化直接通知到界面线程,而是直接更新联系人数据库Roster.db,因为:我在界面线程监听了联系人数据库的动态变化,这就是ContentProvider的好处,下篇文章细说,这里就只提及一下。下面是关键部分代码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. private void registerRosterListener() {  
  2.     mRoster = mXMPPConnection.getRoster();  
  3.     mRosterListener = new RosterListener() {  
  4.         private boolean isFristRoter;  
  5.   
  6.         @Override  
  7.         public void presenceChanged(Presence presence) {// 联系人状态改变,比如在线或离开、隐身之类  
  8.             L.i("presenceChanged(" + presence.getFrom() + "): " + presence);  
  9.             String jabberID = getJabberID(presence.getFrom());  
  10.             RosterEntry rosterEntry = mRoster.getEntry(jabberID);  
  11.             updateRosterEntryInDB(rosterEntry);// 更新联系人数据库  
  12.             mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线  
  13.         }  
  14.   
  15.         @Override  
  16.         public void entriesUpdated(Collection<String> entries) {// 更新数据库,第一次登陆  
  17.             // TODO Auto-generated method stub  
  18.             L.i("entriesUpdated(" + entries + ")");  
  19.             for (String entry : entries) {  
  20.                 RosterEntry rosterEntry = mRoster.getEntry(entry);  
  21.                 updateRosterEntryInDB(rosterEntry);  
  22.             }  
  23.             mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线  
  24.         }  
  25.   
  26.         @Override  
  27.         public void entriesDeleted(Collection<String> entries) {// 有好友删除时,  
  28.             L.i("entriesDeleted(" + entries + ")");  
  29.             for (String entry : entries) {  
  30.                 deleteRosterEntryFromDB(entry);  
  31.             }  
  32.             mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线  
  33.         }  
  34.   
  35.         @Override  
  36.         public void entriesAdded(Collection<String> entries) {// 有人添加好友时,我这里没有弹出对话框确认,直接添加到数据库  
  37.             L.i("entriesAdded(" + entries + ")");  
  38.             ContentValues[] cvs = new ContentValues[entries.size()];  
  39.             int i = 0;  
  40.             for (String entry : entries) {  
  41.                 RosterEntry rosterEntry = mRoster.getEntry(entry);  
  42.                 cvs[i++] = getContentValuesForRosterEntry(rosterEntry);  
  43.             }  
  44.             mContentResolver.bulkInsert(RosterProvider.CONTENT_URI, cvs);  
  45.             if (isFristRoter) {  
  46.                 isFristRoter = false;  
  47.                 mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线  
  48.             }  
  49.         }  
  50.     };  
  51.     mRoster.addRosterListener(mRosterListener);  
  52. }  

②.注册消息监听 ,也跟联系人动态监听是一样的处理方式,将消息的动态变化同步到消息数据库Chat.db,并未直接通知界面,界面也是通过监听数据库变化来作出动态变化的。下面是关键代码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. private void registerMessageListener() {  
  2.         // do not register multiple packet listeners  
  3.         if (mPacketListener != null)  
  4.             mXMPPConnection.removePacketListener(mPacketListener);  
  5.   
  6.         PacketTypeFilter filter = new PacketTypeFilter(Message.class);  
  7.   
  8.         mPacketListener = new PacketListener() {  
  9.             public void processPacket(Packet packet) {  
  10.                 try {  
  11.                     if (packet instanceof Message) {// 如果是消息类型  
  12.                         Message msg = (Message) packet;  
  13.                         String chatMessage = msg.getBody();  
  14.   
  15.                         // try to extract a carbon  
  16.                         Carbon cc = CarbonManager.getCarbon(msg);  
  17.                         if (cc != null  
  18.                                 && cc.getDirection() == Carbon.Direction.received) {// 收到的消息  
  19.                             L.d("carbon: " + cc.toXML());  
  20.                             msg = (Message) cc.getForwarded()  
  21.                                     .getForwardedPacket();  
  22.                             chatMessage = msg.getBody();  
  23.                             // fall through  
  24.                         } else if (cc != null  
  25.                                 && cc.getDirection() == Carbon.Direction.sent) {// 如果是自己发送的消息,则添加到数据库后直接返回  
  26.                             L.d("carbon: " + cc.toXML());  
  27.                             msg = (Message) cc.getForwarded()  
  28.                                     .getForwardedPacket();  
  29.                             chatMessage = msg.getBody();  
  30.                             if (chatMessage == null)  
  31.                                 return;  
  32.                             String fromJID = getJabberID(msg.getTo());  
  33.   
  34.                             addChatMessageToDB(ChatConstants.OUTGOING, fromJID,  
  35.                                     chatMessage, ChatConstants.DS_SENT_OR_READ,  
  36.                                     System.currentTimeMillis(),  
  37.                                     msg.getPacketID());  
  38.                             // always return after adding  
  39.                             return;// 记得要返回  
  40.                         }  
  41.   
  42.                         if (chatMessage == null) {  
  43.                             return;// 如果消息为空,直接返回了  
  44.                         }  
  45.   
  46.                         if (msg.getType() == Message.Type.error) {  
  47.                             chatMessage = "<Error> " + chatMessage;// 错误的消息类型  
  48.                         }  
  49.   
  50.                         long ts;// 消息时间戳  
  51.                         DelayInfo timestamp = (DelayInfo) msg.getExtension(  
  52.                                 "delay""urn:xmpp:delay");  
  53.                         if (timestamp == null)  
  54.                             timestamp = (DelayInfo) msg.getExtension("x",  
  55.                                     "jabber:x:delay");  
  56.                         if (timestamp != null)  
  57.                             ts = timestamp.getStamp().getTime();  
  58.                         else  
  59.                             ts = System.currentTimeMillis();  
  60.   
  61.                         String fromJID = getJabberID(msg.getFrom());// 消息来自对象  
  62.   
  63.                         addChatMessageToDB(ChatConstants.INCOMING, fromJID,  
  64.                                 chatMessage, ChatConstants.DS_NEW, ts,  
  65.                                 msg.getPacketID());// 存入数据库,并标记为新消息DS_NEW  
  66.                         mService.newMessage(fromJID, chatMessage);// 通知service,处理是否需要显示通知栏,  
  67.                     }  
  68.                 } catch (Exception e) {  
  69.                     // SMACK silently discards exceptions dropped from  
  70.                     // processPacket :(  
  71.                     L.e("failed to process packet:");  
  72.                     e.printStackTrace();  
  73.                 }  
  74.             }  
  75.         };  
  76.   
  77.         mXMPPConnection.addPacketListener(mPacketListener, filter);// 这是最关健的了,少了这句,前面的都是白费功夫  
  78.     }  

③.启动保持长连接任务 。我这里与服务器保持长连接,其实是通过每隔一段时间(本应用是15分钟)去ping一次服务器,服务器收到此ping消息,会对应的回复一个pong消息,完成一次ping-pong的过程,我们暂且叫它为心跳。此ping-pong过程有一个唯一的id,用来区分每一次的ping-pong记录。为了保证应用在系统休眠时也能启动ping的任务,我们使用了闹钟服务,而不是定时器,关于闹钟服务具体使用,请参看我之前的博客: Android中的定时器AlarmManager  。具体操作是:

从连上服务器完成登录15分钟后,闹钟响起,开始给服务器发送一条ping消息(随机生成一唯一ID),同时启动超时闹钟(本应用是30+3秒),如果服务器在30+3秒内回复了一条pong消息(与之前发送的ping消息ID相同),代表与服务器任然保持连接,则取消超时闹钟,完成一次ping-pong过程。如果在30+3秒内服务器未响应,或者回复的pong消息与之前发送的ping消息ID不一致,则认为与服务器已经断开。此时,将此消息反馈给界面,同时启动重连任务。实现长连接。

关健代码如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.     /***************** start 处理ping服务器消息 ***********************/  
  2.     private void registerPongListener() {  
  3.         // reset ping expectation on new connection  
  4.         mPingID = null;// 初始化ping的id  
  5.   
  6.         if (mPongListener != null)  
  7.             mXMPPConnection.removePacketListener(mPongListener);// 先移除之前监听对象  
  8.   
  9.         mPongListener = new PacketListener() {  
  10.   
  11.             @Override  
  12.             public void processPacket(Packet packet) {  
  13.                 if (packet == null)  
  14.                     return;  
  15.   
  16.                 if (packet.getPacketID().equals(mPingID)) {// 如果服务器返回的消息为ping服务器时的消息,说明没有掉线  
  17.                     L.i(String.format(  
  18.                             "Ping: server latency %1.3fs",  
  19.                             (System.currentTimeMillis() - mPingTimestamp) / 1000.));  
  20.                     mPingID = null;  
  21.                     ((AlarmManager) mService  
  22.                             .getSystemService(Context.ALARM_SERVICE))  
  23.                             .cancel(mPongTimeoutAlarmPendIntent);// 取消超时闹钟  
  24.                 }  
  25.             }  
  26.   
  27.         };  
  28.   
  29.         mXMPPConnection.addPacketListener(mPongListener, new PacketTypeFilter(  
  30.                 IQ.class));// 正式开始监听  
  31.         mPingAlarmPendIntent = PendingIntent.getBroadcast(  
  32.                 mService.getApplicationContext(), 0, mPingAlarmIntent,  
  33.                 PendingIntent.FLAG_UPDATE_CURRENT);// 定时ping服务器,以此来确定是否掉线  
  34.         mPongTimeoutAlarmPendIntent = PendingIntent.getBroadcast(  
  35.                 mService.getApplicationContext(), 0, mPongTimeoutAlarmIntent,  
  36.                 PendingIntent.FLAG_UPDATE_CURRENT);// 超时闹钟  
  37.         mService.registerReceiver(mPingAlarmReceiver, new IntentFilter(  
  38.                 PING_ALARM));// 注册定时ping服务器广播接收者  
  39.         mService.registerReceiver(mPongTimeoutAlarmReceiver, new IntentFilter(  
  40.                 PONG_TIMEOUT_ALARM));// 注册连接超时广播接收者  
  41.         ((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE))  
  42.                 .setInexactRepeating(AlarmManager.RTC_WAKEUP,  
  43.                         System.currentTimeMillis()  
  44.                                 + AlarmManager.INTERVAL_FIFTEEN_MINUTES,  
  45.                         AlarmManager.INTERVAL_FIFTEEN_MINUTES,  
  46.                         mPingAlarmPendIntent);// 15分钟ping以此服务器  
  47.     }  
  48.   
  49.     /** 
  50.      * BroadcastReceiver to trigger reconnect on pong timeout. 
  51.      */  
  52.     private class PongTimeoutAlarmReceiver extends BroadcastReceiver {  
  53.         public void onReceive(Context ctx, Intent i) {  
  54.             L.d("Ping: timeout for " + mPingID);  
  55.             mService.postConnectionFailed(XXService.PONG_TIMEOUT);  
  56.             //logout();// 超时就断开连接  
  57.         }  
  58.     }  
  59.   
  60.     /** 
  61.      * BroadcastReceiver to trigger sending pings to the server 
  62.      */  
  63.     private class PingAlarmReceiver extends BroadcastReceiver {  
  64.         public void onReceive(Context ctx, Intent i) {  
  65.             if (mXMPPConnection.isAuthenticated()) {  
  66.                 sendServerPing();// 收到ping服务器的闹钟,即ping一下服务器  
  67.             } else  
  68.                 L.d("Ping: alarm received, but not connected to server.");  
  69.         }  
  70.     }  
  71. public void sendServerPing() {  
  72.         if (mPingID != null) {// 此时说明上一次ping服务器还未回应,直接返回,直到连接超时  
  73.             L.d("Ping: requested, but still waiting for " + mPingID);  
  74.             return// a ping is still on its way  
  75.         }  
  76.         Ping ping = new Ping();  
  77.         ping.setType(Type.GET);  
  78.         ping.setTo(PreferenceUtils.getPrefString(mService,  
  79.                 PreferenceConstants.Server, PreferenceConstants.GMAIL_SERVER));  
  80.         mPingID = ping.getPacketID();// 此id其实是随机生成,但是唯一的  
  81.         mPingTimestamp = System.currentTimeMillis();  
  82.         L.d("Ping: sending ping " + mPingID);  
  83.         mXMPPConnection.sendPacket(ping);// 发送ping消息  
  84.   
  85.         // register ping timeout handler: PACKET_TIMEOUT(30s) + 3s  
  86.         ((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE)).set(  
  87.                 AlarmManager.RTC_WAKEUP, System.currentTimeMillis()  
  88.                         + PACKET_TIMEOUT + 3000, mPongTimeoutAlarmPendIntent);// 此时需要启动超时判断的闹钟了,时间间隔为30+3秒  
  89.     }  

④.如果与服务器连接超时 ,则进入了我们掉线重连的任务了,因为 mService .postConnectionFailed(XXService. PONG_TIMEOUT );回反馈到服务中,此时,我们会判断使用是否开启了掉线重连,关健代码如下,首先将消息由子线程发送到界面线程,文章开头说了,我们的连接是在新的线程中执行的:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public void postConnectionFailed(final String reason) {  
  2.         mMainHandler.post(new Runnable() {  
  3.             public void run() {  
  4.                 connectionFailed(reason);  
  5.             }  
  6.         });  
  7.     }  
  8.   
  9.     private void connectionFailed(String reason) {  
  10.         L.i(XXService.class"connectionFailed: " + reason);  
  11.         mConnectedState = DISCONNECTED;// 更新当前连接状态  
  12.         if (mSmackable != null)  
  13.             mSmackable.setStatusOffline();// 将所有联系人标记为离线  
  14.         if (TextUtils.equals(reason, LOGOUT)) {// 如果是手动退出  
  15.             ((AlarmManager) getSystemService(Context.ALARM_SERVICE))  
  16.                     .cancel(mPAlarmIntent);  
  17.             return;  
  18.         }  
  19.         // 回调  
  20.         if (mConnectionStatusCallback != null) {  
  21.             mConnectionStatusCallback.connectionStatusChanged(mConnectedState,  
  22.                     reason);  
  23.             if (mIsFirstLoginAction)// 如果是第一次登录,就算登录失败也不需要继续  
  24.                 return;  
  25.         }  
  26.   
  27.         // 无网络连接时,直接返回  
  28.         if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) {  
  29.             ((AlarmManager) getSystemService(Context.ALARM_SERVICE))  
  30.                     .cancel(mPAlarmIntent);  
  31.             return;  
  32.         }  
  33.   
  34.         String account = PreferenceUtils.getPrefString(XXService.this,  
  35.                 PreferenceConstants.ACCOUNT, "");  
  36.         String password = PreferenceUtils.getPrefString(XXService.this,  
  37.                 PreferenceConstants.PASSWORD, "");  
  38.         // 无保存的帐号密码时,也直接返回  
  39.         if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) {  
  40.             L.d("account = null || password = null");  
  41.             return;  
  42.         }  
  43.         // 如果不是手动退出并且需要重新连接,则开启重连闹钟  
  44.         if (PreferenceUtils.getPrefBoolean(this,  
  45.                 PreferenceConstants.AUTO_RECONNECT, true)) {  
  46.             L.d("connectionFailed(): registering reconnect in "  
  47.                     + mReconnectTimeout + "s");  
  48.             ((AlarmManager) getSystemService(Context.ALARM_SERVICE)).set(  
  49.                     AlarmManager.RTC_WAKEUP, System.currentTimeMillis()  
  50.                             + mReconnectTimeout * 1000, mPAlarmIntent);  
  51.             mReconnectTimeout = mReconnectTimeout * 2;  
  52.             if (mReconnectTimeout > RECONNECT_MAXIMUM)  
  53.                 mReconnectTimeout = RECONNECT_MAXIMUM;  
  54.         } else {  
  55.             ((AlarmManager) getSystemService(Context.ALARM_SERVICE))  
  56.                     .cancel(mPAlarmIntent);  
  57.         }  
  58.   
  59.     }  

从上述代码中可以看出,在connectionFailed函数中,我们除了将此消息通知界面,同时会根据不同的reason来判断是否需要重连,如果是用户手动退出reason=LOGOUT,则直接返回咯,否则也是开启一个闹钟,启动重新连接任务,下面是该闹钟的接收处理:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 自动重连广播接收者  
  2. private class ReconnectAlarmReceiver extends BroadcastReceiver {  
  3.     public void onReceive(Context ctx, Intent i) {  
  4.         L.d("Alarm received.");  
  5.         if (!PreferenceUtils.getPrefBoolean(XXService.this,  
  6.                 PreferenceConstants.AUTO_RECONNECT, true)) {  
  7.             return;  
  8.         }  
  9.         if (mConnectedState != DISCONNECTED) {  
  10.             L.d("Reconnect attempt aborted: we are connected again!");  
  11.             return;  
  12.         }  
  13.         String account = PreferenceUtils.getPrefString(XXService.this,  
  14.                 PreferenceConstants.ACCOUNT, "");  
  15.         String password = PreferenceUtils.getPrefString(XXService.this,  
  16.                 PreferenceConstants.PASSWORD, "");  
  17.         if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) {  
  18.             L.d("account = null || password = null");  
  19.             return;  
  20.         }  
  21.         Login(account, password);  
  22.     }  
  23. }  

是不是这样就实现了长连接呢?也许高兴得太早了,我们还有一个重要的因素没有考虑到,对了,就是手机网络,因为很多手机在系统休眠的时候是会断开网络连接的(应该是为了省电吧),所以,我们必须要动态监听网络变化,来做出处理,以下是关键代码:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public void onNetChange() {  
  2.         if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) {// 如果是网络断开,不作处理  
  3.             connectionFailed(NETWORK_ERROR);  
  4.             return;  
  5.         }  
  6.         if (isAuthenticated())// 如果已经连接上,直接返回  
  7.             return;  
  8.         String account = PreferenceUtils.getPrefString(XXService.this,  
  9.                 PreferenceConstants.ACCOUNT, "");  
  10.         String password = PreferenceUtils.getPrefString(XXService.this,  
  11.                 PreferenceConstants.PASSWORD, "");  
  12.         if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password))// 如果没有帐号,也直接返回  
  13.             return;  
  14.         if (!PreferenceUtils.getPrefBoolean(this,  
  15.                 PreferenceConstants.AUTO_RECONNECT, true))// 不需要重连  
  16.             return;  
  17.         Login(account, password);// 重连  
  18.     }  

OK,与服务器保持长连接,基本上就是这样了,其实还有一些问题没有考虑到,比如说内存过低,服务被系统回收,我们是没有考虑到的,这个就留个读者一个思考吧,我的想法是:在用户唤醒系统时也启动一次服务,接收此广播:

 <action android:name="android.intent.action.USER_PRESENT" />


⑤.实现服务在前台运行:这个我在之前的文章中有介绍过:Android之后台服务判断本应用Activity是否处于栈顶,这里就不在赘述了。


第四步是用户主动退出,注销登陆,这个好像没有多少需要介绍的,无法是释放掉一些资源,关闭一些服务等等,也无需多说。看看代码即可:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. // 退出  
  2.     public boolean logout() {  
  3.         // mIsNeedReConnection = false;// 手动退出就不需要重连闹钟了  
  4.         boolean isLogout = false;  
  5.         if (mConnectingThread != null) {  
  6.             synchronized (mConnectingThread) {  
  7.                 try {  
  8.                     mConnectingThread.interrupt();  
  9.                     mConnectingThread.join(50);  
  10.                 } catch (InterruptedException e) {  
  11.                     L.e("doDisconnect: failed catching connecting thread");  
  12.                 } finally {  
  13.                     mConnectingThread = null;  
  14.                 }  
  15.             }  
  16.         }  
  17.         if (mSmackable != null) {  
  18.             isLogout = mSmackable.logout();  
  19.             mSmackable = null;  
  20.         }  
  21.         connectionFailed(LOGOUT);// 手动退出  
  22.         return isLogout;  
  23.     }  



好了,整个控制层大概就讲到这里,总结一下:

重要的是第三步:注册监听和长连接的处理,其中长连接处理也是最为关键和麻烦的。

文章比较长,其实也花了我几个小时的时间,首先感谢你看到了文章末尾,由于个人水平限制,难免会有一些失误或者不准确的地方,欢迎大家批评指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值