001/** 002 * 003 * Copyright © 2016 Fernando Ramirez, 2018 Florian Schmaus 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 */ 017package org.jivesoftware.smackx.chat_markers; 018 019import java.util.HashSet; 020import java.util.Map; 021import java.util.Set; 022import java.util.WeakHashMap; 023 024import org.jivesoftware.smack.AsyncButOrdered; 025import org.jivesoftware.smack.ConnectionCreationListener; 026import org.jivesoftware.smack.Manager; 027import org.jivesoftware.smack.SmackException; 028import org.jivesoftware.smack.SmackException.NoResponseException; 029import org.jivesoftware.smack.SmackException.NotConnectedException; 030import org.jivesoftware.smack.StanzaListener; 031import org.jivesoftware.smack.XMPPConnection; 032import org.jivesoftware.smack.XMPPConnectionRegistry; 033import org.jivesoftware.smack.XMPPException.XMPPErrorException; 034import org.jivesoftware.smack.chat2.Chat; 035import org.jivesoftware.smack.chat2.ChatManager; 036import org.jivesoftware.smack.filter.AndFilter; 037import org.jivesoftware.smack.filter.MessageTypeFilter; 038import org.jivesoftware.smack.filter.MessageWithBodiesFilter; 039import org.jivesoftware.smack.filter.NotFilter; 040import org.jivesoftware.smack.filter.PossibleFromTypeFilter; 041import org.jivesoftware.smack.filter.StanzaExtensionFilter; 042import org.jivesoftware.smack.filter.StanzaFilter; 043import org.jivesoftware.smack.packet.Message; 044import org.jivesoftware.smack.packet.Stanza; 045 046import org.jivesoftware.smackx.chat_markers.element.ChatMarkersElements; 047import org.jivesoftware.smackx.chat_markers.filter.ChatMarkersFilter; 048import org.jivesoftware.smackx.chat_markers.filter.EligibleForChatMarkerFilter; 049import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 050 051import org.jxmpp.jid.EntityBareJid; 052 053/** 054 * Chat Markers Manager class (XEP-0333). 055 * 056 * @see <a href="http://xmpp.org/extensions/xep-0333.html">XEP-0333: Chat 057 * Markers</a> 058 * @author Miguel Hincapie 059 * @author Fernando Ramirez 060 * 061 */ 062public final class ChatMarkersManager extends Manager { 063 064 static { 065 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 066 @Override 067 public void connectionCreated(XMPPConnection connection) { 068 getInstanceFor(connection); 069 } 070 }); 071 } 072 073 private static final Map<XMPPConnection, ChatMarkersManager> INSTANCES = new WeakHashMap<>(); 074 075 // @FORMATTER:OFF 076 private static final StanzaFilter INCOMING_MESSAGE_FILTER = new AndFilter( 077 MessageTypeFilter.NORMAL_OR_CHAT, 078 new StanzaExtensionFilter(ChatMarkersElements.NAMESPACE), 079 PossibleFromTypeFilter.ENTITY_BARE_JID, 080 EligibleForChatMarkerFilter.INSTANCE 081 ); 082 083 private static final StanzaFilter OUTGOING_MESSAGE_FILTER = new AndFilter( 084 MessageTypeFilter.NORMAL_OR_CHAT, 085 MessageWithBodiesFilter.INSTANCE, 086 new NotFilter(ChatMarkersFilter.INSTANCE), 087 EligibleForChatMarkerFilter.INSTANCE 088 ); 089 // @FORMATTER:ON 090 091 private final Set<ChatMarkersListener> incomingListeners = new HashSet<>(); 092 093 private final AsyncButOrdered<Chat> asyncButOrdered = new AsyncButOrdered<>(); 094 095 private final ChatManager chatManager; 096 097 private final ServiceDiscoveryManager serviceDiscoveryManager; 098 099 private boolean enabled; 100 101 /** 102 * Get the singleton instance of ChatMarkersManager. 103 * 104 * @param connection the connection used to get the ChatMarkersManager instance. 105 * @return the instance of ChatMarkersManager 106 */ 107 public static synchronized ChatMarkersManager getInstanceFor(XMPPConnection connection) { 108 ChatMarkersManager chatMarkersManager = INSTANCES.get(connection); 109 110 if (chatMarkersManager == null) { 111 chatMarkersManager = new ChatMarkersManager(connection); 112 INSTANCES.put(connection, chatMarkersManager); 113 } 114 115 return chatMarkersManager; 116 } 117 118 private ChatMarkersManager(XMPPConnection connection) { 119 super(connection); 120 121 chatManager = ChatManager.getInstanceFor(connection); 122 123 connection.addMessageInterceptor(mb -> mb.addExtension(ChatMarkersElements.MarkableExtension.INSTANCE), 124 m -> { 125 return OUTGOING_MESSAGE_FILTER.accept(m); 126 }); 127 128 connection.addSyncStanzaListener(new StanzaListener() { 129 @Override 130 public void processStanza(Stanza packet) 131 throws 132 NotConnectedException, 133 InterruptedException, 134 SmackException.NotLoggedInException { 135 final Message message = (Message) packet; 136 137 // Note that this listener is used together with a PossibleFromTypeFilter.ENTITY_BARE_JID filter, hence 138 // every message is guaranteed to have a from address which is representable as bare JID. 139 EntityBareJid bareFrom = message.getFrom().asEntityBareJidOrThrow(); 140 141 final Chat chat = chatManager.chatWith(bareFrom); 142 143 asyncButOrdered.performAsyncButOrdered(chat, new Runnable() { 144 @Override 145 public void run() { 146 for (ChatMarkersListener listener : incomingListeners) { 147 if (ChatMarkersElements.MarkableExtension.from(message) != null) { 148 listener.newChatMarkerMessage(ChatMarkersState.markable, message, chat); 149 } 150 else if (ChatMarkersElements.ReceivedExtension.from(message) != null) { 151 listener.newChatMarkerMessage(ChatMarkersState.received, message, chat); 152 } 153 else if (ChatMarkersElements.DisplayedExtension.from(message) != null) { 154 listener.newChatMarkerMessage(ChatMarkersState.displayed, message, chat); 155 } 156 else if (ChatMarkersElements.AcknowledgedExtension.from(message) != null) { 157 listener.newChatMarkerMessage(ChatMarkersState.acknowledged, message, chat); 158 } 159 } 160 } 161 }); 162 163 } 164 }, INCOMING_MESSAGE_FILTER); 165 166 serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); 167 } 168 169 /** 170 * Returns true if Chat Markers is supported by the server. 171 * 172 * @return true if Chat Markers is supported by the server. 173 * @throws NotConnectedException if the connection is not connected. 174 * @throws XMPPErrorException in case an error response was received. 175 * @throws NoResponseException if no response was received. 176 * @throws InterruptedException if the connection is interrupted. 177 */ 178 public boolean isSupportedByServer() 179 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 180 return ServiceDiscoveryManager.getInstanceFor(connection()) 181 .serverSupportsFeature(ChatMarkersElements.NAMESPACE); 182 } 183 184 /** 185 * Register a ChatMarkersListener. That listener will be informed about new 186 * incoming markable messages. 187 * 188 * @param listener ChatMarkersListener 189 * @return true, if the listener was not registered before 190 */ 191 public synchronized boolean addIncomingChatMarkerMessageListener(ChatMarkersListener listener) { 192 boolean res = incomingListeners.add(listener); 193 if (!enabled) { 194 serviceDiscoveryManager.addFeature(ChatMarkersElements.NAMESPACE); 195 enabled = true; 196 } 197 return res; 198 } 199 200 /** 201 * Unregister a ChatMarkersListener. 202 * 203 * @param listener ChatMarkersListener 204 * @return true, if the listener was registered before 205 */ 206 public synchronized boolean removeIncomingChatMarkerMessageListener(ChatMarkersListener listener) { 207 boolean res = incomingListeners.remove(listener); 208 if (incomingListeners.isEmpty() && enabled) { 209 serviceDiscoveryManager.removeFeature(ChatMarkersElements.NAMESPACE); 210 enabled = false; 211 } 212 return res; 213 } 214}