/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.util.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.jabberconstants.*;
import net.java.sip.communicator.util.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smackx.*;
import org.jivesoftware.smackx.packet.*;
/**
* Maps SIP Communicator typing notifications to those going and coming from
* smack lib.
*
* @author Damian Minkov
* @author Emil Ivov
* @author Hristo Terezov
*/
public class OperationSetTypingNotificationsJabberImpl
extends AbstractOperationSetTypingNotifications<ProtocolProviderServiceJabberImpl>
{
/**
* The logger.
*/
private static final Logger logger =
Logger.getLogger(OperationSetTypingNotificationsJabberImpl.class);
/**
* An active instance of the opSetPersPresence operation set. We're using
* it to map incoming events to contacts in our contact list.
*/
private OperationSetPersistentPresenceJabberImpl opSetPersPresence = null;
/**
* An active instance of the opSetBasicIM operation set.
*/
private OperationSetBasicInstantMessagingJabberImpl opSetBasicIM = null;
/**
* We use this listener to cease the moment when the protocol provider
* has been successfully registered.
*/
private ProviderRegListener providerRegListener = new ProviderRegListener();
/**
* The manger which send us the typing info and through which we send inf
*/
private MessageEventManager messageEventManager = null;
/**
* The listener instance that we use to track chat states according to
* XEP-0085;
*/
private SmackChatStateListener smackChatStateListener = null;
/**
* @param provider a ref to the <tt>ProtocolProviderServiceImpl</tt>
* that created us and that we'll use for retrieving the underlying aim
* connection.
*/
OperationSetTypingNotificationsJabberImpl(
ProtocolProviderServiceJabberImpl provider)
{
super(provider);
provider.addRegistrationStateChangeListener(providerRegListener);
}
/**
* Sends a notification to <tt>notifiedContatct</tt> that we have entered
* <tt>typingState</tt>.
*
* @param notifiedContact the <tt>Contact</tt> to notify
* @param typingState the typing state that we have entered.
*
* @throws java.lang.IllegalStateException if the underlying stack is
* not registered and initialized.
* @throws java.lang.IllegalArgumentException if <tt>notifiedContact</tt> is
* not an instance belonging to the underlying implementation.
*/
public void sendTypingNotification(Contact notifiedContact, int typingState)
throws IllegalStateException, IllegalArgumentException
{
assertConnected();
if( !(notifiedContact instanceof ContactJabberImpl) )
throw new IllegalArgumentException(
"The specified contact is not a Jabber contact."
+ notifiedContact);
/**
* Emil Ivov: We used to use this in while we were still using XEP-0022
* to send typing notifications. I am commenting it out today on
* 2008-08-20 as we now also support XEP-0085 (see below) and using both
* mechanisms sends double notifications which, apart from simply being
* redundant, is also causing the jabber slick to fail.
*
String packetID =
(String)packetIDsTable.get(notifiedContact.getAddress());
//First do XEP-0022 notifications
if(packetID != null)
{
if(typingState == STATE_TYPING)
{
messageEventManager.
sendComposingNotification(notifiedContact.getAddress(),
packetID);
}
else if(typingState == STATE_STOPPED)
{
messageEventManager.
sendCancelledNotification(notifiedContact.getAddress(),
packetID);
packetIDsTable.remove(notifiedContact.getAddress());
}
}
*/
//now handle XEP-0085
sendXep85ChatState(notifiedContact, typingState);
}
/**
* Converts <tt>state</tt> into the corresponding smack <tt>ChatState</tt>
* and sends it to contact.
*
* @param contact the contact that we'd like to send our state to.
* @param state the state we'd like to sent.
*/
private void sendXep85ChatState(Contact contact, int state)
{
if(opSetBasicIM == null
|| parentProvider.getConnection() == null)
return;
String toJID = opSetBasicIM.getRecentJIDForAddress(contact.getAddress());
// find the currently contacted jid to send typing info to him
// or if we do not have a jid and we have already sent message to the
// bare jid we will also send typing info there
// if we haven't sent a message yet, do not send typing notifications
if(toJID == null)
return;
if (logger.isTraceEnabled())
logger.trace("Sending XEP-0085 chat state=" + state
+ " to " + toJID);
ChatState chatState;
if(state == STATE_TYPING)
{
chatState = ChatState.composing;
}
else if(state == STATE_STOPPED)
{
chatState = ChatState.inactive;
}
else if(state == STATE_PAUSED)
{
chatState = ChatState.paused;
}
else
{
chatState = ChatState.gone;
}
setCurrentState(chatState, toJID);
}
/**
* Creates and sends a packet for the new chat state.
* @param chatState the new chat state.
* @param jid the JID of the receiver.
*/
private void setCurrentState(ChatState chatState, String jid)
{
String threadID = opSetBasicIM.getThreadIDForAddress(jid);
if(threadID == null)
return;
Message message = new Message();
ChatStateExtension extension = new ChatStateExtension(chatState);
message.addExtension(extension);
message.setTo(jid);
message.setType(Message.Type.chat);
message.setThread(threadID);
message.setFrom(parentProvider.getConnection().getUser());
parentProvider.getConnection().sendPacket(message);
}
/**
* Utility method throwing an exception if the stack is not properly
* initialized.
*
* @throws java.lang.IllegalStateException
* if the underlying stack is not registered and initialized.
*/
@Override
protected void assertConnected()
throws IllegalStateException
{
if(parentProvider != null && !parentProvider.isRegistered()
&& opSetPersPresence.getPresenceStatus().isOnline())
{
// if we are not registered but the current status is online
// change the current status
opSetPersPresence.fireProviderStatusChangeEvent(
opSetPersPresence.getPresenceStatus(),
parentProvider.getJabberStatusEnum().getStatus(
JabberStatusEnum.OFFLINE));
}
super.assertConnected();
}
/**
* Our listener that will tell us when we're registered and
* ready to accept us as a listener.
*/
private class ProviderRegListener
implements RegistrationStateChangeListener
{
/**
* The method is called by a ProtocolProvider implementation whenver
* a change in the registration state of the corresponding provider had
* occurred.
* @param evt ProviderStatusChangeEvent the event describing the status
* change.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (logger.isDebugEnabled())
logger.debug("The provider changed state from: "
+ evt.getOldState()
+ " to: " + evt.getNewState());
if (evt.getNewState() == RegistrationState.REGISTERED)
{
opSetPersPresence =
(OperationSetPersistentPresenceJabberImpl) parentProvider
.getOperationSet(OperationSetPersistentPresence.class);
opSetBasicIM =
(OperationSetBasicInstantMessagingJabberImpl)parentProvider
.getOperationSet(
OperationSetBasicInstantMessaging.class);
messageEventManager =
new MessageEventManager(parentProvider.getConnection());
messageEventManager.addMessageEventRequestListener(
new JabberMessageEventRequestListener());
messageEventManager.addMessageEventNotificationListener(
new IncomingMessageEventsListener());
if(smackChatStateListener == null)
smackChatStateListener = new SmackChatStateListener();
parentProvider.getConnection().addPacketListener(
smackChatStateListener, new PacketTypeFilter(Message.class));
}
else if(evt.getNewState() == RegistrationState.UNREGISTERED
|| evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED
|| evt.getNewState() == RegistrationState.CONNECTION_FAILED)
{
if(parentProvider.getConnection() != null)
{
parentProvider.getConnection()
.removePacketListener(smackChatStateListener);
}
smackChatStateListener = null;
if(messageEventManager != null)
{
messageEventManager.destroy();
messageEventManager = null;
}
}
}
}
/**
* Listens for incoming request for typing info
*/
private class JabberMessageEventRequestListener
implements MessageEventRequestListener
{
public void deliveredNotificationRequested(String from, String packetID,
MessageEventManager messageEventManager)
{
messageEventManager.sendDeliveredNotification(from, packetID);
}
public void displayedNotificationRequested(String from, String packetID,
MessageEventManager messageEventManager)
{
messageEventManager.sendDisplayedNotification(from, packetID);
}
public void composingNotificationRequested(String from, String packetID,
MessageEventManager messageEventManager)
{
// if(packetID != null)
// {
// String fromID = StringUtils.parseBareAddress(from);
// packetIDsTable.put(fromID, packetID);
// }
}
public void offlineNotificationRequested(String from, String packetID,
MessageEventManager
messageEventManager)
{}
}
/**
* Receives incoming typing info
*/
private class IncomingMessageEventsListener
implements MessageEventNotificationListener
{
public void deliveredNotification(String from, String packetID)
{
}
public void displayedNotification(String from, String packetID)
{
}
public void composingNotification(String from, String packetID)
{
String fromID = StringUtils.parseBareAddress(from);
Contact sourceContact = opSetPersPresence.findContactByID(fromID);
if(sourceContact == null)
{
//create the volatile contact
sourceContact = opSetPersPresence.createVolatileContact(from);
}
fireTypingNotificationsEvent(sourceContact, STATE_TYPING);
}
public void offlineNotification(String from, String packetID)
{
}
public void cancelledNotification(String from, String packetID)
{
String fromID = StringUtils.parseBareAddress(from);
Contact sourceContact = opSetPersPresence.findContactByID(fromID);
if(sourceContact == null)
{
//create the volatile contact
sourceContact = opSetPersPresence.createVolatileContact(from);
}
fireTypingNotificationsEvent(sourceContact, STATE_STOPPED);
}
}
/**
* The listener that we use to track chat state notifications according
* to XEP-0085.
*/
private class SmackChatStateListener
implements PacketListener
{
/**
* Called by smack when the state of a chat changes.
*
* @param state the new state of the chat.
* @param message the message containing the new chat state
*/
public void stateChanged(ChatState state,
org.jivesoftware.smack.packet.Message message)
{
String fromJID = message.getFrom();
if (logger.isTraceEnabled())
logger.trace(fromJID + " entered the "
+ state.name()+ " state.");
String fromID = StringUtils.parseBareAddress(fromJID);
boolean isPrivateMessagingAddress = false;
OperationSetMultiUserChat mucOpSet = parentProvider
.getOperationSet(OperationSetMultiUserChat.class);
if(mucOpSet != null)
{
List<ChatRoom> chatRooms
= mucOpSet.getCurrentlyJoinedChatRooms();
for(ChatRoom chatRoom : chatRooms)
{
if(chatRoom.getName().equals(fromID))
{
isPrivateMessagingAddress = true;
break;
}
}
}
Contact sourceContact = opSetPersPresence.findContactByID(
(isPrivateMessagingAddress? message.getFrom() : fromID));
if(sourceContact == null)
{
// in private messaging we can receive some errors
// when we left room (isPrivateMessagingAddress == false)
// and we try to send some message
if(message.getError() != null)
sourceContact = opSetPersPresence.findContactByID(
message.getFrom());
if(sourceContact == null)
{
//create the volatile contact
sourceContact = opSetPersPresence.createVolatileContact(
message.getFrom(), isPrivateMessagingAddress);
}
}
int evtCode = STATE_UNKNOWN;
if (ChatState.composing.equals(state))
{
evtCode = STATE_TYPING;
}
else if (ChatState.paused.equals(state)
|| ChatState.active.equals(state) )
{
evtCode = STATE_PAUSED;
}
else if (ChatState.inactive.equals(state)
|| ChatState.gone.equals(state) )
{
evtCode = STATE_STOPPED;
}
if(message.getError() != null)
fireTypingNotificationsDeliveryFailedEvent(
sourceContact, evtCode);
else if(evtCode != STATE_UNKNOWN)
fireTypingNotificationsEvent(sourceContact, evtCode);
else
logger.warn("Unknown typing state!");
}
@Override
public void processPacket(Packet packet)
{
Message msg = (Message) packet;
ChatStateExtension ext = (ChatStateExtension) msg.getExtension(
"http://jabber.org/protocol/chatstates");
if(ext == null)
return;
stateChanged(ChatState.valueOf(ext.getElementName()), msg);
}
}
}