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