001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.chatstates;
019
020import java.util.Map;
021import java.util.WeakHashMap;
022
023import org.jivesoftware.smack.Chat;
024import org.jivesoftware.smack.ChatManager;
025import org.jivesoftware.smack.ChatManagerListener;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.XMPPConnection;
028import org.jivesoftware.smack.Manager;
029import org.jivesoftware.smack.MessageListener;
030import org.jivesoftware.smack.PacketInterceptor;
031import org.jivesoftware.smack.filter.NotFilter;
032import org.jivesoftware.smack.filter.PacketExtensionFilter;
033import org.jivesoftware.smack.filter.PacketFilter;
034import org.jivesoftware.smack.packet.Message;
035import org.jivesoftware.smack.packet.Packet;
036import org.jivesoftware.smack.packet.PacketExtension;
037import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
038import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
039
040/**
041 * Handles chat state for all chats on a particular XMPPConnection. This class manages both the
042 * packet extensions and the disco response necessary for compliance with
043 * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
044 *
045 * NOTE: {@link org.jivesoftware.smackx.chatstates.ChatStateManager#getInstance(org.jivesoftware.smack.XMPPConnection)}
046 * needs to be called in order for the listeners to be registered appropriately with the connection.
047 * If this does not occur you will not receive the update notifications.
048 *
049 * @author Alexander Wenckus
050 * @see org.jivesoftware.smackx.chatstates.ChatState
051 * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension
052 */
053public class ChatStateManager extends Manager {
054    public static final String NAMESPACE = "http://jabber.org/protocol/chatstates";
055
056    private static final Map<XMPPConnection, ChatStateManager> INSTANCES =
057            new WeakHashMap<XMPPConnection, ChatStateManager>();
058
059    private static final PacketFilter filter = new NotFilter(new PacketExtensionFilter(NAMESPACE));
060
061    /**
062     * Returns the ChatStateManager related to the XMPPConnection and it will create one if it does
063     * not yet exist.
064     *
065     * @param connection the connection to return the ChatStateManager
066     * @return the ChatStateManager related the the connection.
067     */
068    public static synchronized ChatStateManager getInstance(final XMPPConnection connection) {
069            ChatStateManager manager = INSTANCES.get(connection);
070            if (manager == null) {
071                manager = new ChatStateManager(connection);
072            }
073            return manager;
074    }
075
076    private final OutgoingMessageInterceptor outgoingInterceptor = new OutgoingMessageInterceptor();
077
078    private final IncomingMessageInterceptor incomingInterceptor = new IncomingMessageInterceptor();
079
080    /**
081     * Maps chat to last chat state.
082     */
083    private final Map<Chat, ChatState> chatStates = new WeakHashMap<Chat, ChatState>();
084
085    private final ChatManager chatManager;
086
087    private ChatStateManager(XMPPConnection connection) {
088        super(connection);
089        chatManager = ChatManager.getInstanceFor(connection);
090        chatManager.addOutgoingMessageInterceptor(outgoingInterceptor, filter);
091        chatManager.addChatListener(incomingInterceptor);
092
093        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
094        INSTANCES.put(connection, this);
095    }
096
097
098    /**
099     * Sets the current state of the provided chat. This method will send an empty bodied Message
100     * packet with the state attached as a {@link org.jivesoftware.smack.packet.PacketExtension}, if
101     * and only if the new chat state is different than the last state.
102     *
103     * @param newState the new state of the chat
104     * @param chat the chat.
105     * @throws NotConnectedException 
106     */
107    public void setCurrentState(ChatState newState, Chat chat) throws NotConnectedException {
108        if(chat == null || newState == null) {
109            throw new IllegalArgumentException("Arguments cannot be null.");
110        }
111        if(!updateChatState(chat, newState)) {
112            return;
113        }
114        Message message = new Message();
115        ChatStateExtension extension = new ChatStateExtension(newState);
116        message.addExtension(extension);
117
118        chat.sendMessage(message);
119    }
120
121
122    public boolean equals(Object o) {
123        if (this == o) return true;
124        if (o == null || getClass() != o.getClass()) return false;
125
126        ChatStateManager that = (ChatStateManager) o;
127
128        return connection().equals(that.connection());
129
130    }
131
132    public int hashCode() {
133        return connection().hashCode();
134    }
135
136    private synchronized boolean updateChatState(Chat chat, ChatState newState) {
137        ChatState lastChatState = chatStates.get(chat);
138        if (lastChatState != newState) {
139            chatStates.put(chat, newState);
140            return true;
141        }
142        return false;
143    }
144
145    private void fireNewChatState(Chat chat, ChatState state) {
146        for (MessageListener listener : chat.getListeners()) {
147            if (listener instanceof ChatStateListener) {
148                ((ChatStateListener) listener).stateChanged(chat, state);
149            }
150        }
151    }
152
153    private class OutgoingMessageInterceptor implements PacketInterceptor {
154
155        public void interceptPacket(Packet packet) {
156            Message message = (Message) packet;
157            Chat chat = chatManager.getThreadChat(message.getThread());
158            if (chat == null) {
159                return;
160            }
161            if (updateChatState(chat, ChatState.active)) {
162                message.addExtension(new ChatStateExtension(ChatState.active));
163            }
164        }
165    }
166
167    private class IncomingMessageInterceptor implements ChatManagerListener, MessageListener {
168
169        public void chatCreated(final Chat chat, boolean createdLocally) {
170            chat.addMessageListener(this);
171        }
172
173        public void processMessage(Chat chat, Message message) {
174            PacketExtension extension = message.getExtension(NAMESPACE);
175            if (extension == null) {
176                return;
177            }
178
179            ChatState state;
180            try {
181                state = ChatState.valueOf(extension.getElementName());
182            }
183            catch (Exception ex) {
184                return;
185            }
186
187            fireNewChatState(chat, state);
188        }
189    }
190}