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

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的参数配置,我这里是放在类的静态块里面完成的:

[java] view plaincopyprint?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);

}

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);
}

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

[java] view plaincopyprint?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();
}

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();
}

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

[java] view plaincopyprint?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 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();
    }
}

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

[java] view plaincopyprint?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 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);
}

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

[java] view plaincopyprint?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);// 这是最关健的了,少了这句,前面的都是白费功夫
}

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一次服务器,服务器收到此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 plaincopyprint? / 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秒
}  

/***************** 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秒
}

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

[java] view plaincopyprint?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);
    }  

}

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);
    }

}

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

[java] view plaincopyprint?// 自动重连广播接收者
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);
}

}

// 自动重连广播接收者
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);
    }
}

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

[java] view plaincopyprint?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);// 重连
}

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);// 重连
}

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

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

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

[java] view plaincopyprint?// 退出

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;
}

// 退出

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;
}

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

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

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

时间: 2025-01-03 16:26:27

Android之基于XMPP协议即时通讯软件(三)的相关文章

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

http://blog.csdn.net/way_ping_li/article/details/17385379 http://git.oschina.net/way/XMPP

基于XMPP协议的Android即时通信系

以前做过一个基于XMPP协议的聊天社交软件,总结了一下.发出来. 设计基于开源的XMPP即时通信协议,采用C/S体系结构,通过GPRS无线网络用TCP协议连接到服务器,以架设开源的Openfn'e服务器作为即时通讯平台. 系统主要由以下部分组成:一是服务器,负责管理发出的连接或者与其他实体的会话,接收或转发XML(ExtensibleMarkup Language)流元素给授权的客户端.服务器等:二是客户终端.它与服务器相连,通过XMPP获得由服务器或任何其它相关的服务所提供的全部功能.三是协议

[转] 基于XMPP协议的Android即时通信系

转自:http://blog.csdn.net/lnb333666/article/details/7471292 以前做过一个基于XMPP协议的聊天社交软件,总结了一下.发出来. 设计基于开源的XMPP即时通信协议,采用C/S体系结构,通过GPRS无线网络用TCP协议连接到服务器,以架设开源的Openfn'e服务器作为即时通讯平台. 系统主要由以下部分组成:一是服务器,负责管理发出的连接或者与其他实体的会话,接收或转发XML(ExtensibleMarkup Language)流元素给授权的客

基于XMPP协议的手机多方多端即时通讯方案

目   录 基于XMPP协议的手机多方多端即时通讯方案................................................................. 1 目   录.................................................................................................................... 2 一. 开发背景........................

基于XMPP协议聊天程序【Openfire+asmark】

    本文章提供了实现IM聊天程序最基础的配置和使用案例,可以实现点对点聊天.可做为入门阅读使用.文章转载请注明来源:http://blog.csdn.net/fengfeng91 一:搭建服务器: 官方网站下载openfire服务器安装.配置密码,权限,数据库(内置数据库或者添加外部数据库支持Mysql) 注意* 如果配置外部数据库,需先保证数据库服务已被开启,才能保证服务器开启. Windows环境下开启抛出异常/乱码时,关闭服务器,以管理员身份运行,当出现以下提示时,表示服务器启动成功.

IOS基于XMPP协议开发--XMPPFramewok框架(一):基础知识

最近蘑菇街团队的TT的开源,使我对im产生了兴趣,然后在网上找到了XMPPFramework进行学习研究, 并写了以下系列教程供大家参考,有写的不对的地方,请大家多多包涵指正. 目录索引 IOS基于XMPP协议开发--XMPPFramewok框架(一):基础知识 IOS基于XMPP协议开发--XMPPFramewok框架(二):服务器连接 IOS基于XMPP协议开发--XMPPFramewok框架(三):用户注册 IOS基于XMPP协议开发--XMPPFramewok框架(四):用户认证 IOS

.net平台 基于 XMPP协议的即时消息服务端简单实现

.net平台 基于 XMPP协议的即时消息服务端简单实现 昨天抽空学习了一下XMPP,在网上找了好久,中文的资料太少了所以做这个简单的例子,今天才完成.公司也正在准备开发基于XMPP协议的即时通讯工具所以也算是打一个基础吧!如果你还没有了解过XMPP请先阅读附录中链接的文章,本实例是基agsXMPP上开发的,agsXMPP是C#写的支持开源XMPP协议软件,我们可以在agsXMPP上快速构建自已的即时通讯平台,我的这个例子只是修改了服务器端,因为agsXMPP本身自带的服务器端没有实现聊天功能.

Android实现基于http协议的文件下载测试

  概述   网络编程中文件的上传下载是最常见的场景,本着不重复造轮子的原则,日常工作如果遇到相关问题我们首先想到的可能是从网上找现成的代码直接拿来用,很少去关心具体是如何实现的,可能也是没时间去研究别人如何实现.如果代码能够满足我们现阶段的要求,则万事大吉,但是如果使用代码的过程中出现意想不到的问题,我们解决起来可能会比较麻烦,因为代码不是我们自己写的,对代码不熟,不能快速的查找问题的原因.个人认为不重复造轮子的前提是你必须有能力造一个相同的轮子,这样在使用别人的代码时才能更加得心应手.   

基于XMPP 协议的开发 android

设计过一款基于开源的XMPP即时通信协议的软件.採用C/S协议,通过GPRS无线网络用TCP协议到server.以架设开源的Openfire server作为即时通讯平台 系统主要由下面部分组成:一是:server,负责管理发出的链接或者其他实体的会话.接收或转发XML 数据给client,它与server链接,通过XMPP协议获得由server或不论什么其他相关的服务锁提供的所有功能,三是协议网关的信息与外部消息系统但是不信息间的翻译,再就是XMPP网络,实现各个服务 client间的链接.系