001/**
002 *
003 * Copyright 2003-2007 Jive Software, 2018 Paul Schaub.
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.ArrayList;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.WeakHashMap;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.jivesoftware.smack.AsyncButOrdered;
030import org.jivesoftware.smack.Manager;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.StanzaListener;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.chat2.Chat;
035import org.jivesoftware.smack.chat2.ChatManager;
036import org.jivesoftware.smack.chat2.OutgoingChatMessageListener;
037import org.jivesoftware.smack.filter.AndFilter;
038import org.jivesoftware.smack.filter.FromTypeFilter;
039import org.jivesoftware.smack.filter.MessageTypeFilter;
040import org.jivesoftware.smack.filter.NotFilter;
041import org.jivesoftware.smack.filter.StanzaExtensionFilter;
042import org.jivesoftware.smack.filter.StanzaFilter;
043import org.jivesoftware.smack.packet.ExtensionElement;
044import org.jivesoftware.smack.packet.Message;
045import org.jivesoftware.smack.packet.Stanza;
046import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
047import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
048
049import org.jxmpp.jid.EntityBareJid;
050import org.jxmpp.jid.EntityFullJid;
051
052/**
053 * Handles chat state for all chats on a particular XMPPConnection. This class manages both the
054 * stanza extensions and the disco response necessary for compliance with
055 * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
056 *
057 * NOTE: {@link org.jivesoftware.smackx.chatstates.ChatStateManager#getInstance(org.jivesoftware.smack.XMPPConnection)}
058 * needs to be called in order for the listeners to be registered appropriately with the connection.
059 * If this does not occur you will not receive the update notifications.
060 *
061 * @author Alexander Wenckus
062 * @author Paul Schaub
063 * @see org.jivesoftware.smackx.chatstates.ChatState
064 * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension
065 */
066public final class ChatStateManager extends Manager {
067
068    private static final Logger LOGGER = Logger.getLogger(ChatStateManager.class.getName());
069
070    public static final String NAMESPACE = "http://jabber.org/protocol/chatstates";
071
072    private static final Map<XMPPConnection, ChatStateManager> INSTANCES = new WeakHashMap<>();
073
074    private static final StanzaFilter filter = new NotFilter(new StanzaExtensionFilter(NAMESPACE));
075    private static final StanzaFilter INCOMING_MESSAGE_FILTER =
076            new AndFilter(MessageTypeFilter.NORMAL_OR_CHAT, FromTypeFilter.ENTITY_FULL_JID);
077    private static final StanzaFilter INCOMING_CHAT_STATE_FILTER = new AndFilter(INCOMING_MESSAGE_FILTER, new StanzaExtensionFilter(NAMESPACE));
078
079    /**
080     * Registered ChatStateListeners
081     */
082    private final Set<ChatStateListener> chatStateListeners = new HashSet<>();
083
084    /**
085     * Maps chat to last chat state.
086     */
087    private final Map<Chat, ChatState> chatStates = new WeakHashMap<>();
088
089    private final AsyncButOrdered<Chat> asyncButOrdered = new AsyncButOrdered<>();
090
091    /**
092     * Returns the ChatStateManager related to the XMPPConnection and it will create one if it does
093     * not yet exist.
094     *
095     * @param connection the connection to return the ChatStateManager
096     * @return the ChatStateManager related the the connection.
097     */
098    public static synchronized ChatStateManager getInstance(final XMPPConnection connection) {
099            ChatStateManager manager = INSTANCES.get(connection);
100            if (manager == null) {
101                manager = new ChatStateManager(connection);
102                INSTANCES.put(connection, manager);
103            }
104            return manager;
105    }
106
107    /**
108     * Private constructor to create a new ChatStateManager.
109     * This adds ChatMessageListeners as interceptors to the connection and adds the namespace to the disco features.
110     *
111     * @param connection xmpp connection
112     */
113    private ChatStateManager(XMPPConnection connection) {
114        super(connection);
115        ChatManager chatManager = ChatManager.getInstanceFor(connection);
116        chatManager.addOutgoingListener(new OutgoingChatMessageListener() {
117            @Override
118            public void newOutgoingMessage(EntityBareJid to, Message message, Chat chat) {
119                if (chat == null) {
120                    return;
121                }
122
123                // if message already has a chatStateExtension, then do nothing,
124                if (!filter.accept(message)) {
125                    return;
126                }
127
128                // otherwise add a chatState extension if necessary.
129                if (updateChatState(chat, ChatState.active)) {
130                    message.addExtension(new ChatStateExtension(ChatState.active));
131                }
132            }
133        });
134
135        connection.addSyncStanzaListener(new StanzaListener() {
136            @Override
137            public void processStanza(Stanza stanza) {
138                final Message message = (Message) stanza;
139
140                EntityFullJid fullFrom = message.getFrom().asEntityFullJidIfPossible();
141                EntityBareJid bareFrom = fullFrom.asEntityBareJid();
142
143                final Chat chat = ChatManager.getInstanceFor(connection()).chatWith(bareFrom);
144                ExtensionElement extension = message.getExtension(NAMESPACE);
145                String chatStateElementName = extension.getElementName();
146
147                ChatState state;
148                try {
149                    state = ChatState.valueOf(chatStateElementName);
150                }
151                catch (Exception ex) {
152                    LOGGER.log(Level.WARNING, "Invalid chat state element name: " + chatStateElementName, ex);
153                    return;
154                }
155                final ChatState finalState = state;
156
157                List<ChatStateListener> listeners;
158                synchronized (chatStateListeners) {
159                    listeners = new ArrayList<>(chatStateListeners.size());
160                    listeners.addAll(chatStateListeners);
161                }
162
163                final List<ChatStateListener> finalListeners = listeners;
164                asyncButOrdered.performAsyncButOrdered(chat, new Runnable() {
165                    @Override
166                    public void run() {
167                        for (ChatStateListener listener : finalListeners) {
168                            listener.stateChanged(chat, finalState, message);
169                        }
170                    }
171                });
172            }
173        }, INCOMING_CHAT_STATE_FILTER);
174
175        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
176    }
177
178    /**
179     * Register a ChatStateListener. That listener will be informed about changed chat states.
180     *
181     * @param listener chatStateListener
182     * @return true, if the listener was not registered before
183     */
184    public boolean addChatStateListener(ChatStateListener listener) {
185        synchronized (chatStateListeners) {
186            return chatStateListeners.add(listener);
187        }
188    }
189
190    /**
191     * Unregister a ChatStateListener.
192     *
193     * @param listener chatStateListener
194     * @return true, if the listener was registered before
195     */
196    public boolean removeChatStateListener(ChatStateListener listener) {
197        synchronized (chatStateListeners) {
198            return chatStateListeners.remove(listener);
199        }
200    }
201
202
203    /**
204     * Sets the current state of the provided chat. This method will send an empty bodied Message
205     * stanza with the state attached as a {@link org.jivesoftware.smack.packet.ExtensionElement}, if
206     * and only if the new chat state is different than the last state.
207     *
208     * @param newState the new state of the chat
209     * @param chat the chat.
210     * @throws NotConnectedException
211     * @throws InterruptedException
212     */
213    public void setCurrentState(ChatState newState, Chat chat) throws NotConnectedException, InterruptedException {
214        if (chat == null || newState == null) {
215            throw new IllegalArgumentException("Arguments cannot be null.");
216        }
217        if (!updateChatState(chat, newState)) {
218            return;
219        }
220        Message message = new Message();
221        ChatStateExtension extension = new ChatStateExtension(newState);
222        message.addExtension(extension);
223
224        chat.send(message);
225    }
226
227
228    @Override
229    public boolean equals(Object o) {
230        if (this == o) return true;
231        if (o == null || getClass() != o.getClass()) return false;
232
233        ChatStateManager that = (ChatStateManager) o;
234
235        return connection().equals(that.connection());
236
237    }
238
239    @Override
240    public int hashCode() {
241        return connection().hashCode();
242    }
243
244    private synchronized boolean updateChatState(Chat chat, ChatState newState) {
245        ChatState lastChatState = chatStates.get(chat);
246        if (lastChatState != newState) {
247            chatStates.put(chat, newState);
248            return true;
249        }
250        return false;
251    }
252
253}