/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.service.protocol; import java.net.*; import java.util.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.jitsi.service.protocol.event.*; import org.jitsi.util.event.*; /** * Provides a default implementation for most of the CallPeer methods * with the purpose of only leaving custom protocol development to clients using * the PhoneUI service. * * @param the call extension class like for example CallSipImpl * or CallJabberImpl * @param the provider extension class like for example * ProtocolProviderServiceSipImpl or * ProtocolProviderServiceJabberImpl * * @author Emil Ivov * @author Lyubomir Marinov * @author Yana Stamcheva */ public abstract class AbstractCallPeer extends PropertyChangeNotifier implements CallPeer { /** * Our class logger. */ private static final Logger logger = Logger.getLogger(AbstractCallPeer.class); /** * The constant which describes an empty set of ConferenceMembers * (and which can be used to reduce allocations). */ public static final ConferenceMember[] NO_CONFERENCE_MEMBERS = new ConferenceMember[0]; /** * The time this call started at. */ private long callDurationStartTime = CALL_DURATION_START_TIME_UNKNOWN; /** * The list of CallPeerConferenceListeners interested in and to be * notified about changes in conference-related information such as this * peer acting or not acting as a conference focus and conference membership * details. */ protected final List callPeerConferenceListeners = new ArrayList(); /** * All the CallPeer listeners registered with this CallPeer. */ protected final List callPeerListeners = new ArrayList(); /** * All the CallPeerSecurityListener-s registered with this CallPeer. */ protected final List callPeerSecurityListeners = new ArrayList(); /** * The indicator which determines whether this peer is acting as a * conference focus and thus may provide information about * ConferenceMember such as {@link #getConferenceMembers()} and * {@link #getConferenceMemberCount()}. */ private boolean conferenceFocus; /** * The list of ConferenceMembers currently known to and managed in * a conference by this CallPeer. It is implemented as a * copy-on-write storage in order to optimize the implementation of * {@link #getConferenceMembers()} which is used more often than * {@link #addConferenceMember(ConferenceMember)} and * {@link #removeConferenceMember(ConferenceMember)}. */ private List conferenceMembers; /** * The Object which synchronizes the access to * {@link #conferenceMembers} and {@link #unmodifiableConferenceMembers}. */ private final Object conferenceMembersSyncRoot = new Object(); /** * The flag that determines whether our audio stream to this call peer is * currently muted. */ private boolean isMute = false; /** * The last fired security event. */ private CallPeerSecurityStatusEvent lastSecurityEvent; /** * The state of the call peer. */ private CallPeerState state = CallPeerState.UNKNOWN; /** * An unmodifiable view of {@link #conferenceMembers}. The list of * ConferenceMembers participating in the conference managed by * this instance is implemented as a copy-on-write storage in order to * optimize the implementation of {@link #getConferenceMembers()} which is * used more often than {@link #addConferenceMember(ConferenceMember)} and * {@link #removeConferenceMember(ConferenceMember)}. */ private List unmodifiableConferenceMembers; /** * Initializes a new AbstractCallPeer instance. */ protected AbstractCallPeer() { conferenceMembers = Collections.emptyList(); unmodifiableConferenceMembers = Collections.unmodifiableList(conferenceMembers); } /** * Implements * CallPeer#addCallPeerConferenceListener( * CallPeerConferenceListener). In the fashion of the addition of the * other listeners, does not throw an exception on attempting to add a * null listeners and just ignores the call. * * @param listener the CallPeerConferenceListener to add */ public void addCallPeerConferenceListener( CallPeerConferenceListener listener) { if (listener != null) synchronized (callPeerConferenceListeners) { if (!callPeerConferenceListeners.contains(listener)) callPeerConferenceListeners.add(listener); } } /** * Registers the listener to the list of listeners that would be * receiving CallPeerEvents. * * @param listener a listener instance to register with this peer. */ public void addCallPeerListener(CallPeerListener listener) { if (listener == null) return; synchronized(callPeerListeners) { if (!callPeerListeners.contains(listener)) callPeerListeners.add(listener); } } /** * Registers the listener to the list of listeners that would be * receiving CallPeerSecurityEvents. * * @param listener a listener instance to register with this peer. */ public void addCallPeerSecurityListener( CallPeerSecurityListener listener) { if (listener == null) return; synchronized(callPeerSecurityListeners) { if (!callPeerSecurityListeners.contains(listener)) callPeerSecurityListeners.add(listener); } } /** * Adds a specific ConferenceMember to the list of * ConferenceMembers reported by this peer through * {@link #getConferenceMembers()} and {@link #getConferenceMemberCount()} * and fires * CallPeerConferenceEvent#CONFERENCE_MEMBER_ADDED to * the currently registered CallPeerConferenceListeners. * * @param conferenceMember a ConferenceMember to be added to the * list of ConferenceMember reported by this peer. If the specified * ConferenceMember is already contained in the list, it is not * added again and no event is fired. */ public void addConferenceMember(ConferenceMember conferenceMember) { if (conferenceMember == null) throw new NullPointerException("conferenceMember"); else { synchronized (conferenceMembersSyncRoot) { if (conferenceMembers.contains(conferenceMember)) return; else { List newConferenceMembers = new ArrayList(conferenceMembers); if (newConferenceMembers.add(conferenceMember)) { conferenceMembers = newConferenceMembers; unmodifiableConferenceMembers = Collections.unmodifiableList(conferenceMembers); } else return; } } fireCallPeerConferenceEvent( new CallPeerConferenceEvent( this, CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED, conferenceMember)); } } /** * Finds the first ConferenceMember whose audioSsrc is * equals to a specific value. The method is meant for very frequent use so * it iterates over the List of ConferenceMembers without * creating an Iterator. * * @param ssrc the SSRC identifier of the audio RTP streams transmitted by * the ConferenceMember that we are looking for. * @return the first ConferenceMember whose audioSsrc is * equal to ssrc or null if no such * ConferenceMember was found */ protected ConferenceMember findConferenceMember(long ssrc) { List members = getConferenceMembers(); for (int i = 0, memberCount = members.size(); i < memberCount; i++) { ConferenceMember member = members.get(i); if (member.getAudioSsrc() == ssrc) return member; } return null; } /** * Constructs a CallPeerChangeEvent using this call peer as source, * setting it to be of type eventType and the corresponding * oldValue and newValue, * * @param eventType the type of the event to create and dispatch. * @param oldValue the value of the source property before it changed. * @param newValue the current value of the source property. */ protected void fireCallPeerChangeEvent(String eventType, Object oldValue, Object newValue) { this.fireCallPeerChangeEvent( eventType, oldValue, newValue, null); } /** * Constructs a CallPeerChangeEvent using this call peer as source, * setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param eventType the type of the event to create and dispatch. * @param oldValue the value of the source property before it changed. * @param newValue the current value of the source property. * @param reason a string that could be set to contain a human readable * explanation for the transition (particularly handy when moving into a * FAILED state). */ protected void fireCallPeerChangeEvent(String eventType, Object oldValue, Object newValue, String reason) { this.fireCallPeerChangeEvent(eventType, oldValue, newValue, reason, -1); } /** * Constructs a CallPeerChangeEvent using this call peer as source, * setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param eventType the type of the event to create and dispatch. * @param oldValue the value of the source property before it changed. * @param newValue the current value of the source property. * @param reason a string that could be set to contain a human readable * explanation for the transition (particularly handy when moving into a * FAILED state). * @param reasonCode the reason code for the reason of this event. */ protected void fireCallPeerChangeEvent(String eventType, Object oldValue, Object newValue, String reason, int reasonCode) { CallPeerChangeEvent evt = new CallPeerChangeEvent( this, eventType, oldValue, newValue, reason, reasonCode); if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerChangeEvent event to " + callPeerListeners.size() +" listeners. event is: " + evt.toString()); Iterator listeners = null; synchronized (callPeerListeners) { listeners = new ArrayList( callPeerListeners).iterator(); } while (listeners.hasNext()) { CallPeerListener listener = listeners.next(); // catch any possible errors, so we are sure we dispatch events // to all listeners try { if(eventType.equals(CallPeerChangeEvent .CALL_PEER_ADDRESS_CHANGE)) { listener.peerAddressChanged(evt); } else if(eventType.equals(CallPeerChangeEvent .CALL_PEER_DISPLAY_NAME_CHANGE)) { listener.peerDisplayNameChanged(evt); } else if(eventType.equals(CallPeerChangeEvent .CALL_PEER_IMAGE_CHANGE)) { listener.peerImageChanged(evt); } else if(eventType.equals(CallPeerChangeEvent .CALL_PEER_STATE_CHANGE)) { listener.peerStateChanged(evt); } } catch(Throwable t) { logger.error("Error dispatching event of type" + eventType + " in " + listener, t); } } } /** * Fires a specific CallPeerConferenceEvent to the * CallPeerConferenceListeners interested in changes in the * conference-related information provided by this peer. * * @param conferenceEvent a CallPeerConferenceEvent to be fired and * carrying the event data */ protected void fireCallPeerConferenceEvent( CallPeerConferenceEvent conferenceEvent) { CallPeerConferenceListener[] listeners; synchronized (callPeerConferenceListeners) { listeners = callPeerConferenceListeners .toArray( new CallPeerConferenceListener[ callPeerConferenceListeners.size()]); } int eventID = conferenceEvent.getEventID(); if (logger.isDebugEnabled()) { String eventIDString; switch (eventID) { case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED: eventIDString = "CONFERENCE_FOCUS_CHANGED"; break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED: eventIDString = "CONFERENCE_MEMBER_ADDED"; break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED: eventIDString = "CONFERENCE_MEMBER_REMOVED"; break; default: eventIDString = "UNKNOWN"; break; } logger .debug( "Firing CallPeerConferenceEvent with ID " + eventIDString + " to " + listeners.length + " listeners"); } for (CallPeerConferenceListener listener : listeners) switch (eventID) { case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED: listener.conferenceFocusChanged(conferenceEvent); break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED: listener.conferenceMemberAdded(conferenceEvent); break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED: listener.conferenceMemberRemoved(conferenceEvent); break; } } /** * Constructs a CallPeerSecurityStatusEvent using this call peer as * source, setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param messageType the type of the message * @param i18nMessage message * @param severity severity level */ protected void fireCallPeerSecurityMessageEvent( String messageType, String i18nMessage, int severity) { CallPeerSecurityMessageEvent evt = new CallPeerSecurityMessageEvent( this, messageType, i18nMessage, severity); if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerSecurityFailedEvent event to " + callPeerSecurityListeners.size() +" listeners. event is: " + evt.toString()); Iterator listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList( callPeerSecurityListeners).iterator(); } while (listeners.hasNext()) { CallPeerSecurityListener listener = listeners.next(); listener.securityMessageRecieved(evt); } } /** * Constructs a CallPeerSecurityStatusEvent using this call peer as * source, setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param evt the event object with details to pass on to the consumers */ protected void fireCallPeerSecurityNegotiationStartedEvent( CallPeerSecurityNegotiationStartedEvent evt) { lastSecurityEvent = evt; if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerSecurityStatusEvent event to " + callPeerSecurityListeners.size() +" listeners. event is: " + evt.toString()); List listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityNegotiationStarted(evt); } } /** * Constructs a CallPeerSecurityStatusEvent using this call peer as * source, setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param evt the event object with details to pass on to the consumers */ protected void fireCallPeerSecurityOffEvent(CallPeerSecurityOffEvent evt) { lastSecurityEvent = evt; if (logger.isDebugEnabled()) logger.debug( "Dispatching a CallPeerSecurityAuthenticationEvent event to " + callPeerSecurityListeners.size() +" listeners. event is: " + evt.toString()); List listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityOff(evt); } } /** * Constructs a CallPeerSecurityStatusEvent using this call peer as * source, setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param evt the event object with details to pass on to the consumers */ protected void fireCallPeerSecurityOnEvent(CallPeerSecurityOnEvent evt) { lastSecurityEvent = evt; if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerSecurityStatusEvent event to " + callPeerSecurityListeners.size() +" listeners. event is: " + evt.toString()); List listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityOn(evt); } } /** * Constructs a CallPeerSecurityStatusEvent using this call peer as * source, setting it to be of type eventType and the corresponding * oldValue and newValue. * * @param evt the event object with details to pass on to the consumers */ protected void fireCallPeerSecurityTimeoutEvent( CallPeerSecurityTimeoutEvent evt) { lastSecurityEvent = evt; if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerSecurityStatusEvent event to " + callPeerSecurityListeners.size() +" listeners. event is: " + evt.toString()); List listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityTimeout(evt); } } /** * Returns a reference to the call that this peer belongs to. * * @return a reference to the call containing this peer. */ public abstract T getCall(); /** * Gets the time at which this CallPeer transitioned into a state * (likely {@link CallPeerState#CONNECTED}) marking the start of the * duration of the participation in a Call. * * @return the time at which this CallPeer transitioned into a * state marking the start of the duration of the participation in a * Call or {@link CallPeer#CALL_DURATION_START_TIME_UNKNOWN} if * such a transition has not been performed */ public long getCallDurationStartTime() { return callDurationStartTime; } /** * Returns a URL pointing ta a location with call control information for * this peer or null if no such URL is available for this call * peer. * * @return a URL link to a location with call information or a call control * web interface related to this peer or null if no such URL is * available. */ public URL getCallInfoURL() { //if signaling protocols (such as SIP) know where to get this URL from //they should override this method return null; } /** * {@inheritDoc} */ public int getConferenceMemberCount() { synchronized (conferenceMembersSyncRoot) { return isConferenceFocus() ? getConferenceMembers().size() : 0; } } /** * {@inheritDoc} */ public List getConferenceMembers() { synchronized (conferenceMembersSyncRoot) { return unmodifiableConferenceMembers; } } /** * Returns the currently used security settings of this CallPeer. * * @return the CallPeerSecurityStatusEvent that contains the * current security settings. */ public CallPeerSecurityStatusEvent getCurrentSecuritySettings() { return lastSecurityEvent; } /** * Returns the protocol provider that this peer belongs to. * * @return a reference to the ProtocolProviderService that this peer * belongs to. */ public abstract U getProtocolProvider(); /** * Returns an object representing the current state of that peer. * * @return a CallPeerState instance representing the peer's state. */ public CallPeerState getState() { return state; } /** * Determines whether this call peer is currently a conference focus. * * @return true if this peer is a conference focus and * false otherwise. */ public boolean isConferenceFocus() { return conferenceFocus; } /** * Determines whether the audio stream (if any) being sent to this peer is * mute. *

* The default implementation returns false. *

* * @return true if an audio stream is being sent to this peer and * it is currently mute; false, otherwise */ public boolean isMute() { return isMute; } /** * Implements * CallPeer#removeCallPeerConferenceListener( * CallPeerConferenceListener). * * @param listener the CallPeerConferenceListener to remove */ public void removeCallPeerConferenceListener( CallPeerConferenceListener listener) { if (listener != null) synchronized (callPeerConferenceListeners) { callPeerConferenceListeners.remove(listener); } } /** * Unregisters the specified listener. * * @param listener the listener to unregister. */ public void removeCallPeerListener(CallPeerListener listener) { if (listener == null) return; synchronized(callPeerListeners) { callPeerListeners.remove(listener); } } /** * Unregisters the specified listener. * * @param listener the listener to unregister. */ public void removeCallPeerSecurityListener( CallPeerSecurityListener listener) { if (listener == null) return; synchronized(callPeerSecurityListeners) { callPeerSecurityListeners.remove(listener); } } /** * Removes a specific ConferenceMember from the list of * ConferenceMembers reported by this peer through * {@link #getConferenceMembers()} and {@link #getConferenceMemberCount()} * if it is contained and fires * CallPeerConferenceEvent#CONFERENCE_MEMBER_REMOVED to * the currently registered CallPeerConferenceListeners. * * @param conferenceMember a ConferenceMember to be removed from * the list of ConferenceMember reported by this peer. If the * specified ConferenceMember is no contained in the list, no event * is fired. */ public void removeConferenceMember(ConferenceMember conferenceMember) { if (conferenceMember != null) { synchronized (conferenceMembersSyncRoot) { if (conferenceMembers.contains(conferenceMember)) { List newConferenceMembers = new ArrayList(conferenceMembers); if (newConferenceMembers.remove(conferenceMember)) { conferenceMembers = newConferenceMembers; unmodifiableConferenceMembers = Collections.unmodifiableList(conferenceMembers); } else return; } else return; } fireCallPeerConferenceEvent( new CallPeerConferenceEvent( this, CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED, conferenceMember)); } } /** * Specifies whether this peer is a conference focus. * * @param conferenceFocus true if this peer is to become a * conference focus and false otherwise. */ public void setConferenceFocus(boolean conferenceFocus) { if (this.conferenceFocus != conferenceFocus) { this.conferenceFocus = conferenceFocus; fireCallPeerConferenceEvent( new CallPeerConferenceEvent( this, CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED)); } } /** * Sets the mute property for this call peer. * * @param newMuteValue the new value of the mute property for this call peer */ public void setMute(boolean newMuteValue) { this.isMute = newMuteValue; firePropertyChange(MUTE_PROPERTY_NAME, isMute, newMuteValue); } /** * Causes this CallPeer to enter the specified state. The method also sets * the currentStateStartDate field and fires a CallPeerChangeEvent. * * @param newState the state this call peer should enter. */ public void setState(CallPeerState newState) { setState(newState, null); } /** * Causes this CallPeer to enter the specified state. The method also sets * the currentStateStartDate field and fires a CallPeerChangeEvent. * * @param newState the state this call peer should enter. * @param reason a string that could be set to contain a human readable * explanation for the transition (particularly handy when moving into a * FAILED state). */ public void setState(CallPeerState newState, String reason) { setState(newState, reason, -1); } /** * Causes this CallPeer to enter the specified state. The method also sets * the currentStateStartDate field and fires a CallPeerChangeEvent. * * @param newState the state this call peer should enter. * @param reason a string that could be set to contain a human readable * explanation for the transition (particularly handy when moving into a * FAILED state). * @param reasonCode the code for the reason of the state change. */ public void setState(CallPeerState newState, String reason, int reasonCode) { CallPeerState oldState = getState(); if(oldState == newState) return; this.state = newState; if (CallPeerState.CONNECTED.equals(newState) && !CallPeerState.isOnHold(oldState)) { callDurationStartTime = System.currentTimeMillis(); } fireCallPeerChangeEvent( CallPeerChangeEvent.CALL_PEER_STATE_CHANGE, oldState, newState, reason, reasonCode); } /** * Returns a string representation of the peer in the form of *
* Display Name <address>;status=CallPeerStatus * * @return a string representation of the peer and its state. */ @Override public String toString() { return getDisplayName() + " <" + getAddress() + ">;status=" + getState().getStateString(); } }