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.Manager;
024import org.jivesoftware.smack.MessageListener;
025import org.jivesoftware.smack.SmackException.NotConnectedException;
026import org.jivesoftware.smack.XMPPConnection;
027import org.jivesoftware.smack.chat.ChatManagerListener;
028import org.jivesoftware.smack.chat.ChatMessageListener;
029import org.jivesoftware.smack.filter.NotFilter;
030import org.jivesoftware.smack.filter.StanzaExtensionFilter;
031import org.jivesoftware.smack.filter.StanzaFilter;
032import org.jivesoftware.smack.packet.ExtensionElement;
033import org.jivesoftware.smack.packet.Message;
034
035import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
036import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
037
038/**
039 * Handles chat state for all chats on a particular XMPPConnection. This class manages both the
040 * stanza(/packet) extensions and the disco response necessary for compliance with
041 * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
042 *
043 * NOTE: {@link org.jivesoftware.smackx.chatstates.ChatStateManager#getInstance(org.jivesoftware.smack.XMPPConnection)}
044 * needs to be called in order for the listeners to be registered appropriately with the connection.
045 * If this does not occur you will not receive the update notifications.
046 *
047 * @author Alexander Wenckus
048 * @see org.jivesoftware.smackx.chatstates.ChatState
049 * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension
050 */
051// TODO Migrate to new chat2 API on Smack 4.3.
052@SuppressWarnings("deprecation")
053public final 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 StanzaFilter filter = new NotFilter(new StanzaExtensionFilter(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<org.jivesoftware.smack.chat.Chat, ChatState> chatStates = new WeakHashMap<>();
084
085    private final org.jivesoftware.smack.chat.ChatManager chatManager;
086
087    private ChatStateManager(XMPPConnection connection) {
088        super(connection);
089        chatManager = org.jivesoftware.smack.chat.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     * stanza(/packet) with the state attached as a {@link org.jivesoftware.smack.packet.ExtensionElement}, 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     * @throws InterruptedException 
107     */
108    public void setCurrentState(ChatState newState, org.jivesoftware.smack.chat.Chat chat) throws NotConnectedException, InterruptedException {
109        if (chat == null || newState == null) {
110            throw new IllegalArgumentException("Arguments cannot be null.");
111        }
112        if (!updateChatState(chat, newState)) {
113            return;
114        }
115        Message message = new Message();
116        ChatStateExtension extension = new ChatStateExtension(newState);
117        message.addExtension(extension);
118
119        chat.sendMessage(message);
120    }
121
122
123    @Override
124    public boolean equals(Object o) {
125        if (this == o) return true;
126        if (o == null || getClass() != o.getClass()) return false;
127
128        ChatStateManager that = (ChatStateManager) o;
129
130        return connection().equals(that.connection());
131
132    }
133
134    @Override
135    public int hashCode() {
136        return connection().hashCode();
137    }
138
139    private synchronized boolean updateChatState(org.jivesoftware.smack.chat.Chat chat, ChatState newState) {
140        ChatState lastChatState = chatStates.get(chat);
141        if (lastChatState != newState) {
142            chatStates.put(chat, newState);
143            return true;
144        }
145        return false;
146    }
147
148    private static void fireNewChatState(org.jivesoftware.smack.chat.Chat chat, ChatState state, Message message) {
149        for (ChatMessageListener listener : chat.getListeners()) {
150            if (listener instanceof ChatStateListener) {
151                ((ChatStateListener) listener).stateChanged(chat, state, message);
152            }
153        }
154    }
155
156    private class OutgoingMessageInterceptor implements MessageListener {
157
158        @Override
159        public void processMessage(Message message) {
160            org.jivesoftware.smack.chat.Chat chat = chatManager.getThreadChat(message.getThread());
161            if (chat == null) {
162                return;
163            }
164            if (updateChatState(chat, ChatState.active)) {
165                message.addExtension(new ChatStateExtension(ChatState.active));
166            }
167        }
168    }
169
170    private static class IncomingMessageInterceptor implements ChatManagerListener, ChatMessageListener {
171
172        @Override
173        public void chatCreated(final org.jivesoftware.smack.chat.Chat chat, boolean createdLocally) {
174            chat.addMessageListener(this);
175        }
176
177        @Override
178        public void processMessage(org.jivesoftware.smack.chat.Chat chat, Message message) {
179            ExtensionElement extension = message.getExtension(NAMESPACE);
180            if (extension == null) {
181                return;
182            }
183
184            ChatState state;
185            try {
186                state = ChatState.valueOf(extension.getElementName());
187            }
188            catch (Exception ex) {
189                return;
190            }
191
192            fireNewChatState(chat, state, message);
193        }
194    }
195}