/* * 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; import java.net.*; import java.util.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.jitsi.util.event.*; /** * Provides a default implementation for most of the <tt>CallPeer</tt> methods * with the purpose of only leaving custom protocol development to clients using * the PhoneUI service. * * @param <T> the call extension class like for example <tt>CallSipImpl</tt> * or <tt>CallJabberImpl</tt> * @param <U> the provider extension class like for example * <tt>ProtocolProviderServiceSipImpl</tt> or * <tt>ProtocolProviderServiceJabberImpl</tt> * * @author Emil Ivov * @author Lyubomir Marinov * @author Yana Stamcheva */ public abstract class AbstractCallPeer<T extends Call, U extends ProtocolProviderService> extends PropertyChangeNotifier implements CallPeer { /** * Our class logger. */ private static final Logger logger = Logger.getLogger(AbstractCallPeer.class); /** * The constant which describes an empty set of <tt>ConferenceMember</tt>s * (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 <tt>CallPeerConferenceListener</tt>s 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<CallPeerConferenceListener> callPeerConferenceListeners = new ArrayList<CallPeerConferenceListener>(); /** * All the CallPeer listeners registered with this CallPeer. */ protected final List<CallPeerListener> callPeerListeners = new ArrayList<CallPeerListener>(); /** * All the CallPeerSecurityListener-s registered with this CallPeer. */ protected final List<CallPeerSecurityListener> callPeerSecurityListeners = new ArrayList<CallPeerSecurityListener>(); /** * The indicator which determines whether this peer is acting as a * conference focus and thus may provide information about * <tt>ConferenceMember</tt> such as {@link #getConferenceMembers()} and * {@link #getConferenceMemberCount()}. */ private boolean conferenceFocus; /** * The list of <tt>ConferenceMember</tt>s currently known to and managed in * a conference by this <tt>CallPeer</tt>. 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<ConferenceMember> conferenceMembers; /** * The <tt>Object</tt> 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; /** * */ private String alternativeIMPPAddress; /** * An unmodifiable view of {@link #conferenceMembers}. The list of * <tt>ConferenceMember</tt>s 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<ConferenceMember> unmodifiableConferenceMembers; /** * Initializes a new <tt>AbstractCallPeer</tt> instance. */ protected AbstractCallPeer() { conferenceMembers = Collections.emptyList(); unmodifiableConferenceMembers = Collections.unmodifiableList(conferenceMembers); } /** * Returns an alternative IMPP address corresponding to this * <tt>CallPeer</tt>. * * @return a string representing an alternative IMPP address corresponding * to this <tt>CallPeer</tt> */ public String getAlternativeIMPPAddress() { return alternativeIMPPAddress; } /** * Returns an alternative IMPP address corresponding to this * <tt>CallPeer</tt>. * * @param address an alternative IMPP address corresponding to this * <tt>CallPeer</tt> */ public void setAlternativeIMPPAddress(String address) { alternativeIMPPAddress = address; } /** * Implements * <tt>CallPeer#addCallPeerConferenceListener( * CallPeerConferenceListener)</tt>. In the fashion of the addition of the * other listeners, does not throw an exception on attempting to add a * <tt>null</tt> listeners and just ignores the call. * * @param listener the <tt>CallPeerConferenceListener</tt> to add */ public void addCallPeerConferenceListener( CallPeerConferenceListener listener) { if (listener != null) synchronized (callPeerConferenceListeners) { if (!callPeerConferenceListeners.contains(listener)) callPeerConferenceListeners.add(listener); } } /** * Registers the <tt>listener</tt> 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 <tt>listener</tt> 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 <tt>ConferenceMember</tt> to the list of * <tt>ConferenceMember</tt>s reported by this peer through * {@link #getConferenceMembers()} and {@link #getConferenceMemberCount()} * and fires * <tt>CallPeerConferenceEvent#CONFERENCE_MEMBER_ADDED</tt> to * the currently registered <tt>CallPeerConferenceListener</tt>s. * * @param conferenceMember a <tt>ConferenceMember</tt> to be added to the * list of <tt>ConferenceMember</tt> reported by this peer. If the specified * <tt>ConferenceMember</tt> 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<ConferenceMember> newConferenceMembers = new ArrayList<ConferenceMember>(conferenceMembers); if (newConferenceMembers.add(conferenceMember)) { conferenceMembers = newConferenceMembers; unmodifiableConferenceMembers = Collections.unmodifiableList(conferenceMembers); } else return; } } fireCallPeerConferenceEvent( new CallPeerConferenceEvent( this, CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED, conferenceMember)); } } /** * Fires * <tt>CallPeerConferenceEvent#CONFERENCE_MEMBER_ERROR_RECEIVED</tt> to * the currently registered <tt>CallPeerConferenceListener</tt>s. * * @param errorMessage error message that can be displayed. */ public void fireConferenceMemberErrorEvent(String errorMessage) { if(errorMessage == null || errorMessage.length() == 0) { logger.warn("The error message for " + this.getDisplayName() + " null or empty string."); return; } fireCallPeerConferenceEvent( new CallPeerConferenceEvent( this, CallPeerConferenceEvent .CONFERENCE_MEMBER_ERROR_RECEIVED, null, errorMessage)); } /** * Finds the first <tt>ConferenceMember</tt> whose <tt>audioSsrc</tt> is * equals to a specific value. The method is meant for very frequent use so * it iterates over the <tt>List</tt> of <tt>ConferenceMember</tt>s without * creating an <tt>Iterator</tt>. * * @param ssrc the SSRC identifier of the audio RTP streams transmitted by * the <tt>ConferenceMember</tt> that we are looking for. * @return the first <tt>ConferenceMember</tt> whose <tt>audioSsrc</tt> is * equal to <tt>ssrc</tt> or <tt>null</tt> if no such * <tt>ConferenceMember</tt> was found */ protected ConferenceMember findConferenceMember(long ssrc) { List<ConferenceMember> 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 <tt>CallPeerChangeEvent</tt> using this call peer as source, * setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>, * * @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 <tt>CallPeerChangeEvent</tt> using this call peer as source, * setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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 <tt>CallPeerChangeEvent</tt> using this call peer as source, * setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerListener> listeners = null; synchronized (callPeerListeners) { listeners = new ArrayList<CallPeerListener>( callPeerListeners).iterator(); } while (listeners.hasNext()) { CallPeerListener listener = listeners.next(); if (logger.isDebugEnabled()) logger.debug("Dispatching a CallPeerChangeEvent event to " + listener.getClass() +" . event is: " + evt.toString()); // 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 <tt>CallPeerConferenceEvent</tt> to the * <tt>CallPeerConferenceListener</tt>s interested in changes in the * conference-related information provided by this peer. * * @param conferenceEvent a <tt>CallPeerConferenceEvent</tt> 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; case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED: eventIDString = "CONFERENCE_MEMBER_ERROR_RECEIVED"; 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; case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED: listener.conferenceMemberErrorReceived(conferenceEvent); break; } } /** * Constructs a <tt>CallPeerSecurityStatusEvent</tt> using this call peer as * source, setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerSecurityListener> listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList<CallPeerSecurityListener>( callPeerSecurityListeners).iterator(); } while (listeners.hasNext()) { CallPeerSecurityListener listener = listeners.next(); listener.securityMessageRecieved(evt); } } /** * Constructs a <tt>CallPeerSecurityStatusEvent</tt> using this call peer as * source, setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerSecurityListener> listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList<CallPeerSecurityListener>( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityNegotiationStarted(evt); } } /** * Constructs a <tt>CallPeerSecurityStatusEvent</tt> using this call peer as * source, setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerSecurityListener> listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList<CallPeerSecurityListener>( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityOff(evt); } } /** * Constructs a <tt>CallPeerSecurityStatusEvent</tt> using this call peer as * source, setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerSecurityListener> listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList<CallPeerSecurityListener>( callPeerSecurityListeners); } for(CallPeerSecurityListener listener : listeners) { listener.securityOn(evt); } } /** * Constructs a <tt>CallPeerSecurityStatusEvent</tt> using this call peer as * source, setting it to be of type <tt>eventType</tt> and the corresponding * <tt>oldValue</tt> and <tt>newValue</tt>. * * @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<CallPeerSecurityListener> listeners = null; synchronized (callPeerSecurityListeners) { listeners = new ArrayList<CallPeerSecurityListener>( 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 <tt>CallPeer</tt> transitioned into a state * (likely {@link CallPeerState#CONNECTED}) marking the start of the * duration of the participation in a <tt>Call</tt>. * * @return the time at which this <tt>CallPeer</tt> transitioned into a * state marking the start of the duration of the participation in a * <tt>Call</tt> 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 <tt>null</tt> 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 <tt>null</tt> 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<ConferenceMember> getConferenceMembers() { synchronized (conferenceMembersSyncRoot) { return unmodifiableConferenceMembers; } } /** * Returns the currently used security settings of this <tt>CallPeer</tt>. * * @return the <tt>CallPeerSecurityStatusEvent</tt> 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 <tt>true</tt> if this peer is a conference focus and * <tt>false</tt> otherwise. */ public boolean isConferenceFocus() { return conferenceFocus; } /** * Determines whether the audio stream (if any) being sent to this peer is * mute. * <p> * The default implementation returns <tt>false</tt>. * </p> * * @return <tt>true</tt> if an audio stream is being sent to this peer and * it is currently mute; <tt>false</tt>, otherwise */ public boolean isMute() { return isMute; } /** * Implements * <tt>CallPeer#removeCallPeerConferenceListener( * CallPeerConferenceListener)</tt>. * * @param listener the <tt>CallPeerConferenceListener</tt> 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 <tt>ConferenceMember</tt> from the list of * <tt>ConferenceMember</tt>s reported by this peer through * {@link #getConferenceMembers()} and {@link #getConferenceMemberCount()} * if it is contained and fires * <tt>CallPeerConferenceEvent#CONFERENCE_MEMBER_REMOVED</tt> to * the currently registered <tt>CallPeerConferenceListener</tt>s. * * @param conferenceMember a <tt>ConferenceMember</tt> to be removed from * the list of <tt>ConferenceMember</tt> reported by this peer. If the * specified <tt>ConferenceMember</tt> 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<ConferenceMember> newConferenceMembers = new ArrayList<ConferenceMember>(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 <tt>true</tt> if this peer is to become a * conference focus and <tt>false</tt> 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 * <br/> * 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(); } }