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}