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