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.MessageListener; 024import org.jivesoftware.smack.SmackException.NotConnectedException; 025import org.jivesoftware.smack.XMPPConnection; 026import org.jivesoftware.smack.Manager; 027import org.jivesoftware.smack.chat.Chat; 028import org.jivesoftware.smack.chat.ChatManager; 029import org.jivesoftware.smack.chat.ChatManagerListener; 030import org.jivesoftware.smack.chat.ChatMessageListener; 031import org.jivesoftware.smack.filter.NotFilter; 032import org.jivesoftware.smack.filter.StanzaExtensionFilter; 033import org.jivesoftware.smack.filter.StanzaFilter; 034import org.jivesoftware.smack.packet.Message; 035import org.jivesoftware.smack.packet.ExtensionElement; 036import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; 037import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 038 039/** 040 * Handles chat state for all chats on a particular XMPPConnection. This class manages both the 041 * stanza(/packet) extensions and the disco response necessary for compliance with 042 * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>. 043 * 044 * NOTE: {@link org.jivesoftware.smackx.chatstates.ChatStateManager#getInstance(org.jivesoftware.smack.XMPPConnection)} 045 * needs to be called in order for the listeners to be registered appropriately with the connection. 046 * If this does not occur you will not receive the update notifications. 047 * 048 * @author Alexander Wenckus 049 * @see org.jivesoftware.smackx.chatstates.ChatState 050 * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension 051 */ 052public class ChatStateManager extends Manager { 053 public static final String NAMESPACE = "http://jabber.org/protocol/chatstates"; 054 055 private static final Map<XMPPConnection, ChatStateManager> INSTANCES = 056 new WeakHashMap<XMPPConnection, ChatStateManager>(); 057 058 private static final StanzaFilter filter = new NotFilter(new StanzaExtensionFilter(NAMESPACE)); 059 060 /** 061 * Returns the ChatStateManager related to the XMPPConnection and it will create one if it does 062 * not yet exist. 063 * 064 * @param connection the connection to return the ChatStateManager 065 * @return the ChatStateManager related the the connection. 066 */ 067 public static synchronized ChatStateManager getInstance(final XMPPConnection connection) { 068 ChatStateManager manager = INSTANCES.get(connection); 069 if (manager == null) { 070 manager = new ChatStateManager(connection); 071 } 072 return manager; 073 } 074 075 private final OutgoingMessageInterceptor outgoingInterceptor = new OutgoingMessageInterceptor(); 076 077 private final IncomingMessageInterceptor incomingInterceptor = new IncomingMessageInterceptor(); 078 079 /** 080 * Maps chat to last chat state. 081 */ 082 private final Map<Chat, ChatState> chatStates = new WeakHashMap<Chat, ChatState>(); 083 084 private final ChatManager chatManager; 085 086 private ChatStateManager(XMPPConnection connection) { 087 super(connection); 088 chatManager = ChatManager.getInstanceFor(connection); 089 chatManager.addOutgoingMessageInterceptor(outgoingInterceptor, filter); 090 chatManager.addChatListener(incomingInterceptor); 091 092 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE); 093 INSTANCES.put(connection, this); 094 } 095 096 097 /** 098 * Sets the current state of the provided chat. This method will send an empty bodied Message 099 * stanza(/packet) with the state attached as a {@link org.jivesoftware.smack.packet.ExtensionElement}, if 100 * and only if the new chat state is different than the last state. 101 * 102 * @param newState the new state of the chat 103 * @param chat the chat. 104 * @throws NotConnectedException 105 */ 106 public void setCurrentState(ChatState newState, Chat chat) throws NotConnectedException { 107 if(chat == null || newState == null) { 108 throw new IllegalArgumentException("Arguments cannot be null."); 109 } 110 if(!updateChatState(chat, newState)) { 111 return; 112 } 113 Message message = new Message(); 114 ChatStateExtension extension = new ChatStateExtension(newState); 115 message.addExtension(extension); 116 117 chat.sendMessage(message); 118 } 119 120 121 public boolean equals(Object o) { 122 if (this == o) return true; 123 if (o == null || getClass() != o.getClass()) return false; 124 125 ChatStateManager that = (ChatStateManager) o; 126 127 return connection().equals(that.connection()); 128 129 } 130 131 public int hashCode() { 132 return connection().hashCode(); 133 } 134 135 private synchronized boolean updateChatState(Chat chat, ChatState newState) { 136 ChatState lastChatState = chatStates.get(chat); 137 if (lastChatState != newState) { 138 chatStates.put(chat, newState); 139 return true; 140 } 141 return false; 142 } 143 144 private void fireNewChatState(Chat chat, ChatState state) { 145 for (ChatMessageListener listener : chat.getListeners()) { 146 if (listener instanceof ChatStateListener) { 147 ((ChatStateListener) listener).stateChanged(chat, state); 148 } 149 } 150 } 151 152 private class OutgoingMessageInterceptor implements MessageListener { 153 154 @Override 155 public void processMessage(Message message) { 156 Chat chat = chatManager.getThreadChat(message.getThread()); 157 if (chat == null) { 158 return; 159 } 160 if (updateChatState(chat, ChatState.active)) { 161 message.addExtension(new ChatStateExtension(ChatState.active)); 162 } 163 } 164 } 165 166 private class IncomingMessageInterceptor implements ChatManagerListener, ChatMessageListener { 167 168 public void chatCreated(final Chat chat, boolean createdLocally) { 169 chat.addMessageListener(this); 170 } 171 172 public void processMessage(Chat chat, Message message) { 173 ExtensionElement extension = message.getExtension(NAMESPACE); 174 if (extension == null) { 175 return; 176 } 177 178 ChatState state; 179 try { 180 state = ChatState.valueOf(extension.getElementName()); 181 } 182 catch (Exception ex) { 183 return; 184 } 185 186 fireNewChatState(chat, state); 187 } 188 } 189}