diff --git a/resources/images/images.properties b/resources/images/images.properties index 21b01a6b4..2ea676087 100644 --- a/resources/images/images.properties +++ b/resources/images/images.properties @@ -56,6 +56,7 @@ TEXT_BOLD_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textBoldRollover.png TEXT_ITALIC_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textItalicRollover.png TEXT_UNDERLINED_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textUnderlinedRollover.png DIAL_BUTTON=resources/images/impl/gui/buttons/dialButton.png +HOLD_BUTTON=resources/images/impl/gui/buttons/holdButton.png INVITE_DIALOG_ICON=resources/images/impl/gui/common/inviteDialogIcon.png SEND_SMS_ICON=resources/images/impl/gui/common/gsm.png DIAL_BUTTON_BG=resources/images/impl/gui/buttons/dialButtonBg.png diff --git a/resources/images/impl/gui/buttons/holdButton.png b/resources/images/impl/gui/buttons/holdButton.png new file mode 100644 index 000000000..1e03ef156 Binary files /dev/null and b/resources/images/impl/gui/buttons/holdButton.png differ diff --git a/src/net/java/sip/communicator/impl/callhistory/CallHistoryServiceImpl.java b/src/net/java/sip/communicator/impl/callhistory/CallHistoryServiceImpl.java index 22b06b856..aed3ee75d 100644 --- a/src/net/java/sip/communicator/impl/callhistory/CallHistoryServiceImpl.java +++ b/src/net/java/sip/communicator/impl/callhistory/CallHistoryServiceImpl.java @@ -27,6 +27,7 @@ * (i.e. those that implement OperationSetBasicTelephony). * * @author Damian Minkov + * @author Lubomir Marinov */ public class CallHistoryServiceImpl implements CallHistoryService, @@ -448,8 +449,12 @@ else if(state.equals(CallParticipantState._ALERTING_REMOTE_SIDE)) return CallParticipantState.ALERTING_REMOTE_SIDE; else if(state.equals(CallParticipantState._CONNECTING)) return CallParticipantState.CONNECTING; - else if(state.equals(CallParticipantState._ON_HOLD)) - return CallParticipantState.ON_HOLD; + else if(state.equals(CallParticipantState._ON_HOLD_LOCALLY)) + return CallParticipantState.ON_HOLD_LOCALLY; + else if(state.equals(CallParticipantState._ON_HOLD_MUTUALLY)) + return CallParticipantState.ON_HOLD_MUTUALLY; + else if(state.equals(CallParticipantState._ON_HOLD_REMOTELY)) + return CallParticipantState.ON_HOLD_REMOTELY; else if(state.equals(CallParticipantState._INITIATING_CALL)) return CallParticipantState.INITIATING_CALL; else if(state.equals(CallParticipantState._INCOMING_CALL)) @@ -852,7 +857,7 @@ private void handleParticipantAdded(CallParticipant callParticipant) if(callRecord == null) return; - callParticipant.addCallParticipantListener(new CallParticipantListener() + callParticipant.addCallParticipantListener(new CallParticipantAdapter() { public void participantStateChanged(CallParticipantChangeEvent evt) { @@ -866,27 +871,21 @@ public void participantStateChanged(CallParticipantChangeEvent evt) if(participantRecord == null) return; - if(evt.getNewValue().equals(CallParticipantState.CONNECTED)) + CallParticipantState newState = + (CallParticipantState) evt.getNewValue(); + + if (newState.equals(CallParticipantState.CONNECTED) + && !CallParticipantState.isOnHold((CallParticipantState) + evt.getOldValue())) participantRecord.setStartTime(new Date()); - participantRecord. - setState((CallParticipantState)evt.getNewValue()); + participantRecord.setState(newState); //Disconnected / Busy //Disconnected / Connecting - fail //Disconnected / Connected } } - - public void participantDisplayNameChanged( - CallParticipantChangeEvent evt){} - public void participantAddressChanged( - CallParticipantChangeEvent evt){} - public void participantImageChanged( - CallParticipantChangeEvent evt){} - - public void participantTransportAddressChanged( - CallParticipantChangeEvent evt){} }); Date startDate = new Date(); @@ -919,7 +918,10 @@ private void handleParticipantRemoved(CallParticipant callParticipant, if(!callParticipant.getState().equals(CallParticipantState.DISCONNECTED)) cpRecord.setState(callParticipant.getState()); - if(cpRecord.getState().equals(CallParticipantState.CONNECTED)) + CallParticipantState cpRecordState = cpRecord.getState(); + + if (cpRecordState.equals(CallParticipantState.CONNECTED) + || CallParticipantState.isOnHold(cpRecordState)) { cpRecord.setEndTime(new Date()); } diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/CallPanel.java index 9cb2adf33..d7f770e38 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallPanel.java @@ -13,7 +13,6 @@ import javax.swing.*; import javax.swing.Timer; -import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.impl.gui.utils.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; @@ -24,6 +23,7 @@ * shows information about call participants, call duration, etc. * * @author Yana Stamcheva + * @author Lubomir Marinov */ public class CallPanel extends JScrollPane @@ -295,42 +295,50 @@ public void participantStateChanged(CallParticipantChangeEvent evt) participantPanel.setState( sourceParticipant.getState().getStateString()); - if(evt.getNewValue() == CallParticipantState.ALERTING_REMOTE_SIDE) + Object newState = evt.getNewValue(); + + if(newState == CallParticipantState.ALERTING_REMOTE_SIDE) { NotificationManager .fireNotification(NotificationManager.OUTGOING_CALL); } - else if(evt.getNewValue() == CallParticipantState.BUSY) + else if(newState == CallParticipantState.BUSY) { NotificationManager.stopSound(NotificationManager.OUTGOING_CALL); NotificationManager.fireNotification(NotificationManager.BUSY_CALL); } - else if(evt.getNewValue() == CallParticipantState.CONNECTED) { - //start the timer that takes care of refreshing the time label - - NotificationManager.stopSound(NotificationManager.OUTGOING_CALL); - NotificationManager.stopSound(NotificationManager.INCOMING_CALL); - - participantPanel.startCallTimer(); + else if(newState == CallParticipantState.CONNECTED) { + if (!CallParticipantState.isOnHold((CallParticipantState) + evt.getOldValue())) + { + // start the timer that takes care of refreshing the time label + + NotificationManager + .stopSound(NotificationManager.OUTGOING_CALL); + NotificationManager + .stopSound(NotificationManager.INCOMING_CALL); + + participantPanel.startCallTimer(); + } } - else if(evt.getNewValue() == CallParticipantState.CONNECTING) { + else if(newState == CallParticipantState.CONNECTING) { } - else if(evt.getNewValue() == CallParticipantState.DISCONNECTED) { + else if(newState == CallParticipantState.DISCONNECTED) { //The call participant should be already removed from the call //see callParticipantRemoved } - else if(evt.getNewValue() == CallParticipantState.FAILED) { + else if(newState == CallParticipantState.FAILED) { //The call participant should be already removed from the call //see callParticipantRemoved } - else if(evt.getNewValue() == CallParticipantState.INCOMING_CALL) { + else if(newState == CallParticipantState.INCOMING_CALL) { } - else if(evt.getNewValue() == CallParticipantState.INITIATING_CALL) { + else if(newState == CallParticipantState.INITIATING_CALL) { } - else if(evt.getNewValue() == CallParticipantState.ON_HOLD) { + else if(CallParticipantState.isOnHold((CallParticipantState) newState)) { } - else if(evt.getNewValue() == CallParticipantState.UNKNOWN) { + else if(newState == CallParticipantState.UNKNOWN) { } } diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallParticipantPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/CallParticipantPanel.java index ff9e8544e..0f50f2105 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallParticipantPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallParticipantPanel.java @@ -23,6 +23,7 @@ * name, photo, call duration, etc. * * @author Yana Stamcheva + * @author Lubomir Marinov */ public class CallParticipantPanel extends JPanel @@ -77,13 +78,17 @@ public CallParticipantPanel(CallManager callManager, this.callParticipant = callParticipant; this.stateLabel.setText(callParticipant.getState().getStateString()); + + Component holdButton = new HoldButton(this.callParticipant); + holdButton.setBounds(0, 74, 36, 36); + contactPanel.add(holdButton, new Integer(1)); dialButton = new DialButton(callManager, new ImageIcon(ImageLoader.getImage(ImageLoader.DIAL_BUTTON))); dialButton.setBounds(94, 74, 36, 36); - contactPanel.add(dialButton, new Integer(1)); + contactPanel.add(dialButton, new Integer(2)); } /** diff --git a/src/net/java/sip/communicator/impl/gui/main/call/HoldButton.java b/src/net/java/sip/communicator/impl/gui/main/call/HoldButton.java new file mode 100644 index 000000000..28b97dde0 --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/call/HoldButton.java @@ -0,0 +1,184 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.gui.main.call; + +import java.awt.event.*; + +import javax.swing.*; + +import net.java.sip.communicator.impl.gui.utils.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; + +/** + * Represents an UI means to put an associated CallPariticant on/off + * hold. + * + * @author Lubomir Marinov + */ +public class HoldButton + extends JToggleButton +{ + + /** + * Initializes a new HoldButton instance which is to put a specific + * CallParticipant on/off hold. + * + * @param callParticipant the CallParticipant to be associated with + * the new instance and to be put on/off hold upon performing its + * action + */ + public HoldButton(CallParticipant callParticipant) + { + super(new ImageIcon(ImageLoader.getImage(ImageLoader.HOLD_BUTTON))); + + setModel(new HoldButtonModel(callParticipant)); + } + + /** + * Represents the model of a toggle button that puts an associated + * CallParticipant on/off hold. + */ + private static class HoldButtonModel + extends ToggleButtonModel + { + + /** + * The CallParticipant whose state is being adapted for the + * purposes of depicting as a toggle button. + */ + private final CallParticipant callParticipant; + + /** + * Initializes a new HoldButtonModel instance to represent + * the state of a specific CallParticipant as a toggle + * button. + * + * @param callParticipant + * the CallParticipant whose state is to be + * represented as a toggle button + */ + public HoldButtonModel(CallParticipant callParticipant) + { + this.callParticipant = callParticipant; + + InternalListener listener = new InternalListener(); + this.callParticipant.addCallParticipantListener(listener); + addActionListener(listener); + } + + /** + * Handles actions performed on this model on behalf of a specific + * ActionListener. + * + * @param listener the ActionListener notified about the + * performing of the action + * @param evt the ActionEvent containing the data associated + * with the action and the act of its performing + */ + private void actionPerformed(ActionListener listener, ActionEvent evt) + { + Call call = callParticipant.getCall(); + + if (call != null) + { + OperationSetBasicTelephony telephony = + (OperationSetBasicTelephony) call.getProtocolProvider() + .getOperationSet(OperationSetBasicTelephony.class); + + try + { + if (isSelected()) + telephony.putOffHold(callParticipant); + else + telephony.putOnHold(callParticipant); + } + catch (OperationFailedException ex) + { + // TODO Auto-generated method stub + } + } + } + + /** + * Determines whether this model represents a state which should be + * visualized by the currently depicting toggle button as selected. + */ + public boolean isSelected() + { + CallParticipantState state = callParticipant.getState(); + return CallParticipantState.ON_HOLD_LOCALLY.equals(state) + || CallParticipantState.ON_HOLD_MUTUALLY.equals(state); + } + + /** + * Handles changes in the state of the source CallParticipant + * on behalf of a specific CallParticipantListener. + * + * @param listener the CallParticipantListener notified about + * the state change + * @param evt the CallParticipantChangeEvent containing the + * source event as well as its previous and its new state + */ + private void participantStateChanged(CallParticipantListener listener, + CallParticipantChangeEvent evt) + { + CallParticipantState newState = + (CallParticipantState) evt.getNewValue(); + CallParticipant callParticipant = evt.getSourceCallParticipant(); + + fireItemStateChanged(new ItemEvent(this, + ItemEvent.ITEM_STATE_CHANGED, this, + isSelected() ? ItemEvent.SELECTED : ItemEvent.DESELECTED)); + fireStateChanged(); + + /* + * For the sake of completeness, try to not leave a listener on the + * CallParticipant after it's no longer of interest. + */ + if (CallParticipantState.DISCONNECTED.equals(newState)) + { + callParticipant.removeCallParticipantListener(listener); + } + } + + /** + * Represents the set of EventListeners this instance uses + * to track the changes in its CallParticipant model. + */ + private class InternalListener + extends CallParticipantAdapter + implements ActionListener + { + + /** + * Invoked when an action occurs. + * + * @param evt the ActionEvent instance containing the data + * associated with the action and the act of its + * performing + */ + public void actionPerformed(ActionEvent evt) + { + HoldButtonModel.this.actionPerformed(this, evt); + } + + /** + * Indicates that a change has occurred in the state of the source + * CallParticipant. + * + * @param evt The CallParticipantChangeEvent instance + * containing the source event as well as its previous + * and its new status. + */ + public void participantStateChanged(CallParticipantChangeEvent evt) + { + HoldButtonModel.this.participantStateChanged(this, evt); + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java b/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java index d68b1a5b5..ee406c52f 100644 --- a/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java +++ b/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java @@ -23,6 +23,7 @@ * Stores and loads images used throughout this ui implementation. * * @author Yana Stamcheva + * @author Lubomir Marinov */ public class ImageLoader { @@ -395,6 +396,13 @@ public class ImageLoader { public static final ImageID DIAL_BUTTON = new ImageID("DIAL_BUTTON"); + /** + * A put-on/off-hold button icon. The icon shown in the CallParticipant + * panel. + */ + public static final ImageID HOLD_BUTTON + = new ImageID("HOLD_BUTTON"); + /** * The image used, when a contact has no photo specified. */ diff --git a/src/net/java/sip/communicator/impl/gui/utils/images.properties b/src/net/java/sip/communicator/impl/gui/utils/images.properties index a79f4d941..dee63341d 100644 --- a/src/net/java/sip/communicator/impl/gui/utils/images.properties +++ b/src/net/java/sip/communicator/impl/gui/utils/images.properties @@ -57,6 +57,7 @@ TEXT_BOLD_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textBoldRollover.png TEXT_ITALIC_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textItalicRollover.png TEXT_UNDERLINED_ROLLOVER_BUTTON=resources/images/impl/gui/buttons/textUnderlinedRollover.png DIAL_BUTTON=resources/images/impl/gui/buttons/dialButton.png +HOLD_BUTTON=resources/images/impl/gui/buttons/holdButton.png INVITE_DIALOG_ICON=resources/images/impl/gui/common/inviteDialogIcon.png SEND_SMS_ICON=resources/images/impl/gui/common/gsm.png DIAL_BUTTON_BG=resources/images/impl/gui/buttons/dialButtonBg.png diff --git a/src/net/java/sip/communicator/impl/media/CallSessionImpl.java b/src/net/java/sip/communicator/impl/media/CallSessionImpl.java index d5deaf412..eb3924318 100644 --- a/src/net/java/sip/communicator/impl/media/CallSessionImpl.java +++ b/src/net/java/sip/communicator/impl/media/CallSessionImpl.java @@ -62,6 +62,7 @@ * @author Ryan Ricard * @author Ken Larson * @author Dudek Przemyslaw + * @author Lubomir Marinov */ public class CallSessionImpl implements CallSession @@ -159,6 +160,24 @@ public class CallSessionImpl */ private URL callURL = null; + /** + * The flag which signals that this side of the call has put the other on + * hold. + */ + private static final byte ON_HOLD_LOCALLY = 1 << 1; + + /** + * The flag which signals that the other side of the call has put this on + * hold. + */ + private static final byte ON_HOLD_REMOTELY = 1 << 2; + + /** + * The flags which determine whether this side of the call has put the other + * on hold and whether the other side of the call has put this on hold. + */ + private byte onHold; + /** * List of RTP format strings which are supported by SIP Communicator in addition * to the JMF standard formats. @@ -519,6 +538,318 @@ public String createSdpOffer(InetAddress intendedDestination) return createSessionDescription(null, intendedDestination).toString(); } + /** + * The method is meant for use by protocol service implementations when + * willing to send an in-dialog invitation to a remote callee to put her + * on/off hold or to send an answer to an offer to be put on/off hold. + * + * @param participantSdpDescription the last SDP description of the remote + * callee + * @param on true if the SDP description should offer the remote + * callee to be put on hold or answer an offer from the remote + * callee to be put on hold; false to work in the + * context of a put-off-hold offer + * @return an SDP description String which offers the remote + * callee to be put her on/off hold or answers an offer from the + * remote callee to be put on/off hold + * @throws MediaException + */ + public String createSdpDescriptionForHold(String participantSdpDescription, + boolean on) throws MediaException + { + SessionDescription participantDescription = null; + System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!+"+participantSdpDescription); + try + { + participantDescription = + mediaServCallback.getSdpFactory().createSessionDescription( + participantSdpDescription); + } + catch (SdpParseException ex) + { + throwMediaException( + "Failed to parse the SDP description of the participant.", + MediaException.INTERNAL_ERROR, ex); + } + + SessionDescription sdpOffer = + createSessionDescription(participantDescription, null); + + Vector mediaDescriptions = null; + try + { + mediaDescriptions = sdpOffer.getMediaDescriptions(true); + } + catch (SdpException ex) + { + throwMediaException( + "Failed to get media descriptions from SDP offer.", + MediaException.INTERNAL_ERROR, ex); + } + + for (Iterator mediaDescriptionIter = mediaDescriptions.iterator(); mediaDescriptionIter + .hasNext();) + { + MediaDescription mediaDescription = + (MediaDescription) mediaDescriptionIter.next(); + Vector attributes = mediaDescription.getAttributes(false); + + try + { + modifyMediaDescriptionForHold(on, mediaDescription, attributes); + } + catch (SdpException ex) + + { + throwMediaException( + "Failed to modify media description for hold.", + MediaException.INTERNAL_ERROR, ex); + } + } + + try + { + sdpOffer.setMediaDescriptions(mediaDescriptions); + } + catch (SdpException ex) + { + throwMediaException( + "Failed to set media descriptions to SDP offer.", + MediaException.INTERNAL_ERROR, ex); + } + + return sdpOffer.toString(); + } + + /** + * Modifies the attributes of a specific MediaDescription in + * order to make them reflect the state of being on/off hold. + * + * @param on true if the state described by the modified + * MediaDescription should reflect being put on hold; + * false for being put off hold + * @param mediaDescription the MediaDescription to modify the + * attributes of + * @param attributes the attributes of mediaDescription + * @throws SdpException + */ + private void modifyMediaDescriptionForHold(boolean on, + MediaDescription mediaDescription, Vector attributes) + throws SdpException + { + + /* + * The SDP offer to be put on hold represents a transition between + * sendrecv and sendonly or between recvonly and inactive depending on + * the current state. + */ + String oldAttribute = on ? "recvonly" : "inactive"; + String newAttribute = null; + if (attributes != null) + for (Iterator attributeIter = attributes.iterator(); attributeIter + .hasNext();) + { + String attribute = ((Attribute) attributeIter.next()).getName(); + + if (oldAttribute.equalsIgnoreCase(attribute)) + newAttribute = on ? "inactive" : "recvonly"; + } + if (newAttribute == null) + newAttribute = on ? "sendonly" : "sendrecv"; + + mediaDescription.removeAttribute("inactive"); + mediaDescription.removeAttribute("recvonly"); + mediaDescription.removeAttribute("sendonly"); + mediaDescription.removeAttribute("sendrecv"); + mediaDescription.setAttribute(newAttribute, null); + } + + /** + * Logs a specific message and associated Throwable cause as an + * error using the current Logger and then throws a new + * MediaException with the message, a specific error code and the + * cause. + * + * @param message the message to be logged and then wrapped in a new + * MediaException + * @param errorCode the error code to be assigned to the new + * MediaException + * @param cause the Throwable that has caused the necessity to + * log an error and have a new MediaException thrown + * @throws MediaException + */ + private void throwMediaException(String message, int errorCode, + Throwable cause) throws MediaException + { + logger.error(message, cause); + throw new MediaException(message, errorCode, cause); + } + + /** + * Determines whether a specific SDP description String offers + * this party to be put on hold. + * + * @param sdpOffer the SDP description String to be examined for + * an offer to this party to be put on hold + * @return true if the specified SDP description String + * offers this party to be put on hold; false, otherwise + * @throws MediaException + */ + public boolean isSdpOfferToHold(String sdpOffer) throws MediaException + { + SessionDescription description = null; + try + { + description = + mediaServCallback.getSdpFactory().createSessionDescription( + sdpOffer); + } + catch (SdpParseException ex) + { + throwMediaException("Failed to parse SDP offer.", + MediaException.INTERNAL_ERROR, ex); + } + + Vector mediaDescriptions = null; + try + { + mediaDescriptions = description.getMediaDescriptions(true); + } + catch (SdpException ex) + { + throwMediaException( + "Failed to get media descriptions from SDP offer.", + MediaException.INTERNAL_ERROR, ex); + } + + boolean isOfferToHold = true; + for (Iterator mediaDescriptionIter = mediaDescriptions.iterator(); mediaDescriptionIter + .hasNext() + && isOfferToHold;) + { + MediaDescription mediaDescription = + (MediaDescription) mediaDescriptionIter.next(); + Vector attributes = mediaDescription.getAttributes(false); + + isOfferToHold = false; + if (attributes != null) + { + for (Iterator attributeIter = attributes.iterator(); attributeIter + .hasNext() + && !isOfferToHold;) + { + try + { + String attribute = + ((Attribute) attributeIter.next()).getName(); + + if ("sendonly".equalsIgnoreCase(attribute) + || "inactive".equalsIgnoreCase(attribute)) + { + isOfferToHold = true; + } + } + catch (SdpParseException ex) + { + throwMediaException( + "Failed to get SDP media description attribute name", + MediaException.INTERNAL_ERROR, ex); + } + } + } + } + return isOfferToHold; + } + + /** + * Puts the media of this CallSession on/off hold depending on + * the origin of the request. + *
+ * For example, a remote request to have this party put off hold cannot + * override an earlier local request to put the remote party on hold. + *
+ * + * @param on true to request the media of this + * CallSession be put on hold; false, + * otherwise + * @param here true if the request comes from this side of the + * call; false if the remote party is the issuer of + * the request i.e. it's the result of a remote offer + */ + public void putOnHold(boolean on, boolean here) + { + if (on) + { + onHold |= (here ? ON_HOLD_LOCALLY : ON_HOLD_REMOTELY); + } + else + { + onHold &= ~ (here ? ON_HOLD_LOCALLY : ON_HOLD_REMOTELY); + } + + /* Put the send on/off hold. */ + boolean sendOnHold = + (0 != (onHold & (ON_HOLD_LOCALLY | ON_HOLD_REMOTELY))); + putOnHold(getAudioRtpManager(), sendOnHold); + putOnHold(getVideoRtpManager(), sendOnHold); + + /* Put the receive on/off hold. */ + boolean receiveOnHold = (0 != (onHold & ON_HOLD_LOCALLY)); + for (Iterator playerIter = players.iterator(); playerIter.hasNext();) + { + Player player = (Player) playerIter.next(); + + if (receiveOnHold) + player.stop(); + else + player.start(); + } + } + + /** + * Puts a the SendSteams of a specific RTPManager + * on/off hold i.e. stops/starts them. + * + * @param rtpManager the RTPManager to have its + * SendStreams on/off hold i.e. stopped/started + * @param on true to have the SendStreams of + * rtpManager put on hold i.e. stopped; false, + * otherwise + */ + private void putOnHold(RTPManager rtpManager, boolean on) + { + for (Iterator sendStreamIter = rtpManager.getSendStreams().iterator(); sendStreamIter + .hasNext();) + { + SendStream sendStream = (SendStream) sendStreamIter.next(); + + if (on) + { + try + { + sendStream.getDataSource().stop(); + sendStream.stop(); + } + catch (IOException ex) + { + logger.warn("Failed to stop SendStream.", ex); + } + } + else + { + try + { + sendStream.getDataSource().start(); + sendStream.start(); + } + catch (IOException ex) + { + logger.warn("Failed to start SendStream.", ex); + } + } + } + } + /** * The method is meant for use by protocol service implementations upon * reception of an SDP answer in response to an offer sent by us earlier. @@ -916,7 +1247,15 @@ private SessionDescription createSessionDescription( } } - allocateMediaPorts(intendedDestination); + /* + * For example, issuing a Request.INVITE for putting a + * CallParticipant on hold also needs a SessionDescrption. However, + * it just wants to describe the current state. + */ + if ((audioSessionAddress == null) || (videoSessionAddress == null)) + { + allocateMediaPorts(intendedDestination); + } InetAddress publicIpAddress = audioPublicAddress.getAddress(); @@ -1087,12 +1426,15 @@ private Vector createMediaDescriptions( , 1 , "RTP/AVP" , supportedAudioEncodings); + byte onHold = this.onHold; if (!mediaServCallback.getDeviceConfiguration() .isAudioCaptureSupported()) { - am.setAttribute("recvonly", null); + /* We don't have anything to send. */ + onHold |= ON_HOLD_REMOTELY; } + setAttributeOnHold(am, onHold); mediaDescs.add(am); } //--------Video media description @@ -1106,12 +1448,15 @@ private Vector createMediaDescriptions( , 1 , "RTP/AVP" , supportedVideoEncodings); + byte onHold = this.onHold; if (!mediaServCallback.getDeviceConfiguration() .isVideoCaptureSupported()) { - vm.setAttribute("recvonly", null); + /* We don't have anything to send. */ + onHold |= ON_HOLD_REMOTELY; } + setAttributeOnHold(vm, onHold); mediaDescs.add(vm); } @@ -1123,6 +1468,36 @@ private Vector createMediaDescriptions( return mediaDescs; } + /** + * Sets the call-hold related attribute of a specific + * MediaDescription to a specific value depending on the type of + * hold this CallSession is currently in. + * + * @param mediaDescription the MediaDescription to set the + * call-hold related attribute of + * @param onHold the call-hold state of this CallSession which is + * a combination of {@link #ON_HOLD_LOCALLY} and + * {@link #ON_HOLD_REMOTELY} + * @throws SdpException + */ + private void setAttributeOnHold(MediaDescription mediaDescription, + byte onHold) throws SdpException + { + String attribute; + + if (ON_HOLD_LOCALLY == (onHold | ON_HOLD_LOCALLY)) + attribute = + (ON_HOLD_REMOTELY == (onHold | ON_HOLD_REMOTELY)) ? "inactive" + : "sendonly"; + else + attribute = + (ON_HOLD_REMOTELY == (onHold | ON_HOLD_REMOTELY)) ? "recvonly" + : null; + + if (attribute != null) + mediaDescription.setAttribute(attribute, null); + } + /** * Compares audio/video encodings in the offeredEncodings * hashtable with those supported by the currently valid media controller diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java index 59d9108dd..9dd24ba27 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetBasicTelephonySipImpl.java @@ -24,6 +24,7 @@ * implementing OperationSetBasicTelephony. * * @author Emil Ivov + * @author Lubomir Marinov */ public class OperationSetBasicTelephonySipImpl implements OperationSetBasicTelephony @@ -355,22 +356,182 @@ public Iterator getActiveCalls() /** * Resumes communication with a call participant previously put on hold. - * + * * @param participant the call participant to put on hold. + * @throws OperationFailedException */ public synchronized void putOffHold(CallParticipant participant) + throws OperationFailedException { - /** @todo implement putOffHold() */ + putOnHold(participant, false); } /** * Puts the specified CallParticipant "on hold". - * + * * @param participant the participant that we'd like to put on hold. + * @throws OperationFailedException */ public synchronized void putOnHold(CallParticipant participant) + throws OperationFailedException + { + putOnHold(participant, true); + } + + /** + * Puts the specified CallParticipant on or off hold. + * + * @param participant the CallParticipant to be put on or off hold + * @param on true to have the specified CallParticipant + * put on hold; false, otherwise + * @throws OperationFailedException + */ + private void putOnHold(CallParticipant participant, boolean on) + throws OperationFailedException + { + CallSession callSession = + ((CallSipImpl) participant.getCall()).getMediaCallSession(); + CallParticipantSipImpl sipParticipant = + (CallParticipantSipImpl) participant; + + try + { + sendInviteRequest(sipParticipant, callSession + .createSdpDescriptionForHold( + sipParticipant.getSdpDescription(), on)); + } + catch (MediaException ex) + { + throwOperationFailedException( + "Failed to create SDP offer to hold.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + /* + * Putting on hold isn't a negotiation (i.e. the issuing side takes the + * decision and executes it) so we're muting now regardless of the + * desire of the participant to accept the offer. + */ + callSession.putOnHold(on, true); + + CallParticipantState state = sipParticipant.getState(); + if (CallParticipantState.ON_HOLD_LOCALLY.equals(state)) + { + if (!on) + sipParticipant.setState(CallParticipantState.CONNECTED); + } + else if (CallParticipantState.ON_HOLD_MUTUALLY.equals(state)) + { + if (!on) + sipParticipant.setState(CallParticipantState.ON_HOLD_REMOTELY); + } + else if (CallParticipantState.ON_HOLD_REMOTELY.equals(state)) + { + if (on) + sipParticipant.setState(CallParticipantState.ON_HOLD_MUTUALLY); + } + else if (on) + { + sipParticipant.setState(CallParticipantState.ON_HOLD_LOCALLY); + } + } + + /** + * Sends an invite request with a specific SDP offer (description) within + * the current Dialog with a specific call participant. + * + * @param sipParticipant the SIP-specific call participant to send the + * invite to within the current Dialog + * @param sdpOffer the description of the SDP offer to be made to the + * specified call participant with the sent invite + * @throws OperationFailedException + */ + private void sendInviteRequest(CallParticipantSipImpl sipParticipant, + String sdpOffer) throws OperationFailedException + { + Dialog dialog = sipParticipant.getDialog(); + Request invite = null; + try + { + invite = dialog.createRequest(Request.INVITE); + } + catch (SipException ex) + { + throwOperationFailedException("Failed to create invite request.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + /* + * The authorization-related headers are the responsibility of the + * application (according to the Javadoc of JAIN-SIP). + */ + AuthorizationHeader authorization = + protocolProvider.getSipSecurityManager() + .getCachedAuthorizationHeader( + ((CallIdHeader) invite.getHeader(CallIdHeader.NAME)) + .getCallId()); + if (authorization != null) + { + invite.setHeader(authorization); + } + + try + { + invite.setContent(sdpOffer, protocolProvider.getHeaderFactory() + .createContentTypeHeader("application", "sdp")); + } + catch (ParseException ex) + { + throwOperationFailedException( + "Failed to parse SDP offer for the new invite.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + ClientTransaction clientTransaction = null; + try + { + clientTransaction = + sipParticipant.getJainSipProvider().getNewClientTransaction( + invite); + } + catch (TransactionUnavailableException ex) + { + throwOperationFailedException( + "Failed to create a client transaction for the new invite.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + try + { + dialog.sendRequest(clientTransaction); + } + catch (SipException ex) + { + throwOperationFailedException("Failed to send the new invite.", + OperationFailedException.NETWORK_FAILURE, ex); + } + } + + /** + * Logs a specific message and associated Throwable cause as an + * error using the current Logger and then throws a new + * OperationFailedException with the message, a specific error + * code and the cause. + * + * @param message the message to be logged and then wrapped in a new + * OperationFailedException + * @param errorCode the error code to be assigned to the new + * OperationFailedException + * @param cause the Throwable that has caused the necessity to + * log an error and have a new OperationFailedException + * thrown + * @throws OperationFailedException + */ + private void throwOperationFailedException(String message, int errorCode, + Throwable cause) throws OperationFailedException { - /** @todo implement putOnHold() */ + logger.error(message, cause); + throw new OperationFailedException(message, errorCode, cause); } /** @@ -432,16 +593,19 @@ public void processRequest(RequestEvent requestEvent) if (request.getMethod().equals(Request.INVITE)) { logger.debug("received INVITE"); - if (serverTransaction.getDialog().getState() == null) + DialogState dialogState = serverTransaction.getDialog().getState(); + if ((dialogState == null) + || dialogState.equals(DialogState.CONFIRMED)) { if (logger.isDebugEnabled()) logger.debug("request is an INVITE. Dialog state=" - + serverTransaction.getDialog().getState()); + + dialogState); processInvite(jainSipProvider, serverTransaction, request); } else { - logger.error("reINVITE-s are not currently supported."); + logger + .error("reINVITEs while the dialog is not confirmed are not currently supported."); } } //ACK @@ -572,8 +736,11 @@ private void processTrying(ClientTransaction clientTransaction, return; } - //change status - callParticipant.setState(CallParticipantState.CONNECTING); + // change status + CallParticipantState callParticipantState = callParticipant.getState(); + if (!CallParticipantState.CONNECTED.equals(callParticipantState) + && !CallParticipantState.isOnHold(callParticipantState)) + callParticipant.setState(CallParticipantState.CONNECTING); } /** @@ -677,15 +844,11 @@ private void processInviteOK(ClientTransaction clientTransaction, return; } } - - if (callParticipant.getState() == CallParticipantState.CONNECTED) - { - // This can happen if the OK UDP packet has been resent due to a - //timeout. (fix by Michael Koch) - logger.debug("Ignoring invite OK since call participant is " - +"already connected."); - return; - } + + /* + * Receiving an Invite OK is allowed even when the participant is + * already connected for the purposes of call hold. + */ Request ack = null; ContentTypeHeader contentTypeHeader = null; @@ -777,9 +940,13 @@ private void processInviteOK(ClientTransaction clientTransaction, return; } } - - callSession.processSdpAnswer(callParticipant - , callParticipant.getSdpDescription()); + CallParticipantState callParticipantState = callParticipant.getState(); + if ((callParticipantState != CallParticipantState.CONNECTED) + && !CallParticipantState.isOnHold(callParticipantState)) + { + callSession.processSdpAnswer(callParticipant, callParticipant + .getSdpDescription()); + } //set the call url in case there was one /** @todo this should be done in CallSession, once we move * it here.*/ @@ -812,8 +979,9 @@ private void processInviteOK(ClientTransaction clientTransaction, + exc.getMessage()); } - //change status - callParticipant.setState(CallParticipantState.CONNECTED); + // change status + if (!CallParticipantState.isOnHold(callParticipant.getState())) + callParticipant.setState(CallParticipantState.CONNECTED); } /** @@ -1166,12 +1334,21 @@ private void processInvite( SipProvider sourceProvider, ServerTransaction serverTransaction, Request invite) { - logger.trace("Creating call participant."); Dialog dialog = serverTransaction.getDialog(); - CallParticipantSipImpl callParticipant - = createCallParticipantFor(serverTransaction, sourceProvider); - logger.trace("call participant created = " + callParticipant); + CallParticipantSipImpl callParticipant = + activeCallsRepository.findCallParticipant(dialog); + int statusCode; + if (callParticipant == null) + { + statusCode = Response.RINGING; + logger.trace("Creating call participant."); + callParticipant = + createCallParticipantFor(serverTransaction, sourceProvider); + logger.trace("call participant created = " + callParticipant); + } + else + statusCode = Response.OK; //sdp description may be in acks - bug report Laurent Michel ContentLengthHeader cl = invite.getContentLength(); @@ -1241,21 +1418,38 @@ private void processInvite( SipProvider sourceProvider, } } - //Send RINGING - logger.debug("Invite seems ok, we'll say RINGING."); - Response ringing = null; + // Send statusCode + String statusCodeString = + (statusCode == Response.RINGING) ? "RINGING" : "OK"; + logger.debug("Invite seems ok, we'll say " + statusCodeString + "."); + Response response = null; try { - ringing = protocolProvider.getMessageFactory().createResponse( - Response.RINGING, invite); - protocolProvider.attachToTag(ringing, dialog); - ringing.setHeader(protocolProvider.getSipCommUserAgentHeader()); + response = protocolProvider.getMessageFactory().createResponse( + statusCode, invite); + protocolProvider.attachToTag(response, dialog); + response.setHeader(protocolProvider.getSipCommUserAgentHeader()); //set our display name - ((ToHeader)ringing.getHeader(ToHeader.NAME)) + ((ToHeader)response.getHeader(ToHeader.NAME)) .getAddress().setDisplayName(protocolProvider .getOurDisplayName()); - ringing.addHeader(protocolProvider.getContactHeader()); + response.addHeader(protocolProvider.getContactHeader()); + + if (statusCode != Response.RINGING) + { + try + { + processInviteSendingResponse(callParticipant, response); + } + catch (OperationFailedException ex) + { + logger.error("Error while trying to send a request", ex); + callParticipant.setState(CallParticipantState.FAILED, + "Internal Error: " + ex.getMessage()); + return; + } + } } catch (ParseException ex) { logger.error("Error while trying to send a request" @@ -1265,9 +1459,10 @@ private void processInvite( SipProvider sourceProvider, return; } try { - logger.trace("will send ringing response: "); - serverTransaction.sendResponse(ringing); - logger.debug("sent a ringing response: " + ringing); + logger.trace("will send " + statusCodeString + " response: "); + serverTransaction.sendResponse(response); + logger + .debug("sent a " + statusCodeString + " response: " + response); } catch (Exception ex) { logger.error("Error while trying to send a request" @@ -1277,6 +1472,127 @@ private void processInvite( SipProvider sourceProvider, , "Internal Error: " + ex.getMessage()); return; } + + if (statusCode != Response.RINGING) + { + try + { + processInviteSentResponse(callParticipant, response); + } + catch (OperationFailedException ex) + { + logger.error("Error after sending a request", ex); + } + } + } + + /** + * Provides a hook for this instance to take last configuration steps on a + * specific Response before it is sent to a specific + * CallParticipant as part of the execution of + * {@link #processInvite(SipProvider, ServerTransaction, Request)}. + * + * @param participant the CallParticipant to receive a specific + * Response + * @param response the Response to be sent to the + * participant + * @throws OperationFailedException + * @throws ParseException + */ + private void processInviteSendingResponse(CallParticipant participant, + Response response) throws OperationFailedException, ParseException + { + /* + * At the time of this writing, we're only getting called because a + * response to a call-hold invite is to be sent. + */ + + CallSession callSession = + ((CallSipImpl) participant.getCall()).getMediaCallSession(); + CallParticipantSipImpl sipParticipant = + (CallParticipantSipImpl) participant; + String sdpOffer = sipParticipant.getSdpDescription(); + + String sdpAnswer = null; + try + { + sdpAnswer = + callSession.createSdpDescriptionForHold(sdpOffer, callSession + .isSdpOfferToHold(sdpOffer)); + } + catch (MediaException ex) + { + throwOperationFailedException( + "Failed to create SDP answer to put-on/off-hold request.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + response.setContent(sdpAnswer, protocolProvider.getHeaderFactory() + .createContentTypeHeader("application", "sdp")); + } + + /** + * Provides a hook for this instance to take immediate steps after a + * specific Response has been sent to a specific + * CallParticipant as part of the execution of + * {@link #processInvite(SipProvider, ServerTransaction, Request)}. + * + * @param participant the CallParticipant who was sent a specific + * Response + * @param response the Response that has just been sent to the + * participant + * @throws OperationFailedException + * @throws ParseException + */ + private void processInviteSentResponse(CallParticipant participant, + Response response) throws OperationFailedException + { + /* + * At the time of this writing, we're only getting called because a + * response to a call-hold invite is to be sent. + */ + + CallSession callSession = + ((CallSipImpl) participant.getCall()).getMediaCallSession(); + CallParticipantSipImpl sipParticipant = + (CallParticipantSipImpl) participant; + + boolean on = false; + try + { + on = + callSession + .isSdpOfferToHold(sipParticipant.getSdpDescription()); + } + catch (MediaException ex) + { + throwOperationFailedException( + "Failed to create SDP answer to put-on/off-hold request.", + OperationFailedException.INTERNAL_ERROR, ex); + } + + callSession.putOnHold(on, false); + + CallParticipantState state = sipParticipant.getState(); + if (CallParticipantState.ON_HOLD_LOCALLY.equals(state)) + { + if (on) + sipParticipant.setState(CallParticipantState.ON_HOLD_MUTUALLY); + } + else if (CallParticipantState.ON_HOLD_MUTUALLY.equals(state)) + { + if (!on) + sipParticipant.setState(CallParticipantState.ON_HOLD_LOCALLY); + } + else if (CallParticipantState.ON_HOLD_REMOTELY.equals(state)) + { + if (!on) + sipParticipant.setState(CallParticipantState.CONNECTED); + } + else if (on) + { + sipParticipant.setState(CallParticipantState.ON_HOLD_REMOTELY); + } } /** @@ -1358,8 +1674,9 @@ void processAck(ServerTransaction serverTransaction, callParticipant.setSdpDescription( new String(ackRequest.getRawContent())); } - //change status - callParticipant.setState(CallParticipantState.CONNECTED); + // change status + if (!CallParticipantState.isOnHold(callParticipant.getState())) + callParticipant.setState(CallParticipantState.CONNECTED); } /** @@ -1470,16 +1787,16 @@ public synchronized void hangupCallParticipant(CallParticipant participant) CallParticipantSipImpl callParticipant = (CallParticipantSipImpl)participant; - Dialog dialog = callParticipant.getDialog(); - if (callParticipant.getState().equals(CallParticipantState.CONNECTED)) + CallParticipantState participantState = callParticipant.getState(); + if (participantState.equals(CallParticipantState.CONNECTED) + || CallParticipantState.isOnHold(participantState)) { sayBye(callParticipant); callParticipant.setState(CallParticipantState.DISCONNECTED); } - else if (callParticipant.getState() - .equals(CallParticipantState.CONNECTING) - || callParticipant.getState() - .equals(CallParticipantState.ALERTING_REMOTE_SIDE)) + else if (participantState.equals(CallParticipantState.CONNECTING) + || participantState + .equals(CallParticipantState.ALERTING_REMOTE_SIDE)) { if (callParticipant.getFirstTransaction() != null) { @@ -1489,18 +1806,17 @@ else if (callParticipant.getState() } callParticipant.setState(CallParticipantState.DISCONNECTED); } - else if (callParticipant.getState() - .equals(CallParticipantState.INCOMING_CALL)) + else if (participantState.equals(CallParticipantState.INCOMING_CALL)) { callParticipant.setState(CallParticipantState.DISCONNECTED); sayBusyHere(callParticipant); } //For FAILE and BUSY we only need to update CALL_STATUS - else if (callParticipant.getState().equals(CallParticipantState.BUSY)) + else if (participantState.equals(CallParticipantState.BUSY)) { callParticipant.setState(CallParticipantState.DISCONNECTED); } - else if (callParticipant.getState().equals(CallParticipantState.FAILED)) + else if (participantState.equals(CallParticipantState.FAILED)) { callParticipant.setState(CallParticipantState.DISCONNECTED); } @@ -1783,7 +2099,10 @@ public synchronized void answerCallParticipant(CallParticipant participant) , OperationFailedException.INTERNAL_ERROR); } - if(participant.getState().equals(CallParticipantState.CONNECTED)) + CallParticipantState participantState = participant.getState(); + + if (participantState.equals(CallParticipantState.CONNECTED) + || CallParticipantState.isOnHold(participantState)) { logger.info("Ignoring user request to answer a CallParticipant " + "that is already connected. CP:" + participant); diff --git a/src/net/java/sip/communicator/service/media/CallSession.java b/src/net/java/sip/communicator/service/media/CallSession.java index de7cac57b..b83bd889f 100644 --- a/src/net/java/sip/communicator/service/media/CallSession.java +++ b/src/net/java/sip/communicator/service/media/CallSession.java @@ -20,10 +20,11 @@ * single Call may only be associated one CallSession * instance. *- * A call session also allows signalling protocols to generate SDP offers and + * A call session also allows signaling protocols to generate SDP offers and * construct sdp answers. * * @author Emil Ivov + * @author Lubomir Marinov */ public interface CallSession { @@ -31,7 +32,7 @@ public interface CallSession * The method is meant for use by protocol service implementations when * willing to send an invitation to a remote callee. * - * @return a String containing an SDP offer descibing parameters of the + * @return a String containing an SDP offer describing parameters of the * Call associated with this session. * @throws MediaException code INTERNAL_ERROR if generating the offer fails * for some reason. @@ -56,6 +57,54 @@ public String createSdpOffer() public String createSdpOffer(InetAddress intendedDestination) throws MediaException; + /** + * The method is meant for use by protocol service implementations when + * willing to send an in-dialog invitation to a remote callee to put her + * on/off hold or to send an answer to an offer to be put on/off hold. + * + * @param participantSdpDescription the last SDP description of the remote + * callee + * @param on true if the SDP description should offer the remote + * callee to be put on hold or answer an offer from the remote + * callee to be put on hold; false to work in the + * context of a put-off-hold offer + * @return an SDP description String which offers the remote + * callee to be put her on/off hold or answers an offer from the + * remote callee to be put on/off hold + * @throws MediaException + */ + public String createSdpDescriptionForHold(String participantSdpDescription, + boolean on) throws MediaException; + + /** + * Determines whether a specific SDP description String offers + * this party to be put on hold. + * + * @param sdpOffer the SDP description String to be examined for + * an offer to this party to be put on hold + * @return true if the specified SDP description String + * offers this party to be put on hold; false, otherwise + * @throws MediaException + */ + public boolean isSdpOfferToHold(String sdpOffer) throws MediaException; + + /** + * Puts the media of this CallSession on/off hold depending on + * the origin of the request. + *
+ * For example, a remote request to have this party put off hold cannot + * override an earlier local request to put the remote party on hold. + *
+ * + * @param on true to request the media of this + * CallSession be put on hold; false, + * otherwise + * @param here true if the request comes from this side of the + * call; false if the remote party is the issuer of + * the request i.e. it's the result of a remote offer + */ + public void putOnHold(boolean on, boolean here); + /** * The method is meant for use by protocol service implementations when * willing to respond to an invitation received from a remote caller. Apart @@ -65,7 +114,7 @@ public String createSdpOffer(InetAddress intendedDestination) * @param sdpOffer the SDP offer that we'd like to create an answer for. * @param offerer the participant that has sent the offer. * - * @return a String containing an SDP answer descibing parameters of the + * @return a String containing an SDP answer describing parameters of the * Call associated with this session and matching those advertised * by the caller in their sdpOffer. * @@ -111,7 +160,7 @@ public void processSdpAnswer(CallParticipant responder, String sdpAnswer) public int getAudioPort(); /** - * Returns a URL pointing ta a location with call control information for + * Returns a URL pointing to a location with call control information for * this call or null if no such URL is available. * * @return a URL link to a location with call information or a call control diff --git a/src/net/java/sip/communicator/service/protocol/CallParticipantState.java b/src/net/java/sip/communicator/service/protocol/CallParticipantState.java index b63a34206..d749a7b99 100644 --- a/src/net/java/sip/communicator/service/protocol/CallParticipantState.java +++ b/src/net/java/sip/communicator/service/protocol/CallParticipantState.java @@ -27,12 +27,13 @@ *A FAILED state is prone to appear at any place in the above diagram and is * generally followed by a disconnected state. * - *
Information on call participant is shonw in the phone user interface until + *
Information on call participant is shown in the phone user interface until
* they enter the DISCONNECTED state. At that point call participant information
* is automatically removed from the user interface and the call is considered
* terminated.
*
* @author Emil Ivov
+ * @author Lubomir Marinov
*/
public class CallParticipantState
{
@@ -179,18 +180,67 @@ public class CallParticipantState
= new CallParticipantState(_FAILED);
/**
- * This constant value indicates a String representation of the ON_HOLD
- * call state.
- *
This constant has the String value "On Hold".
+ * The constant value being a String representation of the ON_HOLD_LOCALLY
+ * call participant state.
+ *
+ * This constant has the String value "Locally On Hold". + *
*/ - public static final String _ON_HOLD = "On Hold"; + public static final String _ON_HOLD_LOCALLY = "Locally On Hold"; /** - * This constant value indicates that the state of the call participant is - * is ON_HOLD - which means that an attempt to establish a call with that - * participant has failed for an unexpected reason. + * The constant value indicating that the state of a call participant is + * locally put on hold. + */ + public static final CallParticipantState ON_HOLD_LOCALLY + = new CallParticipantState(_ON_HOLD_LOCALLY); + + /** + * The constant value being a String representation of the ON_HOLD_MUTUALLY + * call participant state. + *+ * This constant has the String value "Mutually On Hold". + *
*/ - public static final CallParticipantState ON_HOLD - = new CallParticipantState(_ON_HOLD); + public static final String _ON_HOLD_MUTUALLY = "Mutually On Hold"; + /** + * The constant value indicating that the state of a call participant is + * mutually - locally and remotely - put on hold. + */ + public static final CallParticipantState ON_HOLD_MUTUALLY + = new CallParticipantState(_ON_HOLD_MUTUALLY); + + /** + * The constant value being a String representation of the ON_HOLD_REMOTELY + * call participant state. + *+ * This constant has the String value "Remotely On Hold". + *
+ */ + public static final String _ON_HOLD_REMOTELY = "Remotely On Hold"; + /** + * The constant value indicating that the state of a call participant is + * remotely put on hold. + */ + public static final CallParticipantState ON_HOLD_REMOTELY + = new CallParticipantState(_ON_HOLD_REMOTELY); + + /** + * Determines whether a specific CallParticipantState value + * signal a call hold regardless of the issuer (which may be local and/or + * remote). + * + * @param state + * the CallParticipantState value to be checked + * whether it signals a call hold + * @return true if the specified state signals a call + * hold; false, otherwise + */ + public static final boolean isOnHold(CallParticipantState state) + { + return CallParticipantState.ON_HOLD_LOCALLY.equals(state) + || CallParticipantState.ON_HOLD_MUTUALLY.equals(state) + || CallParticipantState.ON_HOLD_REMOTELY.equals(state); + } /** * A string representationf this Participant Call State. Could be @@ -221,10 +271,10 @@ public String getStateString() } /** - * Returns a string represenation of this call state. Strings returned + * Returns a string representation of this call state. Strings returned * by this method have the following format: * CallParticipantState:+ * Extend this class to create a CallParticipantChangeEvent listener + * and override the methods for the events of interest. (If you implement the + * CallParticipantListener interface, you have to define all of the + * methods in it. This abstract class defines null methods for them all, so you + * only have to define methods for events you care about.) + *
+ * + * @see CallParticipantChangeEvent + * @see CallParticipantListener + * + * @author Lubomir Marinov + */ +public abstract class CallParticipantAdapter + implements CallParticipantListener +{ + + /** + * Indicates that a change has occurred in the address of the source + * CallParticipant. + * + * @param evt The CallParticipantChangeEvent instance containing + * the source event as well as its previous and its new address. + */ + public void participantAddressChanged(CallParticipantChangeEvent evt) + { + } + + /** + * Indicates that a change has occurred in the display name of the source + * CallParticipant. + * + * @param evt The CallParticipantChangeEvent instance containing + * the source event as well as its previous and its new display + * names. + */ + public void participantDisplayNameChanged(CallParticipantChangeEvent evt) + { + } + + /** + * Indicates that a change has occurred in the image of the source + * CallParticipant. + * + * @param evt The CallParticipantChangeEvent instance containing + * the source event as well as its previous and its new image. + */ + public void participantImageChanged(CallParticipantChangeEvent evt) + { + } + + /** + * Indicates that a change has occurred in the status of the source + * CallParticipant. + * + * @param evt The CallParticipantChangeEvent instance containing + * the source event as well as its previous and its new status. + */ + public void participantStateChanged(CallParticipantChangeEvent evt) + { + } + + /** + * Indicates that a change has occurred in the transport address that we use + * to communicate with the participant. + * + * @param evt The CallParticipantChangeEvent instance containing + * the source event as well as its previous and its new transport + * address. + */ + public void participantTransportAddressChanged( + CallParticipantChangeEvent evt) + { + } +} diff --git a/test/net/java/sip/communicator/slick/protocol/jabber/TestOperationSetBasicTelephonyJabberImpl.java b/test/net/java/sip/communicator/slick/protocol/jabber/TestOperationSetBasicTelephonyJabberImpl.java index 3bebd49b7..6e1c6e4eb 100644 --- a/test/net/java/sip/communicator/slick/protocol/jabber/TestOperationSetBasicTelephonyJabberImpl.java +++ b/test/net/java/sip/communicator/slick/protocol/jabber/TestOperationSetBasicTelephonyJabberImpl.java @@ -858,7 +858,7 @@ public void callEnded(CallEvent event) * status changes. */ public class CallParticipantStateEventCollector - implements CallParticipantListener + extends CallParticipantAdapter { public ArrayList collectedEvents = new ArrayList(); private CallParticipant listenedCallParticipant = null; @@ -902,37 +902,6 @@ public void participantStateChanged(CallParticipantChangeEvent event) } } - /** - * Unused by this collector. - * @param event ignored. - */ - public void participantImageChanged(CallParticipantChangeEvent event) - {} - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantAddressChanged(CallParticipantChangeEvent event) - {} - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantTransportAddressChanged( - CallParticipantChangeEvent event) - {} - - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantDisplayNameChanged( - CallParticipantChangeEvent event) - {} - /** * Blocks until an event notifying us of the awaited state change is * received or until waitFor miliseconds pass (whichever happens first). diff --git a/test/net/java/sip/communicator/slick/protocol/sip/TestOperationSetBasicTelephonySipImpl.java b/test/net/java/sip/communicator/slick/protocol/sip/TestOperationSetBasicTelephonySipImpl.java index 2b6c6751d..21d3508e6 100644 --- a/test/net/java/sip/communicator/slick/protocol/sip/TestOperationSetBasicTelephonySipImpl.java +++ b/test/net/java/sip/communicator/slick/protocol/sip/TestOperationSetBasicTelephonySipImpl.java @@ -857,7 +857,7 @@ public void callEnded(CallEvent event) * status changes. */ public class CallParticipantStateEventCollector - implements CallParticipantListener + extends CallParticipantAdapter { public ArrayList collectedEvents = new ArrayList(); private CallParticipant listenedCallParticipant = null; @@ -901,37 +901,6 @@ public void participantStateChanged(CallParticipantChangeEvent event) } } - /** - * Unused by this collector. - * @param event ignored. - */ - public void participantImageChanged(CallParticipantChangeEvent event) - {} - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantAddressChanged(CallParticipantChangeEvent event) - {} - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantTransportAddressChanged( - CallParticipantChangeEvent event) - {} - - - /** - * Unused by this collector - * @param event ignored. - */ - public void participantDisplayNameChanged( - CallParticipantChangeEvent event) - {} - /** * Blocks until an event notifying us of the awaited state change is * received or until waitFor miliseconds pass (whichever happens first).