/*
* 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.impl.protocol.jabber.extensions.coin.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
import org.jitsi.util.xml.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smackx.packet.*;
/**
* Implements <tt>OperationSetTelephonyConferencing</tt> for Jabber.
*
* @author Lyubomir Marinov
* @author Sebastien Vincent
* @author Boris Grozev
* @author Pawel Domas
*/
public class OperationSetTelephonyConferencingJabberImpl
extends AbstractOperationSetTelephonyConferencing<
ProtocolProviderServiceJabberImpl,
OperationSetBasicTelephonyJabberImpl,
CallJabberImpl,
CallPeerJabberImpl,
String>
implements RegistrationStateChangeListener,
PacketListener,
PacketFilter
{
/**
* The <tt>Logger</tt> used by the
* <tt>OperationSetTelephonyConferencingJabberImpl</tt> class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(OperationSetTelephonyConferencingJabberImpl.class);
/**
* The minimum interval in milliseconds between COINs sent to a single
* <tt>CallPeer</tt>.
*/
private static final int COIN_MIN_INTERVAL = 200;
/**
* Property used to disable COIN notifications.
*/
public static final String DISABLE_COIN_PROP_NAME
= "net.java.sip.communicator.impl.protocol.jabber.DISABLE_COIN";
/**
* Synchronization object.
*/
private final Object lock = new Object();
/**
* Field indicates whether COIN notification are disabled or not.
*/
private boolean isCoinDisabled = false;
/**
* Initializes a new <tt>OperationSetTelephonyConferencingJabberImpl</tt>
* instance which is to provide telephony conferencing services for the
* specified Jabber <tt>ProtocolProviderService</tt> implementation.
*
* @param parentProvider the Jabber <tt>ProtocolProviderService</tt>
* implementation which has requested the creation of the new instance and
* for which the new instance is to provide telephony conferencing services
*/
public OperationSetTelephonyConferencingJabberImpl(
ProtocolProviderServiceJabberImpl parentProvider)
{
super(parentProvider);
this.isCoinDisabled
= JabberActivator.getConfigurationService()
.getBoolean(DISABLE_COIN_PROP_NAME, false);
}
/**
* Notifies all <tt>CallPeer</tt>s associated with a specific <tt>Call</tt>
* about changes in the telephony conference-related information. In
* contrast, {@link #notifyAll()} notifies all <tt>CallPeer</tt>s associated
* with the telephony conference in which a specific <tt>Call</tt> is
* participating.
*
* @param call the <tt>Call</tt> whose <tt>CallPeer</tt>s are to be notified
* about changes in the telephony conference-related information
*/
@Override
protected void notifyCallPeers(Call call)
{
if (!isCoinDisabled && call.isConferenceFocus())
{
synchronized (lock)
{
// send conference-info to all CallPeers of the specified call.
for (Iterator<? extends CallPeer> i = call.getCallPeers();
i.hasNext();)
{
notify(i.next());
}
}
}
}
/**
* Notifies a specific <tt>CallPeer</tt> about changes in the telephony
* conference-related information.
*
* @param callPeer the <tt>CallPeer</tt> to notify.
*/
private void notify(CallPeer callPeer)
{
if(!(callPeer instanceof CallPeerJabberImpl))
return;
//Don't send COINs to peers with might not be ready to accept COINs yet
CallPeerState peerState = callPeer.getState();
if (peerState == CallPeerState.CONNECTING
|| peerState == CallPeerState.UNKNOWN
|| peerState == CallPeerState.INITIATING_CALL
|| peerState == CallPeerState.DISCONNECTED
|| peerState == CallPeerState.FAILED)
return;
final CallPeerJabberImpl callPeerJabber = (CallPeerJabberImpl)callPeer;
final long timeSinceLastCoin = System.currentTimeMillis()
- callPeerJabber.getLastConferenceInfoSentTimestamp();
if (timeSinceLastCoin < COIN_MIN_INTERVAL)
{
if (callPeerJabber.isConfInfoScheduled())
return;
logger.info("Scheduling to send a COIN to " + callPeerJabber);
callPeerJabber.setConfInfoScheduled(true);
new Thread(new Runnable(){
@Override
public void run()
{
try
{
Thread.sleep(1 + COIN_MIN_INTERVAL - timeSinceLastCoin);
}
catch (InterruptedException ie) {}
OperationSetTelephonyConferencingJabberImpl.this
.notify(callPeerJabber);
}
}).start();
return;
}
// check that callPeer supports COIN before sending him a
// conference-info
String to = getBasicTelephony().getFullCalleeURI(callPeer.getAddress());
// XXX if this generates actual disco#info requests we might want to
// cache it.
try
{
DiscoverInfo discoverInfo
= parentProvider.getDiscoveryManager().discoverInfo(to);
if (!discoverInfo.containsFeature(
ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_COIN))
{
logger.info(callPeer.getAddress() + " does not support COIN");
callPeerJabber.setConfInfoScheduled(false);
return;
}
}
catch (XMPPException xmppe)
{
logger.warn("Failed to retrieve DiscoverInfo for " + to, xmppe);
}
ConferenceInfoDocument currentConfInfo
= getCurrentConferenceInfo(callPeerJabber);
ConferenceInfoDocument lastSentConfInfo
= callPeerJabber.getLastConferenceInfoSent();
ConferenceInfoDocument diff;
if (lastSentConfInfo == null)
diff = currentConfInfo;
else
diff = getConferenceInfoDiff(lastSentConfInfo, currentConfInfo);
if (diff != null)
{
int newVersion
= lastSentConfInfo == null
? 1
: lastSentConfInfo.getVersion() + 1;
diff.setVersion(newVersion);
IQ iq = getConferenceInfo(callPeerJabber, diff);
if (iq != null)
{
parentProvider.getConnection().sendPacket(iq);
// We save currentConfInfo, because it is of state "full", while
// diff could be a partial
currentConfInfo.setVersion(newVersion);
callPeerJabber.setLastConferenceInfoSent(currentConfInfo);
callPeerJabber.setLastConferenceInfoSentTimestamp(
System.currentTimeMillis());
}
}
callPeerJabber.setConfInfoScheduled(false);
}
/**
* Generates the conference-info IQ to be sent to a specific
* <tt>CallPeer</tt> in order to notify it of the current state of the
* conference managed by the local peer.
*
* @param callPeer the <tt>CallPeer</tt> to generate conference-info XML for
* @param confInfo the <tt>ConferenceInformationDocument</tt> which is to be
* included in the IQ
* @return the conference-info IQ to be sent to the specified
* <tt>callPeer</tt> in order to notify it of the current state of the
* conference managed by the local peer
*/
private IQ getConferenceInfo(CallPeerJabberImpl callPeer,
final ConferenceInfoDocument confInfo)
{
String callPeerSID = callPeer.getSID();
if (callPeerSID == null)
return null;
IQ iq = new IQ(){
@Override
public String getChildElementXML()
{
return confInfo.toXml();
}
};
CallJabberImpl call = callPeer.getCall();
iq.setFrom(call.getProtocolProvider().getOurJID());
iq.setTo(callPeer.getAddress());
iq.setType(Type.SET);
return iq;
}
/**
* Implementation of method <tt>registrationStateChange</tt> from
* interface RegistrationStateChangeListener for setting up (or down)
* our <tt>JingleManager</tt> when an <tt>XMPPConnection</tt> is available
*
* @param evt the event received
*/
@Override
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
super.registrationStateChanged(evt);
RegistrationState registrationState = evt.getNewState();
if (RegistrationState.REGISTERED.equals(registrationState))
{
if(logger.isDebugEnabled())
logger.debug("Subscribes to Coin packets");
subscribeForCoinPackets();
}
else if (RegistrationState.UNREGISTERED.equals(registrationState))
{
if(logger.isDebugEnabled())
logger.debug("Unsubscribes to Coin packets");
unsubscribeForCoinPackets();
}
}
/**
* Creates a new outgoing <tt>Call</tt> into which conference callees are to
* be invited by this <tt>OperationSetTelephonyConferencing</tt>.
*
* @return a new outgoing <tt>Call</tt> into which conference callees are to
* be invited by this <tt>OperationSetTelephonyConferencing</tt>
* @throws OperationFailedException if anything goes wrong
*/
@Override
protected CallJabberImpl createOutgoingCall()
throws OperationFailedException
{
return new CallJabberImpl(getBasicTelephony());
}
/**
* {@inheritDoc}
*
* Implements the protocol-dependent part of the logic of inviting a callee
* to a <tt>Call</tt>. The protocol-independent part of that logic is
* implemented by
* {@link AbstractOperationSetTelephonyConferencing#inviteCalleeToCall(String,Call)}.
*/
@Override
protected CallPeer doInviteCalleeToCall(
String calleeAddress,
CallJabberImpl call)
throws OperationFailedException
{
return
getBasicTelephony().createOutgoingCall(
call,
calleeAddress,
Arrays.asList(
new PacketExtension[]
{
new CoinPacketExtension(true)
}));
}
/**
* Parses a <tt>String</tt> value which represents a callee address
* specified by the user into an object which is to actually represent the
* callee during the invitation to a conference <tt>Call</tt>.
*
* @param calleeAddressString a <tt>String</tt> value which represents a
* callee address to be parsed into an object which is to actually represent
* the callee during the invitation to a conference <tt>Call</tt>
* @return an object which is to actually represent the specified
* <tt>calleeAddressString</tt> during the invitation to a conference
* <tt>Call</tt>
* @throws OperationFailedException if parsing the specified
* <tt>calleeAddressString</tt> fails
*/
@Override
protected String parseAddressString(String calleeAddressString)
throws OperationFailedException
{
return getBasicTelephony().getFullCalleeURI(calleeAddressString);
}
/**
* Subscribes us to notifications about incoming Coin packets.
*/
private void subscribeForCoinPackets()
{
parentProvider.getConnection().addPacketListener(this, this);
}
/**
* Unsubscribes us from notifications about incoming Coin packets.
*/
private void unsubscribeForCoinPackets()
{
Connection connection = parentProvider.getConnection();
if (connection != null)
connection.removePacketListener(this);
}
/**
* Tests whether or not the specified packet should be handled by this
* operation set. This method is called by smack prior to packet delivery
* and it would only accept <tt>CoinIQ</tt>s.
*
* @param packet the packet to test.
* @return true if and only if <tt>packet</tt> passes the filter.
*/
public boolean accept(Packet packet)
{
return (packet instanceof CoinIQ);
}
/**
* Handles incoming jingle packets and passes them to the corresponding
* method based on their action.
*
* @param packet the packet to process.
*/
public void processPacket(Packet packet)
{
CoinIQ coinIQ = (CoinIQ) packet;
String errorMessage = null;
//first ack all "set" requests.
IQ.Type type = coinIQ.getType();
if (type == IQ.Type.SET)
{
IQ ack = IQ.createResultIQ(coinIQ);
parentProvider.getConnection().sendPacket(ack);
}
else if(type == IQ.Type.ERROR)
{
XMPPError error = coinIQ.getError();
if(error != null)
{
String msg = error.getMessage();
errorMessage = ((msg != null)? (msg + " ") : "")
+ "Error code: " + error.getCode();
}
logger.error("Received error in COIN packet. "+errorMessage);
}
String sid = coinIQ.getSID();
if (sid != null)
{
CallPeerJabberImpl callPeer
= getBasicTelephony().getActiveCallsRepository().findCallPeer(
sid);
if (callPeer != null)
{
if(type == IQ.Type.ERROR)
{
callPeer.fireConferenceMemberErrorEvent(errorMessage);
return;
}
if (logger.isDebugEnabled())
logger.debug("Processing COIN from " + coinIQ.getFrom()
+ " (version=" + coinIQ.getVersion() + ")");
handleCoin(callPeer, coinIQ);
}
}
}
/**
* Handles a specific <tt>CoinIQ</tt> sent from a specific
* <tt>CallPeer</tt>.
*
* @param callPeer the <tt>CallPeer</tt> from which the specified
* <tt>CoinIQ</tt> was sent
* @param coinIQ the <tt>CoinIQ</tt> which was sent from the specified
* <tt>callPeer</tt>
*/
private void handleCoin(CallPeerJabberImpl callPeer, CoinIQ coinIQ)
{
try
{
setConferenceInfoXML(callPeer, coinIQ.getChildElementXML());
}
catch (XMLException e)
{
logger.error("Could not handle received COIN from " + callPeer
+ ": " + coinIQ);
}
}
/**
* {@inheritDoc}
*
* For COINs (XEP-0298), we use the attributes of the
* <tt>conference-info</tt> element to piggyback a Jingle SID. This is
* temporary and should be removed once we choose a better way to pass the
* SID.
*/
@Override
protected ConferenceInfoDocument getCurrentConferenceInfo(
MediaAwareCallPeer<?,?,?> callPeer)
{
ConferenceInfoDocument confInfo
= super.getCurrentConferenceInfo(callPeer);
if (callPeer instanceof CallPeerJabberImpl
&& confInfo != null)
{
confInfo.setSid(((CallPeerJabberImpl)callPeer).getSID());
}
return confInfo;
}
/**
* {@inheritDoc}
*/
@Override
protected String getLocalEntity(CallPeer callPeer)
{
JingleIQ sessionIQ = ((CallPeerJabberImpl)callPeer).getSessionIQ();
String from = sessionIQ.getFrom();
String chatRoomName = StringUtils.parseBareAddress(from);
OperationSetMultiUserChatJabberImpl opSetMUC
= (OperationSetMultiUserChatJabberImpl)
parentProvider.getOperationSet(OperationSetMultiUserChat.class);
ChatRoom room = null;
if(opSetMUC != null)
room = opSetMUC.getChatRoom(chatRoomName);
if(room != null)
return "xmpp:" + chatRoomName + "/" + room.getUserNickname();
return "xmpp:" + parentProvider.getOurJID();
}
/**
* {@inheritDoc}
*/
@Override
protected String getLocalDisplayName()
{
return null;
}
/**
* {@inheritDoc}
*
* The URI of the returned <tt>ConferenceDescription</tt> is the occupant
* JID with which we have joined the room.
*
* If a Videobridge is available for our <tt>ProtocolProviderService</tt>
* we use it. TODO: this should be relaxed when we refactor the Videobridge
* implementation, so that any Videobridge (on any protocol provider) can
* be used.
*/
@Override
public ConferenceDescription setupConference(final ChatRoom chatRoom)
{
OperationSetVideoBridge videoBridge
= parentProvider.getOperationSet(OperationSetVideoBridge.class);
boolean isVideobridge = (videoBridge != null) && videoBridge.isActive();
CallJabberImpl call = new CallJabberImpl(getBasicTelephony());
call.setAutoAnswer(true);
String uri = "xmpp:" + chatRoom.getIdentifier() +
"/" + chatRoom.getUserNickname();
ConferenceDescription cd
= new ConferenceDescription(uri, call.getCallID());
call.addCallChangeListener(new CallChangeListener()
{
@Override
public void callStateChanged(CallChangeEvent ev)
{
if(CallState.CALL_ENDED.equals(ev.getNewValue()))
chatRoom.publishConference(null, null);
}
@Override
public void callPeerRemoved(CallPeerEvent ev)
{
}
@Override
public void callPeerAdded(CallPeerEvent ev)
{
}
});
if (isVideobridge)
{
call.setConference(new MediaAwareCallConference(true));
//For Jitsi Videobridge we set the transports to RAW-UDP, otherwise
//we leave them empty (meaning both RAW-UDP and ICE could be used)
cd.addTransport(
ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0);
}
if (logger.isInfoEnabled())
{
logger.info("Setup a conference with uri=" + uri + " and callid=" +
call.getCallID() + ". Videobridge in use: " + isVideobridge);
}
return cd;
}
}