/*
* 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.service.protocol.media;
import java.beans.*;
import java.util.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.util.xml.*;
import org.w3c.dom.*;
/**
* Represents a default implementation of
* OperationSetTelephonyConferencing in order to make it easier for
* implementers to provide complete solutions while focusing on
* implementation-specific details.
*
* @param
* @param
* @param
* @param
* @param
*
* @author Lyubomir Marinov
* @author Boris Grozev
*/
public abstract class AbstractOperationSetTelephonyConferencing<
ProtocolProviderServiceT extends ProtocolProviderService,
OperationSetBasicTelephonyT
extends OperationSetBasicTelephony,
MediaAwareCallT
extends MediaAwareCall<
MediaAwareCallPeerT,
OperationSetBasicTelephonyT,
ProtocolProviderServiceT>,
MediaAwareCallPeerT
extends MediaAwareCallPeer<
MediaAwareCallT,
?,
ProtocolProviderServiceT>,
CalleeAddressT extends Object>
implements OperationSetTelephonyConferencing,
RegistrationStateChangeListener,
PropertyChangeListener,
CallListener,
CallChangeListener
{
/**
* The Logger used by the
* AbstractOperationSetTelephonyConferencing class and its
* instances.
*/
private static final Logger logger
= Logger.getLogger(AbstractOperationSetTelephonyConferencing.class);
/**
* The name of the conference-info XML element display-text.
*/
protected static final String ELEMENT_DISPLAY_TEXT = "display-text";
/**
* The name of the conference-info XML element endpoint.
*/
protected static final String ELEMENT_ENDPOINT = "endpoint";
/**
* The name of the conference-info XML element media.
*/
protected static final String ELEMENT_MEDIA = "media";
/**
* The name of the conference-info XML element src-id.
*/
protected static final String ELEMENT_SRC_ID = "src-id";
/**
* The name of the conference-info XML element status.
*/
protected static final String ELEMENT_STATUS = "status";
/**
* The name of the conference-info XML element type.
*/
protected static final String ELEMENT_TYPE = "type";
/**
* The name of the conference-info XML element user.
*/
protected static final String ELEMENT_USER = "user";
/**
* The name of the conference-info XML element users.
*/
protected static final String ELEMENT_USERS = "users";
/**
* The name of the account property which specifies whether we should
* generate and send RFC4575 partial notifications (as opposed to always
* sending 'full' documents)
*/
private static final String PARTIAL_NOTIFICATIONS_PROP_NAME
= "RFC4575_PARTIAL_NOTIFICATIONS_ENABLED";
/**
* The OperationSetBasicTelephony implementation which this
* instance uses to carry out tasks such as establishing Calls.
*/
private OperationSetBasicTelephonyT basicTelephony;
/**
* The CallPeerListener which listens to modifications in the
* properties/state of CallPeer so that NOTIFY requests can be sent
* from a conference focus to its conference members to update them with
* the latest information about the CallPeer.
*/
private final CallPeerListener callPeerListener = new CallPeerAdapter()
{
/**
* Indicates that a change has occurred in the status of the source
* CallPeer.
*
* @param evt the CallPeerChangeEvent instance containing the
* source event as well as its previous and its new status
*/
@Override
public void peerStateChanged(CallPeerChangeEvent evt)
{
CallPeer peer = evt.getSourceCallPeer();
if (peer != null)
{
Call call = peer.getCall();
if (call != null)
{
CallPeerState state = peer.getState();
if ((state != null)
&& !state.equals(CallPeerState.DISCONNECTED)
&& !state.equals(CallPeerState.FAILED))
{
AbstractOperationSetTelephonyConferencing.this
.notifyAll(call);
}
}
}
}
};
/**
* The ProtocolProviderService implementation which created this
* instance and for which telephony conferencing services are being provided
* by this instance.
*/
protected final ProtocolProviderServiceT parentProvider;
/**
* Initializes a new AbstractOperationSetTelephonyConferencing
* instance which is to provide telephony conferencing services for the
* specified ProtocolProviderService implementation.
*
* @param parentProvider the ProtocolProviderService implementation
* which has requested the creation of the new instance and for which the
* new instance is to provide telephony conferencing services
*/
protected AbstractOperationSetTelephonyConferencing(
ProtocolProviderServiceT parentProvider)
{
this.parentProvider = parentProvider;
this.parentProvider.addRegistrationStateChangeListener(this);
}
/**
* Notifies this OperationSetTelephonyConferencing that its
* basicTelephony property has changed its value from a specific
* oldValue to a specific newValue
*
* @param oldValue the old value of the basicTelephony property
* @param newValue the new value of the basicTelephony property
*/
protected void basicTelephonyChanged(
OperationSetBasicTelephonyT oldValue,
OperationSetBasicTelephonyT newValue)
{
if (oldValue != null)
oldValue.removeCallListener(this);
if (newValue != null)
newValue.addCallListener(this);
}
/**
* Notifies this CallListener that a specific Call has
* been established.
*
* @param event a CallEvent which specified the newly-established
* Call
*/
protected void callBegun(CallEvent event)
{
Call call = event.getSourceCall();
call.addCallChangeListener(this);
/*
* If there were any CallPeers in the Call prior to our realization that
* it has begun, pretend that they are added afterwards.
*/
Iterator extends CallPeer> callPeerIter = call.getCallPeers();
while (callPeerIter.hasNext())
{
callPeerAdded(
new CallPeerEvent(
callPeerIter.next(),
call,
CallPeerEvent.CALL_PEER_ADDED));
}
}
/**
* Notifies this CallListener that a specific Call has
* ended.
*
* @param event a CallEvent which specified the Call which
* has just ended
*/
public void callEnded(CallEvent event)
{
Call call = event.getSourceCall();
/*
* If there are still CallPeers after our realization that it has ended,
* pretend that they are removed before that.
*/
Iterator extends CallPeer> callPeerIter = call.getCallPeers();
while (callPeerIter.hasNext())
{
callPeerRemoved(
new CallPeerEvent(
callPeerIter.next(),
call,
CallPeerEvent.CALL_PEER_REMOVED));
}
call.removeCallChangeListener(this);
}
/**
* Notifies this CallChangeListener that a specific
* CallPeer has been added to a specific Call.
*
* @param event a CallPeerEvent which specifies the
* CallPeer which has been added to a Call
*/
public void callPeerAdded(CallPeerEvent event)
{
MediaAwareCallPeer,?,?> callPeer =
(MediaAwareCallPeer,?,?>)event.getSourceCallPeer();
callPeer.addCallPeerListener(callPeerListener);
callPeer.getMediaHandler().addPropertyChangeListener(this);
callPeersChanged(event);
}
/**
* Notifies this CallChangeListener that a specific
* CallPeer has been remove from a specific Call.
*
* @param event a CallPeerEvent which specifies the
* CallPeer which has been removed from a Call
*/
public void callPeerRemoved(CallPeerEvent event)
{
@SuppressWarnings("unchecked")
MediaAwareCallPeerT callPeer =
(MediaAwareCallPeerT) event.getSourceCallPeer();
callPeer.removeCallPeerListener(callPeerListener);
callPeer.getMediaHandler().removePropertyChangeListener(this);
callPeersChanged(event);
}
/**
* Notifies this CallChangeListener that the CallPeer list
* of a specific Call has been modified by adding or removing a
* specific CallPeer.
*
* @param event a CallPeerEvent which specifies the
* CallPeer which has been added to or removed from a Call
*/
private void callPeersChanged(CallPeerEvent event)
{
notifyAll(event.getSourceCall());
}
/**
* Notifies this CallChangeListener that a specific Call
* has changed its state. Does nothing.
*
* @param event a CallChangeEvent which specifies the Call
* which has changed its state, the very state which has been changed and
* the values of the state before and after the change
*/
public void callStateChanged(CallChangeEvent event)
{
if (CallChangeEvent.CALL_PARTICIPANTS_CHANGE
.equals(event.getPropertyName()))
{
notifyAll(event.getSourceCall());
}
}
/**
* Creates a conference call with the specified callees as call peers.
*
* @param callees the list of addresses that we should call
* @return the newly created conference call containing all CallPeers
* @throws OperationFailedException if establishing the conference call
* fails
* @see OperationSetTelephonyConferencing#createConfCall(String[])
*/
public Call createConfCall(String[] callees)
throws OperationFailedException
{
return createConfCall(callees, null);
}
/**
* Creates a conference Call with the specified callees as
* CallPeers.
*
* @param callees the list of addresses that we should call
* @param conference the CallConference which represents the state
* of the telephony conference into which the specified callees are to be
* invited
* @return the newly-created conference call containing all
* CallPeers
* @throws OperationFailedException if establishing the conference
* Call fails
*/
public Call createConfCall(String[] callees, CallConference conference)
throws OperationFailedException
{
List calleeAddresses
= new ArrayList(callees.length);
for (String callee : callees)
calleeAddresses.add(parseAddressString(callee));
MediaAwareCallT call = createOutgoingCall();
if (conference == null)
conference = call.getConference();
else
call.setConference(conference);
conference.setConferenceFocus(true);
for (CalleeAddressT calleeAddress : calleeAddresses)
doInviteCalleeToCall(calleeAddress, call);
return call;
}
/**
* Creates a new outgoing Call into which conference callees are to
* be invited by this OperationSetTelephonyConferencing.
*
* @return a new outgoing Call into which conference callees are to
* be invited by this OperationSetTelephonyConferencing
* @throws OperationFailedException if anything goes wrong
*/
protected abstract MediaAwareCallT createOutgoingCall()
throws OperationFailedException;
/**
* Invites a callee with a specific address to join a specific Call
* for the purposes of telephony conferencing.
*
* @param calleeAddress the address of the callee to be invited to the
* specified existing Call
* @param call the existing Call to invite the callee with the
* specified address to
* @return a new CallPeer instance which describes the signaling
* and the media streaming of the newly-invited callee within the specified
* Call
* @throws OperationFailedException if inviting the specified callee to the
* specified Call fails
*/
protected abstract CallPeer doInviteCalleeToCall(
CalleeAddressT calleeAddress,
MediaAwareCallT call)
throws OperationFailedException;
/**
* Gets the OperationSetBasicTelephony implementation which this
* instance uses to carry out tasks such as establishing Calls.
*
* @return the OperationSetBasicTelephony implementation which this
* instance uses to carry out tasks such as establishing Calls
*/
public OperationSetBasicTelephonyT getBasicTelephony()
{
return basicTelephony;
}
private void getEndpointMediaProperties(
Node endpoint,
Map properties)
{
NodeList endpointChildList = endpoint.getChildNodes();
int endpoingChildCount = endpointChildList.getLength();
for (int endpointChildIndex = 0;
endpointChildIndex < endpoingChildCount;
endpointChildIndex++)
{
Node endpointChild = endpointChildList.item(endpointChildIndex);
if (ELEMENT_MEDIA.equals(endpointChild.getNodeName()))
{
NodeList mediaChildList = endpointChild.getChildNodes();
int mediaChildCount = mediaChildList.getLength();
String srcId = null;
String status = null;
String type = null;
for (int mediaChildIndex = 0;
mediaChildIndex < mediaChildCount;
mediaChildIndex++)
{
Node mediaChild = mediaChildList.item(mediaChildIndex);
String mediaChildName = mediaChild.getNodeName();
if (ELEMENT_SRC_ID.equals(mediaChildName))
srcId = mediaChild.getTextContent();
else if (ELEMENT_STATUS.equals(mediaChildName))
status = mediaChild.getTextContent();
else if (ELEMENT_TYPE.equals(mediaChildName))
type = mediaChild.getTextContent();
}
if (MediaType.AUDIO.toString().equalsIgnoreCase(type))
{
properties.put(
ConferenceMember.AUDIO_SSRC_PROPERTY_NAME,
srcId);
properties.put(
ConferenceMember.AUDIO_STATUS_PROPERTY_NAME,
status);
}
else if (MediaType.VIDEO.toString().equalsIgnoreCase(type))
{
properties.put(
ConferenceMember.VIDEO_SSRC_PROPERTY_NAME,
srcId);
properties.put(
ConferenceMember.VIDEO_STATUS_PROPERTY_NAME,
status);
}
}
}
}
/**
* Reads the text content of the status XML element of a specific
* endpoint XML element.
*
* @param endpoint an XML Node which represents the
* endpoint XML element from which to get the text content of its
* status XML element
* @return the text content of the status XML element of the
* specified endpoint XML element if any; otherwise, null
*/
private String getEndpointStatus(Node endpoint)
{
NodeList childNodes = endpoint.getChildNodes();
int childCount = childNodes.getLength();
for (int i = 0; i < childCount; i++)
{
Node child = childNodes.item(i);
if (ELEMENT_STATUS.equals(child.getNodeName()))
return child.getTextContent();
}
return null;
}
/**
* Gets the remote SSRC to be reported in the conference-info XML for a
* specific CallPeer's media of a specific MediaType.
*
* @param callPeer the CallPeer whose remote SSRC for the media of
* the specified mediaType is to be returned
* @param mediaType the MediaType of the specified
* callPeer's media whose remote SSRC is to be returned
* @return the remote SSRC to be reported in the conference-info XML for the
* specified callPeer's media of the specified mediaType
*/
protected long getRemoteSourceID(
MediaAwareCallPeer,?,?> callPeer,
MediaType mediaType)
{
long remoteSourceID
= callPeer.getMediaHandler().getRemoteSSRC(mediaType);
if (remoteSourceID != -1)
{
/*
* TODO Technically, we are detecting conflicts within a Call
* while we should be detecting them within the whole
* CallConference.
*/
MediaAwareCall,?,?> call = callPeer.getCall();
if (call != null)
{
for (MediaAwareCallPeer,?,?> aCallPeer
: call.getCallPeerList())
{
if (aCallPeer != callPeer)
{
long aRemoteSourceID
= aCallPeer.getMediaHandler().getRemoteSSRC(
mediaType);
if (aRemoteSourceID == remoteSourceID)
{
remoteSourceID = -1;
break;
}
}
}
}
}
return remoteSourceID;
}
/**
* Notifies this CallListener that a specific incoming
* Call has been received.
*
* @param event a CallEvent which specifies the newly-received
* incoming Call
*/
public void incomingCallReceived(CallEvent event)
{
callBegun(event);
}
/**
* Invites the callee represented by the specified uri to an already
* existing call. The difference between this method and createConfCall is
* that inviteCalleeToCall allows a user to transform an existing 1 to 1
* call into a conference call, or add new peers to an already established
* conference.
*
* @param uri the callee to invite to an existing conf call.
* @param call the call that we should invite the callee to.
* @return the CallPeer object corresponding to the callee represented by
* the specified uri.
* @throws OperationFailedException if inviting the specified callee to the
* specified call fails
*/
public CallPeer inviteCalleeToCall(String uri, Call call)
throws OperationFailedException
{
CalleeAddressT calleeAddress = parseAddressString(uri);
@SuppressWarnings("unchecked")
MediaAwareCallT mediaAwareCallT = (MediaAwareCallT) call;
mediaAwareCallT.getConference().setConferenceFocus(true);
return doInviteCalleeToCall(calleeAddress, mediaAwareCallT);
}
/**
* Notifies all CallPeers associated with the telephony conference
* in which a specific Call is participating about changes in the
* telephony conference-related information.
*
* @param call the Call which specifies the telephony conference
* the associated CallPeers of which are to be notified about
* changes in the telephony conference-related information
*/
@SuppressWarnings("rawtypes")
protected void notifyAll(Call call)
{
CallConference conference = call.getConference();
if (conference == null)
notifyCallPeers(call);
else
{
/*
* Make each Call notify its CallPeers through its
* OperationSetTelephonyConferencing (i.e. its protocol).
*/
for (Call conferenceCall : conference.getCalls())
{
OperationSetTelephonyConferencing opSet
= conferenceCall.getProtocolProvider().getOperationSet(
OperationSetTelephonyConferencing.class);
if (opSet instanceof AbstractOperationSetTelephonyConferencing)
{
((AbstractOperationSetTelephonyConferencing) opSet)
.notifyCallPeers(conferenceCall);
}
}
}
}
/**
* Notifies all CallPeers associated with a specific Call
* about changes in the telephony conference-related information. In
* contrast, {@link #notifyAll()} notifies all CallPeers associated
* with the telephony conference in which a specific Call is
* participating.
*
* @param call the Call whose CallPeers are to be notified
* about changes in the telephony conference-related information
*/
protected abstract void notifyCallPeers(Call call);
/**
* Notifies this CallListener that a specific outgoing
* Call has been created.
*
* @param event a CallEvent which specifies the newly-created
* outgoing Call
*/
public void outgoingCallCreated(CallEvent event)
{
callBegun(event);
}
/**
* Parses a String 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 Call.
*
* @param calleeAddressString a String 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 Call
* @return an object which is to actually represent the specified
* calleeAddressString during the invitation to a conference
* Call
* @throws OperationFailedException if parsing the specified
* calleeAddressString fails
*/
protected abstract CalleeAddressT parseAddressString(
String calleeAddressString)
throws OperationFailedException;
/**
* Notifies this PropertyChangeListener that the value of a
* specific property of the notifier it is registered with has changed.
*
* @param ev a PropertyChangeEvent which describes the source of
* the event, the name of the property which has changed its value and the
* old and new values of the property
* @see PropertyChangeListener#propertyChange(PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent ev)
{
String propertyName = ev.getPropertyName();
if (CallPeerMediaHandler.AUDIO_LOCAL_SSRC.equals(propertyName)
|| CallPeerMediaHandler.AUDIO_REMOTE_SSRC.equals(propertyName)
|| CallPeerMediaHandler.VIDEO_LOCAL_SSRC.equals(propertyName)
|| CallPeerMediaHandler.VIDEO_REMOTE_SSRC.equals(propertyName))
{
@SuppressWarnings("unchecked")
CallPeerMediaHandler mediaHandler
= (CallPeerMediaHandler) ev.getSource();
Call call = mediaHandler.getPeer().getCall();
if (call != null)
notifyAll(call);
}
}
/**
* Notifies this RegistrationStateChangeListener that the
* ProtocolProviderSerivce it is registered with has changed its
* registration state.
*
* @param event a RegistrationStateChangeEvent which specifies the
* old and the new value of the registration state of the
* ProtocolProviderService this
* RegistrationStateChangeListener listens to
*/
public void registrationStateChanged(RegistrationStateChangeEvent event)
{
RegistrationState newState = event.getNewState();
if (RegistrationState.REGISTERED.equals(newState))
{
@SuppressWarnings("unchecked")
OperationSetBasicTelephonyT basicTelephony
= (OperationSetBasicTelephonyT)
parentProvider.getOperationSet(
OperationSetBasicTelephony.class);
if (this.basicTelephony != basicTelephony)
{
OperationSetBasicTelephonyT oldValue = this.basicTelephony;
this.basicTelephony = basicTelephony;
basicTelephonyChanged(oldValue, this.basicTelephony);
}
}
else if (RegistrationState.UNREGISTERED.equals(newState))
{
if (basicTelephony != null)
{
OperationSetBasicTelephonyT oldValue = basicTelephony;
basicTelephony = null;
basicTelephonyChanged(oldValue, null);
}
}
}
/**
* Updates the conference-related properties of a specific CallPeer
* such as conferenceFocus and conferenceMembers with
* the information described in confInfo.
* confInfo must be a document with "full" state.
*
* @param callPeer the CallPeer which is a conference focus and has
* sent the specified conference-info XML document
* @param confInfo the conference-info XML document to use to update
* the conference-related information of the local peer represented
* by the associated Call. It must have a "full" state.
*/
private int setConferenceInfoDocument(
MediaAwareCallPeerT callPeer,
ConferenceInfoDocument confInfo)
{
NodeList usersList
= confInfo.getDocument().getElementsByTagName(ELEMENT_USERS);
ConferenceMember[] toRemove
= callPeer.getConferenceMembers().toArray(
AbstractCallPeer.NO_CONFERENCE_MEMBERS);
int toRemoveCount = toRemove.length;
boolean changed = false;
if (usersList.getLength() > 0)
{
NodeList userList = usersList.item(0).getChildNodes();
int userCount = userList.getLength();
Map conferenceMemberProperties
= new HashMap();
for (int userIndex = 0; userIndex < userCount; userIndex++)
{
Node user = userList.item(userIndex);
if (!ELEMENT_USER.equals(user.getNodeName()))
continue;
String address
= stripParametersFromAddress(
((Element) user).getAttribute("entity"));
if ((address == null) || (address.length() < 1))
continue;
/*
* Determine the ConferenceMembers who are no longer in the list
* i.e. are to be removed.
*/
AbstractConferenceMember conferenceMember = null;
for (int i = 0; i < toRemoveCount; i++)
{
ConferenceMember aConferenceMember
= toRemove[i];
if ((aConferenceMember != null)
&& address.equalsIgnoreCase(
aConferenceMember.getAddress()))
{
toRemove[i] = null;
conferenceMember
= (AbstractConferenceMember) aConferenceMember;
break;
}
}
// Create the new ones.
boolean addConferenceMember;
if (conferenceMember == null)
{
conferenceMember
= new AbstractConferenceMember(callPeer, address);
addConferenceMember = true;
}
else
addConferenceMember = false;
// Update the existing ones.
if (conferenceMember != null)
{
NodeList userChildList = user.getChildNodes();
int userChildCount = userChildList.getLength();
String displayName = null;
String endpointStatus = null;
conferenceMemberProperties.put(
ConferenceMember.AUDIO_SSRC_PROPERTY_NAME,
null);
conferenceMemberProperties.put(
ConferenceMember.AUDIO_STATUS_PROPERTY_NAME,
null);
conferenceMemberProperties.put(
ConferenceMember.VIDEO_SSRC_PROPERTY_NAME,
null);
conferenceMemberProperties.put(
ConferenceMember.VIDEO_STATUS_PROPERTY_NAME,
null);
for (int userChildIndex = 0;
userChildIndex < userChildCount;
userChildIndex++)
{
Node userChild = userChildList.item(userChildIndex);
String userChildName = userChild.getNodeName();
if (ELEMENT_DISPLAY_TEXT.equals(userChildName))
displayName = userChild.getTextContent();
else if (ELEMENT_ENDPOINT.equals(userChildName))
{
endpointStatus = getEndpointStatus(userChild);
getEndpointMediaProperties(
userChild,
conferenceMemberProperties);
}
}
conferenceMember.setDisplayName(displayName);
conferenceMember.setEndpointStatus(endpointStatus);
changed
= conferenceMember.setProperties(
conferenceMemberProperties);
if (addConferenceMember)
callPeer.addConferenceMember(conferenceMember);
}
}
}
/*
* Remove the ConferenceMember instances which are no longer present in
* the conference-info XML document.
*/
for (int i = 0; i < toRemoveCount; i++)
{
ConferenceMember conferenceMemberToRemove = toRemove[i];
if (conferenceMemberToRemove != null)
callPeer.removeConferenceMember(conferenceMemberToRemove);
}
if (changed)
notifyAll(callPeer.getCall());
callPeer.setLastConferenceInfoReceived(confInfo);
return confInfo.getVersion();
}
/**
* Updates the conference-related properties of a specific CallPeer
* such as conferenceFocus and conferenceMembers with
* information received from it as a conference focus in the form of a
* conference-info XML document.
*
* @param callPeer the CallPeer which is a conference focus and has
* sent the specified conference-info XML document
* @param conferenceInfoXML the conference-info XML document sent by
* callPeer in order to update the conference-related information
* of the local peer represented by the associated Call
* @return the value of the version attribute of the
* conference-info XML element of the specified
* conferenceInfoXML if it was successfully parsed and represented
* in the specified callPeer
*
* @throws XMLException If conferenceInfoXML couldn't be parsed as
* a ConferenceInfoDocument
*/
protected int setConferenceInfoXML(
MediaAwareCallPeerT callPeer,
String conferenceInfoXML)
throws XMLException
{
ConferenceInfoDocument confInfo
= new ConferenceInfoDocument(conferenceInfoXML);
/*
* The CallPeer sent conference-info XML so we're sure it's a
* conference focus.
*/
callPeer.setConferenceFocus(true);
/*
* The following implements the procedure outlined in section 4.6 of
* RFC4575 - Constructing Coherent State
*/
int documentVersion = confInfo.getVersion();
int ourVersion = callPeer.getLastConferenceInfoReceivedVersion();
ConferenceInfoDocument.State documentState = confInfo.getState();
if (ourVersion == -1)
{
if (documentState == ConferenceInfoDocument.State.FULL)
{
return setConferenceInfoDocument(callPeer, confInfo);
}
else
{
logger.warn("Received a conference-info document with state '"
+ documentState + "'. Cannot apply it, because we haven't "
+ "initialized a local document yet. Sending peer: "
+ callPeer);
return -1;
}
}
else if (documentVersion <= ourVersion)
{
if (logger.isInfoEnabled())
{
logger.info("Received a stale conference-info document. Local "
+ "version " + ourVersion + ", document version "
+ documentVersion + ". Sending peer: " + callPeer);
}
return -1;
}
else //ourVersion != -1 && ourVersion < documentVersion
{
if (documentState == ConferenceInfoDocument.State.FULL)
return setConferenceInfoDocument(callPeer, confInfo);
else if (documentState == ConferenceInfoDocument.State.DELETED)
{
logger.warn("Received a conference-info document with state" +
"'deleted', can't handle. Sending peer: " + callPeer);
return -1;
}
else if (documentState == ConferenceInfoDocument.State.PARTIAL)
{
if (documentVersion == ourVersion+1)
return updateConferenceInfoDocument(callPeer, confInfo);
else
{
/*
* According to RFC4575 we "MUST generate a subscription
* refresh request to trigger a full state notification".
*/
logger.warn("Received a Conference Information document "
+ "with state '" + documentState + "' and version "
+ documentVersion + ". Cannon apply it, because local "
+ "version is " + ourVersion + ". Sending peer: "
+ callPeer);
return -1;
}
}
else
return -1; //unreachable
}
}
/**
* Removes the parameters (specified after a semicolon) from a specific
* address String if any are present in it.
*
* @param address the String value representing an address from
* which any parameters are to be removed
* @return a String representing the specified address
* without any parameters
*/
public static String stripParametersFromAddress(String address)
{
if (address != null)
{
int parametersBeginIndex = address.indexOf(';');
if (parametersBeginIndex > -1)
address = address.substring(0, parametersBeginIndex);
}
return address;
}
/**
* Creates a ConferenceInfoDocument which describes the current
* state of the conference in which callPeer participates. The
* created document contains a "full" description (as opposed to a partial
* description, see RFC4575).
*
* @return a ConferenceInfoDocument which describes the current
* state of the conference in which this CallPeer participates.
*/
protected ConferenceInfoDocument getCurrentConferenceInfo(
MediaAwareCallPeer,?,?> callPeer)
{
ConferenceInfoDocument confInfo;
try
{
confInfo = new ConferenceInfoDocument();
}
catch (XMLException e)
{
return null;
}
confInfo.setState(ConferenceInfoDocument.State.FULL);
confInfo.setEntity(getLocalEntity(callPeer));
Call call = callPeer.getCall();
if (call == null)
return null;
List conferenceCallPeers = CallConference.getCallPeers(call);
confInfo.setUserCount(
1 /* the local peer/user */ + conferenceCallPeers.size());
/* The local user */
addPeerToConferenceInfo(confInfo, callPeer, false);
/* Remote users */
for (CallPeer conferenceCallPeer : conferenceCallPeers)
{
if (conferenceCallPeer instanceof MediaAwareCallPeer,?,?>)
{
addPeerToConferenceInfo(
confInfo,
(MediaAwareCallPeer,?,?>)conferenceCallPeer,
true);
}
}
return confInfo;
}
/**
* Adds a user element to confInfo which describes
* callPeer, or the local peer if remote is false.
*
* @param confInfo the ConferenceInformationDocument to which to
* add a user element
* @param callPeer the CallPeer which should be described
* @param remote true to describe callPeer, or
* false to describe the local peer.
*/
private void addPeerToConferenceInfo(
ConferenceInfoDocument confInfo,
MediaAwareCallPeer,?,?> callPeer,
boolean remote)
{
String entity
= remote ? callPeer.getEntity() : getLocalEntity(callPeer);
ConferenceInfoDocument.User user = confInfo.addNewUser(entity);
String displayName
= remote ? callPeer.getDisplayName() : getLocalDisplayName();
user.setDisplayText(displayName);
ConferenceInfoDocument.Endpoint endpoint = user.addNewEndpoint(entity);
endpoint.setStatus(
remote
? getEndpointStatus(callPeer)
: ConferenceInfoDocument.EndpointStatusType.connected);
CallPeerMediaHandler> mediaHandler
= callPeer.getMediaHandler();
for (MediaType mediaType : MediaType.values())
{
MediaStream stream = mediaHandler.getStream(mediaType);
if (stream != null || !remote)
{
long srcId = -1;
if (remote)
{
srcId = getRemoteSourceID(callPeer, mediaType);
}
else if (stream != null)
{
srcId = stream.getLocalSourceID();
}
else // stream == null && !remote
{
/*
* If we are describing the local peer, but we don't have
* media streams with callPeer (which is the case when we
* send conference-info while the other side is still
* ringing), we can try to obtain our local SSRC from the
* streams we have already set up with the other
* participants in the conference.
*/
for (MediaAwareCallPeer,?,?> otherCallPeer
: callPeer.getCall().getCallPeerList())
{
MediaStream otherStream
= otherCallPeer.getMediaHandler().getStream(
mediaType);
if (otherStream != null)
{
srcId = otherStream.getLocalSourceID();
break;
}
}
}
MediaDirection direction = MediaDirection.INACTIVE;
if (remote)
{
direction
= callPeer.getDirection(mediaType).getReverseDirection();
}
else
{
if (mediaType == MediaType.AUDIO &&
callPeer.getMediaHandler()
.isLocalAudioTransmissionEnabled())
direction = direction.or(MediaDirection.SENDONLY);
else if (mediaType == MediaType.VIDEO &&
callPeer.isLocalVideoStreaming())
direction = direction.or(MediaDirection.SENDONLY);
if (callPeer.getDirection(mediaType).allowsReceiving())
direction = direction.or(MediaDirection.RECVONLY);
}
if ((srcId != -1) || (direction != MediaDirection.INACTIVE))
{
ConferenceInfoDocument.Media media
= endpoint.addNewMedia(mediaType.toString());
media.setType(mediaType.toString());
if (srcId != -1)
media.setSrcId(Long.toString(srcId));
media.setStatus(direction.toString());
}
}
}
}
/**
* Returns a string to be used for the entity attribute of the
* user element for the local peer, in a Conference Information
* document to be sent to callPeer
*
* @param callPeer The CallPeer for which we are creating a
* Conference Information document.
* @return a string to be used for the entity attribute of the
* user element for the local peer, in a Conference Information
* document to be sent to callPeer
*/
protected abstract String getLocalEntity(CallPeer callPeer);
/**
* Returns the display name for the local peer, which is to be used when
* we send Conference Information.
* @return the display name for the local peer, which is to be used when
* we send Conference Information.
*/
protected abstract String getLocalDisplayName();
/**
* Gets the EndpointStatusType to use when describing
* callPeer in a Conference Information document.
*
* @param callPeer the CallPeer which is to get its state described
* in a status XML element of an endpoint XML element
* @return the EndpointStatusType to use when describing
* callPeer in a Conference Information document.
*/
private ConferenceInfoDocument.EndpointStatusType getEndpointStatus(
CallPeer callPeer)
{
CallPeerState callPeerState = callPeer.getState();
if (CallPeerState.ALERTING_REMOTE_SIDE.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.alerting;
if (CallPeerState.CONNECTING.equals(callPeerState)
|| CallPeerState
.CONNECTING_WITH_EARLY_MEDIA.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.pending;
if (CallPeerState.DISCONNECTED.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.disconnected;
if (CallPeerState.INCOMING_CALL.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.dialing_in;
if (CallPeerState.INITIATING_CALL.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.dialing_out;
/*
* RFC4575 does not list an appropriate endpoint status for
* "remotely on hold", e.g. the endpoint is not "hearing" the conference
* mix, but it's media stream *is* being mixed into the conference.
*
* We use the on-hold status anyway, because it's the one that makes
* the most sense.
*/
if (CallPeerState.ON_HOLD_REMOTELY.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.on_hold;
/*
* he/she is neither "hearing" the conference mix nor is his/her
* media being mixed in the conference
*/
if (CallPeerState.ON_HOLD_LOCALLY.equals(callPeerState)
|| CallPeerState.ON_HOLD_MUTUALLY.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.on_hold;
if (CallPeerState.CONNECTED.equals(callPeerState))
return ConferenceInfoDocument.EndpointStatusType.connected;
return null;
}
/**
* @param from A document with state full from which to generate a
* "diff".
* @param to A document with state full to which to generate a
* "diff"
* @return a ConferenceInfoDocument, such that when it is applied
* to from using the procedure defined in section 4.6 of RFC4575,
* the result is to. May return null if from and
* to are not found to be different (that is, in case no document
* needs to be sent)
*/
protected ConferenceInfoDocument getConferenceInfoDiff(
ConferenceInfoDocument from,
ConferenceInfoDocument to)
throws IllegalArgumentException
{
if (from.getState() != ConferenceInfoDocument.State.FULL)
throw new IllegalArgumentException("The 'from' document needs to "
+ "have state=full");
if (to.getState() != ConferenceInfoDocument.State.FULL)
throw new IllegalArgumentException("The 'to' document needs to "
+ "have state=full");
if (!isPartialNotificationEnabled())
{
return conferenceInfoDocumentsMatch(from, to)
? null
: to;
}
ConferenceInfoDocument diff;
try
{
diff = new ConferenceInfoDocument();
}
catch (XMLException e)
{
return conferenceInfoDocumentsMatch(from, to)
? null
: to;
}
diff.setState(ConferenceInfoDocument.State.PARTIAL);
diff.setUsersState(ConferenceInfoDocument.State.PARTIAL);
//temporary, used for xmpp only
String sid = to.getSid();
if (sid != null && !sid.equals(""))
diff.setSid(to.getSid());
diff.setUserCount(to.getUserCount());
diff.setEntity(to.getEntity());
boolean needsPartial = false;
boolean hasDifference = false;
if (!from.getEntity().equals(to.getEntity())
|| from.getUserCount() != to.getUserCount())
{
hasDifference = true;
}
// find users which have been removed in 'to'
for (ConferenceInfoDocument.User user : from.getUsers())
{
if(to.getUser(user.getEntity()) == null)
{
ConferenceInfoDocument.User deletedUser
= diff.addNewUser(user.getEntity());
deletedUser.setState(ConferenceInfoDocument.State.DELETED);
hasDifference = true;
needsPartial = true;
}
}
for (ConferenceInfoDocument.User toUser : to.getUsers())
{
ConferenceInfoDocument.User fromUser
= from.getUser(toUser.getEntity());
if (!usersMatch(toUser, fromUser))
{
hasDifference = true;
diff.addUser(toUser);
}
else
{
//if there is a "user" element which didn't change, we skip it
//and we need to send state=partial, because otherwise it will
//be removed by the recipient
needsPartial = true;
}
}
if (logger.isDebugEnabled())
{
logger.debug("Generated partial notification. From: " + from
+ "\nTo: " + to + "\nDiff: " + diff + "(hasDifference: "
+ hasDifference + ")");
}
if (!hasDifference)
return null;
/*
* In some cases (when all the user elements have changed, and none have
* been removed) we are essentially generating a full document, but
* marking it 'partial'. In this case it is better to send the full
* document, just in case the receiver lost the previous document
* somehow.
*/
if (!needsPartial)
{
diff.setState(ConferenceInfoDocument.State.FULL);
diff.setUsersState(ConferenceInfoDocument.State.FULL);
}
return diff;
}
/**
* Updates the conference-related properties of a specific CallPeer
* such as conferenceFocus and conferenceMembers with
* information received from it as a conference focus in the form of a
* partial conference-info XML document.
*
* @param callPeer the CallPeer which is a conference focus and has
* sent the specified partial conference-info XML document
* @param diff the partial conference-info XML document sent by
* callPeer in order to update the conference-related information
* of the local peer represented by the associated Call
* @return the value of the version attribute of the
* conference-info XML element of the specified
* conferenceInfoXML if it was successfully parsed and represented
* in the specified callPeer
*/
private int updateConferenceInfoDocument(
MediaAwareCallPeerT callPeer,
ConferenceInfoDocument diff)
{
// "apply" diff to ourDocument, result is in newDocument
ConferenceInfoDocument ourDocument
= callPeer.getLastConferenceInfoReceived();
ConferenceInfoDocument newDocument;
ConferenceInfoDocument.State usersState = diff.getUsersState();
if (usersState == ConferenceInfoDocument.State.FULL)
{
//if users is 'full', all its children must be full
try
{
newDocument = new ConferenceInfoDocument(diff);
}
catch (XMLException e)
{
logger.error("Could not create a new ConferenceInfoDocument");
return -1;
}
newDocument.setState(ConferenceInfoDocument.State.FULL);
}
else if (usersState == ConferenceInfoDocument.State.DELETED)
{
try
{
newDocument = new ConferenceInfoDocument();
}
catch (XMLException e)
{
logger.error("Could not create a new ConferenceInfoDocument", e);
return -1;
}
newDocument.setVersion(diff.getVersion());
newDocument.setEntity(diff.getEntity());
newDocument.setUserCount(diff.getUserCount());
}
else //'partial'
{
try
{
newDocument = new ConferenceInfoDocument(ourDocument);
}
catch (XMLException e)
{
logger.error("Could not create a new ConferenceInfoDocument", e);
return -1;
}
newDocument.setVersion(diff.getVersion());
newDocument.setEntity(diff.getEntity());
newDocument.setUserCount(diff.getUserCount());
for (ConferenceInfoDocument.User user : diff.getUsers())
{
ConferenceInfoDocument.State userState = user.getState();
if (userState == ConferenceInfoDocument.State.FULL)
{
newDocument.removeUser(user.getEntity());
newDocument.addUser(user);
}
else if (userState == ConferenceInfoDocument.State.DELETED)
{
newDocument.removeUser(user.getEntity());
}
else // partial
{
ConferenceInfoDocument.User ourUser
= newDocument.getUser(user.getEntity());
ourUser.setDisplayText(user.getDisplayText());
for (ConferenceInfoDocument.Endpoint endpoint
: user.getEndpoints())
{
ConferenceInfoDocument.State endpointState
= endpoint.getState();
if (endpointState == ConferenceInfoDocument.State.FULL)
{
ourUser.removeEndpoint(endpoint.getEntity());
ourUser.addEndpoint(endpoint);
}
else if (endpointState
== ConferenceInfoDocument.State.DELETED)
{
ourUser.removeEndpoint(endpoint.getEntity());
}
else // partial
{
ConferenceInfoDocument.Endpoint ourEndpoint
= ourUser.getEndpoint(endpoint.getEntity());
for (ConferenceInfoDocument.Media media
: endpoint.getMedias())
{
ourEndpoint.removeMedia(media.getId());
ourEndpoint.addMedia(media);
}
}
}
}
}
}
if (logger.isDebugEnabled())
{
logger.debug("Applied a partial conference-info notification. "
+ " Base: " + ourDocument + "\nDiff: " + diff + "\nResult:"
+ newDocument);
}
return setConferenceInfoDocument(callPeer, newDocument);
}
/**
* @param a A document with state full which to compare to
* b
* @param b A document with state full which to compare to
* a
* @return false if the two documents are found to be different,
* true otherwise (that is, it can return true for non-identical
* documents).
*/
private boolean conferenceInfoDocumentsMatch(
ConferenceInfoDocument a,
ConferenceInfoDocument b)
{
if (a.getState() != ConferenceInfoDocument.State.FULL)
throw new IllegalArgumentException("The 'a' document needs to"
+ "have state=full");
if (b.getState() != ConferenceInfoDocument.State.FULL)
throw new IllegalArgumentException("The 'b' document needs to"
+ "have state=full");
if (!stringsMatch(a.getEntity(), b.getEntity()))
return false;
else if (a.getUserCount() != b.getUserCount())
return false;
else if (a.getUsers().size() != b.getUsers().size())
return false;
for(ConferenceInfoDocument.User aUser : a.getUsers())
{
if (!usersMatch(aUser, b.getUser(aUser.getEntity())))
return false;
}
return true;
}
/**
* Checks whether two ConferenceInfoDocument.User instances
* match according to the needs of our implementation. Can return
* true for users which are not identical.
*
* @param a A ConferenceInfoDocument.User to compare
* @param b A ConferenceInfoDocument.User to compare
* @return false if a and b are found to be
* different in a way that is significant for our needs, true
* otherwise.
*/
private boolean usersMatch(
ConferenceInfoDocument.User a,
ConferenceInfoDocument.User b)
{
if (a == null && b == null)
return true;
else if (a == null || b == null)
return false;
else if (!stringsMatch(a.getEntity(), b.getEntity()))
return false;
else if (!stringsMatch(a.getDisplayText(), b.getDisplayText()))
return false;
else if (a.getEndpoints().size() != b.getEndpoints().size())
return false;
for (ConferenceInfoDocument.Endpoint aEndpoint : a.getEndpoints())
{
if (!endpointsMatch(aEndpoint, b.getEndpoint(aEndpoint.getEntity())))
return false;
}
return true;
}
/**
* Checks whether two ConferenceInfoDocument.Endpoint instances
* match according to the needs of our implementation. Can return
* true for endpoints which are not identical.
*
* @param a A ConferenceInfoDocument.Endpoint to compare
* @param b A ConferenceInfoDocument.Endpoint to compare
* @return false if a and b are found to be
* different in a way that is significant for our needs, true
* otherwise.
*/
private boolean endpointsMatch(
ConferenceInfoDocument.Endpoint a,
ConferenceInfoDocument.Endpoint b)
{
if (a == null && b == null)
return true;
else if (a == null || b == null)
return false;
else if (!stringsMatch(a.getEntity(), b.getEntity()))
return false;
else if (a.getStatus() != b.getStatus())
return false;
else if (a.getMedias().size() != b.getMedias().size())
return false;
for (ConferenceInfoDocument.Media aMedia : a.getMedias())
{
if (!mediasMatch(aMedia, b.getMedia(aMedia.getId())))
return false;
}
return true;
}
/**
* Checks whether two ConferenceInfoDocument.Media instances
* match according to the needs of our implementation. Can return
* true for endpoints which are not identical.
*
* @param a A ConferenceInfoDocument.Media to compare
* @param b A ConferenceInfoDocument.Media to compare
* @return false if a and b are found to be
* different in a way that is significant for our needs, true
* otherwise.
*/
private boolean mediasMatch(
ConferenceInfoDocument.Media a,
ConferenceInfoDocument.Media b)
{
if (a == null && b == null)
return true;
else if (a == null || b == null)
return false;
else if (!stringsMatch(a.getId(), b.getId()))
return false;
else if (!stringsMatch(a.getSrcId(), b.getSrcId()))
return false;
else if (!stringsMatch(a.getType(), b.getType()))
return false;
else if (!stringsMatch(a.getStatus(), b.getStatus()))
return false;
return true;
}
/**
* @param a A String to compare to b
* @param b A String to compare to a
* @return true if and only if a and b are both
* null, or they are equal as Strings
*/
private boolean stringsMatch(String a, String b)
{
if (a == null && b == null)
return true;
else if (a == null || b == null)
return false;
return a.equals(b);
}
/**
* Checks whether sending of RFC4575 partial notifications is enabled in
* the configuration. If disabled, RFC4575 documents will always be sent
* with state 'full'.
* @return true if sending of RFC4575 partial notifications is
* enabled in the configuration.
*/
private boolean isPartialNotificationEnabled()
{
String s = parentProvider.getAccountID()
.getAccountProperties()
.get(PARTIAL_NOTIFICATIONS_PROP_NAME);
return (s == null || Boolean.parseBoolean(s));
}
/**
* {@inheritDoc}
*
* Unimplemented by default, returns null.
*/
@Override
public ConferenceDescription setupConference(ChatRoom chatRoom)
{
return null;
}
}