diff options
author | Lyubomir Marinov <lyubomir.marinov@jitsi.org> | 2013-11-13 22:42:37 +0200 |
---|---|---|
committer | Lyubomir Marinov <lyubomir.marinov@jitsi.org> | 2013-11-14 01:12:59 +0200 |
commit | 755b17033e7ee6395aa7a140d78e365b6f429c58 (patch) | |
tree | b3fd2265989b7e38d812e6eb8b3221cb4a113851 /src/net | |
parent | c307d5ebab5e5f20c5550d6bf341731b8b47497a (diff) | |
download | jitsi-755b17033e7ee6395aa7a140d78e365b6f429c58.zip jitsi-755b17033e7ee6395aa7a140d78e365b6f429c58.tar.gz jitsi-755b17033e7ee6395aa7a140d78e365b6f429c58.tar.bz2 |
Fixes warnings, formatting, naming consistency.
Diffstat (limited to 'src/net')
18 files changed, 19863 insertions, 19853 deletions
diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java index 3af18ee..ca4f8ff 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java @@ -1,4177 +1,4177 @@ -/* - * 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.impl.gui.main.call; - -import java.awt.*; -import java.lang.ref.*; -import java.text.*; -import java.util.*; -import java.util.List; -import java.util.regex.*; - -import javax.swing.*; - -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.impl.gui.customcontrols.*; -import net.java.sip.communicator.impl.gui.main.*; -import net.java.sip.communicator.impl.gui.main.contactlist.*; -import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.plugin.desktoputil.transparent.*; -import net.java.sip.communicator.service.contactlist.*; -import net.java.sip.communicator.service.contactsource.*; -import net.java.sip.communicator.service.gui.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.Logger; -import net.java.sip.communicator.util.account.*; - -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.codec.*; -import org.jitsi.service.neomedia.device.*; -import org.jitsi.service.neomedia.format.*; -import org.jitsi.service.resources.*; -import org.jitsi.util.*; - -/** - * The <tt>CallManager</tt> is the one that handles calls. It contains also - * the "Call" and "Hang up" buttons panel. Here are handles incoming and - * outgoing calls from and to the call operation set. - * - * @author Yana Stamcheva - * @author Lyubomir Marinov - * @author Boris Grozev - */ -public class CallManager -{ - /** - * The <tt>Logger</tt> used by the <tt>CallManager</tt> class and its - * instances for logging output. - */ - private static final Logger logger = Logger.getLogger(CallManager.class); - - /** - * The name of the property which indicates whether the user should be - * warned when starting a desktop sharing session. - */ - private static final String desktopSharingWarningProperty - = "net.java.sip.communicator.impl.gui.main" - + ".call.SHOW_DESKTOP_SHARING_WARNING"; - - /** - * The name of the property which indicates whether the preferred provider - * will be used when calling UIContact (call history). - */ - private static final String IGNORE_PREFERRED_PROVIDER_PROP - = "net.java.sip.communicator.impl.gui.main" - + ".call.IGNORE_PREFERRED_PROVIDER_PROP"; - - /** - * The <tt>CallPanel</tt>s opened by <tt>CallManager</tt> (because - * <tt>CallContainer</tt> does not give access to such lists.) - */ - private static final Map<CallConference, CallPanel> callPanels - = new HashMap<CallConference, CallPanel>(); - - /** - * A map of active outgoing calls per <tt>UIContactImpl</tt>. - */ - private static Map<Call, UIContactImpl> uiContactCalls; - - /** - * The group of notifications dedicated to missed calls. - */ - private static UINotificationGroup missedCallGroup; - - /** - * A <tt>CallListener</tt>. - */ - public static class GuiCallListener - extends SwingCallListener - { - /** - * Maps for incoming call handlers. The handlers needs to be created - * in the protocol thread while their method - * incomingCallReceivedInEventDispatchThread will be called on EDT. - * On the protocol thread a call state changed listener is added, - * if this is done on the EDT there is a almost no gap between incoming - * CallEvent and call state changed when doing auto answer and we - * end up with call answered and dialog for incoming call. - */ - private Map<CallEvent,WeakReference<IncomingCallHandler>> - inCallHandlers = Collections.synchronizedMap( - new WeakHashMap<CallEvent, - WeakReference<IncomingCallHandler>>()); - - /** - * Delivers the <tt>CallEvent</tt> in the protocol thread. - */ - public void incomingCallReceived(CallEvent ev) - { - inCallHandlers.put( - ev, - new WeakReference<IncomingCallHandler>( - new IncomingCallHandler(ev.getSourceCall()))); - - super.incomingCallReceived(ev); - } - - /** - * Implements {@link CallListener#incomingCallReceived(CallEvent)}. When - * a call is received, creates a <tt>ReceivedCallDialog</tt> and plays - * the ring phone sound to the user. - * - * @param ev the <tt>CallEvent</tt> - */ - @Override - public void incomingCallReceivedInEventDispatchThread(CallEvent ev) - { - WeakReference<IncomingCallHandler> ihRef - = inCallHandlers.remove(ev); - - if(ihRef != null) - { - ihRef.get().incomingCallReceivedInEventDispatchThread(ev); - } - } - - /** - * Implements CallListener.callEnded. Stops sounds that are playing at - * the moment if there're any. Removes the <tt>CallPanel</tt> and - * disables the hang-up button. - * - * @param ev the <tt>CallEvent</tt> which specifies the <tt>Call</tt> - * that has ended - */ - @Override - public void callEndedInEventDispatchThread(CallEvent ev) - { - CallConference callConference = ev.getCallConference(); - - closeCallContainerIfNotNecessary(callConference); - - /* - * Notify the existing CallPanels about the CallEvent (in case - * they need to update their UI, for example). - */ - forwardCallEventToCallPanels(ev); - - // If we're currently in the call history view, refresh - // it. - TreeContactList contactList - = GuiActivator.getContactList(); - - if (contactList.getCurrentFilter().equals( - TreeContactList.historyFilter)) - { - contactList.applyFilter( - TreeContactList.historyFilter); - } - } - - /** - * Creates and opens a call dialog. Implements - * {@link CallListener#outgoingCallCreated(CallEvent)}. - * - * @param ev the <tt>CallEvent</tt> - */ - @Override - public void outgoingCallCreatedInEventDispatchThread(CallEvent ev) - { - Call sourceCall = ev.getSourceCall(); - - openCallContainerIfNecessary(sourceCall); - - /* - * Notify the existing CallPanels about the CallEvent (in case they - * need to update their UI, for example). - */ - forwardCallEventToCallPanels(ev); - } - } - - /** - * Handles incoming calls. Must be created on the protocol thread while the - * method incomingCallReceivedInEventDispatchThread is executed on the EDT. - */ - private static class IncomingCallHandler - extends CallChangeAdapter - { - /** - * The dialog shown - */ - private ReceivedCallDialog receivedCallDialog; - - /** - * Peer name. - */ - private String peerName; - - /** - * The time of the incoming call. - */ - private long callTime; - - /** - * Construct - * @param sourceCall - */ - IncomingCallHandler(Call sourceCall) - { - Iterator<? extends CallPeer> peerIter = sourceCall.getCallPeers(); - - if(!peerIter.hasNext()) - { - return; - } - - peerName = peerIter.next().getDisplayName(); - callTime = System.currentTimeMillis(); - - sourceCall.addCallChangeListener(this); - } - - /** - * State has changed. - * @param ev - */ - @Override - public void callStateChanged(final CallChangeEvent ev) - { - if(!SwingUtilities.isEventDispatchThread()) - { - SwingUtilities.invokeLater( - new Runnable() - { - public void run() - { - callStateChanged(ev); - } - }); - return; - } - if (!CallChangeEvent.CALL_STATE_CHANGE - .equals(ev.getPropertyName())) - return; - - // When the call state changes, we ensure here that the - // received call notification dialog is closed. - if (receivedCallDialog != null && receivedCallDialog.isVisible()) - receivedCallDialog.setVisible(false); - - // Ensure that the CallDialog is created, because it is the - // one that listens for CallPeers. - Object newValue = ev.getNewValue(); - Call call = ev.getSourceCall(); - - if (CallState.CALL_INITIALIZATION.equals(newValue) - || CallState.CALL_IN_PROGRESS.equals(newValue)) - { - openCallContainerIfNecessary(call); - } - else if (CallState.CALL_ENDED.equals(newValue)) - { - if (ev.getOldValue().equals( - CallState.CALL_INITIALIZATION)) - { - // If the call was answered elsewhere, don't mark it - // as missed. - CallPeerChangeEvent cause = ev.getCause(); - - if ((cause == null) - || (cause.getReasonCode() - != CallPeerChangeEvent - .NORMAL_CALL_CLEARING)) - { - addMissedCallNotification(peerName, callTime); - } - } - - call.removeCallChangeListener(this); - } - } - - /** - * Executed on EDT cause will create dialog and will show it. - * @param ev - */ - public void incomingCallReceivedInEventDispatchThread(CallEvent ev) - { - Call sourceCall = ev.getSourceCall(); - boolean isVideoCall - = ev.isVideoCall() - && ConfigurationUtils.hasEnabledVideoFormat( - sourceCall.getProtocolProvider()); - receivedCallDialog = new ReceivedCallDialog( - sourceCall, - isVideoCall, - (CallManager.getInProgressCalls().size() > 0)); - - receivedCallDialog.setVisible(true); - - Iterator<? extends CallPeer> peerIter = sourceCall.getCallPeers(); - - if(!peerIter.hasNext()) - { - if (receivedCallDialog.isVisible()) - receivedCallDialog.setVisible(false); - return; - } - - /* - * Notify the existing CallPanels about the CallEvent (in case they - * need to update their UI, for example). - */ - forwardCallEventToCallPanels(ev); - } - } - - /** - * Answers the given call. - * - * @param call the call to answer - */ - public static void answerCall(Call call) - { - answerCall(call, null, false /* without video */); - } - - /** - * Answers a specific <tt>Call</tt> with or without video and, optionally, - * does that in a telephony conference with an existing <tt>Call</tt>. - * - * @param call - * @param existingCall - * @param video - */ - private static void answerCall(Call call, Call existingCall, boolean video) - { - if (existingCall == null) - openCallContainerIfNecessary(call); - - new AnswerCallThread(call, existingCall, video).start(); - } - - /** - * Answers the given call in an existing call. It will end up with a - * conference call. - * - * @param call the call to answer - */ - public static void answerCallInFirstExistingCall(Call call) - { - // Find the first existing call. - Iterator<Call> existingCallIter = getInProgressCalls().iterator(); - Call existingCall - = existingCallIter.hasNext() ? existingCallIter.next() : null; - - answerCall(call, existingCall, false /* without video */); - } - - /** - * Merges specific existing <tt>Call</tt>s into a specific telephony - * conference. - * - * @param conference the conference - * @param calls list of calls - */ - public static void mergeExistingCalls( - CallConference conference, - Collection<Call> calls) - { - new MergeExistingCalls(conference, calls).start(); - } - - /** - * Answers the given call with video. - * - * @param call the call to answer - */ - public static void answerVideoCall(Call call) - { - answerCall(call, null, true /* with video */); - } - - /** - * Hang ups the given call. - * - * @param call the call to hang up - */ - public static void hangupCall(Call call) - { - new HangupCallThread(call).start(); - } - - /** - * Hang ups the given <tt>callPeer</tt>. - * - * @param peer the <tt>CallPeer</tt> to hang up - */ - public static void hangupCallPeer(CallPeer peer) - { - new HangupCallThread(peer).start(); - } - - /** - * Asynchronously hangs up the <tt>Call</tt>s participating in a specific - * <tt>CallConference</tt>. - * - * @param conference the <tt>CallConference</tt> whose participating - * <tt>Call</tt>s are to be hanged up - */ - public static void hangupCalls(CallConference conference) - { - new HangupCallThread(conference).start(); - } - - /** - * Creates a call to the contact represented by the given string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - */ - public static void createCall( ProtocolProviderService protocolProvider, - String contact) - { - new CreateCallThread(protocolProvider, contact, false /* audio-only */) - .start(); - } - - /** - * Creates a call to the contact represented by the given string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - * @param uiContact the meta contact we're calling - */ - public static void createCall( ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact) - { - new CreateCallThread(protocolProvider, null, null, uiContact, - contact, null, null, false /* audio-only */).start(); - } - - /** - * Creates a video call to the contact represented by the given string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - */ - public static void createVideoCall(ProtocolProviderService protocolProvider, - String contact) - { - new CreateCallThread(protocolProvider, contact, true /* video */) - .start(); - } - - /** - * Creates a video call to the contact represented by the given string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - * @param uiContact the <tt>UIContactImpl</tt> we're calling - */ - public static void createVideoCall( ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact) - { - new CreateCallThread(protocolProvider, null, null, uiContact, - contact, null, null, true /* video */).start(); - } - - /** - * Enables/disables local video for a specific <tt>Call</tt>. - * - * @param call the <tt>Call</tt> to enable/disable to local video for - * @param enable <tt>true</tt> to enable the local video; otherwise, - * <tt>false</tt> - */ - public static void enableLocalVideo(Call call, boolean enable) - { - new EnableLocalVideoThread(call, enable).start(); - } - - /** - * Indicates if the desktop sharing is currently enabled for the given - * <tt>call</tt>. - * - * @param call the <tt>Call</tt>, for which we would to check if the desktop - * sharing is currently enabled - * @return <tt>true</tt> if the desktop sharing is currently enabled for the - * given <tt>call</tt>, <tt>false</tt> otherwise - */ - public static boolean isLocalVideoEnabled(Call call) - { - OperationSetVideoTelephony telephony - = call.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - - return (telephony != null) && telephony.isLocalVideoAllowed(call); - } - - /** - * Creates a desktop sharing call to the contact represented by the given - * string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - * @param uiContact the <tt>UIContactImpl</tt> we're calling - */ - private static void createDesktopSharing( - ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact) - { - // If the user presses cancel on the desktop sharing warning then we - // have nothing more to do here. - if (!showDesktopSharingWarning()) - return; - - MediaService mediaService = GuiActivator.getMediaService(); - List<MediaDevice> desktopDevices - = mediaService.getDevices(MediaType.VIDEO, MediaUseCase.DESKTOP); - int deviceNumber = desktopDevices.size(); - - if (deviceNumber == 1) - { - createDesktopSharing( - protocolProvider, - contact, - uiContact, - desktopDevices.get(0)); - } - else if (deviceNumber > 1) - { - SelectScreenDialog selectDialog - = new SelectScreenDialog(desktopDevices); - - selectDialog.setVisible(true); - if (selectDialog.getSelectedDevice() != null) - createDesktopSharing( - protocolProvider, - contact, - uiContact, - selectDialog.getSelectedDevice()); - } - } - - /** - * Creates a region desktop sharing through the given - * <tt>protocolProvider</tt> with the given <tt>contact</tt>. - * - * @param protocolProvider the <tt>ProtocolProviderService</tt>, through - * which the sharing session will be established - * @param contact the address of the contact recipient - * @param uiContact the <tt>UIContactImpl</tt> we're calling - */ - private static void createRegionDesktopSharing( - ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact) - { - if (showDesktopSharingWarning()) - { - TransparentFrame frame = DesktopSharingFrame - .createTransparentFrame( - protocolProvider, contact, uiContact, true); - - frame.setLocationRelativeTo(null); - frame.setVisible(true); - } - } - - /** - * Creates a desktop sharing call to the contact represented by the given - * string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - * @param uiContact the <tt>MetaContact</tt> we're calling - * @param x the x coordinate of the shared region - * @param y the y coordinated of the shared region - * @param width the width of the shared region - * @param height the height of the shared region - */ - public static void createRegionDesktopSharing( - ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact, - int x, - int y, - int width, - int height) - { - MediaService mediaService = GuiActivator.getMediaService(); - - List<MediaDevice> desktopDevices = mediaService.getDevices( - MediaType.VIDEO, MediaUseCase.DESKTOP); - - int deviceNumber = desktopDevices.size(); - - if (deviceNumber > 0) - { - createDesktopSharing( - protocolProvider, - contact, - uiContact, - mediaService.getMediaDeviceForPartialDesktopStreaming( - width, - height, - x, - y)); - } - } - - /** - * Creates a desktop sharing call to the contact represented by the given - * string. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param contact the contact to call to - * @param uiContact the <tt>UIContactImpl</tt> we're calling - * @param mediaDevice the media device corresponding to the screen to share - */ - private static void createDesktopSharing( - ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact, - MediaDevice mediaDevice) - { - new CreateDesktopSharingThread( protocolProvider, - contact, - uiContact, - mediaDevice).start(); - } - - /** - * Enables the desktop sharing in an existing <tt>call</tt>. - * - * @param call the call for which desktop sharing should be enabled - * @param enable indicates if the desktop sharing should be enabled or - * disabled - */ - public static void enableDesktopSharing(Call call, boolean enable) - { - if (!enable) - enableDesktopSharing(call, null, enable); - else if (showDesktopSharingWarning()) - { - MediaService mediaService = GuiActivator.getMediaService(); - List<MediaDevice> desktopDevices - = mediaService.getDevices(MediaType.VIDEO, MediaUseCase.DESKTOP); - int deviceNumber = desktopDevices.size(); - - if (deviceNumber == 1) - enableDesktopSharing(call, null, enable); - else if (deviceNumber > 1) - { - SelectScreenDialog selectDialog - = new SelectScreenDialog(desktopDevices); - - selectDialog.setVisible(true); - - if (selectDialog.getSelectedDevice() != null) - enableDesktopSharing( - call, selectDialog.getSelectedDevice(), enable); - } - } - - // in case we switch to video, disable remote control if it was - // enabled - enableDesktopRemoteControl(call.getCallPeers().next(), false); - } - - /** - * Enables the region desktop sharing for the given call. - * - * @param call the call, for which the region desktop sharing should be - * enabled - * @param enable indicates if the desktop sharing should be enabled or - * disabled - */ - public static void enableRegionDesktopSharing(Call call, boolean enable) - { - if (!enable) - enableDesktopSharing(call, null, enable); - else if (showDesktopSharingWarning()) - { - TransparentFrame frame - = DesktopSharingFrame.createTransparentFrame(call, true); - - frame.setVisible(true); - } - } - - /** - * Creates a desktop sharing call to the contact represented by the given - * string. - * - * @param call the call for which desktop sharing should be enabled - * @param x the x coordinate of the shared region - * @param y the y coordinated of the shared region - * @param width the width of the shared region - * @param height the height of the shared region - */ - public static void enableRegionDesktopSharing( - Call call, - int x, - int y, - int width, - int height) - { - // Use the default media device corresponding to the screen to share - MediaService mediaService = GuiActivator.getMediaService(); - - List<MediaDevice> desktopDevices = mediaService.getDevices( - MediaType.VIDEO, MediaUseCase.DESKTOP); - - int deviceNumber = desktopDevices.size(); - - if (deviceNumber > 0) - { - boolean succeed = enableDesktopSharing( - call, - mediaService.getMediaDeviceForPartialDesktopStreaming( - width, - height, - x, - y), - true); - // If the region sharing succeed, then display the frame of the - // current region shared. - if(succeed) - { - TransparentFrame frame - = DesktopSharingFrame.createTransparentFrame(call, false); - - frame.setVisible(true); - } - } - - // in case we switch to video, disable remote control if it was - // enabled - enableDesktopRemoteControl(call.getCallPeers().next(), false); - } - - /** - * Enables the desktop sharing in an existing <tt>call</tt>. - * - * @param call the call for which desktop sharing should be enabled - * @param mediaDevice the media device corresponding to the screen to share - * @param enable indicates if the desktop sharing should be enabled or - * disabled - * - * @return True if the desktop sharing succeed (we are currently sharing the - * whole or a part of the desktop). False, otherwise. - */ - private static boolean enableDesktopSharing(Call call, - MediaDevice mediaDevice, - boolean enable) - { - OperationSetDesktopSharingServer desktopOpSet - = call.getProtocolProvider().getOperationSet( - OperationSetDesktopSharingServer.class); - boolean enableSucceeded = false; - - // This shouldn't happen at this stage, because we disable the button - // if the operation set isn't available. - if (desktopOpSet != null) - { - // First make sure the local video button is disabled. - if (enable && isLocalVideoEnabled(call)) - getActiveCallContainer(call).setVideoButtonSelected(false); - - try - { - if (mediaDevice != null) - { - desktopOpSet.setLocalVideoAllowed( - call, - mediaDevice, - enable); - } - else - desktopOpSet.setLocalVideoAllowed(call, enable); - - enableSucceeded = true; - } - catch (OperationFailedException ex) - { - logger.error( - "Failed to toggle the streaming of local video.", - ex); - } - } - - return (enable && enableSucceeded); - } - - /** - * Indicates if the desktop sharing is currently enabled for the given - * <tt>call</tt>. - * - * @param call the <tt>Call</tt>, for which we would to check if the desktop - * sharing is currently enabled - * @return <tt>true</tt> if the desktop sharing is currently enabled for the - * given <tt>call</tt>, <tt>false</tt> otherwise - */ - public static boolean isDesktopSharingEnabled(Call call) - { - OperationSetDesktopSharingServer desktopOpSet - = call.getProtocolProvider().getOperationSet( - OperationSetDesktopSharingServer.class); - - if (desktopOpSet != null - && desktopOpSet.isLocalVideoAllowed(call)) - return true; - - return false; - } - - /** - * Indicates if the desktop sharing is currently enabled for the given - * <tt>call</tt>. - * - * @param call the <tt>Call</tt>, for which we would to check if the desktop - * sharing is currently enabled - * @return <tt>true</tt> if the desktop sharing is currently enabled for the - * given <tt>call</tt>, <tt>false</tt> otherwise - */ - public static boolean isRegionDesktopSharingEnabled(Call call) - { - OperationSetDesktopSharingServer desktopOpSet - = call.getProtocolProvider().getOperationSet( - OperationSetDesktopSharingServer.class); - - if (desktopOpSet != null - && desktopOpSet.isPartialStreaming(call)) - return true; - - return false; - } - - /** - * Enables/disables remote control when in a desktop sharing session with - * the given <tt>callPeer</tt>. - * - * @param callPeer the call peer for which we enable/disable remote control - * @param isEnable indicates if the remote control should be enabled - */ - public static void enableDesktopRemoteControl( CallPeer callPeer, - boolean isEnable) - { - OperationSetDesktopSharingServer sharingOpSet - = callPeer.getProtocolProvider().getOperationSet( - OperationSetDesktopSharingServer.class); - - if (sharingOpSet == null) - return; - - if (isEnable) - sharingOpSet.enableRemoteControl(callPeer); - else - sharingOpSet.disableRemoteControl(callPeer); - } - - /** - * Creates a call to the given call string. The given component indicates - * where should be shown the "call via" menu if needed. - * - * @param callString the string to call - * @param c the component, which indicates where should be shown the "call - * via" menu if needed - */ - public static void createCall( String callString, - JComponent c) - { - createCall(callString, c, null); - } - - /** - * Creates a call to the given call string. The given component indicates - * where should be shown the "call via" menu if needed. - * - * @param callString the string to call - * @param c the component, which indicates where should be shown the "call - * via" menu if needed - * @param l listener that is notified when the call interface has been - * started after call was created - */ - public static void createCall( String callString, - JComponent c, - CallInterfaceListener l) - { - callString = callString.trim(); - - // Removes special characters from phone numbers. - if (ConfigurationUtils.isNormalizePhoneNumber()) - callString = PhoneNumberI18nService.normalize(callString); - - List<ProtocolProviderService> telephonyProviders - = CallManager.getTelephonyProviders(); - - if (telephonyProviders.size() == 1) - { - CallManager.createCall( - telephonyProviders.get(0), callString); - - if (l != null) - l.callInterfaceStarted(); - } - else if (telephonyProviders.size() > 1) - { - /* - * Allow plugins which do not have a (Jitsi) UI to create calls by - * automagically picking up a telephony provider. - */ - if (c == null) - { - ProtocolProviderService preferredTelephonyProvider = null; - - for (ProtocolProviderService telephonyProvider - : telephonyProviders) - { - try - { - OperationSetPresence presenceOpSet - = telephonyProvider.getOperationSet( - OperationSetPresence.class); - - if ((presenceOpSet != null) - && (presenceOpSet.findContactByID(callString) - != null)) - { - preferredTelephonyProvider = telephonyProvider; - break; - } - } - catch (Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - } - } - if (preferredTelephonyProvider == null) - preferredTelephonyProvider = telephonyProviders.get(0); - - CallManager.createCall(preferredTelephonyProvider, callString); - if (l != null) - l.callInterfaceStarted(); - } - else - { - ChooseCallAccountPopupMenu chooseAccountDialog - = new ChooseCallAccountPopupMenu( - c, - callString, - telephonyProviders, - l); - - chooseAccountDialog.setLocation(c.getLocation()); - chooseAccountDialog.showPopupMenu(); - } - } - else - { - ResourceManagementService resources = GuiActivator.getResources(); - - new ErrorDialog( - null, - resources.getI18NString("service.gui.WARNING"), - resources.getI18NString( - "service.gui.NO_ONLINE_TELEPHONY_ACCOUNT")) - .showDialog(); - } - } - - /** - * Creates a call to the given list of contacts. - * - * @param protocolProvider the protocol provider to which this call belongs. - * @param callees the list of contacts to call to - */ - public static void createConferenceCall( - String[] callees, - ProtocolProviderService protocolProvider) - { - Map<ProtocolProviderService, List<String>> crossProtocolCallees - = new HashMap<ProtocolProviderService, List<String>>(); - - crossProtocolCallees.put(protocolProvider, Arrays.asList(callees)); - createConferenceCall(crossProtocolCallees); - } - - /** - * Invites the given list of <tt>callees</tt> to the given conference - * <tt>call</tt>. - * - * @param callees the list of contacts to invite - * @param call the protocol provider to which this call belongs - */ - public static void inviteToConferenceCall(String[] callees, Call call) - { - Map<ProtocolProviderService, List<String>> crossProtocolCallees - = new HashMap<ProtocolProviderService, List<String>>(); - - crossProtocolCallees.put( - call.getProtocolProvider(), - Arrays.asList(callees)); - inviteToConferenceCall(crossProtocolCallees, call); - } - - /** - * Invites the given list of <tt>callees</tt> to the given conference - * <tt>call</tt>. - * - * @param callees the list of contacts to invite - * @param call existing call - */ - public static void inviteToConferenceCall( - Map<ProtocolProviderService, List<String>> callees, - Call call) - { - new InviteToConferenceCallThread(callees, call).start(); - } - - /** - * Invites specific <tt>callees</tt> to a specific telephony conference. - * - * @param callees the list of contacts to invite - * @param conference the telephony conference to invite the specified - * <tt>callees</tt> into - */ - public static void inviteToConferenceCall( - Map<ProtocolProviderService, List<String>> callees, - CallConference conference) - { - /* - * InviteToConferenceCallThread takes a specific Call but actually - * invites to the telephony conference associated with the specified - * Call (if any). In order to not change the signature of its - * constructor at this time, just pick up a Call participating in the - * specified telephony conference (if any). - */ - Call call = null; - - if (conference != null) - { - List<Call> calls = conference.getCalls(); - - if (!calls.isEmpty()) - call = calls.get(0); - } - - new InviteToConferenceCallThread(callees, call).start(); - } - - /** - * Asynchronously creates a new conference <tt>Call</tt> with a specific - * list of participants/callees. - * - * @param callees the list of participants/callees to invite to a - * newly-created conference <tt>Call</tt> - */ - public static void createConferenceCall( - Map<ProtocolProviderService, List<String>> callees) - { - new InviteToConferenceCallThread(callees, null).start(); - } - - /** - * Asynchronously creates a new video bridge conference <tt>Call</tt> with - * a specific list of participants/callees. - * - * @param callProvider the <tt>ProtocolProviderService</tt> to use for - * creating the call - * @param callees the list of participants/callees to invite to the - * newly-created video bridge conference <tt>Call</tt> - */ - public static void createVideoBridgeConfCall( - ProtocolProviderService callProvider, - String[] callees) - { - new InviteToConferenceBridgeThread(callProvider, callees, null).start(); - } - - /** - * Invites the given list of <tt>callees</tt> to the given conference - * <tt>call</tt>. - * - * @param callees the list of contacts to invite - * @param call the protocol provider to which this call belongs - */ - public static void inviteToVideoBridgeConfCall(String[] callees, Call call) - { - new InviteToConferenceBridgeThread( call.getProtocolProvider(), - callees, - call).start(); - } - - /** - * Puts on or off hold the given <tt>callPeer</tt>. - * @param callPeer the peer to put on/off hold - * @param isOnHold indicates the action (on hold or off hold) - */ - public static void putOnHold(CallPeer callPeer, boolean isOnHold) - { - new PutOnHoldCallPeerThread(callPeer, isOnHold).start(); - } - - /** - * Transfers the given <tt>peer</tt> to the given <tt>target</tt>. - * @param peer the <tt>CallPeer</tt> to transfer - * @param target the <tt>CallPeer</tt> target to transfer to - */ - public static void transferCall(CallPeer peer, CallPeer target) - { - OperationSetAdvancedTelephony<?> telephony - = peer.getCall().getProtocolProvider() - .getOperationSet(OperationSetAdvancedTelephony.class); - - if (telephony != null) - { - try - { - telephony.transfer(peer, target); - } - catch (OperationFailedException ex) - { - logger.error("Failed to transfer " + peer.getAddress() - + " to " + target, ex); - } - } - } - - /** - * Transfers the given <tt>peer</tt> to the given <tt>target</tt>. - * @param peer the <tt>CallPeer</tt> to transfer - * @param target the target of the transfer - */ - public static void transferCall(CallPeer peer, String target) - { - OperationSetAdvancedTelephony<?> telephony - = peer.getCall().getProtocolProvider() - .getOperationSet(OperationSetAdvancedTelephony.class); - - if (telephony != null) - { - try - { - telephony.transfer(peer, target); - } - catch (OperationFailedException ex) - { - logger.error("Failed to transfer " + peer.getAddress() - + " to " + target, ex); - } - } - } - - /** - * Closes the <tt>CallPanel</tt> of a specific <tt>Call</tt> if it is no - * longer necessary (i.e. is not used by other <tt>Call</tt>s participating - * in the same telephony conference as the specified <tt>Call</tt>.) - * - * @param callConference The <tt>CallConference</tt> which is to have its - * associated <tt>CallPanel</tt>, if any - */ - private static void closeCallContainerIfNotNecessary( - final CallConference callConference) - { - CallPanel callPanel = callPanels.get(callConference); - - if (callPanel != null) - closeCallContainerIfNotNecessary( - callConference, callPanel.isCloseWaitAfterHangup()); - } - - /** - * Closes the <tt>CallPanel</tt> of a specific <tt>Call</tt> if it is no - * longer necessary (i.e. is not used by other <tt>Call</tt>s participating - * in the same telephony conference as the specified <tt>Call</tt>.) - * - * @param callConference The <tt>CallConference</tt> which is to have its - * associated <tt>CallPanel</tt>, if any, closed - * @param wait <tt>true</tt> to set <tt>delay</tt> param of - * {@link CallContainer#close(CallPanel, boolean)} (CallPanel)} - */ - private static void closeCallContainerIfNotNecessary( - final CallConference callConference, - final boolean wait) - { - if (!SwingUtilities.isEventDispatchThread()) - { - SwingUtilities.invokeLater( - new Runnable() - { - public void run() - { - closeCallContainerIfNotNecessary( - callConference, - wait); - } - }); - return; - } - - /* - * XXX The integrity of the execution of the method may be compromised - * if it is not invoked on the AWT event dispatching thread because - * findCallPanel and callPanels.remove must be atomically executed. The - * uninterrupted execution (with respect to the synchronization) is - * guaranteed by requiring all modifications to callPanels to be made on - * the AWT event dispatching thread. - */ - - for (Iterator<Map.Entry<CallConference, CallPanel>> entryIter - = callPanels.entrySet().iterator(); - entryIter.hasNext();) - { - Map.Entry<CallConference, CallPanel> entry = entryIter.next(); - CallConference aConference = entry.getKey(); - boolean notNecessary = aConference.isEnded(); - - if (notNecessary) - { - CallPanel aCallPanel = entry.getValue(); - CallContainer window = aCallPanel.getCallWindow(); - - try - { - window.close( - aCallPanel, - wait && (aConference == callConference)); - } - finally - { - /* - * We allow non-modifications i.e. reads of callPanels on - * threads other than the AWT event dispatching thread so we - * have to make sure that we will not cause - * ConcurrentModificationException. - */ - synchronized (callPanels) - { - entryIter.remove(); - } - - aCallPanel.dispose(); - } - } - } - } - - /** - * Opens a <tt>CallPanel</tt> for a specific <tt>Call</tt> if there is none. - * <p> - * <b>Note</b>: The method can be called only on the AWT event dispatching - * thread. - * </p> - * - * @param call the <tt>Call</tt> to open a <tt>CallPanel</tt> for - * @return the <tt>CallPanel</tt> associated with the <tt>Call</tt> - * @throws RuntimeException if the method is not called on the AWT event - * dispatching thread - */ - private static CallPanel openCallContainerIfNecessary(Call call) - { - /* - * XXX The integrity of the execution of the method may be compromised - * if it is not invoked on the AWT event dispatching thread because - * findCallPanel and callPanels.put must be atomically executed. The - * uninterrupted execution (with respect to the synchronization) is - * guaranteed by requiring all modifications to callPanels to be made on - * the AWT event dispatching thread. - */ - assertIsEventDispatchingThread(); - - /* - * CallPanel displays a CallConference (which may contain multiple - * Calls.) - */ - CallConference conference = call.getConference(); - CallPanel callPanel = findCallPanel(conference); - - if (callPanel == null) - { - // If we're in single-window mode, the single window is the - // CallContainer. - CallContainer callContainer - = GuiActivator.getUIService().getSingleWindowContainer(); - - // If we're in multi-window mode, we create the CallDialog. - if (callContainer == null) - callContainer = new CallDialog(); - - callPanel = new CallPanel(conference, callContainer); - callContainer.addCallPanel(callPanel); - - synchronized (callPanels) - { - callPanels.put(conference, callPanel); - } - } - - return callPanel; - } - - /** - * Returns a list of all currently registered telephony providers. - * @return a list of all currently registered telephony providers - */ - public static List<ProtocolProviderService> getTelephonyProviders() - { - return AccountUtils - .getRegisteredProviders(OperationSetBasicTelephony.class); - } - - /** - * Returns a list of all currently registered telephony providers supporting - * conferencing. - * - * @return a list of all currently registered telephony providers supporting - * conferencing - */ - public static List<ProtocolProviderService> - getTelephonyConferencingProviders() - { - return AccountUtils - .getRegisteredProviders(OperationSetTelephonyConferencing.class); - } - - /** - * Returns a list of all currently active calls. - * - * @return a list of all currently active calls - */ - private static List<Call> getActiveCalls() - { - CallConference[] conferences; - - synchronized (callPanels) - { - Set<CallConference> keySet = callPanels.keySet(); - - conferences = keySet.toArray(new CallConference[keySet.size()]); - } - - List<Call> calls = new ArrayList<Call>(); - - for (CallConference conference : conferences) - { - for (Call call : conference.getCalls()) - { - if (call.getCallState() == CallState.CALL_IN_PROGRESS) - calls.add(call); - } - } - return calls; - } - - /** - * Returns a collection of all currently in progress calls. A call is active - * if it is in progress so the method merely delegates to - * - * @return a collection of all currently in progress calls. - */ - public static Collection<Call> getInProgressCalls() - { - return getActiveCalls(); - } - - /** - * Returns the <tt>CallContainer</tt> corresponding to the given - * <tt>call</tt>. If the call has been finished and no active - * <tt>CallContainer</tt> could be found it returns null. - * - * @param call the <tt>Call</tt>, which dialog we're looking for - * @return the <tt>CallContainer</tt> corresponding to the given - * <tt>call</tt> - */ - public static CallPanel getActiveCallContainer(Call call) - { - return findCallPanel(call.getConference()); - } - - /** - * A informative text to show for the peer. If display name is missing - * return the address. - * @param peer the peer. - * @return the text contain display name. - */ - public static String getPeerDisplayName(CallPeer peer) - { - String displayName = null; - - // We try to find the <tt>UIContact</tt>, to which the call was - // created if this was an outgoing call. - UIContactImpl uiContact - = CallManager.getCallUIContact(peer.getCall()); - - if(uiContact != null) - { - if(uiContact.getDescriptor() instanceof SourceContact) - { - // if it is source contact (history record) - // search for cusax contact match - Contact contact = getPeerCusaxContact(peer.getAddress(), - (SourceContact)uiContact.getDescriptor()); - if(contact != null) - displayName = contact.getDisplayName(); - } - - if(StringUtils.isNullOrEmpty(displayName, true)) - displayName = uiContact.getDisplayName(); - } - - // We search for a contact corresponding to this call peer and - // try to get its display name. - if (StringUtils.isNullOrEmpty(displayName, true) - && peer.getContact() != null) - { - displayName = peer.getContact().getDisplayName(); - } - - // We try to find the an alternative peer address. - if (StringUtils.isNullOrEmpty(displayName, true)) - { - String imppAddress = peer.getAlternativeIMPPAddress(); - - if (!StringUtils.isNullOrEmpty(imppAddress)) - { - int protocolPartIndex = imppAddress.indexOf(":"); - - imppAddress = (protocolPartIndex >= 0) - ? imppAddress.substring(protocolPartIndex + 1) - : imppAddress; - - Collection<ProtocolProviderService> cusaxProviders - = AccountUtils.getRegisteredProviders( - OperationSetCusaxUtils.class); - - if (cusaxProviders != null && cusaxProviders.size() > 0) - { - Contact contact = getPeerContact( - peer, - cusaxProviders.iterator().next(), - imppAddress); - - displayName = (contact != null) - ? contact.getDisplayName() : null; - } - else - { - MetaContact metaContact - = getPeerMetaContact(peer, imppAddress); - - displayName = (metaContact != null) - ? metaContact.getDisplayName() : null; - } - } - } - - if (StringUtils.isNullOrEmpty(displayName, true)) - { - displayName = (!StringUtils.isNullOrEmpty - (peer.getDisplayName(), true)) - ? peer.getDisplayName() - : peer.getAddress(); - - // Try to resolve the display name - String resolvedName = resolveContactSource(displayName); - if(resolvedName != null) - { - displayName = resolvedName; - } - } - - return displayName; - } - - /** - * Returns the image corresponding to the given <tt>peer</tt>. - * - * @param peer the call peer, for which we're returning an image - * @return the peer image - */ - public static byte[] getPeerImage(CallPeer peer) - { - byte[] image = null; - // We search for a contact corresponding to this call peer and - // try to get its image. - if (peer.getContact() != null) - { - image = getContactImage(peer.getContact()); - } - - // We try to find the <tt>UIContact</tt>, to which the call was - // created if this was an outgoing call. - if (image == null || image.length == 0) - { - UIContactImpl uiContact - = CallManager.getCallUIContact(peer.getCall()); - - if (uiContact != null) - { - if(uiContact.getDescriptor() instanceof SourceContact - && ((SourceContact)uiContact.getDescriptor()) - .isDefaultImage()) - { - // if it is source contact (history record) - // search for cusax contact match - Contact contact = getPeerCusaxContact(peer.getAddress(), - (SourceContact)uiContact.getDescriptor()); - - if(contact != null) - image = contact.getImage(); - } - else - image = uiContact.getAvatar(); - } - } - - // We try to find the an alternative peer address. - if (image == null || image.length == 0) - { - String imppAddress = peer.getAlternativeIMPPAddress(); - - if (!StringUtils.isNullOrEmpty(imppAddress)) - { - int protocolPartIndex = imppAddress.indexOf(":"); - - imppAddress = (protocolPartIndex >= 0) - ? imppAddress.substring(protocolPartIndex + 1) - : imppAddress; - - Collection<ProtocolProviderService> cusaxProviders - = AccountUtils.getRegisteredProviders( - OperationSetCusaxUtils.class); - - if (cusaxProviders != null && cusaxProviders.size() > 0) - { - Contact contact = getPeerContact( - peer, - cusaxProviders.iterator().next(), - imppAddress); - - image = (contact != null) ? getContactImage(contact) : null; - } - else - { - MetaContact metaContact - = getPeerMetaContact(peer, imppAddress); - - image = (metaContact != null) - ? metaContact.getAvatar() : null; - } - } - } - - // If the icon is still null we try to get an image from the call - // peer. - if ((image == null || image.length == 0) - && peer.getImage() != null) - image = peer.getImage(); - - return image; - } - - /** - * Searches the cusax enabled providers for a contact with - * the detail (address) of the call peer if found and the contact - * is provided by a provider which is IM capable, return the contact. - * @param peer the peer we are calling. - * @return the im capable contact corresponding the <tt>peer</tt>. - */ - public static Contact getIMCapableCusaxContact(CallPeer peer) - { - // We try to find the <tt>UIContact</tt>, to which the call was - // created if this was an outgoing call. - UIContactImpl uiContact - = CallManager.getCallUIContact(peer.getCall()); - - if (uiContact != null) - { - if(uiContact.getDescriptor() instanceof MetaContact) - { - MetaContact metaContact = - (MetaContact)uiContact.getDescriptor(); - Iterator<Contact> iter = metaContact.getContacts(); - while(iter.hasNext()) - { - Contact contact = iter.next(); - if(contact.getProtocolProvider() - .getOperationSet( - OperationSetBasicInstantMessaging.class) != null) - return contact; - } - } - else if(uiContact.getDescriptor() instanceof SourceContact) - { - // if it is source contact (history record) - // search for cusax contact match - Contact contact = getPeerCusaxContact(peer.getAddress(), - (SourceContact)uiContact.getDescriptor()); - if(contact != null - && contact.getProtocolProvider().getOperationSet( - OperationSetBasicInstantMessaging.class) != null) - return contact; - } - } - - // We try to find the an alternative peer address. - String imppAddress = peer.getAlternativeIMPPAddress(); - - if (!StringUtils.isNullOrEmpty(imppAddress)) - { - int protocolPartIndex = imppAddress.indexOf(":"); - - imppAddress = (protocolPartIndex >= 0) - ? imppAddress.substring(protocolPartIndex + 1) - : imppAddress; - - Collection<ProtocolProviderService> cusaxProviders - = AccountUtils.getRegisteredProviders( - OperationSetCusaxUtils.class); - - if (cusaxProviders != null && cusaxProviders.size() > 0) - { - ProtocolProviderService cusaxProvider - = cusaxProviders.iterator().next(); - - Contact contact = getPeerContact( - peer, - cusaxProvider, - imppAddress); - - if(contact != null - && cusaxProvider.getOperationSet( - OperationSetBasicInstantMessaging.class) != null) - { - return contact; - } - } - } - - return null; - } - - /** - * Find is there a linked cusax protocol provider for this source contact, - * if it exist we try to resolve current peer to one of its contacts - * or details of a contact (numbers). - * @param peerAddress the address of the peer to check - * @param sourceContact the currently selected source contact. - * @return matching cusax contact. - */ - private static Contact getPeerCusaxContact( - String peerAddress, SourceContact sourceContact) - { - ProtocolProviderService linkedCusaxProvider = null; - for(ContactDetail detail : sourceContact.getContactDetails()) - { - ProtocolProviderService pps - = detail.getPreferredProtocolProvider( - OperationSetBasicTelephony.class); - - if(pps != null) - { - OperationSetCusaxUtils cusaxOpSet = - pps.getOperationSet(OperationSetCusaxUtils.class); - - if(cusaxOpSet != null) - { - linkedCusaxProvider - = cusaxOpSet.getLinkedCusaxProvider(); - break; - } - } - } - - if(linkedCusaxProvider != null) - { - OperationSetPersistentPresence opSetPersistentPresence - = linkedCusaxProvider.getOperationSet( - OperationSetPersistentPresence.class); - - if(opSetPersistentPresence != null) - { - // will strip the @server-address part, as the regular expression - // will match it - int index = peerAddress.indexOf("@"); - String peerUserID = - (index > -1) ? peerAddress.substring(0, index) : peerAddress; - - // searches for the whole number/username or with the @serverpart - String peerUserIDQ = Pattern.quote(peerUserID); - - Pattern pattern = Pattern.compile( - "^(" + peerUserIDQ + "|" + peerUserIDQ + "@.*)$"); - - return findContactByPeer( - peerUserID, - pattern, - opSetPersistentPresence.getServerStoredContactListRoot(), - linkedCusaxProvider.getOperationSet( - OperationSetCusaxUtils.class)); - } - } - - return null; - } - - /** - * Finds a matching cusax contact. - * @param peerUserID the userID of the call peer to search for - * @param searchPattern the pattern (userID | userID@...) - * @param parent the parent group of the groups and contacts to search in - * @param cusaxOpSet the opset of the provider which will be used to match - * contact's details to peer userID (stored numbers). - * @return a cusax matching contac - */ - private static Contact findContactByPeer( - String peerUserID, - Pattern searchPattern, - ContactGroup parent, - OperationSetCusaxUtils cusaxOpSet) - { - Iterator<Contact> contactIterator = parent.contacts(); - while(contactIterator.hasNext()) - { - Contact contact = contactIterator.next(); - - if(searchPattern.matcher(contact.getAddress()).find() - || cusaxOpSet.doesDetailBelong(contact, peerUserID)) - { - return contact; - } - } - - Iterator<ContactGroup> groupsIterator = parent.subgroups(); - while(groupsIterator.hasNext()) - { - ContactGroup gr = groupsIterator.next(); - Contact contact = findContactByPeer( - peerUserID, searchPattern, gr, cusaxOpSet); - if(contact != null) - return contact; - } - - return null; - } - - /** - * Returns the image for the given contact. - * - * @param contact the <tt>Contact</tt>, which image we're looking for - * @return the array of bytes representing the image for the given contact - * or null if such image doesn't exist - */ - private static byte[] getContactImage(Contact contact) - { - MetaContact metaContact = GuiActivator.getContactListService() - .findMetaContactByContact(contact); - - if (metaContact != null) - return metaContact.getAvatar(); - - return null; - } - - /** - * Returns the image for the given <tt>alternativePeerAddress</tt> by - * checking the if the <tt>callPeer</tt> exists as a detail in the given - * <tt>cusaxProvider</tt>. - * - * @param callPeer the <tt>CallPeer</tt> to check in the cusax provider - * details - * @param cusaxProvider the linked cusax <tt>ProtocolProviderService</tt> - * @param alternativePeerAddress the alternative peer address to obtain the - * image from - * @return the protocol <tt>Contact</tt> corresponding to the given - * <tt>alternativePeerAddress</tt> - */ - private static Contact getPeerContact( CallPeer callPeer, - ProtocolProviderService cusaxProvider, - String alternativePeerAddress) - { - OperationSetPresence presenceOpSet - = cusaxProvider.getOperationSet(OperationSetPresence.class); - - if (presenceOpSet == null) - return null; - - Contact contact = presenceOpSet.findContactByID(alternativePeerAddress); - - if (contact == null) - return null; - - OperationSetCusaxUtils cusaxOpSet - = cusaxProvider.getOperationSet(OperationSetCusaxUtils.class); - - if (cusaxOpSet != null && cusaxOpSet.doesDetailBelong( - contact, callPeer.getAddress())) - return contact; - - return null; - } - - /** - * Returns the metacontact for the given <tt>CallPeer</tt> by - * checking the if the <tt>callPeer</tt> contact exists, if not checks the - * contacts in our contact list that are provided by cusax enabled - * providers. - * - * @param peer the <tt>CallPeer</tt> to check in contact details - * @return the <tt>MetaContact</tt> corresponding to the given - * <tt>peer</tt>. - */ - public static MetaContact getPeerMetaContact(CallPeer peer) - { - if(peer == null) - return null; - - if(peer.getContact() != null) - return GuiActivator.getContactListService() - .findMetaContactByContact(peer.getContact()); - - // We try to find the <tt>UIContact</tt>, to which the call was - // created if this was an outgoing call. - UIContactImpl uiContact - = CallManager.getCallUIContact(peer.getCall()); - - if (uiContact != null) - { - if(uiContact.getDescriptor() instanceof MetaContact) - { - return (MetaContact)uiContact.getDescriptor(); - } - else if(uiContact.getDescriptor() instanceof SourceContact) - { - // if it is a source contact check for matching cusax contact - Contact contact = getPeerCusaxContact(peer.getAddress(), - (SourceContact)uiContact.getDescriptor()); - if(contact != null) - return GuiActivator.getContactListService() - .findMetaContactByContact(contact); - } - } - - String imppAddress = peer.getAlternativeIMPPAddress(); - - if (!StringUtils.isNullOrEmpty(imppAddress)) - { - int protocolPartIndex = imppAddress.indexOf(":"); - - imppAddress = (protocolPartIndex >= 0) - ? imppAddress.substring(protocolPartIndex + 1) - : imppAddress; - - Collection<ProtocolProviderService> cusaxProviders - = AccountUtils.getRegisteredProviders( - OperationSetCusaxUtils.class); - - if (cusaxProviders != null && cusaxProviders.size() > 0) - { - Contact contact = getPeerContact( - peer, - cusaxProviders.iterator().next(), - imppAddress); - - return GuiActivator.getContactListService() - .findMetaContactByContact(contact); - } - else - { - return getPeerMetaContact(peer, imppAddress); - } - } - - return null; - } - - /** - * Returns the image for the given <tt>alternativePeerAddress</tt> by - * checking the if the <tt>callPeer</tt> exists as a detail in one of the - * contacts in our contact list. - * - * @param callPeer the <tt>CallPeer</tt> to check in contact details - * @param alternativePeerAddress the alternative peer address to obtain the - * image from - * @return the <tt>MetaContact</tt> corresponding to the given - * <tt>alternativePeerAddress</tt> - */ - private static MetaContact getPeerMetaContact( - CallPeer callPeer, - String alternativePeerAddress) - { - Iterator<MetaContact> metaContacts - = GuiActivator.getContactListService() - .findAllMetaContactsForAddress(alternativePeerAddress); - - while (metaContacts.hasNext()) - { - MetaContact metaContact = metaContacts.next(); - - UIPhoneUtil phoneUtil - = UIPhoneUtil.getPhoneUtil(metaContact); - - List<UIContactDetail> additionalNumbers - = phoneUtil.getAdditionalNumbers(); - - if (additionalNumbers == null || additionalNumbers.size() > 0) - continue; - - Iterator<UIContactDetail> numbersIter - = additionalNumbers.iterator(); - while (numbersIter.hasNext()) - { - if (numbersIter.next().getAddress() - .equals(callPeer.getAddress())) - return metaContact; - } - } - - return null; - } - - /** - * Opens a call transfer dialog to transfer the given <tt>peer</tt>. - * @param peer the <tt>CallPeer</tt> to transfer - */ - public static void openCallTransferDialog(CallPeer peer) - { - final TransferCallDialog dialog - = new TransferCallDialog(peer); - - final Call call = peer.getCall(); - - /* - * Transferring a call works only when the call is in progress - * so close the dialog (if it's not already closed, of course) - * once the dialog ends. - */ - CallChangeListener callChangeListener = new CallChangeAdapter() - { - /* - * Overrides - * CallChangeAdapter#callStateChanged(CallChangeEvent). - */ - @Override - public void callStateChanged(CallChangeEvent evt) - { - // we are interested only in CALL_STATE_CHANGEs - if(!evt.getEventType().equals( - CallChangeEvent.CALL_STATE_CHANGE)) - return; - - if (!CallState.CALL_IN_PROGRESS.equals(call - .getCallState())) - { - dialog.setVisible(false); - dialog.dispose(); - } - } - }; - call.addCallChangeListener(callChangeListener); - try - { - dialog.pack(); - dialog.setVisible(true); - } - finally - { - call.removeCallChangeListener(callChangeListener); - } - } - - /** - * Checks whether the <tt>callPeer</tt> supports setting video - * quality presets. If quality controls is null, its not supported. - * @param callPeer the peer, which video quality we're checking - * @return whether call peer supports setting quality preset. - */ - public static boolean isVideoQualityPresetSupported(CallPeer callPeer) - { - ProtocolProviderService provider = callPeer.getProtocolProvider(); - OperationSetVideoTelephony videoOpSet - = provider.getOperationSet(OperationSetVideoTelephony.class); - - if (videoOpSet == null) - return false; - - return videoOpSet.getQualityControl(callPeer) != null; - } - - /** - * Sets the given quality preset for the video of the given call peer. - * - * @param callPeer the peer, which video quality we're setting - * @param qualityPreset the new quality settings - */ - public static void setVideoQualityPreset(final CallPeer callPeer, - final QualityPreset qualityPreset) - { - ProtocolProviderService provider = callPeer.getProtocolProvider(); - final OperationSetVideoTelephony videoOpSet - = provider.getOperationSet(OperationSetVideoTelephony.class); - - if (videoOpSet == null) - return; - - final QualityControl qualityControl = - videoOpSet.getQualityControl(callPeer); - - if (qualityControl != null) - { - new Thread(new Runnable() - { - public void run() - { - try - { - qualityControl.setPreferredRemoteSendMaxPreset( - qualityPreset); - } - catch(org.jitsi.service.protocol.OperationFailedException e) - { - logger.info("Unable to change video quality.", e); - - ResourceManagementService resources - = GuiActivator.getResources(); - - new ErrorDialog( - null, - resources.getI18NString("service.gui.WARNING"), - resources.getI18NString( - "service.gui.UNABLE_TO_CHANGE_VIDEO_QUALITY"), - e) - .showDialog(); - } - } - }).start(); - } - } - - /** - * Indicates if we have video streams to show in this interface. - * - * @param call the call to check for video streaming - * @return <tt>true</tt> if we have video streams to show in this interface; - * otherwise, <tt>false</tt> - */ - public static boolean isVideoStreaming(Call call) - { - return isVideoStreaming(call.getConference()); - } - - /** - * Indicates if we have video streams to show in this interface. - * - * @param conference the conference we check for video streaming - * @return <tt>true</tt> if we have video streams to show in this interface; - * otherwise, <tt>false</tt> - */ - public static boolean isVideoStreaming(CallConference conference) - { - for (Call call : conference.getCalls()) - { - OperationSetVideoTelephony videoTelephony - = call.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - - if (videoTelephony == null) - continue; - - if (videoTelephony.isLocalVideoStreaming(call)) - return true; - - Iterator<? extends CallPeer> callPeers = call.getCallPeers(); - - while (callPeers.hasNext()) - { - List<Component> remoteVideos - = videoTelephony.getVisualComponents(callPeers.next()); - - if ((remoteVideos != null) && (remoteVideos.size() > 0)) - return true; - } - } - return false; - } - - /** - * Determines whether two specific addresses refer to one and the same - * peer/resource/contact. - * <p> - * <b>Warning</b>: Use the functionality sparingly because it assumes that - * an unspecified service is equal to any service. - * </p> - * - * @param a one of the addresses to be compared - * @param b the other address to be compared to <tt>a</tt> - * @return <tt>true</tt> if <tt>a</tt> and <tt>b</tt> name one and the same - * peer/resource/contact; <tt>false</tt>, otherwise - */ - public static boolean addressesAreEqual(String a, String b) - { - if (a.equals(b)) - return true; - - int aProtocolIndex = a.indexOf(':'); - if(aProtocolIndex != -1) - a = a.substring(aProtocolIndex + 1); - - int bProtocolIndex = b.indexOf(':'); - if(bProtocolIndex != -1) - b = b.substring(bProtocolIndex + 1); - - if (a.equals(b)) - return true; - - int aServiceBegin = a.indexOf('@', aProtocolIndex); - String aUserID; - String aService; - - if (aServiceBegin != -1) - { - aUserID = a.substring(0, aServiceBegin); - ++aServiceBegin; - - int aResourceBegin = a.indexOf('/', aServiceBegin); - if (aResourceBegin != -1) - aService = a.substring(aServiceBegin, aResourceBegin); - else - aService = a.substring(aServiceBegin); - } - else - { - aUserID = a; - aService = null; - } - - int bServiceBegin = b.indexOf('@', bProtocolIndex); - String bUserID; - String bService; - - if (bServiceBegin != -1) - { - bUserID = b.substring(0, bServiceBegin); - ++bServiceBegin; - - int bResourceBegin = b.indexOf('/', bServiceBegin); - if (bResourceBegin != -1) - bService = b.substring(bServiceBegin, bResourceBegin); - else - bService = b.substring(bServiceBegin); - } - else - { - bUserID = b; - bService = null; - } - - boolean userIDsAreEqual; - - if ((aUserID == null) || (aUserID.length() < 1)) - userIDsAreEqual = ((bUserID == null) || (bUserID.length() < 1)); - else - userIDsAreEqual = aUserID.equals(bUserID); - if (!userIDsAreEqual) - return false; - - boolean servicesAreEqual; - - /* - * It's probably a veeery long shot but it's assumed here that an - * unspecified service is equal to any service. Such a case is, for - * example, RegistrarLess SIP. - */ - if (((aService == null) || (aService.length() < 1)) - || ((bService == null) || (bService.length() < 1))) - servicesAreEqual = true; - else - servicesAreEqual = aService.equals(bService); - - return servicesAreEqual; - } - - /** - * Indicates if the given <tt>ConferenceMember</tt> corresponds to the local - * user. - * - * @param conferenceMember the conference member to check - * @return <tt>true</tt> if the given <tt>conferenceMember</tt> is the local - * user, <tt>false</tt> - otherwise - */ - public static boolean isLocalUser(ConferenceMember conferenceMember) - { - String localUserAddress - = conferenceMember.getConferenceFocusCallPeer() - .getProtocolProvider().getAccountID().getAccountAddress(); - - return CallManager.addressesAreEqual( - conferenceMember.getAddress(), localUserAddress); - } - - /** - * Adds a missed call notification. - * - * @param peerName the name of the peer - * @param callTime the time of the call - */ - private static void addMissedCallNotification(String peerName, long callTime) - { - if (missedCallGroup == null) - { - missedCallGroup - = new UINotificationGroup( - "MissedCalls", - GuiActivator.getResources().getI18NString( - "service.gui.MISSED_CALLS_TOOL_TIP")); - } - - UINotificationManager.addNotification( - new UINotification(peerName, callTime, missedCallGroup)); - } - - /** - * Returns of supported/enabled list of audio formats for a provider. - * @param device the <tt>MediaDevice</tt>, which audio formats we're - * looking for - * @param protocolProvider the provider to check. - * @return list of supported/enabled auido formats or empty list - * otherwise. - */ - private static List<MediaFormat> getAudioFormats( - MediaDevice device, - ProtocolProviderService protocolProvider) - { - List<MediaFormat> res = new ArrayList<MediaFormat>(); - - Map<String, String> accountProperties - = protocolProvider.getAccountID().getAccountProperties(); - String overrideEncodings - = accountProperties.get(ProtocolProviderFactory.OVERRIDE_ENCODINGS); - - List<MediaFormat> formats; - if(Boolean.parseBoolean(overrideEncodings)) - { - /* - * The account properties associated with account - * override the global EncodingConfiguration. - */ - EncodingConfiguration encodingConfiguration - = ProtocolMediaActivator.getMediaService() - .createEmptyEncodingConfiguration(); - - encodingConfiguration.loadProperties( - accountProperties, - ProtocolProviderFactory.ENCODING_PROP_PREFIX); - - formats = device.getSupportedFormats( - null, null, encodingConfiguration); - } - else /* The global EncodingConfiguration is in effect. */ - { - formats = device.getSupportedFormats(); - } - - // skip the special telephony event - for(MediaFormat format : formats) - { - if(!format.getEncoding().equals(Constants.TELEPHONE_EVENT)) - res.add(format); - } - - return res; - } - - /** - * Creates a new (audio-only or video) <tt>Call</tt> to a contact specified - * as a <tt>Contact</tt> instance or a <tt>String</tt> contact - * address/identifier. - */ - private static class CreateCallThread - extends Thread - { - /** - * The contact to call. - */ - private final Contact contact; - - /** - * The specific contact resource to call. - */ - private final ContactResource contactResource; - - /** - * The <tt>UIContactImpl</tt> we're calling. - */ - private final UIContactImpl uiContact; - - /** - * The protocol provider through which the call goes. - */ - private final ProtocolProviderService protocolProvider; - - /** - * The string to call. - */ - private final String stringContact; - - /** - * The description of a conference to call, if any. - */ - private final ConferenceDescription conferenceDescription; - - /** - * The indicator which determines whether this instance is to create a - * new video (as opposed to audio-only) <tt>Call</tt>. - */ - private final boolean video; - - /** - * The chat room associated with the call. - */ - private final ChatRoom chatRoom; - - /** - * Creates an instance of <tt>CreateCallThread</tt>. - * - * @param protocolProvider the protocol provider through which the call - * is going. - * @param contact the contact to call - * @param contactResource the specific <tt>ContactResource</tt> we're - * calling - * @param video indicates if this is a video call - */ - public CreateCallThread( - ProtocolProviderService protocolProvider, - Contact contact, - ContactResource contactResource, - boolean video) - { - this(protocolProvider, contact, contactResource, null, null, null, - null, video); - } - - /** - * Creates an instance of <tt>CreateCallThread</tt>. - * - * @param protocolProvider the protocol provider through which the call - * is going. - * @param contact the contact to call - * @param video indicates if this is a video call - */ - public CreateCallThread( - ProtocolProviderService protocolProvider, - String contact, - boolean video) - { - this(protocolProvider, null, null, null, contact, null, null, video); - } - - /** - * Initializes a new <tt>CreateCallThread</tt> instance which is to - * create a new <tt>Call</tt> to a conference specified via a - * <tt>ConferenceDescription</tt>. - * @param protocolProvider the <tt>ProtocolProviderService</tt> which is - * to perform the establishment of the new <tt>Call</tt>. - * @param conferenceDescription the description of the conference to - * call. - * @param chatRoom the chat room associated with the call. - */ - public CreateCallThread( - ProtocolProviderService protocolProvider, - ConferenceDescription conferenceDescription, - ChatRoom chatRoom) - { - this(protocolProvider, null, null, null, null, - conferenceDescription, chatRoom, - false /* video */); - } - - /** - * Initializes a new <tt>CreateCallThread</tt> instance which is to - * create a new <tt>Call</tt> to a contact specified either as a - * <tt>Contact</tt> instance or as a <tt>String</tt> contact - * address/identifier. - * <p> - * The constructor is private because it relies on its arguments being - * validated prior to its invocation. - * </p> - * - * @param protocolProvider the <tt>ProtocolProviderService</tt> which is - * to perform the establishment of the new <tt>Call</tt> - * @param contact the contact to call - * @param contactResource the specific contact resource to call - * @param uiContact the ui contact we're calling - * @param stringContact the string to call - * @param video <tt>true</tt> if this instance is to create a new video - * (as opposed to audio-only) <tt>Call</tt> - * @param conferenceDescription the description of a conference to call - * @param chatRoom the chat room associated with the call. - */ - public CreateCallThread( - ProtocolProviderService protocolProvider, - Contact contact, - ContactResource contactResource, - UIContactImpl uiContact, - String stringContact, - ConferenceDescription conferenceDescription, - ChatRoom chatRoom, - boolean video) - { - this.protocolProvider = protocolProvider; - this.contact = contact; - this.contactResource = contactResource; - this.uiContact = uiContact; - this.stringContact = stringContact; - this.video = video; - this.conferenceDescription = conferenceDescription; - this.chatRoom = chatRoom; - } - - @Override - public void run() - { - if(!video) - { - // if it is not video let's check for available audio codecs - // and available audio devices - MediaService mediaService = GuiActivator.getMediaService(); - MediaDevice dev = mediaService.getDefaultDevice( - MediaType.AUDIO, MediaUseCase.CALL); - - List<MediaFormat> formats - = getAudioFormats(dev, protocolProvider); - - String errorMsg = null; - - if(!dev.getDirection().allowsSending()) - errorMsg = GuiActivator.getResources().getI18NString( - "service.gui.CALL_NO_AUDIO_DEVICE"); - else if(formats.isEmpty()) - { - errorMsg = GuiActivator.getResources().getI18NString( - "service.gui.CALL_NO_AUDIO_CODEC"); - } - - if(errorMsg != null) - { - if(GuiActivator.getUIService() - .getPopupDialog().showConfirmPopupDialog( - errorMsg + " " + - GuiActivator.getResources().getI18NString( - "service.gui.CALL_NO_DEVICE_CODECS_Q"), - GuiActivator.getResources().getI18NString( - "service.gui.CALL"), - PopupDialog.YES_NO_OPTION, - PopupDialog.QUESTION_MESSAGE) - == PopupDialog.NO_OPTION) - { - return; - } - } - } - - Contact contact = this.contact; - String stringContact = this.stringContact; - - if (ConfigurationUtils.isNormalizePhoneNumber()) - { - if (contact != null) - { - stringContact = contact.getAddress(); - contact = null; - } - - if (stringContact != null) - { - stringContact - = PhoneNumberI18nService.normalize(stringContact); - } - } - - try - { - if (conferenceDescription != null) - { - internalCall( protocolProvider, - conferenceDescription, - chatRoom); - } - else - { - if (video) - { - internalCallVideo( protocolProvider, - contact, - uiContact, - stringContact); - } - else - { - internalCall( protocolProvider, - contact, - stringContact, - contactResource, - uiContact); - } - } - } - catch (Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - - logger.error("The call could not be created: ", t); - - String message = GuiActivator.getResources() - .getI18NString("servoce.gui.CREATE_CALL_FAILED"); - - if (t.getMessage() != null) - message += " " + t.getMessage(); - - new ErrorDialog( - null, - GuiActivator.getResources().getI18NString( - "service.gui.ERROR"), - message, - t) - .showDialog(); - } - } - } - - /** - * Creates a video call through the given <tt>protocolProvider</tt>. - * - * @param protocolProvider the <tt>ProtocolProviderService</tt> through - * which to make the call - * @param contact the <tt>Contact</tt> to call - * @param uiContact the <tt>UIContactImpl</tt> we're calling - * @param stringContact the contact string to call - * - * @throws OperationFailedException thrown if the call operation fails - * @throws ParseException thrown if the contact string is malformated - */ - private static void internalCallVideo( - ProtocolProviderService protocolProvider, - Contact contact, - UIContactImpl uiContact, - String stringContact) - throws OperationFailedException, - ParseException - { - OperationSetVideoTelephony telephony - = protocolProvider.getOperationSet( - OperationSetVideoTelephony.class); - - Call createdCall = null; - if (telephony != null) - { - if (contact != null) - { - createdCall = telephony.createVideoCall(contact); - } - else if (stringContact != null) - createdCall = telephony.createVideoCall(stringContact); - } - - if (uiContact != null && createdCall != null) - addUIContactCall(uiContact, createdCall); - } - - /** - * Creates a call through the given <tt>protocolProvider</tt>. - * - * @param protocolProvider the <tt>ProtocolProviderService</tt> through - * which to make the call - * @param contact the <tt>Contact</tt> to call - * @param stringContact the contact string to call - * @param contactResource the specific <tt>ContactResource</tt> to call - * @param uiContact the <tt>UIContactImpl</tt> we're calling - * - * @throws OperationFailedException thrown if the call operation fails - * @throws ParseException thrown if the contact string is malformated - */ - private static void internalCall( - ProtocolProviderService protocolProvider, - Contact contact, - String stringContact, - ContactResource contactResource, - UIContactImpl uiContact) - throws OperationFailedException, - ParseException - { - OperationSetBasicTelephony<?> telephony - = protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - - OperationSetResourceAwareTelephony resourceTelephony - = protocolProvider.getOperationSet( - OperationSetResourceAwareTelephony.class); - - Call createdCall = null; - - if (resourceTelephony != null && contactResource != null) - { - if (contact != null) - createdCall - = resourceTelephony.createCall(contact, contactResource); - else if (!StringUtils.isNullOrEmpty(stringContact)) - createdCall = resourceTelephony.createCall( - stringContact, contactResource.getResourceName()); - } - else if (telephony != null) - { - if (contact != null) - { - createdCall = telephony.createCall(contact); - } - else if (!StringUtils.isNullOrEmpty(stringContact)) - createdCall = telephony.createCall(stringContact); - } - - if (uiContact != null && createdCall != null) - addUIContactCall(uiContact, createdCall); - } - - /** - * Creates a call through the given <tt>protocolProvider</tt>. - * - * @param protocolProvider the <tt>ProtocolProviderService</tt> through - * which to make the call - * @param conferenceDescription the description of the conference to call - * @param chatRoom the chat room associated with the call. - */ - private static void internalCall(ProtocolProviderService protocolProvider, - ConferenceDescription conferenceDescription, - ChatRoom chatRoom) - throws OperationFailedException - { - OperationSetBasicTelephony<?> telephony - = protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - - if (telephony != null) - { - telephony.createCall(conferenceDescription, chatRoom); - } - } - - /** - * Returns the <tt>MetaContact</tt>, to which the given <tt>Call</tt> - * was initially created. - * - * @param call the <tt>Call</tt>, which corresponding <tt>MetaContact</tt> - * we're looking for - * @return the <tt>UIContactImpl</tt>, to which the given <tt>Call</tt> - * was initially created - */ - public static UIContactImpl getCallUIContact(Call call) - { - if (uiContactCalls != null) - return uiContactCalls.get(call); - return null; - } - - /** - * Adds a call for a <tt>metaContact</tt>. - * - * @param uiContact the <tt>UIContact</tt> corresponding to the call - * @param call the <tt>Call</tt> corresponding to the <tt>MetaContact</tt> - */ - private static void addUIContactCall( UIContactImpl uiContact, - Call call) - { - if (uiContactCalls == null) - uiContactCalls = new WeakHashMap<Call, UIContactImpl>(); - - uiContactCalls.put(call, uiContact); - } - - /** - * Creates a desktop sharing session with the given Contact or a given - * String. - */ - private static class CreateDesktopSharingThread - extends Thread - { - /** - * The string contact to share the desktop with. - */ - private final String stringContact; - - /** - * The protocol provider through which we share our desktop. - */ - private final ProtocolProviderService protocolProvider; - - /** - * The media device corresponding to the screen we would like to share. - */ - private final MediaDevice mediaDevice; - - /** - * The <tt>UIContactImpl</tt> we're calling. - */ - private final UIContactImpl uiContact; - - /** - * Creates a desktop sharing session thread. - * - * @param protocolProvider protocol provider through which we share our - * desktop - * @param contact the contact to share the desktop with - * @param uiContact the <tt>UIContact</tt>, which initiated the desktop - * sharing session - * @param mediaDevice the media device corresponding to the screen we - * would like to share - */ - public CreateDesktopSharingThread( - ProtocolProviderService protocolProvider, - String contact, - UIContactImpl uiContact, - MediaDevice mediaDevice) - { - this.protocolProvider = protocolProvider; - this.stringContact = contact; - this.uiContact = uiContact; - this.mediaDevice = mediaDevice; - } - - @Override - public void run() - { - OperationSetDesktopSharingServer desktopSharingOpSet - = protocolProvider.getOperationSet( - OperationSetDesktopSharingServer.class); - - /* - * XXX If we are here and we just discover that - * OperationSetDesktopSharingServer is not supported, then we're - * already in trouble - we've already started a whole new thread - * just to check that a reference is null. - */ - if (desktopSharingOpSet == null) - return; - - Throwable exception = null; - - Call createdCall = null; - try - { - if (mediaDevice != null) - { - createdCall = desktopSharingOpSet.createVideoCall( - stringContact, - mediaDevice); - } - else - createdCall - = desktopSharingOpSet.createVideoCall(stringContact); - } - catch (OperationFailedException e) - { - exception = e; - } - catch (ParseException e) - { - exception = e; - } - if (exception != null) - { - logger.error("The call could not be created: ", exception); - - new ErrorDialog( - null, - GuiActivator.getResources().getI18NString( - "service.gui.ERROR"), - exception.getMessage(), - ErrorDialog.ERROR) - .showDialog(); - } - - if (uiContact != null && createdCall != null) - addUIContactCall(uiContact, createdCall); - } - } - - /** - * Answers to all <tt>CallPeer</tt>s associated with a specific - * <tt>Call</tt> and, optionally, does that in a telephony conference with - * an existing <tt>Call</tt>. - */ - private static class AnswerCallThread - extends Thread - { - /** - * The <tt>Call</tt> which is to be answered. - */ - private final Call call; - - /** - * The existing <tt>Call</tt>, if any, which represents a telephony - * conference in which {@link #call} is to be answered. - */ - private final Call existingCall; - - /** - * The indicator which determines whether this instance is to answer - * {@link #call} with video. - */ - private final boolean video; - - public AnswerCallThread(Call call, Call existingCall, boolean video) - { - this.call = call; - this.existingCall = existingCall; - this.video = video; - } - - @Override - public void run() - { - if (existingCall != null) - call.setConference(existingCall.getConference()); - - ProtocolProviderService pps = call.getProtocolProvider(); - Iterator<? extends CallPeer> peers = call.getCallPeers(); - - while (peers.hasNext()) - { - CallPeer peer = peers.next(); - - if (video) - { - OperationSetVideoTelephony telephony - = pps.getOperationSet(OperationSetVideoTelephony.class); - - try - { - telephony.answerVideoCallPeer(peer); - } - catch (OperationFailedException ofe) - { - logger.error( - "Could not answer " - + peer - + " with video" - + " because of the following exception: " - + ofe); - } - } - else - { - OperationSetBasicTelephony<?> telephony - = pps.getOperationSet(OperationSetBasicTelephony.class); - - try - { - telephony.answerCallPeer(peer); - } - catch (OperationFailedException ofe) - { - logger.error( - "Could not answer " - + peer - + " because of the following exception: ", - ofe); - } - } - } - } - } - - /** - * Invites a list of callees to a conference <tt>Call</tt>. If the specified - * <tt>Call</tt> is <tt>null</tt>, creates a brand new telephony conference. - */ - private static class InviteToConferenceCallThread - extends Thread - { - /** - * The addresses of the callees to be invited into the telephony - * conference to be organized by this instance. For further details, - * refer to the documentation on the <tt>callees</tt> parameter of the - * respective <tt>InviteToConferenceCallThread</tt> constructor. - */ - private final Map<ProtocolProviderService, List<String>> - callees; - - /** - * The <tt>Call</tt>, if any, into the telephony conference of which - * {@link #callees} are to be invited. If non-<tt>null</tt>, its - * <tt>CallConference</tt> state will be shared with all <tt>Call</tt>s - * established by this instance for the purposes of having the - * <tt>callees</tt> into the same telephony conference. - */ - private final Call call; - - /** - * Initializes a new <tt>InviteToConferenceCallThread</tt> instance - * which is to invite a list of callees to a conference <tt>Call</tt>. - * If the specified <tt>call</tt> is <tt>null</tt>, creates a brand new - * telephony conference. - * - * @param callees the addresses of the callees to be invited into a - * telephony conference. The addresses are provided in multiple - * <tt>List<String></tt>s. Each such list of addresses is mapped - * by the <tt>ProtocolProviderService</tt> through which they are to be - * invited into the telephony conference. If there are multiple - * <tt>ProtocolProviderService</tt>s in the specified <tt>Map</tt>, the - * resulting telephony conference is known by the name - * "cross-protocol". It is also allowed to have a list of - * addresses mapped to <tt>null</tt> which means that the new instance - * will automatically choose a <tt>ProtocolProviderService</tt> to - * invite the respective callees into the telephony conference. - * @param call the <tt>Call</tt> to invite the specified - * <tt>callees</tt> into. If <tt>null</tt>, this instance will create a - * brand new telephony conference. Technically, a <tt>Call</tt> instance - * is protocol/account-specific and it is possible to have - * cross-protocol/account telephony conferences. That's why the - * specified <tt>callees</tt> are invited into one and the same - * <tt>CallConference</tt>: the one in which the specified <tt>call</tt> - * is participating or a new one if <tt>call</tt> is <tt>null</tt>. Of - * course, an attempt is made to have all callees from one and the same - * protocol/account into one <tt>Call</tt> instance. - */ - public InviteToConferenceCallThread( - Map<ProtocolProviderService, List<String>> callees, - Call call) - { - this.callees = callees; - this.call = call; - } - - /** - * Invites {@link #callees} into a telephony conference which is - * optionally specified by {@link #call}. - */ - @Override - public void run() - { - CallConference conference - = (call == null) ? null : call.getConference(); - - for(Map.Entry<ProtocolProviderService, List<String>> entry - : callees.entrySet()) - { - ProtocolProviderService pps = entry.getKey(); - - /* - * We'd like to allow specifying callees without specifying an - * associated ProtocolProviderService. - */ - if (pps != null) - { - OperationSetBasicTelephony<?> basicTelephony - = pps.getOperationSet(OperationSetBasicTelephony.class); - - if(basicTelephony == null) - continue; - } - - List<String> contactList = entry.getValue(); - String[] contactArray - = contactList.toArray(new String[contactList.size()]); - - if (ConfigurationUtils.isNormalizePhoneNumber()) - normalizePhoneNumbers(contactArray); - - /* Try to have a single Call per ProtocolProviderService. */ - Call ppsCall; - - if ((call != null) && call.getProtocolProvider().equals(pps)) - ppsCall = call; - else - { - ppsCall = null; - if (conference != null) - { - List<Call> conferenceCalls = conference.getCalls(); - - if (pps == null) - { - /* - * We'd like to allow specifying callees without - * specifying an associated ProtocolProviderService. - * The simplest approach is to just choose the first - * ProtocolProviderService involved in the telephony - * conference. - */ - if (call == null) - { - if (!conferenceCalls.isEmpty()) - { - ppsCall = conferenceCalls.get(0); - pps = ppsCall.getProtocolProvider(); - } - } - else - { - ppsCall = call; - pps = ppsCall.getProtocolProvider(); - } - } - else - { - for (Call conferenceCall : conferenceCalls) - { - if (pps.equals( - conferenceCall.getProtocolProvider())) - { - ppsCall = conferenceCall; - break; - } - } - } - } - } - - OperationSetTelephonyConferencing telephonyConferencing - = pps.getOperationSet( - OperationSetTelephonyConferencing.class); - - try - { - if (ppsCall == null) - { - ppsCall - = telephonyConferencing.createConfCall( - contactArray, - conference); - if (conference == null) - conference = ppsCall.getConference(); - } - else - { - for (String contact : contactArray) - { - telephonyConferencing.inviteCalleeToCall( - contact, - ppsCall); - } - } - } - catch(Exception e) - { - logger.error( - "Failed to invite callees: " - + Arrays.toString(contactArray), - e); - new ErrorDialog( - null, - GuiActivator.getResources().getI18NString( - "service.gui.ERROR"), - e.getMessage(), - ErrorDialog.ERROR) - .showDialog(); - } - } - } - } - - /** - * Invites a list of callees to a specific conference <tt>Call</tt>. If the - * specified <tt>Call</tt> is <tt>null</tt>, creates a brand new telephony - * conference. - */ - private static class InviteToConferenceBridgeThread - extends Thread - { - private final ProtocolProviderService callProvider; - - private final String[] callees; - - private final Call call; - - public InviteToConferenceBridgeThread( - ProtocolProviderService callProvider, - String[] callees, - Call call) - { - this.callProvider = callProvider; - this.callees = callees; - this.call = call; - } - - @Override - public void run() - { - OperationSetVideoBridge opSetVideoBridge - = callProvider.getOperationSet( - OperationSetVideoBridge.class); - - // Normally if this method is called then this should not happen - // but we check in order to be sure to be able to proceed. - if (opSetVideoBridge == null || !opSetVideoBridge.isActive()) - return; - - if (ConfigurationUtils.isNormalizePhoneNumber()) - normalizePhoneNumbers(callees); - - try - { - if (call == null) - { - opSetVideoBridge.createConfCall(callees); - } - else - { - for (String contact : callees) - opSetVideoBridge.inviteCalleeToCall(contact, call); - } - } - catch(Exception e) - { - logger.error( - "Failed to invite callees: " - + Arrays.toString(callees), - e); - new ErrorDialog( - null, - GuiActivator.getResources().getI18NString( - "service.gui.ERROR"), - e.getMessage(), - e) - .showDialog(); - } - } - } - - /** - * Hangs up a specific <tt>Call</tt> (i.e. all <tt>CallPeer</tt>s associated - * with a <tt>Call</tt>), <tt>CallConference</tt> (i.e. all <tt>Call</tt>s - * participating in a <tt>CallConference</tt>), or <tt>CallPeer</tt>. - */ - private static class HangupCallThread - extends Thread - { - private final Call call; - - private final CallConference conference; - - private final CallPeer peer; - - /** - * Initializes a new <tt>HangupCallThread</tt> instance which is to hang - * up a specific <tt>Call</tt> i.e. all <tt>CallPeer</tt>s associated - * with the <tt>Call</tt>. - * - * @param call the <tt>Call</tt> whose associated <tt>CallPeer</tt>s are - * to be hanged up - */ - public HangupCallThread(Call call) - { - this(call, null, null); - } - - /** - * Initializes a new <tt>HangupCallThread</tt> instance which is to hang - * up a specific <tt>CallConference</tt> i.e. all <tt>Call</tt>s - * participating in the <tt>CallConference</tt>. - * - * @param conference the <tt>CallConference</tt> whose participating - * <tt>Call</tt>s re to be hanged up - */ - public HangupCallThread(CallConference conference) - { - this(null, conference, null); - } - - /** - * Initializes a new <tt>HangupCallThread</tt> instance which is to hang - * up a specific <tt>CallPeer</tt>. - * - * @param peer the <tt>CallPeer</tt> to hang up - */ - public HangupCallThread(CallPeer peer) - { - this(null, null, peer); - } - - /** - * Initializes a new <tt>HangupCallThread</tt> instance which is to hang - * up a specific <tt>Call</tt>, <tt>CallConference</tt>, or - * <tt>CallPeer</tt>. - * - * @param call the <tt>Call</tt> whose associated <tt>CallPeer</tt>s are - * to be hanged up - * @param conference the <tt>CallConference</tt> whose participating - * <tt>Call</tt>s re to be hanged up - * @param peer the <tt>CallPeer</tt> to hang up - */ - private HangupCallThread( - Call call, - CallConference conference, - CallPeer peer) - { - this.call = call; - this.conference = conference; - this.peer = peer; - } - - @Override - public void run() - { - /* - * There is only an OperationSet which hangs up a CallPeer at a time - * so prepare a list of all CallPeers to be hanged up. - */ - Set<CallPeer> peers = new HashSet<CallPeer>(); - - if (call != null) - { - Iterator<? extends CallPeer> peerIter = call.getCallPeers(); - - while (peerIter.hasNext()) - peers.add(peerIter.next()); - } - - if (conference != null) - peers.addAll(conference.getCallPeers()); - - if (peer != null) - peers.add(peer); - - for (CallPeer peer : peers) - { - OperationSetBasicTelephony<?> basicTelephony - = peer.getProtocolProvider().getOperationSet( - OperationSetBasicTelephony.class); - - try - { - basicTelephony.hangupCallPeer(peer); - } - catch (OperationFailedException ofe) - { - logger.error("Could not hang up: " + peer, ofe); - } - } - } - } - - /** - * Creates the enable local video call thread. - */ - private static class EnableLocalVideoThread - extends Thread - { - private final Call call; - - private final boolean enable; - - /** - * Creates the enable local video call thread. - * - * @param call the call, for which to enable/disable - * @param enable - */ - public EnableLocalVideoThread(Call call, boolean enable) - { - this.call = call; - this.enable = enable; - } - - @Override - public void run() - { - OperationSetVideoTelephony videoTelephony - = call.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - boolean enableSucceeded = false; - - if (videoTelephony != null) - { - // First make sure the desktop sharing is disabled. - if (enable && isDesktopSharingEnabled(call)) - { - JFrame frame = DesktopSharingFrame.getFrameForCall(call); - - if (frame != null) - frame.dispose(); - } - - try - { - videoTelephony.setLocalVideoAllowed(call, enable); - enableSucceeded = true; - } - catch (OperationFailedException ex) - { - logger.error( - "Failed to toggle the streaming of local video.", - ex); - - ResourceManagementService r = GuiActivator.getResources(); - String title - = r.getI18NString( - "service.gui.LOCAL_VIDEO_ERROR_TITLE"); - String message - = r.getI18NString( - "service.gui.LOCAL_VIDEO_ERROR_MESSAGE"); - - GuiActivator.getAlertUIService().showAlertPopup( - title, - message, - ex); - } - } - - // If the operation didn't succeeded for some reason, make sure the - // video button doesn't remain selected. - if (enable && !enableSucceeded) - getActiveCallContainer(call).setVideoButtonSelected(false); - } - } - - /** - * Puts on hold the given <tt>CallPeer</tt>. - */ - private static class PutOnHoldCallPeerThread - extends Thread - { - private final CallPeer callPeer; - - private final boolean isOnHold; - - public PutOnHoldCallPeerThread(CallPeer callPeer, boolean isOnHold) - { - this.callPeer = callPeer; - this.isOnHold = isOnHold; - } - - @Override - public void run() - { - OperationSetBasicTelephony<?> telephony - = callPeer.getProtocolProvider().getOperationSet( - OperationSetBasicTelephony.class); - - try - { - if (isOnHold) - telephony.putOnHold(callPeer); - else - telephony.putOffHold(callPeer); - } - catch (OperationFailedException ex) - { - logger.error( - "Failed to put" - + callPeer.getAddress() - + (isOnHold ? " on hold." : " off hold."), - ex); - } - } - } - - /** - * Merges specific existing <tt>Call</tt>s into a specific telephony - * conference. - */ - private static class MergeExistingCalls - extends Thread - { - /** - * The telephony conference in which {@link #calls} are to be merged. - */ - private final CallConference conference; - - /** - * Second call. - */ - private final Collection<Call> calls; - - /** - * Initializes a new <tt>MergeExistingCalls</tt> instance which is to - * merge specific existing <tt>Call</tt>s into a specific telephony - * conference. - * - * @param conference the telephony conference in which the specified - * <tt>Call</tt>s are to be merged - * @param calls the <tt>Call</tt>s to be merged into the specified - * telephony conference - */ - public MergeExistingCalls( - CallConference conference, - Collection<Call> calls) - { - this.conference = conference; - this.calls = calls; - } - - /** - * Puts off hold the <tt>CallPeer</tt>s of a specific <tt>Call</tt> - * which are locally on hold. - * - * @param call the <tt>Call</tt> which is to have its <tt>CallPeer</tt>s - * put off hold - */ - private void putOffHold(Call call) - { - Iterator<? extends CallPeer> peers = call.getCallPeers(); - OperationSetBasicTelephony<?> telephony - = call.getProtocolProvider().getOperationSet( - OperationSetBasicTelephony.class); - - while (peers.hasNext()) - { - CallPeer callPeer = peers.next(); - boolean putOffHold = true; - - if(callPeer instanceof MediaAwareCallPeer) - { - putOffHold - = ((MediaAwareCallPeer<?,?,?>) callPeer) - .getMediaHandler() - .isLocallyOnHold(); - } - if(putOffHold) - { - try - { - telephony.putOffHold(callPeer); - Thread.sleep(400); - } - catch(Exception ofe) - { - logger.error("Failed to put off hold.", ofe); - } - } - } - } - - @Override - public void run() - { - // conference - for (Call call : conference.getCalls()) - putOffHold(call); - - // calls - if (!calls.isEmpty()) - { - for(Call call : calls) - { - if (conference.containsCall(call)) - continue; - - putOffHold(call); - - /* - * Dispose of the CallPanel associated with the Call which - * is to be merged. - */ - closeCallContainerIfNotNecessary(conference, false); - - call.setConference(conference); - } - } - } - } - - /** - * Shows a warning window to warn the user that she's about to start a - * desktop sharing session. - * - * @return <tt>true</tt> if the user has accepted the desktop sharing - * session; <tt>false</tt>, otherwise - */ - private static boolean showDesktopSharingWarning() - { - Boolean isWarningEnabled - = GuiActivator.getConfigurationService().getBoolean( - desktopSharingWarningProperty, - true); - - if (isWarningEnabled.booleanValue()) - { - ResourceManagementService resources = GuiActivator.getResources(); - MessageDialog warningDialog - = new MessageDialog( - null, - resources.getI18NString("service.gui.WARNING"), - resources.getI18NString( - "service.gui.DESKTOP_SHARING_WARNING"), - true); - - switch (warningDialog.showDialog()) - { - case MessageDialog.OK_RETURN_CODE: - return true; - case MessageDialog.CANCEL_RETURN_CODE: - return false; - case MessageDialog.OK_DONT_ASK_CODE: - GuiActivator.getConfigurationService().setProperty( - desktopSharingWarningProperty, - false); - return true; - } - } - - return true; - } - - /** - * Normalizes the phone numbers (if any) in a list of <tt>String</tt> - * contact addresses or phone numbers. - * - * @param callees the list of contact addresses or phone numbers to be - * normalized - */ - private static void normalizePhoneNumbers(String callees[]) - { - for (int i = 0 ; i < callees.length ; i++) - callees[i] = PhoneNumberI18nService.normalize(callees[i]); - } - - /** - * Throws a <tt>RuntimeException</tt> if the current thread is not the AWT - * event dispatching thread. - */ - public static void assertIsEventDispatchingThread() - { - if (!SwingUtilities.isEventDispatchThread()) - { - throw new RuntimeException( - "The methon can be called only on the AWT event dispatching" - + " thread."); - } - } - - /** - * Finds the <tt>CallPanel</tt>, if any, which depicts a specific - * <tt>CallConference</tt>. - * <p> - * <b>Note</b>: The method can be called only on the AWT event dispatching - * thread. - * </p> - * - * @param conference the <tt>CallConference</tt> to find the depicting - * <tt>CallPanel</tt> of - * @return the <tt>CallPanel</tt> which depicts the specified - * <tt>CallConference</tt> if such a <tt>CallPanel</tt> exists; otherwise, - * <tt>null</tt> - * @throws RuntimeException if the method is not called on the AWT event - * dispatching thread - */ - private static CallPanel findCallPanel(CallConference conference) - { - synchronized (callPanels) - { - return callPanels.get(conference); - } - } - - /** - * Notifies {@link #callPanels} about a specific <tt>CallEvent</tt> received - * by <tt>CallManager</tt> (because they may need to update their UI, for - * example). - * <p> - * <b>Note</b>: The method can be called only on the AWT event dispatching - * thread. - * </p> - * - * @param ev the <tt>CallEvent</tt> received by <tt>CallManager</tt> which - * is to be forwarded to <tt>callPanels</tt> for further - * <tt>CallPanel</tt>-specific handling - * @throws RuntimeException if the method is not called on the AWT event - * dispatching thread - */ - private static void forwardCallEventToCallPanels(CallEvent ev) - { - assertIsEventDispatchingThread(); - - CallPanel[] callPanels; - - synchronized (CallManager.callPanels) - { - Collection<CallPanel> values = CallManager.callPanels.values(); - - callPanels = values.toArray(new CallPanel[values.size()]); - } - - for (CallPanel callPanel : callPanels) - { - try - { - callPanel.onCallEvent(ev); - } - catch (Exception ex) - { - /* - * There is no practical reason while the failure of a CallPanel - * to handle the CallEvent should cause the other CallPanels to - * be left out-of-date. - */ - logger.error("A CallPanel failed to handle a CallEvent", ex); - } - } - } - - /** - * Creates a call for the supplied operation set. - * @param opSetClass the operation set to use to create call. - * @param protocolProviderService the protocol provider - * @param contact the contact address to call - */ - static void createCall(Class<? extends OperationSet> opSetClass, - ProtocolProviderService protocolProviderService, - String contact) - { - createCall(opSetClass, protocolProviderService, contact, null); - } - - /** - * Creates a call for the supplied operation set. - * @param opSetClass the operation set to use to create call. - * @param protocolProviderService the protocol provider - * @param contact the contact address to call - * @param uiContact the <tt>MetaContact</tt> we're calling - */ - static void createCall( - Class<? extends OperationSet> opSetClass, - ProtocolProviderService protocolProviderService, - String contact, - UIContactImpl uiContact) - { - if (opSetClass.equals(OperationSetBasicTelephony.class)) - { - createCall(protocolProviderService, contact, uiContact); - } - else if (opSetClass.equals(OperationSetVideoTelephony.class)) - { - createVideoCall(protocolProviderService, contact, uiContact); - } - else if (opSetClass.equals( - OperationSetDesktopSharingServer.class)) - { - createDesktopSharing( - protocolProviderService, contact, uiContact); - } - } - - /** - * Creates a call for the default contact of the metacontact - * - * @param metaContact the metacontact that will be called. - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param shareRegion if <tt>true</tt> will share a region of the desktop. - */ - public static void call(MetaContact metaContact, - boolean isVideo, - boolean isDesktop, - boolean shareRegion) - { - Contact contact = metaContact - .getDefaultContact(getOperationSetForCall(isVideo, isDesktop)); - - call(contact, isVideo, isDesktop, shareRegion); - } - - /** - * A particular contact has been selected no options to select - * will just call it. - * @param contact the contact to call - * @param contactResource the specific contact resource - * @param isVideo is video enabled - * @param isDesktop is desktop sharing enabled - * @param shareRegion is sharing the whole desktop or just a region. - */ - public static void call(Contact contact, - ContactResource contactResource, - boolean isVideo, - boolean isDesktop, - boolean shareRegion) - { - if(isDesktop) - { - if(shareRegion) - { - createRegionDesktopSharing( - contact.getProtocolProvider(), - contact.getAddress(), - null); - } - else - createDesktopSharing(contact.getProtocolProvider(), - contact.getAddress(), - null); - } - else - { - new CreateCallThread( - contact.getProtocolProvider(), - contact, - contactResource, - isVideo).start(); - } - } - - /** - * Creates a call to the conference described in - * <tt>conferenceDescription</tt> through <tt>protocolProvider</tt> - * - * @param protocolProvider the protocol provider through which to create - * the call - * @param conferenceDescription the description of the conference to call - * @param chatRoom the chat room associated with the call. - */ - public static void call(ProtocolProviderService protocolProvider, - ConferenceDescription conferenceDescription, - ChatRoom chatRoom) - { - new CreateCallThread(protocolProvider, conferenceDescription, chatRoom) - .start(); - } - - /** - * A particular contact has been selected no options to select - * will just call it. - * @param contact the contact to call - * @param isVideo is video enabled - * @param isDesktop is desktop sharing enabled - * @param shareRegion is sharing the whole desktop or just a region. - */ - public static void call(Contact contact, - boolean isVideo, - boolean isDesktop, - boolean shareRegion) - { - if(isDesktop) - { - if(shareRegion) - { - createRegionDesktopSharing( - contact.getProtocolProvider(), - contact.getAddress(), - null); - } - else - createDesktopSharing(contact.getProtocolProvider(), - contact.getAddress(), - null); - } - else - { - new CreateCallThread( contact.getProtocolProvider(), - contact, - null, - isVideo).start(); - } - } - - /** - * Calls a phone showing a dialog to choose a provider. - * @param phone phone to call - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param shareRegion if <tt>true</tt> will share a region of the desktop. - */ - public static void call(final String phone, - boolean isVideo, - boolean isDesktop, - final boolean shareRegion) - { - call(phone, null, isVideo, isDesktop, shareRegion); - } - - /** - * Calls a phone showing a dialog to choose a provider. - * @param phone phone to call - * @param uiContact the <tt>UIContactImpl</tt> we're calling - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param shareRegion if <tt>true</tt> will share a region of the desktop. - */ - public static void call(final String phone, - final UIContactImpl uiContact, - boolean isVideo, - boolean isDesktop, - final boolean shareRegion) - { - List<ProtocolProviderService> providers = - CallManager.getTelephonyProviders(); - - if(providers.size() > 1) - { - ChooseCallAccountDialog chooseAccount = - new ChooseCallAccountDialog( - phone, - getOperationSetForCall(isVideo, isDesktop), - providers) - { - @Override - public void callButtonPressed() - { - if(shareRegion) - { - createRegionDesktopSharing( - getSelectedProvider(), phone, uiContact); - } - else - super.callButtonPressed(); - } - }; - chooseAccount.setUIContact(uiContact); - chooseAccount.setVisible(true); - } - else - { - createCall(providers.get(0), phone, uiContact); - } - } - - /** - * Obtain operation set checking the params. - * @param isVideo if <tt>true</tt> use OperationSetVideoTelephony. - * @param isDesktop if <tt>true</tt> use OperationSetDesktopSharingServer. - * @return the operation set, default is OperationSetBasicTelephony. - */ - private static Class<? extends OperationSet> getOperationSetForCall( - boolean isVideo, boolean isDesktop) - { - if(isVideo) - { - if(isDesktop) - return OperationSetDesktopSharingServer.class; - else - return OperationSetVideoTelephony.class; - } - else - return OperationSetBasicTelephony.class; - } - - /** - * Call any of the supplied details. - * - * @param uiContactDetailList the list with details to choose for calling - * @param uiContact the <tt>UIContactImpl</tt> to check what is enabled, - * available. - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param invoker the invoker component - * @param location the location where this was invoked. - */ - public static void call(List<UIContactDetail> uiContactDetailList, - UIContactImpl uiContact, - boolean isVideo, - boolean isDesktop, - JComponent invoker, - Point location) - { - call(uiContactDetailList, - uiContact, - isVideo, - isDesktop, - invoker, - location, - true); - } - - /** - * Call any of the supplied details. - * - * @param uiContactDetailList the list with details to choose for calling - * @param uiContact the <tt>UIContactImpl</tt> to check what is enabled, - * available. - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param invoker the invoker component - * @param location the location where this was invoked. - * @param usePreferredProvider whether to use the <tt>uiContact</tt> - * preferredProvider if provided. - */ - private static void call(List<UIContactDetail> uiContactDetailList, - UIContactImpl uiContact, - boolean isVideo, - boolean isDesktop, - JComponent invoker, - Point location, - boolean usePreferredProvider) - { - ChooseCallAccountPopupMenu chooseCallAccountPopupMenu = null; - - Class<? extends OperationSet> opsetClass = - getOperationSetForCall(isVideo, isDesktop); - - UIPhoneUtil contactPhoneUtil = null; - if (uiContact != null - && uiContact.getDescriptor() instanceof MetaContact) - contactPhoneUtil = UIPhoneUtil - .getPhoneUtil((MetaContact) uiContact.getDescriptor()); - - if(contactPhoneUtil != null) - { - boolean addAdditionalNumbers = false; - if(!isVideo - || ConfigurationUtils - .isRouteVideoAndDesktopUsingPhoneNumberEnabled()) - { - addAdditionalNumbers = true; - } - else - { - if(isVideo && contactPhoneUtil != null) - { - // lets check is video enabled in additional numbers - addAdditionalNumbers = - contactPhoneUtil.isVideoCallEnabled() ? - isDesktop ? - contactPhoneUtil.isDesktopSharingEnabled() - : true - : false; - } - } - - if(addAdditionalNumbers) - { - uiContactDetailList.addAll( - contactPhoneUtil.getAdditionalNumbers()); - } - } - - if (uiContactDetailList.size() == 1) - { - UIContactDetail detail = uiContactDetailList.get(0); - - ProtocolProviderService preferredProvider = null; - - if(usePreferredProvider) - preferredProvider = - detail.getPreferredProtocolProvider(opsetClass); - - List<ProtocolProviderService> providers = null; - String protocolName = null; - - if (preferredProvider != null) - { - if (preferredProvider.isRegistered()) - { - createCall(opsetClass, - preferredProvider, - detail.getAddress(), - uiContact); - } - - // If we have a provider, but it's not registered we try to - // obtain all registered providers for the same protocol as the - // given preferred provider. - else - { - protocolName = preferredProvider.getProtocolName(); - providers = AccountUtils.getRegisteredProviders(protocolName, - opsetClass); - } - } - // If we don't have a preferred provider we try to obtain a - // preferred protocol name and all registered providers for it. - else - { - protocolName = detail.getPreferredProtocol(opsetClass); - - if (protocolName != null) - providers - = AccountUtils.getRegisteredProviders(protocolName, - opsetClass); - else - providers - = AccountUtils.getRegisteredProviders(opsetClass); - } - - // If our call didn't succeed, try to call through one of the other - // protocol providers obtained above. - if (providers != null) - { - int providersCount = providers.size(); - - if (providersCount <= 0) - { - new ErrorDialog(null, - GuiActivator.getResources().getI18NString( - "service.gui.CALL_FAILED"), - GuiActivator.getResources().getI18NString( - "service.gui.NO_ONLINE_TELEPHONY_ACCOUNT")) - .showDialog(); - } - else if (providersCount == 1) - { - createCall( - opsetClass, - providers.get(0), - detail.getAddress(), - uiContact); - } - else if (providersCount > 1) - { - chooseCallAccountPopupMenu = - new ChooseCallAccountPopupMenu( - invoker, detail.getAddress(), providers, - opsetClass); - } - } - } - else if (uiContactDetailList.size() > 1) - { - chooseCallAccountPopupMenu - = new ChooseCallAccountPopupMenu(invoker, uiContactDetailList, - opsetClass); - } - - // If the choose dialog is created we're going to show it. - if (chooseCallAccountPopupMenu != null) - { - if (uiContact != null) - chooseCallAccountPopupMenu.setUIContact(uiContact); - - chooseCallAccountPopupMenu.showPopupMenu(location.x, location.y); - } - } - - - /** - * Call the ui contact. - * - * @param uiContact the contact to call. - * @param isVideo if <tt>true</tt> will create video call. - * @param isDesktop if <tt>true</tt> will share the desktop. - * @param invoker the invoker component - * @param location the location where this was invoked. - */ - public static void call(UIContact uiContact, - boolean isVideo, - boolean isDesktop, - JComponent invoker, - Point location) - { - UIContactImpl uiContactImpl = null; - if(uiContact instanceof UIContactImpl) - { - uiContactImpl = (UIContactImpl) uiContact; - } - - List<UIContactDetail> telephonyContacts - = uiContact.getContactDetailsForOperationSet( - getOperationSetForCall(isVideo, isDesktop)); - - boolean ignorePreferredProvider = - GuiActivator.getConfigurationService().getBoolean( - IGNORE_PREFERRED_PROVIDER_PROP, false); - - call( telephonyContacts, - uiContactImpl, - isVideo, - isDesktop, - invoker, - location, - !ignorePreferredProvider); - } - - /** - * Tries to resolves a peer address into a display name, by reqesting the - * <tt>ContactSourceService</tt>s. This function returns only the - * first match. - * - * @param peerAddress The peer address. - * - * @return The corresponding display name, if there is a match. Null - * otherwise. - */ - private static String resolveContactSource(String peerAddress) - { - String displayName = null; - - if(!StringUtils.isNullOrEmpty(peerAddress)) - { - Vector<ResolveAddressToDisplayNameContactQueryListener> resolvers - = new Vector<ResolveAddressToDisplayNameContactQueryListener> - (1, 1); - - // will strip the @server-address part, as the regular expression - // will match it - int index = peerAddress.indexOf("@"); - String peerUserID = - (index > -1) ? peerAddress.substring(0, index) : peerAddress; - - // searches for the whole number/username or with the @serverpart - peerUserID = Pattern.quote(peerUserID); - Pattern pattern = Pattern.compile( - "^(" + peerUserID + "|" + peerUserID + "@.*)$"); - - // Queries all available resolvers - for(ContactSourceService contactSourceService: - GuiActivator.getContactSources()) - { - if(!(contactSourceService - instanceof ExtendedContactSourceService)) - continue; - - // use the pattern method of (ExtendedContactSourceService) - ContactQuery query - = ((ExtendedContactSourceService)contactSourceService) - .queryContactSource(pattern); - - resolvers.add( - new ResolveAddressToDisplayNameContactQueryListener( - query)); - } - - long startTime = System.currentTimeMillis(); - long currentTime = startTime; - // The detault timeout is set to 500ms. - long timeout = 500; - // Loops until we found a valid display name, or waits for timeout. - while(displayName == null - && currentTime - startTime < timeout) - { - for(int i = 0; i < resolvers.size() && displayName == null; ++i) - { - ResolveAddressToDisplayNameContactQueryListener resolver - = resolvers.get(i); - if(!resolver.isRunning()) - { - if(resolver.isFound()) - { - displayName = resolver.getResolvedName(); - // If this is the same result as the peer address, - // then that is not what we are looking for. Then, - // continue the search. - if(displayName.equals(peerAddress)) - { - displayName = null; - } - } - } - } - Thread.yield(); - currentTime = System.currentTimeMillis(); - } - - // Free lasting resolvers. - for(int i = 0; i < resolvers.size(); ++i) - { - ResolveAddressToDisplayNameContactQueryListener resolver - = resolvers.get(i); - if(resolver.isRunning()) - { - resolver.stop(); - } - } - } - - return displayName; - } -} +/*
+ * 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.impl.gui.main.call;
+
+import java.awt.*;
+import java.lang.ref.*;
+import java.text.*;
+import java.util.*;
+import java.util.List;
+import java.util.regex.*;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.impl.gui.*;
+import net.java.sip.communicator.impl.gui.customcontrols.*;
+import net.java.sip.communicator.impl.gui.main.*;
+import net.java.sip.communicator.impl.gui.main.contactlist.*;
+import net.java.sip.communicator.plugin.desktoputil.*;
+import net.java.sip.communicator.plugin.desktoputil.transparent.*;
+import net.java.sip.communicator.service.contactlist.*;
+import net.java.sip.communicator.service.contactsource.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.service.protocol.media.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.Logger;
+import net.java.sip.communicator.util.account.*;
+
+import org.jitsi.service.neomedia.*;
+import org.jitsi.service.neomedia.codec.*;
+import org.jitsi.service.neomedia.device.*;
+import org.jitsi.service.neomedia.format.*;
+import org.jitsi.service.resources.*;
+import org.jitsi.util.*;
+
+/**
+ * The <tt>CallManager</tt> is the one that handles calls. It contains also
+ * the "Call" and "Hang up" buttons panel. Here are handles incoming and
+ * outgoing calls from and to the call operation set.
+ *
+ * @author Yana Stamcheva
+ * @author Lyubomir Marinov
+ * @author Boris Grozev
+ */
+public class CallManager
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>CallManager</tt> class and its
+ * instances for logging output.
+ */
+ private static final Logger logger = Logger.getLogger(CallManager.class);
+
+ /**
+ * The name of the property which indicates whether the user should be
+ * warned when starting a desktop sharing session.
+ */
+ private static final String desktopSharingWarningProperty
+ = "net.java.sip.communicator.impl.gui.main"
+ + ".call.SHOW_DESKTOP_SHARING_WARNING";
+
+ /**
+ * The name of the property which indicates whether the preferred provider
+ * will be used when calling UIContact (call history).
+ */
+ private static final String IGNORE_PREFERRED_PROVIDER_PROP
+ = "net.java.sip.communicator.impl.gui.main"
+ + ".call.IGNORE_PREFERRED_PROVIDER_PROP";
+
+ /**
+ * The <tt>CallPanel</tt>s opened by <tt>CallManager</tt> (because
+ * <tt>CallContainer</tt> does not give access to such lists.)
+ */
+ private static final Map<CallConference, CallPanel> callPanels
+ = new HashMap<CallConference, CallPanel>();
+
+ /**
+ * A map of active outgoing calls per <tt>UIContactImpl</tt>.
+ */
+ private static Map<Call, UIContactImpl> uiContactCalls;
+
+ /**
+ * The group of notifications dedicated to missed calls.
+ */
+ private static UINotificationGroup missedCallGroup;
+
+ /**
+ * A <tt>CallListener</tt>.
+ */
+ public static class GuiCallListener
+ extends SwingCallListener
+ {
+ /**
+ * Maps for incoming call handlers. The handlers needs to be created
+ * in the protocol thread while their method
+ * incomingCallReceivedInEventDispatchThread will be called on EDT.
+ * On the protocol thread a call state changed listener is added,
+ * if this is done on the EDT there is a almost no gap between incoming
+ * CallEvent and call state changed when doing auto answer and we
+ * end up with call answered and dialog for incoming call.
+ */
+ private Map<CallEvent,WeakReference<IncomingCallHandler>>
+ inCallHandlers = Collections.synchronizedMap(
+ new WeakHashMap<CallEvent,
+ WeakReference<IncomingCallHandler>>());
+
+ /**
+ * Delivers the <tt>CallEvent</tt> in the protocol thread.
+ */
+ public void incomingCallReceived(CallEvent ev)
+ {
+ inCallHandlers.put(
+ ev,
+ new WeakReference<IncomingCallHandler>(
+ new IncomingCallHandler(ev.getSourceCall())));
+
+ super.incomingCallReceived(ev);
+ }
+
+ /**
+ * Implements {@link CallListener#incomingCallReceived(CallEvent)}. When
+ * a call is received, creates a <tt>ReceivedCallDialog</tt> and plays
+ * the ring phone sound to the user.
+ *
+ * @param ev the <tt>CallEvent</tt>
+ */
+ @Override
+ public void incomingCallReceivedInEventDispatchThread(CallEvent ev)
+ {
+ WeakReference<IncomingCallHandler> ihRef
+ = inCallHandlers.remove(ev);
+
+ if(ihRef != null)
+ {
+ ihRef.get().incomingCallReceivedInEventDispatchThread(ev);
+ }
+ }
+
+ /**
+ * Implements CallListener.callEnded. Stops sounds that are playing at
+ * the moment if there're any. Removes the <tt>CallPanel</tt> and
+ * disables the hang-up button.
+ *
+ * @param ev the <tt>CallEvent</tt> which specifies the <tt>Call</tt>
+ * that has ended
+ */
+ @Override
+ public void callEndedInEventDispatchThread(CallEvent ev)
+ {
+ CallConference callConference = ev.getCallConference();
+
+ closeCallContainerIfNotNecessary(callConference);
+
+ /*
+ * Notify the existing CallPanels about the CallEvent (in case
+ * they need to update their UI, for example).
+ */
+ forwardCallEventToCallPanels(ev);
+
+ // If we're currently in the call history view, refresh
+ // it.
+ TreeContactList contactList
+ = GuiActivator.getContactList();
+
+ if (contactList.getCurrentFilter().equals(
+ TreeContactList.historyFilter))
+ {
+ contactList.applyFilter(
+ TreeContactList.historyFilter);
+ }
+ }
+
+ /**
+ * Creates and opens a call dialog. Implements
+ * {@link CallListener#outgoingCallCreated(CallEvent)}.
+ *
+ * @param ev the <tt>CallEvent</tt>
+ */
+ @Override
+ public void outgoingCallCreatedInEventDispatchThread(CallEvent ev)
+ {
+ Call sourceCall = ev.getSourceCall();
+
+ openCallContainerIfNecessary(sourceCall);
+
+ /*
+ * Notify the existing CallPanels about the CallEvent (in case they
+ * need to update their UI, for example).
+ */
+ forwardCallEventToCallPanels(ev);
+ }
+ }
+
+ /**
+ * Handles incoming calls. Must be created on the protocol thread while the
+ * method incomingCallReceivedInEventDispatchThread is executed on the EDT.
+ */
+ private static class IncomingCallHandler
+ extends CallChangeAdapter
+ {
+ /**
+ * The dialog shown
+ */
+ private ReceivedCallDialog receivedCallDialog;
+
+ /**
+ * Peer name.
+ */
+ private String peerName;
+
+ /**
+ * The time of the incoming call.
+ */
+ private long callTime;
+
+ /**
+ * Construct
+ * @param sourceCall
+ */
+ IncomingCallHandler(Call sourceCall)
+ {
+ Iterator<? extends CallPeer> peerIter = sourceCall.getCallPeers();
+
+ if(!peerIter.hasNext())
+ {
+ return;
+ }
+
+ peerName = peerIter.next().getDisplayName();
+ callTime = System.currentTimeMillis();
+
+ sourceCall.addCallChangeListener(this);
+ }
+
+ /**
+ * State has changed.
+ * @param ev
+ */
+ @Override
+ public void callStateChanged(final CallChangeEvent ev)
+ {
+ if(!SwingUtilities.isEventDispatchThread())
+ {
+ SwingUtilities.invokeLater(
+ new Runnable()
+ {
+ public void run()
+ {
+ callStateChanged(ev);
+ }
+ });
+ return;
+ }
+ if (!CallChangeEvent.CALL_STATE_CHANGE
+ .equals(ev.getPropertyName()))
+ return;
+
+ // When the call state changes, we ensure here that the
+ // received call notification dialog is closed.
+ if (receivedCallDialog != null && receivedCallDialog.isVisible())
+ receivedCallDialog.setVisible(false);
+
+ // Ensure that the CallDialog is created, because it is the
+ // one that listens for CallPeers.
+ Object newValue = ev.getNewValue();
+ Call call = ev.getSourceCall();
+
+ if (CallState.CALL_INITIALIZATION.equals(newValue)
+ || CallState.CALL_IN_PROGRESS.equals(newValue))
+ {
+ openCallContainerIfNecessary(call);
+ }
+ else if (CallState.CALL_ENDED.equals(newValue))
+ {
+ if (ev.getOldValue().equals(
+ CallState.CALL_INITIALIZATION))
+ {
+ // If the call was answered elsewhere, don't mark it
+ // as missed.
+ CallPeerChangeEvent cause = ev.getCause();
+
+ if ((cause == null)
+ || (cause.getReasonCode()
+ != CallPeerChangeEvent
+ .NORMAL_CALL_CLEARING))
+ {
+ addMissedCallNotification(peerName, callTime);
+ }
+ }
+
+ call.removeCallChangeListener(this);
+ }
+ }
+
+ /**
+ * Executed on EDT cause will create dialog and will show it.
+ * @param ev
+ */
+ public void incomingCallReceivedInEventDispatchThread(CallEvent ev)
+ {
+ Call sourceCall = ev.getSourceCall();
+ boolean isVideoCall
+ = ev.isVideoCall()
+ && ConfigurationUtils.hasEnabledVideoFormat(
+ sourceCall.getProtocolProvider());
+ receivedCallDialog = new ReceivedCallDialog(
+ sourceCall,
+ isVideoCall,
+ (CallManager.getInProgressCalls().size() > 0));
+
+ receivedCallDialog.setVisible(true);
+
+ Iterator<? extends CallPeer> peerIter = sourceCall.getCallPeers();
+
+ if(!peerIter.hasNext())
+ {
+ if (receivedCallDialog.isVisible())
+ receivedCallDialog.setVisible(false);
+ return;
+ }
+
+ /*
+ * Notify the existing CallPanels about the CallEvent (in case they
+ * need to update their UI, for example).
+ */
+ forwardCallEventToCallPanels(ev);
+ }
+ }
+
+ /**
+ * Answers the given call.
+ *
+ * @param call the call to answer
+ */
+ public static void answerCall(Call call)
+ {
+ answerCall(call, null, false /* without video */);
+ }
+
+ /**
+ * Answers a specific <tt>Call</tt> with or without video and, optionally,
+ * does that in a telephony conference with an existing <tt>Call</tt>.
+ *
+ * @param call
+ * @param existingCall
+ * @param video
+ */
+ private static void answerCall(Call call, Call existingCall, boolean video)
+ {
+ if (existingCall == null)
+ openCallContainerIfNecessary(call);
+
+ new AnswerCallThread(call, existingCall, video).start();
+ }
+
+ /**
+ * Answers the given call in an existing call. It will end up with a
+ * conference call.
+ *
+ * @param call the call to answer
+ */
+ public static void answerCallInFirstExistingCall(Call call)
+ {
+ // Find the first existing call.
+ Iterator<Call> existingCallIter = getInProgressCalls().iterator();
+ Call existingCall
+ = existingCallIter.hasNext() ? existingCallIter.next() : null;
+
+ answerCall(call, existingCall, false /* without video */);
+ }
+
+ /**
+ * Merges specific existing <tt>Call</tt>s into a specific telephony
+ * conference.
+ *
+ * @param conference the conference
+ * @param calls list of calls
+ */
+ public static void mergeExistingCalls(
+ CallConference conference,
+ Collection<Call> calls)
+ {
+ new MergeExistingCalls(conference, calls).start();
+ }
+
+ /**
+ * Answers the given call with video.
+ *
+ * @param call the call to answer
+ */
+ public static void answerVideoCall(Call call)
+ {
+ answerCall(call, null, true /* with video */);
+ }
+
+ /**
+ * Hang ups the given call.
+ *
+ * @param call the call to hang up
+ */
+ public static void hangupCall(Call call)
+ {
+ new HangupCallThread(call).start();
+ }
+
+ /**
+ * Hang ups the given <tt>callPeer</tt>.
+ *
+ * @param peer the <tt>CallPeer</tt> to hang up
+ */
+ public static void hangupCallPeer(CallPeer peer)
+ {
+ new HangupCallThread(peer).start();
+ }
+
+ /**
+ * Asynchronously hangs up the <tt>Call</tt>s participating in a specific
+ * <tt>CallConference</tt>.
+ *
+ * @param conference the <tt>CallConference</tt> whose participating
+ * <tt>Call</tt>s are to be hanged up
+ */
+ public static void hangupCalls(CallConference conference)
+ {
+ new HangupCallThread(conference).start();
+ }
+
+ /**
+ * Creates a call to the contact represented by the given string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ */
+ public static void createCall( ProtocolProviderService protocolProvider,
+ String contact)
+ {
+ new CreateCallThread(protocolProvider, contact, false /* audio-only */)
+ .start();
+ }
+
+ /**
+ * Creates a call to the contact represented by the given string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ * @param uiContact the meta contact we're calling
+ */
+ public static void createCall( ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact)
+ {
+ new CreateCallThread(protocolProvider, null, null, uiContact,
+ contact, null, null, false /* audio-only */).start();
+ }
+
+ /**
+ * Creates a video call to the contact represented by the given string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ */
+ public static void createVideoCall(ProtocolProviderService protocolProvider,
+ String contact)
+ {
+ new CreateCallThread(protocolProvider, contact, true /* video */)
+ .start();
+ }
+
+ /**
+ * Creates a video call to the contact represented by the given string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ */
+ public static void createVideoCall( ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact)
+ {
+ new CreateCallThread(protocolProvider, null, null, uiContact,
+ contact, null, null, true /* video */).start();
+ }
+
+ /**
+ * Enables/disables local video for a specific <tt>Call</tt>.
+ *
+ * @param call the <tt>Call</tt> to enable/disable to local video for
+ * @param enable <tt>true</tt> to enable the local video; otherwise,
+ * <tt>false</tt>
+ */
+ public static void enableLocalVideo(Call call, boolean enable)
+ {
+ new EnableLocalVideoThread(call, enable).start();
+ }
+
+ /**
+ * Indicates if the desktop sharing is currently enabled for the given
+ * <tt>call</tt>.
+ *
+ * @param call the <tt>Call</tt>, for which we would to check if the desktop
+ * sharing is currently enabled
+ * @return <tt>true</tt> if the desktop sharing is currently enabled for the
+ * given <tt>call</tt>, <tt>false</tt> otherwise
+ */
+ public static boolean isLocalVideoEnabled(Call call)
+ {
+ OperationSetVideoTelephony telephony
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+
+ return (telephony != null) && telephony.isLocalVideoAllowed(call);
+ }
+
+ /**
+ * Creates a desktop sharing call to the contact represented by the given
+ * string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ */
+ private static void createDesktopSharing(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact)
+ {
+ // If the user presses cancel on the desktop sharing warning then we
+ // have nothing more to do here.
+ if (!showDesktopSharingWarning())
+ return;
+
+ MediaService mediaService = GuiActivator.getMediaService();
+ List<MediaDevice> desktopDevices
+ = mediaService.getDevices(MediaType.VIDEO, MediaUseCase.DESKTOP);
+ int deviceNumber = desktopDevices.size();
+
+ if (deviceNumber == 1)
+ {
+ createDesktopSharing(
+ protocolProvider,
+ contact,
+ uiContact,
+ desktopDevices.get(0));
+ }
+ else if (deviceNumber > 1)
+ {
+ SelectScreenDialog selectDialog
+ = new SelectScreenDialog(desktopDevices);
+
+ selectDialog.setVisible(true);
+ if (selectDialog.getSelectedDevice() != null)
+ createDesktopSharing(
+ protocolProvider,
+ contact,
+ uiContact,
+ selectDialog.getSelectedDevice());
+ }
+ }
+
+ /**
+ * Creates a region desktop sharing through the given
+ * <tt>protocolProvider</tt> with the given <tt>contact</tt>.
+ *
+ * @param protocolProvider the <tt>ProtocolProviderService</tt>, through
+ * which the sharing session will be established
+ * @param contact the address of the contact recipient
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ */
+ private static void createRegionDesktopSharing(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact)
+ {
+ if (showDesktopSharingWarning())
+ {
+ TransparentFrame frame = DesktopSharingFrame
+ .createTransparentFrame(
+ protocolProvider, contact, uiContact, true);
+
+ frame.setLocationRelativeTo(null);
+ frame.setVisible(true);
+ }
+ }
+
+ /**
+ * Creates a desktop sharing call to the contact represented by the given
+ * string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ * @param uiContact the <tt>MetaContact</tt> we're calling
+ * @param x the x coordinate of the shared region
+ * @param y the y coordinated of the shared region
+ * @param width the width of the shared region
+ * @param height the height of the shared region
+ */
+ public static void createRegionDesktopSharing(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact,
+ int x,
+ int y,
+ int width,
+ int height)
+ {
+ MediaService mediaService = GuiActivator.getMediaService();
+
+ List<MediaDevice> desktopDevices = mediaService.getDevices(
+ MediaType.VIDEO, MediaUseCase.DESKTOP);
+
+ int deviceNumber = desktopDevices.size();
+
+ if (deviceNumber > 0)
+ {
+ createDesktopSharing(
+ protocolProvider,
+ contact,
+ uiContact,
+ mediaService.getMediaDeviceForPartialDesktopStreaming(
+ width,
+ height,
+ x,
+ y));
+ }
+ }
+
+ /**
+ * Creates a desktop sharing call to the contact represented by the given
+ * string.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param contact the contact to call to
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ * @param mediaDevice the media device corresponding to the screen to share
+ */
+ private static void createDesktopSharing(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact,
+ MediaDevice mediaDevice)
+ {
+ new CreateDesktopSharingThread( protocolProvider,
+ contact,
+ uiContact,
+ mediaDevice).start();
+ }
+
+ /**
+ * Enables the desktop sharing in an existing <tt>call</tt>.
+ *
+ * @param call the call for which desktop sharing should be enabled
+ * @param enable indicates if the desktop sharing should be enabled or
+ * disabled
+ */
+ public static void enableDesktopSharing(Call call, boolean enable)
+ {
+ if (!enable)
+ enableDesktopSharing(call, null, enable);
+ else if (showDesktopSharingWarning())
+ {
+ MediaService mediaService = GuiActivator.getMediaService();
+ List<MediaDevice> desktopDevices
+ = mediaService.getDevices(MediaType.VIDEO, MediaUseCase.DESKTOP);
+ int deviceNumber = desktopDevices.size();
+
+ if (deviceNumber == 1)
+ enableDesktopSharing(call, null, enable);
+ else if (deviceNumber > 1)
+ {
+ SelectScreenDialog selectDialog
+ = new SelectScreenDialog(desktopDevices);
+
+ selectDialog.setVisible(true);
+
+ if (selectDialog.getSelectedDevice() != null)
+ enableDesktopSharing(
+ call, selectDialog.getSelectedDevice(), enable);
+ }
+ }
+
+ // in case we switch to video, disable remote control if it was
+ // enabled
+ enableDesktopRemoteControl(call.getCallPeers().next(), false);
+ }
+
+ /**
+ * Enables the region desktop sharing for the given call.
+ *
+ * @param call the call, for which the region desktop sharing should be
+ * enabled
+ * @param enable indicates if the desktop sharing should be enabled or
+ * disabled
+ */
+ public static void enableRegionDesktopSharing(Call call, boolean enable)
+ {
+ if (!enable)
+ enableDesktopSharing(call, null, enable);
+ else if (showDesktopSharingWarning())
+ {
+ TransparentFrame frame
+ = DesktopSharingFrame.createTransparentFrame(call, true);
+
+ frame.setVisible(true);
+ }
+ }
+
+ /**
+ * Creates a desktop sharing call to the contact represented by the given
+ * string.
+ *
+ * @param call the call for which desktop sharing should be enabled
+ * @param x the x coordinate of the shared region
+ * @param y the y coordinated of the shared region
+ * @param width the width of the shared region
+ * @param height the height of the shared region
+ */
+ public static void enableRegionDesktopSharing(
+ Call call,
+ int x,
+ int y,
+ int width,
+ int height)
+ {
+ // Use the default media device corresponding to the screen to share
+ MediaService mediaService = GuiActivator.getMediaService();
+
+ List<MediaDevice> desktopDevices = mediaService.getDevices(
+ MediaType.VIDEO, MediaUseCase.DESKTOP);
+
+ int deviceNumber = desktopDevices.size();
+
+ if (deviceNumber > 0)
+ {
+ boolean succeed = enableDesktopSharing(
+ call,
+ mediaService.getMediaDeviceForPartialDesktopStreaming(
+ width,
+ height,
+ x,
+ y),
+ true);
+ // If the region sharing succeed, then display the frame of the
+ // current region shared.
+ if(succeed)
+ {
+ TransparentFrame frame
+ = DesktopSharingFrame.createTransparentFrame(call, false);
+
+ frame.setVisible(true);
+ }
+ }
+
+ // in case we switch to video, disable remote control if it was
+ // enabled
+ enableDesktopRemoteControl(call.getCallPeers().next(), false);
+ }
+
+ /**
+ * Enables the desktop sharing in an existing <tt>call</tt>.
+ *
+ * @param call the call for which desktop sharing should be enabled
+ * @param mediaDevice the media device corresponding to the screen to share
+ * @param enable indicates if the desktop sharing should be enabled or
+ * disabled
+ *
+ * @return True if the desktop sharing succeed (we are currently sharing the
+ * whole or a part of the desktop). False, otherwise.
+ */
+ private static boolean enableDesktopSharing(Call call,
+ MediaDevice mediaDevice,
+ boolean enable)
+ {
+ OperationSetDesktopSharingServer desktopOpSet
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetDesktopSharingServer.class);
+ boolean enableSucceeded = false;
+
+ // This shouldn't happen at this stage, because we disable the button
+ // if the operation set isn't available.
+ if (desktopOpSet != null)
+ {
+ // First make sure the local video button is disabled.
+ if (enable && isLocalVideoEnabled(call))
+ getActiveCallContainer(call).setVideoButtonSelected(false);
+
+ try
+ {
+ if (mediaDevice != null)
+ {
+ desktopOpSet.setLocalVideoAllowed(
+ call,
+ mediaDevice,
+ enable);
+ }
+ else
+ desktopOpSet.setLocalVideoAllowed(call, enable);
+
+ enableSucceeded = true;
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error(
+ "Failed to toggle the streaming of local video.",
+ ex);
+ }
+ }
+
+ return (enable && enableSucceeded);
+ }
+
+ /**
+ * Indicates if the desktop sharing is currently enabled for the given
+ * <tt>call</tt>.
+ *
+ * @param call the <tt>Call</tt>, for which we would to check if the desktop
+ * sharing is currently enabled
+ * @return <tt>true</tt> if the desktop sharing is currently enabled for the
+ * given <tt>call</tt>, <tt>false</tt> otherwise
+ */
+ public static boolean isDesktopSharingEnabled(Call call)
+ {
+ OperationSetDesktopSharingServer desktopOpSet
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetDesktopSharingServer.class);
+
+ if (desktopOpSet != null
+ && desktopOpSet.isLocalVideoAllowed(call))
+ return true;
+
+ return false;
+ }
+
+ /**
+ * Indicates if the desktop sharing is currently enabled for the given
+ * <tt>call</tt>.
+ *
+ * @param call the <tt>Call</tt>, for which we would to check if the desktop
+ * sharing is currently enabled
+ * @return <tt>true</tt> if the desktop sharing is currently enabled for the
+ * given <tt>call</tt>, <tt>false</tt> otherwise
+ */
+ public static boolean isRegionDesktopSharingEnabled(Call call)
+ {
+ OperationSetDesktopSharingServer desktopOpSet
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetDesktopSharingServer.class);
+
+ if (desktopOpSet != null
+ && desktopOpSet.isPartialStreaming(call))
+ return true;
+
+ return false;
+ }
+
+ /**
+ * Enables/disables remote control when in a desktop sharing session with
+ * the given <tt>callPeer</tt>.
+ *
+ * @param callPeer the call peer for which we enable/disable remote control
+ * @param isEnable indicates if the remote control should be enabled
+ */
+ public static void enableDesktopRemoteControl( CallPeer callPeer,
+ boolean isEnable)
+ {
+ OperationSetDesktopSharingServer sharingOpSet
+ = callPeer.getProtocolProvider().getOperationSet(
+ OperationSetDesktopSharingServer.class);
+
+ if (sharingOpSet == null)
+ return;
+
+ if (isEnable)
+ sharingOpSet.enableRemoteControl(callPeer);
+ else
+ sharingOpSet.disableRemoteControl(callPeer);
+ }
+
+ /**
+ * Creates a call to the given call string. The given component indicates
+ * where should be shown the "call via" menu if needed.
+ *
+ * @param callString the string to call
+ * @param c the component, which indicates where should be shown the "call
+ * via" menu if needed
+ */
+ public static void createCall( String callString,
+ JComponent c)
+ {
+ createCall(callString, c, null);
+ }
+
+ /**
+ * Creates a call to the given call string. The given component indicates
+ * where should be shown the "call via" menu if needed.
+ *
+ * @param callString the string to call
+ * @param c the component, which indicates where should be shown the "call
+ * via" menu if needed
+ * @param l listener that is notified when the call interface has been
+ * started after call was created
+ */
+ public static void createCall( String callString,
+ JComponent c,
+ CallInterfaceListener l)
+ {
+ callString = callString.trim();
+
+ // Removes special characters from phone numbers.
+ if (ConfigurationUtils.isNormalizePhoneNumber())
+ callString = PhoneNumberI18nService.normalize(callString);
+
+ List<ProtocolProviderService> telephonyProviders
+ = CallManager.getTelephonyProviders();
+
+ if (telephonyProviders.size() == 1)
+ {
+ CallManager.createCall(
+ telephonyProviders.get(0), callString);
+
+ if (l != null)
+ l.callInterfaceStarted();
+ }
+ else if (telephonyProviders.size() > 1)
+ {
+ /*
+ * Allow plugins which do not have a (Jitsi) UI to create calls by
+ * automagically picking up a telephony provider.
+ */
+ if (c == null)
+ {
+ ProtocolProviderService preferredTelephonyProvider = null;
+
+ for (ProtocolProviderService telephonyProvider
+ : telephonyProviders)
+ {
+ try
+ {
+ OperationSetPresence presenceOpSet
+ = telephonyProvider.getOperationSet(
+ OperationSetPresence.class);
+
+ if ((presenceOpSet != null)
+ && (presenceOpSet.findContactByID(callString)
+ != null))
+ {
+ preferredTelephonyProvider = telephonyProvider;
+ break;
+ }
+ }
+ catch (Throwable t)
+ {
+ if (t instanceof ThreadDeath)
+ throw (ThreadDeath) t;
+ }
+ }
+ if (preferredTelephonyProvider == null)
+ preferredTelephonyProvider = telephonyProviders.get(0);
+
+ CallManager.createCall(preferredTelephonyProvider, callString);
+ if (l != null)
+ l.callInterfaceStarted();
+ }
+ else
+ {
+ ChooseCallAccountPopupMenu chooseAccountDialog
+ = new ChooseCallAccountPopupMenu(
+ c,
+ callString,
+ telephonyProviders,
+ l);
+
+ chooseAccountDialog.setLocation(c.getLocation());
+ chooseAccountDialog.showPopupMenu();
+ }
+ }
+ else
+ {
+ ResourceManagementService resources = GuiActivator.getResources();
+
+ new ErrorDialog(
+ null,
+ resources.getI18NString("service.gui.WARNING"),
+ resources.getI18NString(
+ "service.gui.NO_ONLINE_TELEPHONY_ACCOUNT"))
+ .showDialog();
+ }
+ }
+
+ /**
+ * Creates a call to the given list of contacts.
+ *
+ * @param protocolProvider the protocol provider to which this call belongs.
+ * @param callees the list of contacts to call to
+ */
+ public static void createConferenceCall(
+ String[] callees,
+ ProtocolProviderService protocolProvider)
+ {
+ Map<ProtocolProviderService, List<String>> crossProtocolCallees
+ = new HashMap<ProtocolProviderService, List<String>>();
+
+ crossProtocolCallees.put(protocolProvider, Arrays.asList(callees));
+ createConferenceCall(crossProtocolCallees);
+ }
+
+ /**
+ * Invites the given list of <tt>callees</tt> to the given conference
+ * <tt>call</tt>.
+ *
+ * @param callees the list of contacts to invite
+ * @param call the protocol provider to which this call belongs
+ */
+ public static void inviteToConferenceCall(String[] callees, Call call)
+ {
+ Map<ProtocolProviderService, List<String>> crossProtocolCallees
+ = new HashMap<ProtocolProviderService, List<String>>();
+
+ crossProtocolCallees.put(
+ call.getProtocolProvider(),
+ Arrays.asList(callees));
+ inviteToConferenceCall(crossProtocolCallees, call);
+ }
+
+ /**
+ * Invites the given list of <tt>callees</tt> to the given conference
+ * <tt>call</tt>.
+ *
+ * @param callees the list of contacts to invite
+ * @param call existing call
+ */
+ public static void inviteToConferenceCall(
+ Map<ProtocolProviderService, List<String>> callees,
+ Call call)
+ {
+ new InviteToConferenceCallThread(callees, call).start();
+ }
+
+ /**
+ * Invites specific <tt>callees</tt> to a specific telephony conference.
+ *
+ * @param callees the list of contacts to invite
+ * @param conference the telephony conference to invite the specified
+ * <tt>callees</tt> into
+ */
+ public static void inviteToConferenceCall(
+ Map<ProtocolProviderService, List<String>> callees,
+ CallConference conference)
+ {
+ /*
+ * InviteToConferenceCallThread takes a specific Call but actually
+ * invites to the telephony conference associated with the specified
+ * Call (if any). In order to not change the signature of its
+ * constructor at this time, just pick up a Call participating in the
+ * specified telephony conference (if any).
+ */
+ Call call = null;
+
+ if (conference != null)
+ {
+ List<Call> calls = conference.getCalls();
+
+ if (!calls.isEmpty())
+ call = calls.get(0);
+ }
+
+ new InviteToConferenceCallThread(callees, call).start();
+ }
+
+ /**
+ * Asynchronously creates a new conference <tt>Call</tt> with a specific
+ * list of participants/callees.
+ *
+ * @param callees the list of participants/callees to invite to a
+ * newly-created conference <tt>Call</tt>
+ */
+ public static void createConferenceCall(
+ Map<ProtocolProviderService, List<String>> callees)
+ {
+ new InviteToConferenceCallThread(callees, null).start();
+ }
+
+ /**
+ * Asynchronously creates a new video bridge conference <tt>Call</tt> with
+ * a specific list of participants/callees.
+ *
+ * @param callProvider the <tt>ProtocolProviderService</tt> to use for
+ * creating the call
+ * @param callees the list of participants/callees to invite to the
+ * newly-created video bridge conference <tt>Call</tt>
+ */
+ public static void createJitsiVideobridgeConfCall(
+ ProtocolProviderService callProvider,
+ String[] callees)
+ {
+ new InviteToConferenceBridgeThread(callProvider, callees, null).start();
+ }
+
+ /**
+ * Invites the given list of <tt>callees</tt> to the given conference
+ * <tt>call</tt>.
+ *
+ * @param callees the list of contacts to invite
+ * @param call the protocol provider to which this call belongs
+ */
+ public static void inviteToJitsiVideobridgeConfCall(String[] callees, Call call)
+ {
+ new InviteToConferenceBridgeThread( call.getProtocolProvider(),
+ callees,
+ call).start();
+ }
+
+ /**
+ * Puts on or off hold the given <tt>callPeer</tt>.
+ * @param callPeer the peer to put on/off hold
+ * @param isOnHold indicates the action (on hold or off hold)
+ */
+ public static void putOnHold(CallPeer callPeer, boolean isOnHold)
+ {
+ new PutOnHoldCallPeerThread(callPeer, isOnHold).start();
+ }
+
+ /**
+ * Transfers the given <tt>peer</tt> to the given <tt>target</tt>.
+ * @param peer the <tt>CallPeer</tt> to transfer
+ * @param target the <tt>CallPeer</tt> target to transfer to
+ */
+ public static void transferCall(CallPeer peer, CallPeer target)
+ {
+ OperationSetAdvancedTelephony<?> telephony
+ = peer.getCall().getProtocolProvider()
+ .getOperationSet(OperationSetAdvancedTelephony.class);
+
+ if (telephony != null)
+ {
+ try
+ {
+ telephony.transfer(peer, target);
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error("Failed to transfer " + peer.getAddress()
+ + " to " + target, ex);
+ }
+ }
+ }
+
+ /**
+ * Transfers the given <tt>peer</tt> to the given <tt>target</tt>.
+ * @param peer the <tt>CallPeer</tt> to transfer
+ * @param target the target of the transfer
+ */
+ public static void transferCall(CallPeer peer, String target)
+ {
+ OperationSetAdvancedTelephony<?> telephony
+ = peer.getCall().getProtocolProvider()
+ .getOperationSet(OperationSetAdvancedTelephony.class);
+
+ if (telephony != null)
+ {
+ try
+ {
+ telephony.transfer(peer, target);
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error("Failed to transfer " + peer.getAddress()
+ + " to " + target, ex);
+ }
+ }
+ }
+
+ /**
+ * Closes the <tt>CallPanel</tt> of a specific <tt>Call</tt> if it is no
+ * longer necessary (i.e. is not used by other <tt>Call</tt>s participating
+ * in the same telephony conference as the specified <tt>Call</tt>.)
+ *
+ * @param callConference The <tt>CallConference</tt> which is to have its
+ * associated <tt>CallPanel</tt>, if any
+ */
+ private static void closeCallContainerIfNotNecessary(
+ final CallConference callConference)
+ {
+ CallPanel callPanel = callPanels.get(callConference);
+
+ if (callPanel != null)
+ closeCallContainerIfNotNecessary(
+ callConference, callPanel.isCloseWaitAfterHangup());
+ }
+
+ /**
+ * Closes the <tt>CallPanel</tt> of a specific <tt>Call</tt> if it is no
+ * longer necessary (i.e. is not used by other <tt>Call</tt>s participating
+ * in the same telephony conference as the specified <tt>Call</tt>.)
+ *
+ * @param callConference The <tt>CallConference</tt> which is to have its
+ * associated <tt>CallPanel</tt>, if any, closed
+ * @param wait <tt>true</tt> to set <tt>delay</tt> param of
+ * {@link CallContainer#close(CallPanel, boolean)} (CallPanel)}
+ */
+ private static void closeCallContainerIfNotNecessary(
+ final CallConference callConference,
+ final boolean wait)
+ {
+ if (!SwingUtilities.isEventDispatchThread())
+ {
+ SwingUtilities.invokeLater(
+ new Runnable()
+ {
+ public void run()
+ {
+ closeCallContainerIfNotNecessary(
+ callConference,
+ wait);
+ }
+ });
+ return;
+ }
+
+ /*
+ * XXX The integrity of the execution of the method may be compromised
+ * if it is not invoked on the AWT event dispatching thread because
+ * findCallPanel and callPanels.remove must be atomically executed. The
+ * uninterrupted execution (with respect to the synchronization) is
+ * guaranteed by requiring all modifications to callPanels to be made on
+ * the AWT event dispatching thread.
+ */
+
+ for (Iterator<Map.Entry<CallConference, CallPanel>> entryIter
+ = callPanels.entrySet().iterator();
+ entryIter.hasNext();)
+ {
+ Map.Entry<CallConference, CallPanel> entry = entryIter.next();
+ CallConference aConference = entry.getKey();
+ boolean notNecessary = aConference.isEnded();
+
+ if (notNecessary)
+ {
+ CallPanel aCallPanel = entry.getValue();
+ CallContainer window = aCallPanel.getCallWindow();
+
+ try
+ {
+ window.close(
+ aCallPanel,
+ wait && (aConference == callConference));
+ }
+ finally
+ {
+ /*
+ * We allow non-modifications i.e. reads of callPanels on
+ * threads other than the AWT event dispatching thread so we
+ * have to make sure that we will not cause
+ * ConcurrentModificationException.
+ */
+ synchronized (callPanels)
+ {
+ entryIter.remove();
+ }
+
+ aCallPanel.dispose();
+ }
+ }
+ }
+ }
+
+ /**
+ * Opens a <tt>CallPanel</tt> for a specific <tt>Call</tt> if there is none.
+ * <p>
+ * <b>Note</b>: The method can be called only on the AWT event dispatching
+ * thread.
+ * </p>
+ *
+ * @param call the <tt>Call</tt> to open a <tt>CallPanel</tt> for
+ * @return the <tt>CallPanel</tt> associated with the <tt>Call</tt>
+ * @throws RuntimeException if the method is not called on the AWT event
+ * dispatching thread
+ */
+ private static CallPanel openCallContainerIfNecessary(Call call)
+ {
+ /*
+ * XXX The integrity of the execution of the method may be compromised
+ * if it is not invoked on the AWT event dispatching thread because
+ * findCallPanel and callPanels.put must be atomically executed. The
+ * uninterrupted execution (with respect to the synchronization) is
+ * guaranteed by requiring all modifications to callPanels to be made on
+ * the AWT event dispatching thread.
+ */
+ assertIsEventDispatchingThread();
+
+ /*
+ * CallPanel displays a CallConference (which may contain multiple
+ * Calls.)
+ */
+ CallConference conference = call.getConference();
+ CallPanel callPanel = findCallPanel(conference);
+
+ if (callPanel == null)
+ {
+ // If we're in single-window mode, the single window is the
+ // CallContainer.
+ CallContainer callContainer
+ = GuiActivator.getUIService().getSingleWindowContainer();
+
+ // If we're in multi-window mode, we create the CallDialog.
+ if (callContainer == null)
+ callContainer = new CallDialog();
+
+ callPanel = new CallPanel(conference, callContainer);
+ callContainer.addCallPanel(callPanel);
+
+ synchronized (callPanels)
+ {
+ callPanels.put(conference, callPanel);
+ }
+ }
+
+ return callPanel;
+ }
+
+ /**
+ * Returns a list of all currently registered telephony providers.
+ * @return a list of all currently registered telephony providers
+ */
+ public static List<ProtocolProviderService> getTelephonyProviders()
+ {
+ return AccountUtils
+ .getRegisteredProviders(OperationSetBasicTelephony.class);
+ }
+
+ /**
+ * Returns a list of all currently registered telephony providers supporting
+ * conferencing.
+ *
+ * @return a list of all currently registered telephony providers supporting
+ * conferencing
+ */
+ public static List<ProtocolProviderService>
+ getTelephonyConferencingProviders()
+ {
+ return AccountUtils
+ .getRegisteredProviders(OperationSetTelephonyConferencing.class);
+ }
+
+ /**
+ * Returns a list of all currently active calls.
+ *
+ * @return a list of all currently active calls
+ */
+ private static List<Call> getActiveCalls()
+ {
+ CallConference[] conferences;
+
+ synchronized (callPanels)
+ {
+ Set<CallConference> keySet = callPanels.keySet();
+
+ conferences = keySet.toArray(new CallConference[keySet.size()]);
+ }
+
+ List<Call> calls = new ArrayList<Call>();
+
+ for (CallConference conference : conferences)
+ {
+ for (Call call : conference.getCalls())
+ {
+ if (call.getCallState() == CallState.CALL_IN_PROGRESS)
+ calls.add(call);
+ }
+ }
+ return calls;
+ }
+
+ /**
+ * Returns a collection of all currently in progress calls. A call is active
+ * if it is in progress so the method merely delegates to
+ *
+ * @return a collection of all currently in progress calls.
+ */
+ public static Collection<Call> getInProgressCalls()
+ {
+ return getActiveCalls();
+ }
+
+ /**
+ * Returns the <tt>CallContainer</tt> corresponding to the given
+ * <tt>call</tt>. If the call has been finished and no active
+ * <tt>CallContainer</tt> could be found it returns null.
+ *
+ * @param call the <tt>Call</tt>, which dialog we're looking for
+ * @return the <tt>CallContainer</tt> corresponding to the given
+ * <tt>call</tt>
+ */
+ public static CallPanel getActiveCallContainer(Call call)
+ {
+ return findCallPanel(call.getConference());
+ }
+
+ /**
+ * A informative text to show for the peer. If display name is missing
+ * return the address.
+ * @param peer the peer.
+ * @return the text contain display name.
+ */
+ public static String getPeerDisplayName(CallPeer peer)
+ {
+ String displayName = null;
+
+ // We try to find the <tt>UIContact</tt>, to which the call was
+ // created if this was an outgoing call.
+ UIContactImpl uiContact
+ = CallManager.getCallUIContact(peer.getCall());
+
+ if(uiContact != null)
+ {
+ if(uiContact.getDescriptor() instanceof SourceContact)
+ {
+ // if it is source contact (history record)
+ // search for cusax contact match
+ Contact contact = getPeerCusaxContact(peer.getAddress(),
+ (SourceContact)uiContact.getDescriptor());
+ if(contact != null)
+ displayName = contact.getDisplayName();
+ }
+
+ if(StringUtils.isNullOrEmpty(displayName, true))
+ displayName = uiContact.getDisplayName();
+ }
+
+ // We search for a contact corresponding to this call peer and
+ // try to get its display name.
+ if (StringUtils.isNullOrEmpty(displayName, true)
+ && peer.getContact() != null)
+ {
+ displayName = peer.getContact().getDisplayName();
+ }
+
+ // We try to find the an alternative peer address.
+ if (StringUtils.isNullOrEmpty(displayName, true))
+ {
+ String imppAddress = peer.getAlternativeIMPPAddress();
+
+ if (!StringUtils.isNullOrEmpty(imppAddress))
+ {
+ int protocolPartIndex = imppAddress.indexOf(":");
+
+ imppAddress = (protocolPartIndex >= 0)
+ ? imppAddress.substring(protocolPartIndex + 1)
+ : imppAddress;
+
+ Collection<ProtocolProviderService> cusaxProviders
+ = AccountUtils.getRegisteredProviders(
+ OperationSetCusaxUtils.class);
+
+ if (cusaxProviders != null && cusaxProviders.size() > 0)
+ {
+ Contact contact = getPeerContact(
+ peer,
+ cusaxProviders.iterator().next(),
+ imppAddress);
+
+ displayName = (contact != null)
+ ? contact.getDisplayName() : null;
+ }
+ else
+ {
+ MetaContact metaContact
+ = getPeerMetaContact(peer, imppAddress);
+
+ displayName = (metaContact != null)
+ ? metaContact.getDisplayName() : null;
+ }
+ }
+ }
+
+ if (StringUtils.isNullOrEmpty(displayName, true))
+ {
+ displayName = (!StringUtils.isNullOrEmpty
+ (peer.getDisplayName(), true))
+ ? peer.getDisplayName()
+ : peer.getAddress();
+
+ // Try to resolve the display name
+ String resolvedName = resolveContactSource(displayName);
+ if(resolvedName != null)
+ {
+ displayName = resolvedName;
+ }
+ }
+
+ return displayName;
+ }
+
+ /**
+ * Returns the image corresponding to the given <tt>peer</tt>.
+ *
+ * @param peer the call peer, for which we're returning an image
+ * @return the peer image
+ */
+ public static byte[] getPeerImage(CallPeer peer)
+ {
+ byte[] image = null;
+ // We search for a contact corresponding to this call peer and
+ // try to get its image.
+ if (peer.getContact() != null)
+ {
+ image = getContactImage(peer.getContact());
+ }
+
+ // We try to find the <tt>UIContact</tt>, to which the call was
+ // created if this was an outgoing call.
+ if (image == null || image.length == 0)
+ {
+ UIContactImpl uiContact
+ = CallManager.getCallUIContact(peer.getCall());
+
+ if (uiContact != null)
+ {
+ if(uiContact.getDescriptor() instanceof SourceContact
+ && ((SourceContact)uiContact.getDescriptor())
+ .isDefaultImage())
+ {
+ // if it is source contact (history record)
+ // search for cusax contact match
+ Contact contact = getPeerCusaxContact(peer.getAddress(),
+ (SourceContact)uiContact.getDescriptor());
+
+ if(contact != null)
+ image = contact.getImage();
+ }
+ else
+ image = uiContact.getAvatar();
+ }
+ }
+
+ // We try to find the an alternative peer address.
+ if (image == null || image.length == 0)
+ {
+ String imppAddress = peer.getAlternativeIMPPAddress();
+
+ if (!StringUtils.isNullOrEmpty(imppAddress))
+ {
+ int protocolPartIndex = imppAddress.indexOf(":");
+
+ imppAddress = (protocolPartIndex >= 0)
+ ? imppAddress.substring(protocolPartIndex + 1)
+ : imppAddress;
+
+ Collection<ProtocolProviderService> cusaxProviders
+ = AccountUtils.getRegisteredProviders(
+ OperationSetCusaxUtils.class);
+
+ if (cusaxProviders != null && cusaxProviders.size() > 0)
+ {
+ Contact contact = getPeerContact(
+ peer,
+ cusaxProviders.iterator().next(),
+ imppAddress);
+
+ image = (contact != null) ? getContactImage(contact) : null;
+ }
+ else
+ {
+ MetaContact metaContact
+ = getPeerMetaContact(peer, imppAddress);
+
+ image = (metaContact != null)
+ ? metaContact.getAvatar() : null;
+ }
+ }
+ }
+
+ // If the icon is still null we try to get an image from the call
+ // peer.
+ if ((image == null || image.length == 0)
+ && peer.getImage() != null)
+ image = peer.getImage();
+
+ return image;
+ }
+
+ /**
+ * Searches the cusax enabled providers for a contact with
+ * the detail (address) of the call peer if found and the contact
+ * is provided by a provider which is IM capable, return the contact.
+ * @param peer the peer we are calling.
+ * @return the im capable contact corresponding the <tt>peer</tt>.
+ */
+ public static Contact getIMCapableCusaxContact(CallPeer peer)
+ {
+ // We try to find the <tt>UIContact</tt>, to which the call was
+ // created if this was an outgoing call.
+ UIContactImpl uiContact
+ = CallManager.getCallUIContact(peer.getCall());
+
+ if (uiContact != null)
+ {
+ if(uiContact.getDescriptor() instanceof MetaContact)
+ {
+ MetaContact metaContact =
+ (MetaContact)uiContact.getDescriptor();
+ Iterator<Contact> iter = metaContact.getContacts();
+ while(iter.hasNext())
+ {
+ Contact contact = iter.next();
+ if(contact.getProtocolProvider()
+ .getOperationSet(
+ OperationSetBasicInstantMessaging.class) != null)
+ return contact;
+ }
+ }
+ else if(uiContact.getDescriptor() instanceof SourceContact)
+ {
+ // if it is source contact (history record)
+ // search for cusax contact match
+ Contact contact = getPeerCusaxContact(peer.getAddress(),
+ (SourceContact)uiContact.getDescriptor());
+ if(contact != null
+ && contact.getProtocolProvider().getOperationSet(
+ OperationSetBasicInstantMessaging.class) != null)
+ return contact;
+ }
+ }
+
+ // We try to find the an alternative peer address.
+ String imppAddress = peer.getAlternativeIMPPAddress();
+
+ if (!StringUtils.isNullOrEmpty(imppAddress))
+ {
+ int protocolPartIndex = imppAddress.indexOf(":");
+
+ imppAddress = (protocolPartIndex >= 0)
+ ? imppAddress.substring(protocolPartIndex + 1)
+ : imppAddress;
+
+ Collection<ProtocolProviderService> cusaxProviders
+ = AccountUtils.getRegisteredProviders(
+ OperationSetCusaxUtils.class);
+
+ if (cusaxProviders != null && cusaxProviders.size() > 0)
+ {
+ ProtocolProviderService cusaxProvider
+ = cusaxProviders.iterator().next();
+
+ Contact contact = getPeerContact(
+ peer,
+ cusaxProvider,
+ imppAddress);
+
+ if(contact != null
+ && cusaxProvider.getOperationSet(
+ OperationSetBasicInstantMessaging.class) != null)
+ {
+ return contact;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Find is there a linked cusax protocol provider for this source contact,
+ * if it exist we try to resolve current peer to one of its contacts
+ * or details of a contact (numbers).
+ * @param peerAddress the address of the peer to check
+ * @param sourceContact the currently selected source contact.
+ * @return matching cusax contact.
+ */
+ private static Contact getPeerCusaxContact(
+ String peerAddress, SourceContact sourceContact)
+ {
+ ProtocolProviderService linkedCusaxProvider = null;
+ for(ContactDetail detail : sourceContact.getContactDetails())
+ {
+ ProtocolProviderService pps
+ = detail.getPreferredProtocolProvider(
+ OperationSetBasicTelephony.class);
+
+ if(pps != null)
+ {
+ OperationSetCusaxUtils cusaxOpSet =
+ pps.getOperationSet(OperationSetCusaxUtils.class);
+
+ if(cusaxOpSet != null)
+ {
+ linkedCusaxProvider
+ = cusaxOpSet.getLinkedCusaxProvider();
+ break;
+ }
+ }
+ }
+
+ if(linkedCusaxProvider != null)
+ {
+ OperationSetPersistentPresence opSetPersistentPresence
+ = linkedCusaxProvider.getOperationSet(
+ OperationSetPersistentPresence.class);
+
+ if(opSetPersistentPresence != null)
+ {
+ // will strip the @server-address part, as the regular expression
+ // will match it
+ int index = peerAddress.indexOf("@");
+ String peerUserID =
+ (index > -1) ? peerAddress.substring(0, index) : peerAddress;
+
+ // searches for the whole number/username or with the @serverpart
+ String peerUserIDQ = Pattern.quote(peerUserID);
+
+ Pattern pattern = Pattern.compile(
+ "^(" + peerUserIDQ + "|" + peerUserIDQ + "@.*)$");
+
+ return findContactByPeer(
+ peerUserID,
+ pattern,
+ opSetPersistentPresence.getServerStoredContactListRoot(),
+ linkedCusaxProvider.getOperationSet(
+ OperationSetCusaxUtils.class));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds a matching cusax contact.
+ * @param peerUserID the userID of the call peer to search for
+ * @param searchPattern the pattern (userID | userID@...)
+ * @param parent the parent group of the groups and contacts to search in
+ * @param cusaxOpSet the opset of the provider which will be used to match
+ * contact's details to peer userID (stored numbers).
+ * @return a cusax matching contac
+ */
+ private static Contact findContactByPeer(
+ String peerUserID,
+ Pattern searchPattern,
+ ContactGroup parent,
+ OperationSetCusaxUtils cusaxOpSet)
+ {
+ Iterator<Contact> contactIterator = parent.contacts();
+ while(contactIterator.hasNext())
+ {
+ Contact contact = contactIterator.next();
+
+ if(searchPattern.matcher(contact.getAddress()).find()
+ || cusaxOpSet.doesDetailBelong(contact, peerUserID))
+ {
+ return contact;
+ }
+ }
+
+ Iterator<ContactGroup> groupsIterator = parent.subgroups();
+ while(groupsIterator.hasNext())
+ {
+ ContactGroup gr = groupsIterator.next();
+ Contact contact = findContactByPeer(
+ peerUserID, searchPattern, gr, cusaxOpSet);
+ if(contact != null)
+ return contact;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the image for the given contact.
+ *
+ * @param contact the <tt>Contact</tt>, which image we're looking for
+ * @return the array of bytes representing the image for the given contact
+ * or null if such image doesn't exist
+ */
+ private static byte[] getContactImage(Contact contact)
+ {
+ MetaContact metaContact = GuiActivator.getContactListService()
+ .findMetaContactByContact(contact);
+
+ if (metaContact != null)
+ return metaContact.getAvatar();
+
+ return null;
+ }
+
+ /**
+ * Returns the image for the given <tt>alternativePeerAddress</tt> by
+ * checking the if the <tt>callPeer</tt> exists as a detail in the given
+ * <tt>cusaxProvider</tt>.
+ *
+ * @param callPeer the <tt>CallPeer</tt> to check in the cusax provider
+ * details
+ * @param cusaxProvider the linked cusax <tt>ProtocolProviderService</tt>
+ * @param alternativePeerAddress the alternative peer address to obtain the
+ * image from
+ * @return the protocol <tt>Contact</tt> corresponding to the given
+ * <tt>alternativePeerAddress</tt>
+ */
+ private static Contact getPeerContact( CallPeer callPeer,
+ ProtocolProviderService cusaxProvider,
+ String alternativePeerAddress)
+ {
+ OperationSetPresence presenceOpSet
+ = cusaxProvider.getOperationSet(OperationSetPresence.class);
+
+ if (presenceOpSet == null)
+ return null;
+
+ Contact contact = presenceOpSet.findContactByID(alternativePeerAddress);
+
+ if (contact == null)
+ return null;
+
+ OperationSetCusaxUtils cusaxOpSet
+ = cusaxProvider.getOperationSet(OperationSetCusaxUtils.class);
+
+ if (cusaxOpSet != null && cusaxOpSet.doesDetailBelong(
+ contact, callPeer.getAddress()))
+ return contact;
+
+ return null;
+ }
+
+ /**
+ * Returns the metacontact for the given <tt>CallPeer</tt> by
+ * checking the if the <tt>callPeer</tt> contact exists, if not checks the
+ * contacts in our contact list that are provided by cusax enabled
+ * providers.
+ *
+ * @param peer the <tt>CallPeer</tt> to check in contact details
+ * @return the <tt>MetaContact</tt> corresponding to the given
+ * <tt>peer</tt>.
+ */
+ public static MetaContact getPeerMetaContact(CallPeer peer)
+ {
+ if(peer == null)
+ return null;
+
+ if(peer.getContact() != null)
+ return GuiActivator.getContactListService()
+ .findMetaContactByContact(peer.getContact());
+
+ // We try to find the <tt>UIContact</tt>, to which the call was
+ // created if this was an outgoing call.
+ UIContactImpl uiContact
+ = CallManager.getCallUIContact(peer.getCall());
+
+ if (uiContact != null)
+ {
+ if(uiContact.getDescriptor() instanceof MetaContact)
+ {
+ return (MetaContact)uiContact.getDescriptor();
+ }
+ else if(uiContact.getDescriptor() instanceof SourceContact)
+ {
+ // if it is a source contact check for matching cusax contact
+ Contact contact = getPeerCusaxContact(peer.getAddress(),
+ (SourceContact)uiContact.getDescriptor());
+ if(contact != null)
+ return GuiActivator.getContactListService()
+ .findMetaContactByContact(contact);
+ }
+ }
+
+ String imppAddress = peer.getAlternativeIMPPAddress();
+
+ if (!StringUtils.isNullOrEmpty(imppAddress))
+ {
+ int protocolPartIndex = imppAddress.indexOf(":");
+
+ imppAddress = (protocolPartIndex >= 0)
+ ? imppAddress.substring(protocolPartIndex + 1)
+ : imppAddress;
+
+ Collection<ProtocolProviderService> cusaxProviders
+ = AccountUtils.getRegisteredProviders(
+ OperationSetCusaxUtils.class);
+
+ if (cusaxProviders != null && cusaxProviders.size() > 0)
+ {
+ Contact contact = getPeerContact(
+ peer,
+ cusaxProviders.iterator().next(),
+ imppAddress);
+
+ return GuiActivator.getContactListService()
+ .findMetaContactByContact(contact);
+ }
+ else
+ {
+ return getPeerMetaContact(peer, imppAddress);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the image for the given <tt>alternativePeerAddress</tt> by
+ * checking the if the <tt>callPeer</tt> exists as a detail in one of the
+ * contacts in our contact list.
+ *
+ * @param callPeer the <tt>CallPeer</tt> to check in contact details
+ * @param alternativePeerAddress the alternative peer address to obtain the
+ * image from
+ * @return the <tt>MetaContact</tt> corresponding to the given
+ * <tt>alternativePeerAddress</tt>
+ */
+ private static MetaContact getPeerMetaContact(
+ CallPeer callPeer,
+ String alternativePeerAddress)
+ {
+ Iterator<MetaContact> metaContacts
+ = GuiActivator.getContactListService()
+ .findAllMetaContactsForAddress(alternativePeerAddress);
+
+ while (metaContacts.hasNext())
+ {
+ MetaContact metaContact = metaContacts.next();
+
+ UIPhoneUtil phoneUtil
+ = UIPhoneUtil.getPhoneUtil(metaContact);
+
+ List<UIContactDetail> additionalNumbers
+ = phoneUtil.getAdditionalNumbers();
+
+ if (additionalNumbers == null || additionalNumbers.size() > 0)
+ continue;
+
+ Iterator<UIContactDetail> numbersIter
+ = additionalNumbers.iterator();
+ while (numbersIter.hasNext())
+ {
+ if (numbersIter.next().getAddress()
+ .equals(callPeer.getAddress()))
+ return metaContact;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Opens a call transfer dialog to transfer the given <tt>peer</tt>.
+ * @param peer the <tt>CallPeer</tt> to transfer
+ */
+ public static void openCallTransferDialog(CallPeer peer)
+ {
+ final TransferCallDialog dialog
+ = new TransferCallDialog(peer);
+
+ final Call call = peer.getCall();
+
+ /*
+ * Transferring a call works only when the call is in progress
+ * so close the dialog (if it's not already closed, of course)
+ * once the dialog ends.
+ */
+ CallChangeListener callChangeListener = new CallChangeAdapter()
+ {
+ /*
+ * Overrides
+ * CallChangeAdapter#callStateChanged(CallChangeEvent).
+ */
+ @Override
+ public void callStateChanged(CallChangeEvent evt)
+ {
+ // we are interested only in CALL_STATE_CHANGEs
+ if(!evt.getEventType().equals(
+ CallChangeEvent.CALL_STATE_CHANGE))
+ return;
+
+ if (!CallState.CALL_IN_PROGRESS.equals(call
+ .getCallState()))
+ {
+ dialog.setVisible(false);
+ dialog.dispose();
+ }
+ }
+ };
+ call.addCallChangeListener(callChangeListener);
+ try
+ {
+ dialog.pack();
+ dialog.setVisible(true);
+ }
+ finally
+ {
+ call.removeCallChangeListener(callChangeListener);
+ }
+ }
+
+ /**
+ * Checks whether the <tt>callPeer</tt> supports setting video
+ * quality presets. If quality controls is null, its not supported.
+ * @param callPeer the peer, which video quality we're checking
+ * @return whether call peer supports setting quality preset.
+ */
+ public static boolean isVideoQualityPresetSupported(CallPeer callPeer)
+ {
+ ProtocolProviderService provider = callPeer.getProtocolProvider();
+ OperationSetVideoTelephony videoOpSet
+ = provider.getOperationSet(OperationSetVideoTelephony.class);
+
+ if (videoOpSet == null)
+ return false;
+
+ return videoOpSet.getQualityControl(callPeer) != null;
+ }
+
+ /**
+ * Sets the given quality preset for the video of the given call peer.
+ *
+ * @param callPeer the peer, which video quality we're setting
+ * @param qualityPreset the new quality settings
+ */
+ public static void setVideoQualityPreset(final CallPeer callPeer,
+ final QualityPreset qualityPreset)
+ {
+ ProtocolProviderService provider = callPeer.getProtocolProvider();
+ final OperationSetVideoTelephony videoOpSet
+ = provider.getOperationSet(OperationSetVideoTelephony.class);
+
+ if (videoOpSet == null)
+ return;
+
+ final QualityControl qualityControl =
+ videoOpSet.getQualityControl(callPeer);
+
+ if (qualityControl != null)
+ {
+ new Thread(new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ qualityControl.setPreferredRemoteSendMaxPreset(
+ qualityPreset);
+ }
+ catch(org.jitsi.service.protocol.OperationFailedException e)
+ {
+ logger.info("Unable to change video quality.", e);
+
+ ResourceManagementService resources
+ = GuiActivator.getResources();
+
+ new ErrorDialog(
+ null,
+ resources.getI18NString("service.gui.WARNING"),
+ resources.getI18NString(
+ "service.gui.UNABLE_TO_CHANGE_VIDEO_QUALITY"),
+ e)
+ .showDialog();
+ }
+ }
+ }).start();
+ }
+ }
+
+ /**
+ * Indicates if we have video streams to show in this interface.
+ *
+ * @param call the call to check for video streaming
+ * @return <tt>true</tt> if we have video streams to show in this interface;
+ * otherwise, <tt>false</tt>
+ */
+ public static boolean isVideoStreaming(Call call)
+ {
+ return isVideoStreaming(call.getConference());
+ }
+
+ /**
+ * Indicates if we have video streams to show in this interface.
+ *
+ * @param conference the conference we check for video streaming
+ * @return <tt>true</tt> if we have video streams to show in this interface;
+ * otherwise, <tt>false</tt>
+ */
+ public static boolean isVideoStreaming(CallConference conference)
+ {
+ for (Call call : conference.getCalls())
+ {
+ OperationSetVideoTelephony videoTelephony
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+
+ if (videoTelephony == null)
+ continue;
+
+ if (videoTelephony.isLocalVideoStreaming(call))
+ return true;
+
+ Iterator<? extends CallPeer> callPeers = call.getCallPeers();
+
+ while (callPeers.hasNext())
+ {
+ List<Component> remoteVideos
+ = videoTelephony.getVisualComponents(callPeers.next());
+
+ if ((remoteVideos != null) && (remoteVideos.size() > 0))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether two specific addresses refer to one and the same
+ * peer/resource/contact.
+ * <p>
+ * <b>Warning</b>: Use the functionality sparingly because it assumes that
+ * an unspecified service is equal to any service.
+ * </p>
+ *
+ * @param a one of the addresses to be compared
+ * @param b the other address to be compared to <tt>a</tt>
+ * @return <tt>true</tt> if <tt>a</tt> and <tt>b</tt> name one and the same
+ * peer/resource/contact; <tt>false</tt>, otherwise
+ */
+ public static boolean addressesAreEqual(String a, String b)
+ {
+ if (a.equals(b))
+ return true;
+
+ int aProtocolIndex = a.indexOf(':');
+ if(aProtocolIndex != -1)
+ a = a.substring(aProtocolIndex + 1);
+
+ int bProtocolIndex = b.indexOf(':');
+ if(bProtocolIndex != -1)
+ b = b.substring(bProtocolIndex + 1);
+
+ if (a.equals(b))
+ return true;
+
+ int aServiceBegin = a.indexOf('@', aProtocolIndex);
+ String aUserID;
+ String aService;
+
+ if (aServiceBegin != -1)
+ {
+ aUserID = a.substring(0, aServiceBegin);
+ ++aServiceBegin;
+
+ int aResourceBegin = a.indexOf('/', aServiceBegin);
+ if (aResourceBegin != -1)
+ aService = a.substring(aServiceBegin, aResourceBegin);
+ else
+ aService = a.substring(aServiceBegin);
+ }
+ else
+ {
+ aUserID = a;
+ aService = null;
+ }
+
+ int bServiceBegin = b.indexOf('@', bProtocolIndex);
+ String bUserID;
+ String bService;
+
+ if (bServiceBegin != -1)
+ {
+ bUserID = b.substring(0, bServiceBegin);
+ ++bServiceBegin;
+
+ int bResourceBegin = b.indexOf('/', bServiceBegin);
+ if (bResourceBegin != -1)
+ bService = b.substring(bServiceBegin, bResourceBegin);
+ else
+ bService = b.substring(bServiceBegin);
+ }
+ else
+ {
+ bUserID = b;
+ bService = null;
+ }
+
+ boolean userIDsAreEqual;
+
+ if ((aUserID == null) || (aUserID.length() < 1))
+ userIDsAreEqual = ((bUserID == null) || (bUserID.length() < 1));
+ else
+ userIDsAreEqual = aUserID.equals(bUserID);
+ if (!userIDsAreEqual)
+ return false;
+
+ boolean servicesAreEqual;
+
+ /*
+ * It's probably a veeery long shot but it's assumed here that an
+ * unspecified service is equal to any service. Such a case is, for
+ * example, RegistrarLess SIP.
+ */
+ if (((aService == null) || (aService.length() < 1))
+ || ((bService == null) || (bService.length() < 1)))
+ servicesAreEqual = true;
+ else
+ servicesAreEqual = aService.equals(bService);
+
+ return servicesAreEqual;
+ }
+
+ /**
+ * Indicates if the given <tt>ConferenceMember</tt> corresponds to the local
+ * user.
+ *
+ * @param conferenceMember the conference member to check
+ * @return <tt>true</tt> if the given <tt>conferenceMember</tt> is the local
+ * user, <tt>false</tt> - otherwise
+ */
+ public static boolean isLocalUser(ConferenceMember conferenceMember)
+ {
+ String localUserAddress
+ = conferenceMember.getConferenceFocusCallPeer()
+ .getProtocolProvider().getAccountID().getAccountAddress();
+
+ return CallManager.addressesAreEqual(
+ conferenceMember.getAddress(), localUserAddress);
+ }
+
+ /**
+ * Adds a missed call notification.
+ *
+ * @param peerName the name of the peer
+ * @param callTime the time of the call
+ */
+ private static void addMissedCallNotification(String peerName, long callTime)
+ {
+ if (missedCallGroup == null)
+ {
+ missedCallGroup
+ = new UINotificationGroup(
+ "MissedCalls",
+ GuiActivator.getResources().getI18NString(
+ "service.gui.MISSED_CALLS_TOOL_TIP"));
+ }
+
+ UINotificationManager.addNotification(
+ new UINotification(peerName, callTime, missedCallGroup));
+ }
+
+ /**
+ * Returns of supported/enabled list of audio formats for a provider.
+ * @param device the <tt>MediaDevice</tt>, which audio formats we're
+ * looking for
+ * @param protocolProvider the provider to check.
+ * @return list of supported/enabled auido formats or empty list
+ * otherwise.
+ */
+ private static List<MediaFormat> getAudioFormats(
+ MediaDevice device,
+ ProtocolProviderService protocolProvider)
+ {
+ List<MediaFormat> res = new ArrayList<MediaFormat>();
+
+ Map<String, String> accountProperties
+ = protocolProvider.getAccountID().getAccountProperties();
+ String overrideEncodings
+ = accountProperties.get(ProtocolProviderFactory.OVERRIDE_ENCODINGS);
+
+ List<MediaFormat> formats;
+ if(Boolean.parseBoolean(overrideEncodings))
+ {
+ /*
+ * The account properties associated with account
+ * override the global EncodingConfiguration.
+ */
+ EncodingConfiguration encodingConfiguration
+ = ProtocolMediaActivator.getMediaService()
+ .createEmptyEncodingConfiguration();
+
+ encodingConfiguration.loadProperties(
+ accountProperties,
+ ProtocolProviderFactory.ENCODING_PROP_PREFIX);
+
+ formats = device.getSupportedFormats(
+ null, null, encodingConfiguration);
+ }
+ else /* The global EncodingConfiguration is in effect. */
+ {
+ formats = device.getSupportedFormats();
+ }
+
+ // skip the special telephony event
+ for(MediaFormat format : formats)
+ {
+ if(!format.getEncoding().equals(Constants.TELEPHONE_EVENT))
+ res.add(format);
+ }
+
+ return res;
+ }
+
+ /**
+ * Creates a new (audio-only or video) <tt>Call</tt> to a contact specified
+ * as a <tt>Contact</tt> instance or a <tt>String</tt> contact
+ * address/identifier.
+ */
+ private static class CreateCallThread
+ extends Thread
+ {
+ /**
+ * The contact to call.
+ */
+ private final Contact contact;
+
+ /**
+ * The specific contact resource to call.
+ */
+ private final ContactResource contactResource;
+
+ /**
+ * The <tt>UIContactImpl</tt> we're calling.
+ */
+ private final UIContactImpl uiContact;
+
+ /**
+ * The protocol provider through which the call goes.
+ */
+ private final ProtocolProviderService protocolProvider;
+
+ /**
+ * The string to call.
+ */
+ private final String stringContact;
+
+ /**
+ * The description of a conference to call, if any.
+ */
+ private final ConferenceDescription conferenceDescription;
+
+ /**
+ * The indicator which determines whether this instance is to create a
+ * new video (as opposed to audio-only) <tt>Call</tt>.
+ */
+ private final boolean video;
+
+ /**
+ * The chat room associated with the call.
+ */
+ private final ChatRoom chatRoom;
+
+ /**
+ * Creates an instance of <tt>CreateCallThread</tt>.
+ *
+ * @param protocolProvider the protocol provider through which the call
+ * is going.
+ * @param contact the contact to call
+ * @param contactResource the specific <tt>ContactResource</tt> we're
+ * calling
+ * @param video indicates if this is a video call
+ */
+ public CreateCallThread(
+ ProtocolProviderService protocolProvider,
+ Contact contact,
+ ContactResource contactResource,
+ boolean video)
+ {
+ this(protocolProvider, contact, contactResource, null, null, null,
+ null, video);
+ }
+
+ /**
+ * Creates an instance of <tt>CreateCallThread</tt>.
+ *
+ * @param protocolProvider the protocol provider through which the call
+ * is going.
+ * @param contact the contact to call
+ * @param video indicates if this is a video call
+ */
+ public CreateCallThread(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ boolean video)
+ {
+ this(protocolProvider, null, null, null, contact, null, null, video);
+ }
+
+ /**
+ * Initializes a new <tt>CreateCallThread</tt> instance which is to
+ * create a new <tt>Call</tt> to a conference specified via a
+ * <tt>ConferenceDescription</tt>.
+ * @param protocolProvider the <tt>ProtocolProviderService</tt> which is
+ * to perform the establishment of the new <tt>Call</tt>.
+ * @param conferenceDescription the description of the conference to
+ * call.
+ * @param chatRoom the chat room associated with the call.
+ */
+ public CreateCallThread(
+ ProtocolProviderService protocolProvider,
+ ConferenceDescription conferenceDescription,
+ ChatRoom chatRoom)
+ {
+ this(protocolProvider, null, null, null, null,
+ conferenceDescription, chatRoom,
+ false /* video */);
+ }
+
+ /**
+ * Initializes a new <tt>CreateCallThread</tt> instance which is to
+ * create a new <tt>Call</tt> to a contact specified either as a
+ * <tt>Contact</tt> instance or as a <tt>String</tt> contact
+ * address/identifier.
+ * <p>
+ * The constructor is private because it relies on its arguments being
+ * validated prior to its invocation.
+ * </p>
+ *
+ * @param protocolProvider the <tt>ProtocolProviderService</tt> which is
+ * to perform the establishment of the new <tt>Call</tt>
+ * @param contact the contact to call
+ * @param contactResource the specific contact resource to call
+ * @param uiContact the ui contact we're calling
+ * @param stringContact the string to call
+ * @param video <tt>true</tt> if this instance is to create a new video
+ * (as opposed to audio-only) <tt>Call</tt>
+ * @param conferenceDescription the description of a conference to call
+ * @param chatRoom the chat room associated with the call.
+ */
+ public CreateCallThread(
+ ProtocolProviderService protocolProvider,
+ Contact contact,
+ ContactResource contactResource,
+ UIContactImpl uiContact,
+ String stringContact,
+ ConferenceDescription conferenceDescription,
+ ChatRoom chatRoom,
+ boolean video)
+ {
+ this.protocolProvider = protocolProvider;
+ this.contact = contact;
+ this.contactResource = contactResource;
+ this.uiContact = uiContact;
+ this.stringContact = stringContact;
+ this.video = video;
+ this.conferenceDescription = conferenceDescription;
+ this.chatRoom = chatRoom;
+ }
+
+ @Override
+ public void run()
+ {
+ if(!video)
+ {
+ // if it is not video let's check for available audio codecs
+ // and available audio devices
+ MediaService mediaService = GuiActivator.getMediaService();
+ MediaDevice dev = mediaService.getDefaultDevice(
+ MediaType.AUDIO, MediaUseCase.CALL);
+
+ List<MediaFormat> formats
+ = getAudioFormats(dev, protocolProvider);
+
+ String errorMsg = null;
+
+ if(!dev.getDirection().allowsSending())
+ errorMsg = GuiActivator.getResources().getI18NString(
+ "service.gui.CALL_NO_AUDIO_DEVICE");
+ else if(formats.isEmpty())
+ {
+ errorMsg = GuiActivator.getResources().getI18NString(
+ "service.gui.CALL_NO_AUDIO_CODEC");
+ }
+
+ if(errorMsg != null)
+ {
+ if(GuiActivator.getUIService()
+ .getPopupDialog().showConfirmPopupDialog(
+ errorMsg + " " +
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CALL_NO_DEVICE_CODECS_Q"),
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CALL"),
+ PopupDialog.YES_NO_OPTION,
+ PopupDialog.QUESTION_MESSAGE)
+ == PopupDialog.NO_OPTION)
+ {
+ return;
+ }
+ }
+ }
+
+ Contact contact = this.contact;
+ String stringContact = this.stringContact;
+
+ if (ConfigurationUtils.isNormalizePhoneNumber())
+ {
+ if (contact != null)
+ {
+ stringContact = contact.getAddress();
+ contact = null;
+ }
+
+ if (stringContact != null)
+ {
+ stringContact
+ = PhoneNumberI18nService.normalize(stringContact);
+ }
+ }
+
+ try
+ {
+ if (conferenceDescription != null)
+ {
+ internalCall( protocolProvider,
+ conferenceDescription,
+ chatRoom);
+ }
+ else
+ {
+ if (video)
+ {
+ internalCallVideo( protocolProvider,
+ contact,
+ uiContact,
+ stringContact);
+ }
+ else
+ {
+ internalCall( protocolProvider,
+ contact,
+ stringContact,
+ contactResource,
+ uiContact);
+ }
+ }
+ }
+ catch (Throwable t)
+ {
+ if (t instanceof ThreadDeath)
+ throw (ThreadDeath) t;
+
+ logger.error("The call could not be created: ", t);
+
+ String message = GuiActivator.getResources()
+ .getI18NString("servoce.gui.CREATE_CALL_FAILED");
+
+ if (t.getMessage() != null)
+ message += " " + t.getMessage();
+
+ new ErrorDialog(
+ null,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.ERROR"),
+ message,
+ t)
+ .showDialog();
+ }
+ }
+ }
+
+ /**
+ * Creates a video call through the given <tt>protocolProvider</tt>.
+ *
+ * @param protocolProvider the <tt>ProtocolProviderService</tt> through
+ * which to make the call
+ * @param contact the <tt>Contact</tt> to call
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ * @param stringContact the contact string to call
+ *
+ * @throws OperationFailedException thrown if the call operation fails
+ * @throws ParseException thrown if the contact string is malformated
+ */
+ private static void internalCallVideo(
+ ProtocolProviderService protocolProvider,
+ Contact contact,
+ UIContactImpl uiContact,
+ String stringContact)
+ throws OperationFailedException,
+ ParseException
+ {
+ OperationSetVideoTelephony telephony
+ = protocolProvider.getOperationSet(
+ OperationSetVideoTelephony.class);
+
+ Call createdCall = null;
+ if (telephony != null)
+ {
+ if (contact != null)
+ {
+ createdCall = telephony.createVideoCall(contact);
+ }
+ else if (stringContact != null)
+ createdCall = telephony.createVideoCall(stringContact);
+ }
+
+ if (uiContact != null && createdCall != null)
+ addUIContactCall(uiContact, createdCall);
+ }
+
+ /**
+ * Creates a call through the given <tt>protocolProvider</tt>.
+ *
+ * @param protocolProvider the <tt>ProtocolProviderService</tt> through
+ * which to make the call
+ * @param contact the <tt>Contact</tt> to call
+ * @param stringContact the contact string to call
+ * @param contactResource the specific <tt>ContactResource</tt> to call
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ *
+ * @throws OperationFailedException thrown if the call operation fails
+ * @throws ParseException thrown if the contact string is malformated
+ */
+ private static void internalCall(
+ ProtocolProviderService protocolProvider,
+ Contact contact,
+ String stringContact,
+ ContactResource contactResource,
+ UIContactImpl uiContact)
+ throws OperationFailedException,
+ ParseException
+ {
+ OperationSetBasicTelephony<?> telephony
+ = protocolProvider.getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ OperationSetResourceAwareTelephony resourceTelephony
+ = protocolProvider.getOperationSet(
+ OperationSetResourceAwareTelephony.class);
+
+ Call createdCall = null;
+
+ if (resourceTelephony != null && contactResource != null)
+ {
+ if (contact != null)
+ createdCall
+ = resourceTelephony.createCall(contact, contactResource);
+ else if (!StringUtils.isNullOrEmpty(stringContact))
+ createdCall = resourceTelephony.createCall(
+ stringContact, contactResource.getResourceName());
+ }
+ else if (telephony != null)
+ {
+ if (contact != null)
+ {
+ createdCall = telephony.createCall(contact);
+ }
+ else if (!StringUtils.isNullOrEmpty(stringContact))
+ createdCall = telephony.createCall(stringContact);
+ }
+
+ if (uiContact != null && createdCall != null)
+ addUIContactCall(uiContact, createdCall);
+ }
+
+ /**
+ * Creates a call through the given <tt>protocolProvider</tt>.
+ *
+ * @param protocolProvider the <tt>ProtocolProviderService</tt> through
+ * which to make the call
+ * @param conferenceDescription the description of the conference to call
+ * @param chatRoom the chat room associated with the call.
+ */
+ private static void internalCall(ProtocolProviderService protocolProvider,
+ ConferenceDescription conferenceDescription,
+ ChatRoom chatRoom)
+ throws OperationFailedException
+ {
+ OperationSetBasicTelephony<?> telephony
+ = protocolProvider.getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ if (telephony != null)
+ {
+ telephony.createCall(conferenceDescription, chatRoom);
+ }
+ }
+
+ /**
+ * Returns the <tt>MetaContact</tt>, to which the given <tt>Call</tt>
+ * was initially created.
+ *
+ * @param call the <tt>Call</tt>, which corresponding <tt>MetaContact</tt>
+ * we're looking for
+ * @return the <tt>UIContactImpl</tt>, to which the given <tt>Call</tt>
+ * was initially created
+ */
+ public static UIContactImpl getCallUIContact(Call call)
+ {
+ if (uiContactCalls != null)
+ return uiContactCalls.get(call);
+ return null;
+ }
+
+ /**
+ * Adds a call for a <tt>metaContact</tt>.
+ *
+ * @param uiContact the <tt>UIContact</tt> corresponding to the call
+ * @param call the <tt>Call</tt> corresponding to the <tt>MetaContact</tt>
+ */
+ private static void addUIContactCall( UIContactImpl uiContact,
+ Call call)
+ {
+ if (uiContactCalls == null)
+ uiContactCalls = new WeakHashMap<Call, UIContactImpl>();
+
+ uiContactCalls.put(call, uiContact);
+ }
+
+ /**
+ * Creates a desktop sharing session with the given Contact or a given
+ * String.
+ */
+ private static class CreateDesktopSharingThread
+ extends Thread
+ {
+ /**
+ * The string contact to share the desktop with.
+ */
+ private final String stringContact;
+
+ /**
+ * The protocol provider through which we share our desktop.
+ */
+ private final ProtocolProviderService protocolProvider;
+
+ /**
+ * The media device corresponding to the screen we would like to share.
+ */
+ private final MediaDevice mediaDevice;
+
+ /**
+ * The <tt>UIContactImpl</tt> we're calling.
+ */
+ private final UIContactImpl uiContact;
+
+ /**
+ * Creates a desktop sharing session thread.
+ *
+ * @param protocolProvider protocol provider through which we share our
+ * desktop
+ * @param contact the contact to share the desktop with
+ * @param uiContact the <tt>UIContact</tt>, which initiated the desktop
+ * sharing session
+ * @param mediaDevice the media device corresponding to the screen we
+ * would like to share
+ */
+ public CreateDesktopSharingThread(
+ ProtocolProviderService protocolProvider,
+ String contact,
+ UIContactImpl uiContact,
+ MediaDevice mediaDevice)
+ {
+ this.protocolProvider = protocolProvider;
+ this.stringContact = contact;
+ this.uiContact = uiContact;
+ this.mediaDevice = mediaDevice;
+ }
+
+ @Override
+ public void run()
+ {
+ OperationSetDesktopSharingServer desktopSharingOpSet
+ = protocolProvider.getOperationSet(
+ OperationSetDesktopSharingServer.class);
+
+ /*
+ * XXX If we are here and we just discover that
+ * OperationSetDesktopSharingServer is not supported, then we're
+ * already in trouble - we've already started a whole new thread
+ * just to check that a reference is null.
+ */
+ if (desktopSharingOpSet == null)
+ return;
+
+ Throwable exception = null;
+
+ Call createdCall = null;
+ try
+ {
+ if (mediaDevice != null)
+ {
+ createdCall = desktopSharingOpSet.createVideoCall(
+ stringContact,
+ mediaDevice);
+ }
+ else
+ createdCall
+ = desktopSharingOpSet.createVideoCall(stringContact);
+ }
+ catch (OperationFailedException e)
+ {
+ exception = e;
+ }
+ catch (ParseException e)
+ {
+ exception = e;
+ }
+ if (exception != null)
+ {
+ logger.error("The call could not be created: ", exception);
+
+ new ErrorDialog(
+ null,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.ERROR"),
+ exception.getMessage(),
+ ErrorDialog.ERROR)
+ .showDialog();
+ }
+
+ if (uiContact != null && createdCall != null)
+ addUIContactCall(uiContact, createdCall);
+ }
+ }
+
+ /**
+ * Answers to all <tt>CallPeer</tt>s associated with a specific
+ * <tt>Call</tt> and, optionally, does that in a telephony conference with
+ * an existing <tt>Call</tt>.
+ */
+ private static class AnswerCallThread
+ extends Thread
+ {
+ /**
+ * The <tt>Call</tt> which is to be answered.
+ */
+ private final Call call;
+
+ /**
+ * The existing <tt>Call</tt>, if any, which represents a telephony
+ * conference in which {@link #call} is to be answered.
+ */
+ private final Call existingCall;
+
+ /**
+ * The indicator which determines whether this instance is to answer
+ * {@link #call} with video.
+ */
+ private final boolean video;
+
+ public AnswerCallThread(Call call, Call existingCall, boolean video)
+ {
+ this.call = call;
+ this.existingCall = existingCall;
+ this.video = video;
+ }
+
+ @Override
+ public void run()
+ {
+ if (existingCall != null)
+ call.setConference(existingCall.getConference());
+
+ ProtocolProviderService pps = call.getProtocolProvider();
+ Iterator<? extends CallPeer> peers = call.getCallPeers();
+
+ while (peers.hasNext())
+ {
+ CallPeer peer = peers.next();
+
+ if (video)
+ {
+ OperationSetVideoTelephony telephony
+ = pps.getOperationSet(OperationSetVideoTelephony.class);
+
+ try
+ {
+ telephony.answerVideoCallPeer(peer);
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error(
+ "Could not answer "
+ + peer
+ + " with video"
+ + " because of the following exception: "
+ + ofe);
+ }
+ }
+ else
+ {
+ OperationSetBasicTelephony<?> telephony
+ = pps.getOperationSet(OperationSetBasicTelephony.class);
+
+ try
+ {
+ telephony.answerCallPeer(peer);
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error(
+ "Could not answer "
+ + peer
+ + " because of the following exception: ",
+ ofe);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Invites a list of callees to a conference <tt>Call</tt>. If the specified
+ * <tt>Call</tt> is <tt>null</tt>, creates a brand new telephony conference.
+ */
+ private static class InviteToConferenceCallThread
+ extends Thread
+ {
+ /**
+ * The addresses of the callees to be invited into the telephony
+ * conference to be organized by this instance. For further details,
+ * refer to the documentation on the <tt>callees</tt> parameter of the
+ * respective <tt>InviteToConferenceCallThread</tt> constructor.
+ */
+ private final Map<ProtocolProviderService, List<String>>
+ callees;
+
+ /**
+ * The <tt>Call</tt>, if any, into the telephony conference of which
+ * {@link #callees} are to be invited. If non-<tt>null</tt>, its
+ * <tt>CallConference</tt> state will be shared with all <tt>Call</tt>s
+ * established by this instance for the purposes of having the
+ * <tt>callees</tt> into the same telephony conference.
+ */
+ private final Call call;
+
+ /**
+ * Initializes a new <tt>InviteToConferenceCallThread</tt> instance
+ * which is to invite a list of callees to a conference <tt>Call</tt>.
+ * If the specified <tt>call</tt> is <tt>null</tt>, creates a brand new
+ * telephony conference.
+ *
+ * @param callees the addresses of the callees to be invited into a
+ * telephony conference. The addresses are provided in multiple
+ * <tt>List<String></tt>s. Each such list of addresses is mapped
+ * by the <tt>ProtocolProviderService</tt> through which they are to be
+ * invited into the telephony conference. If there are multiple
+ * <tt>ProtocolProviderService</tt>s in the specified <tt>Map</tt>, the
+ * resulting telephony conference is known by the name
+ * "cross-protocol". It is also allowed to have a list of
+ * addresses mapped to <tt>null</tt> which means that the new instance
+ * will automatically choose a <tt>ProtocolProviderService</tt> to
+ * invite the respective callees into the telephony conference.
+ * @param call the <tt>Call</tt> to invite the specified
+ * <tt>callees</tt> into. If <tt>null</tt>, this instance will create a
+ * brand new telephony conference. Technically, a <tt>Call</tt> instance
+ * is protocol/account-specific and it is possible to have
+ * cross-protocol/account telephony conferences. That's why the
+ * specified <tt>callees</tt> are invited into one and the same
+ * <tt>CallConference</tt>: the one in which the specified <tt>call</tt>
+ * is participating or a new one if <tt>call</tt> is <tt>null</tt>. Of
+ * course, an attempt is made to have all callees from one and the same
+ * protocol/account into one <tt>Call</tt> instance.
+ */
+ public InviteToConferenceCallThread(
+ Map<ProtocolProviderService, List<String>> callees,
+ Call call)
+ {
+ this.callees = callees;
+ this.call = call;
+ }
+
+ /**
+ * Invites {@link #callees} into a telephony conference which is
+ * optionally specified by {@link #call}.
+ */
+ @Override
+ public void run()
+ {
+ CallConference conference
+ = (call == null) ? null : call.getConference();
+
+ for(Map.Entry<ProtocolProviderService, List<String>> entry
+ : callees.entrySet())
+ {
+ ProtocolProviderService pps = entry.getKey();
+
+ /*
+ * We'd like to allow specifying callees without specifying an
+ * associated ProtocolProviderService.
+ */
+ if (pps != null)
+ {
+ OperationSetBasicTelephony<?> basicTelephony
+ = pps.getOperationSet(OperationSetBasicTelephony.class);
+
+ if(basicTelephony == null)
+ continue;
+ }
+
+ List<String> contactList = entry.getValue();
+ String[] contactArray
+ = contactList.toArray(new String[contactList.size()]);
+
+ if (ConfigurationUtils.isNormalizePhoneNumber())
+ normalizePhoneNumbers(contactArray);
+
+ /* Try to have a single Call per ProtocolProviderService. */
+ Call ppsCall;
+
+ if ((call != null) && call.getProtocolProvider().equals(pps))
+ ppsCall = call;
+ else
+ {
+ ppsCall = null;
+ if (conference != null)
+ {
+ List<Call> conferenceCalls = conference.getCalls();
+
+ if (pps == null)
+ {
+ /*
+ * We'd like to allow specifying callees without
+ * specifying an associated ProtocolProviderService.
+ * The simplest approach is to just choose the first
+ * ProtocolProviderService involved in the telephony
+ * conference.
+ */
+ if (call == null)
+ {
+ if (!conferenceCalls.isEmpty())
+ {
+ ppsCall = conferenceCalls.get(0);
+ pps = ppsCall.getProtocolProvider();
+ }
+ }
+ else
+ {
+ ppsCall = call;
+ pps = ppsCall.getProtocolProvider();
+ }
+ }
+ else
+ {
+ for (Call conferenceCall : conferenceCalls)
+ {
+ if (pps.equals(
+ conferenceCall.getProtocolProvider()))
+ {
+ ppsCall = conferenceCall;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ OperationSetTelephonyConferencing telephonyConferencing
+ = pps.getOperationSet(
+ OperationSetTelephonyConferencing.class);
+
+ try
+ {
+ if (ppsCall == null)
+ {
+ ppsCall
+ = telephonyConferencing.createConfCall(
+ contactArray,
+ conference);
+ if (conference == null)
+ conference = ppsCall.getConference();
+ }
+ else
+ {
+ for (String contact : contactArray)
+ {
+ telephonyConferencing.inviteCalleeToCall(
+ contact,
+ ppsCall);
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error(
+ "Failed to invite callees: "
+ + Arrays.toString(contactArray),
+ e);
+ new ErrorDialog(
+ null,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.ERROR"),
+ e.getMessage(),
+ ErrorDialog.ERROR)
+ .showDialog();
+ }
+ }
+ }
+ }
+
+ /**
+ * Invites a list of callees to a specific conference <tt>Call</tt>. If the
+ * specified <tt>Call</tt> is <tt>null</tt>, creates a brand new telephony
+ * conference.
+ */
+ private static class InviteToConferenceBridgeThread
+ extends Thread
+ {
+ private final ProtocolProviderService callProvider;
+
+ private final String[] callees;
+
+ private final Call call;
+
+ public InviteToConferenceBridgeThread(
+ ProtocolProviderService callProvider,
+ String[] callees,
+ Call call)
+ {
+ this.callProvider = callProvider;
+ this.callees = callees;
+ this.call = call;
+ }
+
+ @Override
+ public void run()
+ {
+ OperationSetVideoBridge opSetVideoBridge
+ = callProvider.getOperationSet(
+ OperationSetVideoBridge.class);
+
+ // Normally if this method is called then this should not happen
+ // but we check in order to be sure to be able to proceed.
+ if (opSetVideoBridge == null || !opSetVideoBridge.isActive())
+ return;
+
+ if (ConfigurationUtils.isNormalizePhoneNumber())
+ normalizePhoneNumbers(callees);
+
+ try
+ {
+ if (call == null)
+ {
+ opSetVideoBridge.createConfCall(callees);
+ }
+ else
+ {
+ for (String contact : callees)
+ opSetVideoBridge.inviteCalleeToCall(contact, call);
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error(
+ "Failed to invite callees: "
+ + Arrays.toString(callees),
+ e);
+ new ErrorDialog(
+ null,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.ERROR"),
+ e.getMessage(),
+ e)
+ .showDialog();
+ }
+ }
+ }
+
+ /**
+ * Hangs up a specific <tt>Call</tt> (i.e. all <tt>CallPeer</tt>s associated
+ * with a <tt>Call</tt>), <tt>CallConference</tt> (i.e. all <tt>Call</tt>s
+ * participating in a <tt>CallConference</tt>), or <tt>CallPeer</tt>.
+ */
+ private static class HangupCallThread
+ extends Thread
+ {
+ private final Call call;
+
+ private final CallConference conference;
+
+ private final CallPeer peer;
+
+ /**
+ * Initializes a new <tt>HangupCallThread</tt> instance which is to hang
+ * up a specific <tt>Call</tt> i.e. all <tt>CallPeer</tt>s associated
+ * with the <tt>Call</tt>.
+ *
+ * @param call the <tt>Call</tt> whose associated <tt>CallPeer</tt>s are
+ * to be hanged up
+ */
+ public HangupCallThread(Call call)
+ {
+ this(call, null, null);
+ }
+
+ /**
+ * Initializes a new <tt>HangupCallThread</tt> instance which is to hang
+ * up a specific <tt>CallConference</tt> i.e. all <tt>Call</tt>s
+ * participating in the <tt>CallConference</tt>.
+ *
+ * @param conference the <tt>CallConference</tt> whose participating
+ * <tt>Call</tt>s re to be hanged up
+ */
+ public HangupCallThread(CallConference conference)
+ {
+ this(null, conference, null);
+ }
+
+ /**
+ * Initializes a new <tt>HangupCallThread</tt> instance which is to hang
+ * up a specific <tt>CallPeer</tt>.
+ *
+ * @param peer the <tt>CallPeer</tt> to hang up
+ */
+ public HangupCallThread(CallPeer peer)
+ {
+ this(null, null, peer);
+ }
+
+ /**
+ * Initializes a new <tt>HangupCallThread</tt> instance which is to hang
+ * up a specific <tt>Call</tt>, <tt>CallConference</tt>, or
+ * <tt>CallPeer</tt>.
+ *
+ * @param call the <tt>Call</tt> whose associated <tt>CallPeer</tt>s are
+ * to be hanged up
+ * @param conference the <tt>CallConference</tt> whose participating
+ * <tt>Call</tt>s re to be hanged up
+ * @param peer the <tt>CallPeer</tt> to hang up
+ */
+ private HangupCallThread(
+ Call call,
+ CallConference conference,
+ CallPeer peer)
+ {
+ this.call = call;
+ this.conference = conference;
+ this.peer = peer;
+ }
+
+ @Override
+ public void run()
+ {
+ /*
+ * There is only an OperationSet which hangs up a CallPeer at a time
+ * so prepare a list of all CallPeers to be hanged up.
+ */
+ Set<CallPeer> peers = new HashSet<CallPeer>();
+
+ if (call != null)
+ {
+ Iterator<? extends CallPeer> peerIter = call.getCallPeers();
+
+ while (peerIter.hasNext())
+ peers.add(peerIter.next());
+ }
+
+ if (conference != null)
+ peers.addAll(conference.getCallPeers());
+
+ if (peer != null)
+ peers.add(peer);
+
+ for (CallPeer peer : peers)
+ {
+ OperationSetBasicTelephony<?> basicTelephony
+ = peer.getProtocolProvider().getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ try
+ {
+ basicTelephony.hangupCallPeer(peer);
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error("Could not hang up: " + peer, ofe);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates the enable local video call thread.
+ */
+ private static class EnableLocalVideoThread
+ extends Thread
+ {
+ private final Call call;
+
+ private final boolean enable;
+
+ /**
+ * Creates the enable local video call thread.
+ *
+ * @param call the call, for which to enable/disable
+ * @param enable
+ */
+ public EnableLocalVideoThread(Call call, boolean enable)
+ {
+ this.call = call;
+ this.enable = enable;
+ }
+
+ @Override
+ public void run()
+ {
+ OperationSetVideoTelephony videoTelephony
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+ boolean enableSucceeded = false;
+
+ if (videoTelephony != null)
+ {
+ // First make sure the desktop sharing is disabled.
+ if (enable && isDesktopSharingEnabled(call))
+ {
+ JFrame frame = DesktopSharingFrame.getFrameForCall(call);
+
+ if (frame != null)
+ frame.dispose();
+ }
+
+ try
+ {
+ videoTelephony.setLocalVideoAllowed(call, enable);
+ enableSucceeded = true;
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error(
+ "Failed to toggle the streaming of local video.",
+ ex);
+
+ ResourceManagementService r = GuiActivator.getResources();
+ String title
+ = r.getI18NString(
+ "service.gui.LOCAL_VIDEO_ERROR_TITLE");
+ String message
+ = r.getI18NString(
+ "service.gui.LOCAL_VIDEO_ERROR_MESSAGE");
+
+ GuiActivator.getAlertUIService().showAlertPopup(
+ title,
+ message,
+ ex);
+ }
+ }
+
+ // If the operation didn't succeeded for some reason, make sure the
+ // video button doesn't remain selected.
+ if (enable && !enableSucceeded)
+ getActiveCallContainer(call).setVideoButtonSelected(false);
+ }
+ }
+
+ /**
+ * Puts on hold the given <tt>CallPeer</tt>.
+ */
+ private static class PutOnHoldCallPeerThread
+ extends Thread
+ {
+ private final CallPeer callPeer;
+
+ private final boolean isOnHold;
+
+ public PutOnHoldCallPeerThread(CallPeer callPeer, boolean isOnHold)
+ {
+ this.callPeer = callPeer;
+ this.isOnHold = isOnHold;
+ }
+
+ @Override
+ public void run()
+ {
+ OperationSetBasicTelephony<?> telephony
+ = callPeer.getProtocolProvider().getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ try
+ {
+ if (isOnHold)
+ telephony.putOnHold(callPeer);
+ else
+ telephony.putOffHold(callPeer);
+ }
+ catch (OperationFailedException ex)
+ {
+ logger.error(
+ "Failed to put"
+ + callPeer.getAddress()
+ + (isOnHold ? " on hold." : " off hold."),
+ ex);
+ }
+ }
+ }
+
+ /**
+ * Merges specific existing <tt>Call</tt>s into a specific telephony
+ * conference.
+ */
+ private static class MergeExistingCalls
+ extends Thread
+ {
+ /**
+ * The telephony conference in which {@link #calls} are to be merged.
+ */
+ private final CallConference conference;
+
+ /**
+ * Second call.
+ */
+ private final Collection<Call> calls;
+
+ /**
+ * Initializes a new <tt>MergeExistingCalls</tt> instance which is to
+ * merge specific existing <tt>Call</tt>s into a specific telephony
+ * conference.
+ *
+ * @param conference the telephony conference in which the specified
+ * <tt>Call</tt>s are to be merged
+ * @param calls the <tt>Call</tt>s to be merged into the specified
+ * telephony conference
+ */
+ public MergeExistingCalls(
+ CallConference conference,
+ Collection<Call> calls)
+ {
+ this.conference = conference;
+ this.calls = calls;
+ }
+
+ /**
+ * Puts off hold the <tt>CallPeer</tt>s of a specific <tt>Call</tt>
+ * which are locally on hold.
+ *
+ * @param call the <tt>Call</tt> which is to have its <tt>CallPeer</tt>s
+ * put off hold
+ */
+ private void putOffHold(Call call)
+ {
+ Iterator<? extends CallPeer> peers = call.getCallPeers();
+ OperationSetBasicTelephony<?> telephony
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ while (peers.hasNext())
+ {
+ CallPeer callPeer = peers.next();
+ boolean putOffHold = true;
+
+ if(callPeer instanceof MediaAwareCallPeer)
+ {
+ putOffHold
+ = ((MediaAwareCallPeer<?,?,?>) callPeer)
+ .getMediaHandler()
+ .isLocallyOnHold();
+ }
+ if(putOffHold)
+ {
+ try
+ {
+ telephony.putOffHold(callPeer);
+ Thread.sleep(400);
+ }
+ catch(Exception ofe)
+ {
+ logger.error("Failed to put off hold.", ofe);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ // conference
+ for (Call call : conference.getCalls())
+ putOffHold(call);
+
+ // calls
+ if (!calls.isEmpty())
+ {
+ for(Call call : calls)
+ {
+ if (conference.containsCall(call))
+ continue;
+
+ putOffHold(call);
+
+ /*
+ * Dispose of the CallPanel associated with the Call which
+ * is to be merged.
+ */
+ closeCallContainerIfNotNecessary(conference, false);
+
+ call.setConference(conference);
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows a warning window to warn the user that she's about to start a
+ * desktop sharing session.
+ *
+ * @return <tt>true</tt> if the user has accepted the desktop sharing
+ * session; <tt>false</tt>, otherwise
+ */
+ private static boolean showDesktopSharingWarning()
+ {
+ Boolean isWarningEnabled
+ = GuiActivator.getConfigurationService().getBoolean(
+ desktopSharingWarningProperty,
+ true);
+
+ if (isWarningEnabled.booleanValue())
+ {
+ ResourceManagementService resources = GuiActivator.getResources();
+ MessageDialog warningDialog
+ = new MessageDialog(
+ null,
+ resources.getI18NString("service.gui.WARNING"),
+ resources.getI18NString(
+ "service.gui.DESKTOP_SHARING_WARNING"),
+ true);
+
+ switch (warningDialog.showDialog())
+ {
+ case MessageDialog.OK_RETURN_CODE:
+ return true;
+ case MessageDialog.CANCEL_RETURN_CODE:
+ return false;
+ case MessageDialog.OK_DONT_ASK_CODE:
+ GuiActivator.getConfigurationService().setProperty(
+ desktopSharingWarningProperty,
+ false);
+ return true;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Normalizes the phone numbers (if any) in a list of <tt>String</tt>
+ * contact addresses or phone numbers.
+ *
+ * @param callees the list of contact addresses or phone numbers to be
+ * normalized
+ */
+ private static void normalizePhoneNumbers(String callees[])
+ {
+ for (int i = 0 ; i < callees.length ; i++)
+ callees[i] = PhoneNumberI18nService.normalize(callees[i]);
+ }
+
+ /**
+ * Throws a <tt>RuntimeException</tt> if the current thread is not the AWT
+ * event dispatching thread.
+ */
+ public static void assertIsEventDispatchingThread()
+ {
+ if (!SwingUtilities.isEventDispatchThread())
+ {
+ throw new RuntimeException(
+ "The methon can be called only on the AWT event dispatching"
+ + " thread.");
+ }
+ }
+
+ /**
+ * Finds the <tt>CallPanel</tt>, if any, which depicts a specific
+ * <tt>CallConference</tt>.
+ * <p>
+ * <b>Note</b>: The method can be called only on the AWT event dispatching
+ * thread.
+ * </p>
+ *
+ * @param conference the <tt>CallConference</tt> to find the depicting
+ * <tt>CallPanel</tt> of
+ * @return the <tt>CallPanel</tt> which depicts the specified
+ * <tt>CallConference</tt> if such a <tt>CallPanel</tt> exists; otherwise,
+ * <tt>null</tt>
+ * @throws RuntimeException if the method is not called on the AWT event
+ * dispatching thread
+ */
+ private static CallPanel findCallPanel(CallConference conference)
+ {
+ synchronized (callPanels)
+ {
+ return callPanels.get(conference);
+ }
+ }
+
+ /**
+ * Notifies {@link #callPanels} about a specific <tt>CallEvent</tt> received
+ * by <tt>CallManager</tt> (because they may need to update their UI, for
+ * example).
+ * <p>
+ * <b>Note</b>: The method can be called only on the AWT event dispatching
+ * thread.
+ * </p>
+ *
+ * @param ev the <tt>CallEvent</tt> received by <tt>CallManager</tt> which
+ * is to be forwarded to <tt>callPanels</tt> for further
+ * <tt>CallPanel</tt>-specific handling
+ * @throws RuntimeException if the method is not called on the AWT event
+ * dispatching thread
+ */
+ private static void forwardCallEventToCallPanels(CallEvent ev)
+ {
+ assertIsEventDispatchingThread();
+
+ CallPanel[] callPanels;
+
+ synchronized (CallManager.callPanels)
+ {
+ Collection<CallPanel> values = CallManager.callPanels.values();
+
+ callPanels = values.toArray(new CallPanel[values.size()]);
+ }
+
+ for (CallPanel callPanel : callPanels)
+ {
+ try
+ {
+ callPanel.onCallEvent(ev);
+ }
+ catch (Exception ex)
+ {
+ /*
+ * There is no practical reason while the failure of a CallPanel
+ * to handle the CallEvent should cause the other CallPanels to
+ * be left out-of-date.
+ */
+ logger.error("A CallPanel failed to handle a CallEvent", ex);
+ }
+ }
+ }
+
+ /**
+ * Creates a call for the supplied operation set.
+ * @param opSetClass the operation set to use to create call.
+ * @param protocolProviderService the protocol provider
+ * @param contact the contact address to call
+ */
+ static void createCall(Class<? extends OperationSet> opSetClass,
+ ProtocolProviderService protocolProviderService,
+ String contact)
+ {
+ createCall(opSetClass, protocolProviderService, contact, null);
+ }
+
+ /**
+ * Creates a call for the supplied operation set.
+ * @param opSetClass the operation set to use to create call.
+ * @param protocolProviderService the protocol provider
+ * @param contact the contact address to call
+ * @param uiContact the <tt>MetaContact</tt> we're calling
+ */
+ static void createCall(
+ Class<? extends OperationSet> opSetClass,
+ ProtocolProviderService protocolProviderService,
+ String contact,
+ UIContactImpl uiContact)
+ {
+ if (opSetClass.equals(OperationSetBasicTelephony.class))
+ {
+ createCall(protocolProviderService, contact, uiContact);
+ }
+ else if (opSetClass.equals(OperationSetVideoTelephony.class))
+ {
+ createVideoCall(protocolProviderService, contact, uiContact);
+ }
+ else if (opSetClass.equals(
+ OperationSetDesktopSharingServer.class))
+ {
+ createDesktopSharing(
+ protocolProviderService, contact, uiContact);
+ }
+ }
+
+ /**
+ * Creates a call for the default contact of the metacontact
+ *
+ * @param metaContact the metacontact that will be called.
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param shareRegion if <tt>true</tt> will share a region of the desktop.
+ */
+ public static void call(MetaContact metaContact,
+ boolean isVideo,
+ boolean isDesktop,
+ boolean shareRegion)
+ {
+ Contact contact = metaContact
+ .getDefaultContact(getOperationSetForCall(isVideo, isDesktop));
+
+ call(contact, isVideo, isDesktop, shareRegion);
+ }
+
+ /**
+ * A particular contact has been selected no options to select
+ * will just call it.
+ * @param contact the contact to call
+ * @param contactResource the specific contact resource
+ * @param isVideo is video enabled
+ * @param isDesktop is desktop sharing enabled
+ * @param shareRegion is sharing the whole desktop or just a region.
+ */
+ public static void call(Contact contact,
+ ContactResource contactResource,
+ boolean isVideo,
+ boolean isDesktop,
+ boolean shareRegion)
+ {
+ if(isDesktop)
+ {
+ if(shareRegion)
+ {
+ createRegionDesktopSharing(
+ contact.getProtocolProvider(),
+ contact.getAddress(),
+ null);
+ }
+ else
+ createDesktopSharing(contact.getProtocolProvider(),
+ contact.getAddress(),
+ null);
+ }
+ else
+ {
+ new CreateCallThread(
+ contact.getProtocolProvider(),
+ contact,
+ contactResource,
+ isVideo).start();
+ }
+ }
+
+ /**
+ * Creates a call to the conference described in
+ * <tt>conferenceDescription</tt> through <tt>protocolProvider</tt>
+ *
+ * @param protocolProvider the protocol provider through which to create
+ * the call
+ * @param conferenceDescription the description of the conference to call
+ * @param chatRoom the chat room associated with the call.
+ */
+ public static void call(ProtocolProviderService protocolProvider,
+ ConferenceDescription conferenceDescription,
+ ChatRoom chatRoom)
+ {
+ new CreateCallThread(protocolProvider, conferenceDescription, chatRoom)
+ .start();
+ }
+
+ /**
+ * A particular contact has been selected no options to select
+ * will just call it.
+ * @param contact the contact to call
+ * @param isVideo is video enabled
+ * @param isDesktop is desktop sharing enabled
+ * @param shareRegion is sharing the whole desktop or just a region.
+ */
+ public static void call(Contact contact,
+ boolean isVideo,
+ boolean isDesktop,
+ boolean shareRegion)
+ {
+ if(isDesktop)
+ {
+ if(shareRegion)
+ {
+ createRegionDesktopSharing(
+ contact.getProtocolProvider(),
+ contact.getAddress(),
+ null);
+ }
+ else
+ createDesktopSharing(contact.getProtocolProvider(),
+ contact.getAddress(),
+ null);
+ }
+ else
+ {
+ new CreateCallThread( contact.getProtocolProvider(),
+ contact,
+ null,
+ isVideo).start();
+ }
+ }
+
+ /**
+ * Calls a phone showing a dialog to choose a provider.
+ * @param phone phone to call
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param shareRegion if <tt>true</tt> will share a region of the desktop.
+ */
+ public static void call(final String phone,
+ boolean isVideo,
+ boolean isDesktop,
+ final boolean shareRegion)
+ {
+ call(phone, null, isVideo, isDesktop, shareRegion);
+ }
+
+ /**
+ * Calls a phone showing a dialog to choose a provider.
+ * @param phone phone to call
+ * @param uiContact the <tt>UIContactImpl</tt> we're calling
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param shareRegion if <tt>true</tt> will share a region of the desktop.
+ */
+ public static void call(final String phone,
+ final UIContactImpl uiContact,
+ boolean isVideo,
+ boolean isDesktop,
+ final boolean shareRegion)
+ {
+ List<ProtocolProviderService> providers =
+ CallManager.getTelephonyProviders();
+
+ if(providers.size() > 1)
+ {
+ ChooseCallAccountDialog chooseAccount =
+ new ChooseCallAccountDialog(
+ phone,
+ getOperationSetForCall(isVideo, isDesktop),
+ providers)
+ {
+ @Override
+ public void callButtonPressed()
+ {
+ if(shareRegion)
+ {
+ createRegionDesktopSharing(
+ getSelectedProvider(), phone, uiContact);
+ }
+ else
+ super.callButtonPressed();
+ }
+ };
+ chooseAccount.setUIContact(uiContact);
+ chooseAccount.setVisible(true);
+ }
+ else
+ {
+ createCall(providers.get(0), phone, uiContact);
+ }
+ }
+
+ /**
+ * Obtain operation set checking the params.
+ * @param isVideo if <tt>true</tt> use OperationSetVideoTelephony.
+ * @param isDesktop if <tt>true</tt> use OperationSetDesktopSharingServer.
+ * @return the operation set, default is OperationSetBasicTelephony.
+ */
+ private static Class<? extends OperationSet> getOperationSetForCall(
+ boolean isVideo, boolean isDesktop)
+ {
+ if(isVideo)
+ {
+ if(isDesktop)
+ return OperationSetDesktopSharingServer.class;
+ else
+ return OperationSetVideoTelephony.class;
+ }
+ else
+ return OperationSetBasicTelephony.class;
+ }
+
+ /**
+ * Call any of the supplied details.
+ *
+ * @param uiContactDetailList the list with details to choose for calling
+ * @param uiContact the <tt>UIContactImpl</tt> to check what is enabled,
+ * available.
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param invoker the invoker component
+ * @param location the location where this was invoked.
+ */
+ public static void call(List<UIContactDetail> uiContactDetailList,
+ UIContactImpl uiContact,
+ boolean isVideo,
+ boolean isDesktop,
+ JComponent invoker,
+ Point location)
+ {
+ call(uiContactDetailList,
+ uiContact,
+ isVideo,
+ isDesktop,
+ invoker,
+ location,
+ true);
+ }
+
+ /**
+ * Call any of the supplied details.
+ *
+ * @param uiContactDetailList the list with details to choose for calling
+ * @param uiContact the <tt>UIContactImpl</tt> to check what is enabled,
+ * available.
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param invoker the invoker component
+ * @param location the location where this was invoked.
+ * @param usePreferredProvider whether to use the <tt>uiContact</tt>
+ * preferredProvider if provided.
+ */
+ private static void call(List<UIContactDetail> uiContactDetailList,
+ UIContactImpl uiContact,
+ boolean isVideo,
+ boolean isDesktop,
+ JComponent invoker,
+ Point location,
+ boolean usePreferredProvider)
+ {
+ ChooseCallAccountPopupMenu chooseCallAccountPopupMenu = null;
+
+ Class<? extends OperationSet> opsetClass =
+ getOperationSetForCall(isVideo, isDesktop);
+
+ UIPhoneUtil contactPhoneUtil = null;
+ if (uiContact != null
+ && uiContact.getDescriptor() instanceof MetaContact)
+ contactPhoneUtil = UIPhoneUtil
+ .getPhoneUtil((MetaContact) uiContact.getDescriptor());
+
+ if(contactPhoneUtil != null)
+ {
+ boolean addAdditionalNumbers = false;
+ if(!isVideo
+ || ConfigurationUtils
+ .isRouteVideoAndDesktopUsingPhoneNumberEnabled())
+ {
+ addAdditionalNumbers = true;
+ }
+ else
+ {
+ if(isVideo && contactPhoneUtil != null)
+ {
+ // lets check is video enabled in additional numbers
+ addAdditionalNumbers =
+ contactPhoneUtil.isVideoCallEnabled() ?
+ isDesktop ?
+ contactPhoneUtil.isDesktopSharingEnabled()
+ : true
+ : false;
+ }
+ }
+
+ if(addAdditionalNumbers)
+ {
+ uiContactDetailList.addAll(
+ contactPhoneUtil.getAdditionalNumbers());
+ }
+ }
+
+ if (uiContactDetailList.size() == 1)
+ {
+ UIContactDetail detail = uiContactDetailList.get(0);
+
+ ProtocolProviderService preferredProvider = null;
+
+ if(usePreferredProvider)
+ preferredProvider =
+ detail.getPreferredProtocolProvider(opsetClass);
+
+ List<ProtocolProviderService> providers = null;
+ String protocolName = null;
+
+ if (preferredProvider != null)
+ {
+ if (preferredProvider.isRegistered())
+ {
+ createCall(opsetClass,
+ preferredProvider,
+ detail.getAddress(),
+ uiContact);
+ }
+
+ // If we have a provider, but it's not registered we try to
+ // obtain all registered providers for the same protocol as the
+ // given preferred provider.
+ else
+ {
+ protocolName = preferredProvider.getProtocolName();
+ providers = AccountUtils.getRegisteredProviders(protocolName,
+ opsetClass);
+ }
+ }
+ // If we don't have a preferred provider we try to obtain a
+ // preferred protocol name and all registered providers for it.
+ else
+ {
+ protocolName = detail.getPreferredProtocol(opsetClass);
+
+ if (protocolName != null)
+ providers
+ = AccountUtils.getRegisteredProviders(protocolName,
+ opsetClass);
+ else
+ providers
+ = AccountUtils.getRegisteredProviders(opsetClass);
+ }
+
+ // If our call didn't succeed, try to call through one of the other
+ // protocol providers obtained above.
+ if (providers != null)
+ {
+ int providersCount = providers.size();
+
+ if (providersCount <= 0)
+ {
+ new ErrorDialog(null,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CALL_FAILED"),
+ GuiActivator.getResources().getI18NString(
+ "service.gui.NO_ONLINE_TELEPHONY_ACCOUNT"))
+ .showDialog();
+ }
+ else if (providersCount == 1)
+ {
+ createCall(
+ opsetClass,
+ providers.get(0),
+ detail.getAddress(),
+ uiContact);
+ }
+ else if (providersCount > 1)
+ {
+ chooseCallAccountPopupMenu =
+ new ChooseCallAccountPopupMenu(
+ invoker, detail.getAddress(), providers,
+ opsetClass);
+ }
+ }
+ }
+ else if (uiContactDetailList.size() > 1)
+ {
+ chooseCallAccountPopupMenu
+ = new ChooseCallAccountPopupMenu(invoker, uiContactDetailList,
+ opsetClass);
+ }
+
+ // If the choose dialog is created we're going to show it.
+ if (chooseCallAccountPopupMenu != null)
+ {
+ if (uiContact != null)
+ chooseCallAccountPopupMenu.setUIContact(uiContact);
+
+ chooseCallAccountPopupMenu.showPopupMenu(location.x, location.y);
+ }
+ }
+
+
+ /**
+ * Call the ui contact.
+ *
+ * @param uiContact the contact to call.
+ * @param isVideo if <tt>true</tt> will create video call.
+ * @param isDesktop if <tt>true</tt> will share the desktop.
+ * @param invoker the invoker component
+ * @param location the location where this was invoked.
+ */
+ public static void call(UIContact uiContact,
+ boolean isVideo,
+ boolean isDesktop,
+ JComponent invoker,
+ Point location)
+ {
+ UIContactImpl uiContactImpl = null;
+ if(uiContact instanceof UIContactImpl)
+ {
+ uiContactImpl = (UIContactImpl) uiContact;
+ }
+
+ List<UIContactDetail> telephonyContacts
+ = uiContact.getContactDetailsForOperationSet(
+ getOperationSetForCall(isVideo, isDesktop));
+
+ boolean ignorePreferredProvider =
+ GuiActivator.getConfigurationService().getBoolean(
+ IGNORE_PREFERRED_PROVIDER_PROP, false);
+
+ call( telephonyContacts,
+ uiContactImpl,
+ isVideo,
+ isDesktop,
+ invoker,
+ location,
+ !ignorePreferredProvider);
+ }
+
+ /**
+ * Tries to resolves a peer address into a display name, by reqesting the
+ * <tt>ContactSourceService</tt>s. This function returns only the
+ * first match.
+ *
+ * @param peerAddress The peer address.
+ *
+ * @return The corresponding display name, if there is a match. Null
+ * otherwise.
+ */
+ private static String resolveContactSource(String peerAddress)
+ {
+ String displayName = null;
+
+ if(!StringUtils.isNullOrEmpty(peerAddress))
+ {
+ Vector<ResolveAddressToDisplayNameContactQueryListener> resolvers
+ = new Vector<ResolveAddressToDisplayNameContactQueryListener>
+ (1, 1);
+
+ // will strip the @server-address part, as the regular expression
+ // will match it
+ int index = peerAddress.indexOf("@");
+ String peerUserID =
+ (index > -1) ? peerAddress.substring(0, index) : peerAddress;
+
+ // searches for the whole number/username or with the @serverpart
+ peerUserID = Pattern.quote(peerUserID);
+ Pattern pattern = Pattern.compile(
+ "^(" + peerUserID + "|" + peerUserID + "@.*)$");
+
+ // Queries all available resolvers
+ for(ContactSourceService contactSourceService:
+ GuiActivator.getContactSources())
+ {
+ if(!(contactSourceService
+ instanceof ExtendedContactSourceService))
+ continue;
+
+ // use the pattern method of (ExtendedContactSourceService)
+ ContactQuery query
+ = ((ExtendedContactSourceService)contactSourceService)
+ .queryContactSource(pattern);
+
+ resolvers.add(
+ new ResolveAddressToDisplayNameContactQueryListener(
+ query));
+ }
+
+ long startTime = System.currentTimeMillis();
+ long currentTime = startTime;
+ // The detault timeout is set to 500ms.
+ long timeout = 500;
+ // Loops until we found a valid display name, or waits for timeout.
+ while(displayName == null
+ && currentTime - startTime < timeout)
+ {
+ for(int i = 0; i < resolvers.size() && displayName == null; ++i)
+ {
+ ResolveAddressToDisplayNameContactQueryListener resolver
+ = resolvers.get(i);
+ if(!resolver.isRunning())
+ {
+ if(resolver.isFound())
+ {
+ displayName = resolver.getResolvedName();
+ // If this is the same result as the peer address,
+ // then that is not what we are looking for. Then,
+ // continue the search.
+ if(displayName.equals(peerAddress))
+ {
+ displayName = null;
+ }
+ }
+ }
+ }
+ Thread.yield();
+ currentTime = System.currentTimeMillis();
+ }
+
+ // Free lasting resolvers.
+ for(int i = 0; i < resolvers.size(); ++i)
+ {
+ ResolveAddressToDisplayNameContactQueryListener resolver
+ = resolvers.get(i);
+ if(resolver.isRunning())
+ {
+ resolver.stop();
+ }
+ }
+ }
+
+ return displayName;
+ }
+}
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 8515dfc..ec743cd 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 @@ -1,2520 +1,2520 @@ -/* - * 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.impl.gui.main.call; - -import java.awt.*; -import java.awt.Container; -import java.awt.event.*; -import java.beans.*; -import java.util.*; -import java.util.List; - -import javax.swing.*; -import javax.swing.Timer; - -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.impl.gui.event.*; -import net.java.sip.communicator.impl.gui.main.call.conference.*; -import net.java.sip.communicator.impl.gui.utils.*; -import net.java.sip.communicator.impl.gui.utils.Constants; -import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.service.contactlist.*; -import net.java.sip.communicator.service.gui.*; -import net.java.sip.communicator.service.gui.call.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.Logger; -import net.java.sip.communicator.util.skin.*; - -import org.jitsi.service.neomedia.*; -import org.jitsi.util.*; -import org.jitsi.util.event.*; -import org.osgi.framework.*; - -/** - * The dialog created for a given call. - * - * Ordered buttons we are adding/removing, numbers are the index we have set. - * And the order that will be kept. - * 0 dialButton - * 1 conferenceButton - * 2 holdButton - * 3 recordButton - * 4 mergeButton - * 5 transferCallButton - * 6 localLevel - * 7 remoteLevel - * 8 desktopSharingButton - * 9 resizeVideoButton - * 10 fullScreenButton - * 11 videoButton - * 12 showHideVideoButton - * 19 chatButton - * 20 infoButton - * - * @author Yana Stamcheva - * @author Adam Netocny - * @author Lyubomir Marinov - * @author Hristo Terezov - */ -public class CallPanel - extends TransparentPanel - implements ActionListener, - PluginComponentListener, - Skinnable, - ConferencePeerViewListener, - ContactPresenceStatusListener -{ - /** - * The chat button name. - */ - private static final String CHAT_BUTTON = "CHAT_BUTTON"; - - /** - * The conference button name. - */ - private static final String CONFERENCE_BUTTON = "CONFERENCE_BUTTON"; - - /** - * The dial button name. - */ - private static final String DIAL_BUTTON = "DIAL_BUTTON"; - - /** - * The info button name. - */ - private static final String INFO_BUTTON = "INFO_BUTTON"; - - /** - * The logger for this class. - */ - private static final Logger logger = Logger.getLogger(CallDialog.class); - - /** - * The hang up button name. - */ - private static final String MERGE_BUTTON = "MERGE_BUTTON"; - - /** - * Serial version UID. - */ - private static final long serialVersionUID = 0L; - - /** - * Property to disable the info button. - */ - private static final String HIDE_CALL_INFO_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_INFO_BUTTON"; - - /** - * Property to disable the conference "add to call" button. - */ - private static final String HIDE_CONFERENCE_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CONFERENCE_BUTTON"; - - /** - * Property to disable the record button. - */ - private static final String HIDE_CALL_RECORD_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_RECORD_BUTTON"; - - /** - * Property to disable the "call merge" button. - */ - private static final String HIDE_CALL_MERGE_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_MERGE_BUTTON"; - - /** - * Property to disable the "call merge" button. - */ - private static final String HIDE_CALL_TRANSFER_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_TRANSFER_BUTTON"; - - /** - * Property to disable the "hold" button. - */ - private static final String HIDE_CALL_HOLD_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_HOLD_BUTTON"; - - /** - * Property to disable the dial button. - */ - private static final String HIDE_DIAL_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_DIAL_BUTTON"; - - /** - * Property to disable the video button. - */ - private static final String HIDE_VIDEO_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_VIDEO_BUTTON"; - - /** - * Property to disable the desktop sharing button. - */ - private static final String HIDE_DESKTOP_SHARING_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_DESKTOP_SHARING_BUTTON"; - - /** - * Property to disable the full screen button. - */ - private static final String HIDE_FULL_SCREEN_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_FULL_SCREEN_BUTTON"; - - /** - * Property to disable the "show/hide local video" button. - */ - private static final String HIDE_TOGGLE_VIDEO_BUTON_PROP - = "net.java.sip.communicator.impl.gui.main.call.HIDE_TOGGLE_VIDEO_BUTTON"; - - /** - * The <tt>Component</tt> which is at the bottom of this view and contains - * {@link #settingsPanel}. It overrides the Swing-defined background on OS - * X so it needs explicit updating upon switching between full-screen and - * windowed mode in order to respect any background-related settings of the - * ancestors such as black background in full-screen mode. - */ - private JComponent bottomBar; - - /** - * The {@link CallConference} instance depicted by this <tt>CallPanel</tt>. - */ - private final CallConference callConference; - - /** - * The listener which listens to events fired by the <tt>CallConference</tt> - * depicted by this instance, the <tt>Call</tt>s participating in that - * telephony conference, the <tt>CallPeer</tt>s associated with those - * <tt>Call</tt>s and the <tt>ConferenceMember</tt>s participating in the - * telephony conferences organized by those <tt>CallPeer</tt>s. Updates this - * view i.e. CallPanel so that it depicts the current state of its model - * i.e. {@link #callConference}. - */ - private final CallConferenceListener callConferenceListener - = new CallConferenceListener(); - - /** - * The time in milliseconds at which the telephony call/conference depicted - * by this <tt>CallPanel</tt> (i.e. {@link #callConference}) has started. - */ - private long callConferenceStartTime; - - /** - * A timer to count call duration. - */ - private Timer callDurationTimer; - - /** - * The Frame used to display this call information statistics. - */ - private CallInfoFrame callInfoFrame; - - /** - * The panel representing the call. For conference calls this would be an - * instance of <tt>ConferenceCallPanel</tt> and for one-to-one calls this - * would be an instance of <tt>OneToOneCallPanel</tt>. - */ - private JComponent callPanel; - - /** - * Parent window. - */ - private final CallContainer callWindow; - - /** - * Chat button. - */ - private SIPCommButton chatButton; - - /** - * The operation set that will be used to update chatButton icon and - * the corresponding contact. - */ - private OperationSetPresence operationSetPresence; - - /** - * The conference button. - */ - private CallToolBarButton conferenceButton; - - /** - * The desktop sharing button. - */ - private DesktopSharingButton desktopSharingButton; - - /** - * The dial button, which opens a keypad dialog. - */ - private CallToolBarButton dialButton; - - /** - * The dial pad dialog opened when the dial button is clicked. - */ - private DialpadDialog dialpadDialog; - - /** - * The indicator which determines whether {@link #dispose()} has already - * been invoked on this instance. If <tt>true</tt>, this instance is - * considered non-functional and is to be left to the garbage collector. - */ - private boolean disposed = false; - - /** - * The handler for DTMF tones. - */ - private DTMFHandler dtmfHandler; - - /** - * The full screen button. - */ - private FullScreenButton fullScreenButton; - - /** - * HangUp button. - */ - private SIPCommButton hangupButton; - - /** - * The hold button. - */ - private HoldButton holdButton; - - /** - * Info button. - */ - private SIPCommButton infoButton; - - /** - * Indicates if the call timer has been started. - */ - private boolean isCallTimerStarted = false; - - /** - * Sound local level label. - */ - private InputVolumeControlButton localLevel; - - /** - * Merge button. - */ - private CallToolBarButton mergeButton; - - /** - * The button which allows starting and stopping the recording of - * {@link #callConference}. - */ - private RecordButton recordButton; - - /** - * Sound remote level label. - */ - private Component remoteLevel; - - /** - * The video resize button. - */ - private ResizeVideoButton resizeVideoButton; - - /** - * The panel containing call settings. - */ - private CallToolBar settingsPanel; - - /** - * The button responsible for hiding/showing the local video. - */ - private ShowHideVideoButton showHideVideoButton; - - /** - * The title of this call container. - */ - private String title; - - /** - * A collection of listeners, registered for call title change events. - */ - private Collection<CallTitleListener> titleListeners - = new Vector<CallTitleListener>(); - - /** - * The transfer call button. - */ - private TransferCallButton transferCallButton; - - /** - * The facility which aids this instance in the dealing with the - * video-related information. - */ - private final UIVideoHandler2 uiVideoHandler; - - /** - * Indicates if this call panel should be closed immediately after hang up - * or should wait some time so that the user can be notified of the last - * state. By default we wait, so that the user can be notified of the - * current state of the call. - */ - private boolean isCloseWaitAfterHangup = true; - - /** - * The <tt>Observer</tt> which listens to {@link #uiVideoHandler} about - * changes in the video-related information. - */ - private final Observer uiVideoHandlerObserver - = new Observer() - { - public void update(Observable o, Object arg) - { - uiVideoHandlerUpdate(arg); - } - }; - - /** - * The <tt>Runnable</tt> which is scheduled by - * {@link #updateViewFromModel()} for execution in the AWT event dispatching - * thread in order to invoke - * {@link #updateViewFromModelInEventDispatchThread()}. - */ - private final Runnable updateViewFromModelInEventDispatchThread - = new Runnable() - { - public void run() - { - /* - * We receive events/notifications from various threads and we - * respond to them in the AWT event dispatching thread. It is - * possible to first schedule an event to be brought to the AWT - * event dispatching thread, then to have #dispose() invoked on - * this instance and, finally, to receive the scheduled event in - * the AWT event dispatching thread. In such a case, this - * disposed instance should not respond to the event. - */ - if (!disposed) - updateViewFromModelInEventDispatchThread(); - } - }; - - /** - * The video button. - */ - private LocalVideoButton videoButton; - - /** - * Initializes a new <tt>CallPanel</tt> which is to depict a specific - * <tt>CallConference</tt>. - * - * @param callConference the <tt>CallConference</tt> to be depicted by the - * new instance - * @param callWindow the parent window in which the new instance will be - * added - */ - public CallPanel( CallConference callConference, - CallContainer callWindow) - { - super(new BorderLayout()); - - this.callConference = callConference; - this.callWindow = callWindow; - - uiVideoHandler = new UIVideoHandler2(this.callConference); - - callDurationTimer - = new Timer( - 1000, - new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - setCallTitle(callConferenceStartTime); - } - }); - callDurationTimer.setRepeats(true); - - // The call duration parameter is not known yet. - setCallTitle(0); - - initializeUserInterfaceHierarchy(); - - dtmfHandler = new DTMFHandler(this); - - /* - * Adds the listeners which will observe the model and will trigger the - * updates of this view from it. - */ - this.callConference.addCallChangeListener(callConferenceListener); - this.callConference.addCallPeerConferenceListener( - callConferenceListener); - this.callConference.addPropertyChangeListener(callConferenceListener); - uiVideoHandler.addObserver(uiVideoHandlerObserver); - - callWindow.getFrame().addPropertyChangeListener( - CallContainer.PROP_FULL_SCREEN, - callConferenceListener); - - updateViewFromModel(); - - initPluginComponents(); - } - - /** - * Handles action events. - * @param evt the <tt>ActionEvent</tt> that was triggered - */ - public void actionPerformed(ActionEvent evt) - { - JButton button = (JButton) evt.getSource(); - String buttonName = button.getName(); - - if (buttonName.equals(MERGE_BUTTON)) - { - CallManager.mergeExistingCalls( - callConference, - CallManager.getInProgressCalls()); - } - else if (buttonName.equals(DIAL_BUTTON)) - { - if (dialpadDialog == null) - dialpadDialog = this.getDialpadDialog(); - - if(!dialpadDialog.isVisible()) - { - dialpadDialog.pack(); - - Point location = new Point( button.getX(), - button.getY() + button.getHeight()); - SwingUtilities.convertPointToScreen( - location, button.getParent()); - - dialpadDialog.setLocation( - (int) location.getX() + 2, - (int) location.getY() + 2); - - dialpadDialog.addWindowFocusListener(dialpadDialog); - dialpadDialog.setVisible(true); - } - else - { - dialpadDialog.removeWindowFocusListener(dialpadDialog); - dialpadDialog.setVisible(false); - } - } - else if (buttonName.equals(CONFERENCE_BUTTON)) - { - ConferenceInviteDialog inviteDialog; - - if (callConference.isJitsiVideoBridge()) - { - inviteDialog - = new ConferenceInviteDialog( - callConference, - callConference.getCalls().get(0) - .getProtocolProvider(), - true); - } - else - inviteDialog = new ConferenceInviteDialog(callConference); - - inviteDialog.setVisible(true); - } - else if (buttonName.equals(CHAT_BUTTON)) - { - /* - * If there is exactly 1 CallPeer capable of instant messaging, then - * we'll try to start a chat with her. - */ - /* - * TODO The following is very likely to block the user interface in - * a noticeable way sooner or later. - */ - List<Contact> imCapableCallPeers = getIMCapableCallPeers(1); - - if (imCapableCallPeers.size() == 1) - { - Contact contact = imCapableCallPeers.get(0); - MetaContact metaContact - = GuiActivator.getContactListService() - .findMetaContactByContact(contact); - GuiActivator.getUIService().getChatWindowManager().startChat( - metaContact); - } - } - else if (buttonName.equals(INFO_BUTTON)) - { - if (callInfoFrame == null) - { - callInfoFrame = new CallInfoFrame(callConference); - addCallTitleListener(callInfoFrame); - } - callInfoFrame.setVisible( - callInfoFrame.hasCallInfo() && !callInfoFrame.isVisible()); - } - } - - /** - * Executes the action associated with the "Hang up" button which may be - * invoked by clicking the button in question or by closing this dialog. - * - * @param closeWait <tt>true</tt> to close this instance with a few seconds - * of delay or <tt>false</tt> to close it immediately - */ - public void actionPerformedOnHangupButton(boolean closeWait) - { - isCloseWaitAfterHangup = closeWait; - - this.disposeCallInfoFrame(); - - /* - * It is the responsibility of CallManager to close this CallPanel - * when a Call is ended. - */ - if (callConference.getCallCount() > 0) - CallManager.hangupCalls(callConference); - /* - * If however there are no more calls related to this panel we will - * close the window directly. This could happen in the case, where - * the other side has already hanged up the call, the call window shows - * the state disconnected and we press the hang up button. In this - * case the contained call is already null and we should only close the - * call window. - */ - else - callWindow.close(this, false); - } - - /** - * Indicates if this call panel should be closed immediately after hang up - * or should wait some time so that the user can be notified of the last - * state. - * - * @return <tt>true</tt> to indicate that when hanged up this call panel - * should not be closed immediately, <tt>false</tt> - otherwise - */ - public boolean isCloseWaitAfterHangup() - { - return isCloseWaitAfterHangup; - } - - /** - * Adds the given <tt>CallTitleListener</tt> to the list of listeners, - * notified for call title changes. - * - * @param l the <tt>CallTitleListener</tt> to add - */ - public void addCallTitleListener(CallTitleListener l) - { - synchronized (titleListeners) - { - titleListeners.add(l); - } - } - - /** - * Adds remote video specific components. - * - * @param callPeer the <tt>CallPeer</tt> - */ - public void addRemoteVideoSpecificComponents(CallPeer callPeer) - { - if(CallManager.isVideoQualityPresetSupported(callPeer)) - { - if(resizeVideoButton == null) - { - resizeVideoButton = new ResizeVideoButton(callPeer.getCall()); - resizeVideoButton.setIndex(9); - } - - if(resizeVideoButton.countAvailableOptions() > 1) - { - settingsPanel.add(resizeVideoButton); - settingsPanel.revalidate(); - settingsPanel.repaint(); - } - } - } - - /** - * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by - * {@link #callWindow}. - * - * @param ev the <tt>PropertyChangeEvent</tt> fired by <tt>callWindow</tt> - * to notify this instance about - */ - private void callWindowPropertyChange(PropertyChangeEvent ev) - { - /* - * We are registered for CallContainer#PROP_FULL_SCREEN only. This - * instance will fire the notification as its own to allow listeners to - * register with a source which is more similar to them with respect to - * life span. - */ - try - { - if (OSUtils.IS_MAC && (bottomBar != null)) - bottomBar.setOpaque(!isFullScreen()); - } - finally - { - firePropertyChange( - ev.getPropertyName(), - ev.getOldValue(), ev.getNewValue()); - } - } - - /** - * Count the number of the buttons in the supplied components. - * @param cs the components to search for buttons. - * @return number of buttons. - */ - private int countButtons(Component[] cs) - { - int count = 0; - - for(Component c : cs) - { - if(c instanceof SIPCommButton || c instanceof SIPCommToggleButton) - count++; - if(c instanceof Container) - count += countButtons(((Container)c).getComponents()); - } - - return count; - } - - /** - * Creates the bottom bar panel for this <tt>CallPanel</tt>. - * - * @return a new bottom bar panel for this <tt>CallPanel</tt> - */ - private JComponent createBottomBar() - { - bottomBar - = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); - bottomBar.setBorder(BorderFactory.createEmptyBorder(0, 30, 2, 30)); - - /* - * The bottomBar on OS X overrides the Swing-defined background. - * However, full-screen display usually uses a black background. The - * black background will be set elsewhere on an ancestor but we have to - * make sure that bottomBar's background does not interfere with the - * setting. - */ - if (OSUtils.IS_MAC) - { - bottomBar.setOpaque(!isFullScreen()); - bottomBar.setBackground( - new Color(GuiActivator.getResources().getColor( - "service.gui.MAC_PANEL_BACKGROUND"))); - } - - bottomBar.add(settingsPanel); - - return bottomBar; - } - - /** - * Releases the resources acquired by this instance which require explicit - * disposal (e.g. any listeners added to the depicted - * <tt>CallConference</tt>, the participating <tt>Call</tt>s, and their - * associated <tt>CallPeer</tt>s). Invoked by <tt>CallManager</tt> when it - * determines that this <tt>CallPanel</tt> is no longer necessary. - */ - void dispose() - { - disposed = true; - - callConference.removeCallChangeListener(callConferenceListener); - callConference.removeCallPeerConferenceListener(callConferenceListener); - callConference.removePropertyChangeListener(callConferenceListener); - - uiVideoHandler.deleteObserver(uiVideoHandlerObserver); - uiVideoHandler.dispose(); - - callWindow.getFrame().removePropertyChangeListener( - CallContainer.PROP_FULL_SCREEN, - callConferenceListener); - - if (callPanel != null) - { - if(callPanel instanceof BasicConferenceCallPanel) - { - ((BasicConferenceCallPanel) callPanel) - .removePeerViewListener(this); - } - ((CallRenderer) callPanel).dispose(); - } - - // clears the contact status listener - if(operationSetPresence != null) - { - operationSetPresence.removeContactPresenceStatusListener(this); - } - } - - /** - * Disposes the call info frame if it exists. - */ - public void disposeCallInfoFrame() - { - if (callInfoFrame != null) - callInfoFrame.dispose(); - } - - /** - * Updates {@link #settingsPanel} from the model of this view. The update is - * performed in the AWT event dispatching thread. - * <p> - * The center of this view is occupied by {@link #callPanel}, the bottom of - * this view is dedicated to <tt>settingsPanel</tt>. The method - * {@link #updateViewFromModelInEventDispatchThread()} updates - * <tt>callPanel</tt> from the model of this view and then invokes the - * method <tt>updateSettingsPanelInEventDispatchThread()</tt>. Thus this - * whole view is updated so that it depicts the current state of its model. - * </p> - * - * @param callConferenceIsEnded <tt>true</tt> if the method - * <tt>updateViewFromModelInEventDispatchThread()</tt> considers the - * {@link #callConference} ended; otherwise, <tt>false</tt>. When the - * <tt>callConference</tt> is considered ended, the <tt>callPanel</tt> - * instance will not be switched to a specific type (one-to-one, audio-only, - * or audio/video) because, otherwise, the switch will leave it - * <tt>null</tt> and this view will remain blank. In such a case, - * <tt>settingsPanel</tt> may wish to do pretty much the same but disable - * and/or hide the buttons it contains. - */ - private void doUpdateSettingsPanelInEventDispatchThread( - boolean callConferenceIsEnded) - { - settingsPanel.setFullScreen(isFullScreen()); - - boolean isConference = (callPanel instanceof BasicConferenceCallPanel); - - /* - * For whatever reason, we're treating the localLevel and the - * remoteLevel buttons differently and we're adding and removing them in - * accord with the conference state of the user interface. - */ - if (isConference) - { - settingsPanel.add(localLevel); - settingsPanel.add(remoteLevel); - } - else - { - settingsPanel.remove(localLevel); - settingsPanel.remove(remoteLevel); - } - - /* - * We do not support chat conferencing with the participants in a - * telephony conference at this time so we do not want the chatButton - * visible in such a scenario. - */ - List<Contact> imContacts = getIMCapableCallPeers(1); - chatButton.setVisible( - !isConference && (imContacts.size() == 1)); - if(chatButton.isVisible() && operationSetPresence == null) - { - Contact contact = imContacts.get(0); - operationSetPresence = - contact.getProtocolProvider() - .getOperationSet(OperationSetPresence.class); - if(operationSetPresence != null) - operationSetPresence.addContactPresenceStatusListener(this); - - chatButton.setIconImage( - Constants.getMessageStatusIcon(contact.getPresenceStatus())); - chatButton.repaint(); - } - - updateHoldButtonState(); - updateMergeButtonState(); - - List<Call> calls = callConference.getCalls(); - /* - * OperationSetAdvancedTelephony implements call transfer. The feature - * is not supported if the local user/peer is a conference focus. - * Instead of disabling the transferCallButton in this case though, we - * want it hidden. - */ - boolean advancedTelephony = !calls.isEmpty(); - boolean telephonyConferencing = false; - boolean videoTelephony = false; - boolean videoTelephonyIsLocalVideoAllowed = false; - boolean videoTelephonyIsLocalVideoStreaming = false; - boolean desktopSharing = false; - boolean desktopSharingIsStreamed = false; - boolean allCallsConnected = true; - - for (Call call : calls) - { - ProtocolProviderService pps = call.getProtocolProvider(); - - /* - * The transferCallButton requires OperationSetAdvancedTelephony - * for all Calls. - */ - if (advancedTelephony) - { - OperationSetAdvancedTelephony<?> osat - = pps.getOperationSet(OperationSetAdvancedTelephony.class); - - if (osat == null) - advancedTelephony = false; - } - - /* - * The conferenceButton needs at least one Call with - * OperationSetTelephonyConferencing, - */ - if (!telephonyConferencing) - { - OperationSetTelephonyConferencing ostc - = pps.getOperationSet( - OperationSetTelephonyConferencing.class); - - if (ostc != null) - telephonyConferencing = true; - } - - if (!videoTelephony - || !videoTelephonyIsLocalVideoAllowed - || !videoTelephonyIsLocalVideoStreaming) - { - OperationSetVideoTelephony osvt - = pps.getOperationSet(OperationSetVideoTelephony.class); - - if (osvt != null) - { - if (!videoTelephony) - videoTelephony = true; - if (!videoTelephonyIsLocalVideoAllowed - && osvt.isLocalVideoAllowed(call)) - videoTelephonyIsLocalVideoAllowed = true; - if (!videoTelephonyIsLocalVideoStreaming - && osvt.isLocalVideoStreaming(call)) - videoTelephonyIsLocalVideoStreaming = true; - } - } - - if(!desktopSharing) - { - OperationSetDesktopStreaming osds - = pps.getOperationSet( - OperationSetDesktopStreaming.class); - if(osds != null) - { - desktopSharing = true; - - if(videoTelephonyIsLocalVideoStreaming - && call instanceof MediaAwareCall - && ((MediaAwareCall<?,?,?>) call).getMediaUseCase() - == MediaUseCase.DESKTOP) - { - desktopSharingIsStreamed = true; - } - } - } - - if (CallState.CALL_IN_PROGRESS != call.getCallState()) - { - allCallsConnected = false; - } - } - - if(conferenceButton != null) - conferenceButton.setEnabled(telephonyConferencing); - - if(transferCallButton != null) - { - transferCallButton.setEnabled(advancedTelephony); - transferCallButton.setVisible(!callConference.isConferenceFocus()); - } - - /* - * The videoButton is a beast of its own kind because it depends not - * only on the state of the depicted telephony conference but also on - * the global application state. - */ - if(videoButton != null) - { - videoButton.setEnabled(allCallsConnected && videoTelephony); - videoButton.setSelected(videoTelephonyIsLocalVideoAllowed); - - /* - * Consequently, the showHideVideoButton which depends on videoButton - * has to be updated depending on the state of the videoButton as well. - */ - if(showHideVideoButton != null) - { - showHideVideoButton.setEnabled( - videoButton.isEnabled() - && videoTelephonyIsLocalVideoAllowed); - showHideVideoButton.setSelected( - showHideVideoButton.isEnabled() - && uiVideoHandler.isLocalVideoVisible()); - showHideVideoButton.setVisible(showHideVideoButton.isEnabled()); - } - } - - // The desktop sharing button depends on the operation set desktop - // sharing server. - if(desktopSharingButton != null) - { - desktopSharingButton.setEnabled(desktopSharing); - desktopSharingButton.setSelected(desktopSharingIsStreamed); - } - - if (callPanel instanceof OneToOneCallPanel) - { - OneToOneCallPanel oneToOneCallPanel = (OneToOneCallPanel) callPanel; - if(desktopSharingIsStreamed) - oneToOneCallPanel.addDesktopSharingComponents(); - else - oneToOneCallPanel.removeDesktopSharingComponents(); - } - } - - /** - * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current - * state of its model i.e. <tt>callConference</tt>. The update is performed - * in the AWT event dispatching thread. - */ - private void doUpdateViewFromModelInEventDispatchThread() - { - /* - * If the telephony conference depicted by this instance has ended, do - * not update the user interface because it will be left blank. It is - * CallManager's responsibility to dispose of this CallPanel after its - * telephony conference has ended. Additionally, the various types of - * callPanel will usually require at least one CallPeer in order to not - * be blank. The absence of CallPeers usually indicates that a Call and, - * respectively, a telephony conference has ended. So it makes some - * sense to skip the update in such cases in order to try to not have - * the user interface blank. - */ - if (callConference.isEnded() - || (callConference.getCallPeerCount() == 0)) - { - /* - * However, the settingsPanel contains buttons which may still need - * to be disabled and/or hidden. - */ - updateSettingsPanelInEventDispatchThread(true); - return; - } - - boolean isConference = isConference(); - boolean isVideo = CallManager.isVideoStreaming(callConference); - CallPeer callPeer = null; - boolean validateAndRepaint = false; - - if (callPanel != null) - { - boolean removeCallPanel; - - if (isConference) - { - if (callPanel instanceof BasicConferenceCallPanel) - { - if (isVideo) - { - removeCallPanel - = !(callPanel instanceof VideoConferenceCallPanel); - } - else - { - removeCallPanel - = (callPanel instanceof VideoConferenceCallPanel); - } - } - else - { - removeCallPanel = true; - } - } - else - { - if (callPanel instanceof OneToOneCallPanel) - { - if (callPeer == null) - { - List<CallPeer> callPeers - = callConference.getCallPeers(); - - if (!callPeers.isEmpty()) - callPeer = callPeers.get(0); - } - removeCallPanel - = !((OneToOneCallPanel) callPanel).getCallPeer().equals( - callPeer); - } - else - { - if( (callPanel instanceof BasicConferenceCallPanel) && - ((BasicConferenceCallPanel) callPanel) - .hasDelayedCallPeers()) - { - removeCallPanel = false; - } - else - { - removeCallPanel = true; - } - - } - } - if (removeCallPanel) - { - remove(callPanel); - validateAndRepaint = true; - try - { - ((CallRenderer) callPanel).dispose(); - } - finally - { - callPanel = null; - } - } - } - if (callPanel == null) - { - if (isConference) - { - if (isVideo) - { - callPanel - = new VideoConferenceCallPanel( - this, - callConference, - uiVideoHandler); - } - else - { - callPanel - = new AudioConferenceCallPanel(this, callConference); - } - - ((BasicConferenceCallPanel) callPanel) - .addPeerViewlListener(this); - } - else - { - if (callPeer == null) - { - List<CallPeer> callPeers = callConference.getCallPeers(); - - if (!callPeers.isEmpty()) - callPeer = callPeers.get(0); - } - if (callPeer != null) - { - callPanel - = new OneToOneCallPanel(this, callPeer, uiVideoHandler); - } - } - if (callPanel != null) - { - add(callPanel, BorderLayout.CENTER); - validateAndRepaint = true; - } - } - - try - { - /* - * The center of this view is occupied by callPanel and we have just - * updated it. The bottom of this view is dedicated to settingsPanel - * so we have to update it as well. - */ - updateSettingsPanelInEventDispatchThread(false); - } - finally - { - /* - * It seems that AWT/Swing does not validate and/or repaint this - * Container (enough) and, consequently, its display may not update - * itself with an up-to-date drawing of the current callPanel. - */ - if (validateAndRepaint) - { - if (isDisplayable()) - { - validate(); - repaint(); - } - else - doLayout(); - } - } - } - - /** - * Attempts to give a specific <tt>Component</tt> a visible rectangle with a - * specific width and a specific height if possible and sane by resizing - * the <tt>Window</tt> which contains this instance. - * - * @param component the <tt>Component</tt> which requests a visible - * rectangle with the specified <tt>width</tt> and <tt>height</tt> - * @param width the width of the visible rectangle requested by the - * specified <tt>component</tt> - * @param height the height of the visible rectangle requested by the - * specified <tt>component</tt> - */ - private void ensureSize(Component component, int width, int height) - { - CallContainer callContainer = getCallWindow(); - - if (callContainer != null) - callContainer.ensureSize(component, width, height); - } - - /** - * Notifies interested listeners of a call title change. - */ - private void fireTitleChangeEvent() - { - Iterator<CallTitleListener> listeners; - - synchronized (titleListeners) - { - listeners = new Vector<CallTitleListener>(titleListeners).iterator(); - } - - while (listeners.hasNext()) - { - listeners.next().callTitleChanged(this); - } - } - - /** - * Returns the <tt>CallConference</tt> depicted by this <tt>CallPanel</tt> - * - * @return the <tt>CallConference</tt> depicted by this - * <tt>CallConference</tt> - */ - public CallConference getCallConference() - { - return callConference; - } - - /** - * Returns the initial call title. The call title could be then changed by - * call setCallTitle. - * - * @return the call title - */ - public String getCallTitle() - { - return title; - } - - /** - * Returns the parent call window. - * - * @return the parent call window - */ - public CallContainer getCallWindow() - { - return callWindow; - } - - /** - * Returns the currently used <tt>CallRenderer</tt>. - * @return the currently used <tt>CallRenderer</tt> - */ - public CallRenderer getCurrentCallRenderer() - { - return (CallRenderer) callPanel; - } - - /** - * Returns the <tt>DialpadDialog</tt> corresponding to this CallDialog. - * - * @return the <tt>DialpadDialog</tt> corresponding to this CallDialog. - */ - private DialpadDialog getDialpadDialog() - { - return new DialpadDialog(dtmfHandler); - } - - /** - * Finds the <tt>Contact</tt>s which are participating in the telephony - * conference depicted by this instance and which are capable of instant - * messaging i.e. support {@link OperationSetBasicTelephony}. - * - * @param limit the maximum number of <tt>Contact</tt>s to be found. Since - * it is expensive in terms of execution time (at very least) to find a - * <tt>Contact</tt> which stands for a <tt>CallPeer</tt> (and to query it - * whether it supports instant messaging), it is advised to limit the search - * as much as possible. For example, the <tt>chatButton</tt> is enabled - * and/or shown only when there is exactly one such <tt>Contact</tt> so it - * makes perfect sense to specify <tt>1</tt> as the <tt>limit</tt> in the - * case. - * @return a <tt>List</tt> of the <tt>Contact</tt>s which are participating - * in the telephony conference depicted by this instance and which are - * capable of instant messaging i.e. support - * <tt>OperationSetBasicTelephony</tt> - */ - private List<Contact> getIMCapableCallPeers(int limit) - { - List<CallPeer> callPeers = callConference.getCallPeers(); - List<Contact> contacts = new ArrayList<Contact>(callPeers.size()); - - /* - * Choose the CallPeers (or rather their associated Contacts) which are - * capable of basic instant messaging. - */ - for (CallPeer callPeer : callPeers) - { - if (callPeer.getProtocolProvider().getOperationSet( - OperationSetBasicInstantMessaging.class) - != null) - { - /* - * CallPeer#getContact) is more expensive in terms of execution - * than ProtocolProviderService#getOperationSet(Class). - */ - Contact contact = callPeer.getContact(); - - if (contact != null) - contacts.add(contact); - } - else - { - Contact contact = CallManager.getIMCapableCusaxContact(callPeer); - if (contact != null) - { - contacts.add(contact); - } - } - - if (contacts.size() >= limit) - break; - } - return contacts; - } - - /** - * Returns the minimum width needed to show buttons. - * Used to calculate the minimum size of the call dialog. - * @return the minimum width for the buttons. - */ - public int getMinimumButtonWidth() - { - int numberOfButtons = countButtons(settingsPanel.getComponents()); - - if (numberOfButtons > 0) - { - // +1 cause we had and a hangup button - // *32, a button is 28 pixels width and give some border - return (numberOfButtons + 1) * 32; - } - else - return -1; - } - - /** - * Initializes buttons order in the call tool bar. - */ - private void initButtonIndexes() - { - if (dialButton != null) - dialButton.setIndex(0); - if (conferenceButton != null) - conferenceButton.setIndex(1); - if (holdButton != null) - holdButton.setIndex(2); - if (recordButton != null) - recordButton.setIndex(3); - if (mergeButton != null) - mergeButton.setIndex(4); - if (transferCallButton != null) - transferCallButton.setIndex(5); - - localLevel.setIndex(6); - if (remoteLevel instanceof OrderedComponent) - ((OrderedComponent) remoteLevel).setIndex(7); - - if (desktopSharingButton != null) - desktopSharingButton.setIndex(8); - - if (fullScreenButton != null) - fullScreenButton.setIndex(10); - - if (videoButton != null) - videoButton.setIndex(11); - if (showHideVideoButton != null) - showHideVideoButton.setIndex(12); - chatButton.setIndex(19); - - if (infoButton != null) - infoButton.setIndex(20); - - hangupButton.setIndex(100); - } - - /** - * Initialize plug-in components already registered for this container. - */ - private void initPluginComponents() - { - // Search for plug-in components registered through the OSGI - // BundleContext. - - String osgiFilter - = "(" - + net.java.sip.communicator.service.gui.Container.CONTAINER_ID - + "=" - + net.java.sip.communicator.service.gui.Container - .CONTAINER_CALL_DIALOG.getID() - + ")"; - ServiceReference[] serRefs = null; - - try - { - serRefs - = GuiActivator.bundleContext.getServiceReferences( - PluginComponentFactory.class.getName(), - osgiFilter); - } - catch (InvalidSyntaxException ise) - { - logger.error("Could not obtain plugin reference.", ise); - } - - if (serRefs != null) - { - for (ServiceReference serRef : serRefs) - { - PluginComponentFactory factory - = (PluginComponentFactory) - GuiActivator.bundleContext.getService(serRef); - - PluginComponent component = - factory.getPluginComponentInstance(CallPanel.this); - - component.setCurrentContact( - CallManager.getPeerMetaContact( - callConference.getCallPeers().get(0))); - - settingsPanel.add((Component) component.getComponent()); - } - } - - GuiActivator.getUIService().addPluginComponentListener(this); - } - - /** - * Initializes the user interface hierarchy of this <tt>CallPanel</tt> i.e. - * the AWT <tt>Component</tt>s which constitute the user interface to be - * displayed by this <tt>Component</tt>. Their state does not have to depict - * the current state of the model of this view because - * {@link #updateViewFromModel()} will be invoked before this view becomes - * visible. At the center of the user interface of this view is - * {@link #callPanel} but it is dynamically added and removed multiple times - * as part of the execution of the <tt>updateViewFromModel</tt> method so - * it is not dealt with here. - */ - private void initializeUserInterfaceHierarchy() - { - /* - * The settingsPanel will contain the buttons. It is initialized before - * the buttons in case any of the buttons need it (which is hard to - * determine at the time of this writing). - */ - settingsPanel = new CallToolBar(isFullScreen(), false); - - /* - * TODO CallPanel depicts a whole CallConference which may have multiple - * Calls, new Calls may be added to the CallConference and existing - * Calls may be removed from the CallConference. For example, the - * buttons which accept a Call as an argument should be changed to take - * into account the whole CallConference. - */ - Call aCall = callConference.getCalls().get(0); - - chatButton - = new CallToolBarButton( - ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_WHITE), - CHAT_BUTTON, - GuiActivator.getResources().getI18NString( - "service.gui.CHAT")); - - if(isButtonEnabled(HIDE_CONFERENCE_BUTON_PROP)) - { - conferenceButton - = new CallToolBarButton( - ImageLoader.getImage(ImageLoader.ADD_TO_CALL_BUTTON), - CONFERENCE_BUTTON, - GuiActivator.getResources().getI18NString( - "service.gui.CREATE_CONFERENCE_CALL")); - } - - if(isButtonEnabled(HIDE_DESKTOP_SHARING_BUTON_PROP)) - { - desktopSharingButton = new DesktopSharingButton(aCall); - } - - if(isButtonEnabled(HIDE_DIAL_BUTON_PROP)) - { - dialButton - = new CallToolBarButton( - ImageLoader.getImage(ImageLoader.DIAL_BUTTON), - DIAL_BUTTON, - GuiActivator.getResources().getI18NString( - "service.gui.DIALPAD")); - } - - if(isButtonEnabled(HIDE_FULL_SCREEN_BUTON_PROP)) - { - fullScreenButton = new FullScreenButton(this); - } - - hangupButton = new HangupButton(this); - - if(isButtonEnabled(HIDE_CALL_HOLD_BUTON_PROP)) - { - holdButton = new HoldButton(aCall); - } - - if(isButtonEnabled(HIDE_CALL_INFO_BUTON_PROP)) - { - infoButton - = new CallToolBarButton( - ImageLoader.getImage(ImageLoader.CALL_INFO), - INFO_BUTTON, - GuiActivator.getResources().getI18NString( - "service.gui.PRESS_FOR_CALL_INFO")); - } - - if(isButtonEnabled(HIDE_CALL_MERGE_BUTON_PROP)) - { - mergeButton - = new CallToolBarButton( - ImageLoader.getImage(ImageLoader.MERGE_CALL_BUTTON), - MERGE_BUTTON, - GuiActivator.getResources().getI18NString( - "service.gui.MERGE_TO_CALL")); - - } - - if(isButtonEnabled(HIDE_CALL_RECORD_BUTON_PROP)) - { - recordButton = new RecordButton(aCall); - } - - if(isButtonEnabled(HIDE_TOGGLE_VIDEO_BUTON_PROP)) - { - showHideVideoButton = new ShowHideVideoButton(uiVideoHandler); - } - - if(isButtonEnabled(HIDE_CALL_TRANSFER_BUTON_PROP)) - { - transferCallButton = new TransferCallButton(aCall); - } - - if(isButtonEnabled(HIDE_VIDEO_BUTON_PROP)) - { - videoButton = new LocalVideoButton(aCall); - } - - localLevel - = new InputVolumeControlButton( - aCall, - ImageLoader.MICROPHONE, - ImageLoader.MUTE_BUTTON, - true, - false); - remoteLevel - = new OutputVolumeControlButton( - callConference, - ImageLoader.VOLUME_CONTROL_BUTTON, - false, - true) - .getComponent(); - - /* - * Now that the buttons have been initialized, set their order indexes - * so that they get added in the correct order later on. - */ - initButtonIndexes(); - - chatButton.addActionListener(this); - if (conferenceButton != null) - conferenceButton.addActionListener(this); - if (dialButton != null) - dialButton.addActionListener(this); - if (infoButton != null) - infoButton.addActionListener(this); - if (mergeButton != null) - mergeButton.addActionListener(this); - - settingsPanel.add(chatButton); - if (conferenceButton != null) - settingsPanel.add(conferenceButton); - if (desktopSharingButton != null) - settingsPanel.add(desktopSharingButton); - if (dialButton != null) - settingsPanel.add(dialButton); - if (fullScreenButton != null) - settingsPanel.add(fullScreenButton); - - settingsPanel.add(hangupButton); - - if (holdButton != null) - settingsPanel.add(holdButton); - if (infoButton != null) - settingsPanel.add(infoButton); - if (mergeButton != null) - settingsPanel.add(mergeButton); - if (recordButton != null) - settingsPanel.add(recordButton); - if (showHideVideoButton != null) - settingsPanel.add(showHideVideoButton); - if (mergeButton != null) - settingsPanel.add(transferCallButton); - if (videoButton != null) - settingsPanel.add(videoButton); - - // The bottom bar will contain the settingsPanel. - add(createBottomBar(), BorderLayout.SOUTH); - } - - /** - * Tests a provided boolean property name, returning false if it should be - * hidden. - * - * Used in {@link #initializeUserInterfaceHierarchy()} - * @param buttonHidePropertyName the name of the boolean property to check. - * @return false if the button should be hidden, true otherwise. - * - */ - private boolean isButtonEnabled(String buttonHidePropertyName) - { - return !GuiActivator.getConfigurationService().getBoolean( - buttonHidePropertyName, - false); - } - - /** - * Returns <code>true</code> if the call timer has been started, otherwise - * returns <code>false</code>. - * @return <code>true</code> if the call timer has been started, otherwise - * returns <code>false</code> - */ - public boolean isCallTimerStarted() - { - return isCallTimerStarted; - } - - /** - * Checks if the contained call is a conference call. - * - * @return <code>true</code> if the contained <tt>Call</tt> is a conference - * call, otherwise - returns <code>false</code>. - */ - boolean isConference() - { - // If we're the focus of the conference. - if (callConference.isConferenceFocus()) - return true; - - // If one of our peers is a conference focus, we're in a - // conference call. - List<CallPeer> callPeers = callConference.getCallPeers(); - - for (CallPeer callPeer : callPeers) - { - if (callPeer.isConferenceFocus()) - return true; - } - - // the call can have two peers at the same time and there is no one - // is conference focus. This is situation when someone has made an - // attended transfer and has transfered us. We have one call with two - // peers the one we are talking to and the one we have been transfered - // to. And the first one is been hanged up and so the call passes through - // conference call focus a moment and than go again to one to one call. - return callPeers.size() > 1; - } - - /** - * Determines whether this view is displayed in full-screen or windowed - * mode. - * - * @return <tt>true</tt> if this view is displayed in full-screen mode or - * <tt>false</tt> for windowed mode - */ - boolean isFullScreen() - { - return callWindow.isFullScreen(); - } - - /** - * Checks whether recording is currently enabled or not, state retrieved - * from call record button state. - * - * @return <tt>true</tt> if the recording is already started, <tt>false</tt> - * otherwise - */ - public boolean isRecordingStarted() - { - if (recordButton == null) - return false; - - return recordButton.isSelected(); - } - - /** - * Returns <tt>true</tt> if the show/hide video button is currently selected, - * <tt>false</tt> - otherwise. - * - * @return <tt>true</tt> if the show/hide video button is currently selected, - * <tt>false</tt> - otherwise - */ - public boolean isShowHideVideoButtonSelected() - { - return showHideVideoButton.isSelected(); - } - - /** - * Reloads icons. - */ - public void loadSkin() - { - if (dialButton != null) - { - dialButton.setBackgroundImage( - ImageLoader.getImage(ImageLoader.CALL_SETTING_BUTTON_BG)); - dialButton.setIconImage( - ImageLoader.getImage(ImageLoader.DIAL_BUTTON)); - } - - if (conferenceButton != null) - { - conferenceButton.setBackgroundImage( - ImageLoader.getImage(ImageLoader.CALL_SETTING_BUTTON_BG)); - conferenceButton.setIconImage( - ImageLoader.getImage(ImageLoader.ADD_TO_CALL_BUTTON)); - } - - if (hangupButton != null) - hangupButton.setBackgroundImage( - ImageLoader.getImage(ImageLoader.HANGUP_BUTTON_BG)); - } - - /** - * Notifies this instance about a specific <tt>VideoEvent</tt> which may - * warrant {@link #ensureSize(Component, int, int)} to be invoked in order - * to try to have the associated visual <tt>Component</tt> displaying video - * shown without scaling. The method will execute on the AWT event - * dispatching thread because it will be making its judgments based on the - * properties of AWT <tt>Component</tt>s. - * - * @param ev a <tt>VideoEvent</tt> which represents the cause of the - * notification and specifies the visual <tt>Component</tt> displaying video - * which may need an adjustment of a Frame's size in order to be displayed - * without scaling - */ - private void maybeEnsureSize(final VideoEvent ev) - { - if (!SwingUtilities.isEventDispatchThread()) - { - SwingUtilities.invokeLater( - new Runnable() - { - public void run() - { - maybeEnsureSize(ev); - } - }); - return; - } - - if (ev instanceof SizeChangeVideoEvent) - { - /* - * If a visual Component depicting video (streaming between the - * local peer/user and the remote peers) changes its size, try to - * adjust the size of the Frame which displays it so that it appears - * without scaling. - */ - SizeChangeVideoEvent scev = (SizeChangeVideoEvent) ev; - - ensureSize( - scev.getVisualComponent(), - scev.getWidth(), scev.getHeight()); - } - else if (ev.getType() == VideoEvent.VIDEO_ADDED) - { - Component video = ev.getVisualComponent(); - - if ((video != null) - && UIVideoHandler2.isAncestor(this, video) - && video.isPreferredSizeSet()) - { - Dimension prefSize = video.getPreferredSize(); - - if ((prefSize.height > 0) && (prefSize.width > 0)) - { - Dimension size = video.getSize(); - - if ((prefSize.height > size.height) - || (prefSize.width > size.width)) - { - ensureSize( - video, - prefSize.width, prefSize.height); - } - } - } - } - } - - /** - * Invoked by {@link #callConferenceListener} to notify this instance about - * an <tt>EventObject</tt> related to the <tt>CallConference</tt> depicted - * by this <tt>CallPanel</tt>, the <tt>Call</tt>s participating in it, - * the <tt>CallPeer</tt>s associated with them, the - * <tt>ConferenceMember</tt>s participating in any telephony conferences - * organized by them, etc. In other words, notifies this instance about - * any change which may cause an update to be required so that this view - * i.e. <tt>CallPanel</tt> depicts the current state of its model i.e. - * {@link #callConference}. - * - * @param ev the <tt>EventObject</tt> this instance is being notified - * about. - */ - private void onCallConferenceEventObject(EventObject ev) - { - /* - * The main task is to invoke updateViewFromModel() in order to make - * sure that this view depicts the current state of its model. - */ - - try - { - /* - * However, we seem to be keeping track of the duration of the call - * (i.e. the telephony conference) in the user interface. Stop the - * Timer which ticks the duration of the call as soon as the - * telephony conference depicted by this instance appears to have - * ended. The situation will very likely occur when a Call is - * removed from the telephony conference or a CallPeer is removed - * from a Call. - */ - boolean tryStopCallTimer = false; - - if (ev instanceof CallPeerEvent) - { - tryStopCallTimer - = (CallPeerEvent.CALL_PEER_REMOVED - == ((CallPeerEvent) ev).getEventID()); - } - else if (ev instanceof PropertyChangeEvent) - { - PropertyChangeEvent pcev = (PropertyChangeEvent) ev; - - tryStopCallTimer - = (CallConference.CALLS.equals(pcev.getPropertyName()) - && (pcev.getOldValue() instanceof Call) - && (pcev.getNewValue() == null)); - } - if (tryStopCallTimer - && (callConference.isEnded() - || callConference.getCallPeerCount() == 0)) - { - stopCallTimer(); - } - } - finally - { - updateViewFromModel(); - } - } - - /** - * Notifies this <tt>CallPanel</tt> about a specific <tt>CallEvent</tt> - * (received by <tt>CallManager</tt>). The source <tt>Call</tt> may or may - * not be participating in the telephony conference depicted by this - * instance but allows it to update any state which may depend on the - * <tt>Call</tt>s which are established application-wide. - * - * @param ev a <tt>CallEvent</tt> which specifies the <tt>Call</tt> which - * caused this instance to be notified and the exact type of the - * notification event - */ - void onCallEvent(CallEvent ev) - { - updateMergeButtonState(); - } - - /** - * Adds/removes the <tt>Component</tt> of the <tt>PluginComponent</tt> - * specified by a <tt>PluginComponentEvent</tt> to/from - * {@link #settingsPanel} (if it is appropriate for this - * <tt>Container</tt>). - * - * @param ev a <tt>PluginComponentEvent</tt> which specifies the - * <tt>PluginComponent</tt> whose <tt>Component</tt> is to be added/removed - * to/from {@link #settingsPanel} - */ - protected void onPluginComponentEvent(PluginComponentEvent ev) - { - PluginComponentFactory pc = ev.getPluginComponentFactory(); - - if (pc.getContainer().equals( - net.java.sip.communicator.service.gui.Container - .CONTAINER_CALL_DIALOG)) - { - PluginComponent plugin = - pc.getPluginComponentInstance(CallPanel.this); - Component c = (Component)plugin.getComponent(); - plugin.setCurrentContact( - CallManager.getPeerMetaContact( - callConference.getCallPeers().get(0))); - - switch (ev.getEventID()) - { - case PluginComponentEvent.PLUGIN_COMPONENT_ADDED: - settingsPanel.add(c); - break; - case PluginComponentEvent.PLUGIN_COMPONENT_REMOVED: - settingsPanel.remove(c); - break; - } - - settingsPanel.revalidate(); - settingsPanel.repaint(); - } - } - - /** - * Indicates that the peer panel was added. - * - * @param ev the event. - */ - public void peerViewAdded(ConferencePeerViewEvent ev) {} - - /** - * Indicates that the peer panel was removed. - * - * @param ev the event. - */ - public void peerViewRemoved(ConferencePeerViewEvent ev) - { - updateViewFromModel(); - } - - /** - * {@inheritDoc} - * - * Adds the <tt>Component</tt> of the <tt>PluginComponent</tt> specified by - * the <tt>PluginComponentEvent</tt> to {@link #settingsPanel} (if it is - * appropriate for this <tt>Container</tt>). - */ - public void pluginComponentAdded(PluginComponentEvent ev) - { - onPluginComponentEvent(ev); - } - - /** - * {@inheritDoc} - * - * Removes the <tt>Component</tt> of the <tt>PluginComponent</tt> specified - * by the <tt>PluginComponentEvent</tt> from {@link #settingsPanel} (if it - * is appropriate for this <tt>Container</tt>). - */ - public void pluginComponentRemoved(PluginComponentEvent ev) - { - onPluginComponentEvent(ev); - } - - /** - * Removes the given <tt>CallTitleListener</tt> to the list of listeners, - * notified for call title changes. - * - * @param l the <tt>CallTitleListener</tt> to remove - */ - public void removeCallTitleListener(CallTitleListener l) - { - synchronized (titleListeners) - { - titleListeners.remove(l); - } - } - - /** - * Remove remote video specific components. - */ - public void removeRemoteVideoSpecificComponents() - { - if(resizeVideoButton != null) - settingsPanel.remove(resizeVideoButton); - - settingsPanel.revalidate(); - settingsPanel.repaint(); - } - - /** - * Sets the title of this dialog in accord with a specific time of start of - * the telephony call/conference depicted by this <tt>CallPanel</tt>. - * - * @param startTime the time in milliseconds at which the telephony - * call/conference depicted by this <tt>CallPanel</tt> is considered to have - * started - */ - private void setCallTitle(long startTime) - { - StringBuilder title = new StringBuilder(); - - if (startTime != 0) - { - title.append( - GuiUtils.formatTime( - startTime, - System.currentTimeMillis())); - title.append(" | "); - } - else - title.append("00:00:00 | "); - - List<CallPeer> callPeers = callConference.getCallPeers(); - - if ((callPeers.size() > 0) - && (GuiActivator.getUIService().getSingleWindowContainer() - != null)) - { - title.append(callPeers.get(0).getDisplayName()); - } - else - { - title.append( - GuiActivator.getResources().getI18NString( - "service.gui.CALL")); - } - - this.title = title.toString(); - - fireTitleChangeEvent(); - } - - /** - * Sets the display of this view to full-screen or windowed mode. - * - * @param fullScreen <tt>true</tt> to display this view in full-screen mode - * or <tt>false</tt> for windowed mode - */ - void setFullScreen(boolean fullScreen) - { - callWindow.setFullScreen(fullScreen); - } - - /** - * Selects or unselects the video button in this call dialog. - * - * @param isSelected indicates if the video button should be selected or not - */ - public void setVideoButtonSelected(boolean isSelected) - { - if (isSelected && !videoButton.isSelected()) - videoButton.setSelected(true); - else if (!isSelected && videoButton.isSelected()) - videoButton.setSelected(false); - } - - /** - * Starts the timer that counts call duration. - */ - public void startCallTimer() - { - callConferenceStartTime = System.currentTimeMillis(); - callDurationTimer.start(); - isCallTimerStarted = true; - } - - /** - * Stops the timer that counts call duration. - */ - public void stopCallTimer() - { - this.callDurationTimer.stop(); - } - - /** - * Notifies this instance that {@link #uiVideoHandler} has reported a change - * in the video-related information which may warrant an update of this view - * from its model. - * - * @param arg an <tt>Object</tt>, if any, which represents the cause that - * triggered the notification - */ - private void uiVideoHandlerUpdate(Object arg) - { - /* The most important task is to update this view from its model. */ - - /* - * If a visual Component displaying video is reported to have been - * added/prepared/received, we may have to adjust the size of the Frame - * displaying this user interface so that the video appears without - * scaling. - */ - /* - * XXX The following may be making judgments about the user interface - * out of the AWT event dispatching thread which is a prerequisite for - * unexpected behavior. Anyway, that's the only idea at the time of this - * writing. - */ - VideoEvent maybeEnsureSize = null; - - if (arg instanceof VideoEvent) - { - try - { - VideoEvent vev = (VideoEvent) arg; - int vevType = vev.getType(); - - if (vevType == VideoEvent.VIDEO_ADDED) - { - Component video = vev.getVisualComponent(); - - if ((video != null) - && !UIVideoHandler2.isAncestor(this, video)) - { - maybeEnsureSize = vev; - } - } - else if (vevType == SizeChangeVideoEvent.VIDEO_SIZE_CHANGE) - { - /* - * If a visual Component depicting video (streaming between - * the local peer/user and the remote peers) changes its - * size, try to adjust the size of the Frame which displays - * it so that it appears without scaling. - */ - maybeEnsureSize = vev; - } - } - catch (Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - else if (logger.isDebugEnabled()) - { - logger.debug( - "Failed to determine whether it is necessary to" - + " adjust a Frame's size in response to a" - + " VideoEvent.", - t); - } - } - } - - updateViewFromModel(); - - if (maybeEnsureSize != null) - { - /* - * The method maybeEnsureSize will execute on the AWT event - * dispatching thread. - */ - try - { - maybeEnsureSize(maybeEnsureSize); - } - catch (Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - else - { - logger.error( - "Failed to adjust a Frame's size" - + " in response to a VideoEvent.", - t); - } - } - } - } - - /** - * Updates the state of the general hold button. The hold button is selected - * only if all call peers are locally or mutually on hold at the same time. - * In all other cases the hold button is unselected. - */ - public void updateHoldButtonState() - { - // If the hold button has been disabled by its configuration property we - // have nothing more to do here. - if (holdButton == null) - return; - - if(!SwingUtilities.isEventDispatchThread()) - { - SwingUtilities.invokeLater(new Runnable() - { - public void run() - { - updateHoldButtonState(); - } - }); - - return; - } - - List<CallPeer> peers = callConference.getCallPeers(); - boolean areAllPeersLocallyOnHold; - - if (peers.isEmpty()) - { - /* - * It feels natural to not have the holdButton selected when there - * are no peers. - */ - areAllPeersLocallyOnHold = false; - } - else - { - areAllPeersLocallyOnHold = true; - for (CallPeer peer : callConference.getCallPeers()) - { - CallPeerState state = peer.getState(); - - // If we have clicked the hold button in a full screen mode - // we need to update the state of the call dialog hold button. - if (!state.equals(CallPeerState.ON_HOLD_LOCALLY) - && !state.equals(CallPeerState.ON_HOLD_MUTUALLY)) - { - areAllPeersLocallyOnHold = false; - break; - } - } - } - - // If we have clicked the hold button in a full screen mode or selected - // hold of the peer menu in a conference call we need to update the - // state of the call dialog hold button. - holdButton.setSelected(areAllPeersLocallyOnHold); - } - - /** - * Updates the <tt>visible</tt> state/property of {@link #mergeButton} if - * the merge button is present. - */ - private void updateMergeButtonState() - { - // If the merge button isn't present, for example if it's hidden by - // its configuration property we have nothing more to do here. - if (mergeButton == null) - return; - - List<CallConference> conferences = new ArrayList<CallConference>(); - int cpt = 0; - - for (Call call : CallManager.getInProgressCalls()) - { - CallConference conference = call.getConference(); - - if (conference == null) - cpt++; - else if (!conferences.contains(conference)) - { - conferences.add(conference); - cpt++; - } - else - continue; - - if (cpt > 1) - break; - } - - mergeButton.setVisible(cpt > 1); - } - - /** - * Updates {@link #settingsPanel} from the model of this view. The update is - * performed in the AWT event dispatching thread. - * <p> - * The center of this view is occupied by {@link #callPanel}, the bottom of - * this view is dedicated to <tt>settingsPanel</tt>. The method - * {@link #updateViewFromModelInEventDispatchThread()} updates - * <tt>callPanel</tt> from the model of this view and then invokes the - * method <tt>updateSettingsPanelInEventDispatchThread()</tt>. Thus this - * whole view is updated so that it depicts the current state of its model. - * </p> - * - * @param callConferenceIsEnded <tt>true</tt> if the method - * <tt>updateViewFromModelInEventDispatchThread()</tt> considers the - * {@link #callConference} ended; otherwise, <tt>false</tt>. When the - * <tt>callConference</tt> is considered ended, the <tt>callPanel</tt> - * instance will not be switched to a specific type (one-to-one, audio-only, - * or audio/video) because, otherwise, the switch will leave it - * <tt>null</tt> and this view will remain blank. In such a case, - * <tt>settingsPanel</tt> may wish to do pretty much the same but disable - * and/or hide the buttons it contains. - */ - private void updateSettingsPanelInEventDispatchThread( - boolean callConferenceIsEnded) - { - /* - * XXX The method directly delegates to the method - * doUpdateSettingsPanelInEventDispatchThread at the time of this - * writing which may be considered a waste. But in the fashion of the - * method updateViewFromModelInEventDispatchThread we have made it easy - * to add code before and/or after the invocation of the delegate. - */ - doUpdateSettingsPanelInEventDispatchThread(callConferenceIsEnded); - } - - /** - * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current - * state of its model i.e. <tt>callConference</tt>. - */ - private void updateViewFromModel() - { - /* - * We receive events/notifications from various threads and we respond - * to them in the AWT event dispatching thread. It is possible to first - * schedule an event to be brought to the AWT event dispatching thread, - * then to have #dispose() invoked on this instance and, finally, to - * receive the scheduled event in the AWT event dispatching thread. In - * such a case, this disposed instance should not respond to the event. - */ - if (!disposed) - { - if (SwingUtilities.isEventDispatchThread()) - updateViewFromModelInEventDispatchThread(); - else - { - SwingUtilities.invokeLater( - updateViewFromModelInEventDispatchThread); - } - } - } - - /** - * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current - * state of its model i.e. <tt>callConference</tt>. The update is performed - * in the AWT event dispatching thread. - */ - private void updateViewFromModelInEventDispatchThread() - { - /* - * We receive events/notifications from various threads and we respond - * to them in the AWT event dispatching thread. It is possible to first - * schedule an event to be brought to the AWT event dispatching thread, - * then to have #dispose() invoked on this instance and, finally, to - * receive the scheduled event in the AWT event dispatching thread. In - * such a case, this disposed instance should not respond to the event. - */ - if (disposed) - return; - - /* - * We may add, remove, show, and hide various Components of the user - * interface hierarchy of this instance bellow. Consequently, this view - * may become larger in width and/or height than its current Frame has - * dedicated to it. Try to detect such cases and attempt to adjust the - * Frame's size accordingly. - */ - Dimension oldPrefSize = getPreferredSize(); - - doUpdateViewFromModelInEventDispatchThread(); - - /* - * We may have added, removed, shown, and hidden various Components of - * the user interface hierarchy of this instance above. Consequently, - * this view may have become larger in width and/or height than its - * current Frame has dedicated to it. Try to detect such cases and - * attempt to adjust the Frame's size accordingly. - */ - Dimension newPrefSize = getPreferredSize(); - - if ((newPrefSize != null) - && ((newPrefSize.height > getHeight()) - || (newPrefSize.width > getWidth()))) - { - int oldPrefHeight, oldPrefWidth; - - if (oldPrefSize == null) - { - oldPrefHeight = 0; - oldPrefWidth = 0; - } - else - { - oldPrefHeight = oldPrefSize.height; - oldPrefWidth = oldPrefSize.width; - } - if ((newPrefSize.height != oldPrefHeight) - || (newPrefSize.width != oldPrefWidth)) - { - ensureSize( - this, - newPrefSize.width, newPrefSize.height); - } - } - } - - /** - * Listens for contact status changes and updates the image of the - * chat message button. - * @param evt the ContactPresenceStatusChangeEvent describing the status - */ - @Override - public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) - { - Contact contact = getIMCapableCallPeers(1).get(0); - - if(contact != null && contact.equals(evt.getSourceContact())) - { - chatButton.setIconImage( - Constants.getMessageStatusIcon(contact.getPresenceStatus())); - chatButton.repaint(); - } - } - - /** - * Implements the listener which listens to events fired by the - * <tt>CallConference</tt> depicted by this instance, the <tt>Call</tt>s - * participating in that telephony conference, the <tt>CallPeer</tt>s - * associated with those <tt>Call</tt>s and the <tt>ConferenceMember</tt>s - * participating in the telephony conferences organized by those - * <tt>CallPeer</tt>s. Updates this view i.e. CallPanel so that it depicts - * the current state of its model i.e. {@link #callConference}. - */ - private class CallConferenceListener - extends CallPeerConferenceAdapter - implements CallChangeListener, - PropertyChangeListener - { - /** - * {@inheritDoc} - * - * Invokes {@link #onCallPeerEvent(CallPeerEvent)} because the - * <tt>CallPeerEvent</tt> allows distinguishing whether a - * <tt>CallPeer</tt> was added or removed by examining its - * <tt>eventID</tt>. - */ - public void callPeerAdded(CallPeerEvent ev) - { - onCallPeerEvent(ev); - } - - /** - * {@inheritDoc} - * - * Invokes {@link #onCallPeerEvent(CallPeerEvent)} because the - * <tt>CallPeerEvent</tt> allows distinguishing whether a - * <tt>CallPeer</tt> was added or removed by examining its - * <tt>eventID</tt>. - */ - public void callPeerRemoved(CallPeerEvent ev) - { - onCallPeerEvent(ev); - } - - /** - * {@inheritDoc} - * - * Invokes {@link #onEventObject(EventObject)}. - */ - public void callStateChanged(CallChangeEvent ev) - { - onEventObject(ev); - } - - /** - * {@inheritDoc} - * - * Invokes {@link #onEventObject(EventObject)}. - */ - @Override - protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) - { - onEventObject(ev); - } - - /** - * Notifies this <tt>CallChangeListener</tt> about a specific - * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was added to or - * removed from a <tt>Call</tt>. Invokes - * {@link #onEventObject(EventObject)}. - * - * @param ev the <tt>CallPeerEvent</tt> to notify this - * <tt>CallChangeListener</tt> about i.e. which specifies the - * <tt>CallPeer</tt> which was added/removed and the <tt>Call</tt> - * to/from which it was added/removed - */ - private void onCallPeerEvent(CallPeerEvent ev) - { - onEventObject(ev); - } - - /** - * Invoked by the various listener method implementations provided by - * this <tt>CallConferenceListener</tt> to notify this instance about an - * <tt>EventObject</tt> related to the <tt>CallConference</tt> depicted - * by this <tt>CallPanel</tt>, the <tt>Call</tt>s participating in it, - * the <tt>CallPeer</tt>s associated with them, the - * <tt>ConferenceMember</tt>s participating in any telephony conferences - * organized by them, etc. In other words, notifies this instance about - * any change which may cause an update to be required so that this view - * i.e. <tt>CallPanel</tt> depicts the current state of its model i.e. - * {@link CallPanel#callConference}. - * - * @param ev the <tt>EventObject</tt> this instance is being notified - * about. - */ - private void onEventObject(EventObject ev) - { - onCallConferenceEventObject(ev); - } - - /** - * {@inheritDoc} - * - * Invokes {@link #onEventObject(EventObject)}. - */ - public void propertyChange(PropertyChangeEvent ev) - { - String propertyName = ev.getPropertyName(); - - /* - * If a Call is added to or removed from the CallConference depicted - * by this CallPanel, an update of the view from its model will most - * likely be required. - */ - if (propertyName.equals(CallConference.CALLS)) - { - onEventObject(ev); - } - else if (propertyName.equals(CallContainer.PROP_FULL_SCREEN)) - { - if (ev.getSource().equals(callWindow.getFrame())) - { - try - { - /* - * We'll turn the switching between full-screen and - * windowed mode into a model state because a - * significant part of this view changes upon such a - * switch. - */ - onEventObject(ev); - } - finally - { - callWindowPropertyChange(ev); - } - } - } - } - } -} +/*
+ * 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.impl.gui.main.call;
+
+import java.awt.*;
+import java.awt.Container;
+import java.awt.event.*;
+import java.beans.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+import javax.swing.Timer;
+
+import net.java.sip.communicator.impl.gui.*;
+import net.java.sip.communicator.impl.gui.event.*;
+import net.java.sip.communicator.impl.gui.main.call.conference.*;
+import net.java.sip.communicator.impl.gui.utils.*;
+import net.java.sip.communicator.impl.gui.utils.Constants;
+import net.java.sip.communicator.plugin.desktoputil.*;
+import net.java.sip.communicator.service.contactlist.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.gui.call.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.service.protocol.media.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.Logger;
+import net.java.sip.communicator.util.skin.*;
+
+import org.jitsi.service.neomedia.*;
+import org.jitsi.util.*;
+import org.jitsi.util.event.*;
+import org.osgi.framework.*;
+
+/**
+ * The dialog created for a given call.
+ *
+ * Ordered buttons we are adding/removing, numbers are the index we have set.
+ * And the order that will be kept.
+ * 0 dialButton
+ * 1 conferenceButton
+ * 2 holdButton
+ * 3 recordButton
+ * 4 mergeButton
+ * 5 transferCallButton
+ * 6 localLevel
+ * 7 remoteLevel
+ * 8 desktopSharingButton
+ * 9 resizeVideoButton
+ * 10 fullScreenButton
+ * 11 videoButton
+ * 12 showHideVideoButton
+ * 19 chatButton
+ * 20 infoButton
+ *
+ * @author Yana Stamcheva
+ * @author Adam Netocny
+ * @author Lyubomir Marinov
+ * @author Hristo Terezov
+ */
+public class CallPanel
+ extends TransparentPanel
+ implements ActionListener,
+ PluginComponentListener,
+ Skinnable,
+ ConferencePeerViewListener,
+ ContactPresenceStatusListener
+{
+ /**
+ * The chat button name.
+ */
+ private static final String CHAT_BUTTON = "CHAT_BUTTON";
+
+ /**
+ * The conference button name.
+ */
+ private static final String CONFERENCE_BUTTON = "CONFERENCE_BUTTON";
+
+ /**
+ * The dial button name.
+ */
+ private static final String DIAL_BUTTON = "DIAL_BUTTON";
+
+ /**
+ * The info button name.
+ */
+ private static final String INFO_BUTTON = "INFO_BUTTON";
+
+ /**
+ * The logger for this class.
+ */
+ private static final Logger logger = Logger.getLogger(CallDialog.class);
+
+ /**
+ * The hang up button name.
+ */
+ private static final String MERGE_BUTTON = "MERGE_BUTTON";
+
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Property to disable the info button.
+ */
+ private static final String HIDE_CALL_INFO_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_INFO_BUTTON";
+
+ /**
+ * Property to disable the conference "add to call" button.
+ */
+ private static final String HIDE_CONFERENCE_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CONFERENCE_BUTTON";
+
+ /**
+ * Property to disable the record button.
+ */
+ private static final String HIDE_CALL_RECORD_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_RECORD_BUTTON";
+
+ /**
+ * Property to disable the "call merge" button.
+ */
+ private static final String HIDE_CALL_MERGE_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_MERGE_BUTTON";
+
+ /**
+ * Property to disable the "call merge" button.
+ */
+ private static final String HIDE_CALL_TRANSFER_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_TRANSFER_BUTTON";
+
+ /**
+ * Property to disable the "hold" button.
+ */
+ private static final String HIDE_CALL_HOLD_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_CALL_HOLD_BUTTON";
+
+ /**
+ * Property to disable the dial button.
+ */
+ private static final String HIDE_DIAL_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_DIAL_BUTTON";
+
+ /**
+ * Property to disable the video button.
+ */
+ private static final String HIDE_VIDEO_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_VIDEO_BUTTON";
+
+ /**
+ * Property to disable the desktop sharing button.
+ */
+ private static final String HIDE_DESKTOP_SHARING_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_DESKTOP_SHARING_BUTTON";
+
+ /**
+ * Property to disable the full screen button.
+ */
+ private static final String HIDE_FULL_SCREEN_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_FULL_SCREEN_BUTTON";
+
+ /**
+ * Property to disable the "show/hide local video" button.
+ */
+ private static final String HIDE_TOGGLE_VIDEO_BUTON_PROP
+ = "net.java.sip.communicator.impl.gui.main.call.HIDE_TOGGLE_VIDEO_BUTTON";
+
+ /**
+ * The <tt>Component</tt> which is at the bottom of this view and contains
+ * {@link #settingsPanel}. It overrides the Swing-defined background on OS
+ * X so it needs explicit updating upon switching between full-screen and
+ * windowed mode in order to respect any background-related settings of the
+ * ancestors such as black background in full-screen mode.
+ */
+ private JComponent bottomBar;
+
+ /**
+ * The {@link CallConference} instance depicted by this <tt>CallPanel</tt>.
+ */
+ private final CallConference callConference;
+
+ /**
+ * The listener which listens to events fired by the <tt>CallConference</tt>
+ * depicted by this instance, the <tt>Call</tt>s participating in that
+ * telephony conference, the <tt>CallPeer</tt>s associated with those
+ * <tt>Call</tt>s and the <tt>ConferenceMember</tt>s participating in the
+ * telephony conferences organized by those <tt>CallPeer</tt>s. Updates this
+ * view i.e. CallPanel so that it depicts the current state of its model
+ * i.e. {@link #callConference}.
+ */
+ private final CallConferenceListener callConferenceListener
+ = new CallConferenceListener();
+
+ /**
+ * The time in milliseconds at which the telephony call/conference depicted
+ * by this <tt>CallPanel</tt> (i.e. {@link #callConference}) has started.
+ */
+ private long callConferenceStartTime;
+
+ /**
+ * A timer to count call duration.
+ */
+ private Timer callDurationTimer;
+
+ /**
+ * The Frame used to display this call information statistics.
+ */
+ private CallInfoFrame callInfoFrame;
+
+ /**
+ * The panel representing the call. For conference calls this would be an
+ * instance of <tt>ConferenceCallPanel</tt> and for one-to-one calls this
+ * would be an instance of <tt>OneToOneCallPanel</tt>.
+ */
+ private JComponent callPanel;
+
+ /**
+ * Parent window.
+ */
+ private final CallContainer callWindow;
+
+ /**
+ * Chat button.
+ */
+ private SIPCommButton chatButton;
+
+ /**
+ * The operation set that will be used to update chatButton icon and
+ * the corresponding contact.
+ */
+ private OperationSetPresence operationSetPresence;
+
+ /**
+ * The conference button.
+ */
+ private CallToolBarButton conferenceButton;
+
+ /**
+ * The desktop sharing button.
+ */
+ private DesktopSharingButton desktopSharingButton;
+
+ /**
+ * The dial button, which opens a keypad dialog.
+ */
+ private CallToolBarButton dialButton;
+
+ /**
+ * The dial pad dialog opened when the dial button is clicked.
+ */
+ private DialpadDialog dialpadDialog;
+
+ /**
+ * The indicator which determines whether {@link #dispose()} has already
+ * been invoked on this instance. If <tt>true</tt>, this instance is
+ * considered non-functional and is to be left to the garbage collector.
+ */
+ private boolean disposed = false;
+
+ /**
+ * The handler for DTMF tones.
+ */
+ private DTMFHandler dtmfHandler;
+
+ /**
+ * The full screen button.
+ */
+ private FullScreenButton fullScreenButton;
+
+ /**
+ * HangUp button.
+ */
+ private SIPCommButton hangupButton;
+
+ /**
+ * The hold button.
+ */
+ private HoldButton holdButton;
+
+ /**
+ * Info button.
+ */
+ private SIPCommButton infoButton;
+
+ /**
+ * Indicates if the call timer has been started.
+ */
+ private boolean isCallTimerStarted = false;
+
+ /**
+ * Sound local level label.
+ */
+ private InputVolumeControlButton localLevel;
+
+ /**
+ * Merge button.
+ */
+ private CallToolBarButton mergeButton;
+
+ /**
+ * The button which allows starting and stopping the recording of
+ * {@link #callConference}.
+ */
+ private RecordButton recordButton;
+
+ /**
+ * Sound remote level label.
+ */
+ private Component remoteLevel;
+
+ /**
+ * The video resize button.
+ */
+ private ResizeVideoButton resizeVideoButton;
+
+ /**
+ * The panel containing call settings.
+ */
+ private CallToolBar settingsPanel;
+
+ /**
+ * The button responsible for hiding/showing the local video.
+ */
+ private ShowHideVideoButton showHideVideoButton;
+
+ /**
+ * The title of this call container.
+ */
+ private String title;
+
+ /**
+ * A collection of listeners, registered for call title change events.
+ */
+ private Collection<CallTitleListener> titleListeners
+ = new Vector<CallTitleListener>();
+
+ /**
+ * The transfer call button.
+ */
+ private TransferCallButton transferCallButton;
+
+ /**
+ * The facility which aids this instance in the dealing with the
+ * video-related information.
+ */
+ private final UIVideoHandler2 uiVideoHandler;
+
+ /**
+ * Indicates if this call panel should be closed immediately after hang up
+ * or should wait some time so that the user can be notified of the last
+ * state. By default we wait, so that the user can be notified of the
+ * current state of the call.
+ */
+ private boolean isCloseWaitAfterHangup = true;
+
+ /**
+ * The <tt>Observer</tt> which listens to {@link #uiVideoHandler} about
+ * changes in the video-related information.
+ */
+ private final Observer uiVideoHandlerObserver
+ = new Observer()
+ {
+ public void update(Observable o, Object arg)
+ {
+ uiVideoHandlerUpdate(arg);
+ }
+ };
+
+ /**
+ * The <tt>Runnable</tt> which is scheduled by
+ * {@link #updateViewFromModel()} for execution in the AWT event dispatching
+ * thread in order to invoke
+ * {@link #updateViewFromModelInEventDispatchThread()}.
+ */
+ private final Runnable updateViewFromModelInEventDispatchThread
+ = new Runnable()
+ {
+ public void run()
+ {
+ /*
+ * We receive events/notifications from various threads and we
+ * respond to them in the AWT event dispatching thread. It is
+ * possible to first schedule an event to be brought to the AWT
+ * event dispatching thread, then to have #dispose() invoked on
+ * this instance and, finally, to receive the scheduled event in
+ * the AWT event dispatching thread. In such a case, this
+ * disposed instance should not respond to the event.
+ */
+ if (!disposed)
+ updateViewFromModelInEventDispatchThread();
+ }
+ };
+
+ /**
+ * The video button.
+ */
+ private LocalVideoButton videoButton;
+
+ /**
+ * Initializes a new <tt>CallPanel</tt> which is to depict a specific
+ * <tt>CallConference</tt>.
+ *
+ * @param callConference the <tt>CallConference</tt> to be depicted by the
+ * new instance
+ * @param callWindow the parent window in which the new instance will be
+ * added
+ */
+ public CallPanel( CallConference callConference,
+ CallContainer callWindow)
+ {
+ super(new BorderLayout());
+
+ this.callConference = callConference;
+ this.callWindow = callWindow;
+
+ uiVideoHandler = new UIVideoHandler2(this.callConference);
+
+ callDurationTimer
+ = new Timer(
+ 1000,
+ new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ setCallTitle(callConferenceStartTime);
+ }
+ });
+ callDurationTimer.setRepeats(true);
+
+ // The call duration parameter is not known yet.
+ setCallTitle(0);
+
+ initializeUserInterfaceHierarchy();
+
+ dtmfHandler = new DTMFHandler(this);
+
+ /*
+ * Adds the listeners which will observe the model and will trigger the
+ * updates of this view from it.
+ */
+ this.callConference.addCallChangeListener(callConferenceListener);
+ this.callConference.addCallPeerConferenceListener(
+ callConferenceListener);
+ this.callConference.addPropertyChangeListener(callConferenceListener);
+ uiVideoHandler.addObserver(uiVideoHandlerObserver);
+
+ callWindow.getFrame().addPropertyChangeListener(
+ CallContainer.PROP_FULL_SCREEN,
+ callConferenceListener);
+
+ updateViewFromModel();
+
+ initPluginComponents();
+ }
+
+ /**
+ * Handles action events.
+ * @param evt the <tt>ActionEvent</tt> that was triggered
+ */
+ public void actionPerformed(ActionEvent evt)
+ {
+ JButton button = (JButton) evt.getSource();
+ String buttonName = button.getName();
+
+ if (buttonName.equals(MERGE_BUTTON))
+ {
+ CallManager.mergeExistingCalls(
+ callConference,
+ CallManager.getInProgressCalls());
+ }
+ else if (buttonName.equals(DIAL_BUTTON))
+ {
+ if (dialpadDialog == null)
+ dialpadDialog = this.getDialpadDialog();
+
+ if(!dialpadDialog.isVisible())
+ {
+ dialpadDialog.pack();
+
+ Point location = new Point( button.getX(),
+ button.getY() + button.getHeight());
+ SwingUtilities.convertPointToScreen(
+ location, button.getParent());
+
+ dialpadDialog.setLocation(
+ (int) location.getX() + 2,
+ (int) location.getY() + 2);
+
+ dialpadDialog.addWindowFocusListener(dialpadDialog);
+ dialpadDialog.setVisible(true);
+ }
+ else
+ {
+ dialpadDialog.removeWindowFocusListener(dialpadDialog);
+ dialpadDialog.setVisible(false);
+ }
+ }
+ else if (buttonName.equals(CONFERENCE_BUTTON))
+ {
+ ConferenceInviteDialog inviteDialog;
+
+ if (callConference.isJitsiVideobridge())
+ {
+ inviteDialog
+ = new ConferenceInviteDialog(
+ callConference,
+ callConference.getCalls().get(0)
+ .getProtocolProvider(),
+ true);
+ }
+ else
+ inviteDialog = new ConferenceInviteDialog(callConference);
+
+ inviteDialog.setVisible(true);
+ }
+ else if (buttonName.equals(CHAT_BUTTON))
+ {
+ /*
+ * If there is exactly 1 CallPeer capable of instant messaging, then
+ * we'll try to start a chat with her.
+ */
+ /*
+ * TODO The following is very likely to block the user interface in
+ * a noticeable way sooner or later.
+ */
+ List<Contact> imCapableCallPeers = getIMCapableCallPeers(1);
+
+ if (imCapableCallPeers.size() == 1)
+ {
+ Contact contact = imCapableCallPeers.get(0);
+ MetaContact metaContact
+ = GuiActivator.getContactListService()
+ .findMetaContactByContact(contact);
+ GuiActivator.getUIService().getChatWindowManager().startChat(
+ metaContact);
+ }
+ }
+ else if (buttonName.equals(INFO_BUTTON))
+ {
+ if (callInfoFrame == null)
+ {
+ callInfoFrame = new CallInfoFrame(callConference);
+ addCallTitleListener(callInfoFrame);
+ }
+ callInfoFrame.setVisible(
+ callInfoFrame.hasCallInfo() && !callInfoFrame.isVisible());
+ }
+ }
+
+ /**
+ * Executes the action associated with the "Hang up" button which may be
+ * invoked by clicking the button in question or by closing this dialog.
+ *
+ * @param closeWait <tt>true</tt> to close this instance with a few seconds
+ * of delay or <tt>false</tt> to close it immediately
+ */
+ public void actionPerformedOnHangupButton(boolean closeWait)
+ {
+ isCloseWaitAfterHangup = closeWait;
+
+ this.disposeCallInfoFrame();
+
+ /*
+ * It is the responsibility of CallManager to close this CallPanel
+ * when a Call is ended.
+ */
+ if (callConference.getCallCount() > 0)
+ CallManager.hangupCalls(callConference);
+ /*
+ * If however there are no more calls related to this panel we will
+ * close the window directly. This could happen in the case, where
+ * the other side has already hanged up the call, the call window shows
+ * the state disconnected and we press the hang up button. In this
+ * case the contained call is already null and we should only close the
+ * call window.
+ */
+ else
+ callWindow.close(this, false);
+ }
+
+ /**
+ * Indicates if this call panel should be closed immediately after hang up
+ * or should wait some time so that the user can be notified of the last
+ * state.
+ *
+ * @return <tt>true</tt> to indicate that when hanged up this call panel
+ * should not be closed immediately, <tt>false</tt> - otherwise
+ */
+ public boolean isCloseWaitAfterHangup()
+ {
+ return isCloseWaitAfterHangup;
+ }
+
+ /**
+ * Adds the given <tt>CallTitleListener</tt> to the list of listeners,
+ * notified for call title changes.
+ *
+ * @param l the <tt>CallTitleListener</tt> to add
+ */
+ public void addCallTitleListener(CallTitleListener l)
+ {
+ synchronized (titleListeners)
+ {
+ titleListeners.add(l);
+ }
+ }
+
+ /**
+ * Adds remote video specific components.
+ *
+ * @param callPeer the <tt>CallPeer</tt>
+ */
+ public void addRemoteVideoSpecificComponents(CallPeer callPeer)
+ {
+ if(CallManager.isVideoQualityPresetSupported(callPeer))
+ {
+ if(resizeVideoButton == null)
+ {
+ resizeVideoButton = new ResizeVideoButton(callPeer.getCall());
+ resizeVideoButton.setIndex(9);
+ }
+
+ if(resizeVideoButton.countAvailableOptions() > 1)
+ {
+ settingsPanel.add(resizeVideoButton);
+ settingsPanel.revalidate();
+ settingsPanel.repaint();
+ }
+ }
+ }
+
+ /**
+ * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by
+ * {@link #callWindow}.
+ *
+ * @param ev the <tt>PropertyChangeEvent</tt> fired by <tt>callWindow</tt>
+ * to notify this instance about
+ */
+ private void callWindowPropertyChange(PropertyChangeEvent ev)
+ {
+ /*
+ * We are registered for CallContainer#PROP_FULL_SCREEN only. This
+ * instance will fire the notification as its own to allow listeners to
+ * register with a source which is more similar to them with respect to
+ * life span.
+ */
+ try
+ {
+ if (OSUtils.IS_MAC && (bottomBar != null))
+ bottomBar.setOpaque(!isFullScreen());
+ }
+ finally
+ {
+ firePropertyChange(
+ ev.getPropertyName(),
+ ev.getOldValue(), ev.getNewValue());
+ }
+ }
+
+ /**
+ * Count the number of the buttons in the supplied components.
+ * @param cs the components to search for buttons.
+ * @return number of buttons.
+ */
+ private int countButtons(Component[] cs)
+ {
+ int count = 0;
+
+ for(Component c : cs)
+ {
+ if(c instanceof SIPCommButton || c instanceof SIPCommToggleButton)
+ count++;
+ if(c instanceof Container)
+ count += countButtons(((Container)c).getComponents());
+ }
+
+ return count;
+ }
+
+ /**
+ * Creates the bottom bar panel for this <tt>CallPanel</tt>.
+ *
+ * @return a new bottom bar panel for this <tt>CallPanel</tt>
+ */
+ private JComponent createBottomBar()
+ {
+ bottomBar
+ = new TransparentPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
+ bottomBar.setBorder(BorderFactory.createEmptyBorder(0, 30, 2, 30));
+
+ /*
+ * The bottomBar on OS X overrides the Swing-defined background.
+ * However, full-screen display usually uses a black background. The
+ * black background will be set elsewhere on an ancestor but we have to
+ * make sure that bottomBar's background does not interfere with the
+ * setting.
+ */
+ if (OSUtils.IS_MAC)
+ {
+ bottomBar.setOpaque(!isFullScreen());
+ bottomBar.setBackground(
+ new Color(GuiActivator.getResources().getColor(
+ "service.gui.MAC_PANEL_BACKGROUND")));
+ }
+
+ bottomBar.add(settingsPanel);
+
+ return bottomBar;
+ }
+
+ /**
+ * Releases the resources acquired by this instance which require explicit
+ * disposal (e.g. any listeners added to the depicted
+ * <tt>CallConference</tt>, the participating <tt>Call</tt>s, and their
+ * associated <tt>CallPeer</tt>s). Invoked by <tt>CallManager</tt> when it
+ * determines that this <tt>CallPanel</tt> is no longer necessary.
+ */
+ void dispose()
+ {
+ disposed = true;
+
+ callConference.removeCallChangeListener(callConferenceListener);
+ callConference.removeCallPeerConferenceListener(callConferenceListener);
+ callConference.removePropertyChangeListener(callConferenceListener);
+
+ uiVideoHandler.deleteObserver(uiVideoHandlerObserver);
+ uiVideoHandler.dispose();
+
+ callWindow.getFrame().removePropertyChangeListener(
+ CallContainer.PROP_FULL_SCREEN,
+ callConferenceListener);
+
+ if (callPanel != null)
+ {
+ if(callPanel instanceof BasicConferenceCallPanel)
+ {
+ ((BasicConferenceCallPanel) callPanel)
+ .removePeerViewListener(this);
+ }
+ ((CallRenderer) callPanel).dispose();
+ }
+
+ // clears the contact status listener
+ if(operationSetPresence != null)
+ {
+ operationSetPresence.removeContactPresenceStatusListener(this);
+ }
+ }
+
+ /**
+ * Disposes the call info frame if it exists.
+ */
+ public void disposeCallInfoFrame()
+ {
+ if (callInfoFrame != null)
+ callInfoFrame.dispose();
+ }
+
+ /**
+ * Updates {@link #settingsPanel} from the model of this view. The update is
+ * performed in the AWT event dispatching thread.
+ * <p>
+ * The center of this view is occupied by {@link #callPanel}, the bottom of
+ * this view is dedicated to <tt>settingsPanel</tt>. The method
+ * {@link #updateViewFromModelInEventDispatchThread()} updates
+ * <tt>callPanel</tt> from the model of this view and then invokes the
+ * method <tt>updateSettingsPanelInEventDispatchThread()</tt>. Thus this
+ * whole view is updated so that it depicts the current state of its model.
+ * </p>
+ *
+ * @param callConferenceIsEnded <tt>true</tt> if the method
+ * <tt>updateViewFromModelInEventDispatchThread()</tt> considers the
+ * {@link #callConference} ended; otherwise, <tt>false</tt>. When the
+ * <tt>callConference</tt> is considered ended, the <tt>callPanel</tt>
+ * instance will not be switched to a specific type (one-to-one, audio-only,
+ * or audio/video) because, otherwise, the switch will leave it
+ * <tt>null</tt> and this view will remain blank. In such a case,
+ * <tt>settingsPanel</tt> may wish to do pretty much the same but disable
+ * and/or hide the buttons it contains.
+ */
+ private void doUpdateSettingsPanelInEventDispatchThread(
+ boolean callConferenceIsEnded)
+ {
+ settingsPanel.setFullScreen(isFullScreen());
+
+ boolean isConference = (callPanel instanceof BasicConferenceCallPanel);
+
+ /*
+ * For whatever reason, we're treating the localLevel and the
+ * remoteLevel buttons differently and we're adding and removing them in
+ * accord with the conference state of the user interface.
+ */
+ if (isConference)
+ {
+ settingsPanel.add(localLevel);
+ settingsPanel.add(remoteLevel);
+ }
+ else
+ {
+ settingsPanel.remove(localLevel);
+ settingsPanel.remove(remoteLevel);
+ }
+
+ /*
+ * We do not support chat conferencing with the participants in a
+ * telephony conference at this time so we do not want the chatButton
+ * visible in such a scenario.
+ */
+ List<Contact> imContacts = getIMCapableCallPeers(1);
+ chatButton.setVisible(
+ !isConference && (imContacts.size() == 1));
+ if(chatButton.isVisible() && operationSetPresence == null)
+ {
+ Contact contact = imContacts.get(0);
+ operationSetPresence =
+ contact.getProtocolProvider()
+ .getOperationSet(OperationSetPresence.class);
+ if(operationSetPresence != null)
+ operationSetPresence.addContactPresenceStatusListener(this);
+
+ chatButton.setIconImage(
+ Constants.getMessageStatusIcon(contact.getPresenceStatus()));
+ chatButton.repaint();
+ }
+
+ updateHoldButtonState();
+ updateMergeButtonState();
+
+ List<Call> calls = callConference.getCalls();
+ /*
+ * OperationSetAdvancedTelephony implements call transfer. The feature
+ * is not supported if the local user/peer is a conference focus.
+ * Instead of disabling the transferCallButton in this case though, we
+ * want it hidden.
+ */
+ boolean advancedTelephony = !calls.isEmpty();
+ boolean telephonyConferencing = false;
+ boolean videoTelephony = false;
+ boolean videoTelephonyIsLocalVideoAllowed = false;
+ boolean videoTelephonyIsLocalVideoStreaming = false;
+ boolean desktopSharing = false;
+ boolean desktopSharingIsStreamed = false;
+ boolean allCallsConnected = true;
+
+ for (Call call : calls)
+ {
+ ProtocolProviderService pps = call.getProtocolProvider();
+
+ /*
+ * The transferCallButton requires OperationSetAdvancedTelephony
+ * for all Calls.
+ */
+ if (advancedTelephony)
+ {
+ OperationSetAdvancedTelephony<?> osat
+ = pps.getOperationSet(OperationSetAdvancedTelephony.class);
+
+ if (osat == null)
+ advancedTelephony = false;
+ }
+
+ /*
+ * The conferenceButton needs at least one Call with
+ * OperationSetTelephonyConferencing,
+ */
+ if (!telephonyConferencing)
+ {
+ OperationSetTelephonyConferencing ostc
+ = pps.getOperationSet(
+ OperationSetTelephonyConferencing.class);
+
+ if (ostc != null)
+ telephonyConferencing = true;
+ }
+
+ if (!videoTelephony
+ || !videoTelephonyIsLocalVideoAllowed
+ || !videoTelephonyIsLocalVideoStreaming)
+ {
+ OperationSetVideoTelephony osvt
+ = pps.getOperationSet(OperationSetVideoTelephony.class);
+
+ if (osvt != null)
+ {
+ if (!videoTelephony)
+ videoTelephony = true;
+ if (!videoTelephonyIsLocalVideoAllowed
+ && osvt.isLocalVideoAllowed(call))
+ videoTelephonyIsLocalVideoAllowed = true;
+ if (!videoTelephonyIsLocalVideoStreaming
+ && osvt.isLocalVideoStreaming(call))
+ videoTelephonyIsLocalVideoStreaming = true;
+ }
+ }
+
+ if(!desktopSharing)
+ {
+ OperationSetDesktopStreaming osds
+ = pps.getOperationSet(
+ OperationSetDesktopStreaming.class);
+ if(osds != null)
+ {
+ desktopSharing = true;
+
+ if(videoTelephonyIsLocalVideoStreaming
+ && call instanceof MediaAwareCall
+ && ((MediaAwareCall<?,?,?>) call).getMediaUseCase()
+ == MediaUseCase.DESKTOP)
+ {
+ desktopSharingIsStreamed = true;
+ }
+ }
+ }
+
+ if (CallState.CALL_IN_PROGRESS != call.getCallState())
+ {
+ allCallsConnected = false;
+ }
+ }
+
+ if(conferenceButton != null)
+ conferenceButton.setEnabled(telephonyConferencing);
+
+ if(transferCallButton != null)
+ {
+ transferCallButton.setEnabled(advancedTelephony);
+ transferCallButton.setVisible(!callConference.isConferenceFocus());
+ }
+
+ /*
+ * The videoButton is a beast of its own kind because it depends not
+ * only on the state of the depicted telephony conference but also on
+ * the global application state.
+ */
+ if(videoButton != null)
+ {
+ videoButton.setEnabled(allCallsConnected && videoTelephony);
+ videoButton.setSelected(videoTelephonyIsLocalVideoAllowed);
+
+ /*
+ * Consequently, the showHideVideoButton which depends on videoButton
+ * has to be updated depending on the state of the videoButton as well.
+ */
+ if(showHideVideoButton != null)
+ {
+ showHideVideoButton.setEnabled(
+ videoButton.isEnabled()
+ && videoTelephonyIsLocalVideoAllowed);
+ showHideVideoButton.setSelected(
+ showHideVideoButton.isEnabled()
+ && uiVideoHandler.isLocalVideoVisible());
+ showHideVideoButton.setVisible(showHideVideoButton.isEnabled());
+ }
+ }
+
+ // The desktop sharing button depends on the operation set desktop
+ // sharing server.
+ if(desktopSharingButton != null)
+ {
+ desktopSharingButton.setEnabled(desktopSharing);
+ desktopSharingButton.setSelected(desktopSharingIsStreamed);
+ }
+
+ if (callPanel instanceof OneToOneCallPanel)
+ {
+ OneToOneCallPanel oneToOneCallPanel = (OneToOneCallPanel) callPanel;
+ if(desktopSharingIsStreamed)
+ oneToOneCallPanel.addDesktopSharingComponents();
+ else
+ oneToOneCallPanel.removeDesktopSharingComponents();
+ }
+ }
+
+ /**
+ * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current
+ * state of its model i.e. <tt>callConference</tt>. The update is performed
+ * in the AWT event dispatching thread.
+ */
+ private void doUpdateViewFromModelInEventDispatchThread()
+ {
+ /*
+ * If the telephony conference depicted by this instance has ended, do
+ * not update the user interface because it will be left blank. It is
+ * CallManager's responsibility to dispose of this CallPanel after its
+ * telephony conference has ended. Additionally, the various types of
+ * callPanel will usually require at least one CallPeer in order to not
+ * be blank. The absence of CallPeers usually indicates that a Call and,
+ * respectively, a telephony conference has ended. So it makes some
+ * sense to skip the update in such cases in order to try to not have
+ * the user interface blank.
+ */
+ if (callConference.isEnded()
+ || (callConference.getCallPeerCount() == 0))
+ {
+ /*
+ * However, the settingsPanel contains buttons which may still need
+ * to be disabled and/or hidden.
+ */
+ updateSettingsPanelInEventDispatchThread(true);
+ return;
+ }
+
+ boolean isConference = isConference();
+ boolean isVideo = CallManager.isVideoStreaming(callConference);
+ CallPeer callPeer = null;
+ boolean validateAndRepaint = false;
+
+ if (callPanel != null)
+ {
+ boolean removeCallPanel;
+
+ if (isConference)
+ {
+ if (callPanel instanceof BasicConferenceCallPanel)
+ {
+ if (isVideo)
+ {
+ removeCallPanel
+ = !(callPanel instanceof VideoConferenceCallPanel);
+ }
+ else
+ {
+ removeCallPanel
+ = (callPanel instanceof VideoConferenceCallPanel);
+ }
+ }
+ else
+ {
+ removeCallPanel = true;
+ }
+ }
+ else
+ {
+ if (callPanel instanceof OneToOneCallPanel)
+ {
+ if (callPeer == null)
+ {
+ List<CallPeer> callPeers
+ = callConference.getCallPeers();
+
+ if (!callPeers.isEmpty())
+ callPeer = callPeers.get(0);
+ }
+ removeCallPanel
+ = !((OneToOneCallPanel) callPanel).getCallPeer().equals(
+ callPeer);
+ }
+ else
+ {
+ if( (callPanel instanceof BasicConferenceCallPanel) &&
+ ((BasicConferenceCallPanel) callPanel)
+ .hasDelayedCallPeers())
+ {
+ removeCallPanel = false;
+ }
+ else
+ {
+ removeCallPanel = true;
+ }
+
+ }
+ }
+ if (removeCallPanel)
+ {
+ remove(callPanel);
+ validateAndRepaint = true;
+ try
+ {
+ ((CallRenderer) callPanel).dispose();
+ }
+ finally
+ {
+ callPanel = null;
+ }
+ }
+ }
+ if (callPanel == null)
+ {
+ if (isConference)
+ {
+ if (isVideo)
+ {
+ callPanel
+ = new VideoConferenceCallPanel(
+ this,
+ callConference,
+ uiVideoHandler);
+ }
+ else
+ {
+ callPanel
+ = new AudioConferenceCallPanel(this, callConference);
+ }
+
+ ((BasicConferenceCallPanel) callPanel)
+ .addPeerViewlListener(this);
+ }
+ else
+ {
+ if (callPeer == null)
+ {
+ List<CallPeer> callPeers = callConference.getCallPeers();
+
+ if (!callPeers.isEmpty())
+ callPeer = callPeers.get(0);
+ }
+ if (callPeer != null)
+ {
+ callPanel
+ = new OneToOneCallPanel(this, callPeer, uiVideoHandler);
+ }
+ }
+ if (callPanel != null)
+ {
+ add(callPanel, BorderLayout.CENTER);
+ validateAndRepaint = true;
+ }
+ }
+
+ try
+ {
+ /*
+ * The center of this view is occupied by callPanel and we have just
+ * updated it. The bottom of this view is dedicated to settingsPanel
+ * so we have to update it as well.
+ */
+ updateSettingsPanelInEventDispatchThread(false);
+ }
+ finally
+ {
+ /*
+ * It seems that AWT/Swing does not validate and/or repaint this
+ * Container (enough) and, consequently, its display may not update
+ * itself with an up-to-date drawing of the current callPanel.
+ */
+ if (validateAndRepaint)
+ {
+ if (isDisplayable())
+ {
+ validate();
+ repaint();
+ }
+ else
+ doLayout();
+ }
+ }
+ }
+
+ /**
+ * Attempts to give a specific <tt>Component</tt> a visible rectangle with a
+ * specific width and a specific height if possible and sane by resizing
+ * the <tt>Window</tt> which contains this instance.
+ *
+ * @param component the <tt>Component</tt> which requests a visible
+ * rectangle with the specified <tt>width</tt> and <tt>height</tt>
+ * @param width the width of the visible rectangle requested by the
+ * specified <tt>component</tt>
+ * @param height the height of the visible rectangle requested by the
+ * specified <tt>component</tt>
+ */
+ private void ensureSize(Component component, int width, int height)
+ {
+ CallContainer callContainer = getCallWindow();
+
+ if (callContainer != null)
+ callContainer.ensureSize(component, width, height);
+ }
+
+ /**
+ * Notifies interested listeners of a call title change.
+ */
+ private void fireTitleChangeEvent()
+ {
+ Iterator<CallTitleListener> listeners;
+
+ synchronized (titleListeners)
+ {
+ listeners = new Vector<CallTitleListener>(titleListeners).iterator();
+ }
+
+ while (listeners.hasNext())
+ {
+ listeners.next().callTitleChanged(this);
+ }
+ }
+
+ /**
+ * Returns the <tt>CallConference</tt> depicted by this <tt>CallPanel</tt>
+ *
+ * @return the <tt>CallConference</tt> depicted by this
+ * <tt>CallConference</tt>
+ */
+ public CallConference getCallConference()
+ {
+ return callConference;
+ }
+
+ /**
+ * Returns the initial call title. The call title could be then changed by
+ * call setCallTitle.
+ *
+ * @return the call title
+ */
+ public String getCallTitle()
+ {
+ return title;
+ }
+
+ /**
+ * Returns the parent call window.
+ *
+ * @return the parent call window
+ */
+ public CallContainer getCallWindow()
+ {
+ return callWindow;
+ }
+
+ /**
+ * Returns the currently used <tt>CallRenderer</tt>.
+ * @return the currently used <tt>CallRenderer</tt>
+ */
+ public CallRenderer getCurrentCallRenderer()
+ {
+ return (CallRenderer) callPanel;
+ }
+
+ /**
+ * Returns the <tt>DialpadDialog</tt> corresponding to this CallDialog.
+ *
+ * @return the <tt>DialpadDialog</tt> corresponding to this CallDialog.
+ */
+ private DialpadDialog getDialpadDialog()
+ {
+ return new DialpadDialog(dtmfHandler);
+ }
+
+ /**
+ * Finds the <tt>Contact</tt>s which are participating in the telephony
+ * conference depicted by this instance and which are capable of instant
+ * messaging i.e. support {@link OperationSetBasicTelephony}.
+ *
+ * @param limit the maximum number of <tt>Contact</tt>s to be found. Since
+ * it is expensive in terms of execution time (at very least) to find a
+ * <tt>Contact</tt> which stands for a <tt>CallPeer</tt> (and to query it
+ * whether it supports instant messaging), it is advised to limit the search
+ * as much as possible. For example, the <tt>chatButton</tt> is enabled
+ * and/or shown only when there is exactly one such <tt>Contact</tt> so it
+ * makes perfect sense to specify <tt>1</tt> as the <tt>limit</tt> in the
+ * case.
+ * @return a <tt>List</tt> of the <tt>Contact</tt>s which are participating
+ * in the telephony conference depicted by this instance and which are
+ * capable of instant messaging i.e. support
+ * <tt>OperationSetBasicTelephony</tt>
+ */
+ private List<Contact> getIMCapableCallPeers(int limit)
+ {
+ List<CallPeer> callPeers = callConference.getCallPeers();
+ List<Contact> contacts = new ArrayList<Contact>(callPeers.size());
+
+ /*
+ * Choose the CallPeers (or rather their associated Contacts) which are
+ * capable of basic instant messaging.
+ */
+ for (CallPeer callPeer : callPeers)
+ {
+ if (callPeer.getProtocolProvider().getOperationSet(
+ OperationSetBasicInstantMessaging.class)
+ != null)
+ {
+ /*
+ * CallPeer#getContact) is more expensive in terms of execution
+ * than ProtocolProviderService#getOperationSet(Class).
+ */
+ Contact contact = callPeer.getContact();
+
+ if (contact != null)
+ contacts.add(contact);
+ }
+ else
+ {
+ Contact contact = CallManager.getIMCapableCusaxContact(callPeer);
+ if (contact != null)
+ {
+ contacts.add(contact);
+ }
+ }
+
+ if (contacts.size() >= limit)
+ break;
+ }
+ return contacts;
+ }
+
+ /**
+ * Returns the minimum width needed to show buttons.
+ * Used to calculate the minimum size of the call dialog.
+ * @return the minimum width for the buttons.
+ */
+ public int getMinimumButtonWidth()
+ {
+ int numberOfButtons = countButtons(settingsPanel.getComponents());
+
+ if (numberOfButtons > 0)
+ {
+ // +1 cause we had and a hangup button
+ // *32, a button is 28 pixels width and give some border
+ return (numberOfButtons + 1) * 32;
+ }
+ else
+ return -1;
+ }
+
+ /**
+ * Initializes buttons order in the call tool bar.
+ */
+ private void initButtonIndexes()
+ {
+ if (dialButton != null)
+ dialButton.setIndex(0);
+ if (conferenceButton != null)
+ conferenceButton.setIndex(1);
+ if (holdButton != null)
+ holdButton.setIndex(2);
+ if (recordButton != null)
+ recordButton.setIndex(3);
+ if (mergeButton != null)
+ mergeButton.setIndex(4);
+ if (transferCallButton != null)
+ transferCallButton.setIndex(5);
+
+ localLevel.setIndex(6);
+ if (remoteLevel instanceof OrderedComponent)
+ ((OrderedComponent) remoteLevel).setIndex(7);
+
+ if (desktopSharingButton != null)
+ desktopSharingButton.setIndex(8);
+
+ if (fullScreenButton != null)
+ fullScreenButton.setIndex(10);
+
+ if (videoButton != null)
+ videoButton.setIndex(11);
+ if (showHideVideoButton != null)
+ showHideVideoButton.setIndex(12);
+ chatButton.setIndex(19);
+
+ if (infoButton != null)
+ infoButton.setIndex(20);
+
+ hangupButton.setIndex(100);
+ }
+
+ /**
+ * Initialize plug-in components already registered for this container.
+ */
+ private void initPluginComponents()
+ {
+ // Search for plug-in components registered through the OSGI
+ // BundleContext.
+
+ String osgiFilter
+ = "("
+ + net.java.sip.communicator.service.gui.Container.CONTAINER_ID
+ + "="
+ + net.java.sip.communicator.service.gui.Container
+ .CONTAINER_CALL_DIALOG.getID()
+ + ")";
+ ServiceReference[] serRefs = null;
+
+ try
+ {
+ serRefs
+ = GuiActivator.bundleContext.getServiceReferences(
+ PluginComponentFactory.class.getName(),
+ osgiFilter);
+ }
+ catch (InvalidSyntaxException ise)
+ {
+ logger.error("Could not obtain plugin reference.", ise);
+ }
+
+ if (serRefs != null)
+ {
+ for (ServiceReference serRef : serRefs)
+ {
+ PluginComponentFactory factory
+ = (PluginComponentFactory)
+ GuiActivator.bundleContext.getService(serRef);
+
+ PluginComponent component =
+ factory.getPluginComponentInstance(CallPanel.this);
+
+ component.setCurrentContact(
+ CallManager.getPeerMetaContact(
+ callConference.getCallPeers().get(0)));
+
+ settingsPanel.add((Component) component.getComponent());
+ }
+ }
+
+ GuiActivator.getUIService().addPluginComponentListener(this);
+ }
+
+ /**
+ * Initializes the user interface hierarchy of this <tt>CallPanel</tt> i.e.
+ * the AWT <tt>Component</tt>s which constitute the user interface to be
+ * displayed by this <tt>Component</tt>. Their state does not have to depict
+ * the current state of the model of this view because
+ * {@link #updateViewFromModel()} will be invoked before this view becomes
+ * visible. At the center of the user interface of this view is
+ * {@link #callPanel} but it is dynamically added and removed multiple times
+ * as part of the execution of the <tt>updateViewFromModel</tt> method so
+ * it is not dealt with here.
+ */
+ private void initializeUserInterfaceHierarchy()
+ {
+ /*
+ * The settingsPanel will contain the buttons. It is initialized before
+ * the buttons in case any of the buttons need it (which is hard to
+ * determine at the time of this writing).
+ */
+ settingsPanel = new CallToolBar(isFullScreen(), false);
+
+ /*
+ * TODO CallPanel depicts a whole CallConference which may have multiple
+ * Calls, new Calls may be added to the CallConference and existing
+ * Calls may be removed from the CallConference. For example, the
+ * buttons which accept a Call as an argument should be changed to take
+ * into account the whole CallConference.
+ */
+ Call aCall = callConference.getCalls().get(0);
+
+ chatButton
+ = new CallToolBarButton(
+ ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_WHITE),
+ CHAT_BUTTON,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CHAT"));
+
+ if(isButtonEnabled(HIDE_CONFERENCE_BUTON_PROP))
+ {
+ conferenceButton
+ = new CallToolBarButton(
+ ImageLoader.getImage(ImageLoader.ADD_TO_CALL_BUTTON),
+ CONFERENCE_BUTTON,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CREATE_CONFERENCE_CALL"));
+ }
+
+ if(isButtonEnabled(HIDE_DESKTOP_SHARING_BUTON_PROP))
+ {
+ desktopSharingButton = new DesktopSharingButton(aCall);
+ }
+
+ if(isButtonEnabled(HIDE_DIAL_BUTON_PROP))
+ {
+ dialButton
+ = new CallToolBarButton(
+ ImageLoader.getImage(ImageLoader.DIAL_BUTTON),
+ DIAL_BUTTON,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.DIALPAD"));
+ }
+
+ if(isButtonEnabled(HIDE_FULL_SCREEN_BUTON_PROP))
+ {
+ fullScreenButton = new FullScreenButton(this);
+ }
+
+ hangupButton = new HangupButton(this);
+
+ if(isButtonEnabled(HIDE_CALL_HOLD_BUTON_PROP))
+ {
+ holdButton = new HoldButton(aCall);
+ }
+
+ if(isButtonEnabled(HIDE_CALL_INFO_BUTON_PROP))
+ {
+ infoButton
+ = new CallToolBarButton(
+ ImageLoader.getImage(ImageLoader.CALL_INFO),
+ INFO_BUTTON,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.PRESS_FOR_CALL_INFO"));
+ }
+
+ if(isButtonEnabled(HIDE_CALL_MERGE_BUTON_PROP))
+ {
+ mergeButton
+ = new CallToolBarButton(
+ ImageLoader.getImage(ImageLoader.MERGE_CALL_BUTTON),
+ MERGE_BUTTON,
+ GuiActivator.getResources().getI18NString(
+ "service.gui.MERGE_TO_CALL"));
+
+ }
+
+ if(isButtonEnabled(HIDE_CALL_RECORD_BUTON_PROP))
+ {
+ recordButton = new RecordButton(aCall);
+ }
+
+ if(isButtonEnabled(HIDE_TOGGLE_VIDEO_BUTON_PROP))
+ {
+ showHideVideoButton = new ShowHideVideoButton(uiVideoHandler);
+ }
+
+ if(isButtonEnabled(HIDE_CALL_TRANSFER_BUTON_PROP))
+ {
+ transferCallButton = new TransferCallButton(aCall);
+ }
+
+ if(isButtonEnabled(HIDE_VIDEO_BUTON_PROP))
+ {
+ videoButton = new LocalVideoButton(aCall);
+ }
+
+ localLevel
+ = new InputVolumeControlButton(
+ aCall,
+ ImageLoader.MICROPHONE,
+ ImageLoader.MUTE_BUTTON,
+ true,
+ false);
+ remoteLevel
+ = new OutputVolumeControlButton(
+ callConference,
+ ImageLoader.VOLUME_CONTROL_BUTTON,
+ false,
+ true)
+ .getComponent();
+
+ /*
+ * Now that the buttons have been initialized, set their order indexes
+ * so that they get added in the correct order later on.
+ */
+ initButtonIndexes();
+
+ chatButton.addActionListener(this);
+ if (conferenceButton != null)
+ conferenceButton.addActionListener(this);
+ if (dialButton != null)
+ dialButton.addActionListener(this);
+ if (infoButton != null)
+ infoButton.addActionListener(this);
+ if (mergeButton != null)
+ mergeButton.addActionListener(this);
+
+ settingsPanel.add(chatButton);
+ if (conferenceButton != null)
+ settingsPanel.add(conferenceButton);
+ if (desktopSharingButton != null)
+ settingsPanel.add(desktopSharingButton);
+ if (dialButton != null)
+ settingsPanel.add(dialButton);
+ if (fullScreenButton != null)
+ settingsPanel.add(fullScreenButton);
+
+ settingsPanel.add(hangupButton);
+
+ if (holdButton != null)
+ settingsPanel.add(holdButton);
+ if (infoButton != null)
+ settingsPanel.add(infoButton);
+ if (mergeButton != null)
+ settingsPanel.add(mergeButton);
+ if (recordButton != null)
+ settingsPanel.add(recordButton);
+ if (showHideVideoButton != null)
+ settingsPanel.add(showHideVideoButton);
+ if (mergeButton != null)
+ settingsPanel.add(transferCallButton);
+ if (videoButton != null)
+ settingsPanel.add(videoButton);
+
+ // The bottom bar will contain the settingsPanel.
+ add(createBottomBar(), BorderLayout.SOUTH);
+ }
+
+ /**
+ * Tests a provided boolean property name, returning false if it should be
+ * hidden.
+ *
+ * Used in {@link #initializeUserInterfaceHierarchy()}
+ * @param buttonHidePropertyName the name of the boolean property to check.
+ * @return false if the button should be hidden, true otherwise.
+ *
+ */
+ private boolean isButtonEnabled(String buttonHidePropertyName)
+ {
+ return !GuiActivator.getConfigurationService().getBoolean(
+ buttonHidePropertyName,
+ false);
+ }
+
+ /**
+ * Returns <code>true</code> if the call timer has been started, otherwise
+ * returns <code>false</code>.
+ * @return <code>true</code> if the call timer has been started, otherwise
+ * returns <code>false</code>
+ */
+ public boolean isCallTimerStarted()
+ {
+ return isCallTimerStarted;
+ }
+
+ /**
+ * Checks if the contained call is a conference call.
+ *
+ * @return <code>true</code> if the contained <tt>Call</tt> is a conference
+ * call, otherwise - returns <code>false</code>.
+ */
+ boolean isConference()
+ {
+ // If we're the focus of the conference.
+ if (callConference.isConferenceFocus())
+ return true;
+
+ // If one of our peers is a conference focus, we're in a
+ // conference call.
+ List<CallPeer> callPeers = callConference.getCallPeers();
+
+ for (CallPeer callPeer : callPeers)
+ {
+ if (callPeer.isConferenceFocus())
+ return true;
+ }
+
+ // the call can have two peers at the same time and there is no one
+ // is conference focus. This is situation when someone has made an
+ // attended transfer and has transfered us. We have one call with two
+ // peers the one we are talking to and the one we have been transfered
+ // to. And the first one is been hanged up and so the call passes through
+ // conference call focus a moment and than go again to one to one call.
+ return callPeers.size() > 1;
+ }
+
+ /**
+ * Determines whether this view is displayed in full-screen or windowed
+ * mode.
+ *
+ * @return <tt>true</tt> if this view is displayed in full-screen mode or
+ * <tt>false</tt> for windowed mode
+ */
+ boolean isFullScreen()
+ {
+ return callWindow.isFullScreen();
+ }
+
+ /**
+ * Checks whether recording is currently enabled or not, state retrieved
+ * from call record button state.
+ *
+ * @return <tt>true</tt> if the recording is already started, <tt>false</tt>
+ * otherwise
+ */
+ public boolean isRecordingStarted()
+ {
+ if (recordButton == null)
+ return false;
+
+ return recordButton.isSelected();
+ }
+
+ /**
+ * Returns <tt>true</tt> if the show/hide video button is currently selected,
+ * <tt>false</tt> - otherwise.
+ *
+ * @return <tt>true</tt> if the show/hide video button is currently selected,
+ * <tt>false</tt> - otherwise
+ */
+ public boolean isShowHideVideoButtonSelected()
+ {
+ return showHideVideoButton.isSelected();
+ }
+
+ /**
+ * Reloads icons.
+ */
+ public void loadSkin()
+ {
+ if (dialButton != null)
+ {
+ dialButton.setBackgroundImage(
+ ImageLoader.getImage(ImageLoader.CALL_SETTING_BUTTON_BG));
+ dialButton.setIconImage(
+ ImageLoader.getImage(ImageLoader.DIAL_BUTTON));
+ }
+
+ if (conferenceButton != null)
+ {
+ conferenceButton.setBackgroundImage(
+ ImageLoader.getImage(ImageLoader.CALL_SETTING_BUTTON_BG));
+ conferenceButton.setIconImage(
+ ImageLoader.getImage(ImageLoader.ADD_TO_CALL_BUTTON));
+ }
+
+ if (hangupButton != null)
+ hangupButton.setBackgroundImage(
+ ImageLoader.getImage(ImageLoader.HANGUP_BUTTON_BG));
+ }
+
+ /**
+ * Notifies this instance about a specific <tt>VideoEvent</tt> which may
+ * warrant {@link #ensureSize(Component, int, int)} to be invoked in order
+ * to try to have the associated visual <tt>Component</tt> displaying video
+ * shown without scaling. The method will execute on the AWT event
+ * dispatching thread because it will be making its judgments based on the
+ * properties of AWT <tt>Component</tt>s.
+ *
+ * @param ev a <tt>VideoEvent</tt> which represents the cause of the
+ * notification and specifies the visual <tt>Component</tt> displaying video
+ * which may need an adjustment of a Frame's size in order to be displayed
+ * without scaling
+ */
+ private void maybeEnsureSize(final VideoEvent ev)
+ {
+ if (!SwingUtilities.isEventDispatchThread())
+ {
+ SwingUtilities.invokeLater(
+ new Runnable()
+ {
+ public void run()
+ {
+ maybeEnsureSize(ev);
+ }
+ });
+ return;
+ }
+
+ if (ev instanceof SizeChangeVideoEvent)
+ {
+ /*
+ * If a visual Component depicting video (streaming between the
+ * local peer/user and the remote peers) changes its size, try to
+ * adjust the size of the Frame which displays it so that it appears
+ * without scaling.
+ */
+ SizeChangeVideoEvent scev = (SizeChangeVideoEvent) ev;
+
+ ensureSize(
+ scev.getVisualComponent(),
+ scev.getWidth(), scev.getHeight());
+ }
+ else if (ev.getType() == VideoEvent.VIDEO_ADDED)
+ {
+ Component video = ev.getVisualComponent();
+
+ if ((video != null)
+ && UIVideoHandler2.isAncestor(this, video)
+ && video.isPreferredSizeSet())
+ {
+ Dimension prefSize = video.getPreferredSize();
+
+ if ((prefSize.height > 0) && (prefSize.width > 0))
+ {
+ Dimension size = video.getSize();
+
+ if ((prefSize.height > size.height)
+ || (prefSize.width > size.width))
+ {
+ ensureSize(
+ video,
+ prefSize.width, prefSize.height);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Invoked by {@link #callConferenceListener} to notify this instance about
+ * an <tt>EventObject</tt> related to the <tt>CallConference</tt> depicted
+ * by this <tt>CallPanel</tt>, the <tt>Call</tt>s participating in it,
+ * the <tt>CallPeer</tt>s associated with them, the
+ * <tt>ConferenceMember</tt>s participating in any telephony conferences
+ * organized by them, etc. In other words, notifies this instance about
+ * any change which may cause an update to be required so that this view
+ * i.e. <tt>CallPanel</tt> depicts the current state of its model i.e.
+ * {@link #callConference}.
+ *
+ * @param ev the <tt>EventObject</tt> this instance is being notified
+ * about.
+ */
+ private void onCallConferenceEventObject(EventObject ev)
+ {
+ /*
+ * The main task is to invoke updateViewFromModel() in order to make
+ * sure that this view depicts the current state of its model.
+ */
+
+ try
+ {
+ /*
+ * However, we seem to be keeping track of the duration of the call
+ * (i.e. the telephony conference) in the user interface. Stop the
+ * Timer which ticks the duration of the call as soon as the
+ * telephony conference depicted by this instance appears to have
+ * ended. The situation will very likely occur when a Call is
+ * removed from the telephony conference or a CallPeer is removed
+ * from a Call.
+ */
+ boolean tryStopCallTimer = false;
+
+ if (ev instanceof CallPeerEvent)
+ {
+ tryStopCallTimer
+ = (CallPeerEvent.CALL_PEER_REMOVED
+ == ((CallPeerEvent) ev).getEventID());
+ }
+ else if (ev instanceof PropertyChangeEvent)
+ {
+ PropertyChangeEvent pcev = (PropertyChangeEvent) ev;
+
+ tryStopCallTimer
+ = (CallConference.CALLS.equals(pcev.getPropertyName())
+ && (pcev.getOldValue() instanceof Call)
+ && (pcev.getNewValue() == null));
+ }
+ if (tryStopCallTimer
+ && (callConference.isEnded()
+ || callConference.getCallPeerCount() == 0))
+ {
+ stopCallTimer();
+ }
+ }
+ finally
+ {
+ updateViewFromModel();
+ }
+ }
+
+ /**
+ * Notifies this <tt>CallPanel</tt> about a specific <tt>CallEvent</tt>
+ * (received by <tt>CallManager</tt>). The source <tt>Call</tt> may or may
+ * not be participating in the telephony conference depicted by this
+ * instance but allows it to update any state which may depend on the
+ * <tt>Call</tt>s which are established application-wide.
+ *
+ * @param ev a <tt>CallEvent</tt> which specifies the <tt>Call</tt> which
+ * caused this instance to be notified and the exact type of the
+ * notification event
+ */
+ void onCallEvent(CallEvent ev)
+ {
+ updateMergeButtonState();
+ }
+
+ /**
+ * Adds/removes the <tt>Component</tt> of the <tt>PluginComponent</tt>
+ * specified by a <tt>PluginComponentEvent</tt> to/from
+ * {@link #settingsPanel} (if it is appropriate for this
+ * <tt>Container</tt>).
+ *
+ * @param ev a <tt>PluginComponentEvent</tt> which specifies the
+ * <tt>PluginComponent</tt> whose <tt>Component</tt> is to be added/removed
+ * to/from {@link #settingsPanel}
+ */
+ protected void onPluginComponentEvent(PluginComponentEvent ev)
+ {
+ PluginComponentFactory pc = ev.getPluginComponentFactory();
+
+ if (pc.getContainer().equals(
+ net.java.sip.communicator.service.gui.Container
+ .CONTAINER_CALL_DIALOG))
+ {
+ PluginComponent plugin =
+ pc.getPluginComponentInstance(CallPanel.this);
+ Component c = (Component)plugin.getComponent();
+ plugin.setCurrentContact(
+ CallManager.getPeerMetaContact(
+ callConference.getCallPeers().get(0)));
+
+ switch (ev.getEventID())
+ {
+ case PluginComponentEvent.PLUGIN_COMPONENT_ADDED:
+ settingsPanel.add(c);
+ break;
+ case PluginComponentEvent.PLUGIN_COMPONENT_REMOVED:
+ settingsPanel.remove(c);
+ break;
+ }
+
+ settingsPanel.revalidate();
+ settingsPanel.repaint();
+ }
+ }
+
+ /**
+ * Indicates that the peer panel was added.
+ *
+ * @param ev the event.
+ */
+ public void peerViewAdded(ConferencePeerViewEvent ev) {}
+
+ /**
+ * Indicates that the peer panel was removed.
+ *
+ * @param ev the event.
+ */
+ public void peerViewRemoved(ConferencePeerViewEvent ev)
+ {
+ updateViewFromModel();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Adds the <tt>Component</tt> of the <tt>PluginComponent</tt> specified by
+ * the <tt>PluginComponentEvent</tt> to {@link #settingsPanel} (if it is
+ * appropriate for this <tt>Container</tt>).
+ */
+ public void pluginComponentAdded(PluginComponentEvent ev)
+ {
+ onPluginComponentEvent(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Removes the <tt>Component</tt> of the <tt>PluginComponent</tt> specified
+ * by the <tt>PluginComponentEvent</tt> from {@link #settingsPanel} (if it
+ * is appropriate for this <tt>Container</tt>).
+ */
+ public void pluginComponentRemoved(PluginComponentEvent ev)
+ {
+ onPluginComponentEvent(ev);
+ }
+
+ /**
+ * Removes the given <tt>CallTitleListener</tt> to the list of listeners,
+ * notified for call title changes.
+ *
+ * @param l the <tt>CallTitleListener</tt> to remove
+ */
+ public void removeCallTitleListener(CallTitleListener l)
+ {
+ synchronized (titleListeners)
+ {
+ titleListeners.remove(l);
+ }
+ }
+
+ /**
+ * Remove remote video specific components.
+ */
+ public void removeRemoteVideoSpecificComponents()
+ {
+ if(resizeVideoButton != null)
+ settingsPanel.remove(resizeVideoButton);
+
+ settingsPanel.revalidate();
+ settingsPanel.repaint();
+ }
+
+ /**
+ * Sets the title of this dialog in accord with a specific time of start of
+ * the telephony call/conference depicted by this <tt>CallPanel</tt>.
+ *
+ * @param startTime the time in milliseconds at which the telephony
+ * call/conference depicted by this <tt>CallPanel</tt> is considered to have
+ * started
+ */
+ private void setCallTitle(long startTime)
+ {
+ StringBuilder title = new StringBuilder();
+
+ if (startTime != 0)
+ {
+ title.append(
+ GuiUtils.formatTime(
+ startTime,
+ System.currentTimeMillis()));
+ title.append(" | ");
+ }
+ else
+ title.append("00:00:00 | ");
+
+ List<CallPeer> callPeers = callConference.getCallPeers();
+
+ if ((callPeers.size() > 0)
+ && (GuiActivator.getUIService().getSingleWindowContainer()
+ != null))
+ {
+ title.append(callPeers.get(0).getDisplayName());
+ }
+ else
+ {
+ title.append(
+ GuiActivator.getResources().getI18NString(
+ "service.gui.CALL"));
+ }
+
+ this.title = title.toString();
+
+ fireTitleChangeEvent();
+ }
+
+ /**
+ * Sets the display of this view to full-screen or windowed mode.
+ *
+ * @param fullScreen <tt>true</tt> to display this view in full-screen mode
+ * or <tt>false</tt> for windowed mode
+ */
+ void setFullScreen(boolean fullScreen)
+ {
+ callWindow.setFullScreen(fullScreen);
+ }
+
+ /**
+ * Selects or unselects the video button in this call dialog.
+ *
+ * @param isSelected indicates if the video button should be selected or not
+ */
+ public void setVideoButtonSelected(boolean isSelected)
+ {
+ if (isSelected && !videoButton.isSelected())
+ videoButton.setSelected(true);
+ else if (!isSelected && videoButton.isSelected())
+ videoButton.setSelected(false);
+ }
+
+ /**
+ * Starts the timer that counts call duration.
+ */
+ public void startCallTimer()
+ {
+ callConferenceStartTime = System.currentTimeMillis();
+ callDurationTimer.start();
+ isCallTimerStarted = true;
+ }
+
+ /**
+ * Stops the timer that counts call duration.
+ */
+ public void stopCallTimer()
+ {
+ this.callDurationTimer.stop();
+ }
+
+ /**
+ * Notifies this instance that {@link #uiVideoHandler} has reported a change
+ * in the video-related information which may warrant an update of this view
+ * from its model.
+ *
+ * @param arg an <tt>Object</tt>, if any, which represents the cause that
+ * triggered the notification
+ */
+ private void uiVideoHandlerUpdate(Object arg)
+ {
+ /* The most important task is to update this view from its model. */
+
+ /*
+ * If a visual Component displaying video is reported to have been
+ * added/prepared/received, we may have to adjust the size of the Frame
+ * displaying this user interface so that the video appears without
+ * scaling.
+ */
+ /*
+ * XXX The following may be making judgments about the user interface
+ * out of the AWT event dispatching thread which is a prerequisite for
+ * unexpected behavior. Anyway, that's the only idea at the time of this
+ * writing.
+ */
+ VideoEvent maybeEnsureSize = null;
+
+ if (arg instanceof VideoEvent)
+ {
+ try
+ {
+ VideoEvent vev = (VideoEvent) arg;
+ int vevType = vev.getType();
+
+ if (vevType == VideoEvent.VIDEO_ADDED)
+ {
+ Component video = vev.getVisualComponent();
+
+ if ((video != null)
+ && !UIVideoHandler2.isAncestor(this, video))
+ {
+ maybeEnsureSize = vev;
+ }
+ }
+ else if (vevType == SizeChangeVideoEvent.VIDEO_SIZE_CHANGE)
+ {
+ /*
+ * If a visual Component depicting video (streaming between
+ * the local peer/user and the remote peers) changes its
+ * size, try to adjust the size of the Frame which displays
+ * it so that it appears without scaling.
+ */
+ maybeEnsureSize = vev;
+ }
+ }
+ catch (Throwable t)
+ {
+ if (t instanceof ThreadDeath)
+ throw (ThreadDeath) t;
+ else if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Failed to determine whether it is necessary to"
+ + " adjust a Frame's size in response to a"
+ + " VideoEvent.",
+ t);
+ }
+ }
+ }
+
+ updateViewFromModel();
+
+ if (maybeEnsureSize != null)
+ {
+ /*
+ * The method maybeEnsureSize will execute on the AWT event
+ * dispatching thread.
+ */
+ try
+ {
+ maybeEnsureSize(maybeEnsureSize);
+ }
+ catch (Throwable t)
+ {
+ if (t instanceof ThreadDeath)
+ throw (ThreadDeath) t;
+ else
+ {
+ logger.error(
+ "Failed to adjust a Frame's size"
+ + " in response to a VideoEvent.",
+ t);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the state of the general hold button. The hold button is selected
+ * only if all call peers are locally or mutually on hold at the same time.
+ * In all other cases the hold button is unselected.
+ */
+ public void updateHoldButtonState()
+ {
+ // If the hold button has been disabled by its configuration property we
+ // have nothing more to do here.
+ if (holdButton == null)
+ return;
+
+ if(!SwingUtilities.isEventDispatchThread())
+ {
+ SwingUtilities.invokeLater(new Runnable()
+ {
+ public void run()
+ {
+ updateHoldButtonState();
+ }
+ });
+
+ return;
+ }
+
+ List<CallPeer> peers = callConference.getCallPeers();
+ boolean areAllPeersLocallyOnHold;
+
+ if (peers.isEmpty())
+ {
+ /*
+ * It feels natural to not have the holdButton selected when there
+ * are no peers.
+ */
+ areAllPeersLocallyOnHold = false;
+ }
+ else
+ {
+ areAllPeersLocallyOnHold = true;
+ for (CallPeer peer : callConference.getCallPeers())
+ {
+ CallPeerState state = peer.getState();
+
+ // If we have clicked the hold button in a full screen mode
+ // we need to update the state of the call dialog hold button.
+ if (!state.equals(CallPeerState.ON_HOLD_LOCALLY)
+ && !state.equals(CallPeerState.ON_HOLD_MUTUALLY))
+ {
+ areAllPeersLocallyOnHold = false;
+ break;
+ }
+ }
+ }
+
+ // If we have clicked the hold button in a full screen mode or selected
+ // hold of the peer menu in a conference call we need to update the
+ // state of the call dialog hold button.
+ holdButton.setSelected(areAllPeersLocallyOnHold);
+ }
+
+ /**
+ * Updates the <tt>visible</tt> state/property of {@link #mergeButton} if
+ * the merge button is present.
+ */
+ private void updateMergeButtonState()
+ {
+ // If the merge button isn't present, for example if it's hidden by
+ // its configuration property we have nothing more to do here.
+ if (mergeButton == null)
+ return;
+
+ List<CallConference> conferences = new ArrayList<CallConference>();
+ int cpt = 0;
+
+ for (Call call : CallManager.getInProgressCalls())
+ {
+ CallConference conference = call.getConference();
+
+ if (conference == null)
+ cpt++;
+ else if (!conferences.contains(conference))
+ {
+ conferences.add(conference);
+ cpt++;
+ }
+ else
+ continue;
+
+ if (cpt > 1)
+ break;
+ }
+
+ mergeButton.setVisible(cpt > 1);
+ }
+
+ /**
+ * Updates {@link #settingsPanel} from the model of this view. The update is
+ * performed in the AWT event dispatching thread.
+ * <p>
+ * The center of this view is occupied by {@link #callPanel}, the bottom of
+ * this view is dedicated to <tt>settingsPanel</tt>. The method
+ * {@link #updateViewFromModelInEventDispatchThread()} updates
+ * <tt>callPanel</tt> from the model of this view and then invokes the
+ * method <tt>updateSettingsPanelInEventDispatchThread()</tt>. Thus this
+ * whole view is updated so that it depicts the current state of its model.
+ * </p>
+ *
+ * @param callConferenceIsEnded <tt>true</tt> if the method
+ * <tt>updateViewFromModelInEventDispatchThread()</tt> considers the
+ * {@link #callConference} ended; otherwise, <tt>false</tt>. When the
+ * <tt>callConference</tt> is considered ended, the <tt>callPanel</tt>
+ * instance will not be switched to a specific type (one-to-one, audio-only,
+ * or audio/video) because, otherwise, the switch will leave it
+ * <tt>null</tt> and this view will remain blank. In such a case,
+ * <tt>settingsPanel</tt> may wish to do pretty much the same but disable
+ * and/or hide the buttons it contains.
+ */
+ private void updateSettingsPanelInEventDispatchThread(
+ boolean callConferenceIsEnded)
+ {
+ /*
+ * XXX The method directly delegates to the method
+ * doUpdateSettingsPanelInEventDispatchThread at the time of this
+ * writing which may be considered a waste. But in the fashion of the
+ * method updateViewFromModelInEventDispatchThread we have made it easy
+ * to add code before and/or after the invocation of the delegate.
+ */
+ doUpdateSettingsPanelInEventDispatchThread(callConferenceIsEnded);
+ }
+
+ /**
+ * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current
+ * state of its model i.e. <tt>callConference</tt>.
+ */
+ private void updateViewFromModel()
+ {
+ /*
+ * We receive events/notifications from various threads and we respond
+ * to them in the AWT event dispatching thread. It is possible to first
+ * schedule an event to be brought to the AWT event dispatching thread,
+ * then to have #dispose() invoked on this instance and, finally, to
+ * receive the scheduled event in the AWT event dispatching thread. In
+ * such a case, this disposed instance should not respond to the event.
+ */
+ if (!disposed)
+ {
+ if (SwingUtilities.isEventDispatchThread())
+ updateViewFromModelInEventDispatchThread();
+ else
+ {
+ SwingUtilities.invokeLater(
+ updateViewFromModelInEventDispatchThread);
+ }
+ }
+ }
+
+ /**
+ * Updates this view i.e. <tt>CallPanel</tt> so that it depicts the current
+ * state of its model i.e. <tt>callConference</tt>. The update is performed
+ * in the AWT event dispatching thread.
+ */
+ private void updateViewFromModelInEventDispatchThread()
+ {
+ /*
+ * We receive events/notifications from various threads and we respond
+ * to them in the AWT event dispatching thread. It is possible to first
+ * schedule an event to be brought to the AWT event dispatching thread,
+ * then to have #dispose() invoked on this instance and, finally, to
+ * receive the scheduled event in the AWT event dispatching thread. In
+ * such a case, this disposed instance should not respond to the event.
+ */
+ if (disposed)
+ return;
+
+ /*
+ * We may add, remove, show, and hide various Components of the user
+ * interface hierarchy of this instance bellow. Consequently, this view
+ * may become larger in width and/or height than its current Frame has
+ * dedicated to it. Try to detect such cases and attempt to adjust the
+ * Frame's size accordingly.
+ */
+ Dimension oldPrefSize = getPreferredSize();
+
+ doUpdateViewFromModelInEventDispatchThread();
+
+ /*
+ * We may have added, removed, shown, and hidden various Components of
+ * the user interface hierarchy of this instance above. Consequently,
+ * this view may have become larger in width and/or height than its
+ * current Frame has dedicated to it. Try to detect such cases and
+ * attempt to adjust the Frame's size accordingly.
+ */
+ Dimension newPrefSize = getPreferredSize();
+
+ if ((newPrefSize != null)
+ && ((newPrefSize.height > getHeight())
+ || (newPrefSize.width > getWidth())))
+ {
+ int oldPrefHeight, oldPrefWidth;
+
+ if (oldPrefSize == null)
+ {
+ oldPrefHeight = 0;
+ oldPrefWidth = 0;
+ }
+ else
+ {
+ oldPrefHeight = oldPrefSize.height;
+ oldPrefWidth = oldPrefSize.width;
+ }
+ if ((newPrefSize.height != oldPrefHeight)
+ || (newPrefSize.width != oldPrefWidth))
+ {
+ ensureSize(
+ this,
+ newPrefSize.width, newPrefSize.height);
+ }
+ }
+ }
+
+ /**
+ * Listens for contact status changes and updates the image of the
+ * chat message button.
+ * @param evt the ContactPresenceStatusChangeEvent describing the status
+ */
+ @Override
+ public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt)
+ {
+ Contact contact = getIMCapableCallPeers(1).get(0);
+
+ if(contact != null && contact.equals(evt.getSourceContact()))
+ {
+ chatButton.setIconImage(
+ Constants.getMessageStatusIcon(contact.getPresenceStatus()));
+ chatButton.repaint();
+ }
+ }
+
+ /**
+ * Implements the listener which listens to events fired by the
+ * <tt>CallConference</tt> depicted by this instance, the <tt>Call</tt>s
+ * participating in that telephony conference, the <tt>CallPeer</tt>s
+ * associated with those <tt>Call</tt>s and the <tt>ConferenceMember</tt>s
+ * participating in the telephony conferences organized by those
+ * <tt>CallPeer</tt>s. Updates this view i.e. CallPanel so that it depicts
+ * the current state of its model i.e. {@link #callConference}.
+ */
+ private class CallConferenceListener
+ extends CallPeerConferenceAdapter
+ implements CallChangeListener,
+ PropertyChangeListener
+ {
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #onCallPeerEvent(CallPeerEvent)} because the
+ * <tt>CallPeerEvent</tt> allows distinguishing whether a
+ * <tt>CallPeer</tt> was added or removed by examining its
+ * <tt>eventID</tt>.
+ */
+ public void callPeerAdded(CallPeerEvent ev)
+ {
+ onCallPeerEvent(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #onCallPeerEvent(CallPeerEvent)} because the
+ * <tt>CallPeerEvent</tt> allows distinguishing whether a
+ * <tt>CallPeer</tt> was added or removed by examining its
+ * <tt>eventID</tt>.
+ */
+ public void callPeerRemoved(CallPeerEvent ev)
+ {
+ onCallPeerEvent(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #onEventObject(EventObject)}.
+ */
+ public void callStateChanged(CallChangeEvent ev)
+ {
+ onEventObject(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #onEventObject(EventObject)}.
+ */
+ @Override
+ protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
+ {
+ onEventObject(ev);
+ }
+
+ /**
+ * Notifies this <tt>CallChangeListener</tt> about a specific
+ * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was added to or
+ * removed from a <tt>Call</tt>. Invokes
+ * {@link #onEventObject(EventObject)}.
+ *
+ * @param ev the <tt>CallPeerEvent</tt> to notify this
+ * <tt>CallChangeListener</tt> about i.e. which specifies the
+ * <tt>CallPeer</tt> which was added/removed and the <tt>Call</tt>
+ * to/from which it was added/removed
+ */
+ private void onCallPeerEvent(CallPeerEvent ev)
+ {
+ onEventObject(ev);
+ }
+
+ /**
+ * Invoked by the various listener method implementations provided by
+ * this <tt>CallConferenceListener</tt> to notify this instance about an
+ * <tt>EventObject</tt> related to the <tt>CallConference</tt> depicted
+ * by this <tt>CallPanel</tt>, the <tt>Call</tt>s participating in it,
+ * the <tt>CallPeer</tt>s associated with them, the
+ * <tt>ConferenceMember</tt>s participating in any telephony conferences
+ * organized by them, etc. In other words, notifies this instance about
+ * any change which may cause an update to be required so that this view
+ * i.e. <tt>CallPanel</tt> depicts the current state of its model i.e.
+ * {@link CallPanel#callConference}.
+ *
+ * @param ev the <tt>EventObject</tt> this instance is being notified
+ * about.
+ */
+ private void onEventObject(EventObject ev)
+ {
+ onCallConferenceEventObject(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #onEventObject(EventObject)}.
+ */
+ public void propertyChange(PropertyChangeEvent ev)
+ {
+ String propertyName = ev.getPropertyName();
+
+ /*
+ * If a Call is added to or removed from the CallConference depicted
+ * by this CallPanel, an update of the view from its model will most
+ * likely be required.
+ */
+ if (propertyName.equals(CallConference.CALLS))
+ {
+ onEventObject(ev);
+ }
+ else if (propertyName.equals(CallContainer.PROP_FULL_SCREEN))
+ {
+ if (ev.getSource().equals(callWindow.getFrame()))
+ {
+ try
+ {
+ /*
+ * We'll turn the switching between full-screen and
+ * windowed mode into a model state because a
+ * significant part of this view changes upon such a
+ * switch.
+ */
+ onEventObject(ev);
+ }
+ finally
+ {
+ callWindowPropertyChange(ev);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallTransferHandler.java b/src/net/java/sip/communicator/impl/gui/main/call/CallTransferHandler.java index 01f316c..68e55b8 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallTransferHandler.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallTransferHandler.java @@ -1,279 +1,279 @@ -/* - * 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.impl.gui.main.call; - -import java.awt.datatransfer.*; -import java.awt.im.*; -import java.io.*; -import java.util.*; - -import javax.swing.*; - -import org.jitsi.service.resources.*; - -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.impl.gui.main.contactlist.*; -import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.service.gui.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; - -/** - * A <tt>TransferHandler</tt> that handles dropping of <tt>UIContact</tt>s or - * <tt>String</tt> addresses on a <tt>CallConference</tt>. Dropping such data on - * the <tt>CallDialog</tt> will turn a one-to-one <tt>Call</tt> into a telephony - * conference. - * - * @author Yana Stamcheva - */ -public class CallTransferHandler - extends ExtendedTransferHandler -{ - /** - * Serial version UID. - */ - private static final long serialVersionUID = 0L; - - /** - * The data flavor used when transferring <tt>UIContact</tt>s. - */ - protected static final DataFlavor uiContactDataFlavor - = new DataFlavor(UIContact.class, "UIContact"); - - /** - * The logger. - */ - private static final Logger logger - = Logger.getLogger(CallTransferHandler.class); - - /** - * The <tt>CallConference</tt> into which the dropped callees are to be - * invited. - */ - private final CallConference callConference; - - /** - * Initializes a new <tt>CallTransferHandler</tt> instance which is to - * invite dropped callees to a telephony conference specified by a specific - * <tt>Call</tt> which participates in it. - * - * @param call the <tt>Call</tt> which specifies the telephony conference to - * which dropped callees are to be invited - */ - public CallTransferHandler(Call call) - { - this(call.getConference()); - } - - /** - * Initializes a new <tt>CallTransferHandler</tt> instance which is to - * invite dropped callees to a specific <tt>CallConference</tt>. - * - * @param callConference the <tt>CallConference</tt> to which dropped - * callees are to be invited - */ - public CallTransferHandler(CallConference callConference) - { - this.callConference = callConference; - } - - /** - * Indicates whether a component will accept an import of the given - * set of data flavors prior to actually attempting to import it. We return - * <tt>true</tt> to indicate that the transfer with at least one of the - * given flavors would work and <tt>false</tt> to reject the transfer. - * <p> - * @param comp component - * @param flavor the data formats available - * @return true if the data can be inserted into the component, false - * otherwise - * @throws NullPointerException if <code>support</code> is {@code null} - */ - @Override - public boolean canImport(JComponent comp, DataFlavor[] flavor) - { - for (DataFlavor f : flavor) - { - if (f.equals(DataFlavor.stringFlavor) - || f.equals(uiContactDataFlavor)) - { - return (comp instanceof JPanel); - } - } - return false; - } - - /** - * Handles transfers to the chat panel from the clip board or a - * DND drop operation. The <tt>Transferable</tt> parameter contains the - * data that needs to be imported. - * <p> - * @param comp the component to receive the transfer; - * @param t the data to import - * @return true if the data was inserted into the component and false - * otherwise - */ - @Override - public boolean importData(JComponent comp, Transferable t) - { - String callee = null; - ProtocolProviderService provider = null; - - if (t.isDataFlavorSupported(uiContactDataFlavor)) - { - Object o = null; - - try - { - o = t.getTransferData(uiContactDataFlavor); - } - catch (UnsupportedFlavorException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to drop meta contact.", e); - } - catch (IOException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to drop meta contact.", e); - } - - if (o instanceof ContactNode) - { - UIContact uiContact = ((ContactNode) o).getContactDescriptor(); - Iterator<UIContactDetail> contactDetails - = uiContact - .getContactDetailsForOperationSet( - OperationSetBasicTelephony.class) - .iterator(); - - while (contactDetails.hasNext()) - { - UIContactDetail detail = contactDetails.next(); - ProtocolProviderService detailProvider - = detail.getPreferredProtocolProvider( - OperationSetBasicTelephony.class); - - if (detailProvider != null) - { - /* - * Currently for videobridge conferences we only support - * adding contacts via the account with the videobridge - */ - if (callConference.isJitsiVideoBridge()) - { - for (Call call : callConference.getCalls()) - { - if (detailProvider == call.getProtocolProvider()) - { - callee = detail.getAddress(); - provider = detailProvider; - break; - } - } - } - else - { - callee = detail.getAddress(); - provider = detailProvider; - break; - } - } - } - - if (callee == null) - { - /* - * It turns out that the error message to be reported would - * like to display information about the account which could - * not add the dropped callee to the telephony conference. - * Unfortunately, a telephony conference may have multiple - * accounts involved. Anyway, choose the first account - * involved in the telephony conference. - */ - ProtocolProviderService callProvider - = callConference.getCalls().get(0) - .getProtocolProvider(); - - ResourceManagementService resources - = GuiActivator.getResources(); - AccountID accountID = callProvider.getAccountID(); - - new ErrorDialog(null, - resources.getI18NString("service.gui.ERROR"), - resources.getI18NString( - "service.gui.CALL_NOT_SUPPORTING_PARTICIPANT", - new String[] - { - accountID.getService(), - accountID.getUserID(), - uiContact.getDisplayName() - })) - .showDialog(); - } - } - } - else if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) - { - InputContext inputContext = comp.getInputContext(); - - if (inputContext != null) - inputContext.endComposition(); - - try - { - BufferedReader reader - = new BufferedReader( - DataFlavor.stringFlavor.getReaderForText(t)); - - try - { - String line; - StringBuilder calleeBuilder = new StringBuilder(); - - while ((line = reader.readLine()) != null) - calleeBuilder.append(line); - - callee = calleeBuilder.toString(); - /* - * The value of the local variable provider will be null - * because we have a String only and hence we have no - * associated ProtocolProviderService. - * CallManager.inviteToConferenceCall will accept it. - */ - } - finally - { - reader.close(); - } - } - catch (UnsupportedFlavorException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to drop string.", e); - } - catch (IOException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to drop string.", e); - } - } - - if (callee == null) - return false; - else - { - Map<ProtocolProviderService, List<String>> callees - = new HashMap<ProtocolProviderService, List<String>>(); - - callees.put(provider, Arrays.asList(callee)); - CallManager.inviteToConferenceCall(callees, callConference); - - return true; - } - } -} +/*
+ * 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.impl.gui.main.call;
+
+import java.awt.datatransfer.*;
+import java.awt.im.*;
+import java.io.*;
+import java.util.*;
+
+import javax.swing.*;
+
+import org.jitsi.service.resources.*;
+
+import net.java.sip.communicator.impl.gui.*;
+import net.java.sip.communicator.impl.gui.main.contactlist.*;
+import net.java.sip.communicator.plugin.desktoputil.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * A <tt>TransferHandler</tt> that handles dropping of <tt>UIContact</tt>s or
+ * <tt>String</tt> addresses on a <tt>CallConference</tt>. Dropping such data on
+ * the <tt>CallDialog</tt> will turn a one-to-one <tt>Call</tt> into a telephony
+ * conference.
+ *
+ * @author Yana Stamcheva
+ */
+public class CallTransferHandler
+ extends ExtendedTransferHandler
+{
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * The data flavor used when transferring <tt>UIContact</tt>s.
+ */
+ protected static final DataFlavor uiContactDataFlavor
+ = new DataFlavor(UIContact.class, "UIContact");
+
+ /**
+ * The logger.
+ */
+ private static final Logger logger
+ = Logger.getLogger(CallTransferHandler.class);
+
+ /**
+ * The <tt>CallConference</tt> into which the dropped callees are to be
+ * invited.
+ */
+ private final CallConference callConference;
+
+ /**
+ * Initializes a new <tt>CallTransferHandler</tt> instance which is to
+ * invite dropped callees to a telephony conference specified by a specific
+ * <tt>Call</tt> which participates in it.
+ *
+ * @param call the <tt>Call</tt> which specifies the telephony conference to
+ * which dropped callees are to be invited
+ */
+ public CallTransferHandler(Call call)
+ {
+ this(call.getConference());
+ }
+
+ /**
+ * Initializes a new <tt>CallTransferHandler</tt> instance which is to
+ * invite dropped callees to a specific <tt>CallConference</tt>.
+ *
+ * @param callConference the <tt>CallConference</tt> to which dropped
+ * callees are to be invited
+ */
+ public CallTransferHandler(CallConference callConference)
+ {
+ this.callConference = callConference;
+ }
+
+ /**
+ * Indicates whether a component will accept an import of the given
+ * set of data flavors prior to actually attempting to import it. We return
+ * <tt>true</tt> to indicate that the transfer with at least one of the
+ * given flavors would work and <tt>false</tt> to reject the transfer.
+ * <p>
+ * @param comp component
+ * @param flavor the data formats available
+ * @return true if the data can be inserted into the component, false
+ * otherwise
+ * @throws NullPointerException if <code>support</code> is {@code null}
+ */
+ @Override
+ public boolean canImport(JComponent comp, DataFlavor[] flavor)
+ {
+ for (DataFlavor f : flavor)
+ {
+ if (f.equals(DataFlavor.stringFlavor)
+ || f.equals(uiContactDataFlavor))
+ {
+ return (comp instanceof JPanel);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handles transfers to the chat panel from the clip board or a
+ * DND drop operation. The <tt>Transferable</tt> parameter contains the
+ * data that needs to be imported.
+ * <p>
+ * @param comp the component to receive the transfer;
+ * @param t the data to import
+ * @return true if the data was inserted into the component and false
+ * otherwise
+ */
+ @Override
+ public boolean importData(JComponent comp, Transferable t)
+ {
+ String callee = null;
+ ProtocolProviderService provider = null;
+
+ if (t.isDataFlavorSupported(uiContactDataFlavor))
+ {
+ Object o = null;
+
+ try
+ {
+ o = t.getTransferData(uiContactDataFlavor);
+ }
+ catch (UnsupportedFlavorException e)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Failed to drop meta contact.", e);
+ }
+ catch (IOException e)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Failed to drop meta contact.", e);
+ }
+
+ if (o instanceof ContactNode)
+ {
+ UIContact uiContact = ((ContactNode) o).getContactDescriptor();
+ Iterator<UIContactDetail> contactDetails
+ = uiContact
+ .getContactDetailsForOperationSet(
+ OperationSetBasicTelephony.class)
+ .iterator();
+
+ while (contactDetails.hasNext())
+ {
+ UIContactDetail detail = contactDetails.next();
+ ProtocolProviderService detailProvider
+ = detail.getPreferredProtocolProvider(
+ OperationSetBasicTelephony.class);
+
+ if (detailProvider != null)
+ {
+ /*
+ * Currently for videobridge conferences we only support
+ * adding contacts via the account with the videobridge
+ */
+ if (callConference.isJitsiVideobridge())
+ {
+ for (Call call : callConference.getCalls())
+ {
+ if (detailProvider == call.getProtocolProvider())
+ {
+ callee = detail.getAddress();
+ provider = detailProvider;
+ break;
+ }
+ }
+ }
+ else
+ {
+ callee = detail.getAddress();
+ provider = detailProvider;
+ break;
+ }
+ }
+ }
+
+ if (callee == null)
+ {
+ /*
+ * It turns out that the error message to be reported would
+ * like to display information about the account which could
+ * not add the dropped callee to the telephony conference.
+ * Unfortunately, a telephony conference may have multiple
+ * accounts involved. Anyway, choose the first account
+ * involved in the telephony conference.
+ */
+ ProtocolProviderService callProvider
+ = callConference.getCalls().get(0)
+ .getProtocolProvider();
+
+ ResourceManagementService resources
+ = GuiActivator.getResources();
+ AccountID accountID = callProvider.getAccountID();
+
+ new ErrorDialog(null,
+ resources.getI18NString("service.gui.ERROR"),
+ resources.getI18NString(
+ "service.gui.CALL_NOT_SUPPORTING_PARTICIPANT",
+ new String[]
+ {
+ accountID.getService(),
+ accountID.getUserID(),
+ uiContact.getDisplayName()
+ }))
+ .showDialog();
+ }
+ }
+ }
+ else if (t.isDataFlavorSupported(DataFlavor.stringFlavor))
+ {
+ InputContext inputContext = comp.getInputContext();
+
+ if (inputContext != null)
+ inputContext.endComposition();
+
+ try
+ {
+ BufferedReader reader
+ = new BufferedReader(
+ DataFlavor.stringFlavor.getReaderForText(t));
+
+ try
+ {
+ String line;
+ StringBuilder calleeBuilder = new StringBuilder();
+
+ while ((line = reader.readLine()) != null)
+ calleeBuilder.append(line);
+
+ callee = calleeBuilder.toString();
+ /*
+ * The value of the local variable provider will be null
+ * because we have a String only and hence we have no
+ * associated ProtocolProviderService.
+ * CallManager.inviteToConferenceCall will accept it.
+ */
+ }
+ finally
+ {
+ reader.close();
+ }
+ }
+ catch (UnsupportedFlavorException e)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Failed to drop string.", e);
+ }
+ catch (IOException e)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Failed to drop string.", e);
+ }
+ }
+
+ if (callee == null)
+ return false;
+ else
+ {
+ Map<ProtocolProviderService, List<String>> callees
+ = new HashMap<ProtocolProviderService, List<String>>();
+
+ callees.put(provider, Arrays.asList(callee));
+ CallManager.inviteToConferenceCall(callees, callConference);
+
+ return true;
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferenceInviteDialog.java b/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferenceInviteDialog.java index a79ae91..82cfdde 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferenceInviteDialog.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferenceInviteDialog.java @@ -1,573 +1,573 @@ -/* - * 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.impl.gui.main.call.conference; - -import java.awt.*; -import java.awt.event.*; -import java.util.*; -import java.util.List; - -import javax.swing.*; - -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.impl.gui.main.call.*; -import net.java.sip.communicator.impl.gui.main.contactlist.contactsource.*; -import net.java.sip.communicator.impl.gui.utils.*; -import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.service.contactsource.*; -import net.java.sip.communicator.service.gui.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; - -/** - * The invite dialog is the one shown when the user clicks on the conference - * button in the chat toolbar. - * - * @author Yana Stamcheva - * @author Lyubomir Marinov - */ -public class ConferenceInviteDialog - extends InviteDialog -{ - /** - * Serial version UID. - */ - private static final long serialVersionUID = 0L; - - /** - * The account selector box. - */ - private final JComboBox accountSelectorBox = new JComboBox(); - - /** - * The last selected account. - */ - private Object lastSelectedAccount; - - /** - * The telephony conference into which this instance is to invite - * participants. - */ - private final CallConference conference; - - /** - * The current provider contact source. - */ - private ContactSourceService currentProviderContactSource; - - /** - * The current string contact source. - */ - private ContactSourceService currentStringContactSource; - - /** - * The previously selected protocol provider, with which this dialog has - * been instantiated. - */ - private ProtocolProviderService preselectedProtocolProvider; - - /** - * Indicates if this conference invite dialog is associated with a video - * bridge invite. - */ - private final boolean isVideoBridge; - - /** - * Initializes a new <tt>ConferenceInviteDialog</tt> instance which is to - * invite contacts/participants in a specific telephony conference. - * - * @param conference the telephony conference in which the new instance is - * to invite contacts/participants - */ - public ConferenceInviteDialog( - CallConference conference, - ProtocolProviderService preselectedProvider, - List<ProtocolProviderService> protocolProviders, - final boolean isVideoBridge) - { - // Set the correct dialog title depending if we're going to create a - // video bridge conference call - super((isVideoBridge - ? GuiActivator.getResources() - .getI18NString("service.gui.INVITE_CONTACT_TO_VIDEO_BRIDGE") - : GuiActivator.getResources() - .getI18NString("service.gui.INVITE_CONTACT_TO_CALL")), - false); - - this.conference = conference; - this.preselectedProtocolProvider = preselectedProvider; - this.isVideoBridge = isVideoBridge; - - if (preselectedProtocolProvider == null) - initAccountSelectorPanel(protocolProviders); - - // init the list, as we check whether features are supported - // it may take some time if we have too much contacts - SwingUtilities.invokeLater(new Runnable() - { - public void run() - { - initContactSources(); - - // Initialize the list of contacts to select from. - if (preselectedProtocolProvider != null) - initContactListData(preselectedProtocolProvider); - else - initContactListData( - (ProtocolProviderService) accountSelectorBox - .getSelectedItem()); - } - }); - - this.addInviteButtonListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - Collection<UIContact> selectedContacts - = destContactList.getContacts(null); - - if (selectedContacts != null && selectedContacts.size() > 0) - { - if (preselectedProtocolProvider == null) - preselectedProtocolProvider - = (ProtocolProviderService) accountSelectorBox - .getSelectedItem(); - - if (isVideoBridge) - inviteVideoBridgeContacts( preselectedProtocolProvider, - selectedContacts); - else - inviteContacts(selectedContacts); - - // Store the last used account in order to pre-select it - // next time. - ConfigurationUtils.setLastCallConferenceProvider( - preselectedProtocolProvider); - - dispose(); - } - else - { - // TODO: The underlying invite dialog should show a message - // to the user that she should select at least two contacts - // in order to create a conference. - } - } - }); - - this.addCancelButtonListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - dispose(); - } - }); - } - - /** - * Constructs the <tt>ConferenceInviteDialog</tt>. - */ - public ConferenceInviteDialog() - { - this(null, null, null, false); - } - - /** - * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying an - * already created conference. To use when inviting contacts to an existing - * conference is needed. - * - * @param conference the existing <tt>CallConference</tt> - */ - public ConferenceInviteDialog(CallConference conference) - { - this(conference, null, null, false); - } - - /** - * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying an - * already created conference. To use when inviting contacts to an existing - * conference is needed. - * - * @param conference the existing <tt>CallConference</tt> - */ - public ConferenceInviteDialog( - CallConference conference, - ProtocolProviderService preselectedProtocolProvider, - boolean isVideoBridge) - { - this(conference, preselectedProtocolProvider, null, isVideoBridge); - } - - /** - * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying a - * preselected protocol provider to be used and if this is an invite for - * a video bridge conference. - * - * @param selectedConfProvider the preselected protocol provider - * @param isVideoBridge indicates if this dialog should create a conference - * through a video bridge - */ - public ConferenceInviteDialog( - List<ProtocolProviderService> protocolProviders, - boolean isVideoBridge) - { - this(null, null, protocolProviders, isVideoBridge); - } - - /** - * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying a - * preselected protocol provider to be used and if this is an invite for - * a video bridge conference. - * - * @param selectedConfProvider the preselected protocol provider - * @param isVideoBridge indicates if this dialog should create a conference - * through a video bridge - */ - public ConferenceInviteDialog( - ProtocolProviderService selectedConfProvider, - boolean isVideoBridge) - { - this(null, selectedConfProvider, null, isVideoBridge); - } - - /** - * Initializes the account selector panel. - * - * @param protocolProviders the list of protocol providers we'd like to - * show in the account selector box - */ - private void initAccountSelectorPanel( - List<ProtocolProviderService> protocolProviders) - { - JLabel accountSelectorLabel = new JLabel( - GuiActivator.getResources().getI18NString("service.gui.CALL_VIA")); - - TransparentPanel accountSelectorPanel - = new TransparentPanel(new BorderLayout()); - - accountSelectorPanel.setBorder( - BorderFactory.createEmptyBorder(5, 5, 5, 5)); - accountSelectorPanel.add(accountSelectorLabel, BorderLayout.WEST); - accountSelectorPanel.add(accountSelectorBox, BorderLayout.CENTER); - - // Initialize the account selector box. - if (protocolProviders != null && protocolProviders.size() > 0) - this.initAccountListData(protocolProviders); - else - this.initAccountListData(); - - this.accountSelectorBox.setRenderer(new DefaultListCellRenderer() - { - private static final long serialVersionUID = 0L; - - @Override - public Component getListCellRendererComponent(JList list, - Object value, int index, boolean isSelected, - boolean cellHasFocus) - { - ProtocolProviderService protocolProvider - = (ProtocolProviderService) value; - - if (protocolProvider != null) - { - this.setText( - protocolProvider.getAccountID().getDisplayName()); - this.setIcon( - ImageLoader.getAccountStatusImage(protocolProvider)); - } - - return this; - } - }); - - this.accountSelectorBox.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - Object accountSelectorBoxSelectedItem - = accountSelectorBox.getSelectedItem(); - - if (lastSelectedAccount == null - || !lastSelectedAccount - .equals(accountSelectorBoxSelectedItem)) - { - lastSelectedAccount = accountSelectorBoxSelectedItem; - - initContactListData( - (ProtocolProviderService) accountSelectorBox - .getSelectedItem()); - - if (isVideoBridge) - destContactList.removeAll(); - } - } - }); - - this.getContentPane().add(accountSelectorPanel, BorderLayout.NORTH); - } - - /** - * Initializes the account selector box with the given list of - * <tt>ProtocolProviderService</tt>-s. - * - * @param protocolProviders the list of <tt>ProtocolProviderService</tt>-s - * we'd like to show in the account selector box - */ - private void initAccountListData( - List<ProtocolProviderService> protocolProviders) - { - Iterator<ProtocolProviderService> providersIter - = protocolProviders.iterator(); - - while (providersIter.hasNext()) - { - ProtocolProviderService protocolProvider - = providersIter.next(); - - accountSelectorBox.addItem(protocolProvider); - } - - if (accountSelectorBox.getItemCount() > 0) - accountSelectorBox.setSelectedIndex(0); - } - - /** - * Initializes the account list. - */ - private void initAccountListData() - { - Iterator<ProtocolProviderService> protocolProviders - = GuiActivator.getUIService().getMainFrame().getProtocolProviders(); - - while(protocolProviders.hasNext()) - { - ProtocolProviderService protocolProvider - = protocolProviders.next(); - OperationSet opSet - = protocolProvider.getOperationSet( - OperationSetTelephonyConferencing.class); - - if ((opSet != null) && protocolProvider.isRegistered()) - accountSelectorBox.addItem(protocolProvider); - } - - // Try to select the last used account if available. - ProtocolProviderService pps - = ConfigurationUtils.getLastCallConferenceProvider(); - - if (pps == null && conference != null) - { - /* - * Pick up the first account from the ones participating in the - * associated telephony conference which supports - * OperationSetTelephonyConferencing. - */ - for (Call call : conference.getCalls()) - { - ProtocolProviderService callPps = call.getProtocolProvider(); - - if (callPps.getOperationSet( - OperationSetTelephonyConferencing.class) - != null) - { - pps = callPps; - break; - } - } - } - - if (pps != null) - accountSelectorBox.setSelectedItem(pps); - else if (accountSelectorBox.getItemCount() > 0) - accountSelectorBox.setSelectedIndex(0); - } - - /** - * Initializes contact list sources. - */ - private void initContactSources() - { - DemuxContactSourceService demuxCSService - = GuiActivator.getDemuxContactSourceService(); - - // If the DemuxContactSourceService isn't registered we use the default - // contact source set. - if (demuxCSService == null) - return; - - Iterator<UIContactSource> sourcesIter - = new ArrayList<UIContactSource>( - srcContactList.getContactSources()).iterator(); - - srcContactList.removeAllContactSources(); - - while (sourcesIter.hasNext()) - { - ContactSourceService contactSource - = sourcesIter.next().getContactSourceService(); - - srcContactList.addContactSource( - demuxCSService.createDemuxContactSource(contactSource)); - } - } - - /** - * Initializes the left contact list with the contacts that could be added - * to the current chat session. - * @param protocolProvider the protocol provider from which to initialize - * the contact list data - */ - private void initContactListData(ProtocolProviderService protocolProvider) - { - this.setCurrentProvider(protocolProvider); - - Iterator<UIContactSource> sourcesIter - = new ArrayList<UIContactSource>( - srcContactList.getContactSources()).iterator(); - - while (sourcesIter.hasNext()) - { - ContactSourceService contactSource - = sourcesIter.next().getContactSourceService(); - - if (contactSource instanceof ProtocolAwareContactSourceService) - { - ((ProtocolAwareContactSourceService) contactSource) - .setPreferredProtocolProvider( - OperationSetBasicTelephony.class, protocolProvider); - } - } - - srcContactList.removeContactSource(currentProviderContactSource); - srcContactList.removeContactSource(currentStringContactSource); - - currentProviderContactSource - = new ProtocolContactSourceServiceImpl( - protocolProvider, - OperationSetBasicTelephony.class); - currentStringContactSource - = new StringContactSourceServiceImpl( - protocolProvider, - OperationSetBasicTelephony.class); - - srcContactList.addContactSource(currentProviderContactSource); - srcContactList.addContactSource(currentStringContactSource); - - srcContactList.applyDefaultFilter(); - } - - /** - * Invites the contacts to the chat conference. - * - * @param contacts the list of contacts to invite - */ - private void inviteContacts(Collection<UIContact> contacts) - { - ProtocolProviderService selectedProvider = null; - Map<ProtocolProviderService, List<String>> selectedProviderCallees - = new HashMap<ProtocolProviderService, List<String>>(); - List<String> callees = null; - - Iterator<UIContact> contactsIter = contacts.iterator(); - - while (contactsIter.hasNext()) - { - UIContact uiContact = contactsIter.next(); - - Iterator<UIContactDetail> contactDetailsIter = uiContact - .getContactDetailsForOperationSet( - OperationSetBasicTelephony.class).iterator(); - - // We invite the first protocol contact that corresponds to the - // invite provider. - if (contactDetailsIter.hasNext()) - { - UIContactDetail inviteDetail = contactDetailsIter.next(); - selectedProvider = inviteDetail - .getPreferredProtocolProvider( - OperationSetBasicTelephony.class); - - if (selectedProvider == null) - { - selectedProvider - = (ProtocolProviderService) - accountSelectorBox.getSelectedItem(); - } - - if(selectedProvider != null - && selectedProviderCallees.get(selectedProvider) != null) - { - callees = selectedProviderCallees.get(selectedProvider); - } - else - { - callees = new ArrayList<String>(); - } - - callees.add(inviteDetail.getAddress()); - selectedProviderCallees.put(selectedProvider, callees); - } - } - - if(conference != null) - { - CallManager.inviteToConferenceCall( - selectedProviderCallees, - conference); - } - else - { - CallManager.createConferenceCall(selectedProviderCallees); - } - } - - /** - * Invites the contacts to the chat conference. - * - * @param contacts the list of contacts to invite - */ - private void inviteVideoBridgeContacts( - ProtocolProviderService preselectedProvider, - Collection<UIContact> contacts) - { - List<String> callees = new ArrayList<String>(); - - Iterator<UIContact> contactsIter = contacts.iterator(); - - while (contactsIter.hasNext()) - { - UIContact uiContact = contactsIter.next(); - - Iterator<UIContactDetail> contactDetailsIter = uiContact - .getContactDetailsForOperationSet( - OperationSetBasicTelephony.class).iterator(); - - // We invite the first protocol contact that corresponds to the - // invite provider. - if (contactDetailsIter.hasNext()) - { - UIContactDetail inviteDetail = contactDetailsIter.next(); - - callees.add(inviteDetail.getAddress()); - } - } - - if(conference != null) - { - CallManager.inviteToVideoBridgeConfCall( - callees.toArray(new String[callees.size()]), - conference.getCalls().get(0)); - } - else - { - CallManager.createVideoBridgeConfCall( - preselectedProvider, - callees.toArray(new String[callees.size()])); - } - } -} +/*
+ * 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.impl.gui.main.call.conference;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.impl.gui.*;
+import net.java.sip.communicator.impl.gui.main.call.*;
+import net.java.sip.communicator.impl.gui.main.contactlist.contactsource.*;
+import net.java.sip.communicator.impl.gui.utils.*;
+import net.java.sip.communicator.plugin.desktoputil.*;
+import net.java.sip.communicator.service.contactsource.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * The invite dialog is the one shown when the user clicks on the conference
+ * button in the chat toolbar.
+ *
+ * @author Yana Stamcheva
+ * @author Lyubomir Marinov
+ */
+public class ConferenceInviteDialog
+ extends InviteDialog
+{
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * The account selector box.
+ */
+ private final JComboBox accountSelectorBox = new JComboBox();
+
+ /**
+ * The last selected account.
+ */
+ private Object lastSelectedAccount;
+
+ /**
+ * The telephony conference into which this instance is to invite
+ * participants.
+ */
+ private final CallConference conference;
+
+ /**
+ * The current provider contact source.
+ */
+ private ContactSourceService currentProviderContactSource;
+
+ /**
+ * The current string contact source.
+ */
+ private ContactSourceService currentStringContactSource;
+
+ /**
+ * The previously selected protocol provider, with which this dialog has
+ * been instantiated.
+ */
+ private ProtocolProviderService preselectedProtocolProvider;
+
+ /**
+ * Indicates whether this conference invite dialog is associated with a
+ * Jitsi Videobridge invite.
+ */
+ private final boolean isJitsiVideobridge;
+
+ /**
+ * Initializes a new <tt>ConferenceInviteDialog</tt> instance which is to
+ * invite contacts/participants in a specific telephony conference.
+ *
+ * @param conference the telephony conference in which the new instance is
+ * to invite contacts/participants
+ */
+ public ConferenceInviteDialog(
+ CallConference conference,
+ ProtocolProviderService preselectedProvider,
+ List<ProtocolProviderService> protocolProviders,
+ final boolean isJitsiVideobridge)
+ {
+ // Set the correct dialog title depending if we're going to create a
+ // video bridge conference call
+ super((isJitsiVideobridge
+ ? GuiActivator.getResources()
+ .getI18NString("service.gui.INVITE_CONTACT_TO_VIDEO_BRIDGE")
+ : GuiActivator.getResources()
+ .getI18NString("service.gui.INVITE_CONTACT_TO_CALL")),
+ false);
+
+ this.conference = conference;
+ this.preselectedProtocolProvider = preselectedProvider;
+ this.isJitsiVideobridge = isJitsiVideobridge;
+
+ if (preselectedProtocolProvider == null)
+ initAccountSelectorPanel(protocolProviders);
+
+ // init the list, as we check whether features are supported
+ // it may take some time if we have too much contacts
+ SwingUtilities.invokeLater(new Runnable()
+ {
+ public void run()
+ {
+ initContactSources();
+
+ // Initialize the list of contacts to select from.
+ if (preselectedProtocolProvider != null)
+ initContactListData(preselectedProtocolProvider);
+ else
+ initContactListData(
+ (ProtocolProviderService) accountSelectorBox
+ .getSelectedItem());
+ }
+ });
+
+ this.addInviteButtonListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ Collection<UIContact> selectedContacts
+ = destContactList.getContacts(null);
+
+ if (selectedContacts != null && selectedContacts.size() > 0)
+ {
+ if (preselectedProtocolProvider == null)
+ preselectedProtocolProvider
+ = (ProtocolProviderService) accountSelectorBox
+ .getSelectedItem();
+
+ if (isJitsiVideobridge)
+ inviteJitsiVideobridgeContacts( preselectedProtocolProvider,
+ selectedContacts);
+ else
+ inviteContacts(selectedContacts);
+
+ // Store the last used account in order to pre-select it
+ // next time.
+ ConfigurationUtils.setLastCallConferenceProvider(
+ preselectedProtocolProvider);
+
+ dispose();
+ }
+ else
+ {
+ // TODO: The underlying invite dialog should show a message
+ // to the user that she should select at least two contacts
+ // in order to create a conference.
+ }
+ }
+ });
+
+ this.addCancelButtonListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ dispose();
+ }
+ });
+ }
+
+ /**
+ * Constructs the <tt>ConferenceInviteDialog</tt>.
+ */
+ public ConferenceInviteDialog()
+ {
+ this(null, null, null, false);
+ }
+
+ /**
+ * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying an
+ * already created conference. To use when inviting contacts to an existing
+ * conference is needed.
+ *
+ * @param conference the existing <tt>CallConference</tt>
+ */
+ public ConferenceInviteDialog(CallConference conference)
+ {
+ this(conference, null, null, false);
+ }
+
+ /**
+ * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying an
+ * already created conference. To use when inviting contacts to an existing
+ * conference is needed.
+ *
+ * @param conference the existing <tt>CallConference</tt>
+ */
+ public ConferenceInviteDialog(
+ CallConference conference,
+ ProtocolProviderService preselectedProtocolProvider,
+ boolean isJitsiVideobridge)
+ {
+ this(conference, preselectedProtocolProvider, null, isJitsiVideobridge);
+ }
+
+ /**
+ * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying a
+ * preselected protocol provider to be used and if this is an invite for
+ * a video bridge conference.
+ *
+ * @param selectedConfProvider the preselected protocol provider
+ * @param isJitsiVideobridge <tt>true</tt> if this dialog should create a
+ * conference through a Jitsi Videobridge; otherwise, <tt>false</tt>
+ */
+ public ConferenceInviteDialog(
+ List<ProtocolProviderService> protocolProviders,
+ boolean isJitsiVideobridge)
+ {
+ this(null, null, protocolProviders, isJitsiVideobridge);
+ }
+
+ /**
+ * Creates an instance of <tt>ConferenceInviteDialog</tt> by specifying a
+ * preselected protocol provider to be used and if this is an invite for
+ * a video bridge conference.
+ *
+ * @param selectedConfProvider the preselected protocol provider
+ * @param isJitsiVideobridge <tt>true</tt> if this dialog should create a
+ * conference through a Jitsi Videobridge; otherwise, <tt>false</tt>
+ */
+ public ConferenceInviteDialog(
+ ProtocolProviderService selectedConfProvider,
+ boolean isJitsiVideobridge)
+ {
+ this(null, selectedConfProvider, null, isJitsiVideobridge);
+ }
+
+ /**
+ * Initializes the account selector panel.
+ *
+ * @param protocolProviders the list of protocol providers we'd like to
+ * show in the account selector box
+ */
+ private void initAccountSelectorPanel(
+ List<ProtocolProviderService> protocolProviders)
+ {
+ JLabel accountSelectorLabel = new JLabel(
+ GuiActivator.getResources().getI18NString("service.gui.CALL_VIA"));
+
+ TransparentPanel accountSelectorPanel
+ = new TransparentPanel(new BorderLayout());
+
+ accountSelectorPanel.setBorder(
+ BorderFactory.createEmptyBorder(5, 5, 5, 5));
+ accountSelectorPanel.add(accountSelectorLabel, BorderLayout.WEST);
+ accountSelectorPanel.add(accountSelectorBox, BorderLayout.CENTER);
+
+ // Initialize the account selector box.
+ if (protocolProviders != null && protocolProviders.size() > 0)
+ this.initAccountListData(protocolProviders);
+ else
+ this.initAccountListData();
+
+ this.accountSelectorBox.setRenderer(new DefaultListCellRenderer()
+ {
+ private static final long serialVersionUID = 0L;
+
+ @Override
+ public Component getListCellRendererComponent(JList list,
+ Object value, int index, boolean isSelected,
+ boolean cellHasFocus)
+ {
+ ProtocolProviderService protocolProvider
+ = (ProtocolProviderService) value;
+
+ if (protocolProvider != null)
+ {
+ this.setText(
+ protocolProvider.getAccountID().getDisplayName());
+ this.setIcon(
+ ImageLoader.getAccountStatusImage(protocolProvider));
+ }
+
+ return this;
+ }
+ });
+
+ this.accountSelectorBox.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ Object accountSelectorBoxSelectedItem
+ = accountSelectorBox.getSelectedItem();
+
+ if (lastSelectedAccount == null
+ || !lastSelectedAccount
+ .equals(accountSelectorBoxSelectedItem))
+ {
+ lastSelectedAccount = accountSelectorBoxSelectedItem;
+
+ initContactListData(
+ (ProtocolProviderService) accountSelectorBox
+ .getSelectedItem());
+
+ if (isJitsiVideobridge)
+ destContactList.removeAll();
+ }
+ }
+ });
+
+ this.getContentPane().add(accountSelectorPanel, BorderLayout.NORTH);
+ }
+
+ /**
+ * Initializes the account selector box with the given list of
+ * <tt>ProtocolProviderService</tt>-s.
+ *
+ * @param protocolProviders the list of <tt>ProtocolProviderService</tt>-s
+ * we'd like to show in the account selector box
+ */
+ private void initAccountListData(
+ List<ProtocolProviderService> protocolProviders)
+ {
+ Iterator<ProtocolProviderService> providersIter
+ = protocolProviders.iterator();
+
+ while (providersIter.hasNext())
+ {
+ ProtocolProviderService protocolProvider
+ = providersIter.next();
+
+ accountSelectorBox.addItem(protocolProvider);
+ }
+
+ if (accountSelectorBox.getItemCount() > 0)
+ accountSelectorBox.setSelectedIndex(0);
+ }
+
+ /**
+ * Initializes the account list.
+ */
+ private void initAccountListData()
+ {
+ Iterator<ProtocolProviderService> protocolProviders
+ = GuiActivator.getUIService().getMainFrame().getProtocolProviders();
+
+ while(protocolProviders.hasNext())
+ {
+ ProtocolProviderService protocolProvider
+ = protocolProviders.next();
+ OperationSet opSet
+ = protocolProvider.getOperationSet(
+ OperationSetTelephonyConferencing.class);
+
+ if ((opSet != null) && protocolProvider.isRegistered())
+ accountSelectorBox.addItem(protocolProvider);
+ }
+
+ // Try to select the last used account if available.
+ ProtocolProviderService pps
+ = ConfigurationUtils.getLastCallConferenceProvider();
+
+ if (pps == null && conference != null)
+ {
+ /*
+ * Pick up the first account from the ones participating in the
+ * associated telephony conference which supports
+ * OperationSetTelephonyConferencing.
+ */
+ for (Call call : conference.getCalls())
+ {
+ ProtocolProviderService callPps = call.getProtocolProvider();
+
+ if (callPps.getOperationSet(
+ OperationSetTelephonyConferencing.class)
+ != null)
+ {
+ pps = callPps;
+ break;
+ }
+ }
+ }
+
+ if (pps != null)
+ accountSelectorBox.setSelectedItem(pps);
+ else if (accountSelectorBox.getItemCount() > 0)
+ accountSelectorBox.setSelectedIndex(0);
+ }
+
+ /**
+ * Initializes contact list sources.
+ */
+ private void initContactSources()
+ {
+ DemuxContactSourceService demuxCSService
+ = GuiActivator.getDemuxContactSourceService();
+
+ // If the DemuxContactSourceService isn't registered we use the default
+ // contact source set.
+ if (demuxCSService == null)
+ return;
+
+ Iterator<UIContactSource> sourcesIter
+ = new ArrayList<UIContactSource>(
+ srcContactList.getContactSources()).iterator();
+
+ srcContactList.removeAllContactSources();
+
+ while (sourcesIter.hasNext())
+ {
+ ContactSourceService contactSource
+ = sourcesIter.next().getContactSourceService();
+
+ srcContactList.addContactSource(
+ demuxCSService.createDemuxContactSource(contactSource));
+ }
+ }
+
+ /**
+ * Initializes the left contact list with the contacts that could be added
+ * to the current chat session.
+ * @param protocolProvider the protocol provider from which to initialize
+ * the contact list data
+ */
+ private void initContactListData(ProtocolProviderService protocolProvider)
+ {
+ this.setCurrentProvider(protocolProvider);
+
+ Iterator<UIContactSource> sourcesIter
+ = new ArrayList<UIContactSource>(
+ srcContactList.getContactSources()).iterator();
+
+ while (sourcesIter.hasNext())
+ {
+ ContactSourceService contactSource
+ = sourcesIter.next().getContactSourceService();
+
+ if (contactSource instanceof ProtocolAwareContactSourceService)
+ {
+ ((ProtocolAwareContactSourceService) contactSource)
+ .setPreferredProtocolProvider(
+ OperationSetBasicTelephony.class, protocolProvider);
+ }
+ }
+
+ srcContactList.removeContactSource(currentProviderContactSource);
+ srcContactList.removeContactSource(currentStringContactSource);
+
+ currentProviderContactSource
+ = new ProtocolContactSourceServiceImpl(
+ protocolProvider,
+ OperationSetBasicTelephony.class);
+ currentStringContactSource
+ = new StringContactSourceServiceImpl(
+ protocolProvider,
+ OperationSetBasicTelephony.class);
+
+ srcContactList.addContactSource(currentProviderContactSource);
+ srcContactList.addContactSource(currentStringContactSource);
+
+ srcContactList.applyDefaultFilter();
+ }
+
+ /**
+ * Invites the contacts to the chat conference.
+ *
+ * @param contacts the list of contacts to invite
+ */
+ private void inviteContacts(Collection<UIContact> contacts)
+ {
+ ProtocolProviderService selectedProvider = null;
+ Map<ProtocolProviderService, List<String>> selectedProviderCallees
+ = new HashMap<ProtocolProviderService, List<String>>();
+ List<String> callees = null;
+
+ Iterator<UIContact> contactsIter = contacts.iterator();
+
+ while (contactsIter.hasNext())
+ {
+ UIContact uiContact = contactsIter.next();
+
+ Iterator<UIContactDetail> contactDetailsIter = uiContact
+ .getContactDetailsForOperationSet(
+ OperationSetBasicTelephony.class).iterator();
+
+ // We invite the first protocol contact that corresponds to the
+ // invite provider.
+ if (contactDetailsIter.hasNext())
+ {
+ UIContactDetail inviteDetail = contactDetailsIter.next();
+ selectedProvider = inviteDetail
+ .getPreferredProtocolProvider(
+ OperationSetBasicTelephony.class);
+
+ if (selectedProvider == null)
+ {
+ selectedProvider
+ = (ProtocolProviderService)
+ accountSelectorBox.getSelectedItem();
+ }
+
+ if(selectedProvider != null
+ && selectedProviderCallees.get(selectedProvider) != null)
+ {
+ callees = selectedProviderCallees.get(selectedProvider);
+ }
+ else
+ {
+ callees = new ArrayList<String>();
+ }
+
+ callees.add(inviteDetail.getAddress());
+ selectedProviderCallees.put(selectedProvider, callees);
+ }
+ }
+
+ if(conference != null)
+ {
+ CallManager.inviteToConferenceCall(
+ selectedProviderCallees,
+ conference);
+ }
+ else
+ {
+ CallManager.createConferenceCall(selectedProviderCallees);
+ }
+ }
+
+ /**
+ * Invites the contacts to the chat conference.
+ *
+ * @param contacts the list of contacts to invite
+ */
+ private void inviteJitsiVideobridgeContacts(
+ ProtocolProviderService preselectedProvider,
+ Collection<UIContact> contacts)
+ {
+ List<String> callees = new ArrayList<String>();
+
+ Iterator<UIContact> contactsIter = contacts.iterator();
+
+ while (contactsIter.hasNext())
+ {
+ UIContact uiContact = contactsIter.next();
+
+ Iterator<UIContactDetail> contactDetailsIter = uiContact
+ .getContactDetailsForOperationSet(
+ OperationSetBasicTelephony.class).iterator();
+
+ // We invite the first protocol contact that corresponds to the
+ // invite provider.
+ if (contactDetailsIter.hasNext())
+ {
+ UIContactDetail inviteDetail = contactDetailsIter.next();
+
+ callees.add(inviteDetail.getAddress());
+ }
+ }
+
+ if(conference != null)
+ {
+ CallManager.inviteToJitsiVideobridgeConfCall(
+ callees.toArray(new String[callees.size()]),
+ conference.getCalls().get(0));
+ }
+ else
+ {
+ CallManager.createJitsiVideobridgeConfCall(
+ preselectedProvider,
+ callees.toArray(new String[callees.size()]));
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java index 742bfd2..b56bada 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java @@ -1,1385 +1,1385 @@ -/* - * 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.impl.gui.main.call.conference; - -import java.awt.*; -import java.util.*; -import java.util.List; - -import javax.swing.*; - -import net.java.sip.communicator.impl.gui.main.call.*; -import net.java.sip.communicator.impl.gui.utils.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.plugin.desktoputil.TransparentPanel; - -import org.jitsi.util.swing.*; - -/** - * Extends <tt>BasicConferenceCallPanel</tt> to implement a user interface - * <tt>Component</tt> which depicts a <tt>CallConference</tt> with audio and - * video and is contained in a <tt>CallPanel</tt>. - * - * @author Yana Stamcheva - * @author Lyubomir Marinov - */ -public class VideoConferenceCallPanel - extends BasicConferenceCallPanel -{ - /** - * The <tt>Logger</tt> used by the <tt>VideoConferenceCallPanel</tt> class - * and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(VideoConferenceCallPanel.class); - - /** - * The compile-time flag which indicates whether each video displayed by - * <tt>VideoConferenceCallPanel</tt> is to be depicted with an associated - * tool bar showing information and controls related to the (local or - * remote) peer sending the respective video. - */ - private static final boolean SHOW_TOOLBARS = true; - - /** - * The facility which aids this instance with the video-related information. - */ - private final UIVideoHandler2 uiVideoHandler; - - /** - * The <tt>Observer</tt> which listens to {@link #uiVideoHandler} about - * changes in the video-related information. - */ - private final Observer uiVideoHandlerObserver - = new Observer() - { - public void update(Observable o, Object arg) - { - updateViewFromModel(); - } - }; - - /** - * The <tt>VideoContainer</tt> which occupies this whole <tt>Component</tt> - * and arranges the visual <tt>Component</tt>s displaying the video - * streaming between the local peer/user and the remote peer(s). - */ - private final VideoContainer videoContainer; - - /** - * The set of visual <tt>Component</tt>s displaying video streaming between - * the local peer/user and the remote peers which are depicted by this - * instance. - */ - private final Set<Component> videos = new HashSet<Component>(); - - /** - * The thumbnail container. - */ - private final ThumbnailConferenceCallPanel thumbnailContainer; - - /** - * Initializes a new <tt>VideoConferenceCallPanel</tt> instance which is to - * be used by a specific <tt>CallPanel</tt> to depict a specific - * <tt>CallConference</tt>. The new instance will depict both the - * audio-related and the video-related information. - * - * @param callPanel the <tt>CallPanel</tt> which will use the new instance - * to depict the specified <tt>CallConference</tt>. - * @param callConference the <tt>CallConference</tt> to be depicted by the - * new instance - * @param uiVideoHandler the utility which is to aid the new instance in - * dealing with the video-related information - */ - public VideoConferenceCallPanel( - CallPanel callPanel, - CallConference callConference, - UIVideoHandler2 uiVideoHandler) - { - super(callPanel, callConference); - - this.uiVideoHandler = uiVideoHandler; - - thumbnailContainer - = new ThumbnailConferenceCallPanel( callPanel, - callConference, - uiVideoHandler); - - videoContainer = createVideoContainer(); - - /* - * Our user interface hierarchy has been initialized so we are ready to - * begin receiving events warranting updates of this view from its - * model. - */ - uiVideoHandler.addObserver(uiVideoHandlerObserver); - - /* - * Notify the super that this instance has completed its initialization - * and the view that it implements is ready to be updated from the - * model. - */ - initializeComplete(); - } - - private void addConferenceMemberContainers( - ConferenceParticipantContainer cpc) - { - List<ConferenceParticipantContainer> cmcs - = cpc.conferenceMemberContainers; - - if ((cmcs != null) && !cmcs.isEmpty()) - { - for (ConferenceParticipantContainer cmc : cmcs) - { - if (!cmc.toBeRemoved) - { - videoContainer.add( - cmc.getComponent(), - VideoLayout.CENTER_REMOTE); - } - } - } - } - - private Component createDefaultPhotoPanel(Call call) - { - OperationSetServerStoredAccountInfo accountInfo - = call.getProtocolProvider().getOperationSet( - OperationSetServerStoredAccountInfo.class); - ImageIcon photoLabelIcon = null; - - if (accountInfo != null) - { - byte[] accountImage = AccountInfoUtils.getImage(accountInfo); - - // do not set empty images - if ((accountImage != null) && (accountImage.length > 0)) - photoLabelIcon = new ImageIcon(accountImage); - } - if (photoLabelIcon == null) - { - photoLabelIcon - = new ImageIcon( - ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO)); - } - - return createDefaultPhotoPanel(photoLabelIcon); - } - - private Component createDefaultPhotoPanel(CallPeer callPeer) - { - byte[] peerImage = CallManager.getPeerImage(callPeer); - ImageIcon photoLabelIcon - = (peerImage == null) - ? new ImageIcon( - ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO)) - : new ImageIcon(peerImage); - - return createDefaultPhotoPanel(photoLabelIcon); - } - - private Component createDefaultPhotoPanel(ConferenceMember conferenceMember) - { - return - createDefaultPhotoPanel( - new ImageIcon( - ImageLoader.getImage( - ImageLoader.DEFAULT_USER_PHOTO))); - } - - /** - * Creates a new <tt>Component</tt> which is to display a specific - * <tt>ImageIcon</tt> representing the photo of a participant in a call. - * - * @param photoLabelIcon the <tt>ImageIcon</tt> which represents the photo - * of a participant in a call and which is to be displayed by the new - * <tt>Component</tt> - * @return a new <tt>Component</tt> which displays the specified - * <tt>photoLabelIcon</tt> - */ - private Component createDefaultPhotoPanel(ImageIcon photoLabelIcon) - { - JLabel photoLabel = new JLabel(); - - photoLabel.setIcon(photoLabelIcon); - - @SuppressWarnings("serial") - JPanel photoPanel - = new TransparentPanel(new GridBagLayout()) - { - /** - * @{inheritDoc} - */ - @Override - public void paintComponent(Graphics g) - { - super.paintComponent(g); - - g = g.create(); - try - { - AntialiasingManager.activateAntialiasing(g); - - g.setColor(Color.GRAY); - g.fillRoundRect( - 0, 0, this.getWidth(), this.getHeight(), - 6, 6); - } - finally - { - g.dispose(); - } - } - }; - - photoPanel.setPreferredSize(new Dimension(320, 240)); - - GridBagConstraints photoPanelConstraints = new GridBagConstraints(); - - photoPanelConstraints.anchor = GridBagConstraints.CENTER; - photoPanelConstraints.fill = GridBagConstraints.NONE; - photoPanel.add(photoLabel, photoPanelConstraints); - - return photoPanel; - } - - /** - * Initializes a new <tt>VideoContainer</tt> instance which is to contain - * the visual/video <tt>Component</tt>s of the telephony conference depicted - * by this instance. - */ - private VideoContainer createVideoContainer() - { - VideoContainer videoContainer = new VideoContainer(null, true); - - JPanel thumbnailPanel = new JPanel(new BorderLayout()); - thumbnailPanel.setBackground(Color.DARK_GRAY); - thumbnailPanel.add(thumbnailContainer, BorderLayout.NORTH); - - add(thumbnailPanel, BorderLayout.EAST); - add(videoContainer, BorderLayout.CENTER); - - return videoContainer; - } - - /** - * {@inheritDoc} - */ - @Override - public void dispose() - { - try - { - uiVideoHandler.deleteObserver(uiVideoHandlerObserver); - } - finally - { - super.dispose(); - } - } - - /** - * Determines whether a specific <tt>ConferenceMember</tt> represents the - * same conference participant as a specific <tt>CallPeer</tt>. If the - * specified <tt>conferenceMember</tt> is <tt>null</tt>, returns - * <tt>true</tt>. Otherwise, determines whether the addresses of the - * specified <tt>conferenceMember</tt> and the specified <tt>callPeer</tt> - * identify one and the same entity. - * - * @param conferenceMember the <tt>ConferenceMember</tt> to be checked - * whether is represents the same conference participant as the specified - * <tt>callPeer</tt>. If it is <tt>null</tt>, <tt>true</tt> is returned. - * @param callPeer the <tt>CallPeer</tt> to be checked whether it represents - * the same conference participant as the specified - * <tt>conferenceMember</tt> - * @return <tt>true</tt> if the specified <tt>conferenceMember</tt> and the - * specified <tt>callPeer</tt> represent the same conference participant or - * the specified <tt>conferenceMember</tt> is <tt>null</tt>; otherwise, - * <tt>false</tt> - */ - private boolean isConferenceMemberCallPeer( - ConferenceMember conferenceMember, - CallPeer callPeer) - { - return - (conferenceMember == null) - ? true - : CallManager.addressesAreEqual( - conferenceMember.getAddress(), - callPeer.getAddress()); - } - - /** - * Determines whether a specific <tt>ConferenceMember</tt> represents the - * local peer/user. Since this instance depicts a whole telephony - * conference, the local peer/user may be participating with multiple - * <tt>Call</tt>s in it. The <tt>Call</tt>s may be through different - * (local) accounts. That's why the implementation determines whether the - * address of the specified <tt>conferenceMember</tt> identifies the address - * of a (local) accounts involved in the telephony conference depicted by - * this instance. - * - * @param conferenceMember the <tt>ConferenceMember</tt> to be checked - * whether it represents the local peer/user - * @return <tt>true</tt> if the specified <tt>conferenceMember</tt> - * represents the local peer/user; otherwise, <tt>false</tt> - */ - private boolean isConferenceMemberLocalUser( - ConferenceMember conferenceMember) - { - String address = conferenceMember.getAddress(); - - for (Call call : callConference.getCalls()) - { - if (CallManager.addressesAreEqual( - address, - call.getProtocolProvider().getAccountID() - .getAccountAddress())) - { - return true; - } - } - return false; - } - - private void removeConferenceMemberContainers( - ConferenceParticipantContainer cpc, - boolean all) - { - List<ConferenceParticipantContainer> cmcs - = cpc.conferenceMemberContainers; - - if ((cmcs != null) && !cmcs.isEmpty()) - { - Iterator<ConferenceParticipantContainer> i = cmcs.iterator(); - - while (i.hasNext()) - { - ConferenceParticipantContainer cmc = i.next(); - - if (all || cmc.toBeRemoved) - { - i.remove(); - - videoContainer.remove(cmc.getComponent()); - cmc.dispose(); - } - } - } - } - - /** - * Updates the <tt>ConferenceParticipantContainer</tt>s which depict the - * <tt>ConferenceMember</tt>s of the <tt>CallPeer</tt> depicted by a - * specific <tt>ConferenceParticipantContainer</tt>. - * - * @param cpc the <tt>ConferenceParticipantContainer</tt> which depicts the - * <tt>CallPeer</tt> whose <tt>ConferenceMember</tt>s are to be depicted - * @param videos the visual <tt>Component</tt>s displaying video streaming - * from the remote peer (represented by <tt>cpc</tt>) to the local peer/user - * @param videoTelephony the <tt>OperationSetVideoTelephony</tt> which - * retrieved the specified <tt>videos</tt> from the <tt>CallPeer</tt> - * depicted by <tt>cpc</tt>. While the <tt>CallPeer</tt> could be queried - * for it, such a query would waste more resources at run time given that - * the invoker has it already. - */ - private void updateConferenceMemberContainers( - ConferenceParticipantContainer cpc, - List<Component> videos, - OperationSetVideoTelephony videoTelephony) - { - CallPeer callPeer = (CallPeer) cpc.getParticipant(); - List<ConferenceParticipantContainer> cmcs - = cpc.conferenceMemberContainers; - - /* - * Invalidate all conferenceMemberContainers. Then validate which of - * them are to remain and which of them are to really be removed - * later on. - */ - if (cmcs != null) - { - for (ConferenceParticipantContainer cmc : cmcs) - cmc.toBeRemoved = true; - } - - /* - * Depict the remote videos. They may or may not be associated with - * ConferenceMembers so the ConferenceMembers which have no - * associated videos will be depicted afterwards. - */ - if (videos != null) - { - Component video = cpc.getVideo(); - - for (Component conferenceMemberVideo : videos) - { - /* - * One of the remote videos is already used to depict the - * callPeer. - */ - if (conferenceMemberVideo == video) - continue; - - boolean addNewConferenceParticipantContainer = true; - ConferenceMember conferenceMember - = videoTelephony.getConferenceMember( - callPeer, - conferenceMemberVideo); - - if (cmcs == null) - { - cmcs = new LinkedList<ConferenceParticipantContainer>(); - cpc.conferenceMemberContainers = cmcs; - } - else - { - for (ConferenceParticipantContainer cmc : cmcs) - { - Object cmcParticipant = cmc.getParticipant(); - - if (conferenceMember == null) - { - if (cmcParticipant == callPeer) - { - Component cmcVideo = cmc.getVideo(); - - if (cmcVideo == null) - { - cmc.setVideo(conferenceMemberVideo); - cmc.toBeRemoved = false; - addNewConferenceParticipantContainer - = false; - break; - } - else if (cmcVideo == conferenceMemberVideo) - { - cmc.toBeRemoved = false; - addNewConferenceParticipantContainer - = false; - break; - } - } - } - else if (cmcParticipant == conferenceMember) - { - cmc.setVideo(conferenceMemberVideo); - cmc.toBeRemoved = false; - addNewConferenceParticipantContainer = false; - break; - } - } - } - - if (addNewConferenceParticipantContainer) - { - ConferenceParticipantContainer cmc - = (conferenceMember == null) - ? new ConferenceParticipantContainer( - callPeer, - conferenceMemberVideo) - : new ConferenceParticipantContainer( - conferenceMember, - conferenceMemberVideo); - - cmcs.add(cmc); - } - } - } - - /* - * Depict the ConferenceMembers which have not been depicted yet. - * They have no associated videos. - */ - List<ConferenceMember> conferenceMembers - = callPeer.getConferenceMembers(); - - if (!conferenceMembers.isEmpty()) - { - if (cmcs == null) - { - cmcs = new LinkedList<ConferenceParticipantContainer>(); - cpc.conferenceMemberContainers = cmcs; - } - for (ConferenceMember conferenceMember : conferenceMembers) - { - /* - * If the callPeer reports itself as a ConferenceMember, then - * we've already depicted it with cpc. - */ - if (isConferenceMemberCallPeer(conferenceMember, callPeer)) - continue; - /* - * If the callPeer reports the local peer/user as a - * ConferenceMember, then we've already depicted it. - */ - if (isConferenceMemberLocalUser(conferenceMember)) - continue; - - boolean addNewConferenceParticipantContainer = true; - - for (ConferenceParticipantContainer cmc : cmcs) - { - if (cmc.getParticipant() == conferenceMember) - { - /* - * It is possible to have a ConferenceMember who is - * sending video but we just do not have the SSRC of - * that video to associate the video with the - * ConferenceMember. In such a case, we may be depicting - * the ConferenceMember twice: once with video without a - * ConferenceMember and once with a ConferenceMember - * without video. This will surely be the case at the - * time of this writing with non-focus participants in a - * telephony conference hosted on a Jitsi VideoBridge. - * Such a display is undesirable. If the - * conferenceMember is known to send video, we will not - * display it until we associated it with a video. This - * way, if a ConferenceMember is not sending video, we - * will depict it and we can be sure that no video - * without a ConferenceMember association will be - * depicting it a second time. - */ - if (cmc.toBeRemoved - && !conferenceMember - .getVideoStatus() - .allowsSending()) - { - cmc.setVideo(null); - cmc.toBeRemoved = false; - } - addNewConferenceParticipantContainer = false; - break; - } - } - - if (addNewConferenceParticipantContainer) - { - ConferenceParticipantContainer cmc - = new ConferenceParticipantContainer( - conferenceMember, - null); - - cmcs.add(cmc); - } - } - } - - if ((cmcs != null) && !cmcs.isEmpty()) - { - removeConferenceMemberContainers(cpc, false); - /* - * If cpc is already added to the user interface hierarchy of this - * instance, then it was there before the update procedure and it - * was determined to be appropriate to continue to depict its model. - * Consequently, its Component will be neither added to (because it - * was already added) nor removed from the user interface hierarchy - * of this instance. That's why we have make sure that the - * Components of its conferenceMemberContainers are also added to - * the user interface. - */ - if (UIVideoHandler2.isAncestor(this, cpc.getComponent())) - addConferenceMemberContainers(cpc); - } - } - - /** - * {@inheritDoc} - */ - @Override - protected ConferenceCallPeerRenderer updateViewFromModel( - ConferenceCallPeerRenderer callPeerPanel, - CallPeer callPeer) - { - if (callPeer == null) - { - /* - * The local peer/user will be represented by a Call which has a - * CallPeer who provides local video. However, if the user has - * selected to hide the local video, the local peer/user will not be - * represented at all. - */ - Component video = null; - - if (uiVideoHandler.isLocalVideoVisible()) - { - for (Call aCall : callConference.getCalls()) - { - Iterator<? extends CallPeer> callPeerIter - = aCall.getCallPeers(); - OperationSetVideoTelephony videoTelephony - = aCall.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - - while (callPeerIter.hasNext()) - { - callPeer = callPeerIter.next(); - - if (videoTelephony != null) - { - try - { - video - = videoTelephony.getLocalVisualComponent( - callPeer); - } - catch (OperationFailedException ofe) - { - logger.error( - "Failed to retrieve the local video" - + " for display", - ofe); - } - if (video != null) - break; - } - } - if (video != null) - break; - } - } - - if (callPeer == null) - callPeerPanel = null; - else - { - Call call = callPeer.getCall(); - - if (callPeerPanel instanceof ConferenceParticipantContainer) - { - ConferenceParticipantContainer cpc - = (ConferenceParticipantContainer) callPeerPanel; - - if (cpc.getParticipant() == call) - cpc.setVideo(video); - else - callPeerPanel = null; - } - else - callPeerPanel = null; - if (callPeerPanel == null) - { - callPeerPanel - = new ConferenceParticipantContainer(call, video); - } - } - } - else - { - /* - * The specified callPeer will be represented by one of its remote - * videos which is not associated with a ConferenceMember or is - * associated with a ConferenceMember representing the callPeer - * itself. - */ - OperationSetVideoTelephony videoTelephony - = callPeer.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - List<Component> videos = null; - Component video = null; - - if (videoTelephony != null) - { - videos = videoTelephony.getVisualComponents(callPeer); - if ((videos != null) && !videos.isEmpty()) - { - for (Component aVideo : videos) - { - ConferenceMember conferenceMember - = videoTelephony.getConferenceMember( - callPeer, - aVideo); - - if (isConferenceMemberCallPeer( - conferenceMember, - callPeer)) - { - video = aVideo; - break; - } - } - } - } - - ConferenceParticipantContainer cpc = null; - - if (callPeerPanel instanceof ConferenceParticipantContainer) - { - cpc = (ConferenceParticipantContainer) callPeerPanel; - if (cpc.getParticipant() == callPeer) - cpc.setVideo(video); - else - cpc = null; - } - if (cpc == null) - cpc = new ConferenceParticipantContainer(callPeer, video); - callPeerPanel = cpc; - - // Update the conferenceMemberContainers of the cpc. - updateConferenceMemberContainers(cpc, videos, videoTelephony); - } - return callPeerPanel; - } - - /** - * {@inheritDoc} - * - * If {@link #SHOW_TOOLBARS} is <tt>false</tt>, disables the use of - * <tt>ConferenceParticipantContainer</tt>. A reason for such a value of - * <tt>SHOW_TOOLBARS</tt> may be that the functionality implemented in the - * model may not fully support mapping of visual <tt>Component</tt>s - * displaying video to telephony conference participants (e.g. in telephony - * conferences utilizing the Jitsi VideoBridge server-side technology). In - * such a case displays the videos only, does not map videos to participants - * and does not display participants who do not have videos. - */ - @Override - protected void updateViewFromModelInEventDispatchThread() - { - if (SHOW_TOOLBARS) - { - super.updateViewFromModelInEventDispatchThread(); - return; - } - - /* - * Determine the set of visual Components displaying video streaming - * between the local peer/user and the remote peers which are to be - * depicted by this instance. - */ - Component localVideo = null; - Set<Component> videos = new HashSet<Component>(); - - for (Call call : callConference.getCalls()) - { - OperationSetVideoTelephony videoTelephony - = call.getProtocolProvider().getOperationSet( - OperationSetVideoTelephony.class); - - if (videoTelephony == null) - continue; - - Iterator<? extends CallPeer> callPeerIter = call.getCallPeers(); - - while (callPeerIter.hasNext()) - { - CallPeer callPeer = callPeerIter.next(); - - /* - * TODO VideoConferenceCallPanel respects - * UIVideoHandler2.isLocalVideoVisible() in order to react to - * the associated button at the bottom of the CallPanel. - * However, it does not add a close button on top of the local - * video in contrast to OneToOneCallPeerPanel. Overall, the - * result is questionable. - */ - if (uiVideoHandler.isLocalVideoVisible() - && (localVideo == null)) - { - try - { - localVideo - = videoTelephony.getLocalVisualComponent(callPeer); - } - catch (OperationFailedException ofe) - { - /* - * We'll just try to get the local video through another - * CallPeer then. - */ - } - if (localVideo != null) - videos.add(localVideo); - } - - List<Component> callPeerRemoteVideos - = videoTelephony.getVisualComponents(callPeer); - - videos.addAll(callPeerRemoteVideos); - } - } - - /* - * Remove the Components of this view which are no longer present in the - * model. - */ - Iterator<Component> thisVideoIter = this.videos.iterator(); - - while (thisVideoIter.hasNext()) - { - Component thisVideo = thisVideoIter.next(); - - if (!videos.contains(thisVideo)) - { - thisVideoIter.remove(); - videoContainer.remove(thisVideo); - } - - /* - * If a video is known to be depicted by this view and is still - * present in the model, then we could remove it from the set of - * videos present in the model in order to prevent going through the - * procedure of adding it to this view. However, we choose to play - * on the safe side. - */ - } - - /* - * Add the Components of the model which are not depicted by this view. - */ - for (Component video : videos) - { - if (!UIVideoHandler2.isAncestor(videoContainer, video)) - { - this.videos.add(video); - videoContainer.add( - video, - (video == localVideo) - ? VideoLayout.LOCAL - : VideoLayout.CENTER_REMOTE); - } - } - } - - @Override - protected void viewForModelAdded( - ConferenceCallPeerRenderer callPeerPanel, - CallPeer callPeer) - { - videoContainer.add( - callPeerPanel.getComponent(), - VideoLayout.CENTER_REMOTE); - if ((callPeer != null) - && (callPeerPanel instanceof ConferenceParticipantContainer)) - { - addConferenceMemberContainers( - (ConferenceParticipantContainer) callPeerPanel); - } - } - - @Override - protected void viewForModelRemoved( - ConferenceCallPeerRenderer callPeerPanel, - CallPeer callPeer) - { - videoContainer.remove(callPeerPanel.getComponent()); - if ((callPeer != null) - && (callPeerPanel instanceof ConferenceParticipantContainer)) - { - removeConferenceMemberContainers( - (ConferenceParticipantContainer) callPeerPanel, - true); - } - } - - /** - * Implements an AWT <tt>Component</tt> which contains the user interface - * elements depicting a specific participant in the telephony conference - * depicted by a <tt>VideoConferenceCallPanel</tt>. - */ - private class ConferenceParticipantContainer - extends TransparentPanel - implements ConferenceCallPeerRenderer - { - /** - * The list of <tt>ConferenceParticipantContainer</tt>s which represent - * the <tt>ConferenceMember</tt>s of the participant represented by this - * instance. Since a <tt>CallPeer</tt> may send the local peer/user - * multiple videos without providing a way to associate a - * ConferenceMember with each one of them, the list may contain - * <tt>ConferenceParticipantContainer</tt>s which do not represent a - * specific <tt>ConferenceMember</tt> instance but rather a video sent - * by a <tt>CallPeer</tt> to the local peer/user which looks like (in - * the terms of <tt>VideoConferenceCallPanel) a member of a conference - * organized by the <tt>CallPeer</tt> in question. - * <p> - * Implements a state which is private to - * <tt>VideoConferenceCallPanel</tt> and is of no concern to - * <tt>ConferenceParticipantContainer</tt>. - * </p> - */ - List<ConferenceParticipantContainer> conferenceMemberContainers; - - /** - * The indicator which determines whether this instance is to be removed - * because it has become out-of-date, obsolete, unnecessary. - * <p> - * Implements a state which is private to - * <tt>VideoConferenceCallPanel</tt> and is of no concern to - * <tt>ConferenceParticipantContainer</tt>. - * </p> - */ - boolean toBeRemoved; - - /** - * The <tt>BasicConferenceParticipantPanel</tt> which is displayed at - * the bottom of this instance, bellow the {@link #video} (i.e. - * {@link #videoContainer}) and is referred to as the tool bar. - */ - private final BasicConferenceParticipantPanel<?> toolBar; - - /** - * The visual <tt>Component</tt>, if any, displaying video which is - * depicted by this instance. - */ - private Component video; - - /** - * The <tt>VideoContainer</tt> which lays out the video depicted by this - * instance i.e. {@link #video}. - */ - private final VideoContainer videoContainer; - - /** - * The <tt>CallPeer</tt> associated with this container, if it has been - * created to represent a <tt>CallPeer</tt>. - */ - private CallPeer callPeer; - - /** - * The <tt>conferenceMember</tt> associated with this container, if it - * has been created to represent a <tt>conferenceMember</tt>. - */ - private ConferenceMember conferenceMember; - - /** - * Indicates that this container contains information for the local - * user. - */ - private boolean isLocalUser; - - /** - * Initializes a new <tt>ConferenceParticipantContainer</tt> instance - * which is to depict the local peer/user. - * - * @param call a <tt>Call</tt> which is to provide information about the - * local peer/user - * @param video the visual <tt>Component</tt>, if any, displaying the - * video streaming from the local peer/user to the remote peer(s) - */ - public ConferenceParticipantContainer(Call call, Component video) - { - this( - createDefaultPhotoPanel(call), - video, - new ConferencePeerPanel( - VideoConferenceCallPanel.this, - call, - true), - null, null, true); - } - - public ConferenceParticipantContainer( - CallPeer callPeer, - Component video) - { - this( createDefaultPhotoPanel(callPeer), - video, - new ConferencePeerPanel( - VideoConferenceCallPanel.this, - callPeer, - true), - callPeer, null, false); - } - - private ConferenceParticipantContainer( - Component noVideo, - Component video, - BasicConferenceParticipantPanel<?> toolBar, - CallPeer callPeer, - ConferenceMember conferenceMember, - boolean isLocalUser) - { - super(new BorderLayout()); - - this.callPeer = callPeer; - this.conferenceMember = conferenceMember; - this.isLocalUser = isLocalUser; - - videoContainer = new VideoContainer(noVideo, false); - add(videoContainer, BorderLayout.CENTER); - - this.toolBar = toolBar; - if (this.toolBar != null) - add(this.toolBar, BorderLayout.SOUTH); - - if (video != null) - { - setVideo(video); - } - else - setVisible(false); - } - - public ConferenceParticipantContainer( - ConferenceMember conferenceMember, - Component video) - { - this( - createDefaultPhotoPanel(conferenceMember), - video, - new ConferenceMemberPanel( - VideoConferenceCallPanel.this, - conferenceMember, - true), - null, conferenceMember, false); - } - - public void dispose() - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.dispose(); - - // Dispose of the conferenceMemberContainers if any. - /* - * XXX The field conferenceMemberContainers implements a state - * private to VideoConferenceCallPanel which the latter makes sure - * to access on the AWT event dispatching thread only. Since we are - * going out of our way here to help VideoConferenceCallPanel, - * ensure that the mentioned synchronization rule is not violated. - */ - CallManager.assertIsEventDispatchingThread(); - if (conferenceMemberContainers != null) - { - for (ConferenceParticipantContainer cmc - : conferenceMemberContainers) - { - cmc.dispose(); - } - } - } - - public CallPanel getCallPanel() - { - return getCallRenderer().getCallContainer(); - } - - public SwingCallRenderer getCallRenderer() - { - return VideoConferenceCallPanel.this; - } - - public Component getComponent() - { - return this; - } - - private ConferenceCallPeerRenderer - getConferenceCallPeerRendererDelegate() - { - return - (toolBar instanceof ConferenceCallPeerRenderer) - ? (ConferenceCallPeerRenderer) toolBar - : null; - } - - /** - * Gets the conference participant depicted by this instance. - * - * @return the conference participant depicted by this instance - */ - public Object getParticipant() - { - return (toolBar == null) ? null : toolBar.getParticipant(); - } - - public Component getVideo() - { - return video; - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. Otherwise, returns <tt>false</tt>. - */ - public boolean isLocalVideoVisible() - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - return (delegate == null) ? false : delegate.isLocalVideoVisible(); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void printDTMFTone(char dtmfChar) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.printDTMFTone(dtmfChar); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void securityNegotiationStarted( - CallPeerSecurityNegotiationStartedEvent ev) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.securityNegotiationStarted(ev); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void securityOff(CallPeerSecurityOffEvent ev) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.securityOff(ev); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void securityOn(CallPeerSecurityOnEvent ev) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.securityOn(ev); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void securityPending() - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.securityPending(); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void securityTimeout(CallPeerSecurityTimeoutEvent ev) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.securityTimeout(ev); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setErrorReason(String reason) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setErrorReason(reason); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setLocalVideoVisible(boolean visible) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setLocalVideoVisible(visible); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setMute(boolean mute) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setMute(mute); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setOnHold(boolean onHold) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setOnHold(onHold); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setPeerImage(byte[] image) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setPeerImage(image); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setPeerName(String name) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setPeerName(name); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setPeerState( - CallPeerState oldState, - CallPeerState newState, - String stateString) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setPeerState(oldState, newState, stateString); - } - - /** - * {@inheritDoc} - * - * Delegates to the <tt>toolBar</tt>, if the latter implements - * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a - * container only. - */ - public void setSecurityPanelVisible(boolean visible) - { - ConferenceCallPeerRenderer delegate - = getConferenceCallPeerRendererDelegate(); - - if (delegate != null) - delegate.setSecurityPanelVisible(visible); - } - - /** - * Sets the visual <tt>Component</tt> displaying the video associated - * with the participant depicted by this instance. - * - * @param video the visual <tt>Component</tt> displaying video which is - * to be associated with the participant depicted by this instance - */ - void setVideo(Component video) - { - if (this.video != video) - { - if (this.video != null) - videoContainer.remove(this.video); - - this.video = video; - - if (this.video != null) - { - setVisible(true); - videoContainer.add(this.video, VideoLayout.CENTER_REMOTE); - } - else - setVisible(false); - - // Update thumbnails container according to video status. - if (thumbnailContainer != null) - { - if (conferenceMember != null) - thumbnailContainer - .updateThumbnail(conferenceMember, (video != null)); - else if (callPeer != null) - thumbnailContainer - .updateThumbnail(callPeer, (video != null)); - else if (isLocalUser) - thumbnailContainer - .updateThumbnail((video != null)); - } - } - } - } -} +/*
+ * 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.impl.gui.main.call.conference;
+
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.impl.gui.main.call.*;
+import net.java.sip.communicator.impl.gui.utils.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.plugin.desktoputil.*;
+import net.java.sip.communicator.plugin.desktoputil.TransparentPanel;
+
+import org.jitsi.util.swing.*;
+
+/**
+ * Extends <tt>BasicConferenceCallPanel</tt> to implement a user interface
+ * <tt>Component</tt> which depicts a <tt>CallConference</tt> with audio and
+ * video and is contained in a <tt>CallPanel</tt>.
+ *
+ * @author Yana Stamcheva
+ * @author Lyubomir Marinov
+ */
+public class VideoConferenceCallPanel
+ extends BasicConferenceCallPanel
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>VideoConferenceCallPanel</tt> class
+ * and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(VideoConferenceCallPanel.class);
+
+ /**
+ * The compile-time flag which indicates whether each video displayed by
+ * <tt>VideoConferenceCallPanel</tt> is to be depicted with an associated
+ * tool bar showing information and controls related to the (local or
+ * remote) peer sending the respective video.
+ */
+ private static final boolean SHOW_TOOLBARS = true;
+
+ /**
+ * The facility which aids this instance with the video-related information.
+ */
+ private final UIVideoHandler2 uiVideoHandler;
+
+ /**
+ * The <tt>Observer</tt> which listens to {@link #uiVideoHandler} about
+ * changes in the video-related information.
+ */
+ private final Observer uiVideoHandlerObserver
+ = new Observer()
+ {
+ public void update(Observable o, Object arg)
+ {
+ updateViewFromModel();
+ }
+ };
+
+ /**
+ * The <tt>VideoContainer</tt> which occupies this whole <tt>Component</tt>
+ * and arranges the visual <tt>Component</tt>s displaying the video
+ * streaming between the local peer/user and the remote peer(s).
+ */
+ private final VideoContainer videoContainer;
+
+ /**
+ * The set of visual <tt>Component</tt>s displaying video streaming between
+ * the local peer/user and the remote peers which are depicted by this
+ * instance.
+ */
+ private final Set<Component> videos = new HashSet<Component>();
+
+ /**
+ * The thumbnail container.
+ */
+ private final ThumbnailConferenceCallPanel thumbnailContainer;
+
+ /**
+ * Initializes a new <tt>VideoConferenceCallPanel</tt> instance which is to
+ * be used by a specific <tt>CallPanel</tt> to depict a specific
+ * <tt>CallConference</tt>. The new instance will depict both the
+ * audio-related and the video-related information.
+ *
+ * @param callPanel the <tt>CallPanel</tt> which will use the new instance
+ * to depict the specified <tt>CallConference</tt>.
+ * @param callConference the <tt>CallConference</tt> to be depicted by the
+ * new instance
+ * @param uiVideoHandler the utility which is to aid the new instance in
+ * dealing with the video-related information
+ */
+ public VideoConferenceCallPanel(
+ CallPanel callPanel,
+ CallConference callConference,
+ UIVideoHandler2 uiVideoHandler)
+ {
+ super(callPanel, callConference);
+
+ this.uiVideoHandler = uiVideoHandler;
+
+ thumbnailContainer
+ = new ThumbnailConferenceCallPanel( callPanel,
+ callConference,
+ uiVideoHandler);
+
+ videoContainer = createVideoContainer();
+
+ /*
+ * Our user interface hierarchy has been initialized so we are ready to
+ * begin receiving events warranting updates of this view from its
+ * model.
+ */
+ uiVideoHandler.addObserver(uiVideoHandlerObserver);
+
+ /*
+ * Notify the super that this instance has completed its initialization
+ * and the view that it implements is ready to be updated from the
+ * model.
+ */
+ initializeComplete();
+ }
+
+ private void addConferenceMemberContainers(
+ ConferenceParticipantContainer cpc)
+ {
+ List<ConferenceParticipantContainer> cmcs
+ = cpc.conferenceMemberContainers;
+
+ if ((cmcs != null) && !cmcs.isEmpty())
+ {
+ for (ConferenceParticipantContainer cmc : cmcs)
+ {
+ if (!cmc.toBeRemoved)
+ {
+ videoContainer.add(
+ cmc.getComponent(),
+ VideoLayout.CENTER_REMOTE);
+ }
+ }
+ }
+ }
+
+ private Component createDefaultPhotoPanel(Call call)
+ {
+ OperationSetServerStoredAccountInfo accountInfo
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetServerStoredAccountInfo.class);
+ ImageIcon photoLabelIcon = null;
+
+ if (accountInfo != null)
+ {
+ byte[] accountImage = AccountInfoUtils.getImage(accountInfo);
+
+ // do not set empty images
+ if ((accountImage != null) && (accountImage.length > 0))
+ photoLabelIcon = new ImageIcon(accountImage);
+ }
+ if (photoLabelIcon == null)
+ {
+ photoLabelIcon
+ = new ImageIcon(
+ ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO));
+ }
+
+ return createDefaultPhotoPanel(photoLabelIcon);
+ }
+
+ private Component createDefaultPhotoPanel(CallPeer callPeer)
+ {
+ byte[] peerImage = CallManager.getPeerImage(callPeer);
+ ImageIcon photoLabelIcon
+ = (peerImage == null)
+ ? new ImageIcon(
+ ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO))
+ : new ImageIcon(peerImage);
+
+ return createDefaultPhotoPanel(photoLabelIcon);
+ }
+
+ private Component createDefaultPhotoPanel(ConferenceMember conferenceMember)
+ {
+ return
+ createDefaultPhotoPanel(
+ new ImageIcon(
+ ImageLoader.getImage(
+ ImageLoader.DEFAULT_USER_PHOTO)));
+ }
+
+ /**
+ * Creates a new <tt>Component</tt> which is to display a specific
+ * <tt>ImageIcon</tt> representing the photo of a participant in a call.
+ *
+ * @param photoLabelIcon the <tt>ImageIcon</tt> which represents the photo
+ * of a participant in a call and which is to be displayed by the new
+ * <tt>Component</tt>
+ * @return a new <tt>Component</tt> which displays the specified
+ * <tt>photoLabelIcon</tt>
+ */
+ private Component createDefaultPhotoPanel(ImageIcon photoLabelIcon)
+ {
+ JLabel photoLabel = new JLabel();
+
+ photoLabel.setIcon(photoLabelIcon);
+
+ @SuppressWarnings("serial")
+ JPanel photoPanel
+ = new TransparentPanel(new GridBagLayout())
+ {
+ /**
+ * @{inheritDoc}
+ */
+ @Override
+ public void paintComponent(Graphics g)
+ {
+ super.paintComponent(g);
+
+ g = g.create();
+ try
+ {
+ AntialiasingManager.activateAntialiasing(g);
+
+ g.setColor(Color.GRAY);
+ g.fillRoundRect(
+ 0, 0, this.getWidth(), this.getHeight(),
+ 6, 6);
+ }
+ finally
+ {
+ g.dispose();
+ }
+ }
+ };
+
+ photoPanel.setPreferredSize(new Dimension(320, 240));
+
+ GridBagConstraints photoPanelConstraints = new GridBagConstraints();
+
+ photoPanelConstraints.anchor = GridBagConstraints.CENTER;
+ photoPanelConstraints.fill = GridBagConstraints.NONE;
+ photoPanel.add(photoLabel, photoPanelConstraints);
+
+ return photoPanel;
+ }
+
+ /**
+ * Initializes a new <tt>VideoContainer</tt> instance which is to contain
+ * the visual/video <tt>Component</tt>s of the telephony conference depicted
+ * by this instance.
+ */
+ private VideoContainer createVideoContainer()
+ {
+ VideoContainer videoContainer = new VideoContainer(null, true);
+
+ JPanel thumbnailPanel = new JPanel(new BorderLayout());
+ thumbnailPanel.setBackground(Color.DARK_GRAY);
+ thumbnailPanel.add(thumbnailContainer, BorderLayout.NORTH);
+
+ add(thumbnailPanel, BorderLayout.EAST);
+ add(videoContainer, BorderLayout.CENTER);
+
+ return videoContainer;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dispose()
+ {
+ try
+ {
+ uiVideoHandler.deleteObserver(uiVideoHandlerObserver);
+ }
+ finally
+ {
+ super.dispose();
+ }
+ }
+
+ /**
+ * Determines whether a specific <tt>ConferenceMember</tt> represents the
+ * same conference participant as a specific <tt>CallPeer</tt>. If the
+ * specified <tt>conferenceMember</tt> is <tt>null</tt>, returns
+ * <tt>true</tt>. Otherwise, determines whether the addresses of the
+ * specified <tt>conferenceMember</tt> and the specified <tt>callPeer</tt>
+ * identify one and the same entity.
+ *
+ * @param conferenceMember the <tt>ConferenceMember</tt> to be checked
+ * whether is represents the same conference participant as the specified
+ * <tt>callPeer</tt>. If it is <tt>null</tt>, <tt>true</tt> is returned.
+ * @param callPeer the <tt>CallPeer</tt> to be checked whether it represents
+ * the same conference participant as the specified
+ * <tt>conferenceMember</tt>
+ * @return <tt>true</tt> if the specified <tt>conferenceMember</tt> and the
+ * specified <tt>callPeer</tt> represent the same conference participant or
+ * the specified <tt>conferenceMember</tt> is <tt>null</tt>; otherwise,
+ * <tt>false</tt>
+ */
+ private boolean isConferenceMemberCallPeer(
+ ConferenceMember conferenceMember,
+ CallPeer callPeer)
+ {
+ return
+ (conferenceMember == null)
+ ? true
+ : CallManager.addressesAreEqual(
+ conferenceMember.getAddress(),
+ callPeer.getAddress());
+ }
+
+ /**
+ * Determines whether a specific <tt>ConferenceMember</tt> represents the
+ * local peer/user. Since this instance depicts a whole telephony
+ * conference, the local peer/user may be participating with multiple
+ * <tt>Call</tt>s in it. The <tt>Call</tt>s may be through different
+ * (local) accounts. That's why the implementation determines whether the
+ * address of the specified <tt>conferenceMember</tt> identifies the address
+ * of a (local) accounts involved in the telephony conference depicted by
+ * this instance.
+ *
+ * @param conferenceMember the <tt>ConferenceMember</tt> to be checked
+ * whether it represents the local peer/user
+ * @return <tt>true</tt> if the specified <tt>conferenceMember</tt>
+ * represents the local peer/user; otherwise, <tt>false</tt>
+ */
+ private boolean isConferenceMemberLocalUser(
+ ConferenceMember conferenceMember)
+ {
+ String address = conferenceMember.getAddress();
+
+ for (Call call : callConference.getCalls())
+ {
+ if (CallManager.addressesAreEqual(
+ address,
+ call.getProtocolProvider().getAccountID()
+ .getAccountAddress()))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void removeConferenceMemberContainers(
+ ConferenceParticipantContainer cpc,
+ boolean all)
+ {
+ List<ConferenceParticipantContainer> cmcs
+ = cpc.conferenceMemberContainers;
+
+ if ((cmcs != null) && !cmcs.isEmpty())
+ {
+ Iterator<ConferenceParticipantContainer> i = cmcs.iterator();
+
+ while (i.hasNext())
+ {
+ ConferenceParticipantContainer cmc = i.next();
+
+ if (all || cmc.toBeRemoved)
+ {
+ i.remove();
+
+ videoContainer.remove(cmc.getComponent());
+ cmc.dispose();
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the <tt>ConferenceParticipantContainer</tt>s which depict the
+ * <tt>ConferenceMember</tt>s of the <tt>CallPeer</tt> depicted by a
+ * specific <tt>ConferenceParticipantContainer</tt>.
+ *
+ * @param cpc the <tt>ConferenceParticipantContainer</tt> which depicts the
+ * <tt>CallPeer</tt> whose <tt>ConferenceMember</tt>s are to be depicted
+ * @param videos the visual <tt>Component</tt>s displaying video streaming
+ * from the remote peer (represented by <tt>cpc</tt>) to the local peer/user
+ * @param videoTelephony the <tt>OperationSetVideoTelephony</tt> which
+ * retrieved the specified <tt>videos</tt> from the <tt>CallPeer</tt>
+ * depicted by <tt>cpc</tt>. While the <tt>CallPeer</tt> could be queried
+ * for it, such a query would waste more resources at run time given that
+ * the invoker has it already.
+ */
+ private void updateConferenceMemberContainers(
+ ConferenceParticipantContainer cpc,
+ List<Component> videos,
+ OperationSetVideoTelephony videoTelephony)
+ {
+ CallPeer callPeer = (CallPeer) cpc.getParticipant();
+ List<ConferenceParticipantContainer> cmcs
+ = cpc.conferenceMemberContainers;
+
+ /*
+ * Invalidate all conferenceMemberContainers. Then validate which of
+ * them are to remain and which of them are to really be removed
+ * later on.
+ */
+ if (cmcs != null)
+ {
+ for (ConferenceParticipantContainer cmc : cmcs)
+ cmc.toBeRemoved = true;
+ }
+
+ /*
+ * Depict the remote videos. They may or may not be associated with
+ * ConferenceMembers so the ConferenceMembers which have no
+ * associated videos will be depicted afterwards.
+ */
+ if (videos != null)
+ {
+ Component video = cpc.getVideo();
+
+ for (Component conferenceMemberVideo : videos)
+ {
+ /*
+ * One of the remote videos is already used to depict the
+ * callPeer.
+ */
+ if (conferenceMemberVideo == video)
+ continue;
+
+ boolean addNewConferenceParticipantContainer = true;
+ ConferenceMember conferenceMember
+ = videoTelephony.getConferenceMember(
+ callPeer,
+ conferenceMemberVideo);
+
+ if (cmcs == null)
+ {
+ cmcs = new LinkedList<ConferenceParticipantContainer>();
+ cpc.conferenceMemberContainers = cmcs;
+ }
+ else
+ {
+ for (ConferenceParticipantContainer cmc : cmcs)
+ {
+ Object cmcParticipant = cmc.getParticipant();
+
+ if (conferenceMember == null)
+ {
+ if (cmcParticipant == callPeer)
+ {
+ Component cmcVideo = cmc.getVideo();
+
+ if (cmcVideo == null)
+ {
+ cmc.setVideo(conferenceMemberVideo);
+ cmc.toBeRemoved = false;
+ addNewConferenceParticipantContainer
+ = false;
+ break;
+ }
+ else if (cmcVideo == conferenceMemberVideo)
+ {
+ cmc.toBeRemoved = false;
+ addNewConferenceParticipantContainer
+ = false;
+ break;
+ }
+ }
+ }
+ else if (cmcParticipant == conferenceMember)
+ {
+ cmc.setVideo(conferenceMemberVideo);
+ cmc.toBeRemoved = false;
+ addNewConferenceParticipantContainer = false;
+ break;
+ }
+ }
+ }
+
+ if (addNewConferenceParticipantContainer)
+ {
+ ConferenceParticipantContainer cmc
+ = (conferenceMember == null)
+ ? new ConferenceParticipantContainer(
+ callPeer,
+ conferenceMemberVideo)
+ : new ConferenceParticipantContainer(
+ conferenceMember,
+ conferenceMemberVideo);
+
+ cmcs.add(cmc);
+ }
+ }
+ }
+
+ /*
+ * Depict the ConferenceMembers which have not been depicted yet.
+ * They have no associated videos.
+ */
+ List<ConferenceMember> conferenceMembers
+ = callPeer.getConferenceMembers();
+
+ if (!conferenceMembers.isEmpty())
+ {
+ if (cmcs == null)
+ {
+ cmcs = new LinkedList<ConferenceParticipantContainer>();
+ cpc.conferenceMemberContainers = cmcs;
+ }
+ for (ConferenceMember conferenceMember : conferenceMembers)
+ {
+ /*
+ * If the callPeer reports itself as a ConferenceMember, then
+ * we've already depicted it with cpc.
+ */
+ if (isConferenceMemberCallPeer(conferenceMember, callPeer))
+ continue;
+ /*
+ * If the callPeer reports the local peer/user as a
+ * ConferenceMember, then we've already depicted it.
+ */
+ if (isConferenceMemberLocalUser(conferenceMember))
+ continue;
+
+ boolean addNewConferenceParticipantContainer = true;
+
+ for (ConferenceParticipantContainer cmc : cmcs)
+ {
+ if (cmc.getParticipant() == conferenceMember)
+ {
+ /*
+ * It is possible to have a ConferenceMember who is
+ * sending video but we just do not have the SSRC of
+ * that video to associate the video with the
+ * ConferenceMember. In such a case, we may be depicting
+ * the ConferenceMember twice: once with video without a
+ * ConferenceMember and once with a ConferenceMember
+ * without video. This will surely be the case at the
+ * time of this writing with non-focus participants in a
+ * telephony conference hosted on a Jitsi Videobridge.
+ * Such a display is undesirable. If the
+ * conferenceMember is known to send video, we will not
+ * display it until we associated it with a video. This
+ * way, if a ConferenceMember is not sending video, we
+ * will depict it and we can be sure that no video
+ * without a ConferenceMember association will be
+ * depicting it a second time.
+ */
+ if (cmc.toBeRemoved
+ && !conferenceMember
+ .getVideoStatus()
+ .allowsSending())
+ {
+ cmc.setVideo(null);
+ cmc.toBeRemoved = false;
+ }
+ addNewConferenceParticipantContainer = false;
+ break;
+ }
+ }
+
+ if (addNewConferenceParticipantContainer)
+ {
+ ConferenceParticipantContainer cmc
+ = new ConferenceParticipantContainer(
+ conferenceMember,
+ null);
+
+ cmcs.add(cmc);
+ }
+ }
+ }
+
+ if ((cmcs != null) && !cmcs.isEmpty())
+ {
+ removeConferenceMemberContainers(cpc, false);
+ /*
+ * If cpc is already added to the user interface hierarchy of this
+ * instance, then it was there before the update procedure and it
+ * was determined to be appropriate to continue to depict its model.
+ * Consequently, its Component will be neither added to (because it
+ * was already added) nor removed from the user interface hierarchy
+ * of this instance. That's why we have make sure that the
+ * Components of its conferenceMemberContainers are also added to
+ * the user interface.
+ */
+ if (UIVideoHandler2.isAncestor(this, cpc.getComponent()))
+ addConferenceMemberContainers(cpc);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected ConferenceCallPeerRenderer updateViewFromModel(
+ ConferenceCallPeerRenderer callPeerPanel,
+ CallPeer callPeer)
+ {
+ if (callPeer == null)
+ {
+ /*
+ * The local peer/user will be represented by a Call which has a
+ * CallPeer who provides local video. However, if the user has
+ * selected to hide the local video, the local peer/user will not be
+ * represented at all.
+ */
+ Component video = null;
+
+ if (uiVideoHandler.isLocalVideoVisible())
+ {
+ for (Call aCall : callConference.getCalls())
+ {
+ Iterator<? extends CallPeer> callPeerIter
+ = aCall.getCallPeers();
+ OperationSetVideoTelephony videoTelephony
+ = aCall.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+
+ while (callPeerIter.hasNext())
+ {
+ callPeer = callPeerIter.next();
+
+ if (videoTelephony != null)
+ {
+ try
+ {
+ video
+ = videoTelephony.getLocalVisualComponent(
+ callPeer);
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error(
+ "Failed to retrieve the local video"
+ + " for display",
+ ofe);
+ }
+ if (video != null)
+ break;
+ }
+ }
+ if (video != null)
+ break;
+ }
+ }
+
+ if (callPeer == null)
+ callPeerPanel = null;
+ else
+ {
+ Call call = callPeer.getCall();
+
+ if (callPeerPanel instanceof ConferenceParticipantContainer)
+ {
+ ConferenceParticipantContainer cpc
+ = (ConferenceParticipantContainer) callPeerPanel;
+
+ if (cpc.getParticipant() == call)
+ cpc.setVideo(video);
+ else
+ callPeerPanel = null;
+ }
+ else
+ callPeerPanel = null;
+ if (callPeerPanel == null)
+ {
+ callPeerPanel
+ = new ConferenceParticipantContainer(call, video);
+ }
+ }
+ }
+ else
+ {
+ /*
+ * The specified callPeer will be represented by one of its remote
+ * videos which is not associated with a ConferenceMember or is
+ * associated with a ConferenceMember representing the callPeer
+ * itself.
+ */
+ OperationSetVideoTelephony videoTelephony
+ = callPeer.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+ List<Component> videos = null;
+ Component video = null;
+
+ if (videoTelephony != null)
+ {
+ videos = videoTelephony.getVisualComponents(callPeer);
+ if ((videos != null) && !videos.isEmpty())
+ {
+ for (Component aVideo : videos)
+ {
+ ConferenceMember conferenceMember
+ = videoTelephony.getConferenceMember(
+ callPeer,
+ aVideo);
+
+ if (isConferenceMemberCallPeer(
+ conferenceMember,
+ callPeer))
+ {
+ video = aVideo;
+ break;
+ }
+ }
+ }
+ }
+
+ ConferenceParticipantContainer cpc = null;
+
+ if (callPeerPanel instanceof ConferenceParticipantContainer)
+ {
+ cpc = (ConferenceParticipantContainer) callPeerPanel;
+ if (cpc.getParticipant() == callPeer)
+ cpc.setVideo(video);
+ else
+ cpc = null;
+ }
+ if (cpc == null)
+ cpc = new ConferenceParticipantContainer(callPeer, video);
+ callPeerPanel = cpc;
+
+ // Update the conferenceMemberContainers of the cpc.
+ updateConferenceMemberContainers(cpc, videos, videoTelephony);
+ }
+ return callPeerPanel;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * If {@link #SHOW_TOOLBARS} is <tt>false</tt>, disables the use of
+ * <tt>ConferenceParticipantContainer</tt>. A reason for such a value of
+ * <tt>SHOW_TOOLBARS</tt> may be that the functionality implemented in the
+ * model may not fully support mapping of visual <tt>Component</tt>s
+ * displaying video to telephony conference participants (e.g. in telephony
+ * conferences utilizing the Jitsi Videobridge server-side technology). In
+ * such a case displays the videos only, does not map videos to participants
+ * and does not display participants who do not have videos.
+ */
+ @Override
+ protected void updateViewFromModelInEventDispatchThread()
+ {
+ if (SHOW_TOOLBARS)
+ {
+ super.updateViewFromModelInEventDispatchThread();
+ return;
+ }
+
+ /*
+ * Determine the set of visual Components displaying video streaming
+ * between the local peer/user and the remote peers which are to be
+ * depicted by this instance.
+ */
+ Component localVideo = null;
+ Set<Component> videos = new HashSet<Component>();
+
+ for (Call call : callConference.getCalls())
+ {
+ OperationSetVideoTelephony videoTelephony
+ = call.getProtocolProvider().getOperationSet(
+ OperationSetVideoTelephony.class);
+
+ if (videoTelephony == null)
+ continue;
+
+ Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();
+
+ while (callPeerIter.hasNext())
+ {
+ CallPeer callPeer = callPeerIter.next();
+
+ /*
+ * TODO VideoConferenceCallPanel respects
+ * UIVideoHandler2.isLocalVideoVisible() in order to react to
+ * the associated button at the bottom of the CallPanel.
+ * However, it does not add a close button on top of the local
+ * video in contrast to OneToOneCallPeerPanel. Overall, the
+ * result is questionable.
+ */
+ if (uiVideoHandler.isLocalVideoVisible()
+ && (localVideo == null))
+ {
+ try
+ {
+ localVideo
+ = videoTelephony.getLocalVisualComponent(callPeer);
+ }
+ catch (OperationFailedException ofe)
+ {
+ /*
+ * We'll just try to get the local video through another
+ * CallPeer then.
+ */
+ }
+ if (localVideo != null)
+ videos.add(localVideo);
+ }
+
+ List<Component> callPeerRemoteVideos
+ = videoTelephony.getVisualComponents(callPeer);
+
+ videos.addAll(callPeerRemoteVideos);
+ }
+ }
+
+ /*
+ * Remove the Components of this view which are no longer present in the
+ * model.
+ */
+ Iterator<Component> thisVideoIter = this.videos.iterator();
+
+ while (thisVideoIter.hasNext())
+ {
+ Component thisVideo = thisVideoIter.next();
+
+ if (!videos.contains(thisVideo))
+ {
+ thisVideoIter.remove();
+ videoContainer.remove(thisVideo);
+ }
+
+ /*
+ * If a video is known to be depicted by this view and is still
+ * present in the model, then we could remove it from the set of
+ * videos present in the model in order to prevent going through the
+ * procedure of adding it to this view. However, we choose to play
+ * on the safe side.
+ */
+ }
+
+ /*
+ * Add the Components of the model which are not depicted by this view.
+ */
+ for (Component video : videos)
+ {
+ if (!UIVideoHandler2.isAncestor(videoContainer, video))
+ {
+ this.videos.add(video);
+ videoContainer.add(
+ video,
+ (video == localVideo)
+ ? VideoLayout.LOCAL
+ : VideoLayout.CENTER_REMOTE);
+ }
+ }
+ }
+
+ @Override
+ protected void viewForModelAdded(
+ ConferenceCallPeerRenderer callPeerPanel,
+ CallPeer callPeer)
+ {
+ videoContainer.add(
+ callPeerPanel.getComponent(),
+ VideoLayout.CENTER_REMOTE);
+ if ((callPeer != null)
+ && (callPeerPanel instanceof ConferenceParticipantContainer))
+ {
+ addConferenceMemberContainers(
+ (ConferenceParticipantContainer) callPeerPanel);
+ }
+ }
+
+ @Override
+ protected void viewForModelRemoved(
+ ConferenceCallPeerRenderer callPeerPanel,
+ CallPeer callPeer)
+ {
+ videoContainer.remove(callPeerPanel.getComponent());
+ if ((callPeer != null)
+ && (callPeerPanel instanceof ConferenceParticipantContainer))
+ {
+ removeConferenceMemberContainers(
+ (ConferenceParticipantContainer) callPeerPanel,
+ true);
+ }
+ }
+
+ /**
+ * Implements an AWT <tt>Component</tt> which contains the user interface
+ * elements depicting a specific participant in the telephony conference
+ * depicted by a <tt>VideoConferenceCallPanel</tt>.
+ */
+ private class ConferenceParticipantContainer
+ extends TransparentPanel
+ implements ConferenceCallPeerRenderer
+ {
+ /**
+ * The list of <tt>ConferenceParticipantContainer</tt>s which represent
+ * the <tt>ConferenceMember</tt>s of the participant represented by this
+ * instance. Since a <tt>CallPeer</tt> may send the local peer/user
+ * multiple videos without providing a way to associate a
+ * ConferenceMember with each one of them, the list may contain
+ * <tt>ConferenceParticipantContainer</tt>s which do not represent a
+ * specific <tt>ConferenceMember</tt> instance but rather a video sent
+ * by a <tt>CallPeer</tt> to the local peer/user which looks like (in
+ * the terms of <tt>VideoConferenceCallPanel) a member of a conference
+ * organized by the <tt>CallPeer</tt> in question.
+ * <p>
+ * Implements a state which is private to
+ * <tt>VideoConferenceCallPanel</tt> and is of no concern to
+ * <tt>ConferenceParticipantContainer</tt>.
+ * </p>
+ */
+ List<ConferenceParticipantContainer> conferenceMemberContainers;
+
+ /**
+ * The indicator which determines whether this instance is to be removed
+ * because it has become out-of-date, obsolete, unnecessary.
+ * <p>
+ * Implements a state which is private to
+ * <tt>VideoConferenceCallPanel</tt> and is of no concern to
+ * <tt>ConferenceParticipantContainer</tt>.
+ * </p>
+ */
+ boolean toBeRemoved;
+
+ /**
+ * The <tt>BasicConferenceParticipantPanel</tt> which is displayed at
+ * the bottom of this instance, bellow the {@link #video} (i.e.
+ * {@link #videoContainer}) and is referred to as the tool bar.
+ */
+ private final BasicConferenceParticipantPanel<?> toolBar;
+
+ /**
+ * The visual <tt>Component</tt>, if any, displaying video which is
+ * depicted by this instance.
+ */
+ private Component video;
+
+ /**
+ * The <tt>VideoContainer</tt> which lays out the video depicted by this
+ * instance i.e. {@link #video}.
+ */
+ private final VideoContainer videoContainer;
+
+ /**
+ * The <tt>CallPeer</tt> associated with this container, if it has been
+ * created to represent a <tt>CallPeer</tt>.
+ */
+ private CallPeer callPeer;
+
+ /**
+ * The <tt>conferenceMember</tt> associated with this container, if it
+ * has been created to represent a <tt>conferenceMember</tt>.
+ */
+ private ConferenceMember conferenceMember;
+
+ /**
+ * Indicates that this container contains information for the local
+ * user.
+ */
+ private boolean isLocalUser;
+
+ /**
+ * Initializes a new <tt>ConferenceParticipantContainer</tt> instance
+ * which is to depict the local peer/user.
+ *
+ * @param call a <tt>Call</tt> which is to provide information about the
+ * local peer/user
+ * @param video the visual <tt>Component</tt>, if any, displaying the
+ * video streaming from the local peer/user to the remote peer(s)
+ */
+ public ConferenceParticipantContainer(Call call, Component video)
+ {
+ this(
+ createDefaultPhotoPanel(call),
+ video,
+ new ConferencePeerPanel(
+ VideoConferenceCallPanel.this,
+ call,
+ true),
+ null, null, true);
+ }
+
+ public ConferenceParticipantContainer(
+ CallPeer callPeer,
+ Component video)
+ {
+ this( createDefaultPhotoPanel(callPeer),
+ video,
+ new ConferencePeerPanel(
+ VideoConferenceCallPanel.this,
+ callPeer,
+ true),
+ callPeer, null, false);
+ }
+
+ private ConferenceParticipantContainer(
+ Component noVideo,
+ Component video,
+ BasicConferenceParticipantPanel<?> toolBar,
+ CallPeer callPeer,
+ ConferenceMember conferenceMember,
+ boolean isLocalUser)
+ {
+ super(new BorderLayout());
+
+ this.callPeer = callPeer;
+ this.conferenceMember = conferenceMember;
+ this.isLocalUser = isLocalUser;
+
+ videoContainer = new VideoContainer(noVideo, false);
+ add(videoContainer, BorderLayout.CENTER);
+
+ this.toolBar = toolBar;
+ if (this.toolBar != null)
+ add(this.toolBar, BorderLayout.SOUTH);
+
+ if (video != null)
+ {
+ setVideo(video);
+ }
+ else
+ setVisible(false);
+ }
+
+ public ConferenceParticipantContainer(
+ ConferenceMember conferenceMember,
+ Component video)
+ {
+ this(
+ createDefaultPhotoPanel(conferenceMember),
+ video,
+ new ConferenceMemberPanel(
+ VideoConferenceCallPanel.this,
+ conferenceMember,
+ true),
+ null, conferenceMember, false);
+ }
+
+ public void dispose()
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.dispose();
+
+ // Dispose of the conferenceMemberContainers if any.
+ /*
+ * XXX The field conferenceMemberContainers implements a state
+ * private to VideoConferenceCallPanel which the latter makes sure
+ * to access on the AWT event dispatching thread only. Since we are
+ * going out of our way here to help VideoConferenceCallPanel,
+ * ensure that the mentioned synchronization rule is not violated.
+ */
+ CallManager.assertIsEventDispatchingThread();
+ if (conferenceMemberContainers != null)
+ {
+ for (ConferenceParticipantContainer cmc
+ : conferenceMemberContainers)
+ {
+ cmc.dispose();
+ }
+ }
+ }
+
+ public CallPanel getCallPanel()
+ {
+ return getCallRenderer().getCallContainer();
+ }
+
+ public SwingCallRenderer getCallRenderer()
+ {
+ return VideoConferenceCallPanel.this;
+ }
+
+ public Component getComponent()
+ {
+ return this;
+ }
+
+ private ConferenceCallPeerRenderer
+ getConferenceCallPeerRendererDelegate()
+ {
+ return
+ (toolBar instanceof ConferenceCallPeerRenderer)
+ ? (ConferenceCallPeerRenderer) toolBar
+ : null;
+ }
+
+ /**
+ * Gets the conference participant depicted by this instance.
+ *
+ * @return the conference participant depicted by this instance
+ */
+ public Object getParticipant()
+ {
+ return (toolBar == null) ? null : toolBar.getParticipant();
+ }
+
+ public Component getVideo()
+ {
+ return video;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only. Otherwise, returns <tt>false</tt>.
+ */
+ public boolean isLocalVideoVisible()
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ return (delegate == null) ? false : delegate.isLocalVideoVisible();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void printDTMFTone(char dtmfChar)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.printDTMFTone(dtmfChar);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void securityNegotiationStarted(
+ CallPeerSecurityNegotiationStartedEvent ev)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.securityNegotiationStarted(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void securityOff(CallPeerSecurityOffEvent ev)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.securityOff(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void securityOn(CallPeerSecurityOnEvent ev)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.securityOn(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void securityPending()
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.securityPending();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void securityTimeout(CallPeerSecurityTimeoutEvent ev)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.securityTimeout(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setErrorReason(String reason)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setErrorReason(reason);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setLocalVideoVisible(boolean visible)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setLocalVideoVisible(visible);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setMute(boolean mute)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setMute(mute);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setOnHold(boolean onHold)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setOnHold(onHold);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setPeerImage(byte[] image)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setPeerImage(image);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setPeerName(String name)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setPeerName(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setPeerState(
+ CallPeerState oldState,
+ CallPeerState newState,
+ String stateString)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setPeerState(oldState, newState, stateString);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegates to the <tt>toolBar</tt>, if the latter implements
+ * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
+ * container only.
+ */
+ public void setSecurityPanelVisible(boolean visible)
+ {
+ ConferenceCallPeerRenderer delegate
+ = getConferenceCallPeerRendererDelegate();
+
+ if (delegate != null)
+ delegate.setSecurityPanelVisible(visible);
+ }
+
+ /**
+ * Sets the visual <tt>Component</tt> displaying the video associated
+ * with the participant depicted by this instance.
+ *
+ * @param video the visual <tt>Component</tt> displaying video which is
+ * to be associated with the participant depicted by this instance
+ */
+ void setVideo(Component video)
+ {
+ if (this.video != video)
+ {
+ if (this.video != null)
+ videoContainer.remove(this.video);
+
+ this.video = video;
+
+ if (this.video != null)
+ {
+ setVisible(true);
+ videoContainer.add(this.video, VideoLayout.CENTER_REMOTE);
+ }
+ else
+ setVisible(false);
+
+ // Update thumbnails container according to video status.
+ if (thumbnailContainer != null)
+ {
+ if (conferenceMember != null)
+ thumbnailContainer
+ .updateThumbnail(conferenceMember, (video != null));
+ else if (callPeer != null)
+ thumbnailContainer
+ .updateThumbnail(callPeer, (video != null));
+ else if (isLocalUser)
+ thumbnailContainer
+ .updateThumbnail((video != null));
+ }
+ }
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/libjitsi/LibJitsiActivator.java b/src/net/java/sip/communicator/impl/libjitsi/LibJitsiActivator.java index a2b21cf..0a1833e 100644 --- a/src/net/java/sip/communicator/impl/libjitsi/LibJitsiActivator.java +++ b/src/net/java/sip/communicator/impl/libjitsi/LibJitsiActivator.java @@ -1,60 +1,60 @@ -/* - * 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.impl.libjitsi; - -import java.lang.reflect.*; - -import org.jitsi.service.libjitsi.*; -import org.osgi.framework.*; - -public class LibJitsiActivator - implements BundleActivator -{ - public void start(BundleContext bundleContext) - throws Exception - { - /* - * XXX To start/initialize the libjitsi library, simply call - * LibJitsi#start(). The following is a temporary workaround for the - * benefit of the Jitsi VideoBridge project (which uses Jitsi's libjitsi - * bundle and runs on an incomplete OSGi implementation) and not the - * Jitsi project. - */ - Method start; - - try - { - start = LibJitsi.class.getDeclaredMethod("start", Object.class); - if (Modifier.isStatic(start.getModifiers())) - { - start.setAccessible(true); - if (!start.isAccessible()) - start = null; - } - else - start = null; - } - catch (NoSuchMethodException nsme) - { - start = null; - } - catch (SecurityException se) - { - start = null; - } - if (start == null) - LibJitsi.start(); - else - start.invoke(null, bundleContext); - } - - public void stop(BundleContext bundleContext) - throws Exception - { - LibJitsi.stop(); - } -} +/*
+ * 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.impl.libjitsi;
+
+import java.lang.reflect.*;
+
+import org.jitsi.service.libjitsi.*;
+import org.osgi.framework.*;
+
+public class LibJitsiActivator
+ implements BundleActivator
+{
+ public void start(BundleContext bundleContext)
+ throws Exception
+ {
+ /*
+ * XXX To start/initialize the libjitsi library, simply call
+ * LibJitsi#start(). The following is a temporary workaround for the
+ * benefit of the Jitsi Videobridge project (which uses Jitsi's libjitsi
+ * bundle and runs on an incomplete OSGi implementation) and not the
+ * Jitsi project.
+ */
+ Method start;
+
+ try
+ {
+ start = LibJitsi.class.getDeclaredMethod("start", Object.class);
+ if (Modifier.isStatic(start.getModifiers()))
+ {
+ start.setAccessible(true);
+ if (!start.isAccessible())
+ start = null;
+ }
+ else
+ start = null;
+ }
+ catch (NoSuchMethodException nsme)
+ {
+ start = null;
+ }
+ catch (SecurityException se)
+ {
+ start = null;
+ }
+ if (start == null)
+ LibJitsi.start();
+ else
+ start.invoke(null, bundleContext);
+ }
+
+ public void stop(BundleContext bundleContext)
+ throws Exception
+ {
+ LibJitsi.stop();
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java index 233e92c..976baae 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/AbstractCallPeerMediaHandlerJabberGTalkImpl.java @@ -1,604 +1,604 @@ -/* - * 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.impl.protocol.jabber; - -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; - -import ch.imvs.sdes4j.srtp.*; - -/** - * An implementation of the <tt>CallPeerMediaHandler</tt> abstract class for the - * common part of Jabber and Gtalk protocols. - * - * @author Vincent Lucas - * @author Lyubomir Marinov - */ -public abstract class AbstractCallPeerMediaHandlerJabberGTalkImpl - <T extends AbstractCallPeerJabberGTalkImpl<?,?,?>> - extends CallPeerMediaHandler<T> -{ - /** - * The <tt>Logger</tt> used by the <tt>CallPeerMediaHandlerJabberImpl</tt> - * class and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(AbstractCallPeerMediaHandlerJabberGTalkImpl.class); - - /** - * Indicates if the <tt>CallPeer</tt> will support </tt>inputevt</tt> - * extension (i.e. will be able to be remote-controlled). - */ - private boolean localInputEvtAware = false; - - /** - * Creates a new handler that will be managing media streams for - * <tt>peer</tt>. - * - * @param peer that <tt>AbstractCallPeerJabberGTalkImpl</tt> instance that - * we will be managing media for. - */ - public AbstractCallPeerMediaHandlerJabberGTalkImpl(T peer) - { - super(peer, peer); - } - - /** - * Gets the <tt>inputevt</tt> support: true for enable, false for disable. - * - * @return The state of inputevt support: true for enable, false for - * disable. - */ - public boolean getLocalInputEvtAware() - { - return this.localInputEvtAware; - } - - /** - * Enable or disable <tt>inputevt</tt> support (remote-control). - * - * @param enable new state of inputevt support - */ - public void setLocalInputEvtAware(boolean enable) - { - localInputEvtAware = enable; - } - - /** - * Detects and adds ZRTP available encryption method present in the - * description given in parameter. - * - * @param isInitiator True if the local call instance is the initiator of - * the call. False otherwise. - * @param description The DESCRIPTION element of the JINGLE element which - * contains the PAYLOAD-TYPE and (more important here) the ENCRYPTION. - * @param mediaType The type of media (AUDIO or VIDEO). - */ - protected void addZrtpAdvertisedEncryptions( - boolean isInitiator, - RtpDescriptionPacketExtension description, - MediaType mediaType) - { - CallPeer peer = getPeer(); - Call call = peer.getCall(); - - /* - * ZRTP is not supported in telephony conferences utilizing the - * server-side technology Jitsi Videobridge yet. - */ - if (call.getConference().isJitsiVideoBridge()) - return; - - // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element for - // a given DESCRIPTION. - EncryptionPacketExtension encryptionPacketExtension - = description.getFirstChildOfType( - EncryptionPacketExtension.class); - - if(encryptionPacketExtension != null) - { - AccountID accountID = peer.getProtocolProvider().getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - ZrtpControl.PROTO_NAME) - && call.isSipZrtpAttribute()) - { - // ZRTP - ZrtpHashPacketExtension zrtpHashPacketExtension - = encryptionPacketExtension.getFirstChildOfType( - ZrtpHashPacketExtension.class); - - if ((zrtpHashPacketExtension != null) - && (zrtpHashPacketExtension.getValue() != null)) - { - addAdvertisedEncryptionMethod(SrtpControlType.ZRTP); - } - } - } - } - - /** - * Detects and adds SDES available encryption method present in the - * description given in parameter. - * - * @param isInitiator True if the local call instance is the initiator of - * the call. False otherwise. - * @param description The DESCRIPTION element of the JINGLE element which - * contains the PAYLOAD-TYPE and (more important here) the ENCRYPTION. - * @param mediaType The type of media (AUDIO or VIDEO). - */ - protected void addSDesAdvertisedEncryptions( - boolean isInitiator, - RtpDescriptionPacketExtension description, - MediaType mediaType) - { - CallPeer peer = getPeer(); - - /* - * SDES is not supported in telephony conferences utilizing the - * server-side technology Jitsi Videobridge yet. - */ - if (peer.getCall().getConference().isJitsiVideoBridge()) - return; - - // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element for - // a given DESCRIPTION. - EncryptionPacketExtension encryptionPacketExtension - = description.getFirstChildOfType( - EncryptionPacketExtension.class); - - if(encryptionPacketExtension != null) - { - AccountID accountID = peer.getProtocolProvider().getAccountID(); - - // SDES - if(accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - SDesControl.PROTO_NAME)) - { - SrtpControls srtpControls = getSrtpControls(); - SDesControl sdesControl - = (SDesControl) - srtpControls.getOrCreate( - mediaType, - SrtpControlType.SDES); - SrtpCryptoAttribute selectedSdes - = selectSdesCryptoSuite( - isInitiator, - sdesControl, - encryptionPacketExtension); - - if(selectedSdes != null) - { - //found an SDES answer, remove all other controls - removeAndCleanupOtherSrtpControls( - mediaType, - SrtpControlType.SDES); - addAdvertisedEncryptionMethod(SrtpControlType.SDES); - } - else - { - sdesControl.cleanup(); - srtpControls.remove(mediaType, SrtpControlType.SDES); - } - } - } - // If we were initiating the encryption, and the remote peer does not - // manage it, then we must remove the unusable SDES srtpControl. - else if(isInitiator) - { - // SDES - SrtpControl sdesControl - = getSrtpControls().remove(mediaType, SrtpControlType.SDES); - - if (sdesControl != null) - sdesControl.cleanup(); - } - } - - /** - * Returns the selected SDES crypto suite selected. - * - * @param isInitiator True if the local call instance is the initiator of - * the call. False otherwise. - * @param sDesControl The SDES based SRTP MediaStream encryption - * control. - * @param encryptionPacketExtension The ENCRYPTION element received from the - * remote peer. This may contain the SDES crypto suites available for the - * remote peer. - * - * @return The selected SDES crypto suite supported by both the local and - * the remote peer. Or null, if there is no crypto suite supported by both - * of the peers. - */ - protected SrtpCryptoAttribute selectSdesCryptoSuite( - boolean isInitiator, - SDesControl sDesControl, - EncryptionPacketExtension encryptionPacketExtension) - { - List<CryptoPacketExtension> cryptoPacketExtensions - = encryptionPacketExtension.getCryptoList(); - List<SrtpCryptoAttribute> peerAttributes - = new ArrayList<SrtpCryptoAttribute>(cryptoPacketExtensions.size()); - - for (CryptoPacketExtension cpe : cryptoPacketExtensions) - peerAttributes.add(cpe.toSrtpCryptoAttribute()); - - return - isInitiator - ? sDesControl.initiatorSelectAttribute(peerAttributes) - : sDesControl.responderSelectAttribute(peerAttributes); - } - - /** - * Returns if the remote peer supports ZRTP. - * - * @param encryptionPacketExtension The ENCRYPTION element received from - * the remote peer. This may contain the ZRTP packet element for the remote - * peer. - * - * @return True if the remote peer supports ZRTP. False, otherwise. - */ - protected boolean isRemoteZrtpCapable( - EncryptionPacketExtension encryptionPacketExtension) - { - return - (encryptionPacketExtension.getFirstChildOfType( - ZrtpHashPacketExtension.class) - != null); - } - - /** - * Sets ZRTP element to the ENCRYPTION element of the DESCRIPTION for a - * given media. - * - * @param mediaType The type of media we are modifying the DESCRIPTION to - * integrate the ENCRYPTION element. - * @param description The element containing the media DESCRIPTION and its - * encryption. - * @param remoteDescription The element containing the media DESCRIPTION and - * its encryption for the remote peer. Null, if the local peer is the - * initiator of the call. - * - * @return True if the ZRTP element has been added to encryption. False, - * otherwise. - */ - protected boolean setZrtpEncryptionToDescription( - MediaType mediaType, - RtpDescriptionPacketExtension description, - RtpDescriptionPacketExtension remoteDescription) - { - CallPeer peer = getPeer(); - Call call = peer.getCall(); - - /* - * ZRTP is not supported in telephony conferences utilizing the - * server-side technology Jitsi Videobridge yet. - */ - if (call.getConference().isJitsiVideoBridge()) - return false; - - boolean isRemoteZrtpCapable; - - if (remoteDescription == null) - isRemoteZrtpCapable = true; - else - { - // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element - // for a given DESCRIPTION. - EncryptionPacketExtension remoteEncryption - = remoteDescription.getFirstChildOfType( - EncryptionPacketExtension.class); - - isRemoteZrtpCapable - = (remoteEncryption != null) - && isRemoteZrtpCapable(remoteEncryption); - } - - boolean zrtpHashSet = false; // Will become true if at least one is set. - - if (isRemoteZrtpCapable) - { - AccountID accountID = peer.getProtocolProvider().getAccountID(); - - if(accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - ZrtpControl.PROTO_NAME) - && call.isSipZrtpAttribute()) - { - ZrtpControl zrtpControl - = (ZrtpControl) - getSrtpControls().getOrCreate( - mediaType, - SrtpControlType.ZRTP); - int numberSupportedVersions - = zrtpControl.getNumberSupportedVersions(); - - for (int i = 0; i < numberSupportedVersions; i++) - { - String helloHash[] = zrtpControl.getHelloHashSep(i); - - if ((helloHash != null) && (helloHash[1].length() > 0)) - { - ZrtpHashPacketExtension hash - = new ZrtpHashPacketExtension(); - - hash.setVersion(helloHash[0]); - hash.setValue(helloHash[1]); - - EncryptionPacketExtension encryption - = description.getFirstChildOfType( - EncryptionPacketExtension.class); - - if (encryption == null) - { - encryption = new EncryptionPacketExtension(); - description.addChildExtension(encryption); - } - encryption.addChildExtension(hash); - zrtpHashSet = true; - } - } - } - } - - return zrtpHashSet; - } - - /** - * Sets SDES element(s) to the ENCRYPTION element of the DESCRIPTION for a - * given media. - * - * @param mediaType The type of media we are modifying the DESCRIPTION to - * integrate the ENCRYPTION element. - * @param localDescription The element containing the media DESCRIPTION and - * its encryption. - * @param remoteDescription The element containing the media DESCRIPTION and - * its encryption for the remote peer. Null, if the local peer is the - * initiator of the call. - * - * @return True if the crypto element has been added to encryption. False, - * otherwise. - */ - protected boolean setSDesEncryptionToDescription( - MediaType mediaType, - RtpDescriptionPacketExtension localDescription, - RtpDescriptionPacketExtension remoteDescription) - { - CallPeer peer = getPeer(); - - /* - * SDES is not supported in telephony conferences utilizing the - * server-side technology Jitsi Videobridge yet. - */ - if (peer.getCall().getConference().isJitsiVideoBridge()) - return false; - - AccountID accountID = peer.getProtocolProvider().getAccountID(); - - // check if SDES and encryption is enabled at all - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - SDesControl.PROTO_NAME)) - { - // get or create the control - SrtpControls srtpControls = getSrtpControls(); - SDesControl sdesControl - = (SDesControl) - srtpControls.getOrCreate(mediaType, SrtpControlType.SDES); - // set the enabled ciphers suites - String ciphers - = accountID.getAccountPropertyString( - ProtocolProviderFactory.SDES_CIPHER_SUITES); - - if (ciphers == null) - { - ciphers = - JabberActivator.getResources().getSettingsString( - SDesControl.SDES_CIPHER_SUITES); - } - sdesControl.setEnabledCiphers(Arrays.asList(ciphers.split(","))); - - // act as initiator - if (remoteDescription == null) - { - EncryptionPacketExtension localEncryption - = localDescription.getFirstChildOfType( - EncryptionPacketExtension.class); - - if(localEncryption == null) - { - localEncryption = new EncryptionPacketExtension(); - localDescription.addChildExtension(localEncryption); - } - for(SrtpCryptoAttribute ca: - sdesControl.getInitiatorCryptoAttributes()) - { - CryptoPacketExtension crypto - = new CryptoPacketExtension(ca); - localEncryption.addChildExtension(crypto); - } - - return true; - } - // act as responder - else - { - // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION - // element for a given DESCRIPTION. - EncryptionPacketExtension remoteEncryption - = remoteDescription.getFirstChildOfType( - EncryptionPacketExtension.class); - - if(remoteEncryption != null) - { - SrtpCryptoAttribute selectedSdes = selectSdesCryptoSuite( - false, - sdesControl, - remoteEncryption); - - if(selectedSdes != null) - { - EncryptionPacketExtension localEncryption - = localDescription.getFirstChildOfType( - EncryptionPacketExtension.class); - - if(localEncryption == null) - { - localEncryption = new EncryptionPacketExtension(); - localDescription.addChildExtension(localEncryption); - } - - CryptoPacketExtension crypto - = new CryptoPacketExtension(selectedSdes); - - localEncryption.addChildExtension(crypto); - - return true; - } - else - { - // none of the offered suites match, destroy the sdes - // control - sdesControl.cleanup(); - srtpControls.remove(mediaType, SrtpControlType.SDES); - logger.warn( - "Received unsupported sdes crypto attribute"); - } - } - else - { - // peer doesn't offer any SDES attribute, destroy the sdes - // control - sdesControl.cleanup(); - srtpControls.remove(mediaType, SrtpControlType.SDES); - } - } - } - - return false; - } - - /** - * Selects the preferred encryption protocol (only used by the callee). - * - * @param mediaType The type of media (AUDIO or VIDEO). - * @param localDescription The element containing the media DESCRIPTION and - * its encryption. - * @param remoteDescription The element containing the media DESCRIPTION and - * its encryption for the remote peer; <tt>null</tt> if the local peer is - * the initiator of the call. - */ - protected void setAndAddPreferredEncryptionProtocol( - MediaType mediaType, - RtpDescriptionPacketExtension localDescription, - RtpDescriptionPacketExtension remoteDescription) - { - // Sets ZRTP or SDES, depending on the preferences for this account. - List<String> preferredEncryptionProtocols - = getPeer() - .getProtocolProvider() - .getAccountID() - .getSortedEnabledEncryptionProtocolList(); - - for(String preferredEncryptionProtocol : preferredEncryptionProtocols) - { - String protoName - = preferredEncryptionProtocol.substring( - ProtocolProviderFactory.ENCRYPTION_PROTOCOL.length() - + 1); - - if (setAndAddPreferredEncryptionProtocol( - protoName, - mediaType, - localDescription, - remoteDescription)) - { - // Stop once an encryption advertisement has been chosen. - return; - } - } - } - - /** - * Selects a specific encryption protocol if it is the preferred (only used - * by the callee). - * - * @param protoName the name of the encryption protocol which is to be - * selected - * @param mediaType The type of media (AUDIO or VIDEO). - * @param localDescription The element containing the media DESCRIPTION and - * its encryption. - * @param remoteDescription The element containing the media DESCRIPTION and - * its encryption for the remote peer; <tt>null</tt> if the local peer is - * the initiator of the call. - * @return <tt>true</tt> if the specified encryption protocol has been - * selected; <tt>false</tt>, otherwise - */ - protected boolean setAndAddPreferredEncryptionProtocol( - String protoName, - MediaType mediaType, - RtpDescriptionPacketExtension localDescription, - RtpDescriptionPacketExtension remoteDescription) - { - /* - * Neither SDES nor ZRTP is supported in telephony conferences utilizing - * the server-side technology Jitsi Videobridge yet. - */ - if (getPeer().isJitsiVideoBridge()) - return false; - - // SDES - if(SDesControl.PROTO_NAME.equals(protoName)) - { - addSDesAdvertisedEncryptions( - false, - remoteDescription, - mediaType); - if(setSDesEncryptionToDescription( - mediaType, - localDescription, - remoteDescription)) - { - // Stop once an encryption advertisement has been chosen. - return true; - } - } - // ZRTP - else if(ZrtpControl.PROTO_NAME.equals(protoName)) - { - if(setZrtpEncryptionToDescription( - mediaType, - localDescription, - remoteDescription)) - { - addZrtpAdvertisedEncryptions( - false, - remoteDescription, - mediaType); - // Stop once an encryption advertisement has been chosen. - return true; - } - } - return false; - } -} +/*
+ * 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.impl.protocol.jabber;
+
+import java.util.*;
+
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.media.*;
+import net.java.sip.communicator.util.*;
+
+import org.jitsi.service.neomedia.*;
+
+import ch.imvs.sdes4j.srtp.*;
+
+/**
+ * An implementation of the <tt>CallPeerMediaHandler</tt> abstract class for the
+ * common part of Jabber and Gtalk protocols.
+ *
+ * @author Vincent Lucas
+ * @author Lyubomir Marinov
+ */
+public abstract class AbstractCallPeerMediaHandlerJabberGTalkImpl
+ <T extends AbstractCallPeerJabberGTalkImpl<?,?,?>>
+ extends CallPeerMediaHandler<T>
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>CallPeerMediaHandlerJabberImpl</tt>
+ * class and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(AbstractCallPeerMediaHandlerJabberGTalkImpl.class);
+
+ /**
+ * Indicates if the <tt>CallPeer</tt> will support </tt>inputevt</tt>
+ * extension (i.e. will be able to be remote-controlled).
+ */
+ private boolean localInputEvtAware = false;
+
+ /**
+ * Creates a new handler that will be managing media streams for
+ * <tt>peer</tt>.
+ *
+ * @param peer that <tt>AbstractCallPeerJabberGTalkImpl</tt> instance that
+ * we will be managing media for.
+ */
+ public AbstractCallPeerMediaHandlerJabberGTalkImpl(T peer)
+ {
+ super(peer, peer);
+ }
+
+ /**
+ * Gets the <tt>inputevt</tt> support: true for enable, false for disable.
+ *
+ * @return The state of inputevt support: true for enable, false for
+ * disable.
+ */
+ public boolean getLocalInputEvtAware()
+ {
+ return this.localInputEvtAware;
+ }
+
+ /**
+ * Enable or disable <tt>inputevt</tt> support (remote-control).
+ *
+ * @param enable new state of inputevt support
+ */
+ public void setLocalInputEvtAware(boolean enable)
+ {
+ localInputEvtAware = enable;
+ }
+
+ /**
+ * Detects and adds ZRTP available encryption method present in the
+ * description given in parameter.
+ *
+ * @param isInitiator True if the local call instance is the initiator of
+ * the call. False otherwise.
+ * @param description The DESCRIPTION element of the JINGLE element which
+ * contains the PAYLOAD-TYPE and (more important here) the ENCRYPTION.
+ * @param mediaType The type of media (AUDIO or VIDEO).
+ */
+ protected void addZrtpAdvertisedEncryptions(
+ boolean isInitiator,
+ RtpDescriptionPacketExtension description,
+ MediaType mediaType)
+ {
+ CallPeer peer = getPeer();
+ Call call = peer.getCall();
+
+ /*
+ * ZRTP is not supported in telephony conferences utilizing the
+ * server-side technology Jitsi Videobridge yet.
+ */
+ if (call.getConference().isJitsiVideobridge())
+ return;
+
+ // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element for
+ // a given DESCRIPTION.
+ EncryptionPacketExtension encryptionPacketExtension
+ = description.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if(encryptionPacketExtension != null)
+ {
+ AccountID accountID = peer.getProtocolProvider().getAccountID();
+
+ if (accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.DEFAULT_ENCRYPTION,
+ true)
+ && accountID.isEncryptionProtocolEnabled(
+ ZrtpControl.PROTO_NAME)
+ && call.isSipZrtpAttribute())
+ {
+ // ZRTP
+ ZrtpHashPacketExtension zrtpHashPacketExtension
+ = encryptionPacketExtension.getFirstChildOfType(
+ ZrtpHashPacketExtension.class);
+
+ if ((zrtpHashPacketExtension != null)
+ && (zrtpHashPacketExtension.getValue() != null))
+ {
+ addAdvertisedEncryptionMethod(SrtpControlType.ZRTP);
+ }
+ }
+ }
+ }
+
+ /**
+ * Detects and adds SDES available encryption method present in the
+ * description given in parameter.
+ *
+ * @param isInitiator True if the local call instance is the initiator of
+ * the call. False otherwise.
+ * @param description The DESCRIPTION element of the JINGLE element which
+ * contains the PAYLOAD-TYPE and (more important here) the ENCRYPTION.
+ * @param mediaType The type of media (AUDIO or VIDEO).
+ */
+ protected void addSDesAdvertisedEncryptions(
+ boolean isInitiator,
+ RtpDescriptionPacketExtension description,
+ MediaType mediaType)
+ {
+ CallPeer peer = getPeer();
+
+ /*
+ * SDES is not supported in telephony conferences utilizing the
+ * server-side technology Jitsi Videobridge yet.
+ */
+ if (peer.getCall().getConference().isJitsiVideobridge())
+ return;
+
+ // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element for
+ // a given DESCRIPTION.
+ EncryptionPacketExtension encryptionPacketExtension
+ = description.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if(encryptionPacketExtension != null)
+ {
+ AccountID accountID = peer.getProtocolProvider().getAccountID();
+
+ // SDES
+ if(accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.DEFAULT_ENCRYPTION,
+ true)
+ && accountID.isEncryptionProtocolEnabled(
+ SDesControl.PROTO_NAME))
+ {
+ SrtpControls srtpControls = getSrtpControls();
+ SDesControl sdesControl
+ = (SDesControl)
+ srtpControls.getOrCreate(
+ mediaType,
+ SrtpControlType.SDES);
+ SrtpCryptoAttribute selectedSdes
+ = selectSdesCryptoSuite(
+ isInitiator,
+ sdesControl,
+ encryptionPacketExtension);
+
+ if(selectedSdes != null)
+ {
+ //found an SDES answer, remove all other controls
+ removeAndCleanupOtherSrtpControls(
+ mediaType,
+ SrtpControlType.SDES);
+ addAdvertisedEncryptionMethod(SrtpControlType.SDES);
+ }
+ else
+ {
+ sdesControl.cleanup();
+ srtpControls.remove(mediaType, SrtpControlType.SDES);
+ }
+ }
+ }
+ // If we were initiating the encryption, and the remote peer does not
+ // manage it, then we must remove the unusable SDES srtpControl.
+ else if(isInitiator)
+ {
+ // SDES
+ SrtpControl sdesControl
+ = getSrtpControls().remove(mediaType, SrtpControlType.SDES);
+
+ if (sdesControl != null)
+ sdesControl.cleanup();
+ }
+ }
+
+ /**
+ * Returns the selected SDES crypto suite selected.
+ *
+ * @param isInitiator True if the local call instance is the initiator of
+ * the call. False otherwise.
+ * @param sDesControl The SDES based SRTP MediaStream encryption
+ * control.
+ * @param encryptionPacketExtension The ENCRYPTION element received from the
+ * remote peer. This may contain the SDES crypto suites available for the
+ * remote peer.
+ *
+ * @return The selected SDES crypto suite supported by both the local and
+ * the remote peer. Or null, if there is no crypto suite supported by both
+ * of the peers.
+ */
+ protected SrtpCryptoAttribute selectSdesCryptoSuite(
+ boolean isInitiator,
+ SDesControl sDesControl,
+ EncryptionPacketExtension encryptionPacketExtension)
+ {
+ List<CryptoPacketExtension> cryptoPacketExtensions
+ = encryptionPacketExtension.getCryptoList();
+ List<SrtpCryptoAttribute> peerAttributes
+ = new ArrayList<SrtpCryptoAttribute>(cryptoPacketExtensions.size());
+
+ for (CryptoPacketExtension cpe : cryptoPacketExtensions)
+ peerAttributes.add(cpe.toSrtpCryptoAttribute());
+
+ return
+ isInitiator
+ ? sDesControl.initiatorSelectAttribute(peerAttributes)
+ : sDesControl.responderSelectAttribute(peerAttributes);
+ }
+
+ /**
+ * Returns if the remote peer supports ZRTP.
+ *
+ * @param encryptionPacketExtension The ENCRYPTION element received from
+ * the remote peer. This may contain the ZRTP packet element for the remote
+ * peer.
+ *
+ * @return True if the remote peer supports ZRTP. False, otherwise.
+ */
+ protected boolean isRemoteZrtpCapable(
+ EncryptionPacketExtension encryptionPacketExtension)
+ {
+ return
+ (encryptionPacketExtension.getFirstChildOfType(
+ ZrtpHashPacketExtension.class)
+ != null);
+ }
+
+ /**
+ * Sets ZRTP element to the ENCRYPTION element of the DESCRIPTION for a
+ * given media.
+ *
+ * @param mediaType The type of media we are modifying the DESCRIPTION to
+ * integrate the ENCRYPTION element.
+ * @param description The element containing the media DESCRIPTION and its
+ * encryption.
+ * @param remoteDescription The element containing the media DESCRIPTION and
+ * its encryption for the remote peer. Null, if the local peer is the
+ * initiator of the call.
+ *
+ * @return True if the ZRTP element has been added to encryption. False,
+ * otherwise.
+ */
+ protected boolean setZrtpEncryptionOnDescription(
+ MediaType mediaType,
+ RtpDescriptionPacketExtension description,
+ RtpDescriptionPacketExtension remoteDescription)
+ {
+ CallPeer peer = getPeer();
+ Call call = peer.getCall();
+
+ /*
+ * ZRTP is not supported in telephony conferences utilizing the
+ * server-side technology Jitsi Videobridge yet.
+ */
+ if (call.getConference().isJitsiVideobridge())
+ return false;
+
+ boolean isRemoteZrtpCapable;
+
+ if (remoteDescription == null)
+ isRemoteZrtpCapable = true;
+ else
+ {
+ // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION element
+ // for a given DESCRIPTION.
+ EncryptionPacketExtension remoteEncryption
+ = remoteDescription.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ isRemoteZrtpCapable
+ = (remoteEncryption != null)
+ && isRemoteZrtpCapable(remoteEncryption);
+ }
+
+ boolean zrtpHashSet = false; // Will become true if at least one is set.
+
+ if (isRemoteZrtpCapable)
+ {
+ AccountID accountID = peer.getProtocolProvider().getAccountID();
+
+ if(accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.DEFAULT_ENCRYPTION,
+ true)
+ && accountID.isEncryptionProtocolEnabled(
+ ZrtpControl.PROTO_NAME)
+ && call.isSipZrtpAttribute())
+ {
+ ZrtpControl zrtpControl
+ = (ZrtpControl)
+ getSrtpControls().getOrCreate(
+ mediaType,
+ SrtpControlType.ZRTP);
+ int numberSupportedVersions
+ = zrtpControl.getNumberSupportedVersions();
+
+ for (int i = 0; i < numberSupportedVersions; i++)
+ {
+ String helloHash[] = zrtpControl.getHelloHashSep(i);
+
+ if ((helloHash != null) && (helloHash[1].length() > 0))
+ {
+ ZrtpHashPacketExtension hash
+ = new ZrtpHashPacketExtension();
+
+ hash.setVersion(helloHash[0]);
+ hash.setValue(helloHash[1]);
+
+ EncryptionPacketExtension encryption
+ = description.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if (encryption == null)
+ {
+ encryption = new EncryptionPacketExtension();
+ description.addChildExtension(encryption);
+ }
+ encryption.addChildExtension(hash);
+ zrtpHashSet = true;
+ }
+ }
+ }
+ }
+
+ return zrtpHashSet;
+ }
+
+ /**
+ * Sets SDES element(s) to the ENCRYPTION element of the DESCRIPTION for a
+ * given media.
+ *
+ * @param mediaType The type of media we are modifying the DESCRIPTION to
+ * integrate the ENCRYPTION element.
+ * @param localDescription The element containing the media DESCRIPTION and
+ * its encryption.
+ * @param remoteDescription The element containing the media DESCRIPTION and
+ * its encryption for the remote peer. Null, if the local peer is the
+ * initiator of the call.
+ *
+ * @return True if the crypto element has been added to encryption. False,
+ * otherwise.
+ */
+ protected boolean setSDesEncryptionOnDescription(
+ MediaType mediaType,
+ RtpDescriptionPacketExtension localDescription,
+ RtpDescriptionPacketExtension remoteDescription)
+ {
+ CallPeer peer = getPeer();
+
+ /*
+ * SDES is not supported in telephony conferences utilizing the
+ * server-side technology Jitsi Videobridge yet.
+ */
+ if (peer.getCall().getConference().isJitsiVideobridge())
+ return false;
+
+ AccountID accountID = peer.getProtocolProvider().getAccountID();
+
+ // check if SDES and encryption is enabled at all
+ if (accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.DEFAULT_ENCRYPTION,
+ true)
+ && accountID.isEncryptionProtocolEnabled(
+ SDesControl.PROTO_NAME))
+ {
+ // get or create the control
+ SrtpControls srtpControls = getSrtpControls();
+ SDesControl sdesControl
+ = (SDesControl)
+ srtpControls.getOrCreate(mediaType, SrtpControlType.SDES);
+ // set the enabled ciphers suites
+ String ciphers
+ = accountID.getAccountPropertyString(
+ ProtocolProviderFactory.SDES_CIPHER_SUITES);
+
+ if (ciphers == null)
+ {
+ ciphers =
+ JabberActivator.getResources().getSettingsString(
+ SDesControl.SDES_CIPHER_SUITES);
+ }
+ sdesControl.setEnabledCiphers(Arrays.asList(ciphers.split(",")));
+
+ // act as initiator
+ if (remoteDescription == null)
+ {
+ EncryptionPacketExtension localEncryption
+ = localDescription.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if(localEncryption == null)
+ {
+ localEncryption = new EncryptionPacketExtension();
+ localDescription.addChildExtension(localEncryption);
+ }
+ for(SrtpCryptoAttribute ca:
+ sdesControl.getInitiatorCryptoAttributes())
+ {
+ CryptoPacketExtension crypto
+ = new CryptoPacketExtension(ca);
+ localEncryption.addChildExtension(crypto);
+ }
+
+ return true;
+ }
+ // act as responder
+ else
+ {
+ // Conforming to XEP-0167 schema there is 0 or 1 ENCRYPTION
+ // element for a given DESCRIPTION.
+ EncryptionPacketExtension remoteEncryption
+ = remoteDescription.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if(remoteEncryption != null)
+ {
+ SrtpCryptoAttribute selectedSdes = selectSdesCryptoSuite(
+ false,
+ sdesControl,
+ remoteEncryption);
+
+ if(selectedSdes != null)
+ {
+ EncryptionPacketExtension localEncryption
+ = localDescription.getFirstChildOfType(
+ EncryptionPacketExtension.class);
+
+ if(localEncryption == null)
+ {
+ localEncryption = new EncryptionPacketExtension();
+ localDescription.addChildExtension(localEncryption);
+ }
+
+ CryptoPacketExtension crypto
+ = new CryptoPacketExtension(selectedSdes);
+
+ localEncryption.addChildExtension(crypto);
+
+ return true;
+ }
+ else
+ {
+ // none of the offered suites match, destroy the sdes
+ // control
+ sdesControl.cleanup();
+ srtpControls.remove(mediaType, SrtpControlType.SDES);
+ logger.warn(
+ "Received unsupported sdes crypto attribute");
+ }
+ }
+ else
+ {
+ // peer doesn't offer any SDES attribute, destroy the sdes
+ // control
+ sdesControl.cleanup();
+ srtpControls.remove(mediaType, SrtpControlType.SDES);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Selects the preferred encryption protocol (only used by the callee).
+ *
+ * @param mediaType The type of media (AUDIO or VIDEO).
+ * @param localDescription The element containing the media DESCRIPTION and
+ * its encryption.
+ * @param remoteDescription The element containing the media DESCRIPTION and
+ * its encryption for the remote peer; <tt>null</tt> if the local peer is
+ * the initiator of the call.
+ */
+ protected void setAndAddPreferredEncryptionProtocol(
+ MediaType mediaType,
+ RtpDescriptionPacketExtension localDescription,
+ RtpDescriptionPacketExtension remoteDescription)
+ {
+ // Sets ZRTP or SDES, depending on the preferences for this account.
+ List<String> preferredEncryptionProtocols
+ = getPeer()
+ .getProtocolProvider()
+ .getAccountID()
+ .getSortedEnabledEncryptionProtocolList();
+
+ for(String preferredEncryptionProtocol : preferredEncryptionProtocols)
+ {
+ String protoName
+ = preferredEncryptionProtocol.substring(
+ ProtocolProviderFactory.ENCRYPTION_PROTOCOL.length()
+ + 1);
+
+ if (setAndAddPreferredEncryptionProtocol(
+ protoName,
+ mediaType,
+ localDescription,
+ remoteDescription))
+ {
+ // Stop once an encryption advertisement has been chosen.
+ return;
+ }
+ }
+ }
+
+ /**
+ * Selects a specific encryption protocol if it is the preferred (only used
+ * by the callee).
+ *
+ * @param protoName the name of the encryption protocol which is to be
+ * selected
+ * @param mediaType The type of media (AUDIO or VIDEO).
+ * @param localDescription The element containing the media DESCRIPTION and
+ * its encryption.
+ * @param remoteDescription The element containing the media DESCRIPTION and
+ * its encryption for the remote peer; <tt>null</tt> if the local peer is
+ * the initiator of the call.
+ * @return <tt>true</tt> if the specified encryption protocol has been
+ * selected; <tt>false</tt>, otherwise
+ */
+ protected boolean setAndAddPreferredEncryptionProtocol(
+ String protoName,
+ MediaType mediaType,
+ RtpDescriptionPacketExtension localDescription,
+ RtpDescriptionPacketExtension remoteDescription)
+ {
+ /*
+ * Neither SDES nor ZRTP is supported in telephony conferences utilizing
+ * the server-side technology Jitsi Videobridge yet.
+ */
+ if (getPeer().isJitsiVideobridge())
+ return false;
+
+ // SDES
+ if(SDesControl.PROTO_NAME.equals(protoName))
+ {
+ addSDesAdvertisedEncryptions(
+ false,
+ remoteDescription,
+ mediaType);
+ if(setSDesEncryptionOnDescription(
+ mediaType,
+ localDescription,
+ remoteDescription))
+ {
+ // Stop once an encryption advertisement has been chosen.
+ return true;
+ }
+ }
+ // ZRTP
+ else if(ZrtpControl.PROTO_NAME.equals(protoName))
+ {
+ if(setZrtpEncryptionOnDescription(
+ mediaType,
+ localDescription,
+ remoteDescription))
+ {
+ addZrtpAdvertisedEncryptions(
+ false,
+ remoteDescription,
+ mediaType);
+ // Stop once an encryption advertisement has been chosen.
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java index 7281eaa..5af5b1e 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java @@ -41,7 +41,7 @@ public class CallJabberImpl private static final Logger logger = Logger.getLogger(CallJabberImpl.class); /** - * The Jitsi VideoBridge conference which the local peer represented by this + * The Jitsi Videobridge conference which the local peer represented by this * instance is a focus of. */ private ColibriConferenceIQ colibri; @@ -158,7 +158,7 @@ public class CallJabberImpl CallPeerJabberImpl peer, Map<ContentPacketExtension,ContentPacketExtension> contentMap) { - if (!getConference().isJitsiVideoBridge()) + if (!getConference().isJitsiVideobridge()) return null; /* @@ -180,12 +180,12 @@ public class CallJabberImpl ProtocolProviderServiceJabberImpl protocolProvider = getProtocolProvider(); - String jitsiVideoBridge + String jitsiVideobridge = (colibri == null) - ? protocolProvider.getJitsiVideoBridge() + ? protocolProvider.getJitsiVideobridge() : colibri.getFrom(); - if ((jitsiVideoBridge == null) || (jitsiVideoBridge.length() == 0)) + if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0)) { logger.error( "Failed to allocate colibri channels: no videobridge" @@ -243,7 +243,7 @@ public class CallJabberImpl localChannelRequest.addPayloadType(ptpe); // DTLS-SRTP setDtlsEncryptionToChannel( - jitsiVideoBridge, + jitsiVideobridge, peer, mediaType, localChannelRequest); @@ -269,7 +269,7 @@ public class CallJabberImpl = connection.createPacketCollector( new PacketIDFilter(conferenceRequest.getPacketID())); - conferenceRequest.setTo(jitsiVideoBridge); + conferenceRequest.setTo(jitsiVideobridge); conferenceRequest.setType(IQ.Type.GET); connection.sendPacket(conferenceRequest); @@ -770,7 +770,7 @@ public class CallJabberImpl * processed by this instance and no further processing is to be performed * by other possible processors of <tt>ColibriConferenceIQ</tt>s; otherwise, * <tt>false</tt>. Because a <tt>ColibriConferenceIQ</tt> request sent from - * the Jitsi VideoBridge server to the application as its client concerns a + * the Jitsi Videobridge server to the application as its client concerns a * specific <tt>CallJabberImpl</tt> implementation, no further processing by * other <tt>CallJabberImpl</tt> instances is necessary once the * <tt>ColibriConferenceIQ</tt> is processed by the associated @@ -782,7 +782,7 @@ public class CallJabberImpl { /* * This instance has not set up any conference using the Jitsi - * VideoBridge server-side technology yet so it cannot be bothered + * Videobridge server-side technology yet so it cannot be bothered * with related requests. */ return false; @@ -792,7 +792,7 @@ public class CallJabberImpl /* * Remove the local Channels (from the specified conferenceIQ) i.e. * the Channels on which the local peer/user is sending to the Jitsi - * VideoBridge server because they concern this Call only and not + * Videobridge server because they concern this Call only and not * its CallPeers. */ for (MediaType mediaType : MediaType.values()) @@ -833,7 +833,7 @@ public class CallJabberImpl else { /* - * This instance has set up a conference using the Jitsi VideoBridge + * This instance has set up a conference using the Jitsi Videobridge * server-side technology but it is not the one referred to by the * specified conferenceIQ i.e. the specified conferenceIQ does not * concern this instance. @@ -1186,7 +1186,7 @@ public class CallJabberImpl * purposes of transmitting media between the local peer and the Jitsi * Videobridge server. * - * @param jitsiVideoBridge the address/JID of the Jitsi Videobridge + * @param jitsiVideobridge the address/JID of the Jitsi Videobridge * @param peer the <tt>CallPeer</tt> associated with the method invocation * @param mediaType the <tt>MediaType</tt> of the media to be transmitted * over the DTLS-SRTP session @@ -1194,7 +1194,7 @@ public class CallJabberImpl * the state of the remote DTLS-SRTP endpoint. */ private void setDtlsEncryptionToChannel( - String jitsiVideoBridge, + String jitsiVideobridge, CallPeerJabberImpl peer, MediaType mediaType, ColibriConferenceIQ.Channel channel) @@ -1209,7 +1209,7 @@ public class CallJabberImpl && accountID.isEncryptionProtocolEnabled( DtlsControl.PROTO_NAME) && protocolProvider.isFeatureSupported( - jitsiVideoBridge, + jitsiVideobridge, ProtocolProviderServiceJabberImpl .URN_XMPP_JINGLE_DTLS_SRTP)) { diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java index 726d12f..110bb6c 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java @@ -1,1623 +1,1627 @@ -/* - * 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.impl.protocol.jabber; - -import java.lang.reflect.*; -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.ContentPacketExtension.SendersEnum; -import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.filter.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smack.util.*; -import org.jivesoftware.smackx.packet.*; - -/** - * Implements a Jabber <tt>CallPeer</tt>. - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Boris Grozev - */ -public class CallPeerJabberImpl - extends AbstractCallPeerJabberGTalkImpl - <CallJabberImpl, CallPeerMediaHandlerJabberImpl, JingleIQ> -{ - /** - * The <tt>Logger</tt> used by the <tt>CallPeerJabberImpl</tt> class and its - * instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(CallPeerJabberImpl.class); - - /** - * If the call is cancelled before session-initiate is sent. - */ - private boolean cancelled = false; - - /** - * Synchronization object for candidates available. - */ - private final Object candSyncRoot = new Object(); - - /** - * If the content-add does not contains candidates. - */ - private boolean contentAddWithNoCands = false; - - /** - * If we have processed the session initiate. - */ - private boolean sessionInitiateProcessed = false; - - /** - * Synchronization object. Synchronization object? Wow, who would have - * thought! ;) Would be great to have a word on what we are syncing with it - */ - private final Object sessionInitiateSyncRoot = new Object(); - - /** - * Synchronization object for SID. - */ - private final Object sidSyncRoot = new Object(); - - /** - * The current value of the 'senders' field of the audio content in the - * Jingle session with this <tt>CallPeer</tt>. - * <tt>null</tt> should be interpreted as 'both', which is the default in - * Jingle if the XML attribute is missing. - */ - private SendersEnum audioSenders = SendersEnum.none; - - /** - * The current value of the 'senders' field of the video content in the - * Jingle session with this <tt>CallPeer</tt>. - * <tt>null</tt> should be interpreted as 'both', which is the default in - * Jingle if the XML attribute is missing. - */ - private SendersEnum videoSenders = SendersEnum.none; - - /** - * Creates a new call peer with address <tt>peerAddress</tt>. - * - * @param peerAddress the Jabber address of the new call peer. - * @param owningCall the call that contains this call peer. - */ - public CallPeerJabberImpl(String peerAddress, - CallJabberImpl owningCall) - { - super(peerAddress, owningCall); - - setMediaHandler(new CallPeerMediaHandlerJabberImpl(this)); - } - - /** - * Creates a new call peer with address <tt>peerAddress</tt>. - * - * @param peerAddress the Jabber address of the new call peer. - * @param owningCall the call that contains this call peer. - * @param sessionIQ The session-initiate <tt>JingleIQ</tt> which was - * received from <tt>peerAddress</tt> and caused the creation of this - * <tt>CallPeerJabberImpl</tt> - */ - public CallPeerJabberImpl(String peerAddress, - CallJabberImpl owningCall, - JingleIQ sessionIQ) - { - this(peerAddress, owningCall); - this.sessionInitIQ = sessionIQ; - } - - /** - * Send a session-accept <tt>JingleIQ</tt> to this <tt>CallPeer</tt> - * @throws OperationFailedException if we fail to create or send the - * response. - */ - public synchronized void answer() - throws OperationFailedException - { - Iterable<ContentPacketExtension> answer; - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - try - { - mediaHandler - .getTransportManager().wrapupConnectivityEstablishment(); - answer = mediaHandler.generateSessionAccept(); - for (ContentPacketExtension c : answer) - setSenders(getMediaType(c), c.getSenders()); - } - catch(Exception exc) - { - logger.info("Failed to answer an incoming call", exc); - - //send an error response - String reasonText = "Error: " + exc.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - sessionInitIQ.getTo(), - sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), - Reason.FAILED_APPLICATION, - reasonText); - - setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - - JingleIQ response - = JinglePacketFactory.createSessionAccept( - sessionInitIQ.getTo(), - sessionInitIQ.getFrom(), - getSID(), - answer); - - //send the packet first and start the stream later in case the media - //relay needs to see it before letting hole punching techniques through. - getProtocolProvider().getConnection().sendPacket(response); - - try - { - mediaHandler.start(); - } - catch(UndeclaredThrowableException e) - { - Throwable exc = e.getUndeclaredThrowable(); - - logger.info("Failed to establish a connection", exc); - - //send an error response - String reasonText = "Error: " + exc.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - sessionInitIQ.getTo(), - sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), - Reason.GENERAL_ERROR, - reasonText); - - setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - - //tell everyone we are connected so that the audio notifications would - //stop - setState(CallPeerState.CONNECTED); - } - - /** - * Returns the session ID of the Jingle session associated with this call. - * - * @return the session ID of the Jingle session associated with this call. - */ - @Override - public String getSID() - { - return sessionInitIQ != null ? sessionInitIQ.getSID() : null; - } - - /** - * Returns the IQ ID of the Jingle session-initiate packet associated with - * this call. - * - * @return the IQ ID of the Jingle session-initiate packet associated with - * this call. - */ - public JingleIQ getSessionIQ() - { - return sessionInitIQ; - } - - /** - * Ends the call with this <tt>CallPeer</tt>. Depending on the state - * of the peer the method would send a CANCEL, BYE, or BUSY_HERE message - * and set the new state to DISCONNECTED. - * - * @param failed indicates if the hangup is following to a call failure or - * simply a disconnect - * @param reasonText the text, if any, to be set on the - * <tt>ReasonPacketExtension</tt> as the value of its - * @param reasonOtherExtension the <tt>PacketExtension</tt>, if any, to be - * set on the <tt>ReasonPacketExtension</tt> as the value of its - * <tt>otherExtension</tt> property - */ - public void hangup(boolean failed, - String reasonText, - PacketExtension reasonOtherExtension) - { - CallPeerState prevPeerState = getState(); - - // do nothing if the call is already ended - if (CallPeerState.DISCONNECTED.equals(prevPeerState) - || CallPeerState.FAILED.equals(prevPeerState)) - { - if (logger.isDebugEnabled()) - logger.debug("Ignoring a request to hangup a call peer " - + "that is already DISCONNECTED"); - return; - } - - setState( - failed ? CallPeerState.FAILED : CallPeerState.DISCONNECTED, - reasonText); - - JingleIQ responseIQ = null; - - if (prevPeerState.equals(CallPeerState.CONNECTED) - || CallPeerState.isOnHold(prevPeerState)) - { - responseIQ = JinglePacketFactory.createBye( - getProtocolProvider().getOurJID(), peerJID, getSID()); - } - else if (CallPeerState.CONNECTING.equals(prevPeerState) - || CallPeerState.CONNECTING_WITH_EARLY_MEDIA.equals(prevPeerState) - || CallPeerState.ALERTING_REMOTE_SIDE.equals(prevPeerState)) - { - String jingleSID = getSID(); - - if(jingleSID == null) - { - synchronized(sidSyncRoot) - { - // we cancelled the call too early because the jingleSID - // is null (i.e. the session-initiate has not been created) - // and no need to send the session-terminate - cancelled = true; - return; - } - } - - responseIQ = JinglePacketFactory.createCancel( - getProtocolProvider().getOurJID(), peerJID, getSID()); - } - else if (prevPeerState.equals(CallPeerState.INCOMING_CALL)) - { - responseIQ = JinglePacketFactory.createBusy( - getProtocolProvider().getOurJID(), peerJID, getSID()); - } - else if (prevPeerState.equals(CallPeerState.BUSY) - || prevPeerState.equals(CallPeerState.FAILED)) - { - // For FAILED and BUSY we only need to update CALL_STATUS - // as everything else has been done already. - } - else - { - logger.info("Could not determine call peer state!"); - } - - if (responseIQ != null) - { - if (reasonOtherExtension != null) - { - ReasonPacketExtension reason - = (ReasonPacketExtension) - responseIQ.getExtension( - ReasonPacketExtension.ELEMENT_NAME, - ReasonPacketExtension.NAMESPACE); - - if (reason != null) - { - reason.setOtherExtension(reasonOtherExtension); - } - else if(reasonOtherExtension instanceof ReasonPacketExtension) - { - responseIQ.setReason( - (ReasonPacketExtension)reasonOtherExtension); - } - } - - getProtocolProvider().getConnection().sendPacket(responseIQ); - } - } - - /** - * Creates and sends a session-initiate {@link JingleIQ}. - * - * @param sessionInitiateExtensions a collection of additional and optional - * <tt>PacketExtension</tt>s to be added to the <tt>session-initiate</tt> - * {@link JingleIQ} which is to initiate the session with this - * <tt>CallPeerJabberImpl</tt> - * @throws OperationFailedException exception - */ - protected synchronized void initiateSession( - Iterable<PacketExtension> sessionInitiateExtensions) - throws OperationFailedException - { - initiator = false; - - //Create the media description that we'd like to send to the other side. - List<ContentPacketExtension> offer - = getMediaHandler().createContentList(); - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - - synchronized(sidSyncRoot) - { - sessionInitIQ - = JinglePacketFactory.createSessionInitiate( - protocolProvider.getOurJID(), - this.peerJID, - JingleIQ.generateSID(), - offer); - - if(cancelled) - { - // we cancelled the call too early so no need to send the - // session-initiate to peer - getMediaHandler().getTransportManager().close(); - return; - } - } - - if (sessionInitiateExtensions != null) - { - for (PacketExtension sessionInitiateExtension - : sessionInitiateExtensions) - { - sessionInitIQ.addExtension(sessionInitiateExtension); - } - } - - protocolProvider.getConnection().sendPacket(sessionInitIQ); - } - - /** - * Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has - * been received. This <tt>CallPeerJabberImpl</tt> uses the part of the - * information provided in the specified <tt>conferenceIQ</tt> which - * concerns it only. - * - * @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been - * received - */ - void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) - { - /* - * CallPeerJabberImpl does not itself/directly know the specifics - * related to the channels allocated on the Jitsi VideoBridge server. - * The channels contain transport and media-related information so - * forward the notification to CallPeerMediaHandlerJabberImpl. - */ - getMediaHandler().processColibriConferenceIQ(conferenceIQ); - } - - /** - * Processes the content-accept {@link JingleIQ}. - * - * @param content The {@link JingleIQ} that contains content that remote - * peer has accepted - */ - public void processContentAccept(JingleIQ content) - { - List<ContentPacketExtension> contents = content.getContentList(); - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - try - { - mediaHandler - .getTransportManager().wrapupConnectivityEstablishment(); - mediaHandler.processAnswer(contents); - for (ContentPacketExtension c : contents) - setSenders(getMediaType(c), c.getSenders()); - } - catch (Exception e) - { - logger.warn("Failed to process a content-accept", e); - - // Send an error response. - String reason = "Error: " + e.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - getProtocolProvider().getOurJID(), - peerJID, - sessionInitIQ.getSID(), - Reason.INCOMPATIBLE_PARAMETERS, - reason); - - setState(CallPeerState.FAILED, reason); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - - mediaHandler.start(); - } - - /** - * Processes the content-add {@link JingleIQ}. - * - * @param content The {@link JingleIQ} that contains content that remote - * peer wants to be added - */ - public void processContentAdd(final JingleIQ content) - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - List<ContentPacketExtension> contents = content.getContentList(); - Iterable<ContentPacketExtension> answerContents; - JingleIQ contentIQ; - boolean noCands = false; - MediaStream oldVideoStream = mediaHandler.getStream(MediaType.VIDEO); - - if(logger.isInfoEnabled()) - logger.info("Looking for candidates in content-add."); - try - { - if(!contentAddWithNoCands) - { - mediaHandler.processOffer(contents); - - /* - * Gingle transport will not put candidate in session-initiate - * and content-add. - */ - for(ContentPacketExtension c : contents) - { - if(JingleUtils.getFirstCandidate(c, 1) == null) - { - contentAddWithNoCands = true; - noCands = true; - } - } - } - - // if no candidates are present, launch a new Thread which will - // process and wait for the connectivity establishment (otherwise - // the existing thread will be blocked and thus cannot receive - // transport-info with candidates - if(noCands) - { - new Thread() - { - @Override - public void run() - { - try - { - synchronized(candSyncRoot) - { - candSyncRoot.wait(); - } - } - catch(InterruptedException e) - { - } - - processContentAdd(content); - contentAddWithNoCands = false; - } - }.start(); - if(logger.isInfoEnabled()) - logger.info("No candidates found in content-add, started " - + "new thread."); - return; - } - - mediaHandler.getTransportManager(). - wrapupConnectivityEstablishment(); - if(logger.isInfoEnabled()) - logger.info("Wrapping up connectivity establishment"); - answerContents = mediaHandler.generateSessionAccept(); - contentIQ = null; - } - catch(Exception e) - { - logger.warn("Exception occurred", e); - - answerContents = null; - contentIQ - = JinglePacketFactory.createContentReject( - getProtocolProvider().getOurJID(), - this.peerJID, - getSID(), - answerContents); - } - - if(contentIQ == null) - { - /* send content-accept */ - contentIQ - = JinglePacketFactory.createContentAccept( - getProtocolProvider().getOurJID(), - this.peerJID, - getSID(), - answerContents); - for (ContentPacketExtension c : answerContents) - setSenders(getMediaType(c), c.getSenders()); - } - - getProtocolProvider().getConnection().sendPacket(contentIQ); - mediaHandler.start(); - - /* - * If a remote peer turns her video on in a conference which is hosted - * by the local peer and the local peer is not streaming her local - * video, reinvite the other remote peers to enable RTP translation. - */ - if (oldVideoStream == null) - { - MediaStream newVideoStream - = mediaHandler.getStream(MediaType.VIDEO); - - if ((newVideoStream != null) - && mediaHandler.isRTPTranslationEnabled(MediaType.VIDEO)) - { - try - { - getCall().modifyVideoContent(); - } - catch (OperationFailedException ofe) - { - logger.error("Failed to enable RTP translation", ofe); - } - } - } - } - - /** - * Processes the content-modify {@link JingleIQ}. - * - * @param content The {@link JingleIQ} that contains content that remote - * peer wants to be modified - */ - public void processContentModify(JingleIQ content) - { - ContentPacketExtension ext = content.getContentList().get(0); - MediaType mediaType = getMediaType(ext); - - try - { - boolean modify - = (ext.getFirstChildOfType(RtpDescriptionPacketExtension.class) - != null); - - getMediaHandler().reinitContent(ext.getName(), ext, modify); - - setSenders(mediaType, ext.getSenders()); - - if (MediaType.VIDEO.equals(mediaType)) - getCall().modifyVideoContent(); - } - catch(Exception e) - { - logger.info("Failed to process an incoming content-modify", e); - - // Send an error response. - String reason = "Error: " + e.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - getProtocolProvider().getOurJID(), - peerJID, - sessionInitIQ.getSID(), - Reason.INCOMPATIBLE_PARAMETERS, - reason); - - setState(CallPeerState.FAILED, reason); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - } - - /** - * Processes the content-reject {@link JingleIQ}. - * - * @param content The {@link JingleIQ} - */ - public void processContentReject(JingleIQ content) - { - if(content.getContentList().isEmpty()) - { - //send an error response; - JingleIQ errResp = JinglePacketFactory.createSessionTerminate( - sessionInitIQ.getTo(), sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS, - "Error: content rejected"); - - setState(CallPeerState.FAILED, "Error: content rejected"); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - } - - /** - * Processes the content-remove {@link JingleIQ}. - * - * @param content The {@link JingleIQ} that contains content that remote - * peer wants to be removed - */ - public void processContentRemove(JingleIQ content) - { - List<ContentPacketExtension> contents = content.getContentList(); - boolean videoContentRemoved = false; - - if (!contents.isEmpty()) - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - for(ContentPacketExtension c : contents) - { - mediaHandler.removeContent(c.getName()); - - MediaType mediaType = getMediaType(c); - setSenders(mediaType, SendersEnum.none); - - if (MediaType.VIDEO.equals(mediaType)) - videoContentRemoved = true; - } - - /* - * TODO XEP-0166: Jingle says: If the content-remove results in zero - * content definitions for the session, the entity that receives the - * content-remove SHOULD send a session-terminate action to the - * other party (since a session with no content definitions is - * void). - */ - } - - if (videoContentRemoved) - { - // removing of the video content might affect the other sessions - // in the call - try - { - getCall().modifyVideoContent(); - } - catch (Exception e) - { - logger.warn("Failed to update Jingle sessions"); - } - } - } - - /** - * Processes a session-accept {@link JingleIQ}. - * - * @param sessionInitIQ The session-accept {@link JingleIQ} to process. - */ - public void processSessionAccept(JingleIQ sessionInitIQ) - { - this.sessionInitIQ = sessionInitIQ; - - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - List<ContentPacketExtension> answer = sessionInitIQ.getContentList(); - - try - { - mediaHandler - .getTransportManager().wrapupConnectivityEstablishment(); - mediaHandler.processAnswer(answer); - for (ContentPacketExtension c : answer) - setSenders(getMediaType(c), c.getSenders()); - } - catch(Exception exc) - { - if (logger.isInfoEnabled()) - logger.info("Failed to process a session-accept", exc); - - //send an error response; - JingleIQ errResp = JinglePacketFactory.createSessionTerminate( - sessionInitIQ.getTo(), sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS, - exc.getClass().getName() + ": " + exc.getMessage()); - - setState(CallPeerState.FAILED, "Error: " + exc.getMessage()); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - - //tell everyone we are connected so that the audio notifications would - //stop - setState(CallPeerState.CONNECTED); - - mediaHandler.start(); - - /* - * If video was added to the call after we sent the session-initiate - * to this peer, it needs to be added to this peer's session with a - * content-add. - */ - sendModifyVideoContent(); - } - - /** - * Handles the specified session <tt>info</tt> packet according to its - * content. - * - * @param info the {@link SessionInfoPacketExtension} that we just received. - */ - public void processSessionInfo(SessionInfoPacketExtension info) - { - switch (info.getType()) - { - case ringing: - setState(CallPeerState.ALERTING_REMOTE_SIDE); - break; - case hold: - getMediaHandler().setRemotelyOnHold(true); - reevalRemoteHoldStatus(); - break; - case unhold: - case active: - getMediaHandler().setRemotelyOnHold(false); - reevalRemoteHoldStatus(); - break; - default: - logger.warn("Received SessionInfoPacketExtension of unknown type"); - } - } - - /** - * Processes the session initiation {@link JingleIQ} that we were created - * with, passing its content to the media handler and then sends either a - * "session-info/ringing" or a "session-terminate" response. - * - * @param sessionInitIQ The {@link JingleIQ} that created the session that - * we are handling here. - */ - protected synchronized void processSessionInitiate(JingleIQ sessionInitIQ) - { - // Do initiate the session. - this.sessionInitIQ = sessionInitIQ; - this.initiator = true; - - // This is the SDP offer that came from the initial session-initiate. - // Contrary to SIP, we are guaranteed to have content because XEP-0166 - // says: "A session consists of at least one content type at a time." - List<ContentPacketExtension> offer = sessionInitIQ.getContentList(); - - try - { - getMediaHandler().processOffer(offer); - - CoinPacketExtension coin = null; - - for(PacketExtension ext : sessionInitIQ.getExtensions()) - { - if(ext.getElementName().equals( - CoinPacketExtension.ELEMENT_NAME)) - { - coin = (CoinPacketExtension)ext; - break; - } - } - - /* does the call peer acts as a conference focus ? */ - if(coin != null) - { - setConferenceFocus(Boolean.parseBoolean( - (String)coin.getAttribute("isfocus"))); - } - } - catch(Exception ex) - { - logger.info("Failed to process an incoming session initiate", ex); - - //send an error response; - String reasonText = "Error: " + ex.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - sessionInitIQ.getTo(), - sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), - Reason.INCOMPATIBLE_PARAMETERS, - reasonText); - - setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - return; - } - - // If we do not get the info about the remote peer yet. Get it right - // now. - if(this.getDiscoveryInfo() == null) - { - String calleeURI = sessionInitIQ.getFrom(); - retrieveDiscoveryInfo(calleeURI); - } - - //send a ringing response - if (logger.isTraceEnabled()) - logger.trace("will send ringing response: "); - - getProtocolProvider().getConnection().sendPacket( - JinglePacketFactory.createRinging(sessionInitIQ)); - - synchronized(sessionInitiateSyncRoot) - { - sessionInitiateProcessed = true; - sessionInitiateSyncRoot.notify(); - } - - //if this is a 3264 initiator, let's give them an early peek at our - //answer so that they could start ICE (SIP-2-Jingle gateways won't - //be able to send their candidates unless they have this) - DiscoverInfo discoverInfo = getDiscoveryInfo(); - if ((discoverInfo != null) - && discoverInfo.containsFeature( - ProtocolProviderServiceJabberImpl.URN_IETF_RFC_3264)) - { - getProtocolProvider().getConnection().sendPacket( - JinglePacketFactory.createDescriptionInfo( - sessionInitIQ.getTo(), - sessionInitIQ.getFrom(), - sessionInitIQ.getSID(), - getMediaHandler().getLocalContentList())); - } - } - - /** - * Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a - * reason to the user, if there is one. - * - * @param jingleIQ the {@link JingleIQ} that's terminating our session. - */ - public void processSessionTerminate(JingleIQ jingleIQ) - { - String reasonStr = "Call ended by remote side."; - ReasonPacketExtension reasonExt = jingleIQ.getReason(); - - if(reasonExt != null) - { - Reason reason = reasonExt.getReason(); - - if(reason != null) - reasonStr += " Reason: " + reason.toString() + "."; - - String text = reasonExt.getText(); - - if(text != null) - reasonStr += " " + text; - } - - setState(CallPeerState.DISCONNECTED, reasonStr); - } - - /** - * Processes a specific "XEP-0251: Jingle Session Transfer" - * <tt>transfer</tt> packet (extension). - * - * @param transfer the "XEP-0251: Jingle Session Transfer" transfer packet - * (extension) to process - * @throws OperationFailedException if anything goes wrong while processing - * the specified <tt>transfer</tt> packet (extension) - */ - public void processTransfer(TransferPacketExtension transfer) - throws OperationFailedException - { - String attendantAddress = transfer.getFrom(); - - if (attendantAddress == null) - { - throw new OperationFailedException( - "Session transfer must contain a \'from\' attribute value.", - OperationFailedException.ILLEGAL_ARGUMENT); - } - - String calleeAddress = transfer.getTo(); - - if (calleeAddress == null) - { - throw new OperationFailedException( - "Session transfer must contain a \'to\' attribute value.", - OperationFailedException.ILLEGAL_ARGUMENT); - } - - // Checks if the transfer remote peer is contained by the roster of this - // account. - Roster roster = getProtocolProvider().getConnection().getRoster(); - if(!roster.contains(StringUtils.parseBareAddress(calleeAddress))) - { - String failedMessage = - "Tranfer impossible:\n" - + "Account roster does not contain tansfer peer: " - + StringUtils.parseBareAddress(calleeAddress); - setState(CallPeerState.FAILED, failedMessage); - logger.info(failedMessage); - } - - OperationSetBasicTelephonyJabberImpl basicTelephony - = (OperationSetBasicTelephonyJabberImpl) - getProtocolProvider() - .getOperationSet(OperationSetBasicTelephony.class); - CallJabberImpl calleeCall = new CallJabberImpl(basicTelephony); - TransferPacketExtension calleeTransfer = new TransferPacketExtension(); - String sid = transfer.getSID(); - - calleeTransfer.setFrom(attendantAddress); - if (sid != null) - { - calleeTransfer.setSID(sid); - calleeTransfer.setTo(calleeAddress); - } - basicTelephony.createOutgoingCall( - calleeCall, - calleeAddress, - Arrays.asList(new PacketExtension[] { calleeTransfer })); - } - - /** - * Processes the <tt>transport-info</tt> {@link JingleIQ}. - * - * @param jingleIQ the <tt>transport-info</tt> {@link JingleIQ} to process - */ - public void processTransportInfo(JingleIQ jingleIQ) - { - /* - * The transport-info action is used to exchange transport candidates so - * it only concerns the mediaHandler. - */ - try - { - if(isInitiator()) - { - synchronized(sessionInitiateSyncRoot) - { - if(!sessionInitiateProcessed) - { - try - { - sessionInitiateSyncRoot.wait(); - } - catch(InterruptedException e) - { - } - } - } - } - - getMediaHandler().processTransportInfo( - jingleIQ.getContentList()); - } - catch (OperationFailedException ofe) - { - logger.warn("Failed to process an incoming transport-info", ofe); - - //send an error response - String reasonText = "Error: " + ofe.getMessage(); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - getProtocolProvider().getOurJID(), - peerJID, - sessionInitIQ.getSID(), - Reason.GENERAL_ERROR, - reasonText); - - setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - - return; - } - - synchronized(candSyncRoot) - { - candSyncRoot.notify(); - } - } - - /** - * Puts the <tt>CallPeer</tt> represented by this instance on or off hold. - * - * @param onHold <tt>true</tt> to have the <tt>CallPeer</tt> put on hold; - * <tt>false</tt>, otherwise - * - * @throws OperationFailedException if we fail to construct or send the - * INVITE request putting the remote side on/off hold. - */ - public void putOnHold(boolean onHold) - throws OperationFailedException - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - mediaHandler.setLocallyOnHold(onHold); - - SessionInfoType type; - - if(onHold) - type = SessionInfoType.hold; - else - { - type = SessionInfoType.unhold; - getMediaHandler().reinitAllContents(); - } - - //we are now on hold and need to realize this before potentially - //spoiling it all with an exception while sending the packet :). - reevalLocalHoldStatus(); - - JingleIQ onHoldIQ = JinglePacketFactory.createSessionInfo( - getProtocolProvider().getOurJID(), - peerJID, - getSID(), - type); - - getProtocolProvider().getConnection().sendPacket(onHoldIQ); - } - - /** - * Send a <tt>content-add</tt> to add video setup. - */ - private void sendAddVideoContent() - { - List<ContentPacketExtension> contents; - - try - { - contents = getMediaHandler().createContentList(MediaType.VIDEO); - } - catch(Exception exc) - { - logger.warn("Failed to gather content for video type", exc); - return; - } - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - JingleIQ contentIQ - = JinglePacketFactory.createContentAdd( - protocolProvider.getOurJID(), - this.peerJID, - getSID(), - contents); - - protocolProvider.getConnection().sendPacket(contentIQ); - } - - /** - * Sends a <tt>content</tt> message to reflect changes in the setup such as - * the local peer/user becoming a conference focus. - */ - public void sendCoinSessionInfo() - { - JingleIQ sessionInfoIQ - = JinglePacketFactory.createSessionInfo( - getProtocolProvider().getOurJID(), - this.peerJID, - getSID()); - CoinPacketExtension coinExt - = new CoinPacketExtension(getCall().isConferenceFocus()); - - sessionInfoIQ.addExtension(coinExt); - getProtocolProvider().getConnection().sendPacket(sessionInfoIQ); - } - - /** - * Returns the <tt>MediaDirection</tt> that should be set for the content - * of type <tt>mediaType</tt> in the Jingle session for this - * <tt>CallPeer</tt>. - * If we are the focus of a conference and are doing RTP translation, - * takes into account the other <tt>CallPeer</tt>s in the <tt>Call</tt>. - * - * @param mediaType the <tt>MediaType</tt> for which to return the - * <tt>MediaDirection</tt> - * @return the <tt>MediaDirection</tt> that should be used for the content - * of type <tt>mediaType</tt> in the Jingle session for this - * <tt>CallPeer</tt>. - */ - private MediaDirection getDirectionForJingle(MediaType mediaType) - { - MediaDirection direction = MediaDirection.INACTIVE; - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - // If we are streaming media, the direction should allow sending - if ( (MediaType.AUDIO == mediaType && - mediaHandler.isLocalAudioTransmissionEnabled()) || - (MediaType.VIDEO == mediaType && - isLocalVideoStreaming())) - direction = direction.or(MediaDirection.SENDONLY); - - // If we are receiving media from this CallPeer, the direction should - // allow receiving - SendersEnum senders = getSenders(mediaType); - if (senders == null || senders == SendersEnum.both || - (isInitiator() && senders == SendersEnum.initiator) || - (!isInitiator() && senders == SendersEnum.responder)) - direction = direction.or(MediaDirection.RECVONLY); - - // If we are the focus of a conference and we are receiving media from - // another CallPeer in the same Call, the direction should allow sending - if (getCall().isConferenceFocus()) - { - for (CallPeerJabberImpl peer : getCall().getCallPeerList()) - { - if (peer != this) - { - senders = peer.getSenders(mediaType); - if (senders == null || senders == SendersEnum.both || - (peer.isInitiator() - && senders == SendersEnum.initiator) || - (!peer.isInitiator() - && senders == SendersEnum.responder)) - { - direction = direction.or(MediaDirection.SENDONLY); - break; - } - } - } - } - - return direction; - } - - /** - * Send, if necessary, a jingle <tt>content</tt> message to reflect change - * in video setup. Whether the jingle session should have a video content, - * and if so, the value of the <tt>senders</tt> field is determined - * based on whether we are streaming local video and, if we are the focus - * of a conference, on the other peers in the conference. - * The message can be content-modify if video content exists (and the - * <tt>senders</tt> field changes), content-add or content-remove. - * - * @return <tt>true</tt> if a jingle <tt>content</tt> message was sent. - */ - public boolean sendModifyVideoContent() - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - MediaDirection direction = getDirectionForJingle(MediaType.VIDEO); - - ContentPacketExtension remoteContent - = mediaHandler.getLocalContent(MediaType.VIDEO.toString()); - - if (remoteContent == null) - { - if (direction == MediaDirection.INACTIVE) - { - // no video content, none needed - return false; - } - else - { - if (getState() == CallPeerState.CONNECTED) - { - if (logger.isInfoEnabled()) - logger.info("Adding video content for " + this); - sendAddVideoContent(); - return true; - } - return false; - } - } - else - { - if (direction == MediaDirection.INACTIVE) - { - sendRemoveVideoContent(); - return true; - } - } - - SendersEnum senders = getSenders(MediaType.VIDEO); - if (senders == null) - senders = SendersEnum.both; - - SendersEnum newSenders = SendersEnum.none; - if (MediaDirection.SENDRECV == direction) - newSenders = SendersEnum.both; - else if (MediaDirection.RECVONLY == direction) - newSenders = isInitiator() - ? SendersEnum.initiator : SendersEnum.responder; - else if (MediaDirection.SENDONLY == direction) - newSenders = isInitiator() - ? SendersEnum.responder : SendersEnum.initiator; - - /* - * Send Content-Modify - */ - ContentPacketExtension ext = new ContentPacketExtension(); - String remoteContentName = remoteContent.getName(); - - ext.setSenders(newSenders); - ext.setCreator(remoteContent.getCreator()); - ext.setName(remoteContentName); - - if (newSenders != senders) - { - if (logger.isInfoEnabled()) - logger.info("Sending content modify, senders: " - + senders + "->" + newSenders); - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - JingleIQ contentIQ - = JinglePacketFactory.createContentModify( - protocolProvider.getOurJID(), - this.peerJID, - getSID(), - ext); - - protocolProvider.getConnection().sendPacket(contentIQ); - } - - try - { - mediaHandler.reinitContent(remoteContentName, ext, false); - mediaHandler.start(); - } - catch(Exception e) - { - logger.warn("Exception occurred during media reinitialization", e); - } - - return (newSenders != senders); - } - - /** - * Send a <tt>content</tt> message to reflect change in video setup (start - * or stop). - */ - public void sendModifyVideoResolutionContent() - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - ContentPacketExtension remoteContent - = mediaHandler.getRemoteContent(MediaType.VIDEO.toString()); - ContentPacketExtension content; - - logger.info("send modify-content to change resolution"); - - // send content-modify with RTP description - - // create content list with resolution - try - { - content = mediaHandler.createContentForMedia(MediaType.VIDEO); - } - catch (Exception e) - { - logger.warn("Failed to gather content for video type", e); - return; - } - - // if we are only receiving video senders is null - SendersEnum senders = remoteContent.getSenders(); - - if (senders != null) - content.setSenders(senders); - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - JingleIQ contentIQ - = JinglePacketFactory.createContentModify( - protocolProvider.getOurJID(), - this.peerJID, - getSID(), - content); - - protocolProvider.getConnection().sendPacket(contentIQ); - - try - { - mediaHandler.reinitContent(remoteContent.getName(), content, false); - mediaHandler.start(); - } - catch(Exception e) - { - logger.warn("Exception occurred when media reinitialization", e); - } - } - - /** - * Send a <tt>content-remove</tt> to remove video setup. - */ - private void sendRemoveVideoContent() - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - - ContentPacketExtension content = new ContentPacketExtension(); - ContentPacketExtension remoteContent - = mediaHandler.getRemoteContent(MediaType.VIDEO.toString()); - if (remoteContent == null) - return; - String remoteContentName = remoteContent.getName(); - - content.setName(remoteContentName); - content.setCreator(remoteContent.getCreator()); - content.setSenders(remoteContent.getSenders()); - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - JingleIQ contentIQ - = JinglePacketFactory.createContentRemove( - protocolProvider.getOurJID(), - this.peerJID, - getSID(), - Arrays.asList(content)); - - protocolProvider.getConnection().sendPacket(contentIQ); - mediaHandler.removeContent(remoteContentName); - setSenders(MediaType.VIDEO, SendersEnum.none); - } - - /** - * Sends local candidate addresses from the local peer to the remote peer - * using the <tt>transport-info</tt> {@link JingleIQ}. - * - * @param contents the local candidate addresses to be sent from the local - * peer to the remote peer using the <tt>transport-info</tt> - * {@link JingleIQ} - */ - protected void sendTransportInfo(Iterable<ContentPacketExtension> contents) - { - // if the call is canceled, do not start sending candidates in - // transport-info - if(cancelled) - return; - - JingleIQ transportInfo = new JingleIQ(); - - for (ContentPacketExtension content : contents) - transportInfo.addContent(content); - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - - transportInfo.setAction(JingleAction.TRANSPORT_INFO); - transportInfo.setFrom(protocolProvider.getOurJID()); - transportInfo.setSID(getSID()); - transportInfo.setTo(getAddress()); - transportInfo.setType(IQ.Type.SET); - - PacketCollector collector - = protocolProvider.getConnection().createPacketCollector( - new PacketIDFilter(transportInfo.getPacketID())); - - protocolProvider.getConnection().sendPacket(transportInfo); - collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); - collector.cancel(); - } - - @Override - public void setState(CallPeerState newState, String reason, int reasonCode) - { - CallPeerState oldState = getState(); - try - { - /* - * We need to dispose of the transport manager before the - * 'call' field is set to null, because if Jitsi VideoBridge is in - * use, it (the call) is needed in order to expire the - * VideoBridge channels. - */ - if (CallPeerState.DISCONNECTED.equals(newState) - || CallPeerState.FAILED.equals(newState)) - getMediaHandler().getTransportManager().close(); - } - finally - { - super.setState(newState, reason, reasonCode); - } - - if (CallPeerState.isOnHold(oldState) - && CallPeerState.CONNECTED.equals(newState)) - { - try - { - getCall().modifyVideoContent(); - } - catch (OperationFailedException ofe) - { - logger.error("Failed to update call video state after " + - "'hold' status removed for "+this); - } - } - } - - /** - * Transfer (in the sense of call transfer) this <tt>CallPeer</tt> to a - * specific callee address which may optionally be participating in an - * active <tt>Call</tt>. - * - * @param to the address of the callee to transfer this <tt>CallPeer</tt> to - * @param sid the Jingle session ID of the active <tt>Call</tt> between the - * local peer and the callee in the case of attended transfer; <tt>null</tt> - * in the case of unattended transfer - * @throws OperationFailedException if something goes wrong - */ - protected void transfer(String to, String sid) - throws OperationFailedException - { - JingleIQ transferSessionInfo = new JingleIQ(); - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - - transferSessionInfo.setAction(JingleAction.SESSION_INFO); - transferSessionInfo.setFrom(protocolProvider.getOurJID()); - transferSessionInfo.setSID(getSID()); - transferSessionInfo.setTo(getAddress()); - transferSessionInfo.setType(IQ.Type.SET); - - TransferPacketExtension transfer = new TransferPacketExtension(); - - // Attended transfer. - if (sid != null) - { - /* - * Not really sure what the value of the "from" attribute of the - * "transfer" element should be but the examples in "XEP-0251: - * Jingle Session Transfer" has it in the case of attended transfer. - */ - transfer.setFrom(protocolProvider.getOurJID()); - transfer.setSID(sid); - - // Puts on hold the 2 calls before making the attended transfer. - OperationSetBasicTelephonyJabberImpl basicTelephony - = (OperationSetBasicTelephonyJabberImpl) - protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - CallPeerJabberImpl callPeer = basicTelephony.getActiveCallPeer(sid); - if(callPeer != null) - { - if(!CallPeerState.isOnHold(callPeer.getState())) - { - callPeer.putOnHold(true); - } - } - - if(!CallPeerState.isOnHold(this.getState())) - { - this.putOnHold(true); - } - } - transfer.setTo(to); - - transferSessionInfo.addExtension(transfer); - - Connection connection = protocolProvider.getConnection(); - PacketCollector collector = connection.createPacketCollector( - new PacketIDFilter(transferSessionInfo.getPacketID())); - protocolProvider.getConnection().sendPacket(transferSessionInfo); - - Packet result - = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); - - if(result == null) - { - // Log the failed transfer call and notify the user. - throw new OperationFailedException( - "No response to the \"transfer\" request.", - OperationFailedException.ILLEGAL_ARGUMENT); - } - else if (((IQ) result).getType() != IQ.Type.RESULT) - { - // Log the failed transfer call and notify the user. - throw new OperationFailedException( - "Remote peer does not manage call \"transfer\"." - + "Response to the \"transfer\" request is: " - + ((IQ) result).getType(), - OperationFailedException.ILLEGAL_ARGUMENT); - } - else - { - String message = ((sid == null) ? "Unattended" : "Attended") - + " transfer to: " - + to; - // Implements the SIP behavior: once the transfer is accepted, the - // current call is closed. - hangup( - false, - message, - new ReasonPacketExtension(Reason.SUCCESS, - message, - new TransferredPacketExtension())); - } - } - - /** - * {@inheritDoc} - */ - public String getEntity() - { - return getAddress(); - } - - /** - * {@inheritDoc} - * - * In Jingle there isn't an actual "direction" parameter. We use the - * <tt>senders</tt> field to calculate the direction. - */ - @Override - public MediaDirection getDirection(MediaType mediaType) - { - SendersEnum senders = getSenders(mediaType); - if (senders == SendersEnum.none) - return MediaDirection.INACTIVE; - else if (senders == null || senders == SendersEnum.both) - return MediaDirection.SENDRECV; - else if (senders == SendersEnum.initiator) - return isInitiator() - ? MediaDirection.RECVONLY - : MediaDirection.SENDONLY; - else //senders == SendersEnum.responder - return isInitiator() - ? MediaDirection.SENDONLY - : MediaDirection.RECVONLY; - } - - /** - * Get the current value of the <tt>senders</tt> field of the content with - * name <tt>mediaType</tt> in the Jingle session with this <tt>CallPeer</tt> - * @param mediaType the <tt>MediaType</tt> for which to get the current - * value of the <tt>senders</tt> field. - * @return the current value of the <tt>senders</tt> field of the content - * with name <tt>mediaType</tt> in the Jingle session with this - * <tt>CallPeer</tt>. - */ - public SendersEnum getSenders(MediaType mediaType) - { - if (MediaType.AUDIO.equals(mediaType)) - return audioSenders; - else if (MediaType.VIDEO.equals(mediaType)) - return videoSenders; - else - throw new IllegalArgumentException("mediaType"); - } - - /** - * Set the current value of the <tt>senders</tt> field of the content with - * name <tt>mediaType</tt> in the Jingle session with this <tt>CallPeer</tt> - * @param mediaType the <tt>MediaType</tt> for which to get the current - * value of the <tt>senders</tt> field. - * @param senders the value to set - */ - public void setSenders(MediaType mediaType, SendersEnum senders) - { - if (mediaType == null) - return; - else if (MediaType.AUDIO.equals(mediaType)) - this.audioSenders = senders; - else if (MediaType.VIDEO.equals(mediaType)) - this.videoSenders = senders; - else - throw new IllegalArgumentException("mediaType"); - } - - /** - * Gets the <tt>MediaType</tt> of <tt>content</tt>. If <tt>content</tt> - * does not have a <tt>description</tt> child and therefore not - * <tt>MediaType</tt> can be associated with it, tries to take the - * <tt>MediaType</tt> from the session's already established contents with - * the same name as <tt>content</tt> - * @param content the <tt>ContentPacketExtention</tt> for which to get the - * <tt>MediaType</tt> - * @return the <tt>MediaType</tt> of <tt>content</tt>. - */ - public MediaType getMediaType (ContentPacketExtension content) - { - String contentName = content.getName(); - if (contentName == null) - return null; - - MediaType mediaType = JingleUtils.getMediaType(content); - if (mediaType == null) - { - CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler(); - for (MediaType m : MediaType.values()) - { - ContentPacketExtension sessionContent - = mediaHandler.getRemoteContent(m.toString()); - if (sessionContent == null) - sessionContent = mediaHandler.getLocalContent(m.toString()); - - if (sessionContent != null - && contentName.equals(sessionContent.getName())) - { - mediaType = m; - break; - } - } - } - - return mediaType; - } -} +/*
+ * 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.impl.protocol.jabber;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.ContentPacketExtension.SendersEnum;
+import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+import org.jitsi.service.neomedia.*;
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.util.*;
+import org.jivesoftware.smackx.packet.*;
+
+/**
+ * Implements a Jabber <tt>CallPeer</tt>.
+ *
+ * @author Emil Ivov
+ * @author Lyubomir Marinov
+ * @author Boris Grozev
+ */
+public class CallPeerJabberImpl
+ extends AbstractCallPeerJabberGTalkImpl
+ <CallJabberImpl, CallPeerMediaHandlerJabberImpl, JingleIQ>
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>CallPeerJabberImpl</tt> class and its
+ * instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(CallPeerJabberImpl.class);
+
+ /**
+ * If the call is cancelled before session-initiate is sent.
+ */
+ private boolean cancelled = false;
+
+ /**
+ * Synchronization object for candidates available.
+ */
+ private final Object candSyncRoot = new Object();
+
+ /**
+ * If the content-add does not contains candidates.
+ */
+ private boolean contentAddWithNoCands = false;
+
+ /**
+ * If we have processed the session initiate.
+ */
+ private boolean sessionInitiateProcessed = false;
+
+ /**
+ * Synchronization object. Synchronization object? Wow, who would have
+ * thought! ;) Would be great to have a word on what we are syncing with it
+ */
+ private final Object sessionInitiateSyncRoot = new Object();
+
+ /**
+ * Synchronization object for SID.
+ */
+ private final Object sidSyncRoot = new Object();
+
+ /**
+ * The current value of the 'senders' field of the audio content in the
+ * Jingle session with this <tt>CallPeer</tt>.
+ * <tt>null</tt> should be interpreted as 'both', which is the default in
+ * Jingle if the XML attribute is missing.
+ */
+ private SendersEnum audioSenders = SendersEnum.none;
+
+ /**
+ * The current value of the 'senders' field of the video content in the
+ * Jingle session with this <tt>CallPeer</tt>.
+ * <tt>null</tt> should be interpreted as 'both', which is the default in
+ * Jingle if the XML attribute is missing.
+ */
+ private SendersEnum videoSenders = SendersEnum.none;
+
+ /**
+ * Creates a new call peer with address <tt>peerAddress</tt>.
+ *
+ * @param peerAddress the Jabber address of the new call peer.
+ * @param owningCall the call that contains this call peer.
+ */
+ public CallPeerJabberImpl(String peerAddress,
+ CallJabberImpl owningCall)
+ {
+ super(peerAddress, owningCall);
+
+ setMediaHandler(new CallPeerMediaHandlerJabberImpl(this));
+ }
+
+ /**
+ * Creates a new call peer with address <tt>peerAddress</tt>.
+ *
+ * @param peerAddress the Jabber address of the new call peer.
+ * @param owningCall the call that contains this call peer.
+ * @param sessionIQ The session-initiate <tt>JingleIQ</tt> which was
+ * received from <tt>peerAddress</tt> and caused the creation of this
+ * <tt>CallPeerJabberImpl</tt>
+ */
+ public CallPeerJabberImpl(String peerAddress,
+ CallJabberImpl owningCall,
+ JingleIQ sessionIQ)
+ {
+ this(peerAddress, owningCall);
+ this.sessionInitIQ = sessionIQ;
+ }
+
+ /**
+ * Send a session-accept <tt>JingleIQ</tt> to this <tt>CallPeer</tt>
+ * @throws OperationFailedException if we fail to create or send the
+ * response.
+ */
+ public synchronized void answer()
+ throws OperationFailedException
+ {
+ Iterable<ContentPacketExtension> answer;
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ try
+ {
+ mediaHandler
+ .getTransportManager()
+ .wrapupConnectivityEstablishment();
+ answer = mediaHandler.generateSessionAccept();
+ for (ContentPacketExtension c : answer)
+ setSenders(getMediaType(c), c.getSenders());
+ }
+ catch(Exception exc)
+ {
+ logger.info("Failed to answer an incoming call", exc);
+
+ //send an error response
+ String reasonText = "Error: " + exc.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ sessionInitIQ.getTo(),
+ sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(),
+ Reason.FAILED_APPLICATION,
+ reasonText);
+
+ setState(CallPeerState.FAILED, reasonText);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+
+ JingleIQ response
+ = JinglePacketFactory.createSessionAccept(
+ sessionInitIQ.getTo(),
+ sessionInitIQ.getFrom(),
+ getSID(),
+ answer);
+
+ //send the packet first and start the stream later in case the media
+ //relay needs to see it before letting hole punching techniques through.
+ getProtocolProvider().getConnection().sendPacket(response);
+
+ try
+ {
+ mediaHandler.start();
+ }
+ catch(UndeclaredThrowableException e)
+ {
+ Throwable exc = e.getUndeclaredThrowable();
+
+ logger.info("Failed to establish a connection", exc);
+
+ //send an error response
+ String reasonText = "Error: " + exc.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ sessionInitIQ.getTo(),
+ sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(),
+ Reason.GENERAL_ERROR,
+ reasonText);
+
+ setState(CallPeerState.FAILED, reasonText);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+
+ //tell everyone we are connected so that the audio notifications would
+ //stop
+ setState(CallPeerState.CONNECTED);
+ }
+
+ /**
+ * Returns the session ID of the Jingle session associated with this call.
+ *
+ * @return the session ID of the Jingle session associated with this call.
+ */
+ @Override
+ public String getSID()
+ {
+ return sessionInitIQ != null ? sessionInitIQ.getSID() : null;
+ }
+
+ /**
+ * Returns the IQ ID of the Jingle session-initiate packet associated with
+ * this call.
+ *
+ * @return the IQ ID of the Jingle session-initiate packet associated with
+ * this call.
+ */
+ public JingleIQ getSessionIQ()
+ {
+ return sessionInitIQ;
+ }
+
+ /**
+ * Ends the call with this <tt>CallPeer</tt>. Depending on the state
+ * of the peer the method would send a CANCEL, BYE, or BUSY_HERE message
+ * and set the new state to DISCONNECTED.
+ *
+ * @param failed indicates if the hangup is following to a call failure or
+ * simply a disconnect
+ * @param reasonText the text, if any, to be set on the
+ * <tt>ReasonPacketExtension</tt> as the value of its
+ * @param reasonOtherExtension the <tt>PacketExtension</tt>, if any, to be
+ * set on the <tt>ReasonPacketExtension</tt> as the value of its
+ * <tt>otherExtension</tt> property
+ */
+ public void hangup(boolean failed,
+ String reasonText,
+ PacketExtension reasonOtherExtension)
+ {
+ CallPeerState prevPeerState = getState();
+
+ // do nothing if the call is already ended
+ if (CallPeerState.DISCONNECTED.equals(prevPeerState)
+ || CallPeerState.FAILED.equals(prevPeerState))
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Ignoring a request to hangup a call peer "
+ + "that is already DISCONNECTED");
+ return;
+ }
+
+ setState(
+ failed ? CallPeerState.FAILED : CallPeerState.DISCONNECTED,
+ reasonText);
+
+ JingleIQ responseIQ = null;
+
+ if (prevPeerState.equals(CallPeerState.CONNECTED)
+ || CallPeerState.isOnHold(prevPeerState))
+ {
+ responseIQ = JinglePacketFactory.createBye(
+ getProtocolProvider().getOurJID(), peerJID, getSID());
+ }
+ else if (CallPeerState.CONNECTING.equals(prevPeerState)
+ || CallPeerState.CONNECTING_WITH_EARLY_MEDIA.equals(prevPeerState)
+ || CallPeerState.ALERTING_REMOTE_SIDE.equals(prevPeerState))
+ {
+ String jingleSID = getSID();
+
+ if(jingleSID == null)
+ {
+ synchronized(sidSyncRoot)
+ {
+ // we cancelled the call too early because the jingleSID
+ // is null (i.e. the session-initiate has not been created)
+ // and no need to send the session-terminate
+ cancelled = true;
+ return;
+ }
+ }
+
+ responseIQ = JinglePacketFactory.createCancel(
+ getProtocolProvider().getOurJID(), peerJID, getSID());
+ }
+ else if (prevPeerState.equals(CallPeerState.INCOMING_CALL))
+ {
+ responseIQ = JinglePacketFactory.createBusy(
+ getProtocolProvider().getOurJID(), peerJID, getSID());
+ }
+ else if (prevPeerState.equals(CallPeerState.BUSY)
+ || prevPeerState.equals(CallPeerState.FAILED))
+ {
+ // For FAILED and BUSY we only need to update CALL_STATUS
+ // as everything else has been done already.
+ }
+ else
+ {
+ logger.info("Could not determine call peer state!");
+ }
+
+ if (responseIQ != null)
+ {
+ if (reasonOtherExtension != null)
+ {
+ ReasonPacketExtension reason
+ = (ReasonPacketExtension)
+ responseIQ.getExtension(
+ ReasonPacketExtension.ELEMENT_NAME,
+ ReasonPacketExtension.NAMESPACE);
+
+ if (reason != null)
+ {
+ reason.setOtherExtension(reasonOtherExtension);
+ }
+ else if(reasonOtherExtension instanceof ReasonPacketExtension)
+ {
+ responseIQ.setReason(
+ (ReasonPacketExtension)reasonOtherExtension);
+ }
+ }
+
+ getProtocolProvider().getConnection().sendPacket(responseIQ);
+ }
+ }
+
+ /**
+ * Creates and sends a session-initiate {@link JingleIQ}.
+ *
+ * @param sessionInitiateExtensions a collection of additional and optional
+ * <tt>PacketExtension</tt>s to be added to the <tt>session-initiate</tt>
+ * {@link JingleIQ} which is to initiate the session with this
+ * <tt>CallPeerJabberImpl</tt>
+ * @throws OperationFailedException exception
+ */
+ protected synchronized void initiateSession(
+ Iterable<PacketExtension> sessionInitiateExtensions)
+ throws OperationFailedException
+ {
+ initiator = false;
+
+ //Create the media description that we'd like to send to the other side.
+ List<ContentPacketExtension> offer
+ = getMediaHandler().createContentList();
+
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+
+ synchronized(sidSyncRoot)
+ {
+ sessionInitIQ
+ = JinglePacketFactory.createSessionInitiate(
+ protocolProvider.getOurJID(),
+ this.peerJID,
+ JingleIQ.generateSID(),
+ offer);
+
+ if(cancelled)
+ {
+ // we cancelled the call too early so no need to send the
+ // session-initiate to peer
+ getMediaHandler().getTransportManager().close();
+ return;
+ }
+ }
+
+ if (sessionInitiateExtensions != null)
+ {
+ for (PacketExtension sessionInitiateExtension
+ : sessionInitiateExtensions)
+ {
+ sessionInitIQ.addExtension(sessionInitiateExtension);
+ }
+ }
+
+ protocolProvider.getConnection().sendPacket(sessionInitIQ);
+ }
+
+ /**
+ * Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has
+ * been received. This <tt>CallPeerJabberImpl</tt> uses the part of the
+ * information provided in the specified <tt>conferenceIQ</tt> which
+ * concerns it only.
+ *
+ * @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been
+ * received
+ */
+ void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ)
+ {
+ /*
+ * CallPeerJabberImpl does not itself/directly know the specifics
+ * related to the channels allocated on the Jitsi Videobridge server.
+ * The channels contain transport and media-related information so
+ * forward the notification to CallPeerMediaHandlerJabberImpl.
+ */
+ getMediaHandler().processColibriConferenceIQ(conferenceIQ);
+ }
+
+ /**
+ * Processes the content-accept {@link JingleIQ}.
+ *
+ * @param content The {@link JingleIQ} that contains content that remote
+ * peer has accepted
+ */
+ public void processContentAccept(JingleIQ content)
+ {
+ List<ContentPacketExtension> contents = content.getContentList();
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ try
+ {
+ mediaHandler
+ .getTransportManager()
+ .wrapupConnectivityEstablishment();
+ mediaHandler.processAnswer(contents);
+ for (ContentPacketExtension c : contents)
+ setSenders(getMediaType(c), c.getSenders());
+ }
+ catch (Exception e)
+ {
+ logger.warn("Failed to process a content-accept", e);
+
+ // Send an error response.
+ String reason = "Error: " + e.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ getProtocolProvider().getOurJID(),
+ peerJID,
+ sessionInitIQ.getSID(),
+ Reason.INCOMPATIBLE_PARAMETERS,
+ reason);
+
+ setState(CallPeerState.FAILED, reason);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+
+ mediaHandler.start();
+ }
+
+ /**
+ * Processes the content-add {@link JingleIQ}.
+ *
+ * @param content The {@link JingleIQ} that contains content that remote
+ * peer wants to be added
+ */
+ public void processContentAdd(final JingleIQ content)
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+ List<ContentPacketExtension> contents = content.getContentList();
+ Iterable<ContentPacketExtension> answerContents;
+ JingleIQ contentIQ;
+ boolean noCands = false;
+ MediaStream oldVideoStream = mediaHandler.getStream(MediaType.VIDEO);
+
+ if(logger.isInfoEnabled())
+ logger.info("Looking for candidates in content-add.");
+ try
+ {
+ if(!contentAddWithNoCands)
+ {
+ mediaHandler.processOffer(contents);
+
+ /*
+ * Gingle transport will not put candidate in session-initiate
+ * and content-add.
+ */
+ for(ContentPacketExtension c : contents)
+ {
+ if(JingleUtils.getFirstCandidate(c, 1) == null)
+ {
+ contentAddWithNoCands = true;
+ noCands = true;
+ }
+ }
+ }
+
+ // if no candidates are present, launch a new Thread which will
+ // process and wait for the connectivity establishment (otherwise
+ // the existing thread will be blocked and thus cannot receive
+ // transport-info with candidates
+ if(noCands)
+ {
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ synchronized(candSyncRoot)
+ {
+ candSyncRoot.wait();
+ }
+ }
+ catch(InterruptedException e)
+ {
+ }
+
+ processContentAdd(content);
+ contentAddWithNoCands = false;
+ }
+ }.start();
+ if(logger.isInfoEnabled())
+ logger.info("No candidates found in content-add, started "
+ + "new thread.");
+ return;
+ }
+
+ mediaHandler
+ .getTransportManager()
+ .wrapupConnectivityEstablishment();
+ if(logger.isInfoEnabled())
+ logger.info("Wrapping up connectivity establishment");
+ answerContents = mediaHandler.generateSessionAccept();
+ contentIQ = null;
+ }
+ catch(Exception e)
+ {
+ logger.warn("Exception occurred", e);
+
+ answerContents = null;
+ contentIQ
+ = JinglePacketFactory.createContentReject(
+ getProtocolProvider().getOurJID(),
+ this.peerJID,
+ getSID(),
+ answerContents);
+ }
+
+ if(contentIQ == null)
+ {
+ /* send content-accept */
+ contentIQ
+ = JinglePacketFactory.createContentAccept(
+ getProtocolProvider().getOurJID(),
+ this.peerJID,
+ getSID(),
+ answerContents);
+ for (ContentPacketExtension c : answerContents)
+ setSenders(getMediaType(c), c.getSenders());
+ }
+
+ getProtocolProvider().getConnection().sendPacket(contentIQ);
+ mediaHandler.start();
+
+ /*
+ * If a remote peer turns her video on in a conference which is hosted
+ * by the local peer and the local peer is not streaming her local
+ * video, reinvite the other remote peers to enable RTP translation.
+ */
+ if (oldVideoStream == null)
+ {
+ MediaStream newVideoStream
+ = mediaHandler.getStream(MediaType.VIDEO);
+
+ if ((newVideoStream != null)
+ && mediaHandler.isRTPTranslationEnabled(MediaType.VIDEO))
+ {
+ try
+ {
+ getCall().modifyVideoContent();
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error("Failed to enable RTP translation", ofe);
+ }
+ }
+ }
+ }
+
+ /**
+ * Processes the content-modify {@link JingleIQ}.
+ *
+ * @param content The {@link JingleIQ} that contains content that remote
+ * peer wants to be modified
+ */
+ public void processContentModify(JingleIQ content)
+ {
+ ContentPacketExtension ext = content.getContentList().get(0);
+ MediaType mediaType = getMediaType(ext);
+
+ try
+ {
+ boolean modify
+ = (ext.getFirstChildOfType(RtpDescriptionPacketExtension.class)
+ != null);
+
+ getMediaHandler().reinitContent(ext.getName(), ext, modify);
+
+ setSenders(mediaType, ext.getSenders());
+
+ if (MediaType.VIDEO.equals(mediaType))
+ getCall().modifyVideoContent();
+ }
+ catch(Exception e)
+ {
+ logger.info("Failed to process an incoming content-modify", e);
+
+ // Send an error response.
+ String reason = "Error: " + e.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ getProtocolProvider().getOurJID(),
+ peerJID,
+ sessionInitIQ.getSID(),
+ Reason.INCOMPATIBLE_PARAMETERS,
+ reason);
+
+ setState(CallPeerState.FAILED, reason);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+ }
+
+ /**
+ * Processes the content-reject {@link JingleIQ}.
+ *
+ * @param content The {@link JingleIQ}
+ */
+ public void processContentReject(JingleIQ content)
+ {
+ if(content.getContentList().isEmpty())
+ {
+ //send an error response;
+ JingleIQ errResp = JinglePacketFactory.createSessionTerminate(
+ sessionInitIQ.getTo(), sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS,
+ "Error: content rejected");
+
+ setState(CallPeerState.FAILED, "Error: content rejected");
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+ }
+
+ /**
+ * Processes the content-remove {@link JingleIQ}.
+ *
+ * @param content The {@link JingleIQ} that contains content that remote
+ * peer wants to be removed
+ */
+ public void processContentRemove(JingleIQ content)
+ {
+ List<ContentPacketExtension> contents = content.getContentList();
+ boolean videoContentRemoved = false;
+
+ if (!contents.isEmpty())
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ for(ContentPacketExtension c : contents)
+ {
+ mediaHandler.removeContent(c.getName());
+
+ MediaType mediaType = getMediaType(c);
+ setSenders(mediaType, SendersEnum.none);
+
+ if (MediaType.VIDEO.equals(mediaType))
+ videoContentRemoved = true;
+ }
+
+ /*
+ * TODO XEP-0166: Jingle says: If the content-remove results in zero
+ * content definitions for the session, the entity that receives the
+ * content-remove SHOULD send a session-terminate action to the
+ * other party (since a session with no content definitions is
+ * void).
+ */
+ }
+
+ if (videoContentRemoved)
+ {
+ // removing of the video content might affect the other sessions
+ // in the call
+ try
+ {
+ getCall().modifyVideoContent();
+ }
+ catch (Exception e)
+ {
+ logger.warn("Failed to update Jingle sessions");
+ }
+ }
+ }
+
+ /**
+ * Processes a session-accept {@link JingleIQ}.
+ *
+ * @param sessionInitIQ The session-accept {@link JingleIQ} to process.
+ */
+ public void processSessionAccept(JingleIQ sessionInitIQ)
+ {
+ this.sessionInitIQ = sessionInitIQ;
+
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+ List<ContentPacketExtension> answer = sessionInitIQ.getContentList();
+
+ try
+ {
+ mediaHandler
+ .getTransportManager()
+ .wrapupConnectivityEstablishment();
+ mediaHandler.processAnswer(answer);
+ for (ContentPacketExtension c : answer)
+ setSenders(getMediaType(c), c.getSenders());
+ }
+ catch(Exception exc)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("Failed to process a session-accept", exc);
+
+ //send an error response;
+ JingleIQ errResp = JinglePacketFactory.createSessionTerminate(
+ sessionInitIQ.getTo(), sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS,
+ exc.getClass().getName() + ": " + exc.getMessage());
+
+ setState(CallPeerState.FAILED, "Error: " + exc.getMessage());
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+
+ //tell everyone we are connected so that the audio notifications would
+ //stop
+ setState(CallPeerState.CONNECTED);
+
+ mediaHandler.start();
+
+ /*
+ * If video was added to the call after we sent the session-initiate
+ * to this peer, it needs to be added to this peer's session with a
+ * content-add.
+ */
+ sendModifyVideoContent();
+ }
+
+ /**
+ * Handles the specified session <tt>info</tt> packet according to its
+ * content.
+ *
+ * @param info the {@link SessionInfoPacketExtension} that we just received.
+ */
+ public void processSessionInfo(SessionInfoPacketExtension info)
+ {
+ switch (info.getType())
+ {
+ case ringing:
+ setState(CallPeerState.ALERTING_REMOTE_SIDE);
+ break;
+ case hold:
+ getMediaHandler().setRemotelyOnHold(true);
+ reevalRemoteHoldStatus();
+ break;
+ case unhold:
+ case active:
+ getMediaHandler().setRemotelyOnHold(false);
+ reevalRemoteHoldStatus();
+ break;
+ default:
+ logger.warn("Received SessionInfoPacketExtension of unknown type");
+ }
+ }
+
+ /**
+ * Processes the session initiation {@link JingleIQ} that we were created
+ * with, passing its content to the media handler and then sends either a
+ * "session-info/ringing" or a "session-terminate" response.
+ *
+ * @param sessionInitIQ The {@link JingleIQ} that created the session that
+ * we are handling here.
+ */
+ protected synchronized void processSessionInitiate(JingleIQ sessionInitIQ)
+ {
+ // Do initiate the session.
+ this.sessionInitIQ = sessionInitIQ;
+ this.initiator = true;
+
+ // This is the SDP offer that came from the initial session-initiate.
+ // Contrary to SIP, we are guaranteed to have content because XEP-0166
+ // says: "A session consists of at least one content type at a time."
+ List<ContentPacketExtension> offer = sessionInitIQ.getContentList();
+
+ try
+ {
+ getMediaHandler().processOffer(offer);
+
+ CoinPacketExtension coin = null;
+
+ for(PacketExtension ext : sessionInitIQ.getExtensions())
+ {
+ if(ext.getElementName().equals(
+ CoinPacketExtension.ELEMENT_NAME))
+ {
+ coin = (CoinPacketExtension)ext;
+ break;
+ }
+ }
+
+ /* does the call peer acts as a conference focus ? */
+ if(coin != null)
+ {
+ setConferenceFocus(Boolean.parseBoolean(
+ (String)coin.getAttribute("isfocus")));
+ }
+ }
+ catch(Exception ex)
+ {
+ logger.info("Failed to process an incoming session initiate", ex);
+
+ //send an error response;
+ String reasonText = "Error: " + ex.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ sessionInitIQ.getTo(),
+ sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(),
+ Reason.INCOMPATIBLE_PARAMETERS,
+ reasonText);
+
+ setState(CallPeerState.FAILED, reasonText);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+ return;
+ }
+
+ // If we do not get the info about the remote peer yet. Get it right
+ // now.
+ if(this.getDiscoveryInfo() == null)
+ {
+ String calleeURI = sessionInitIQ.getFrom();
+ retrieveDiscoveryInfo(calleeURI);
+ }
+
+ //send a ringing response
+ if (logger.isTraceEnabled())
+ logger.trace("will send ringing response: ");
+
+ getProtocolProvider().getConnection().sendPacket(
+ JinglePacketFactory.createRinging(sessionInitIQ));
+
+ synchronized(sessionInitiateSyncRoot)
+ {
+ sessionInitiateProcessed = true;
+ sessionInitiateSyncRoot.notify();
+ }
+
+ //if this is a 3264 initiator, let's give them an early peek at our
+ //answer so that they could start ICE (SIP-2-Jingle gateways won't
+ //be able to send their candidates unless they have this)
+ DiscoverInfo discoverInfo = getDiscoveryInfo();
+ if ((discoverInfo != null)
+ && discoverInfo.containsFeature(
+ ProtocolProviderServiceJabberImpl.URN_IETF_RFC_3264))
+ {
+ getProtocolProvider().getConnection().sendPacket(
+ JinglePacketFactory.createDescriptionInfo(
+ sessionInitIQ.getTo(),
+ sessionInitIQ.getFrom(),
+ sessionInitIQ.getSID(),
+ getMediaHandler().getLocalContentList()));
+ }
+ }
+
+ /**
+ * Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a
+ * reason to the user, if there is one.
+ *
+ * @param jingleIQ the {@link JingleIQ} that's terminating our session.
+ */
+ public void processSessionTerminate(JingleIQ jingleIQ)
+ {
+ String reasonStr = "Call ended by remote side.";
+ ReasonPacketExtension reasonExt = jingleIQ.getReason();
+
+ if(reasonExt != null)
+ {
+ Reason reason = reasonExt.getReason();
+
+ if(reason != null)
+ reasonStr += " Reason: " + reason.toString() + ".";
+
+ String text = reasonExt.getText();
+
+ if(text != null)
+ reasonStr += " " + text;
+ }
+
+ setState(CallPeerState.DISCONNECTED, reasonStr);
+ }
+
+ /**
+ * Processes a specific "XEP-0251: Jingle Session Transfer"
+ * <tt>transfer</tt> packet (extension).
+ *
+ * @param transfer the "XEP-0251: Jingle Session Transfer" transfer packet
+ * (extension) to process
+ * @throws OperationFailedException if anything goes wrong while processing
+ * the specified <tt>transfer</tt> packet (extension)
+ */
+ public void processTransfer(TransferPacketExtension transfer)
+ throws OperationFailedException
+ {
+ String attendantAddress = transfer.getFrom();
+
+ if (attendantAddress == null)
+ {
+ throw new OperationFailedException(
+ "Session transfer must contain a \'from\' attribute value.",
+ OperationFailedException.ILLEGAL_ARGUMENT);
+ }
+
+ String calleeAddress = transfer.getTo();
+
+ if (calleeAddress == null)
+ {
+ throw new OperationFailedException(
+ "Session transfer must contain a \'to\' attribute value.",
+ OperationFailedException.ILLEGAL_ARGUMENT);
+ }
+
+ // Checks if the transfer remote peer is contained by the roster of this
+ // account.
+ Roster roster = getProtocolProvider().getConnection().getRoster();
+ if(!roster.contains(StringUtils.parseBareAddress(calleeAddress)))
+ {
+ String failedMessage =
+ "Tranfer impossible:\n"
+ + "Account roster does not contain tansfer peer: "
+ + StringUtils.parseBareAddress(calleeAddress);
+ setState(CallPeerState.FAILED, failedMessage);
+ logger.info(failedMessage);
+ }
+
+ OperationSetBasicTelephonyJabberImpl basicTelephony
+ = (OperationSetBasicTelephonyJabberImpl)
+ getProtocolProvider()
+ .getOperationSet(OperationSetBasicTelephony.class);
+ CallJabberImpl calleeCall = new CallJabberImpl(basicTelephony);
+ TransferPacketExtension calleeTransfer = new TransferPacketExtension();
+ String sid = transfer.getSID();
+
+ calleeTransfer.setFrom(attendantAddress);
+ if (sid != null)
+ {
+ calleeTransfer.setSID(sid);
+ calleeTransfer.setTo(calleeAddress);
+ }
+ basicTelephony.createOutgoingCall(
+ calleeCall,
+ calleeAddress,
+ Arrays.asList(new PacketExtension[] { calleeTransfer }));
+ }
+
+ /**
+ * Processes the <tt>transport-info</tt> {@link JingleIQ}.
+ *
+ * @param jingleIQ the <tt>transport-info</tt> {@link JingleIQ} to process
+ */
+ public void processTransportInfo(JingleIQ jingleIQ)
+ {
+ /*
+ * The transport-info action is used to exchange transport candidates so
+ * it only concerns the mediaHandler.
+ */
+ try
+ {
+ if(isInitiator())
+ {
+ synchronized(sessionInitiateSyncRoot)
+ {
+ if(!sessionInitiateProcessed)
+ {
+ try
+ {
+ sessionInitiateSyncRoot.wait();
+ }
+ catch(InterruptedException e)
+ {
+ }
+ }
+ }
+ }
+
+ getMediaHandler().processTransportInfo(
+ jingleIQ.getContentList());
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.warn("Failed to process an incoming transport-info", ofe);
+
+ //send an error response
+ String reasonText = "Error: " + ofe.getMessage();
+ JingleIQ errResp
+ = JinglePacketFactory.createSessionTerminate(
+ getProtocolProvider().getOurJID(),
+ peerJID,
+ sessionInitIQ.getSID(),
+ Reason.GENERAL_ERROR,
+ reasonText);
+
+ setState(CallPeerState.FAILED, reasonText);
+ getProtocolProvider().getConnection().sendPacket(errResp);
+
+ return;
+ }
+
+ synchronized(candSyncRoot)
+ {
+ candSyncRoot.notify();
+ }
+ }
+
+ /**
+ * Puts the <tt>CallPeer</tt> represented by this instance on or off hold.
+ *
+ * @param onHold <tt>true</tt> to have the <tt>CallPeer</tt> put on hold;
+ * <tt>false</tt>, otherwise
+ *
+ * @throws OperationFailedException if we fail to construct or send the
+ * INVITE request putting the remote side on/off hold.
+ */
+ public void putOnHold(boolean onHold)
+ throws OperationFailedException
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ mediaHandler.setLocallyOnHold(onHold);
+
+ SessionInfoType type;
+
+ if(onHold)
+ type = SessionInfoType.hold;
+ else
+ {
+ type = SessionInfoType.unhold;
+ getMediaHandler().reinitAllContents();
+ }
+
+ //we are now on hold and need to realize this before potentially
+ //spoiling it all with an exception while sending the packet :).
+ reevalLocalHoldStatus();
+
+ JingleIQ onHoldIQ = JinglePacketFactory.createSessionInfo(
+ getProtocolProvider().getOurJID(),
+ peerJID,
+ getSID(),
+ type);
+
+ getProtocolProvider().getConnection().sendPacket(onHoldIQ);
+ }
+
+ /**
+ * Send a <tt>content-add</tt> to add video setup.
+ */
+ private void sendAddVideoContent()
+ {
+ List<ContentPacketExtension> contents;
+
+ try
+ {
+ contents = getMediaHandler().createContentList(MediaType.VIDEO);
+ }
+ catch(Exception exc)
+ {
+ logger.warn("Failed to gather content for video type", exc);
+ return;
+ }
+
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+ JingleIQ contentIQ
+ = JinglePacketFactory.createContentAdd(
+ protocolProvider.getOurJID(),
+ this.peerJID,
+ getSID(),
+ contents);
+
+ protocolProvider.getConnection().sendPacket(contentIQ);
+ }
+
+ /**
+ * Sends a <tt>content</tt> message to reflect changes in the setup such as
+ * the local peer/user becoming a conference focus.
+ */
+ public void sendCoinSessionInfo()
+ {
+ JingleIQ sessionInfoIQ
+ = JinglePacketFactory.createSessionInfo(
+ getProtocolProvider().getOurJID(),
+ this.peerJID,
+ getSID());
+ CoinPacketExtension coinExt
+ = new CoinPacketExtension(getCall().isConferenceFocus());
+
+ sessionInfoIQ.addExtension(coinExt);
+ getProtocolProvider().getConnection().sendPacket(sessionInfoIQ);
+ }
+
+ /**
+ * Returns the <tt>MediaDirection</tt> that should be set for the content
+ * of type <tt>mediaType</tt> in the Jingle session for this
+ * <tt>CallPeer</tt>.
+ * If we are the focus of a conference and are doing RTP translation,
+ * takes into account the other <tt>CallPeer</tt>s in the <tt>Call</tt>.
+ *
+ * @param mediaType the <tt>MediaType</tt> for which to return the
+ * <tt>MediaDirection</tt>
+ * @return the <tt>MediaDirection</tt> that should be used for the content
+ * of type <tt>mediaType</tt> in the Jingle session for this
+ * <tt>CallPeer</tt>.
+ */
+ private MediaDirection getDirectionForJingle(MediaType mediaType)
+ {
+ MediaDirection direction = MediaDirection.INACTIVE;
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ // If we are streaming media, the direction should allow sending
+ if ( (MediaType.AUDIO == mediaType &&
+ mediaHandler.isLocalAudioTransmissionEnabled()) ||
+ (MediaType.VIDEO == mediaType &&
+ isLocalVideoStreaming()))
+ direction = direction.or(MediaDirection.SENDONLY);
+
+ // If we are receiving media from this CallPeer, the direction should
+ // allow receiving
+ SendersEnum senders = getSenders(mediaType);
+ if (senders == null || senders == SendersEnum.both ||
+ (isInitiator() && senders == SendersEnum.initiator) ||
+ (!isInitiator() && senders == SendersEnum.responder))
+ direction = direction.or(MediaDirection.RECVONLY);
+
+ // If we are the focus of a conference and we are receiving media from
+ // another CallPeer in the same Call, the direction should allow sending
+ if (getCall().isConferenceFocus())
+ {
+ for (CallPeerJabberImpl peer : getCall().getCallPeerList())
+ {
+ if (peer != this)
+ {
+ senders = peer.getSenders(mediaType);
+ if (senders == null || senders == SendersEnum.both ||
+ (peer.isInitiator()
+ && senders == SendersEnum.initiator) ||
+ (!peer.isInitiator()
+ && senders == SendersEnum.responder))
+ {
+ direction = direction.or(MediaDirection.SENDONLY);
+ break;
+ }
+ }
+ }
+ }
+
+ return direction;
+ }
+
+ /**
+ * Send, if necessary, a jingle <tt>content</tt> message to reflect change
+ * in video setup. Whether the jingle session should have a video content,
+ * and if so, the value of the <tt>senders</tt> field is determined
+ * based on whether we are streaming local video and, if we are the focus
+ * of a conference, on the other peers in the conference.
+ * The message can be content-modify if video content exists (and the
+ * <tt>senders</tt> field changes), content-add or content-remove.
+ *
+ * @return <tt>true</tt> if a jingle <tt>content</tt> message was sent.
+ */
+ public boolean sendModifyVideoContent()
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+ MediaDirection direction = getDirectionForJingle(MediaType.VIDEO);
+
+ ContentPacketExtension remoteContent
+ = mediaHandler.getLocalContent(MediaType.VIDEO.toString());
+
+ if (remoteContent == null)
+ {
+ if (direction == MediaDirection.INACTIVE)
+ {
+ // no video content, none needed
+ return false;
+ }
+ else
+ {
+ if (getState() == CallPeerState.CONNECTED)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("Adding video content for " + this);
+ sendAddVideoContent();
+ return true;
+ }
+ return false;
+ }
+ }
+ else
+ {
+ if (direction == MediaDirection.INACTIVE)
+ {
+ sendRemoveVideoContent();
+ return true;
+ }
+ }
+
+ SendersEnum senders = getSenders(MediaType.VIDEO);
+ if (senders == null)
+ senders = SendersEnum.both;
+
+ SendersEnum newSenders = SendersEnum.none;
+ if (MediaDirection.SENDRECV == direction)
+ newSenders = SendersEnum.both;
+ else if (MediaDirection.RECVONLY == direction)
+ newSenders = isInitiator()
+ ? SendersEnum.initiator : SendersEnum.responder;
+ else if (MediaDirection.SENDONLY == direction)
+ newSenders = isInitiator()
+ ? SendersEnum.responder : SendersEnum.initiator;
+
+ /*
+ * Send Content-Modify
+ */
+ ContentPacketExtension ext = new ContentPacketExtension();
+ String remoteContentName = remoteContent.getName();
+
+ ext.setSenders(newSenders);
+ ext.setCreator(remoteContent.getCreator());
+ ext.setName(remoteContentName);
+
+ if (newSenders != senders)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("Sending content modify, senders: "
+ + senders + "->" + newSenders);
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+ JingleIQ contentIQ
+ = JinglePacketFactory.createContentModify(
+ protocolProvider.getOurJID(),
+ this.peerJID,
+ getSID(),
+ ext);
+
+ protocolProvider.getConnection().sendPacket(contentIQ);
+ }
+
+ try
+ {
+ mediaHandler.reinitContent(remoteContentName, ext, false);
+ mediaHandler.start();
+ }
+ catch(Exception e)
+ {
+ logger.warn("Exception occurred during media reinitialization", e);
+ }
+
+ return (newSenders != senders);
+ }
+
+ /**
+ * Send a <tt>content</tt> message to reflect change in video setup (start
+ * or stop).
+ */
+ public void sendModifyVideoResolutionContent()
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+ ContentPacketExtension remoteContent
+ = mediaHandler.getRemoteContent(MediaType.VIDEO.toString());
+ ContentPacketExtension content;
+
+ logger.info("send modify-content to change resolution");
+
+ // send content-modify with RTP description
+
+ // create content list with resolution
+ try
+ {
+ content = mediaHandler.createContentForMedia(MediaType.VIDEO);
+ }
+ catch (Exception e)
+ {
+ logger.warn("Failed to gather content for video type", e);
+ return;
+ }
+
+ // if we are only receiving video senders is null
+ SendersEnum senders = remoteContent.getSenders();
+
+ if (senders != null)
+ content.setSenders(senders);
+
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+ JingleIQ contentIQ
+ = JinglePacketFactory.createContentModify(
+ protocolProvider.getOurJID(),
+ this.peerJID,
+ getSID(),
+ content);
+
+ protocolProvider.getConnection().sendPacket(contentIQ);
+
+ try
+ {
+ mediaHandler.reinitContent(remoteContent.getName(), content, false);
+ mediaHandler.start();
+ }
+ catch(Exception e)
+ {
+ logger.warn("Exception occurred when media reinitialization", e);
+ }
+ }
+
+ /**
+ * Send a <tt>content-remove</tt> to remove video setup.
+ */
+ private void sendRemoveVideoContent()
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+
+ ContentPacketExtension content = new ContentPacketExtension();
+ ContentPacketExtension remoteContent
+ = mediaHandler.getRemoteContent(MediaType.VIDEO.toString());
+ if (remoteContent == null)
+ return;
+ String remoteContentName = remoteContent.getName();
+
+ content.setName(remoteContentName);
+ content.setCreator(remoteContent.getCreator());
+ content.setSenders(remoteContent.getSenders());
+
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+ JingleIQ contentIQ
+ = JinglePacketFactory.createContentRemove(
+ protocolProvider.getOurJID(),
+ this.peerJID,
+ getSID(),
+ Arrays.asList(content));
+
+ protocolProvider.getConnection().sendPacket(contentIQ);
+ mediaHandler.removeContent(remoteContentName);
+ setSenders(MediaType.VIDEO, SendersEnum.none);
+ }
+
+ /**
+ * Sends local candidate addresses from the local peer to the remote peer
+ * using the <tt>transport-info</tt> {@link JingleIQ}.
+ *
+ * @param contents the local candidate addresses to be sent from the local
+ * peer to the remote peer using the <tt>transport-info</tt>
+ * {@link JingleIQ}
+ */
+ protected void sendTransportInfo(Iterable<ContentPacketExtension> contents)
+ {
+ // if the call is canceled, do not start sending candidates in
+ // transport-info
+ if(cancelled)
+ return;
+
+ JingleIQ transportInfo = new JingleIQ();
+
+ for (ContentPacketExtension content : contents)
+ transportInfo.addContent(content);
+
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+
+ transportInfo.setAction(JingleAction.TRANSPORT_INFO);
+ transportInfo.setFrom(protocolProvider.getOurJID());
+ transportInfo.setSID(getSID());
+ transportInfo.setTo(getAddress());
+ transportInfo.setType(IQ.Type.SET);
+
+ PacketCollector collector
+ = protocolProvider.getConnection().createPacketCollector(
+ new PacketIDFilter(transportInfo.getPacketID()));
+
+ protocolProvider.getConnection().sendPacket(transportInfo);
+ collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ }
+
+ @Override
+ public void setState(CallPeerState newState, String reason, int reasonCode)
+ {
+ CallPeerState oldState = getState();
+ try
+ {
+ /*
+ * We need to dispose of the transport manager before the
+ * 'call' field is set to null, because if Jitsi Videobridge is in
+ * use, it (the call) is needed in order to expire the
+ * Videobridge channels.
+ */
+ if (CallPeerState.DISCONNECTED.equals(newState)
+ || CallPeerState.FAILED.equals(newState))
+ getMediaHandler().getTransportManager().close();
+ }
+ finally
+ {
+ super.setState(newState, reason, reasonCode);
+ }
+
+ if (CallPeerState.isOnHold(oldState)
+ && CallPeerState.CONNECTED.equals(newState))
+ {
+ try
+ {
+ getCall().modifyVideoContent();
+ }
+ catch (OperationFailedException ofe)
+ {
+ logger.error("Failed to update call video state after " +
+ "'hold' status removed for "+this);
+ }
+ }
+ }
+
+ /**
+ * Transfer (in the sense of call transfer) this <tt>CallPeer</tt> to a
+ * specific callee address which may optionally be participating in an
+ * active <tt>Call</tt>.
+ *
+ * @param to the address of the callee to transfer this <tt>CallPeer</tt> to
+ * @param sid the Jingle session ID of the active <tt>Call</tt> between the
+ * local peer and the callee in the case of attended transfer; <tt>null</tt>
+ * in the case of unattended transfer
+ * @throws OperationFailedException if something goes wrong
+ */
+ protected void transfer(String to, String sid)
+ throws OperationFailedException
+ {
+ JingleIQ transferSessionInfo = new JingleIQ();
+ ProtocolProviderServiceJabberImpl protocolProvider
+ = getProtocolProvider();
+
+ transferSessionInfo.setAction(JingleAction.SESSION_INFO);
+ transferSessionInfo.setFrom(protocolProvider.getOurJID());
+ transferSessionInfo.setSID(getSID());
+ transferSessionInfo.setTo(getAddress());
+ transferSessionInfo.setType(IQ.Type.SET);
+
+ TransferPacketExtension transfer = new TransferPacketExtension();
+
+ // Attended transfer.
+ if (sid != null)
+ {
+ /*
+ * Not really sure what the value of the "from" attribute of the
+ * "transfer" element should be but the examples in "XEP-0251:
+ * Jingle Session Transfer" has it in the case of attended transfer.
+ */
+ transfer.setFrom(protocolProvider.getOurJID());
+ transfer.setSID(sid);
+
+ // Puts on hold the 2 calls before making the attended transfer.
+ OperationSetBasicTelephonyJabberImpl basicTelephony
+ = (OperationSetBasicTelephonyJabberImpl)
+ protocolProvider.getOperationSet(
+ OperationSetBasicTelephony.class);
+ CallPeerJabberImpl callPeer = basicTelephony.getActiveCallPeer(sid);
+ if(callPeer != null)
+ {
+ if(!CallPeerState.isOnHold(callPeer.getState()))
+ {
+ callPeer.putOnHold(true);
+ }
+ }
+
+ if(!CallPeerState.isOnHold(this.getState()))
+ {
+ this.putOnHold(true);
+ }
+ }
+ transfer.setTo(to);
+
+ transferSessionInfo.addExtension(transfer);
+
+ Connection connection = protocolProvider.getConnection();
+ PacketCollector collector = connection.createPacketCollector(
+ new PacketIDFilter(transferSessionInfo.getPacketID()));
+ protocolProvider.getConnection().sendPacket(transferSessionInfo);
+
+ Packet result
+ = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ if(result == null)
+ {
+ // Log the failed transfer call and notify the user.
+ throw new OperationFailedException(
+ "No response to the \"transfer\" request.",
+ OperationFailedException.ILLEGAL_ARGUMENT);
+ }
+ else if (((IQ) result).getType() != IQ.Type.RESULT)
+ {
+ // Log the failed transfer call and notify the user.
+ throw new OperationFailedException(
+ "Remote peer does not manage call \"transfer\"."
+ + "Response to the \"transfer\" request is: "
+ + ((IQ) result).getType(),
+ OperationFailedException.ILLEGAL_ARGUMENT);
+ }
+ else
+ {
+ String message = ((sid == null) ? "Unattended" : "Attended")
+ + " transfer to: "
+ + to;
+ // Implements the SIP behavior: once the transfer is accepted, the
+ // current call is closed.
+ hangup(
+ false,
+ message,
+ new ReasonPacketExtension(Reason.SUCCESS,
+ message,
+ new TransferredPacketExtension()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getEntity()
+ {
+ return getAddress();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * In Jingle there isn't an actual "direction" parameter. We use the
+ * <tt>senders</tt> field to calculate the direction.
+ */
+ @Override
+ public MediaDirection getDirection(MediaType mediaType)
+ {
+ SendersEnum senders = getSenders(mediaType);
+ if (senders == SendersEnum.none)
+ return MediaDirection.INACTIVE;
+ else if (senders == null || senders == SendersEnum.both)
+ return MediaDirection.SENDRECV;
+ else if (senders == SendersEnum.initiator)
+ return isInitiator()
+ ? MediaDirection.RECVONLY
+ : MediaDirection.SENDONLY;
+ else //senders == SendersEnum.responder
+ return isInitiator()
+ ? MediaDirection.SENDONLY
+ : MediaDirection.RECVONLY;
+ }
+
+ /**
+ * Get the current value of the <tt>senders</tt> field of the content with
+ * name <tt>mediaType</tt> in the Jingle session with this <tt>CallPeer</tt>
+ * @param mediaType the <tt>MediaType</tt> for which to get the current
+ * value of the <tt>senders</tt> field.
+ * @return the current value of the <tt>senders</tt> field of the content
+ * with name <tt>mediaType</tt> in the Jingle session with this
+ * <tt>CallPeer</tt>.
+ */
+ public SendersEnum getSenders(MediaType mediaType)
+ {
+ if (MediaType.AUDIO.equals(mediaType))
+ return audioSenders;
+ else if (MediaType.VIDEO.equals(mediaType))
+ return videoSenders;
+ else
+ throw new IllegalArgumentException("mediaType");
+ }
+
+ /**
+ * Set the current value of the <tt>senders</tt> field of the content with
+ * name <tt>mediaType</tt> in the Jingle session with this <tt>CallPeer</tt>
+ * @param mediaType the <tt>MediaType</tt> for which to get the current
+ * value of the <tt>senders</tt> field.
+ * @param senders the value to set
+ */
+ public void setSenders(MediaType mediaType, SendersEnum senders)
+ {
+ if (mediaType == null)
+ return;
+ else if (MediaType.AUDIO.equals(mediaType))
+ this.audioSenders = senders;
+ else if (MediaType.VIDEO.equals(mediaType))
+ this.videoSenders = senders;
+ else
+ throw new IllegalArgumentException("mediaType");
+ }
+
+ /**
+ * Gets the <tt>MediaType</tt> of <tt>content</tt>. If <tt>content</tt>
+ * does not have a <tt>description</tt> child and therefore not
+ * <tt>MediaType</tt> can be associated with it, tries to take the
+ * <tt>MediaType</tt> from the session's already established contents with
+ * the same name as <tt>content</tt>
+ * @param content the <tt>ContentPacketExtention</tt> for which to get the
+ * <tt>MediaType</tt>
+ * @return the <tt>MediaType</tt> of <tt>content</tt>.
+ */
+ public MediaType getMediaType (ContentPacketExtension content)
+ {
+ String contentName = content.getName();
+ if (contentName == null)
+ return null;
+
+ MediaType mediaType = JingleUtils.getMediaType(content);
+ if (mediaType == null)
+ {
+ CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
+ for (MediaType m : MediaType.values())
+ {
+ ContentPacketExtension sessionContent
+ = mediaHandler.getRemoteContent(m.toString());
+ if (sessionContent == null)
+ sessionContent = mediaHandler.getLocalContent(m.toString());
+
+ if (sessionContent != null
+ && contentName.equals(sessionContent.getName()))
+ {
+ mediaType = m;
+ break;
+ }
+ }
+ }
+
+ return mediaType;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java index 2961bef..9adc434 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetTelephonyConferencingJabberImpl.java @@ -1,577 +1,570 @@ -/* - * 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.impl.protocol.jabber; - -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.util.xml.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.filter.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smack.packet.IQ.Type; -import org.jivesoftware.smack.util.*; -import org.jivesoftware.smackx.packet.*; - -/** - * Implements <tt>OperationSetTelephonyConferencing</tt> for Jabber. - * - * @author Lyubomir Marinov - * @author Sebastien Vincent - * @author Boris Grozev - */ -public class OperationSetTelephonyConferencingJabberImpl - extends AbstractOperationSetTelephonyConferencing< - ProtocolProviderServiceJabberImpl, - OperationSetBasicTelephonyJabberImpl, - CallJabberImpl, - CallPeerJabberImpl, - String> - implements RegistrationStateChangeListener, - PacketListener, - PacketFilter - -{ - /** - * The <tt>Logger</tt> used by the - * <tt>OperationSetTelephonyConferencingJabberImpl</tt> class and its - * instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(OperationSetTelephonyConferencingJabberImpl.class); - - /** - * The minimum interval in milliseconds between COINs sent to a single - * <tt>CallPeer</tt>. - */ - private static final int COIN_MIN_INTERVAL = 200; - - /** - * Synchronization object. - */ - private final Object lock = new Object(); - - /** - * Initializes a new <tt>OperationSetTelephonyConferencingJabberImpl</tt> - * instance which is to provide telephony conferencing services for the - * specified Jabber <tt>ProtocolProviderService</tt> implementation. - * - * @param parentProvider the Jabber <tt>ProtocolProviderService</tt> - * implementation which has requested the creation of the new instance and - * for which the new instance is to provide telephony conferencing services - */ - public OperationSetTelephonyConferencingJabberImpl( - ProtocolProviderServiceJabberImpl parentProvider) - { - super(parentProvider); - } - - /** - * Notifies all <tt>CallPeer</tt>s associated with a specific <tt>Call</tt> - * about changes in the telephony conference-related information. In - * contrast, {@link #notifyAll()} notifies all <tt>CallPeer</tt>s associated - * with the telephony conference in which a specific <tt>Call</tt> is - * participating. - * - * @param call the <tt>Call</tt> whose <tt>CallPeer</tt>s are to be notified - * about changes in the telephony conference-related information - */ - @Override - protected void notifyCallPeers(Call call) - { - if (call.isConferenceFocus()) - { - synchronized (lock) - { - // send conference-info to all CallPeers of the specified call. - for (Iterator<? extends CallPeer> i = call.getCallPeers(); - i.hasNext();) - { - notify(i.next()); - } - } - } - } - - /** - * Notifies a specific <tt>CallPeer</tt> about changes in the telephony - * conference-related information. - * - * @param callPeer the <tt>CallPeer</tt> to notify. - */ - private void notify(CallPeer callPeer) - { - if(!(callPeer instanceof CallPeerJabberImpl)) - return; - - //Don't send COINs to peers with might not be ready to accept COINs yet - CallPeerState peerState = callPeer.getState(); - if (peerState == CallPeerState.CONNECTING - || peerState == CallPeerState.UNKNOWN - || peerState == CallPeerState.INITIATING_CALL - || peerState == CallPeerState.DISCONNECTED - || peerState == CallPeerState.FAILED) - return; - - final CallPeerJabberImpl callPeerJabber = (CallPeerJabberImpl)callPeer; - - final long timeSinceLastCoin = System.currentTimeMillis() - - callPeerJabber.getLastConferenceInfoSentTimestamp(); - if (timeSinceLastCoin < COIN_MIN_INTERVAL) - { - if (callPeerJabber.isConfInfoScheduled()) - return; - - logger.info("Scheduling to send a COIN to " + callPeerJabber); - callPeerJabber.setConfInfoScheduled(true); - new Thread(new Runnable(){ - @Override - public void run() - { - try - { - Thread.sleep(1 + COIN_MIN_INTERVAL - timeSinceLastCoin); - } - catch (InterruptedException ie) {} - - OperationSetTelephonyConferencingJabberImpl.this - .notify(callPeerJabber); - } - }).start(); - - return; - } - - // check that callPeer supports COIN before sending him a - // conference-info - String to = getBasicTelephony().getFullCalleeURI(callPeer.getAddress()); - - // XXX if this generates actual disco#info requests we might want to - // cache it. - try - { - DiscoverInfo discoverInfo - = parentProvider.getDiscoveryManager().discoverInfo(to); - - if (!discoverInfo.containsFeature( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_COIN)) - { - logger.info(callPeer.getAddress() + " does not support COIN"); - callPeerJabber.setConfInfoScheduled(false); - return; - } - } - catch (XMPPException xmppe) - { - logger.warn("Failed to retrieve DiscoverInfo for " + to, xmppe); - } - - ConferenceInfoDocument currentConfInfo - = getCurrentConferenceInfo(callPeerJabber); - ConferenceInfoDocument lastSentConfInfo - = callPeerJabber.getLastConferenceInfoSent(); - - ConferenceInfoDocument diff; - - if (lastSentConfInfo == null) - diff = currentConfInfo; - else - diff = getConferenceInfoDiff(lastSentConfInfo, currentConfInfo); - - if (diff != null) - { - int newVersion - = lastSentConfInfo == null - ? 1 - : lastSentConfInfo.getVersion() + 1; - diff.setVersion(newVersion); - - IQ iq = getConferenceInfo(callPeerJabber, diff); - - if (iq != null) - { - parentProvider.getConnection().sendPacket(iq); - - // We save currentConfInfo, because it is of state "full", while - // diff could be a partial - currentConfInfo.setVersion(newVersion); - callPeerJabber.setLastConferenceInfoSent(currentConfInfo); - callPeerJabber.setLastConferenceInfoSentTimestamp( - System.currentTimeMillis()); - } - } - callPeerJabber.setConfInfoScheduled(false); - } - - /** - * Generates the conference-info IQ to be sent to a specific - * <tt>CallPeer</tt> in order to notify it of the current state of the - * conference managed by the local peer. - * - * @param callPeer the <tt>CallPeer</tt> to generate conference-info XML for - * @param confInfo the <tt>ConferenceInformationDocument</tt> which is to be - * included in the IQ - * @return the conference-info IQ to be sent to the specified - * <tt>callPeer</tt> in order to notify it of the current state of the - * conference managed by the local peer - */ - private IQ getConferenceInfo(CallPeerJabberImpl callPeer, - final ConferenceInfoDocument confInfo) - { - String callPeerSID = callPeer.getSID(); - - if (callPeerSID == null) - return null; - - IQ iq = new IQ(){ - @Override - public String getChildElementXML() - { - return confInfo.toXml(); - } - }; - - CallJabberImpl call = callPeer.getCall(); - - iq.setFrom(call.getProtocolProvider().getOurJID()); - iq.setTo(callPeer.getAddress()); - iq.setType(Type.SET); - - return iq; - } - - /** - * Implementation of method <tt>registrationStateChange</tt> from - * interface RegistrationStateChangeListener for setting up (or down) - * our <tt>JingleManager</tt> when an <tt>XMPPConnection</tt> is available - * - * @param evt the event received - */ - @Override - public void registrationStateChanged(RegistrationStateChangeEvent evt) - { - super.registrationStateChanged(evt); - - RegistrationState registrationState = evt.getNewState(); - - if (RegistrationState.REGISTERED.equals(registrationState)) - { - if(logger.isDebugEnabled()) - logger.debug("Subscribes to Coin packets"); - subscribeForCoinPackets(); - } - else if (RegistrationState.UNREGISTERED.equals(registrationState)) - { - if(logger.isDebugEnabled()) - logger.debug("Unsubscribes to Coin packets"); - unsubscribeForCoinPackets(); - } - } - - /** - * Creates a new outgoing <tt>Call</tt> into which conference callees are to - * be invited by this <tt>OperationSetTelephonyConferencing</tt>. - * - * @return a new outgoing <tt>Call</tt> into which conference callees are to - * be invited by this <tt>OperationSetTelephonyConferencing</tt> - * @throws OperationFailedException if anything goes wrong - */ - @Override - protected CallJabberImpl createOutgoingCall() - throws OperationFailedException - { - return new CallJabberImpl(getBasicTelephony()); - } - - /** - * {@inheritDoc} - * - * Implements the protocol-dependent part of the logic of inviting a callee - * to a <tt>Call</tt>. The protocol-independent part of that logic is - * implemented by - * {@link AbstractOperationSetTelephonyConferencing#inviteCalleeToCall(String,Call)}. - */ - @Override - protected CallPeer doInviteCalleeToCall( - String calleeAddress, - CallJabberImpl call) - throws OperationFailedException - { - return - getBasicTelephony().createOutgoingCall( - call, - calleeAddress, - Arrays.asList( - new PacketExtension[] - { - new CoinPacketExtension(true) - })); - } - - /** - * Parses a <tt>String</tt> value which represents a callee address - * specified by the user into an object which is to actually represent the - * callee during the invitation to a conference <tt>Call</tt>. - * - * @param calleeAddressString a <tt>String</tt> value which represents a - * callee address to be parsed into an object which is to actually represent - * the callee during the invitation to a conference <tt>Call</tt> - * @return an object which is to actually represent the specified - * <tt>calleeAddressString</tt> during the invitation to a conference - * <tt>Call</tt> - * @throws OperationFailedException if parsing the specified - * <tt>calleeAddressString</tt> fails - */ - @Override - protected String parseAddressString(String calleeAddressString) - throws OperationFailedException - { - return getBasicTelephony().getFullCalleeURI(calleeAddressString); - } - - /** - * Subscribes us to notifications about incoming Coin packets. - */ - private void subscribeForCoinPackets() - { - parentProvider.getConnection().addPacketListener(this, this); - } - - /** - * Unsubscribes us from notifications about incoming Coin packets. - */ - private void unsubscribeForCoinPackets() - { - XMPPConnection connection = parentProvider.getConnection(); - - if (connection != null) - connection.removePacketListener(this); - } - - /** - * Tests whether or not the specified packet should be handled by this - * operation set. This method is called by smack prior to packet delivery - * and it would only accept <tt>CoinIQ</tt>s. - * - * @param packet the packet to test. - * @return true if and only if <tt>packet</tt> passes the filter. - */ - public boolean accept(Packet packet) - { - return (packet instanceof CoinIQ); - } - - /** - * Handles incoming jingle packets and passes them to the corresponding - * method based on their action. - * - * @param packet the packet to process. - */ - public void processPacket(Packet packet) - { - CoinIQ coinIQ = (CoinIQ) packet; - String errorMessage = null; - - //first ack all "set" requests. - IQ.Type type = coinIQ.getType(); - if (type == IQ.Type.SET) - { - IQ ack = IQ.createResultIQ(coinIQ); - - parentProvider.getConnection().sendPacket(ack); - } - else if(type == IQ.Type.ERROR) - { - XMPPError error = coinIQ.getError(); - if(error != null) - { - String msg = error.getMessage(); - errorMessage = ((msg != null)? (msg + " ") : "") - + "Error code: " + error.getCode(); - } - - logger.error("Received error in COIN packet. "+errorMessage); - } - - String sid = coinIQ.getSID(); - - if (sid != null) - { - CallPeerJabberImpl callPeer - = getBasicTelephony().getActiveCallsRepository().findCallPeer( - sid); - - - if (callPeer != null) - { - if(type == IQ.Type.ERROR) - { - callPeer.fireConferenceMemberErrorEvent(errorMessage); - return; - } - - if (logger.isDebugEnabled()) - logger.debug("Processing COIN from " + coinIQ.getFrom() - + " (version=" + coinIQ.getVersion() + ")"); - - handleCoin(callPeer, coinIQ); - } - } - } - - /** - * Handles a specific <tt>CoinIQ</tt> sent from a specific - * <tt>CallPeer</tt>. - * - * @param callPeer the <tt>CallPeer</tt> from which the specified - * <tt>CoinIQ</tt> was sent - * @param coinIQ the <tt>CoinIQ</tt> which was sent from the specified - * <tt>callPeer</tt> - */ - private void handleCoin(CallPeerJabberImpl callPeer, CoinIQ coinIQ) - { - try - { - setConferenceInfoXML(callPeer, coinIQ.getChildElementXML()); - } - catch (XMLException e) - { - logger.error("Could not handle received COIN from " + callPeer - + ": " + coinIQ); - } - } - - /** - * {@inheritDoc} - * - * For COINs (XEP-0298), we use the attributes of the - * <tt>conference-info</tt> element to piggyback a Jingle SID. This is - * temporary and should be removed once we choose a better way to pass the - * SID. - */ - protected ConferenceInfoDocument getCurrentConferenceInfo( - MediaAwareCallPeer callPeer) - { - ConferenceInfoDocument confInfo - = super.getCurrentConferenceInfo(callPeer); - - if (callPeer instanceof CallPeerJabberImpl - && confInfo != null) - { - confInfo.setSid(((CallPeerJabberImpl)callPeer).getSID()); - } - return confInfo; - } - - /** - * {@inheritDoc} - */ - @Override - protected String getLocalEntity(CallPeer callPeer) - { - JingleIQ sessionIQ = ((CallPeerJabberImpl)callPeer).getSessionIQ(); - String from = sessionIQ.getFrom(); - String chatRoomName = StringUtils.parseBareAddress(from); - OperationSetMultiUserChatJabberImpl opSetMUC - = (OperationSetMultiUserChatJabberImpl)parentProvider.getOperationSet(OperationSetMultiUserChat.class); - ChatRoom room = null; - room = opSetMUC.getChatRoom(chatRoomName); - - if(room != null) - return "xmpp:" + chatRoomName + "/" + room.getUserNickname(); - - return "xmpp:" + parentProvider.getOurJID(); - } - - /** - * {@inheritDoc} - */ - @Override - protected String getLocalDisplayName() - { - return null; - } - - /** - * {@inheritDoc} - * - * The URI of the returned <tt>ConferenceDescription</tt> is the occupant - * JID with which we have joined the room. - * - * If a videobridge is available for our <tt>ProtocolProviderService</tt> - * we use it. TODO: this should be relaxed when we refactor the videobrdige - * implementation, so that any videobrdige (on any protocol provider) can - * be used. - * - */ - @Override - public ConferenceDescription setupConference(final ChatRoom chatRoom) - { - OperationSetVideoBridge videoBridge - = parentProvider.getOperationSet(OperationSetVideoBridge.class); - boolean isVideoBridge = (videoBridge != null) && videoBridge.isActive(); - - CallJabberImpl call = new CallJabberImpl(getBasicTelephony()); - call.setAutoAnswer(true); - - String uri = "xmpp:" + chatRoom.getIdentifier() + - "/" + chatRoom.getUserNickname(); - - ConferenceDescription cd - = new ConferenceDescription(uri, call.getCallID()); - - call.addCallChangeListener(new CallChangeListener() - { - - @Override - public void callStateChanged(CallChangeEvent evt) - { - if(CallState.CALL_ENDED.equals(evt.getNewValue())) - { - chatRoom.publishConference(null, null); - } - - } - - @Override - public void callPeerRemoved(CallPeerEvent evt) - { - - } - - @Override - public void callPeerAdded(CallPeerEvent evt) - { - - } - }); - if (isVideoBridge) - { - call.setConference(new MediaAwareCallConference(true)); - - - //For videobridge we set the transports to RAW-UDP, otherwise - //we leave them empty (meaning both RAW-UDP and ICE could be used) - cd.addTransport( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0); - } - - if (logger.isInfoEnabled()) - { - logger.info("Setup a conference with uri=" + uri + " and callid=" + - call.getCallID() + ". Videobridge in use: " + isVideoBridge); - } - - return cd; - } -} +/*
+ * 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.impl.protocol.jabber;
+
+import java.util.*;
+
+import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.service.protocol.media.*;
+import net.java.sip.communicator.util.*;
+
+import org.jitsi.util.xml.*;
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smack.util.*;
+import org.jivesoftware.smackx.packet.*;
+
+/**
+ * Implements <tt>OperationSetTelephonyConferencing</tt> for Jabber.
+ *
+ * @author Lyubomir Marinov
+ * @author Sebastien Vincent
+ * @author Boris Grozev
+ */
+public class OperationSetTelephonyConferencingJabberImpl
+ extends AbstractOperationSetTelephonyConferencing<
+ ProtocolProviderServiceJabberImpl,
+ OperationSetBasicTelephonyJabberImpl,
+ CallJabberImpl,
+ CallPeerJabberImpl,
+ String>
+ implements RegistrationStateChangeListener,
+ PacketListener,
+ PacketFilter
+
+{
+ /**
+ * The <tt>Logger</tt> used by the
+ * <tt>OperationSetTelephonyConferencingJabberImpl</tt> class and its
+ * instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(OperationSetTelephonyConferencingJabberImpl.class);
+
+ /**
+ * The minimum interval in milliseconds between COINs sent to a single
+ * <tt>CallPeer</tt>.
+ */
+ private static final int COIN_MIN_INTERVAL = 200;
+
+ /**
+ * Synchronization object.
+ */
+ private final Object lock = new Object();
+
+ /**
+ * Initializes a new <tt>OperationSetTelephonyConferencingJabberImpl</tt>
+ * instance which is to provide telephony conferencing services for the
+ * specified Jabber <tt>ProtocolProviderService</tt> implementation.
+ *
+ * @param parentProvider the Jabber <tt>ProtocolProviderService</tt>
+ * implementation which has requested the creation of the new instance and
+ * for which the new instance is to provide telephony conferencing services
+ */
+ public OperationSetTelephonyConferencingJabberImpl(
+ ProtocolProviderServiceJabberImpl parentProvider)
+ {
+ super(parentProvider);
+ }
+
+ /**
+ * Notifies all <tt>CallPeer</tt>s associated with a specific <tt>Call</tt>
+ * about changes in the telephony conference-related information. In
+ * contrast, {@link #notifyAll()} notifies all <tt>CallPeer</tt>s associated
+ * with the telephony conference in which a specific <tt>Call</tt> is
+ * participating.
+ *
+ * @param call the <tt>Call</tt> whose <tt>CallPeer</tt>s are to be notified
+ * about changes in the telephony conference-related information
+ */
+ @Override
+ protected void notifyCallPeers(Call call)
+ {
+ if (call.isConferenceFocus())
+ {
+ synchronized (lock)
+ {
+ // send conference-info to all CallPeers of the specified call.
+ for (Iterator<? extends CallPeer> i = call.getCallPeers();
+ i.hasNext();)
+ {
+ notify(i.next());
+ }
+ }
+ }
+ }
+
+ /**
+ * Notifies a specific <tt>CallPeer</tt> about changes in the telephony
+ * conference-related information.
+ *
+ * @param callPeer the <tt>CallPeer</tt> to notify.
+ */
+ private void notify(CallPeer callPeer)
+ {
+ if(!(callPeer instanceof CallPeerJabberImpl))
+ return;
+
+ //Don't send COINs to peers with might not be ready to accept COINs yet
+ CallPeerState peerState = callPeer.getState();
+ if (peerState == CallPeerState.CONNECTING
+ || peerState == CallPeerState.UNKNOWN
+ || peerState == CallPeerState.INITIATING_CALL
+ || peerState == CallPeerState.DISCONNECTED
+ || peerState == CallPeerState.FAILED)
+ return;
+
+ final CallPeerJabberImpl callPeerJabber = (CallPeerJabberImpl)callPeer;
+
+ final long timeSinceLastCoin = System.currentTimeMillis()
+ - callPeerJabber.getLastConferenceInfoSentTimestamp();
+ if (timeSinceLastCoin < COIN_MIN_INTERVAL)
+ {
+ if (callPeerJabber.isConfInfoScheduled())
+ return;
+
+ logger.info("Scheduling to send a COIN to " + callPeerJabber);
+ callPeerJabber.setConfInfoScheduled(true);
+ new Thread(new Runnable(){
+ @Override
+ public void run()
+ {
+ try
+ {
+ Thread.sleep(1 + COIN_MIN_INTERVAL - timeSinceLastCoin);
+ }
+ catch (InterruptedException ie) {}
+
+ OperationSetTelephonyConferencingJabberImpl.this
+ .notify(callPeerJabber);
+ }
+ }).start();
+
+ return;
+ }
+
+ // check that callPeer supports COIN before sending him a
+ // conference-info
+ String to = getBasicTelephony().getFullCalleeURI(callPeer.getAddress());
+
+ // XXX if this generates actual disco#info requests we might want to
+ // cache it.
+ try
+ {
+ DiscoverInfo discoverInfo
+ = parentProvider.getDiscoveryManager().discoverInfo(to);
+
+ if (!discoverInfo.containsFeature(
+ ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_COIN))
+ {
+ logger.info(callPeer.getAddress() + " does not support COIN");
+ callPeerJabber.setConfInfoScheduled(false);
+ return;
+ }
+ }
+ catch (XMPPException xmppe)
+ {
+ logger.warn("Failed to retrieve DiscoverInfo for " + to, xmppe);
+ }
+
+ ConferenceInfoDocument currentConfInfo
+ = getCurrentConferenceInfo(callPeerJabber);
+ ConferenceInfoDocument lastSentConfInfo
+ = callPeerJabber.getLastConferenceInfoSent();
+
+ ConferenceInfoDocument diff;
+
+ if (lastSentConfInfo == null)
+ diff = currentConfInfo;
+ else
+ diff = getConferenceInfoDiff(lastSentConfInfo, currentConfInfo);
+
+ if (diff != null)
+ {
+ int newVersion
+ = lastSentConfInfo == null
+ ? 1
+ : lastSentConfInfo.getVersion() + 1;
+ diff.setVersion(newVersion);
+
+ IQ iq = getConferenceInfo(callPeerJabber, diff);
+
+ if (iq != null)
+ {
+ parentProvider.getConnection().sendPacket(iq);
+
+ // We save currentConfInfo, because it is of state "full", while
+ // diff could be a partial
+ currentConfInfo.setVersion(newVersion);
+ callPeerJabber.setLastConferenceInfoSent(currentConfInfo);
+ callPeerJabber.setLastConferenceInfoSentTimestamp(
+ System.currentTimeMillis());
+ }
+ }
+ callPeerJabber.setConfInfoScheduled(false);
+ }
+
+ /**
+ * Generates the conference-info IQ to be sent to a specific
+ * <tt>CallPeer</tt> in order to notify it of the current state of the
+ * conference managed by the local peer.
+ *
+ * @param callPeer the <tt>CallPeer</tt> to generate conference-info XML for
+ * @param confInfo the <tt>ConferenceInformationDocument</tt> which is to be
+ * included in the IQ
+ * @return the conference-info IQ to be sent to the specified
+ * <tt>callPeer</tt> in order to notify it of the current state of the
+ * conference managed by the local peer
+ */
+ private IQ getConferenceInfo(CallPeerJabberImpl callPeer,
+ final ConferenceInfoDocument confInfo)
+ {
+ String callPeerSID = callPeer.getSID();
+
+ if (callPeerSID == null)
+ return null;
+
+ IQ iq = new IQ(){
+ @Override
+ public String getChildElementXML()
+ {
+ return confInfo.toXml();
+ }
+ };
+
+ CallJabberImpl call = callPeer.getCall();
+
+ iq.setFrom(call.getProtocolProvider().getOurJID());
+ iq.setTo(callPeer.getAddress());
+ iq.setType(Type.SET);
+
+ return iq;
+ }
+
+ /**
+ * Implementation of method <tt>registrationStateChange</tt> from
+ * interface RegistrationStateChangeListener for setting up (or down)
+ * our <tt>JingleManager</tt> when an <tt>XMPPConnection</tt> is available
+ *
+ * @param evt the event received
+ */
+ @Override
+ public void registrationStateChanged(RegistrationStateChangeEvent evt)
+ {
+ super.registrationStateChanged(evt);
+
+ RegistrationState registrationState = evt.getNewState();
+
+ if (RegistrationState.REGISTERED.equals(registrationState))
+ {
+ if(logger.isDebugEnabled())
+ logger.debug("Subscribes to Coin packets");
+ subscribeForCoinPackets();
+ }
+ else if (RegistrationState.UNREGISTERED.equals(registrationState))
+ {
+ if(logger.isDebugEnabled())
+ logger.debug("Unsubscribes to Coin packets");
+ unsubscribeForCoinPackets();
+ }
+ }
+
+ /**
+ * Creates a new outgoing <tt>Call</tt> into which conference callees are to
+ * be invited by this <tt>OperationSetTelephonyConferencing</tt>.
+ *
+ * @return a new outgoing <tt>Call</tt> into which conference callees are to
+ * be invited by this <tt>OperationSetTelephonyConferencing</tt>
+ * @throws OperationFailedException if anything goes wrong
+ */
+ @Override
+ protected CallJabberImpl createOutgoingCall()
+ throws OperationFailedException
+ {
+ return new CallJabberImpl(getBasicTelephony());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Implements the protocol-dependent part of the logic of inviting a callee
+ * to a <tt>Call</tt>. The protocol-independent part of that logic is
+ * implemented by
+ * {@link AbstractOperationSetTelephonyConferencing#inviteCalleeToCall(String,Call)}.
+ */
+ @Override
+ protected CallPeer doInviteCalleeToCall(
+ String calleeAddress,
+ CallJabberImpl call)
+ throws OperationFailedException
+ {
+ return
+ getBasicTelephony().createOutgoingCall(
+ call,
+ calleeAddress,
+ Arrays.asList(
+ new PacketExtension[]
+ {
+ new CoinPacketExtension(true)
+ }));
+ }
+
+ /**
+ * Parses a <tt>String</tt> value which represents a callee address
+ * specified by the user into an object which is to actually represent the
+ * callee during the invitation to a conference <tt>Call</tt>.
+ *
+ * @param calleeAddressString a <tt>String</tt> value which represents a
+ * callee address to be parsed into an object which is to actually represent
+ * the callee during the invitation to a conference <tt>Call</tt>
+ * @return an object which is to actually represent the specified
+ * <tt>calleeAddressString</tt> during the invitation to a conference
+ * <tt>Call</tt>
+ * @throws OperationFailedException if parsing the specified
+ * <tt>calleeAddressString</tt> fails
+ */
+ @Override
+ protected String parseAddressString(String calleeAddressString)
+ throws OperationFailedException
+ {
+ return getBasicTelephony().getFullCalleeURI(calleeAddressString);
+ }
+
+ /**
+ * Subscribes us to notifications about incoming Coin packets.
+ */
+ private void subscribeForCoinPackets()
+ {
+ parentProvider.getConnection().addPacketListener(this, this);
+ }
+
+ /**
+ * Unsubscribes us from notifications about incoming Coin packets.
+ */
+ private void unsubscribeForCoinPackets()
+ {
+ XMPPConnection connection = parentProvider.getConnection();
+
+ if (connection != null)
+ connection.removePacketListener(this);
+ }
+
+ /**
+ * Tests whether or not the specified packet should be handled by this
+ * operation set. This method is called by smack prior to packet delivery
+ * and it would only accept <tt>CoinIQ</tt>s.
+ *
+ * @param packet the packet to test.
+ * @return true if and only if <tt>packet</tt> passes the filter.
+ */
+ public boolean accept(Packet packet)
+ {
+ return (packet instanceof CoinIQ);
+ }
+
+ /**
+ * Handles incoming jingle packets and passes them to the corresponding
+ * method based on their action.
+ *
+ * @param packet the packet to process.
+ */
+ public void processPacket(Packet packet)
+ {
+ CoinIQ coinIQ = (CoinIQ) packet;
+ String errorMessage = null;
+
+ //first ack all "set" requests.
+ IQ.Type type = coinIQ.getType();
+ if (type == IQ.Type.SET)
+ {
+ IQ ack = IQ.createResultIQ(coinIQ);
+
+ parentProvider.getConnection().sendPacket(ack);
+ }
+ else if(type == IQ.Type.ERROR)
+ {
+ XMPPError error = coinIQ.getError();
+ if(error != null)
+ {
+ String msg = error.getMessage();
+ errorMessage = ((msg != null)? (msg + " ") : "")
+ + "Error code: " + error.getCode();
+ }
+
+ logger.error("Received error in COIN packet. "+errorMessage);
+ }
+
+ String sid = coinIQ.getSID();
+
+ if (sid != null)
+ {
+ CallPeerJabberImpl callPeer
+ = getBasicTelephony().getActiveCallsRepository().findCallPeer(
+ sid);
+
+
+ if (callPeer != null)
+ {
+ if(type == IQ.Type.ERROR)
+ {
+ callPeer.fireConferenceMemberErrorEvent(errorMessage);
+ return;
+ }
+
+ if (logger.isDebugEnabled())
+ logger.debug("Processing COIN from " + coinIQ.getFrom()
+ + " (version=" + coinIQ.getVersion() + ")");
+
+ handleCoin(callPeer, coinIQ);
+ }
+ }
+ }
+
+ /**
+ * Handles a specific <tt>CoinIQ</tt> sent from a specific
+ * <tt>CallPeer</tt>.
+ *
+ * @param callPeer the <tt>CallPeer</tt> from which the specified
+ * <tt>CoinIQ</tt> was sent
+ * @param coinIQ the <tt>CoinIQ</tt> which was sent from the specified
+ * <tt>callPeer</tt>
+ */
+ private void handleCoin(CallPeerJabberImpl callPeer, CoinIQ coinIQ)
+ {
+ try
+ {
+ setConferenceInfoXML(callPeer, coinIQ.getChildElementXML());
+ }
+ catch (XMLException e)
+ {
+ logger.error("Could not handle received COIN from " + callPeer
+ + ": " + coinIQ);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * For COINs (XEP-0298), we use the attributes of the
+ * <tt>conference-info</tt> element to piggyback a Jingle SID. This is
+ * temporary and should be removed once we choose a better way to pass the
+ * SID.
+ */
+ protected ConferenceInfoDocument getCurrentConferenceInfo(
+ MediaAwareCallPeer callPeer)
+ {
+ ConferenceInfoDocument confInfo
+ = super.getCurrentConferenceInfo(callPeer);
+
+ if (callPeer instanceof CallPeerJabberImpl
+ && confInfo != null)
+ {
+ confInfo.setSid(((CallPeerJabberImpl)callPeer).getSID());
+ }
+ return confInfo;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalEntity(CallPeer callPeer)
+ {
+ JingleIQ sessionIQ = ((CallPeerJabberImpl)callPeer).getSessionIQ();
+ String from = sessionIQ.getFrom();
+ String chatRoomName = StringUtils.parseBareAddress(from);
+ OperationSetMultiUserChatJabberImpl opSetMUC
+ = (OperationSetMultiUserChatJabberImpl)
+ parentProvider.getOperationSet(OperationSetMultiUserChat.class);
+ ChatRoom room = null;
+ room = opSetMUC.getChatRoom(chatRoomName);
+
+ if(room != null)
+ return "xmpp:" + chatRoomName + "/" + room.getUserNickname();
+
+ return "xmpp:" + parentProvider.getOurJID();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected String getLocalDisplayName()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The URI of the returned <tt>ConferenceDescription</tt> is the occupant
+ * JID with which we have joined the room.
+ *
+ * If a Videobridge is available for our <tt>ProtocolProviderService</tt>
+ * we use it. TODO: this should be relaxed when we refactor the Videobridge
+ * implementation, so that any Videobridge (on any protocol provider) can
+ * be used.
+ */
+ @Override
+ public ConferenceDescription setupConference(final ChatRoom chatRoom)
+ {
+ OperationSetVideoBridge videoBridge
+ = parentProvider.getOperationSet(OperationSetVideoBridge.class);
+ boolean isVideobridge = (videoBridge != null) && videoBridge.isActive();
+
+ CallJabberImpl call = new CallJabberImpl(getBasicTelephony());
+ call.setAutoAnswer(true);
+
+ String uri = "xmpp:" + chatRoom.getIdentifier() +
+ "/" + chatRoom.getUserNickname();
+
+ ConferenceDescription cd
+ = new ConferenceDescription(uri, call.getCallID());
+
+ call.addCallChangeListener(new CallChangeListener()
+ {
+ @Override
+ public void callStateChanged(CallChangeEvent ev)
+ {
+ if(CallState.CALL_ENDED.equals(ev.getNewValue()))
+ chatRoom.publishConference(null, null);
+ }
+
+ @Override
+ public void callPeerRemoved(CallPeerEvent ev)
+ {
+ }
+
+ @Override
+ public void callPeerAdded(CallPeerEvent ev)
+ {
+ }
+ });
+ if (isVideobridge)
+ {
+ call.setConference(new MediaAwareCallConference(true));
+
+ //For Jitsi Videobridge we set the transports to RAW-UDP, otherwise
+ //we leave them empty (meaning both RAW-UDP and ICE could be used)
+ cd.addTransport(
+ ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0);
+ }
+
+ if (logger.isInfoEnabled())
+ {
+ logger.info("Setup a conference with uri=" + uri + " and callid=" +
+ call.getCallID() + ". Videobridge in use: " + isVideobridge);
+ }
+
+ return cd;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetVideoBridgeImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetVideoBridgeImpl.java index bb7084c..eaf69d4 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetVideoBridgeImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetVideoBridgeImpl.java @@ -1,285 +1,285 @@ -/* - * 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.impl.protocol.jabber; - -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.filter.*; -import org.jivesoftware.smack.packet.*; - -/** - * Implements <tt>OperationSetVideoBridge</tt> for Jabber. - * - * @author Yana Stamcheva - * @author Lyubomir Marinov - */ -public class OperationSetVideoBridgeImpl - implements OperationSetVideoBridge, - PacketFilter, - PacketListener, - RegistrationStateChangeListener -{ - /** - * The <tt>Logger</tt> used by the <tt>OperationSetVideoBridgeImpl</tt> - * class and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(OperationSetVideoBridgeImpl.class); - - /** - * The <tt>ProtocolProviderService</tt> implementation which initialized - * this instance, owns it and is often referred to as its parent. - */ - private final ProtocolProviderServiceJabberImpl protocolProvider; - - /** - * Creates an instance of <tt>OperationSetVideoBridgeImpl</tt> by - * specifying the parent <tt>ProtocolProviderService</tt> announcing this - * operation set. - * - * @param protocolProvider the parent Jabber protocol provider - */ - public OperationSetVideoBridgeImpl( - ProtocolProviderServiceJabberImpl protocolProvider) - { - this.protocolProvider = protocolProvider; - this.protocolProvider.addRegistrationStateChangeListener(this); - } - - /** - * Implements {@link PacketFilter}. Determines whether this instance is - * interested in a specific {@link Packet}. - * <tt>OperationSetVideoBridgeImpl</tt> returns <tt>true</tt> if the - * specified <tt>packet</tt> is a {@link ColibriConferenceIQ}; otherwise, - * <tt>false</tt>. - * - * @param packet the <tt>Packet</tt> to be determined whether this instance - * is interested in it - * @return <tt>true</tt> if the specified <tt>packet</tt> is a - * <tt>ColibriConferenceIQ</tt>; otherwise, <tt>false</tt> - */ - public boolean accept(Packet packet) - { - return (packet instanceof ColibriConferenceIQ); - } - - /** - * Creates a conference call with the specified callees as call peers via a - * video bridge provided by the parent Jabber provider. - * - * @param callees the list of addresses that we should call - * @return the newly created conference call containing all CallPeers - * @throws OperationFailedException if establishing the conference call - * fails - * @throws OperationNotSupportedException if the provider does not have any - * conferencing features. - */ - public Call createConfCall(String[] callees) - throws OperationFailedException, - OperationNotSupportedException - { - return - protocolProvider - .getOperationSet(OperationSetTelephonyConferencing.class) - .createConfCall( - callees, - new MediaAwareCallConference(true)); - } - - /** - * Invites the callee represented by the specified uri to an already - * existing call using a video bridge provided by the parent Jabber provider. - * The difference between this method and createConfCall is that - * inviteCalleeToCall allows a user to add new peers to an already - * established conference. - * - * @param uri the callee to invite to an existing conf call. - * @param call the call that we should invite the callee to. - * @return the CallPeer object corresponding to the callee represented by - * the specified uri. - * @throws OperationFailedException if inviting the specified callee to the - * specified call fails - * @throws OperationNotSupportedException if allowing additional callees to - * a pre-established call is not supported. - */ - public CallPeer inviteCalleeToCall(String uri, Call call) - throws OperationFailedException, - OperationNotSupportedException - { - return - protocolProvider - .getOperationSet(OperationSetTelephonyConferencing.class) - .inviteCalleeToCall(uri, call); - } - - /** - * Indicates if there's an active video bridge available at this moment. The - * Jabber provider may announce support for video bridge, but it should not - * be used for calling until it becomes actually active. - * - * @return <tt>true</tt> to indicate that there's currently an active - * available video bridge, <tt>false</tt> - otherwise - */ - public boolean isActive() - { - String jitsiVideoBridge = protocolProvider.getJitsiVideoBridge(); - - return ((jitsiVideoBridge != null) && (jitsiVideoBridge.length() > 0)); - } - - /** - * Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has - * been received. - * - * @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been - * received - */ - private void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) - { - /* - * The application is not a Jitsi VideoBridge server, it is a client. - * Consequently, the specified ColibriConferenceIQ is sent to it in - * relation to the part of the application's functionality which makes - * requests to a Jitsi VideoBridge server i.e. CallJabberImpl. - * - * Additionally, the method processColibriConferenceIQ is presently tasked - * with processing ColibriConferenceIQ requests only. They are SET IQs - * sent by the Jitsi VideoBridge server to notify the application about - * updates in the states of (colibri) conferences organized by the - * application. - */ - if (IQ.Type.SET.equals(conferenceIQ.getType()) - && conferenceIQ.getID() != null) - { - OperationSetBasicTelephony<?> basicTelephony - = protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - - if (basicTelephony != null) - { - Iterator<? extends Call> i = basicTelephony.getActiveCalls(); - - while (i.hasNext()) - { - Call call = i.next(); - - if (call instanceof CallJabberImpl) - { - CallJabberImpl callJabberImpl = (CallJabberImpl) call; - MediaAwareCallConference conference - = callJabberImpl.getConference(); - - if ((conference != null) - && conference.isJitsiVideoBridge()) - { - /* - * TODO We may want to disallow rogue CallJabberImpl - * instances which may throw an exception to prevent - * the conferenceIQ from reaching the CallJabberImpl - * instance which it was meant for. - */ - if (callJabberImpl.processColibriConferenceIQ( - conferenceIQ)) - break; - } - } - } - } - } - } - - /** - * Implements {@link PacketListener}. Notifies this instance that a specific - * {@link Packet} (which this instance has already expressed interest into - * by returning <tt>true</tt> from {@link #accept(Packet)}) has been - * received. - * - * @param packet the <tt>Packet</tt> which has been received and which this - * instance is given a chance to process - */ - public void processPacket(Packet packet) - { - /* - * As we do elsewhere, acknowledge the receipt of the Packet first and - * then go about our business with it. - */ - IQ iq = (IQ) packet; - - if (iq.getType() == IQ.Type.SET) - protocolProvider.getConnection().sendPacket(IQ.createResultIQ(iq)); - - /* - * Now that the acknowledging is out of the way, do go about our - * business with the Packet. - */ - ColibriConferenceIQ conferenceIQ = (ColibriConferenceIQ) iq; - boolean interrupted = false; - - try - { - processColibriConferenceIQ(conferenceIQ); - } - catch (Throwable t) - { - logger.error( - "An error occurred during the processing of a " - + packet.getClass().getName() + " packet", - t); - - if (t instanceof InterruptedException) - { - /* - * We cleared the interrupted state of the current Thread by - * catching the InterruptedException. However, we do not really - * care whether the current Thread has been interrupted - we - * caught the InterruptedException because we want to swallow - * any Throwable. Consequently, we should better restore the - * interrupted state. - */ - interrupted = true; - } - else if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - } - if (interrupted) - Thread.currentThread().interrupt(); - } - - /** - * {@inheritDoc} - * - * Implements {@link RegistrationStateChangeListener}. Notifies this - * instance that there has been a change in the <tt>RegistrationState</tt> - * of {@link #protocolProvider}. Subscribes this instance to - * {@link ColibriConferenceIQ}s as soon as <tt>protocolProvider</tt> is - * registered and unsubscribes it as soon as <tt>protocolProvider</tt> is - * unregistered. - */ - public void registrationStateChanged(RegistrationStateChangeEvent ev) - { - RegistrationState registrationState = ev.getNewState(); - - if (RegistrationState.REGISTERED.equals(registrationState)) - { - protocolProvider.getConnection().addPacketListener(this, this); - } - else if (RegistrationState.UNREGISTERED.equals(registrationState)) - { - XMPPConnection connection = protocolProvider.getConnection(); - - if (connection != null) - connection.removePacketListener(this); - } - } -} +/*
+ * 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.impl.protocol.jabber;
+
+import java.util.*;
+
+import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.service.protocol.media.*;
+import net.java.sip.communicator.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+
+/**
+ * Implements <tt>OperationSetVideoBridge</tt> for Jabber.
+ *
+ * @author Yana Stamcheva
+ * @author Lyubomir Marinov
+ */
+public class OperationSetVideoBridgeImpl
+ implements OperationSetVideoBridge,
+ PacketFilter,
+ PacketListener,
+ RegistrationStateChangeListener
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>OperationSetVideoBridgeImpl</tt>
+ * class and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(OperationSetVideoBridgeImpl.class);
+
+ /**
+ * The <tt>ProtocolProviderService</tt> implementation which initialized
+ * this instance, owns it and is often referred to as its parent.
+ */
+ private final ProtocolProviderServiceJabberImpl protocolProvider;
+
+ /**
+ * Creates an instance of <tt>OperationSetVideoBridgeImpl</tt> by
+ * specifying the parent <tt>ProtocolProviderService</tt> announcing this
+ * operation set.
+ *
+ * @param protocolProvider the parent Jabber protocol provider
+ */
+ public OperationSetVideoBridgeImpl(
+ ProtocolProviderServiceJabberImpl protocolProvider)
+ {
+ this.protocolProvider = protocolProvider;
+ this.protocolProvider.addRegistrationStateChangeListener(this);
+ }
+
+ /**
+ * Implements {@link PacketFilter}. Determines whether this instance is
+ * interested in a specific {@link Packet}.
+ * <tt>OperationSetVideoBridgeImpl</tt> returns <tt>true</tt> if the
+ * specified <tt>packet</tt> is a {@link ColibriConferenceIQ}; otherwise,
+ * <tt>false</tt>.
+ *
+ * @param packet the <tt>Packet</tt> to be determined whether this instance
+ * is interested in it
+ * @return <tt>true</tt> if the specified <tt>packet</tt> is a
+ * <tt>ColibriConferenceIQ</tt>; otherwise, <tt>false</tt>
+ */
+ public boolean accept(Packet packet)
+ {
+ return (packet instanceof ColibriConferenceIQ);
+ }
+
+ /**
+ * Creates a conference call with the specified callees as call peers via a
+ * video bridge provided by the parent Jabber provider.
+ *
+ * @param callees the list of addresses that we should call
+ * @return the newly created conference call containing all CallPeers
+ * @throws OperationFailedException if establishing the conference call
+ * fails
+ * @throws OperationNotSupportedException if the provider does not have any
+ * conferencing features.
+ */
+ public Call createConfCall(String[] callees)
+ throws OperationFailedException,
+ OperationNotSupportedException
+ {
+ return
+ protocolProvider
+ .getOperationSet(OperationSetTelephonyConferencing.class)
+ .createConfCall(
+ callees,
+ new MediaAwareCallConference(true));
+ }
+
+ /**
+ * Invites the callee represented by the specified uri to an already
+ * existing call using a video bridge provided by the parent Jabber provider.
+ * The difference between this method and createConfCall is that
+ * inviteCalleeToCall allows a user to add new peers to an already
+ * established conference.
+ *
+ * @param uri the callee to invite to an existing conf call.
+ * @param call the call that we should invite the callee to.
+ * @return the CallPeer object corresponding to the callee represented by
+ * the specified uri.
+ * @throws OperationFailedException if inviting the specified callee to the
+ * specified call fails
+ * @throws OperationNotSupportedException if allowing additional callees to
+ * a pre-established call is not supported.
+ */
+ public CallPeer inviteCalleeToCall(String uri, Call call)
+ throws OperationFailedException,
+ OperationNotSupportedException
+ {
+ return
+ protocolProvider
+ .getOperationSet(OperationSetTelephonyConferencing.class)
+ .inviteCalleeToCall(uri, call);
+ }
+
+ /**
+ * Indicates if there's an active video bridge available at this moment. The
+ * Jabber provider may announce support for video bridge, but it should not
+ * be used for calling until it becomes actually active.
+ *
+ * @return <tt>true</tt> to indicate that there's currently an active
+ * available video bridge, <tt>false</tt> - otherwise
+ */
+ public boolean isActive()
+ {
+ String jitsiVideobridge = protocolProvider.getJitsiVideobridge();
+
+ return ((jitsiVideobridge != null) && (jitsiVideobridge.length() > 0));
+ }
+
+ /**
+ * Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has
+ * been received.
+ *
+ * @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been
+ * received
+ */
+ private void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ)
+ {
+ /*
+ * The application is not a Jitsi Videobridge server, it is a client.
+ * Consequently, the specified ColibriConferenceIQ is sent to it in
+ * relation to the part of the application's functionality which makes
+ * requests to a Jitsi Videobridge server i.e. CallJabberImpl.
+ *
+ * Additionally, the method processColibriConferenceIQ is presently tasked
+ * with processing ColibriConferenceIQ requests only. They are SET IQs
+ * sent by the Jitsi Videobridge server to notify the application about
+ * updates in the states of (colibri) conferences organized by the
+ * application.
+ */
+ if (IQ.Type.SET.equals(conferenceIQ.getType())
+ && conferenceIQ.getID() != null)
+ {
+ OperationSetBasicTelephony<?> basicTelephony
+ = protocolProvider.getOperationSet(
+ OperationSetBasicTelephony.class);
+
+ if (basicTelephony != null)
+ {
+ Iterator<? extends Call> i = basicTelephony.getActiveCalls();
+
+ while (i.hasNext())
+ {
+ Call call = i.next();
+
+ if (call instanceof CallJabberImpl)
+ {
+ CallJabberImpl callJabberImpl = (CallJabberImpl) call;
+ MediaAwareCallConference conference
+ = callJabberImpl.getConference();
+
+ if ((conference != null)
+ && conference.isJitsiVideobridge())
+ {
+ /*
+ * TODO We may want to disallow rogue CallJabberImpl
+ * instances which may throw an exception to prevent
+ * the conferenceIQ from reaching the CallJabberImpl
+ * instance which it was meant for.
+ */
+ if (callJabberImpl.processColibriConferenceIQ(
+ conferenceIQ))
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Implements {@link PacketListener}. Notifies this instance that a specific
+ * {@link Packet} (which this instance has already expressed interest into
+ * by returning <tt>true</tt> from {@link #accept(Packet)}) has been
+ * received.
+ *
+ * @param packet the <tt>Packet</tt> which has been received and which this
+ * instance is given a chance to process
+ */
+ public void processPacket(Packet packet)
+ {
+ /*
+ * As we do elsewhere, acknowledge the receipt of the Packet first and
+ * then go about our business with it.
+ */
+ IQ iq = (IQ) packet;
+
+ if (iq.getType() == IQ.Type.SET)
+ protocolProvider.getConnection().sendPacket(IQ.createResultIQ(iq));
+
+ /*
+ * Now that the acknowledging is out of the way, do go about our
+ * business with the Packet.
+ */
+ ColibriConferenceIQ conferenceIQ = (ColibriConferenceIQ) iq;
+ boolean interrupted = false;
+
+ try
+ {
+ processColibriConferenceIQ(conferenceIQ);
+ }
+ catch (Throwable t)
+ {
+ logger.error(
+ "An error occurred during the processing of a "
+ + packet.getClass().getName() + " packet",
+ t);
+
+ if (t instanceof InterruptedException)
+ {
+ /*
+ * We cleared the interrupted state of the current Thread by
+ * catching the InterruptedException. However, we do not really
+ * care whether the current Thread has been interrupted - we
+ * caught the InterruptedException because we want to swallow
+ * any Throwable. Consequently, we should better restore the
+ * interrupted state.
+ */
+ interrupted = true;
+ }
+ else if (t instanceof ThreadDeath)
+ throw (ThreadDeath) t;
+ }
+ if (interrupted)
+ Thread.currentThread().interrupt();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Implements {@link RegistrationStateChangeListener}. Notifies this
+ * instance that there has been a change in the <tt>RegistrationState</tt>
+ * of {@link #protocolProvider}. Subscribes this instance to
+ * {@link ColibriConferenceIQ}s as soon as <tt>protocolProvider</tt> is
+ * registered and unsubscribes it as soon as <tt>protocolProvider</tt> is
+ * unregistered.
+ */
+ public void registrationStateChanged(RegistrationStateChangeEvent ev)
+ {
+ RegistrationState registrationState = ev.getNewState();
+
+ if (RegistrationState.REGISTERED.equals(registrationState))
+ {
+ protocolProvider.getConnection().addPacketListener(this, this);
+ }
+ else if (RegistrationState.UNREGISTERED.equals(registrationState))
+ {
+ XMPPConnection connection = protocolProvider.getConnection();
+
+ if (connection != null)
+ connection.removePacketListener(this);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java index 05f3d51..244cfe9 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/ProtocolProviderServiceJabberImpl.java @@ -1,2857 +1,2857 @@ -/* - * 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.impl.protocol.jabber; - -import java.math.*; -import java.net.*; -import java.security.*; -import java.security.cert.*; -import java.text.*; -import java.util.*; - -import javax.net.ssl.*; - -import net.java.sip.communicator.impl.protocol.jabber.debugger.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.caps.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*; -import net.java.sip.communicator.service.certificate.*; -import net.java.sip.communicator.service.dns.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.jabberconstants.*; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.Logger; - -import org.jitsi.service.configuration.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.util.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smack.provider.*; -import org.jivesoftware.smack.util.StringUtils; -import org.jivesoftware.smackx.*; -import org.jivesoftware.smackx.packet.*; -import org.osgi.framework.*; -import org.xmpp.jnodes.smack.*; - -/** - * An implementation of the protocol provider service over the Jabber protocol - * - * @author Damian Minkov - * @author Symphorien Wanko - * @author Lyubomir Marinov - * @author Yana Stamcheva - * @author Emil Ivov - */ -public class ProtocolProviderServiceJabberImpl - extends AbstractProtocolProviderService -{ - /** - * Logger of this class - */ - private static final Logger logger = - Logger.getLogger(ProtocolProviderServiceJabberImpl.class); - - /** - * Jingle's Discovery Info common URN. - */ - public static final String URN_XMPP_JINGLE = JingleIQ.NAMESPACE; - - /** - * Jingle's Discovery Info URN for RTP support. - */ - public static final String URN_XMPP_JINGLE_RTP - = RtpDescriptionPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for RTP support with audio. - */ - public static final String URN_XMPP_JINGLE_RTP_AUDIO - = "urn:xmpp:jingle:apps:rtp:audio"; - - /** - * Jingle's Discovery Info URN for RTP support with video. - */ - public static final String URN_XMPP_JINGLE_RTP_VIDEO - = "urn:xmpp:jingle:apps:rtp:video"; - - /** - * Jingle's Discovery Info URN for ZRTP support with RTP. - */ - public static final String URN_XMPP_JINGLE_RTP_ZRTP - = ZrtpHashPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for ICE_UDP transport support. - */ - public static final String URN_XMPP_JINGLE_RAW_UDP_0 - = RawUdpTransportPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for ICE_UDP transport support. - */ - public static final String URN_XMPP_JINGLE_ICE_UDP_1 - = IceUdpTransportPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for Jingle Nodes support. - */ - public static final String URN_XMPP_JINGLE_NODES - = "http://jabber.org/protocol/jinglenodes"; - - /** - * Jingle's Discovery Info URN for "XEP-0251: Jingle Session Transfer" - * support. - */ - public static final String URN_XMPP_JINGLE_TRANSFER_0 - = TransferPacketExtension.NAMESPACE; - - /** - * Jingle's Discovery Info URN for "XEP-298 :Delivering Conference - * Information to Jingle Participants (Coin)" support. - */ - public static final String URN_XMPP_JINGLE_COIN = "urn:xmpp:coin"; - - /** - * Jingle's Discovery Info URN for "XEP-0320: Use of DTLS-SRTP in - * Jingle Sessions". - */ - public static final String URN_XMPP_JINGLE_DTLS_SRTP - = "urn:xmpp:jingle:apps:dtls:0"; - - /** - * Discovery Info URN for classic RFC3264-style Offer/Answer negotiation - * with no support for Trickle ICE and low tolerance to transport/payload - * separation. Defined in XEP-0176 - */ - public static final String URN_IETF_RFC_3264 = "urn:ietf:rfc:3264"; - - /** - * Jingle's Discovery Info URN for "XEP-0294: Jingle RTP Header Extensions - * Negotiation" support. - */ - public static final String URN_XMPP_JINGLE_RTP_HDREXT = - "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; - - /** - * Capabilities name for audio call in Google Talk web version. - */ - public static final String CAPS_GTALK_WEB_VOICE = "voice-v1"; - - /** - * Capabilities name for video call (receive side) in Google Talk web - * version. - */ - public static final String CAPS_GTALK_WEB_VIDEO = "video-v1"; - - /** - * Capabilities name for video call (sender side) in Google Talk web - * version. - */ - public static final String CAPS_GTALK_WEB_CAMERA = "camera-v1"; - - /** - * Google P2P transport URN. - */ - public static final String URN_GOOGLE_TRANSPORT_P2P - = "http://www.google.com/transport/p2p"; - - /** - * URN for Google voice. - */ - public static final String URN_GOOGLE_VOICE = - "http://www.google.com/xmpp/protocol/voice/v1"; - - /** - * URN for Google camera. - */ - public static final String URN_GOOGLE_CAMERA = - "http://www.google.com/xmpp/protocol/camera/v1"; - - /** - * URN for Google video. - */ - public static final String URN_GOOGLE_VIDEO = - "http://www.google.com/xmpp/protocol/video/v1"; - - /** - * URN for XEP-0077 inband registration - */ - public static final String URN_REGISTER = "jabber:iq:register"; - - /** - * The name of the property under which the user may specify if the desktop - * streaming or sharing should be disabled. - */ - private static final String IS_DESKTOP_STREAMING_DISABLED - = "net.java.sip.communicator.impl.protocol.jabber." + - "DESKTOP_STREAMING_DISABLED"; - - /** - * The name of the property under which the user may specify if audio/video - * calls should be disabled. - */ - private static final String IS_CALLING_DISABLED - = "net.java.sip.communicator.impl.protocol.jabber.CALLING_DISABLED"; - - /** - * Smack packet reply timeout. - */ - public static final int SMACK_PACKET_REPLY_TIMEOUT = 45000; - - /** - * Property for vcard reply timeout. Time to wait before - * we think vcard retrieving has timeouted, default value - * of smack is 5000 (5 sec.). - */ - public static final String VCARD_REPLY_TIMEOUT_PROPERTY = - "net.java.sip.communicator.impl.protocol.jabber.VCARD_REPLY_TIMEOUT"; - - /** - * XMPP signaling DSCP configuration property name. - */ - private static final String XMPP_DSCP_PROPERTY = - "net.java.sip.communicator.impl.protocol.XMPP_DSCP"; - - /** - * Google voice domain name. - */ - public static final String GOOGLE_VOICE_DOMAIN = "voice.google.com"; - - /** - * Used to connect to a XMPP server. - */ - private XMPPConnection connection; - - /** - * Indicates whether or not the provider is initialized and ready for use. - */ - private boolean isInitialized = false; - - /** - * We use this to lock access to initialization. - */ - private final Object initializationLock = new Object(); - - /** - * The identifier of the account that this provider represents. - */ - private AccountID accountID = null; - - /** - * Used when we need to re-register - */ - private SecurityAuthority authority = null; - - /** - * The resource we will use when connecting during this run. - */ - private String resource = null; - - /** - * The icon corresponding to the jabber protocol. - */ - private ProtocolIconJabberImpl jabberIcon; - - /** - * A set of features supported by our Jabber implementation. - * In general, we add new feature(s) when we add new operation sets. - * (see xep-0030 : http://www.xmpp.org/extensions/xep-0030.html#info). - * Example : to tell the world that we support jingle, we simply have - * to do : - * supportedFeatures.add("http://www.xmpp.org/extensions/xep-0166.html#ns"); - * Beware there is no canonical mapping between op set and jabber features - * (op set is a SC "concept"). This means that one op set in SC can - * correspond to many jabber features. It is also possible that there is no - * jabber feature corresponding to a SC op set or again, - * we can currently support some features wich do not have a specific - * op set in SC (the mandatory feature : - * http://jabber.org/protocol/disco#info is one example). - * We can find features corresponding to op set in the xep(s) related - * to implemented functionality. - */ - private final List<String> supportedFeatures = new ArrayList<String>(); - - /** - * The <tt>ServiceDiscoveryManager</tt> is responsible for advertising - * <tt>supportedFeatures</tt> when asked by a remote client. It can also - * be used to query remote clients for supported features. - */ - private ScServiceDiscoveryManager discoveryManager = null; - - /** - * The <tt>OperationSetContactCapabilities</tt> of this - * <tt>ProtocolProviderService</tt> which is the service-public counterpart - * of {@link #discoveryManager}. - */ - private OperationSetContactCapabilitiesJabberImpl opsetContactCapabilities; - - /** - * The statuses. - */ - private JabberStatusEnum jabberStatusEnum; - - /** - * The service we use to interact with user. - */ - private CertificateService guiVerification; - - /** - * Used with tls connecting when certificates are not trusted - * and we ask the user to confirm connection. When some timeout expires - * connect method returns, and we use abortConnecting to abort further - * execution cause after user chooses we make further processing from there. - */ - private boolean abortConnecting = false; - - /** - * Flag indicating are we currently executing connectAndLogin method. - */ - private boolean inConnectAndLogin = false; - - /** - * Object used to synchronize the flag inConnectAndLogin. - */ - private final Object connectAndLoginLock = new Object(); - - /** - * If an event occurs during login we fire it at the end of the login - * process (at the end of connectAndLogin method). - */ - private RegistrationStateChangeEvent eventDuringLogin; - - /** - * Listens for connection closes or errors. - */ - private JabberConnectionListener connectionListener; - - /** - * The details of the proxy we are using to connect to the server (if any) - */ - private org.jivesoftware.smack.proxy.ProxyInfo proxy; - - /** - * Our provider manager instances. - */ - private static ProviderManager providerManager = null; - - /** - * Lock for creating provider. - */ - private static Object providerCreationLock = new Object(); - - /** - * State for connect and login state. - */ - enum ConnectState - { - /** - * Abort any further connecting. - */ - ABORT_CONNECTING, - /** - * Continue trying with next address. - */ - CONTINUE_TRYING, - /** - * Stop trying we succeeded or just have a final state for - * the whole connecting procedure. - */ - STOP_TRYING - } - - /** - * The debugger who logs packets. - */ - private SmackPacketDebugger debugger = null; - - /** - * Jingle Nodes service. - */ - private SmackServiceNode jingleNodesServiceNode = null; - - /** - * Synchronization object to monitore jingle nodes auto discovery. - */ - private final Object jingleNodesSyncRoot = new Object(); - - /** - * Stores user credentials for local use if user hasn't stored - * its password. - */ - private UserCredentials userCredentials = null; - - /** - * The currently running keepAliveManager if enabled. - */ - private KeepAliveManager keepAliveManager = null; - - /** - * The version manager. - */ - private VersionManager versionManager = null; - - // load xmpp manager classes - static - { - if(OSUtils.IS_ANDROID) - loadJabberServiceClasses(); - } - - /** - * Returns the state of the registration of this protocol provider - * @return the <tt>RegistrationState</tt> that this provider is - * currently in or null in case it is in a unknown state. - */ - public RegistrationState getRegistrationState() - { - if(connection == null) - return RegistrationState.UNREGISTERED; - else if(connection.isConnected() && connection.isAuthenticated()) - return RegistrationState.REGISTERED; - else - return RegistrationState.UNREGISTERED; - } - - /** - * Return the certificate verification service impl. - * @return the CertificateVerification service. - */ - private CertificateService getCertificateVerificationService() - { - if(guiVerification == null) - { - ServiceReference guiVerifyReference - = JabberActivator.getBundleContext().getServiceReference( - CertificateService.class.getName()); - if(guiVerifyReference != null) - guiVerification = (CertificateService) - JabberActivator.getBundleContext().getService( - guiVerifyReference); - } - - return guiVerification; - } - - /** - * Starts the registration process. Connection details such as - * registration server, user name/number are provided through the - * configuration service through implementation specific properties. - * - * @param authority the security authority that will be used for resolving - * any security challenges that may be returned during the - * registration or at any moment while we're registered. - * @throws OperationFailedException with the corresponding code it the - * registration fails for some reason (e.g. a networking error or an - * implementation problem). - */ - public void register(final SecurityAuthority authority) - throws OperationFailedException - { - if(authority == null) - throw new IllegalArgumentException( - "The register method needs a valid non-null authority impl " - + " in order to be able and retrieve passwords."); - - this.authority = authority; - - try - { - // reset states - abortConnecting = false; - - // indicate we started connectAndLogin process - synchronized(connectAndLoginLock) - { - inConnectAndLogin = true; - } - - initializeConnectAndLogin(authority, - SecurityAuthority.AUTHENTICATION_REQUIRED); - } - catch (XMPPException ex) - { - logger.error("Error registering", ex); - - eventDuringLogin = null; - - fireRegistrationStateChanged(ex); - } - finally - { - synchronized(connectAndLoginLock) - { - // Checks if an error has occurred during login, if so we fire - // it here in order to avoid a deadlock which occurs in - // reconnect plugin. The deadlock is cause we fired an event - // during login process and have locked initializationLock and - // we cannot unregister from reconnect, cause unregister method - // also needs this lock. - if(eventDuringLogin != null) - { - if(eventDuringLogin.getNewState().equals( - RegistrationState.CONNECTION_FAILED) || - eventDuringLogin.getNewState().equals( - RegistrationState.UNREGISTERED)) - disconnectAndCleanConnection(); - - fireRegistrationStateChanged( - eventDuringLogin.getOldState(), - eventDuringLogin.getNewState(), - eventDuringLogin.getReasonCode(), - eventDuringLogin.getReason()); - - eventDuringLogin = null; - inConnectAndLogin = false; - return; - } - - inConnectAndLogin = false; - } - } - } - - /** - * Connects and logins again to the server. - * - * @param authReasonCode indicates the reason of the re-authentication. - */ - void reregister(int authReasonCode) - { - try - { - if (logger.isTraceEnabled()) - logger.trace("Trying to reregister us!"); - - // sets this if any is trying to use us through registration - // to know we are not registered - this.unregister(false); - - // reset states - this.abortConnecting = false; - - // indicate we started connectAndLogin process - synchronized(connectAndLoginLock) - { - inConnectAndLogin = true; - } - - initializeConnectAndLogin(authority, authReasonCode); - } - catch(OperationFailedException ex) - { - logger.error("Error ReRegistering", ex); - - eventDuringLogin = null; - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null); - } - catch (XMPPException ex) - { - logger.error("Error ReRegistering", ex); - - eventDuringLogin = null; - - fireRegistrationStateChanged(ex); - } - finally - { - synchronized(connectAndLoginLock) - { - // Checks if an error has occurred during login, if so we fire - // it here in order to avoid a deadlock which occurs in - // reconnect plugin. The deadlock is cause we fired an event - // during login process and have locked initializationLock and - // we cannot unregister from reconnect, cause unregister method - // also needs this lock. - if(eventDuringLogin != null) - { - if(eventDuringLogin.getNewState().equals( - RegistrationState.CONNECTION_FAILED) || - eventDuringLogin.getNewState().equals( - RegistrationState.UNREGISTERED)) - disconnectAndCleanConnection(); - - fireRegistrationStateChanged( - eventDuringLogin.getOldState(), - eventDuringLogin.getNewState(), - eventDuringLogin.getReasonCode(), - eventDuringLogin.getReason()); - - eventDuringLogin = null; - inConnectAndLogin = false; - return; - } - - inConnectAndLogin = false; - } - } - } - - /** - * Indicates if the XMPP transport channel is using a TLS secured socket. - * - * @return True when TLS is used, false otherwise. - */ - public boolean isSignalingTransportSecure() - { - return connection != null && connection.isUsingTLS(); - } - - /** - * Returns the "transport" protocol of this instance used to carry the - * control channel for the current protocol service. - * - * @return The "transport" protocol of this instance: TCP, TLS or UNKNOWN. - */ - public TransportProtocol getTransportProtocol() - { - // Without a connection, there is no transport available. - if(connection != null && connection.isConnected()) - { - // Transport using a secure connection. - if(connection.isUsingTLS()) - { - return TransportProtocol.TLS; - } - // Transport using a unsecure connection. - return TransportProtocol.TCP; - } - return TransportProtocol.UNKNOWN; - } - - /** - * Connects and logins to the server - * @param authority SecurityAuthority - * @param reasonCode the authentication reason code. Indicates the reason of - * this authentication. - * @throws XMPPException if we cannot connect to the server - network problem - * @throws OperationFailedException if login parameters - * as server port are not correct - */ - private void initializeConnectAndLogin(SecurityAuthority authority, - int reasonCode) - throws XMPPException, OperationFailedException - { - synchronized(initializationLock) - { - JabberLoginStrategy loginStrategy = createLoginStrategy(); - userCredentials = loginStrategy.prepareLogin(authority, reasonCode); - if(!loginStrategy.loginPreparationSuccessful()) - return; - - String serviceName - = StringUtils.parseServer(getAccountID().getUserID()); - - loadResource(); - loadProxy(); - Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual); - - ConnectState state; - //[0] = hadDnsSecException - boolean[] hadDnsSecException = new boolean[]{false}; - - // try connecting with auto-detection if enabled - boolean isServerOverriden = - getAccountID().getAccountPropertyBoolean( - ProtocolProviderFactory.IS_SERVER_OVERRIDDEN, false); - - if(!isServerOverriden) - { - state = connectUsingSRVRecords(serviceName, - serviceName, hadDnsSecException, loginStrategy); - if(hadDnsSecException[0]) - { - setDnssecLoginFailure(); - return; - } - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - - // check for custom xmpp domain which we will check for - // SRV records for server addresses - String customXMPPDomain = getAccountID() - .getAccountPropertyString("CUSTOM_XMPP_DOMAIN"); - - if(customXMPPDomain != null && !hadDnsSecException[0]) - { - logger.info("Connect using custom xmpp domain: " + - customXMPPDomain); - - state = connectUsingSRVRecords( - customXMPPDomain, serviceName, - hadDnsSecException, loginStrategy); - - logger.info("state for connectUsingSRVRecords: " + state); - - if(hadDnsSecException[0]) - { - setDnssecLoginFailure(); - return; - } - - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - - // connect with specified server name - String serverAddressUserSetting - = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.SERVER_ADDRESS); - - int serverPort = getAccountID().getAccountPropertyInt( - ProtocolProviderFactory.SERVER_PORT, 5222); - - InetSocketAddress[] addrs = null; - try - { - addrs = NetworkUtils.getAandAAAARecords( - serverAddressUserSetting, - serverPort - ); - } - catch (ParseException e) - { - logger.error("Domain not resolved", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for overridden server", e); - setDnssecLoginFailure(); - return; - } - - if (addrs == null || addrs.length == 0) - { - logger.error("No server addresses found"); - - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, - "No server addresses found"); - } - else - { - for (InetSocketAddress isa : addrs) - { - try - { - state = connectAndLogin(isa, serviceName, - loginStrategy); - if(state == ConnectState.ABORT_CONNECTING - || state == ConnectState.STOP_TRYING) - return; - } - catch(XMPPException ex) - { - disconnectAndCleanConnection(); - if(isAuthenticationFailed(ex)) - throw ex; - } - } - } - } - } - - /** - * Creates the JabberLoginStrategy to use for the current account. - */ - private JabberLoginStrategy createLoginStrategy() - { - String clientCertId = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE); - if(clientCertId != null) - { - return new LoginByClientCertificateStrategy(getAccountID()); - } - else - { - return new LoginByPasswordStrategy(this, getAccountID()); - } - } - - private void setDnssecLoginFailure() - { - eventDuringLogin = new RegistrationStateChangeEvent( - this, - getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, - "No usable host found due to DNSSEC failures"); - } - - /** - * Connects using the domain specified and its SRV records. - * @param domain the domain to use - * @param serviceName the domain name of the user's login - * @param dnssecState state of possible received DNSSEC exceptions - * @param loginStrategy the login strategy to use - * @return whether to continue trying or stop. - */ - private ConnectState connectUsingSRVRecords( - String domain, - String serviceName, - boolean[] dnssecState, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - // check to see is there SRV records for this server domain - SRVRecord srvRecords[] = null; - try - { - srvRecords = NetworkUtils - .getSRVRecords("xmpp-client", "tcp", domain); - } - catch (ParseException e) - { - logger.error("SRV record not resolved", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for SRV lookup", e); - dnssecState[0] = true; - } - - if(srvRecords != null) - { - for(SRVRecord srv : srvRecords) - { - InetSocketAddress[] addrs = null; - try - { - addrs = - NetworkUtils.getAandAAAARecords( - srv.getTarget(), - srv.getPort() - ); - } - catch (ParseException e) - { - logger.error("Invalid SRV record target", e); - } - catch (DnssecException e) - { - logger.error("DNSSEC failure for A/AAAA lookup of SRV", e); - dnssecState[0] = true; - } - - if (addrs == null || addrs.length == 0) - { - logger.error("No A/AAAA addresses found for " + - srv.getTarget()); - continue; - } - - for (InetSocketAddress isa : addrs) - { - try - { - // if failover mechanism is enabled, use it, - // default is not enabled. - if(JabberActivator.getConfigurationService() - .getBoolean(FailoverConnectionMonitor - .REVERSE_FAILOVER_ENABLED_PROP, - false - )) - { - FailoverConnectionMonitor.getInstance(this) - .setCurrent(serviceName, - srv.getTarget()); - } - - ConnectState state = connectAndLogin( - isa, serviceName, loginStrategy); - return state; - } - catch(XMPPException ex) - { - logger.error("Error connecting to " + isa - + " for domain:" + domain - + " serviceName:" + serviceName, ex); - - disconnectAndCleanConnection(); - - if(isAuthenticationFailed(ex)) - throw ex; - } - } - } - } - else - logger.error("No SRV addresses found for _xmpp-client._tcp." - + domain); - - return ConnectState.CONTINUE_TRYING; - } - - /** - * Tries to login to the XMPP server with the supplied user ID. If the - * protocol is Google Talk, the user ID including the service name is used. - * For other protocols, if the login with the user ID without the service - * name fails, a second attempt including the service name is made. - * - * @param currentAddress the IP address to connect to - * @param serviceName the domain name of the user's login - * @param loginStrategy the login strategy to use - * @throws XMPPException when a failure occurs - */ - private ConnectState connectAndLogin(InetSocketAddress currentAddress, - String serviceName, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - String userID = null; - boolean qualifiedUserID; - - /* with a google account (either gmail or google apps - * related ones), the userID MUST be the full e-mail address - * not just the ID - */ - if(getAccountID().getProtocolDisplayName().equals("Google Talk")) - { - userID = getAccountID().getUserID(); - qualifiedUserID = true; - } - else - { - userID = StringUtils.parseName(getAccountID().getUserID()); - qualifiedUserID = false; - } - - try - { - return connectAndLogin( - currentAddress, serviceName, - userID, resource, loginStrategy); - } - catch(XMPPException ex) - { - // server disconnect us after such an error, do cleanup - disconnectAndCleanConnection(); - - //no need to check with a different username if the - //socket could not be opened - if (ex.getWrappedThrowable() instanceof ConnectException - || ex.getWrappedThrowable() instanceof NoRouteToHostException) - { - //as we got an exception not handled in connectAndLogin - //no state was set, so fire it here so we can continue - //with the re-register process - //2013-08-07 do not fire event, if we have several - // addresses and we fire event will activate reconnect - // but we will continue connecting with other addresses - // and can register with address, then unregister and try again - // that is from reconnect plugin. - // Storing event for fire after all have failed and we have - // tried every address. - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, - null); - - throw ex; - } - - // don't attempt to append the service name if it's already there - if (!qualifiedUserID) - { - try - { - // logging in might need the service name - return connectAndLogin( - currentAddress, serviceName, - userID + "@" + serviceName, - resource, - loginStrategy); - } - catch(XMPPException ex2) - { - disconnectAndCleanConnection(); - throw ex; //throw the original exception - } - } - else - throw ex; - } - } - - /** - * Initializes the Jabber Resource identifier. - */ - private void loadResource() - { - if(resource == null) - { - String defaultResource = "jitsi"; - String autoGenenerateResource = - getAccountID().getAccountPropertyString( - ProtocolProviderFactory.AUTO_GENERATE_RESOURCE); - if(autoGenenerateResource == null || - Boolean.parseBoolean(autoGenenerateResource)) - { - SecureRandom random = new SecureRandom(); - - resource = defaultResource + "-" + - new BigInteger(32, random).toString(32); - } - else - { - resource = getAccountID().getAccountPropertyString( - ProtocolProviderFactory.RESOURCE); - - if(resource == null || resource.length() == 0) - resource = defaultResource; - } - } - } - - /** - * Sets the global proxy information based on the configuration - * - * @throws OperationFailedException - */ - private void loadProxy() throws OperationFailedException - { - String globalProxyType = - JabberActivator.getConfigurationService() - .getString(ProxyInfo.CONNECTION_PROXY_TYPE_PROPERTY_NAME); - if(globalProxyType == null || - globalProxyType.equals(ProxyInfo.ProxyType.NONE.name())) - { - proxy = org.jivesoftware.smack.proxy.ProxyInfo.forNoProxy(); - } - else - { - String globalProxyAddress = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_ADDRESS_PROPERTY_NAME); - String globalProxyPortStr = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_PORT_PROPERTY_NAME); - int globalProxyPort; - try - { - globalProxyPort = Integer.parseInt( - globalProxyPortStr); - } - catch(NumberFormatException ex) - { - throw new OperationFailedException("Wrong proxy port, " - + globalProxyPortStr - + " does not represent an integer", - OperationFailedException.INVALID_ACCOUNT_PROPERTIES, - ex); - } - String globalProxyUsername = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_USERNAME_PROPERTY_NAME); - String globalProxyPassword = - JabberActivator.getConfigurationService().getString( - ProxyInfo.CONNECTION_PROXY_PASSWORD_PROPERTY_NAME); - if(globalProxyAddress == null || - globalProxyAddress.length() <= 0) - { - throw new OperationFailedException( - "Missing Proxy Address", - OperationFailedException.INVALID_ACCOUNT_PROPERTIES); - } - try - { - proxy = new org.jivesoftware.smack.proxy.ProxyInfo( - Enum.valueOf(org.jivesoftware.smack.proxy.ProxyInfo. - ProxyType.class, globalProxyType), - globalProxyAddress, globalProxyPort, - globalProxyUsername, globalProxyPassword); - } - catch(IllegalArgumentException e) - { - logger.error("Invalid value for smack proxy enum", e); - proxy = null; - } - } - } - - /** - * Connects xmpp connection and login. Returning the state whether is it - * final - Abort due to certificate cancel or keep trying cause only current - * address has failed or stop trying cause we succeeded. - * @param address the address to connect to - * @param serviceName the service name to use - * @param userName the username to use - * @param resource and the resource. - * @param loginStrategy the login strategy to use - * @return return the state how to continue the connect process. - * @throws XMPPException if we cannot connect for some reason - */ - private ConnectState connectAndLogin( - InetSocketAddress address, String serviceName, - String userName, String resource, - JabberLoginStrategy loginStrategy) - throws XMPPException - { - ConnectionConfiguration confConn = new ConnectionConfiguration( - address.getAddress().getHostAddress(), - address.getPort(), - serviceName, proxy - ); - - confConn.setReconnectionAllowed(false); - boolean tlsRequired = loginStrategy.isTlsRequired(); - - // user have the possibility to disable TLS but in this case, it will - // not be able to connect to a server which requires TLS - confConn.setSecurityMode( - tlsRequired ? ConnectionConfiguration.SecurityMode.required : - ConnectionConfiguration.SecurityMode.enabled); - - if(connection != null) - { - logger.error("Connection is not null and isConnected:" - + connection.isConnected(), - new Exception("Trace possible duplicate connections: " + - getAccountID().getAccountAddress())); - disconnectAndCleanConnection(); - } - - connection = new XMPPConnection(confConn); - - try - { - CertificateService cvs = - getCertificateVerificationService(); - if(cvs != null) - { - SSLContext sslContext = loginStrategy.createSslContext(cvs, - getTrustManager(cvs, serviceName)); - connection.setCustomSslContext(sslContext); - } - else if (tlsRequired) - throw new XMPPException( - "Certificate verification service is " - + "unavailable and TLS is required"); - } - catch(GeneralSecurityException e) - { - logger.error("Error creating custom trust manager", e); - throw new XMPPException("Error creating custom trust manager", e); - } - - if(debugger == null) - debugger = new SmackPacketDebugger(); - - // sets the debugger - debugger.setConnection(connection); - connection.addPacketListener(debugger, null); - connection.addPacketInterceptor(debugger, null); - - connection.connect(); - - setTrafficClass(); - - if(abortConnecting) - { - abortConnecting = false; - disconnectAndCleanConnection(); - - return ConnectState.ABORT_CONNECTING; - } - - registerServiceDiscoveryManager(); - - if(connectionListener == null) - { - connectionListener = new JabberConnectionListener(); - } - - if(!connection.isSecureConnection() && tlsRequired) - { - throw new XMPPException("TLS is required by client"); - } - - if(!connection.isConnected()) - { - // connection is not connected, lets set state to our connection - // as failed seems there is some lag/problem with network - // and this way we will inform for it and later reconnect if needed - // as IllegalStateException that is thrown within - // addConnectionListener is not handled properly - disconnectAndCleanConnection(); - - logger.error("Connection not established, server not found!"); - - eventDuringLogin = null; - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, null); - - return ConnectState.ABORT_CONNECTING; - } - else - { - connection.addConnectionListener(connectionListener); - } - - if(abortConnecting) - { - abortConnecting = false; - disconnectAndCleanConnection(); - - return ConnectState.ABORT_CONNECTING; - } - - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.REGISTERING - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - - if (!loginStrategy.login(connection, userName, resource)) - { - disconnectAndCleanConnection(); - eventDuringLogin = null; - fireRegistrationStateChanged( - getRegistrationState(), - // not auth failed, or there would be no info-popup - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED, - loginStrategy.getClass().getName() + " requests abort"); - - return ConnectState.ABORT_CONNECTING; - } - - if(connection.isAuthenticated()) - { - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.REGISTERED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null); - - /* The initial presence message is sent by smack stack and does not - * include priority information. In case the original status is - * AVAILABLE, we will not update our presence information (such as - * our priority) when we registered. - */ - OperationSetPersistentPresenceJabberImpl opSet = - (OperationSetPersistentPresenceJabberImpl) - this.getOperationSet(OperationSetPersistentPresence.class); - - try - { - opSet.publishPresenceStatus(getJabberStatusEnum().getStatus( - JabberStatusEnum.AVAILABLE), ""); - } - catch(Exception e) - { - logger.error("Failed to publish presence status"); - } - - return ConnectState.STOP_TRYING; - } - else - { - disconnectAndCleanConnection(); - - eventDuringLogin = null; - - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.UNREGISTERED - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - - return ConnectState.CONTINUE_TRYING; - } - } - - /** - * Gets the TrustManager that should be used for the specified service - * - * @param serviceName the service name - * @param cvs The CertificateVerificationService to retrieve the - * trust manager - * @return the trust manager - */ - private X509TrustManager getTrustManager(CertificateService cvs, - String serviceName) - throws GeneralSecurityException - { - return new HostTrustManager( - cvs.getTrustManager( - Arrays.asList(new String[]{ - serviceName, - "_xmpp-client." + serviceName - }) - ) - ); - } - - /** - * Registers our ServiceDiscoveryManager - */ - private void registerServiceDiscoveryManager() - { - // we setup supported features no packets are actually sent - //during feature registration so we'd better do it here so that - //our first presence update would contain a caps with the right - //features. - String name - = System.getProperty( - "sip-communicator.application.name", - "SIP Communicator ") - + System.getProperty("sip-communicator.version","SVN"); - - ServiceDiscoveryManager.setIdentityName(name); - ServiceDiscoveryManager.setIdentityType("pc"); - - discoveryManager - = new ScServiceDiscoveryManager( - this, - new String[] { "http://jabber.org/protocol/commands"}, - // Add features Jitsi supports in addition to smack. - supportedFeatures.toArray( - new String[supportedFeatures.size()])); - - boolean isCallingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_CALLING_DISABLED, false); - - boolean isCallingDisabledForAccount = false; - if (accountID != null && accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, - false)) - isCallingDisabled = true; - - if(isGTalkTesting() - && !isCallingDisabled - && !isCallingDisabledForAccount) - { - // Add Google Talk "ext" capabilities - discoveryManager.addExtFeature(CAPS_GTALK_WEB_VOICE); - discoveryManager.addExtFeature(CAPS_GTALK_WEB_VIDEO); - discoveryManager.addExtFeature(CAPS_GTALK_WEB_CAMERA); - discoveryManager.addFeature(URN_GOOGLE_VOICE); - discoveryManager.addFeature(URN_GOOGLE_VIDEO); - discoveryManager.addFeature(URN_GOOGLE_CAMERA); - discoveryManager.addFeature(URN_GOOGLE_TRANSPORT_P2P); - } - - /* - * Expose the discoveryManager as service-public through the - * OperationSetContactCapabilities of this ProtocolProviderService. - */ - if (opsetContactCapabilities != null) - opsetContactCapabilities.setDiscoveryManager(discoveryManager); - } - - /** - * Used to disconnect current connection and clean it. - */ - public void disconnectAndCleanConnection() - { - if(connection != null) - { - connection.removeConnectionListener(connectionListener); - - // disconnect anyway cause it will clear any listeners - // that maybe added even if its not connected - try - { - OperationSetPersistentPresenceJabberImpl opSet = - (OperationSetPersistentPresenceJabberImpl) - this.getOperationSet(OperationSetPersistentPresence.class); - - Presence unavailablePresence = - new Presence(Presence.Type.unavailable); - - if(opSet != null - && !org.jitsi.util.StringUtils - .isNullOrEmpty(opSet.getCurrentStatusMessage())) - { - unavailablePresence.setStatus( - opSet.getCurrentStatusMessage()); - } - - connection.disconnect(unavailablePresence); - } catch (Exception e) - {} - - connectionListener = null; - connection = null; - // make it null as it also holds a reference to the old connection - // will be created again on new connection - try - { - /* - * The discoveryManager is exposed as service-public by the - * OperationSetContactCapabilities of this - * ProtocolProviderService. No longer expose it because it's - * going away. - */ - if (opsetContactCapabilities != null) - opsetContactCapabilities.setDiscoveryManager(null); - } - finally - { - if(discoveryManager != null) - { - discoveryManager.stop(); - discoveryManager = null; - } - } - } - } - - /** - * Ends the registration of this protocol provider with the service. - */ - public void unregister() - { - unregister(true); - } - - /** - * Unregister and fire the event if requested - * @param fireEvent boolean - */ - public void unregister(boolean fireEvent) - { - synchronized(initializationLock) - { - if(fireEvent) - { - eventDuringLogin = null; - fireRegistrationStateChanged( - getRegistrationState() - , RegistrationState.UNREGISTERING - , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED - , null); - } - - disconnectAndCleanConnection(); - - RegistrationState currRegState = getRegistrationState(); - - if(fireEvent) - { - eventDuringLogin = null; - fireRegistrationStateChanged( - currRegState, - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, null); - } - } - } - - /** - * Returns the short name of the protocol that the implementation of this - * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for - * example). - * - * @return a String containing the short name of the protocol this - * service is taking care of. - */ - public String getProtocolName() - { - return ProtocolNames.JABBER; - } - - /** - * Initialized the service implementation, and puts it in a sate where it - * could interoperate with other services. It is strongly recommended that - * properties in this Map be mapped to property names as specified by - * <tt>AccountProperties</tt>. - * - * @param screenname the account id/uin/screenname of the account that - * we're about to create - * @param accountID the identifier of the account that this protocol - * provider represents. - * - * @see net.java.sip.communicator.service.protocol.AccountID - */ - protected void initialize(String screenname, - AccountID accountID) - { - synchronized(initializationLock) - { - this.accountID = accountID; - - // in case of modified account, we clear list of supported features - // and every state change listeners, otherwise we can have two - // OperationSet for same feature and it can causes problem (i.e. - // two OperationSetBasicTelephony can launch two ICE negociations - // (with different ufrag/passwd) and peer will failed call. And - // by the way user will see two dialog for answering/refusing the - // call - supportedFeatures.clear(); - this.clearRegistrationStateChangeListener(); - this.clearSupportedOperationSet(); - - synchronized(providerCreationLock) - { - if(providerManager == null) - { - try - { - ProviderManager.setInstance(new ProviderManagerExt()); - } - catch(Throwable t) - { - // once loaded if we try to set instance second time - // IllegalStateException is thrown - } - finally - { - providerManager = ProviderManager.getInstance(); - } - } - } - - String protocolIconPath - = accountID.getAccountPropertyString( - ProtocolProviderFactory.PROTOCOL_ICON_PATH); - - if (protocolIconPath == null) - protocolIconPath = "resources/images/protocol/jabber"; - - jabberIcon = new ProtocolIconJabberImpl(protocolIconPath); - - jabberStatusEnum - = JabberStatusEnum.getJabberStatusEnum(protocolIconPath); - - //this feature is mandatory to be compliant with Service Discovery - supportedFeatures.add("http://jabber.org/protocol/disco#info"); - - String keepAliveStrValue - = accountID.getAccountPropertyString( - ProtocolProviderFactory.KEEP_ALIVE_METHOD); - - InfoRetreiver infoRetreiver = new InfoRetreiver(this, screenname); - - //initialize the presence operationset - OperationSetPersistentPresenceJabberImpl persistentPresence = - new OperationSetPersistentPresenceJabberImpl(this, infoRetreiver); - - addSupportedOperationSet( - OperationSetPersistentPresence.class, - persistentPresence); - // TODO: add the feature, if any, corresponding to persistent - // presence, if someone knows - // supportedFeatures.add(_PRESENCE_); - - //register it once again for those that simply need presence - addSupportedOperationSet( - OperationSetPresence.class, - persistentPresence); - - //initialize the IM operation set - OperationSetBasicInstantMessagingJabberImpl basicInstantMessaging = - new OperationSetBasicInstantMessagingJabberImpl(this); - - if (keepAliveStrValue == null - || keepAliveStrValue.equalsIgnoreCase("XEP-0199")) - { - if(keepAliveManager == null) - keepAliveManager = new KeepAliveManager(this); - } - - addSupportedOperationSet( - OperationSetBasicInstantMessaging.class, - basicInstantMessaging); - - // The http://jabber.org/protocol/xhtml-im feature is included - // already in smack. - - addSupportedOperationSet( - OperationSetExtendedAuthorizations.class, - new OperationSetExtendedAuthorizationsJabberImpl( - this, - persistentPresence)); - - //initialize the Whiteboard operation set - addSupportedOperationSet( - OperationSetWhiteboarding.class, - new OperationSetWhiteboardingJabberImpl(this)); - - //initialize the typing notifications operation set - addSupportedOperationSet( - OperationSetTypingNotifications.class, - new OperationSetTypingNotificationsJabberImpl(this)); - - // The http://jabber.org/protocol/chatstates feature implemented in - // OperationSetTypingNotifications is included already in smack. - - //initialize the multi user chat operation set - addSupportedOperationSet( - OperationSetMultiUserChat.class, - new OperationSetMultiUserChatJabberImpl(this)); - - addSupportedOperationSet( - OperationSetServerStoredContactInfo.class, - new OperationSetServerStoredContactInfoJabberImpl( - infoRetreiver)); - - OperationSetServerStoredAccountInfo accountInfo = - new OperationSetServerStoredAccountInfoJabberImpl(this, - infoRetreiver, - screenname); - - addSupportedOperationSet( - OperationSetServerStoredAccountInfo.class, - accountInfo); - - // Initialize avatar operation set - addSupportedOperationSet( - OperationSetAvatar.class, - new OperationSetAvatarJabberImpl(this, accountInfo)); - - // initialize the file transfer operation set - addSupportedOperationSet( - OperationSetFileTransfer.class, - new OperationSetFileTransferJabberImpl(this)); - - addSupportedOperationSet( - OperationSetInstantMessageTransform.class, - new OperationSetInstantMessageTransformImpl()); - - // Include features we're supporting in addition to the four - // included by smack itself: - // http://jabber.org/protocol/si/profile/file-transfer - // http://jabber.org/protocol/si - // http://jabber.org/protocol/bytestreams - // http://jabber.org/protocol/ibb - supportedFeatures.add("urn:xmpp:thumbs:0"); - supportedFeatures.add("urn:xmpp:bob"); - - // initialize the thumbnailed file factory operation set - addSupportedOperationSet( - OperationSetThumbnailedFileFactory.class, - new OperationSetThumbnailedFileFactoryImpl()); - - // TODO: this is the "main" feature to advertise when a client - // support muc. We have to add some features for - // specific functionality we support in muc. - // see http://www.xmpp.org/extensions/xep-0045.html - - // The http://jabber.org/protocol/muc feature is already included in - // smack. - supportedFeatures.add("http://jabber.org/protocol/muc#rooms"); - supportedFeatures.add("http://jabber.org/protocol/muc#traffic"); - - // RTP HDR extension - supportedFeatures.add(URN_XMPP_JINGLE_RTP_HDREXT); - - //register our jingle provider - providerManager.addIQProvider( JingleIQ.ELEMENT_NAME, - JingleIQ.NAMESPACE, - new JingleIQProvider()); - - // register our input event provider - providerManager.addIQProvider(InputEvtIQ.ELEMENT_NAME, - InputEvtIQ.NAMESPACE, - new InputEvtIQProvider()); - - // register our coin provider - providerManager.addIQProvider(CoinIQ.ELEMENT_NAME, - CoinIQ.NAMESPACE, - new CoinIQProvider()); - supportedFeatures.add(URN_XMPP_JINGLE_COIN); - - //register our GTalk dialect provider - providerManager.addIQProvider( SessionIQ.ELEMENT_NAME, - SessionIQ.NAMESPACE, - new SessionIQProvider()); - - // register our JingleInfo provider - providerManager.addIQProvider(JingleInfoQueryIQ.ELEMENT_NAME, - JingleInfoQueryIQ.NAMESPACE, - new JingleInfoQueryIQProvider()); - - // Jitsi VideoBridge IQProvider and PacketExtensionProvider - providerManager.addIQProvider( - ColibriConferenceIQ.ELEMENT_NAME, - ColibriConferenceIQ.NAMESPACE, - new ColibriIQProvider()); - providerManager.addExtensionProvider( - PayloadTypePacketExtension.ELEMENT_NAME, - ColibriConferenceIQ.NAMESPACE, - new DefaultPacketExtensionProvider< - PayloadTypePacketExtension>( - PayloadTypePacketExtension.class)); - providerManager.addExtensionProvider( - ParameterPacketExtension.ELEMENT_NAME, - ColibriConferenceIQ.NAMESPACE, - new DefaultPacketExtensionProvider< - ParameterPacketExtension>( - ParameterPacketExtension.class)); - - providerManager.addExtensionProvider( - ConferenceDescriptionPacketExtension.ELEMENT_NAME, - ConferenceDescriptionPacketExtension.NAMESPACE, - new ConferenceDescriptionPacketExtension.Provider()); - - //initialize the telephony operation set - boolean isCallingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_CALLING_DISABLED, false); - - boolean isCallingDisabledForAccount - = accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT, - false); - - // Check if calling is enabled. - if (!isCallingDisabled && !isCallingDisabledForAccount) - { - OperationSetBasicTelephonyJabberImpl basicTelephony - = new OperationSetBasicTelephonyJabberImpl(this); - - addSupportedOperationSet( - OperationSetAdvancedTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetBasicTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetSecureZrtpTelephony.class, - basicTelephony); - addSupportedOperationSet( - OperationSetSecureSDesTelephony.class, - basicTelephony); - - // initialize video telephony OperationSet - addSupportedOperationSet( - OperationSetVideoTelephony.class, - new OperationSetVideoTelephonyJabberImpl(basicTelephony)); - - addSupportedOperationSet( - OperationSetTelephonyConferencing.class, - new OperationSetTelephonyConferencingJabberImpl(this)); - - addSupportedOperationSet( - OperationSetBasicAutoAnswer.class, - new OperationSetAutoAnswerJabberImpl(this)); - - addSupportedOperationSet( - OperationSetResourceAwareTelephony.class, - new OperationSetResAwareTelephonyJabberImpl(basicTelephony)); - - // Only init video bridge if enabled - boolean isVideoBridgeDisabled - = JabberActivator.getConfigurationService() - .getBoolean(OperationSetVideoBridge. - IS_VIDEO_BRIDGE_DISABLED, false); - - if (!isVideoBridgeDisabled) - { - // init video bridge - addSupportedOperationSet( - OperationSetVideoBridge.class, - new OperationSetVideoBridgeImpl(this)); - } - - // init DTMF - OperationSetDTMFJabberImpl operationSetDTMFSip - = new OperationSetDTMFJabberImpl(this); - addSupportedOperationSet( - OperationSetDTMF.class, operationSetDTMFSip); - - addJingleFeatures(); - - // Check if desktop streaming is enabled. - boolean isDesktopStreamingDisabled - = JabberActivator.getConfigurationService() - .getBoolean(IS_DESKTOP_STREAMING_DISABLED, false); - - boolean isAccountDesktopStreamingDisabled - = accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_DESKTOP_STREAMING_DISABLED, - false); - - if (!isDesktopStreamingDisabled - && !isAccountDesktopStreamingDisabled) - { - // initialize desktop streaming OperationSet - addSupportedOperationSet( - OperationSetDesktopStreaming.class, - new OperationSetDesktopStreamingJabberImpl( - basicTelephony)); - - // initialize desktop sharing OperationSets - addSupportedOperationSet( - OperationSetDesktopSharingServer.class, - new OperationSetDesktopSharingServerJabberImpl( - basicTelephony)); - - // Adds extension to support remote control as a sharing - // server (sharer). - supportedFeatures.add(InputEvtIQ.NAMESPACE_SERVER); - - addSupportedOperationSet( - OperationSetDesktopSharingClient.class, - new OperationSetDesktopSharingClientJabberImpl(this) - ); - // Adds extension to support remote control as a sharing - // client (sharee). - supportedFeatures.add(InputEvtIQ.NAMESPACE_CLIENT); - } - } - - // OperationSetContactCapabilities - opsetContactCapabilities - = new OperationSetContactCapabilitiesJabberImpl(this); - if (discoveryManager != null) - opsetContactCapabilities.setDiscoveryManager(discoveryManager); - addSupportedOperationSet( - OperationSetContactCapabilities.class, - opsetContactCapabilities); - - addSupportedOperationSet( - OperationSetGenericNotifications.class, - new OperationSetGenericNotificationsJabberImpl(this)); - - supportedFeatures.add("jabber:iq:version"); - if(versionManager == null) - versionManager = new VersionManager(this); - - supportedFeatures.add(MessageCorrectionExtension.NAMESPACE); - addSupportedOperationSet(OperationSetMessageCorrection.class, - basicInstantMessaging); - - OperationSetChangePassword opsetChangePassword - = new OperationSetChangePasswordJabberImpl(this); - addSupportedOperationSet(OperationSetChangePassword.class, - opsetChangePassword); - - OperationSetCusaxUtils opsetCusaxCusaxUtils - = new OperationSetCusaxUtilsJabberImpl(this); - addSupportedOperationSet(OperationSetCusaxUtils.class, - opsetCusaxCusaxUtils); - - isInitialized = true; - } - } - - /** - * Adds Jingle related features to the supported features. - */ - private void addJingleFeatures() - { - // Add Jingle features to supported features. - supportedFeatures.add(URN_XMPP_JINGLE); - supportedFeatures.add(URN_XMPP_JINGLE_RTP); - supportedFeatures.add(URN_XMPP_JINGLE_RAW_UDP_0); - - /* - * Reflect the preference of the user with respect to the use of - * ICE. - */ - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_USE_ICE, - true)) - { - supportedFeatures.add(URN_XMPP_JINGLE_ICE_UDP_1); - } - - supportedFeatures.add(URN_XMPP_JINGLE_RTP_AUDIO); - supportedFeatures.add(URN_XMPP_JINGLE_RTP_VIDEO); - supportedFeatures.add(URN_XMPP_JINGLE_RTP_ZRTP); - - /* - * Reflect the preference of the user with respect to the use of - * Jingle Nodes. - */ - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactoryJabberImpl.IS_USE_JINGLE_NODES, - true)) - { - supportedFeatures.add(URN_XMPP_JINGLE_NODES); - } - - // XEP-0251: Jingle Session Transfer - supportedFeatures.add(URN_XMPP_JINGLE_TRANSFER_0); - - // XEP-0320: Use of DTLS-SRTP in Jingle Sessions - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME)) - { - supportedFeatures.add(URN_XMPP_JINGLE_DTLS_SRTP); - } - } - - /** - * Makes the service implementation close all open sockets and release - * any resources that it might have taken and prepare for - * shutdown/garbage collection. - */ - public void shutdown() - { - synchronized(initializationLock) - { - if (logger.isTraceEnabled()) - logger.trace("Killing the Jabber Protocol Provider."); - - //kill all active calls - OperationSetBasicTelephonyJabberImpl telephony - = (OperationSetBasicTelephonyJabberImpl)getOperationSet( - OperationSetBasicTelephony.class); - if (telephony != null) - { - telephony.shutdown(); - } - - disconnectAndCleanConnection(); - - isInitialized = false; - } - } - - /** - * Returns true if the provider service implementation is initialized and - * ready for use by other services, and false otherwise. - * - * @return true if the provider is initialized and ready for use and false - * otherwise - */ - public boolean isInitialized() - { - return isInitialized; - } - - /** - * Returns the AccountID that uniquely identifies the account represented - * by this instance of the ProtocolProviderService. - * @return the id of the account represented by this provider. - */ - public AccountID getAccountID() - { - return accountID; - } - - /** - * Returns the <tt>XMPPConnection</tt>opened by this provider - * @return a reference to the <tt>XMPPConnection</tt> last opened by this - * provider. - */ - public XMPPConnection getConnection() - { - return connection; - } - - /** - * Determines whether a specific <tt>XMPPException</tt> signals that - * attempted authentication has failed. - * - * @param ex the <tt>XMPPException</tt> which is to be determined whether it - * signals that attempted authentication has failed - * @return <tt>true</tt> if the specified <tt>ex</tt> signals that attempted - * authentication has failed; otherwise, <tt>false</tt> - */ - private boolean isAuthenticationFailed(XMPPException ex) - { - String exMsg = ex.getMessage().toLowerCase(); - - // as there are no types or reasons for XMPPException - // we try determine the reason according to their message - // all messages that were found in smack 3.1.0 were took in count - return - ((exMsg.indexOf("sasl authentication") != -1) - && (exMsg.indexOf("failed") != -1)) - || (exMsg.indexOf( - "does not support compatible authentication mechanism") - != -1) - || (exMsg.indexOf("unable to determine password") != -1); - } - - /** - * Tries to determine the appropriate message and status to fire, - * according the exception. - * - * @param ex the {@link XMPPException} that caused the state change. - */ - private void fireRegistrationStateChanged(XMPPException ex) - { - int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; - RegistrationState regState = RegistrationState.UNREGISTERED; - String reasonStr = null; - - Throwable wrappedEx = ex.getWrappedThrowable(); - if(wrappedEx != null - && (wrappedEx instanceof UnknownHostException - || wrappedEx instanceof ConnectException - || wrappedEx instanceof SocketException)) - { - reason = RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND; - regState = RegistrationState.CONNECTION_FAILED; - } - else - { - String exMsg = ex.getMessage().toLowerCase(); - - // as there are no types or reasons for XMPPException - // we try determine the reason according to their message - // all messages that were found in smack 3.1.0 were took in count - if(isAuthenticationFailed(ex)) - { - JabberActivator.getProtocolProviderFactory(). - storePassword(getAccountID(), null); - - reason = RegistrationStateChangeEvent - .REASON_AUTHENTICATION_FAILED; - - regState = RegistrationState.AUTHENTICATION_FAILED; - - fireRegistrationStateChanged( - getRegistrationState(), regState, reason, null); - - // Try to reregister and to ask user for a new password. - reregister(SecurityAuthority.WRONG_PASSWORD); - - return; - } - else if(exMsg.indexOf("no response from the server") != -1 - || exMsg.indexOf("connection failed") != -1) - { - reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED; - regState = RegistrationState.CONNECTION_FAILED; - } - else if(exMsg.indexOf("tls is required") != -1) - { - regState = RegistrationState.AUTHENTICATION_FAILED; - reason = RegistrationStateChangeEvent.REASON_TLS_REQUIRED; - } - } - - if(regState == RegistrationState.UNREGISTERED - || regState == RegistrationState.CONNECTION_FAILED) - { - // we fired that for some reason we are going offline - // lets clean the connection state for any future connections - disconnectAndCleanConnection(); - } - - fireRegistrationStateChanged( - getRegistrationState(), regState, reason, reasonStr); - } - - /** - * Enable to listen for jabber connection events - */ - private class JabberConnectionListener - implements ConnectionListener - { - /** - * Implements <tt>connectionClosed</tt> from <tt>ConnectionListener</tt> - */ - public void connectionClosed() - { - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - null); - return; - } - } - // fire that a connection failed, the reconnection mechanism - // will look after us and will clean us, other wise we can do - // a dead lock (connection closed is called - // within xmppConneciton and calling disconnect again can lock it) - fireRegistrationStateChanged( - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - null); - } - - /** - * Implements <tt>connectionClosedOnError</tt> from - * <tt>ConnectionListener</tt>. - * - * @param exception contains information on the error. - */ - public void connectionClosedOnError(Exception exception) - { - logger.error("connectionClosedOnError " + - exception.getLocalizedMessage()); - - if(exception instanceof XMPPException) - { - StreamError err = ((XMPPException)exception).getStreamError(); - - if(err != null && err.getCode().equals( - XMPPError.Condition.conflict.toString())) - { - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, - "Connecting multiple times with the same resource"); - return; - } - } - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS, - "Connecting multiple times with the same resource"); - - return; - } - } // Ignore certificate exceptions as we handle them elsewhere - else if(exception instanceof SSLHandshakeException && - exception.getCause() instanceof CertificateException) - { - return; - } - - // if we are in the middle of connecting process - // do not fire events, will do it later when the method - // connectAndLogin finishes its work - synchronized(connectAndLoginLock) - { - if(inConnectAndLogin) - { - eventDuringLogin = new RegistrationStateChangeEvent( - ProtocolProviderServiceJabberImpl.this, - getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - exception.getMessage()); - return; - } - } - - disconnectAndCleanConnection(); - - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.CONNECTION_FAILED, - RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, - exception.getMessage()); - } - - /** - * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt> - * - * @param i delay in seconds for reconnection. - */ - public void reconnectingIn(int i) - { - if (logger.isInfoEnabled()) - logger.info("reconnectingIn " + i); - } - - /** - * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt> - */ - public void reconnectionSuccessful() - { - if (logger.isInfoEnabled()) - logger.info("reconnectionSuccessful"); - } - - /** - * Implements <tt>reconnectionFailed</tt> from - * <tt>ConnectionListener</tt>. - * - * @param exception description of the failure - */ - public void reconnectionFailed(Exception exception) - { - if (logger.isInfoEnabled()) - logger.info("reconnectionFailed ", exception); - } - } - - /** - * Returns the jabber protocol icon. - * @return the jabber protocol icon - */ - public ProtocolIcon getProtocolIcon() - { - return jabberIcon; - } - - /** - * Returns the current instance of <tt>JabberStatusEnum</tt>. - * - * @return the current instance of <tt>JabberStatusEnum</tt>. - */ - JabberStatusEnum getJabberStatusEnum() - { - return jabberStatusEnum; - } - - /** - * Determines if the given list of <tt>ext features</tt> is supported by the - * specified jabber id. - * - * @param jid the jabber id for which to check - * @param extFeatures the list of ext features to check for - * - * @return <tt>true</tt> if the list of ext features is supported; - * otherwise, <tt>false</tt> - */ - public boolean isExtFeatureListSupported(String jid, String... extFeatures) - { - EntityCapsManager capsManager = discoveryManager.getCapsManager(); - EntityCapsManager.Caps caps = capsManager.getCapsByUser(jid); - - String bypassDomain = accountID.getAccountPropertyString( - "TELEPHONY_BYPASS_GTALK_CAPS"); - String domain = StringUtils.parseServer(jid); - boolean domainEquals = domain.equals(bypassDomain); - - if(caps != null && caps.ext != null) - { - String exts[] = caps.ext.split(" "); - boolean found = false; - - for(String extFeature : extFeatures) - { - // in case we have a domain that have to bypass GTalk caps - if(extFeature.equals(CAPS_GTALK_WEB_VOICE) && domainEquals) - { - return true; - } - - found = false; - for(String ext : exts) - { - if(ext.equals(extFeature)) - { - found = true; - break; - } - } - - if(!found) - { - break; - } - } - - return found; - } - - return false; - } - - /** - * Determines if the given list of <tt>features</tt> is supported by the - * specified jabber id. - * - * @param jid the jabber id for which to check - * @param features the list of features to check for - * - * @return <tt>true</tt> if the list of features is supported; otherwise, - * <tt>false</tt> - */ - public boolean isFeatureListSupported(String jid, String... features) - { - boolean isFeatureListSupported = true; - - try - { - if(discoveryManager == null) - return isFeatureListSupported; - - DiscoverInfo featureInfo = - discoveryManager.discoverInfoNonBlocking(jid); - - if(featureInfo == null) - return isFeatureListSupported; - - for (String feature : features) - { - if (!featureInfo.containsFeature(feature)) - { - // If one is not supported we return false and don't check - // the others. - isFeatureListSupported = false; - break; - } - } - } - catch (XMPPException e) - { - if (logger.isDebugEnabled()) - logger.debug("Failed to retrive discovery info.", e); - } - return isFeatureListSupported; - } - - /** - * Determines if the given list of <tt>features</tt> is supported by the - * specified jabber id. - * - * @param jid the jabber id that we'd like to get information about - * @param feature the feature to check for - * - * @return <tt>true</tt> if the list of features is supported, otherwise - * returns <tt>false</tt> - */ - public boolean isFeatureSupported(String jid, String feature) - { - return isFeatureListSupported(jid, feature); - } - - /** - * Returns the full jabber id (jid) corresponding to the given contact. If - * the provider is not connected returns null. - * - * @param contact the contact, for which we're looking for a jid - * @return the jid of the specified contact or null if the provider is not - * yet connected; - */ - public String getFullJid(Contact contact) - { - return getFullJid(contact.getAddress()); - } - - /** - * Returns the full jabber id (jid) corresponding to the given bare jid. If - * the provider is not connected returns null. - * - * @param bareJid the bare contact address (i.e. no resource) whose full - * jid we are looking for. - * @return the jid of the specified contact or null if the provider is not - * yet connected; - */ - public String getFullJid(String bareJid) - { - XMPPConnection connection = getConnection(); - - // when we are not connected there is no full jid - if (connection != null && connection.isConnected()) - { - Roster roster = connection.getRoster(); - - if (roster != null) - return roster.getPresence(bareJid).getFrom(); - } - return null; - } - - /** - * The trust manager which asks the client whether to trust particular - * certificate which is not globally trusted. - */ - private class HostTrustManager - implements X509TrustManager - { - /** - * The default trust manager. - */ - private final X509TrustManager tm; - - /** - * Creates the custom trust manager. - * @param tm the default trust manager. - */ - HostTrustManager(X509TrustManager tm) - { - this.tm = tm; - } - - /** - * Not used. - * - * @return nothing. - */ - public X509Certificate[] getAcceptedIssuers() - { - return new X509Certificate[0]; - } - - /** - * Not used. - * @param chain the cert chain. - * @param authType authentication type like: RSA. - * @throws CertificateException never - * @throws UnsupportedOperationException always - */ - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException, UnsupportedOperationException - { - throw new UnsupportedOperationException(); - } - - /** - * Check whether a certificate is trusted, if not as user whether he - * trust it. - * @param chain the certificate chain. - * @param authType authentication type like: RSA. - * @throws CertificateException not trusted. - */ - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException - { - abortConnecting = true; - try - { - tm.checkServerTrusted(chain, authType); - } - catch(CertificateException e) - { - fireRegistrationStateChanged(getRegistrationState(), - RegistrationState.UNREGISTERED, - RegistrationStateChangeEvent.REASON_USER_REQUEST, - "Not trusted certificate"); - throw e; - } - - if(abortConnecting) - { - // connect hasn't finished we will continue normally - abortConnecting = false; - return; - } - else - { - // in this situation connect method has finished - // and it was disconnected so we wont to connect. - - // register.connect in new thread so we can release the - // current connecting thread, otherwise this blocks - // jabber - new Thread(new Runnable() - { - public void run() - { - reregister(SecurityAuthority.CONNECTION_FAILED); - } - }).start(); - return; - } - } - } - - /** - * Returns the currently valid {@link ScServiceDiscoveryManager}. - * - * @return the currently valid {@link ScServiceDiscoveryManager}. - */ - public ScServiceDiscoveryManager getDiscoveryManager() - { - return discoveryManager; - } - - /** - * Returns our own Jabber ID. - * - * @return our own Jabber ID. - */ - public String getOurJID() - { - String jid = null; - - if (connection != null) - jid = connection.getUser(); - - if (jid == null) - { - // seems like the connection is not yet initialized so lets try to - // construct our jid ourselves. - String accountIDUserID = getAccountID().getUserID(); - String userID = StringUtils.parseName(accountIDUserID); - String serviceName = StringUtils.parseServer(accountIDUserID); - - jid = userID + "@" + serviceName; - } - - return jid; - } - - /** - * Returns the <tt>InetAddress</tt> that is most likely to be to be used - * as a next hop when contacting our XMPP server. This is an utility method - * that is used whenever we have to choose one of our local addresses (e.g. - * when trying to pick a best candidate for raw udp). It is based on the - * assumption that, in absence of any more specific details, chances are - * that we will be accessing remote destinations via the same interface - * that we are using to access our jabber server. - * - * @return the <tt>InetAddress</tt> that is most likely to be to be used - * as a next hop when contacting our server. - * - * @throws IllegalArgumentException if we don't have a valid server. - */ - public InetAddress getNextHop() - throws IllegalArgumentException - { - InetAddress nextHop = null; - String nextHopStr = null; - - if ( proxy != null - && proxy.getProxyType() - != org.jivesoftware.smack.proxy.ProxyInfo.ProxyType.NONE) - { - nextHopStr = proxy.getProxyAddress(); - } - else - { - nextHopStr = getConnection().getHost(); - } - - try - { - nextHop = NetworkUtils.getInetAddress(nextHopStr); - } - catch (UnknownHostException ex) - { - throw new IllegalArgumentException( - "seems we don't have a valid next hop.", ex); - } - - if(logger.isDebugEnabled()) - logger.debug("Returning address " + nextHop + " as next hop."); - - return nextHop; - } - - /** - * Start auto-discovery of JingleNodes tracker/relays. - */ - public void startJingleNodesDiscovery() - { - // Jingle Nodes Service Initialization - final JabberAccountIDImpl accID = (JabberAccountIDImpl)getAccountID(); - final SmackServiceNode service = new SmackServiceNode(connection, - 60000); - // make sure SmackServiceNode will clean up when connection is closed - connection.addConnectionListener(service); - - for(JingleNodeDescriptor desc : accID.getJingleNodes()) - { - TrackerEntry entry = new TrackerEntry( - desc.isRelaySupported() ? TrackerEntry.Type.relay : - TrackerEntry.Type.tracker, - TrackerEntry.Policy._public, - desc.getJID(), - JingleChannelIQ.UDP); - - service.addTrackerEntry(entry); - } - - new Thread(new JingleNodesServiceDiscovery( - service, - connection, - accID, - jingleNodesSyncRoot)) - .start(); - - jingleNodesServiceNode = service; - } - - /** - * Get the Jingle Nodes service. Note that this method will block until - * Jingle Nodes auto discovery (if enabled) finished. - * - * @return Jingle Nodes service - */ - public SmackServiceNode getJingleNodesServiceNode() - { - synchronized(jingleNodesSyncRoot) - { - return jingleNodesServiceNode; - } - } - - /** - * Logs a specific message and associated <tt>Throwable</tt> cause as an - * error using the current <tt>Logger</tt> and then throws a new - * <tt>OperationFailedException</tt> with the message, a specific error code - * and the cause. - * - * @param message the message to be logged and then wrapped in a new - * <tt>OperationFailedException</tt> - * @param errorCode the error code to be assigned to the new - * <tt>OperationFailedException</tt> - * @param cause the <tt>Throwable</tt> that has caused the necessity to log - * an error and have a new <tt>OperationFailedException</tt> thrown - * @param logger the logger that we'd like to log the error <tt>message</tt> - * and <tt>cause</tt>. - * - * @throws OperationFailedException the exception that we wanted this method - * to throw. - */ - public static void throwOperationFailedException( String message, - int errorCode, - Throwable cause, - Logger logger) - throws OperationFailedException - { - logger.error(message, cause); - - if(cause == null) - throw new OperationFailedException(message, errorCode); - else - throw new OperationFailedException(message, errorCode, cause); - } - - /** - * Used when we need to re-register or someone needs to obtain credentials. - * @return the SecurityAuthority. - */ - public SecurityAuthority getAuthority() - { - return authority; - } - - /** - * Returns true if gtalktesting is enabled, false otherwise. - * - * @return true if gtalktesting is enabled, false otherwise. - */ - public boolean isGTalkTesting() - { - return - Boolean.getBoolean("gtalktesting") - || JabberActivator.getConfigurationService().getBoolean( - "net.java.sip.communicator.impl.protocol.jabber" - + ".gtalktesting", - false) - || accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.IS_USE_GOOGLE_ICE, - true); - } - - UserCredentials getUserCredentials() - { - return userCredentials; - } - - /** - * Returns true if our account is a Gmail or a Google Apps ones. - * - * @return true if our account is a Gmail or a Google Apps ones. - */ - public boolean isGmailOrGoogleAppsAccount() - { - String domain = StringUtils.parseServer( - getAccountID().getUserID()); - return isGmailOrGoogleAppsAccount(domain); - } - - /** - * Returns true if our account is a Gmail or a Google Apps ones. - * - * @param domain domain to check - * @return true if our account is a Gmail or a Google Apps ones. - */ - public static boolean isGmailOrGoogleAppsAccount(String domain) - { - SRVRecord srvRecords[] = null; - - try - { - srvRecords = NetworkUtils.getSRVRecords("xmpp-client", "tcp", - domain); - } - catch (ParseException e) - { - logger.info("Failed to get SRV records for XMPP domain"); - return false; - } - catch (DnssecException e) - { - logger.error("DNSSEC failure while checking for google domains", e); - return false; - } - - if(srvRecords == null) - { - return false; - } - - for(SRVRecord srv : srvRecords) - { - if(srv.getTarget().endsWith("google.com") || - srv.getTarget().endsWith("google.com.")) - { - return true; - } - } - - return false; - } - - /** - * Sets the traffic class for the XMPP signalling socket. - */ - private void setTrafficClass() - { - Socket s = connection.getSocket(); - - if(s != null) - { - ConfigurationService configService = - JabberActivator.getConfigurationService(); - String dscp = configService.getString(XMPP_DSCP_PROPERTY); - - if(dscp != null) - { - try - { - int dscpInt = Integer.parseInt(dscp) << 2; - - if(dscpInt > 0) - s.setTrafficClass(dscpInt); - } - catch (Exception e) - { - logger.info("Failed to set trafficClass", e); - } - } - } - } - - /** - * Gets the entity ID of the first Jitsi VideoBridge associated with - * {@link #connection} i.e. provided by the <tt>serviceName</tt> of - * <tt>connection</tt>. - * - * @return the entity ID of the first Jitsi VideoBridge associated with - * <tt>connection</tt> - */ - public String getJitsiVideoBridge() - { - XMPPConnection connection = getConnection(); - - if (connection != null) - { - ScServiceDiscoveryManager discoveryManager = getDiscoveryManager(); - String serviceName = connection.getServiceName(); - DiscoverItems discoverItems = null; - - try - { - discoverItems = discoveryManager.discoverItems(serviceName); - } - catch (XMPPException xmppe) - { - if (logger.isDebugEnabled()) - { - logger.debug( - "Failed to discover the items associated with" - + " Jabber entity: " + serviceName, - xmppe); - } - } - if (discoverItems != null) - { - Iterator<DiscoverItems.Item> discoverItemIter - = discoverItems.getItems(); - - while (discoverItemIter.hasNext()) - { - DiscoverItems.Item discoverItem = discoverItemIter.next(); - String entityID = discoverItem.getEntityID(); - DiscoverInfo discoverInfo = null; - - try - { - discoverInfo = discoveryManager.discoverInfo(entityID); - } - catch (XMPPException xmppe) - { - logger.warn( - "Failed to discover information about Jabber" - + " entity: " + entityID, - xmppe); - } - if ((discoverInfo != null) - && discoverInfo.containsFeature( - ColibriConferenceIQ.NAMESPACE)) - { - return entityID; - } - } - } - } - - return null; - } - - /** - * Load jabber service class, their static context will register - * what is needed. Used in android as when using the other jars - * these services are loaded from the jar manifest. - */ - private static void loadJabberServiceClasses() - { - if(!OSUtils.IS_ANDROID) - return; - - try - { - // pre-configure smack in android - // just to load class to init their static blocks - SmackConfiguration.getVersion(); - Class.forName(ServiceDiscoveryManager.class.getName()); - - Class.forName(DelayInformation.class.getName()); - Class.forName(org.jivesoftware.smackx - .provider.DelayInformationProvider.class.getName()); - Class.forName(org.jivesoftware.smackx - .bytestreams.socks5.Socks5BytestreamManager.class.getName()); - Class.forName(XHTMLManager.class.getName()); - Class.forName(org.jivesoftware.smackx - .bytestreams.ibb.InBandBytestreamManager.class.getName()); - - } - catch(ClassNotFoundException e) - { - logger.error("Error loading classes in smack", e); - } - } -} +/*
+ * 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.impl.protocol.jabber;
+
+import java.math.*;
+import java.net.*;
+import java.security.*;
+import java.security.cert.*;
+import java.text.*;
+import java.util.*;
+
+import javax.net.ssl.*;
+
+import net.java.sip.communicator.impl.protocol.jabber.debugger.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.caps.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*;
+import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*;
+import net.java.sip.communicator.service.certificate.*;
+import net.java.sip.communicator.service.dns.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.service.protocol.jabberconstants.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.Logger;
+
+import org.jitsi.service.configuration.*;
+import org.jitsi.service.neomedia.*;
+import org.jitsi.util.*;
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.*;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.*;
+import org.jivesoftware.smackx.packet.*;
+import org.osgi.framework.*;
+import org.xmpp.jnodes.smack.*;
+
+/**
+ * An implementation of the protocol provider service over the Jabber protocol
+ *
+ * @author Damian Minkov
+ * @author Symphorien Wanko
+ * @author Lyubomir Marinov
+ * @author Yana Stamcheva
+ * @author Emil Ivov
+ */
+public class ProtocolProviderServiceJabberImpl
+ extends AbstractProtocolProviderService
+{
+ /**
+ * Logger of this class
+ */
+ private static final Logger logger =
+ Logger.getLogger(ProtocolProviderServiceJabberImpl.class);
+
+ /**
+ * Jingle's Discovery Info common URN.
+ */
+ public static final String URN_XMPP_JINGLE = JingleIQ.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for RTP support.
+ */
+ public static final String URN_XMPP_JINGLE_RTP
+ = RtpDescriptionPacketExtension.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for RTP support with audio.
+ */
+ public static final String URN_XMPP_JINGLE_RTP_AUDIO
+ = "urn:xmpp:jingle:apps:rtp:audio";
+
+ /**
+ * Jingle's Discovery Info URN for RTP support with video.
+ */
+ public static final String URN_XMPP_JINGLE_RTP_VIDEO
+ = "urn:xmpp:jingle:apps:rtp:video";
+
+ /**
+ * Jingle's Discovery Info URN for ZRTP support with RTP.
+ */
+ public static final String URN_XMPP_JINGLE_RTP_ZRTP
+ = ZrtpHashPacketExtension.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for ICE_UDP transport support.
+ */
+ public static final String URN_XMPP_JINGLE_RAW_UDP_0
+ = RawUdpTransportPacketExtension.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for ICE_UDP transport support.
+ */
+ public static final String URN_XMPP_JINGLE_ICE_UDP_1
+ = IceUdpTransportPacketExtension.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for Jingle Nodes support.
+ */
+ public static final String URN_XMPP_JINGLE_NODES
+ = "http://jabber.org/protocol/jinglenodes";
+
+ /**
+ * Jingle's Discovery Info URN for "XEP-0251: Jingle Session Transfer"
+ * support.
+ */
+ public static final String URN_XMPP_JINGLE_TRANSFER_0
+ = TransferPacketExtension.NAMESPACE;
+
+ /**
+ * Jingle's Discovery Info URN for "XEP-298 :Delivering Conference
+ * Information to Jingle Participants (Coin)" support.
+ */
+ public static final String URN_XMPP_JINGLE_COIN = "urn:xmpp:coin";
+
+ /**
+ * Jingle's Discovery Info URN for "XEP-0320: Use of DTLS-SRTP in
+ * Jingle Sessions".
+ */
+ public static final String URN_XMPP_JINGLE_DTLS_SRTP
+ = "urn:xmpp:jingle:apps:dtls:0";
+
+ /**
+ * Discovery Info URN for classic RFC3264-style Offer/Answer negotiation
+ * with no support for Trickle ICE and low tolerance to transport/payload
+ * separation. Defined in XEP-0176
+ */
+ public static final String URN_IETF_RFC_3264 = "urn:ietf:rfc:3264";
+
+ /**
+ * Jingle's Discovery Info URN for "XEP-0294: Jingle RTP Header Extensions
+ * Negotiation" support.
+ */
+ public static final String URN_XMPP_JINGLE_RTP_HDREXT =
+ "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
+
+ /**
+ * Capabilities name for audio call in Google Talk web version.
+ */
+ public static final String CAPS_GTALK_WEB_VOICE = "voice-v1";
+
+ /**
+ * Capabilities name for video call (receive side) in Google Talk web
+ * version.
+ */
+ public static final String CAPS_GTALK_WEB_VIDEO = "video-v1";
+
+ /**
+ * Capabilities name for video call (sender side) in Google Talk web
+ * version.
+ */
+ public static final String CAPS_GTALK_WEB_CAMERA = "camera-v1";
+
+ /**
+ * Google P2P transport URN.
+ */
+ public static final String URN_GOOGLE_TRANSPORT_P2P
+ = "http://www.google.com/transport/p2p";
+
+ /**
+ * URN for Google voice.
+ */
+ public static final String URN_GOOGLE_VOICE =
+ "http://www.google.com/xmpp/protocol/voice/v1";
+
+ /**
+ * URN for Google camera.
+ */
+ public static final String URN_GOOGLE_CAMERA =
+ "http://www.google.com/xmpp/protocol/camera/v1";
+
+ /**
+ * URN for Google video.
+ */
+ public static final String URN_GOOGLE_VIDEO =
+ "http://www.google.com/xmpp/protocol/video/v1";
+
+ /**
+ * URN for XEP-0077 inband registration
+ */
+ public static final String URN_REGISTER = "jabber:iq:register";
+
+ /**
+ * The name of the property under which the user may specify if the desktop
+ * streaming or sharing should be disabled.
+ */
+ private static final String IS_DESKTOP_STREAMING_DISABLED
+ = "net.java.sip.communicator.impl.protocol.jabber." +
+ "DESKTOP_STREAMING_DISABLED";
+
+ /**
+ * The name of the property under which the user may specify if audio/video
+ * calls should be disabled.
+ */
+ private static final String IS_CALLING_DISABLED
+ = "net.java.sip.communicator.impl.protocol.jabber.CALLING_DISABLED";
+
+ /**
+ * Smack packet reply timeout.
+ */
+ public static final int SMACK_PACKET_REPLY_TIMEOUT = 45000;
+
+ /**
+ * Property for vcard reply timeout. Time to wait before
+ * we think vcard retrieving has timeouted, default value
+ * of smack is 5000 (5 sec.).
+ */
+ public static final String VCARD_REPLY_TIMEOUT_PROPERTY =
+ "net.java.sip.communicator.impl.protocol.jabber.VCARD_REPLY_TIMEOUT";
+
+ /**
+ * XMPP signaling DSCP configuration property name.
+ */
+ private static final String XMPP_DSCP_PROPERTY =
+ "net.java.sip.communicator.impl.protocol.XMPP_DSCP";
+
+ /**
+ * Google voice domain name.
+ */
+ public static final String GOOGLE_VOICE_DOMAIN = "voice.google.com";
+
+ /**
+ * Used to connect to a XMPP server.
+ */
+ private XMPPConnection connection;
+
+ /**
+ * Indicates whether or not the provider is initialized and ready for use.
+ */
+ private boolean isInitialized = false;
+
+ /**
+ * We use this to lock access to initialization.
+ */
+ private final Object initializationLock = new Object();
+
+ /**
+ * The identifier of the account that this provider represents.
+ */
+ private AccountID accountID = null;
+
+ /**
+ * Used when we need to re-register
+ */
+ private SecurityAuthority authority = null;
+
+ /**
+ * The resource we will use when connecting during this run.
+ */
+ private String resource = null;
+
+ /**
+ * The icon corresponding to the jabber protocol.
+ */
+ private ProtocolIconJabberImpl jabberIcon;
+
+ /**
+ * A set of features supported by our Jabber implementation.
+ * In general, we add new feature(s) when we add new operation sets.
+ * (see xep-0030 : http://www.xmpp.org/extensions/xep-0030.html#info).
+ * Example : to tell the world that we support jingle, we simply have
+ * to do :
+ * supportedFeatures.add("http://www.xmpp.org/extensions/xep-0166.html#ns");
+ * Beware there is no canonical mapping between op set and jabber features
+ * (op set is a SC "concept"). This means that one op set in SC can
+ * correspond to many jabber features. It is also possible that there is no
+ * jabber feature corresponding to a SC op set or again,
+ * we can currently support some features wich do not have a specific
+ * op set in SC (the mandatory feature :
+ * http://jabber.org/protocol/disco#info is one example).
+ * We can find features corresponding to op set in the xep(s) related
+ * to implemented functionality.
+ */
+ private final List<String> supportedFeatures = new ArrayList<String>();
+
+ /**
+ * The <tt>ServiceDiscoveryManager</tt> is responsible for advertising
+ * <tt>supportedFeatures</tt> when asked by a remote client. It can also
+ * be used to query remote clients for supported features.
+ */
+ private ScServiceDiscoveryManager discoveryManager = null;
+
+ /**
+ * The <tt>OperationSetContactCapabilities</tt> of this
+ * <tt>ProtocolProviderService</tt> which is the service-public counterpart
+ * of {@link #discoveryManager}.
+ */
+ private OperationSetContactCapabilitiesJabberImpl opsetContactCapabilities;
+
+ /**
+ * The statuses.
+ */
+ private JabberStatusEnum jabberStatusEnum;
+
+ /**
+ * The service we use to interact with user.
+ */
+ private CertificateService guiVerification;
+
+ /**
+ * Used with tls connecting when certificates are not trusted
+ * and we ask the user to confirm connection. When some timeout expires
+ * connect method returns, and we use abortConnecting to abort further
+ * execution cause after user chooses we make further processing from there.
+ */
+ private boolean abortConnecting = false;
+
+ /**
+ * Flag indicating are we currently executing connectAndLogin method.
+ */
+ private boolean inConnectAndLogin = false;
+
+ /**
+ * Object used to synchronize the flag inConnectAndLogin.
+ */
+ private final Object connectAndLoginLock = new Object();
+
+ /**
+ * If an event occurs during login we fire it at the end of the login
+ * process (at the end of connectAndLogin method).
+ */
+ private RegistrationStateChangeEvent eventDuringLogin;
+
+ /**
+ * Listens for connection closes or errors.
+ */
+ private JabberConnectionListener connectionListener;
+
+ /**
+ * The details of the proxy we are using to connect to the server (if any)
+ */
+ private org.jivesoftware.smack.proxy.ProxyInfo proxy;
+
+ /**
+ * Our provider manager instances.
+ */
+ private static ProviderManager providerManager = null;
+
+ /**
+ * Lock for creating provider.
+ */
+ private static Object providerCreationLock = new Object();
+
+ /**
+ * State for connect and login state.
+ */
+ enum ConnectState
+ {
+ /**
+ * Abort any further connecting.
+ */
+ ABORT_CONNECTING,
+ /**
+ * Continue trying with next address.
+ */
+ CONTINUE_TRYING,
+ /**
+ * Stop trying we succeeded or just have a final state for
+ * the whole connecting procedure.
+ */
+ STOP_TRYING
+ }
+
+ /**
+ * The debugger who logs packets.
+ */
+ private SmackPacketDebugger debugger = null;
+
+ /**
+ * Jingle Nodes service.
+ */
+ private SmackServiceNode jingleNodesServiceNode = null;
+
+ /**
+ * Synchronization object to monitore jingle nodes auto discovery.
+ */
+ private final Object jingleNodesSyncRoot = new Object();
+
+ /**
+ * Stores user credentials for local use if user hasn't stored
+ * its password.
+ */
+ private UserCredentials userCredentials = null;
+
+ /**
+ * The currently running keepAliveManager if enabled.
+ */
+ private KeepAliveManager keepAliveManager = null;
+
+ /**
+ * The version manager.
+ */
+ private VersionManager versionManager = null;
+
+ // load xmpp manager classes
+ static
+ {
+ if(OSUtils.IS_ANDROID)
+ loadJabberServiceClasses();
+ }
+
+ /**
+ * Returns the state of the registration of this protocol provider
+ * @return the <tt>RegistrationState</tt> that this provider is
+ * currently in or null in case it is in a unknown state.
+ */
+ public RegistrationState getRegistrationState()
+ {
+ if(connection == null)
+ return RegistrationState.UNREGISTERED;
+ else if(connection.isConnected() && connection.isAuthenticated())
+ return RegistrationState.REGISTERED;
+ else
+ return RegistrationState.UNREGISTERED;
+ }
+
+ /**
+ * Return the certificate verification service impl.
+ * @return the CertificateVerification service.
+ */
+ private CertificateService getCertificateVerificationService()
+ {
+ if(guiVerification == null)
+ {
+ ServiceReference guiVerifyReference
+ = JabberActivator.getBundleContext().getServiceReference(
+ CertificateService.class.getName());
+ if(guiVerifyReference != null)
+ guiVerification = (CertificateService)
+ JabberActivator.getBundleContext().getService(
+ guiVerifyReference);
+ }
+
+ return guiVerification;
+ }
+
+ /**
+ * Starts the registration process. Connection details such as
+ * registration server, user name/number are provided through the
+ * configuration service through implementation specific properties.
+ *
+ * @param authority the security authority that will be used for resolving
+ * any security challenges that may be returned during the
+ * registration or at any moment while we're registered.
+ * @throws OperationFailedException with the corresponding code it the
+ * registration fails for some reason (e.g. a networking error or an
+ * implementation problem).
+ */
+ public void register(final SecurityAuthority authority)
+ throws OperationFailedException
+ {
+ if(authority == null)
+ throw new IllegalArgumentException(
+ "The register method needs a valid non-null authority impl "
+ + " in order to be able and retrieve passwords.");
+
+ this.authority = authority;
+
+ try
+ {
+ // reset states
+ abortConnecting = false;
+
+ // indicate we started connectAndLogin process
+ synchronized(connectAndLoginLock)
+ {
+ inConnectAndLogin = true;
+ }
+
+ initializeConnectAndLogin(authority,
+ SecurityAuthority.AUTHENTICATION_REQUIRED);
+ }
+ catch (XMPPException ex)
+ {
+ logger.error("Error registering", ex);
+
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(ex);
+ }
+ finally
+ {
+ synchronized(connectAndLoginLock)
+ {
+ // Checks if an error has occurred during login, if so we fire
+ // it here in order to avoid a deadlock which occurs in
+ // reconnect plugin. The deadlock is cause we fired an event
+ // during login process and have locked initializationLock and
+ // we cannot unregister from reconnect, cause unregister method
+ // also needs this lock.
+ if(eventDuringLogin != null)
+ {
+ if(eventDuringLogin.getNewState().equals(
+ RegistrationState.CONNECTION_FAILED) ||
+ eventDuringLogin.getNewState().equals(
+ RegistrationState.UNREGISTERED))
+ disconnectAndCleanConnection();
+
+ fireRegistrationStateChanged(
+ eventDuringLogin.getOldState(),
+ eventDuringLogin.getNewState(),
+ eventDuringLogin.getReasonCode(),
+ eventDuringLogin.getReason());
+
+ eventDuringLogin = null;
+ inConnectAndLogin = false;
+ return;
+ }
+
+ inConnectAndLogin = false;
+ }
+ }
+ }
+
+ /**
+ * Connects and logins again to the server.
+ *
+ * @param authReasonCode indicates the reason of the re-authentication.
+ */
+ void reregister(int authReasonCode)
+ {
+ try
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Trying to reregister us!");
+
+ // sets this if any is trying to use us through registration
+ // to know we are not registered
+ this.unregister(false);
+
+ // reset states
+ this.abortConnecting = false;
+
+ // indicate we started connectAndLogin process
+ synchronized(connectAndLoginLock)
+ {
+ inConnectAndLogin = true;
+ }
+
+ initializeConnectAndLogin(authority, authReasonCode);
+ }
+ catch(OperationFailedException ex)
+ {
+ logger.error("Error ReRegistering", ex);
+
+ eventDuringLogin = null;
+
+ disconnectAndCleanConnection();
+
+ fireRegistrationStateChanged(getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null);
+ }
+ catch (XMPPException ex)
+ {
+ logger.error("Error ReRegistering", ex);
+
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(ex);
+ }
+ finally
+ {
+ synchronized(connectAndLoginLock)
+ {
+ // Checks if an error has occurred during login, if so we fire
+ // it here in order to avoid a deadlock which occurs in
+ // reconnect plugin. The deadlock is cause we fired an event
+ // during login process and have locked initializationLock and
+ // we cannot unregister from reconnect, cause unregister method
+ // also needs this lock.
+ if(eventDuringLogin != null)
+ {
+ if(eventDuringLogin.getNewState().equals(
+ RegistrationState.CONNECTION_FAILED) ||
+ eventDuringLogin.getNewState().equals(
+ RegistrationState.UNREGISTERED))
+ disconnectAndCleanConnection();
+
+ fireRegistrationStateChanged(
+ eventDuringLogin.getOldState(),
+ eventDuringLogin.getNewState(),
+ eventDuringLogin.getReasonCode(),
+ eventDuringLogin.getReason());
+
+ eventDuringLogin = null;
+ inConnectAndLogin = false;
+ return;
+ }
+
+ inConnectAndLogin = false;
+ }
+ }
+ }
+
+ /**
+ * Indicates if the XMPP transport channel is using a TLS secured socket.
+ *
+ * @return True when TLS is used, false otherwise.
+ */
+ public boolean isSignalingTransportSecure()
+ {
+ return connection != null && connection.isUsingTLS();
+ }
+
+ /**
+ * Returns the "transport" protocol of this instance used to carry the
+ * control channel for the current protocol service.
+ *
+ * @return The "transport" protocol of this instance: TCP, TLS or UNKNOWN.
+ */
+ public TransportProtocol getTransportProtocol()
+ {
+ // Without a connection, there is no transport available.
+ if(connection != null && connection.isConnected())
+ {
+ // Transport using a secure connection.
+ if(connection.isUsingTLS())
+ {
+ return TransportProtocol.TLS;
+ }
+ // Transport using a unsecure connection.
+ return TransportProtocol.TCP;
+ }
+ return TransportProtocol.UNKNOWN;
+ }
+
+ /**
+ * Connects and logins to the server
+ * @param authority SecurityAuthority
+ * @param reasonCode the authentication reason code. Indicates the reason of
+ * this authentication.
+ * @throws XMPPException if we cannot connect to the server - network problem
+ * @throws OperationFailedException if login parameters
+ * as server port are not correct
+ */
+ private void initializeConnectAndLogin(SecurityAuthority authority,
+ int reasonCode)
+ throws XMPPException, OperationFailedException
+ {
+ synchronized(initializationLock)
+ {
+ JabberLoginStrategy loginStrategy = createLoginStrategy();
+ userCredentials = loginStrategy.prepareLogin(authority, reasonCode);
+ if(!loginStrategy.loginPreparationSuccessful())
+ return;
+
+ String serviceName
+ = StringUtils.parseServer(getAccountID().getUserID());
+
+ loadResource();
+ loadProxy();
+ Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);
+
+ ConnectState state;
+ //[0] = hadDnsSecException
+ boolean[] hadDnsSecException = new boolean[]{false};
+
+ // try connecting with auto-detection if enabled
+ boolean isServerOverriden =
+ getAccountID().getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_SERVER_OVERRIDDEN, false);
+
+ if(!isServerOverriden)
+ {
+ state = connectUsingSRVRecords(serviceName,
+ serviceName, hadDnsSecException, loginStrategy);
+ if(hadDnsSecException[0])
+ {
+ setDnssecLoginFailure();
+ return;
+ }
+ if(state == ConnectState.ABORT_CONNECTING
+ || state == ConnectState.STOP_TRYING)
+ return;
+ }
+
+ // check for custom xmpp domain which we will check for
+ // SRV records for server addresses
+ String customXMPPDomain = getAccountID()
+ .getAccountPropertyString("CUSTOM_XMPP_DOMAIN");
+
+ if(customXMPPDomain != null && !hadDnsSecException[0])
+ {
+ logger.info("Connect using custom xmpp domain: " +
+ customXMPPDomain);
+
+ state = connectUsingSRVRecords(
+ customXMPPDomain, serviceName,
+ hadDnsSecException, loginStrategy);
+
+ logger.info("state for connectUsingSRVRecords: " + state);
+
+ if(hadDnsSecException[0])
+ {
+ setDnssecLoginFailure();
+ return;
+ }
+
+ if(state == ConnectState.ABORT_CONNECTING
+ || state == ConnectState.STOP_TRYING)
+ return;
+ }
+
+ // connect with specified server name
+ String serverAddressUserSetting
+ = getAccountID().getAccountPropertyString(
+ ProtocolProviderFactory.SERVER_ADDRESS);
+
+ int serverPort = getAccountID().getAccountPropertyInt(
+ ProtocolProviderFactory.SERVER_PORT, 5222);
+
+ InetSocketAddress[] addrs = null;
+ try
+ {
+ addrs = NetworkUtils.getAandAAAARecords(
+ serverAddressUserSetting,
+ serverPort
+ );
+ }
+ catch (ParseException e)
+ {
+ logger.error("Domain not resolved", e);
+ }
+ catch (DnssecException e)
+ {
+ logger.error("DNSSEC failure for overridden server", e);
+ setDnssecLoginFailure();
+ return;
+ }
+
+ if (addrs == null || addrs.length == 0)
+ {
+ logger.error("No server addresses found");
+
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(
+ getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND,
+ "No server addresses found");
+ }
+ else
+ {
+ for (InetSocketAddress isa : addrs)
+ {
+ try
+ {
+ state = connectAndLogin(isa, serviceName,
+ loginStrategy);
+ if(state == ConnectState.ABORT_CONNECTING
+ || state == ConnectState.STOP_TRYING)
+ return;
+ }
+ catch(XMPPException ex)
+ {
+ disconnectAndCleanConnection();
+ if(isAuthenticationFailed(ex))
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates the JabberLoginStrategy to use for the current account.
+ */
+ private JabberLoginStrategy createLoginStrategy()
+ {
+ String clientCertId = getAccountID().getAccountPropertyString(
+ ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE);
+ if(clientCertId != null)
+ {
+ return new LoginByClientCertificateStrategy(getAccountID());
+ }
+ else
+ {
+ return new LoginByPasswordStrategy(this, getAccountID());
+ }
+ }
+
+ private void setDnssecLoginFailure()
+ {
+ eventDuringLogin = new RegistrationStateChangeEvent(
+ this,
+ getRegistrationState(),
+ RegistrationState.UNREGISTERED,
+ RegistrationStateChangeEvent.REASON_USER_REQUEST,
+ "No usable host found due to DNSSEC failures");
+ }
+
+ /**
+ * Connects using the domain specified and its SRV records.
+ * @param domain the domain to use
+ * @param serviceName the domain name of the user's login
+ * @param dnssecState state of possible received DNSSEC exceptions
+ * @param loginStrategy the login strategy to use
+ * @return whether to continue trying or stop.
+ */
+ private ConnectState connectUsingSRVRecords(
+ String domain,
+ String serviceName,
+ boolean[] dnssecState,
+ JabberLoginStrategy loginStrategy)
+ throws XMPPException
+ {
+ // check to see is there SRV records for this server domain
+ SRVRecord srvRecords[] = null;
+ try
+ {
+ srvRecords = NetworkUtils
+ .getSRVRecords("xmpp-client", "tcp", domain);
+ }
+ catch (ParseException e)
+ {
+ logger.error("SRV record not resolved", e);
+ }
+ catch (DnssecException e)
+ {
+ logger.error("DNSSEC failure for SRV lookup", e);
+ dnssecState[0] = true;
+ }
+
+ if(srvRecords != null)
+ {
+ for(SRVRecord srv : srvRecords)
+ {
+ InetSocketAddress[] addrs = null;
+ try
+ {
+ addrs =
+ NetworkUtils.getAandAAAARecords(
+ srv.getTarget(),
+ srv.getPort()
+ );
+ }
+ catch (ParseException e)
+ {
+ logger.error("Invalid SRV record target", e);
+ }
+ catch (DnssecException e)
+ {
+ logger.error("DNSSEC failure for A/AAAA lookup of SRV", e);
+ dnssecState[0] = true;
+ }
+
+ if (addrs == null || addrs.length == 0)
+ {
+ logger.error("No A/AAAA addresses found for " +
+ srv.getTarget());
+ continue;
+ }
+
+ for (InetSocketAddress isa : addrs)
+ {
+ try
+ {
+ // if failover mechanism is enabled, use it,
+ // default is not enabled.
+ if(JabberActivator.getConfigurationService()
+ .getBoolean(FailoverConnectionMonitor
+ .REVERSE_FAILOVER_ENABLED_PROP,
+ false
+ ))
+ {
+ FailoverConnectionMonitor.getInstance(this)
+ .setCurrent(serviceName,
+ srv.getTarget());
+ }
+
+ ConnectState state = connectAndLogin(
+ isa, serviceName, loginStrategy);
+ return state;
+ }
+ catch(XMPPException ex)
+ {
+ logger.error("Error connecting to " + isa
+ + " for domain:" + domain
+ + " serviceName:" + serviceName, ex);
+
+ disconnectAndCleanConnection();
+
+ if(isAuthenticationFailed(ex))
+ throw ex;
+ }
+ }
+ }
+ }
+ else
+ logger.error("No SRV addresses found for _xmpp-client._tcp."
+ + domain);
+
+ return ConnectState.CONTINUE_TRYING;
+ }
+
+ /**
+ * Tries to login to the XMPP server with the supplied user ID. If the
+ * protocol is Google Talk, the user ID including the service name is used.
+ * For other protocols, if the login with the user ID without the service
+ * name fails, a second attempt including the service name is made.
+ *
+ * @param currentAddress the IP address to connect to
+ * @param serviceName the domain name of the user's login
+ * @param loginStrategy the login strategy to use
+ * @throws XMPPException when a failure occurs
+ */
+ private ConnectState connectAndLogin(InetSocketAddress currentAddress,
+ String serviceName,
+ JabberLoginStrategy loginStrategy)
+ throws XMPPException
+ {
+ String userID = null;
+ boolean qualifiedUserID;
+
+ /* with a google account (either gmail or google apps
+ * related ones), the userID MUST be the full e-mail address
+ * not just the ID
+ */
+ if(getAccountID().getProtocolDisplayName().equals("Google Talk"))
+ {
+ userID = getAccountID().getUserID();
+ qualifiedUserID = true;
+ }
+ else
+ {
+ userID = StringUtils.parseName(getAccountID().getUserID());
+ qualifiedUserID = false;
+ }
+
+ try
+ {
+ return connectAndLogin(
+ currentAddress, serviceName,
+ userID, resource, loginStrategy);
+ }
+ catch(XMPPException ex)
+ {
+ // server disconnect us after such an error, do cleanup
+ disconnectAndCleanConnection();
+
+ //no need to check with a different username if the
+ //socket could not be opened
+ if (ex.getWrappedThrowable() instanceof ConnectException
+ || ex.getWrappedThrowable() instanceof NoRouteToHostException)
+ {
+ //as we got an exception not handled in connectAndLogin
+ //no state was set, so fire it here so we can continue
+ //with the re-register process
+ //2013-08-07 do not fire event, if we have several
+ // addresses and we fire event will activate reconnect
+ // but we will continue connecting with other addresses
+ // and can register with address, then unregister and try again
+ // that is from reconnect plugin.
+ // Storing event for fire after all have failed and we have
+ // tried every address.
+ eventDuringLogin = new RegistrationStateChangeEvent(
+ ProtocolProviderServiceJabberImpl.this,
+ getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND,
+ null);
+
+ throw ex;
+ }
+
+ // don't attempt to append the service name if it's already there
+ if (!qualifiedUserID)
+ {
+ try
+ {
+ // logging in might need the service name
+ return connectAndLogin(
+ currentAddress, serviceName,
+ userID + "@" + serviceName,
+ resource,
+ loginStrategy);
+ }
+ catch(XMPPException ex2)
+ {
+ disconnectAndCleanConnection();
+ throw ex; //throw the original exception
+ }
+ }
+ else
+ throw ex;
+ }
+ }
+
+ /**
+ * Initializes the Jabber Resource identifier.
+ */
+ private void loadResource()
+ {
+ if(resource == null)
+ {
+ String defaultResource = "jitsi";
+ String autoGenenerateResource =
+ getAccountID().getAccountPropertyString(
+ ProtocolProviderFactory.AUTO_GENERATE_RESOURCE);
+ if(autoGenenerateResource == null ||
+ Boolean.parseBoolean(autoGenenerateResource))
+ {
+ SecureRandom random = new SecureRandom();
+
+ resource = defaultResource + "-" +
+ new BigInteger(32, random).toString(32);
+ }
+ else
+ {
+ resource = getAccountID().getAccountPropertyString(
+ ProtocolProviderFactory.RESOURCE);
+
+ if(resource == null || resource.length() == 0)
+ resource = defaultResource;
+ }
+ }
+ }
+
+ /**
+ * Sets the global proxy information based on the configuration
+ *
+ * @throws OperationFailedException
+ */
+ private void loadProxy() throws OperationFailedException
+ {
+ String globalProxyType =
+ JabberActivator.getConfigurationService()
+ .getString(ProxyInfo.CONNECTION_PROXY_TYPE_PROPERTY_NAME);
+ if(globalProxyType == null ||
+ globalProxyType.equals(ProxyInfo.ProxyType.NONE.name()))
+ {
+ proxy = org.jivesoftware.smack.proxy.ProxyInfo.forNoProxy();
+ }
+ else
+ {
+ String globalProxyAddress =
+ JabberActivator.getConfigurationService().getString(
+ ProxyInfo.CONNECTION_PROXY_ADDRESS_PROPERTY_NAME);
+ String globalProxyPortStr =
+ JabberActivator.getConfigurationService().getString(
+ ProxyInfo.CONNECTION_PROXY_PORT_PROPERTY_NAME);
+ int globalProxyPort;
+ try
+ {
+ globalProxyPort = Integer.parseInt(
+ globalProxyPortStr);
+ }
+ catch(NumberFormatException ex)
+ {
+ throw new OperationFailedException("Wrong proxy port, "
+ + globalProxyPortStr
+ + " does not represent an integer",
+ OperationFailedException.INVALID_ACCOUNT_PROPERTIES,
+ ex);
+ }
+ String globalProxyUsername =
+ JabberActivator.getConfigurationService().getString(
+ ProxyInfo.CONNECTION_PROXY_USERNAME_PROPERTY_NAME);
+ String globalProxyPassword =
+ JabberActivator.getConfigurationService().getString(
+ ProxyInfo.CONNECTION_PROXY_PASSWORD_PROPERTY_NAME);
+ if(globalProxyAddress == null ||
+ globalProxyAddress.length() <= 0)
+ {
+ throw new OperationFailedException(
+ "Missing Proxy Address",
+ OperationFailedException.INVALID_ACCOUNT_PROPERTIES);
+ }
+ try
+ {
+ proxy = new org.jivesoftware.smack.proxy.ProxyInfo(
+ Enum.valueOf(org.jivesoftware.smack.proxy.ProxyInfo.
+ ProxyType.class, globalProxyType),
+ globalProxyAddress, globalProxyPort,
+ globalProxyUsername, globalProxyPassword);
+ }
+ catch(IllegalArgumentException e)
+ {
+ logger.error("Invalid value for smack proxy enum", e);
+ proxy = null;
+ }
+ }
+ }
+
+ /**
+ * Connects xmpp connection and login. Returning the state whether is it
+ * final - Abort due to certificate cancel or keep trying cause only current
+ * address has failed or stop trying cause we succeeded.
+ * @param address the address to connect to
+ * @param serviceName the service name to use
+ * @param userName the username to use
+ * @param resource and the resource.
+ * @param loginStrategy the login strategy to use
+ * @return return the state how to continue the connect process.
+ * @throws XMPPException if we cannot connect for some reason
+ */
+ private ConnectState connectAndLogin(
+ InetSocketAddress address, String serviceName,
+ String userName, String resource,
+ JabberLoginStrategy loginStrategy)
+ throws XMPPException
+ {
+ ConnectionConfiguration confConn = new ConnectionConfiguration(
+ address.getAddress().getHostAddress(),
+ address.getPort(),
+ serviceName, proxy
+ );
+
+ confConn.setReconnectionAllowed(false);
+ boolean tlsRequired = loginStrategy.isTlsRequired();
+
+ // user have the possibility to disable TLS but in this case, it will
+ // not be able to connect to a server which requires TLS
+ confConn.setSecurityMode(
+ tlsRequired ? ConnectionConfiguration.SecurityMode.required :
+ ConnectionConfiguration.SecurityMode.enabled);
+
+ if(connection != null)
+ {
+ logger.error("Connection is not null and isConnected:"
+ + connection.isConnected(),
+ new Exception("Trace possible duplicate connections: " +
+ getAccountID().getAccountAddress()));
+ disconnectAndCleanConnection();
+ }
+
+ connection = new XMPPConnection(confConn);
+
+ try
+ {
+ CertificateService cvs =
+ getCertificateVerificationService();
+ if(cvs != null)
+ {
+ SSLContext sslContext = loginStrategy.createSslContext(cvs,
+ getTrustManager(cvs, serviceName));
+ connection.setCustomSslContext(sslContext);
+ }
+ else if (tlsRequired)
+ throw new XMPPException(
+ "Certificate verification service is "
+ + "unavailable and TLS is required");
+ }
+ catch(GeneralSecurityException e)
+ {
+ logger.error("Error creating custom trust manager", e);
+ throw new XMPPException("Error creating custom trust manager", e);
+ }
+
+ if(debugger == null)
+ debugger = new SmackPacketDebugger();
+
+ // sets the debugger
+ debugger.setConnection(connection);
+ connection.addPacketListener(debugger, null);
+ connection.addPacketInterceptor(debugger, null);
+
+ connection.connect();
+
+ setTrafficClass();
+
+ if(abortConnecting)
+ {
+ abortConnecting = false;
+ disconnectAndCleanConnection();
+
+ return ConnectState.ABORT_CONNECTING;
+ }
+
+ registerServiceDiscoveryManager();
+
+ if(connectionListener == null)
+ {
+ connectionListener = new JabberConnectionListener();
+ }
+
+ if(!connection.isSecureConnection() && tlsRequired)
+ {
+ throw new XMPPException("TLS is required by client");
+ }
+
+ if(!connection.isConnected())
+ {
+ // connection is not connected, lets set state to our connection
+ // as failed seems there is some lag/problem with network
+ // and this way we will inform for it and later reconnect if needed
+ // as IllegalStateException that is thrown within
+ // addConnectionListener is not handled properly
+ disconnectAndCleanConnection();
+
+ logger.error("Connection not established, server not found!");
+
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, null);
+
+ return ConnectState.ABORT_CONNECTING;
+ }
+ else
+ {
+ connection.addConnectionListener(connectionListener);
+ }
+
+ if(abortConnecting)
+ {
+ abortConnecting = false;
+ disconnectAndCleanConnection();
+
+ return ConnectState.ABORT_CONNECTING;
+ }
+
+ fireRegistrationStateChanged(
+ getRegistrationState()
+ , RegistrationState.REGISTERING
+ , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
+ , null);
+
+ if (!loginStrategy.login(connection, userName, resource))
+ {
+ disconnectAndCleanConnection();
+ eventDuringLogin = null;
+ fireRegistrationStateChanged(
+ getRegistrationState(),
+ // not auth failed, or there would be no info-popup
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED,
+ loginStrategy.getClass().getName() + " requests abort");
+
+ return ConnectState.ABORT_CONNECTING;
+ }
+
+ if(connection.isAuthenticated())
+ {
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(
+ getRegistrationState(),
+ RegistrationState.REGISTERED,
+ RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null);
+
+ /* The initial presence message is sent by smack stack and does not
+ * include priority information. In case the original status is
+ * AVAILABLE, we will not update our presence information (such as
+ * our priority) when we registered.
+ */
+ OperationSetPersistentPresenceJabberImpl opSet =
+ (OperationSetPersistentPresenceJabberImpl)
+ this.getOperationSet(OperationSetPersistentPresence.class);
+
+ try
+ {
+ opSet.publishPresenceStatus(getJabberStatusEnum().getStatus(
+ JabberStatusEnum.AVAILABLE), "");
+ }
+ catch(Exception e)
+ {
+ logger.error("Failed to publish presence status");
+ }
+
+ return ConnectState.STOP_TRYING;
+ }
+ else
+ {
+ disconnectAndCleanConnection();
+
+ eventDuringLogin = null;
+
+ fireRegistrationStateChanged(
+ getRegistrationState()
+ , RegistrationState.UNREGISTERED
+ , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
+ , null);
+
+ return ConnectState.CONTINUE_TRYING;
+ }
+ }
+
+ /**
+ * Gets the TrustManager that should be used for the specified service
+ *
+ * @param serviceName the service name
+ * @param cvs The CertificateVerificationService to retrieve the
+ * trust manager
+ * @return the trust manager
+ */
+ private X509TrustManager getTrustManager(CertificateService cvs,
+ String serviceName)
+ throws GeneralSecurityException
+ {
+ return new HostTrustManager(
+ cvs.getTrustManager(
+ Arrays.asList(new String[]{
+ serviceName,
+ "_xmpp-client." + serviceName
+ })
+ )
+ );
+ }
+
+ /**
+ * Registers our ServiceDiscoveryManager
+ */
+ private void registerServiceDiscoveryManager()
+ {
+ // we setup supported features no packets are actually sent
+ //during feature registration so we'd better do it here so that
+ //our first presence update would contain a caps with the right
+ //features.
+ String name
+ = System.getProperty(
+ "sip-communicator.application.name",
+ "SIP Communicator ")
+ + System.getProperty("sip-communicator.version","SVN");
+
+ ServiceDiscoveryManager.setIdentityName(name);
+ ServiceDiscoveryManager.setIdentityType("pc");
+
+ discoveryManager
+ = new ScServiceDiscoveryManager(
+ this,
+ new String[] { "http://jabber.org/protocol/commands"},
+ // Add features Jitsi supports in addition to smack.
+ supportedFeatures.toArray(
+ new String[supportedFeatures.size()]));
+
+ boolean isCallingDisabled
+ = JabberActivator.getConfigurationService()
+ .getBoolean(IS_CALLING_DISABLED, false);
+
+ boolean isCallingDisabledForAccount = false;
+ if (accountID != null && accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT,
+ false))
+ isCallingDisabled = true;
+
+ if(isGTalkTesting()
+ && !isCallingDisabled
+ && !isCallingDisabledForAccount)
+ {
+ // Add Google Talk "ext" capabilities
+ discoveryManager.addExtFeature(CAPS_GTALK_WEB_VOICE);
+ discoveryManager.addExtFeature(CAPS_GTALK_WEB_VIDEO);
+ discoveryManager.addExtFeature(CAPS_GTALK_WEB_CAMERA);
+ discoveryManager.addFeature(URN_GOOGLE_VOICE);
+ discoveryManager.addFeature(URN_GOOGLE_VIDEO);
+ discoveryManager.addFeature(URN_GOOGLE_CAMERA);
+ discoveryManager.addFeature(URN_GOOGLE_TRANSPORT_P2P);
+ }
+
+ /*
+ * Expose the discoveryManager as service-public through the
+ * OperationSetContactCapabilities of this ProtocolProviderService.
+ */
+ if (opsetContactCapabilities != null)
+ opsetContactCapabilities.setDiscoveryManager(discoveryManager);
+ }
+
+ /**
+ * Used to disconnect current connection and clean it.
+ */
+ public void disconnectAndCleanConnection()
+ {
+ if(connection != null)
+ {
+ connection.removeConnectionListener(connectionListener);
+
+ // disconnect anyway cause it will clear any listeners
+ // that maybe added even if its not connected
+ try
+ {
+ OperationSetPersistentPresenceJabberImpl opSet =
+ (OperationSetPersistentPresenceJabberImpl)
+ this.getOperationSet(OperationSetPersistentPresence.class);
+
+ Presence unavailablePresence =
+ new Presence(Presence.Type.unavailable);
+
+ if(opSet != null
+ && !org.jitsi.util.StringUtils
+ .isNullOrEmpty(opSet.getCurrentStatusMessage()))
+ {
+ unavailablePresence.setStatus(
+ opSet.getCurrentStatusMessage());
+ }
+
+ connection.disconnect(unavailablePresence);
+ } catch (Exception e)
+ {}
+
+ connectionListener = null;
+ connection = null;
+ // make it null as it also holds a reference to the old connection
+ // will be created again on new connection
+ try
+ {
+ /*
+ * The discoveryManager is exposed as service-public by the
+ * OperationSetContactCapabilities of this
+ * ProtocolProviderService. No longer expose it because it's
+ * going away.
+ */
+ if (opsetContactCapabilities != null)
+ opsetContactCapabilities.setDiscoveryManager(null);
+ }
+ finally
+ {
+ if(discoveryManager != null)
+ {
+ discoveryManager.stop();
+ discoveryManager = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Ends the registration of this protocol provider with the service.
+ */
+ public void unregister()
+ {
+ unregister(true);
+ }
+
+ /**
+ * Unregister and fire the event if requested
+ * @param fireEvent boolean
+ */
+ public void unregister(boolean fireEvent)
+ {
+ synchronized(initializationLock)
+ {
+ if(fireEvent)
+ {
+ eventDuringLogin = null;
+ fireRegistrationStateChanged(
+ getRegistrationState()
+ , RegistrationState.UNREGISTERING
+ , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
+ , null);
+ }
+
+ disconnectAndCleanConnection();
+
+ RegistrationState currRegState = getRegistrationState();
+
+ if(fireEvent)
+ {
+ eventDuringLogin = null;
+ fireRegistrationStateChanged(
+ currRegState,
+ RegistrationState.UNREGISTERED,
+ RegistrationStateChangeEvent.REASON_USER_REQUEST, null);
+ }
+ }
+ }
+
+ /**
+ * Returns the short name of the protocol that the implementation of this
+ * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for
+ * example).
+ *
+ * @return a String containing the short name of the protocol this
+ * service is taking care of.
+ */
+ public String getProtocolName()
+ {
+ return ProtocolNames.JABBER;
+ }
+
+ /**
+ * Initialized the service implementation, and puts it in a sate where it
+ * could interoperate with other services. It is strongly recommended that
+ * properties in this Map be mapped to property names as specified by
+ * <tt>AccountProperties</tt>.
+ *
+ * @param screenname the account id/uin/screenname of the account that
+ * we're about to create
+ * @param accountID the identifier of the account that this protocol
+ * provider represents.
+ *
+ * @see net.java.sip.communicator.service.protocol.AccountID
+ */
+ protected void initialize(String screenname,
+ AccountID accountID)
+ {
+ synchronized(initializationLock)
+ {
+ this.accountID = accountID;
+
+ // in case of modified account, we clear list of supported features
+ // and every state change listeners, otherwise we can have two
+ // OperationSet for same feature and it can causes problem (i.e.
+ // two OperationSetBasicTelephony can launch two ICE negociations
+ // (with different ufrag/passwd) and peer will failed call. And
+ // by the way user will see two dialog for answering/refusing the
+ // call
+ supportedFeatures.clear();
+ this.clearRegistrationStateChangeListener();
+ this.clearSupportedOperationSet();
+
+ synchronized(providerCreationLock)
+ {
+ if(providerManager == null)
+ {
+ try
+ {
+ ProviderManager.setInstance(new ProviderManagerExt());
+ }
+ catch(Throwable t)
+ {
+ // once loaded if we try to set instance second time
+ // IllegalStateException is thrown
+ }
+ finally
+ {
+ providerManager = ProviderManager.getInstance();
+ }
+ }
+ }
+
+ String protocolIconPath
+ = accountID.getAccountPropertyString(
+ ProtocolProviderFactory.PROTOCOL_ICON_PATH);
+
+ if (protocolIconPath == null)
+ protocolIconPath = "resources/images/protocol/jabber";
+
+ jabberIcon = new ProtocolIconJabberImpl(protocolIconPath);
+
+ jabberStatusEnum
+ = JabberStatusEnum.getJabberStatusEnum(protocolIconPath);
+
+ //this feature is mandatory to be compliant with Service Discovery
+ supportedFeatures.add("http://jabber.org/protocol/disco#info");
+
+ String keepAliveStrValue
+ = accountID.getAccountPropertyString(
+ ProtocolProviderFactory.KEEP_ALIVE_METHOD);
+
+ InfoRetreiver infoRetreiver = new InfoRetreiver(this, screenname);
+
+ //initialize the presence operationset
+ OperationSetPersistentPresenceJabberImpl persistentPresence =
+ new OperationSetPersistentPresenceJabberImpl(this, infoRetreiver);
+
+ addSupportedOperationSet(
+ OperationSetPersistentPresence.class,
+ persistentPresence);
+ // TODO: add the feature, if any, corresponding to persistent
+ // presence, if someone knows
+ // supportedFeatures.add(_PRESENCE_);
+
+ //register it once again for those that simply need presence
+ addSupportedOperationSet(
+ OperationSetPresence.class,
+ persistentPresence);
+
+ //initialize the IM operation set
+ OperationSetBasicInstantMessagingJabberImpl basicInstantMessaging =
+ new OperationSetBasicInstantMessagingJabberImpl(this);
+
+ if (keepAliveStrValue == null
+ || keepAliveStrValue.equalsIgnoreCase("XEP-0199"))
+ {
+ if(keepAliveManager == null)
+ keepAliveManager = new KeepAliveManager(this);
+ }
+
+ addSupportedOperationSet(
+ OperationSetBasicInstantMessaging.class,
+ basicInstantMessaging);
+
+ // The http://jabber.org/protocol/xhtml-im feature is included
+ // already in smack.
+
+ addSupportedOperationSet(
+ OperationSetExtendedAuthorizations.class,
+ new OperationSetExtendedAuthorizationsJabberImpl(
+ this,
+ persistentPresence));
+
+ //initialize the Whiteboard operation set
+ addSupportedOperationSet(
+ OperationSetWhiteboarding.class,
+ new OperationSetWhiteboardingJabberImpl(this));
+
+ //initialize the typing notifications operation set
+ addSupportedOperationSet(
+ OperationSetTypingNotifications.class,
+ new OperationSetTypingNotificationsJabberImpl(this));
+
+ // The http://jabber.org/protocol/chatstates feature implemented in
+ // OperationSetTypingNotifications is included already in smack.
+
+ //initialize the multi user chat operation set
+ addSupportedOperationSet(
+ OperationSetMultiUserChat.class,
+ new OperationSetMultiUserChatJabberImpl(this));
+
+ addSupportedOperationSet(
+ OperationSetServerStoredContactInfo.class,
+ new OperationSetServerStoredContactInfoJabberImpl(
+ infoRetreiver));
+
+ OperationSetServerStoredAccountInfo accountInfo =
+ new OperationSetServerStoredAccountInfoJabberImpl(this,
+ infoRetreiver,
+ screenname);
+
+ addSupportedOperationSet(
+ OperationSetServerStoredAccountInfo.class,
+ accountInfo);
+
+ // Initialize avatar operation set
+ addSupportedOperationSet(
+ OperationSetAvatar.class,
+ new OperationSetAvatarJabberImpl(this, accountInfo));
+
+ // initialize the file transfer operation set
+ addSupportedOperationSet(
+ OperationSetFileTransfer.class,
+ new OperationSetFileTransferJabberImpl(this));
+
+ addSupportedOperationSet(
+ OperationSetInstantMessageTransform.class,
+ new OperationSetInstantMessageTransformImpl());
+
+ // Include features we're supporting in addition to the four
+ // included by smack itself:
+ // http://jabber.org/protocol/si/profile/file-transfer
+ // http://jabber.org/protocol/si
+ // http://jabber.org/protocol/bytestreams
+ // http://jabber.org/protocol/ibb
+ supportedFeatures.add("urn:xmpp:thumbs:0");
+ supportedFeatures.add("urn:xmpp:bob");
+
+ // initialize the thumbnailed file factory operation set
+ addSupportedOperationSet(
+ OperationSetThumbnailedFileFactory.class,
+ new OperationSetThumbnailedFileFactoryImpl());
+
+ // TODO: this is the "main" feature to advertise when a client
+ // support muc. We have to add some features for
+ // specific functionality we support in muc.
+ // see http://www.xmpp.org/extensions/xep-0045.html
+
+ // The http://jabber.org/protocol/muc feature is already included in
+ // smack.
+ supportedFeatures.add("http://jabber.org/protocol/muc#rooms");
+ supportedFeatures.add("http://jabber.org/protocol/muc#traffic");
+
+ // RTP HDR extension
+ supportedFeatures.add(URN_XMPP_JINGLE_RTP_HDREXT);
+
+ //register our jingle provider
+ providerManager.addIQProvider( JingleIQ.ELEMENT_NAME,
+ JingleIQ.NAMESPACE,
+ new JingleIQProvider());
+
+ // register our input event provider
+ providerManager.addIQProvider(InputEvtIQ.ELEMENT_NAME,
+ InputEvtIQ.NAMESPACE,
+ new InputEvtIQProvider());
+
+ // register our coin provider
+ providerManager.addIQProvider(CoinIQ.ELEMENT_NAME,
+ CoinIQ.NAMESPACE,
+ new CoinIQProvider());
+ supportedFeatures.add(URN_XMPP_JINGLE_COIN);
+
+ //register our GTalk dialect provider
+ providerManager.addIQProvider( SessionIQ.ELEMENT_NAME,
+ SessionIQ.NAMESPACE,
+ new SessionIQProvider());
+
+ // register our JingleInfo provider
+ providerManager.addIQProvider(JingleInfoQueryIQ.ELEMENT_NAME,
+ JingleInfoQueryIQ.NAMESPACE,
+ new JingleInfoQueryIQProvider());
+
+ // Jitsi Videobridge IQProvider and PacketExtensionProvider
+ providerManager.addIQProvider(
+ ColibriConferenceIQ.ELEMENT_NAME,
+ ColibriConferenceIQ.NAMESPACE,
+ new ColibriIQProvider());
+ providerManager.addExtensionProvider(
+ PayloadTypePacketExtension.ELEMENT_NAME,
+ ColibriConferenceIQ.NAMESPACE,
+ new DefaultPacketExtensionProvider<
+ PayloadTypePacketExtension>(
+ PayloadTypePacketExtension.class));
+ providerManager.addExtensionProvider(
+ ParameterPacketExtension.ELEMENT_NAME,
+ ColibriConferenceIQ.NAMESPACE,
+ new DefaultPacketExtensionProvider<
+ ParameterPacketExtension>(
+ ParameterPacketExtension.class));
+
+ providerManager.addExtensionProvider(
+ ConferenceDescriptionPacketExtension.ELEMENT_NAME,
+ ConferenceDescriptionPacketExtension.NAMESPACE,
+ new ConferenceDescriptionPacketExtension.Provider());
+
+ //initialize the telephony operation set
+ boolean isCallingDisabled
+ = JabberActivator.getConfigurationService()
+ .getBoolean(IS_CALLING_DISABLED, false);
+
+ boolean isCallingDisabledForAccount
+ = accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT,
+ false);
+
+ // Check if calling is enabled.
+ if (!isCallingDisabled && !isCallingDisabledForAccount)
+ {
+ OperationSetBasicTelephonyJabberImpl basicTelephony
+ = new OperationSetBasicTelephonyJabberImpl(this);
+
+ addSupportedOperationSet(
+ OperationSetAdvancedTelephony.class,
+ basicTelephony);
+ addSupportedOperationSet(
+ OperationSetBasicTelephony.class,
+ basicTelephony);
+ addSupportedOperationSet(
+ OperationSetSecureZrtpTelephony.class,
+ basicTelephony);
+ addSupportedOperationSet(
+ OperationSetSecureSDesTelephony.class,
+ basicTelephony);
+
+ // initialize video telephony OperationSet
+ addSupportedOperationSet(
+ OperationSetVideoTelephony.class,
+ new OperationSetVideoTelephonyJabberImpl(basicTelephony));
+
+ addSupportedOperationSet(
+ OperationSetTelephonyConferencing.class,
+ new OperationSetTelephonyConferencingJabberImpl(this));
+
+ addSupportedOperationSet(
+ OperationSetBasicAutoAnswer.class,
+ new OperationSetAutoAnswerJabberImpl(this));
+
+ addSupportedOperationSet(
+ OperationSetResourceAwareTelephony.class,
+ new OperationSetResAwareTelephonyJabberImpl(basicTelephony));
+
+ // Only init video bridge if enabled
+ boolean isVideobridgeDisabled
+ = JabberActivator.getConfigurationService()
+ .getBoolean(OperationSetVideoBridge.
+ IS_VIDEO_BRIDGE_DISABLED, false);
+
+ if (!isVideobridgeDisabled)
+ {
+ // init video bridge
+ addSupportedOperationSet(
+ OperationSetVideoBridge.class,
+ new OperationSetVideoBridgeImpl(this));
+ }
+
+ // init DTMF
+ OperationSetDTMFJabberImpl operationSetDTMFSip
+ = new OperationSetDTMFJabberImpl(this);
+ addSupportedOperationSet(
+ OperationSetDTMF.class, operationSetDTMFSip);
+
+ addJingleFeatures();
+
+ // Check if desktop streaming is enabled.
+ boolean isDesktopStreamingDisabled
+ = JabberActivator.getConfigurationService()
+ .getBoolean(IS_DESKTOP_STREAMING_DISABLED, false);
+
+ boolean isAccountDesktopStreamingDisabled
+ = accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_DESKTOP_STREAMING_DISABLED,
+ false);
+
+ if (!isDesktopStreamingDisabled
+ && !isAccountDesktopStreamingDisabled)
+ {
+ // initialize desktop streaming OperationSet
+ addSupportedOperationSet(
+ OperationSetDesktopStreaming.class,
+ new OperationSetDesktopStreamingJabberImpl(
+ basicTelephony));
+
+ // initialize desktop sharing OperationSets
+ addSupportedOperationSet(
+ OperationSetDesktopSharingServer.class,
+ new OperationSetDesktopSharingServerJabberImpl(
+ basicTelephony));
+
+ // Adds extension to support remote control as a sharing
+ // server (sharer).
+ supportedFeatures.add(InputEvtIQ.NAMESPACE_SERVER);
+
+ addSupportedOperationSet(
+ OperationSetDesktopSharingClient.class,
+ new OperationSetDesktopSharingClientJabberImpl(this)
+ );
+ // Adds extension to support remote control as a sharing
+ // client (sharee).
+ supportedFeatures.add(InputEvtIQ.NAMESPACE_CLIENT);
+ }
+ }
+
+ // OperationSetContactCapabilities
+ opsetContactCapabilities
+ = new OperationSetContactCapabilitiesJabberImpl(this);
+ if (discoveryManager != null)
+ opsetContactCapabilities.setDiscoveryManager(discoveryManager);
+ addSupportedOperationSet(
+ OperationSetContactCapabilities.class,
+ opsetContactCapabilities);
+
+ addSupportedOperationSet(
+ OperationSetGenericNotifications.class,
+ new OperationSetGenericNotificationsJabberImpl(this));
+
+ supportedFeatures.add("jabber:iq:version");
+ if(versionManager == null)
+ versionManager = new VersionManager(this);
+
+ supportedFeatures.add(MessageCorrectionExtension.NAMESPACE);
+ addSupportedOperationSet(OperationSetMessageCorrection.class,
+ basicInstantMessaging);
+
+ OperationSetChangePassword opsetChangePassword
+ = new OperationSetChangePasswordJabberImpl(this);
+ addSupportedOperationSet(OperationSetChangePassword.class,
+ opsetChangePassword);
+
+ OperationSetCusaxUtils opsetCusaxCusaxUtils
+ = new OperationSetCusaxUtilsJabberImpl(this);
+ addSupportedOperationSet(OperationSetCusaxUtils.class,
+ opsetCusaxCusaxUtils);
+
+ isInitialized = true;
+ }
+ }
+
+ /**
+ * Adds Jingle related features to the supported features.
+ */
+ private void addJingleFeatures()
+ {
+ // Add Jingle features to supported features.
+ supportedFeatures.add(URN_XMPP_JINGLE);
+ supportedFeatures.add(URN_XMPP_JINGLE_RTP);
+ supportedFeatures.add(URN_XMPP_JINGLE_RAW_UDP_0);
+
+ /*
+ * Reflect the preference of the user with respect to the use of
+ * ICE.
+ */
+ if (accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_USE_ICE,
+ true))
+ {
+ supportedFeatures.add(URN_XMPP_JINGLE_ICE_UDP_1);
+ }
+
+ supportedFeatures.add(URN_XMPP_JINGLE_RTP_AUDIO);
+ supportedFeatures.add(URN_XMPP_JINGLE_RTP_VIDEO);
+ supportedFeatures.add(URN_XMPP_JINGLE_RTP_ZRTP);
+
+ /*
+ * Reflect the preference of the user with respect to the use of
+ * Jingle Nodes.
+ */
+ if (accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactoryJabberImpl.IS_USE_JINGLE_NODES,
+ true))
+ {
+ supportedFeatures.add(URN_XMPP_JINGLE_NODES);
+ }
+
+ // XEP-0251: Jingle Session Transfer
+ supportedFeatures.add(URN_XMPP_JINGLE_TRANSFER_0);
+
+ // XEP-0320: Use of DTLS-SRTP in Jingle Sessions
+ if (accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.DEFAULT_ENCRYPTION,
+ true)
+ && accountID.isEncryptionProtocolEnabled(
+ DtlsControl.PROTO_NAME))
+ {
+ supportedFeatures.add(URN_XMPP_JINGLE_DTLS_SRTP);
+ }
+ }
+
+ /**
+ * Makes the service implementation close all open sockets and release
+ * any resources that it might have taken and prepare for
+ * shutdown/garbage collection.
+ */
+ public void shutdown()
+ {
+ synchronized(initializationLock)
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Killing the Jabber Protocol Provider.");
+
+ //kill all active calls
+ OperationSetBasicTelephonyJabberImpl telephony
+ = (OperationSetBasicTelephonyJabberImpl)getOperationSet(
+ OperationSetBasicTelephony.class);
+ if (telephony != null)
+ {
+ telephony.shutdown();
+ }
+
+ disconnectAndCleanConnection();
+
+ isInitialized = false;
+ }
+ }
+
+ /**
+ * Returns true if the provider service implementation is initialized and
+ * ready for use by other services, and false otherwise.
+ *
+ * @return true if the provider is initialized and ready for use and false
+ * otherwise
+ */
+ public boolean isInitialized()
+ {
+ return isInitialized;
+ }
+
+ /**
+ * Returns the AccountID that uniquely identifies the account represented
+ * by this instance of the ProtocolProviderService.
+ * @return the id of the account represented by this provider.
+ */
+ public AccountID getAccountID()
+ {
+ return accountID;
+ }
+
+ /**
+ * Returns the <tt>XMPPConnection</tt>opened by this provider
+ * @return a reference to the <tt>XMPPConnection</tt> last opened by this
+ * provider.
+ */
+ public XMPPConnection getConnection()
+ {
+ return connection;
+ }
+
+ /**
+ * Determines whether a specific <tt>XMPPException</tt> signals that
+ * attempted authentication has failed.
+ *
+ * @param ex the <tt>XMPPException</tt> which is to be determined whether it
+ * signals that attempted authentication has failed
+ * @return <tt>true</tt> if the specified <tt>ex</tt> signals that attempted
+ * authentication has failed; otherwise, <tt>false</tt>
+ */
+ private boolean isAuthenticationFailed(XMPPException ex)
+ {
+ String exMsg = ex.getMessage().toLowerCase();
+
+ // as there are no types or reasons for XMPPException
+ // we try determine the reason according to their message
+ // all messages that were found in smack 3.1.0 were took in count
+ return
+ ((exMsg.indexOf("sasl authentication") != -1)
+ && (exMsg.indexOf("failed") != -1))
+ || (exMsg.indexOf(
+ "does not support compatible authentication mechanism")
+ != -1)
+ || (exMsg.indexOf("unable to determine password") != -1);
+ }
+
+ /**
+ * Tries to determine the appropriate message and status to fire,
+ * according the exception.
+ *
+ * @param ex the {@link XMPPException} that caused the state change.
+ */
+ private void fireRegistrationStateChanged(XMPPException ex)
+ {
+ int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;
+ RegistrationState regState = RegistrationState.UNREGISTERED;
+ String reasonStr = null;
+
+ Throwable wrappedEx = ex.getWrappedThrowable();
+ if(wrappedEx != null
+ && (wrappedEx instanceof UnknownHostException
+ || wrappedEx instanceof ConnectException
+ || wrappedEx instanceof SocketException))
+ {
+ reason = RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND;
+ regState = RegistrationState.CONNECTION_FAILED;
+ }
+ else
+ {
+ String exMsg = ex.getMessage().toLowerCase();
+
+ // as there are no types or reasons for XMPPException
+ // we try determine the reason according to their message
+ // all messages that were found in smack 3.1.0 were took in count
+ if(isAuthenticationFailed(ex))
+ {
+ JabberActivator.getProtocolProviderFactory().
+ storePassword(getAccountID(), null);
+
+ reason = RegistrationStateChangeEvent
+ .REASON_AUTHENTICATION_FAILED;
+
+ regState = RegistrationState.AUTHENTICATION_FAILED;
+
+ fireRegistrationStateChanged(
+ getRegistrationState(), regState, reason, null);
+
+ // Try to reregister and to ask user for a new password.
+ reregister(SecurityAuthority.WRONG_PASSWORD);
+
+ return;
+ }
+ else if(exMsg.indexOf("no response from the server") != -1
+ || exMsg.indexOf("connection failed") != -1)
+ {
+ reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;
+ regState = RegistrationState.CONNECTION_FAILED;
+ }
+ else if(exMsg.indexOf("tls is required") != -1)
+ {
+ regState = RegistrationState.AUTHENTICATION_FAILED;
+ reason = RegistrationStateChangeEvent.REASON_TLS_REQUIRED;
+ }
+ }
+
+ if(regState == RegistrationState.UNREGISTERED
+ || regState == RegistrationState.CONNECTION_FAILED)
+ {
+ // we fired that for some reason we are going offline
+ // lets clean the connection state for any future connections
+ disconnectAndCleanConnection();
+ }
+
+ fireRegistrationStateChanged(
+ getRegistrationState(), regState, reason, reasonStr);
+ }
+
+ /**
+ * Enable to listen for jabber connection events
+ */
+ private class JabberConnectionListener
+ implements ConnectionListener
+ {
+ /**
+ * Implements <tt>connectionClosed</tt> from <tt>ConnectionListener</tt>
+ */
+ public void connectionClosed()
+ {
+ // if we are in the middle of connecting process
+ // do not fire events, will do it later when the method
+ // connectAndLogin finishes its work
+ synchronized(connectAndLoginLock)
+ {
+ if(inConnectAndLogin)
+ {
+ eventDuringLogin = new RegistrationStateChangeEvent(
+ ProtocolProviderServiceJabberImpl.this,
+ getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
+ null);
+ return;
+ }
+ }
+ // fire that a connection failed, the reconnection mechanism
+ // will look after us and will clean us, other wise we can do
+ // a dead lock (connection closed is called
+ // within xmppConneciton and calling disconnect again can lock it)
+ fireRegistrationStateChanged(
+ getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
+ null);
+ }
+
+ /**
+ * Implements <tt>connectionClosedOnError</tt> from
+ * <tt>ConnectionListener</tt>.
+ *
+ * @param exception contains information on the error.
+ */
+ public void connectionClosedOnError(Exception exception)
+ {
+ logger.error("connectionClosedOnError " +
+ exception.getLocalizedMessage());
+
+ if(exception instanceof XMPPException)
+ {
+ StreamError err = ((XMPPException)exception).getStreamError();
+
+ if(err != null && err.getCode().equals(
+ XMPPError.Condition.conflict.toString()))
+ {
+ // if we are in the middle of connecting process
+ // do not fire events, will do it later when the method
+ // connectAndLogin finishes its work
+ synchronized(connectAndLoginLock)
+ {
+ if(inConnectAndLogin)
+ {
+ eventDuringLogin = new RegistrationStateChangeEvent(
+ ProtocolProviderServiceJabberImpl.this,
+ getRegistrationState(),
+ RegistrationState.UNREGISTERED,
+ RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS,
+ "Connecting multiple times with the same resource");
+ return;
+ }
+ }
+
+ disconnectAndCleanConnection();
+
+ fireRegistrationStateChanged(getRegistrationState(),
+ RegistrationState.UNREGISTERED,
+ RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS,
+ "Connecting multiple times with the same resource");
+
+ return;
+ }
+ } // Ignore certificate exceptions as we handle them elsewhere
+ else if(exception instanceof SSLHandshakeException &&
+ exception.getCause() instanceof CertificateException)
+ {
+ return;
+ }
+
+ // if we are in the middle of connecting process
+ // do not fire events, will do it later when the method
+ // connectAndLogin finishes its work
+ synchronized(connectAndLoginLock)
+ {
+ if(inConnectAndLogin)
+ {
+ eventDuringLogin = new RegistrationStateChangeEvent(
+ ProtocolProviderServiceJabberImpl.this,
+ getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
+ exception.getMessage());
+ return;
+ }
+ }
+
+ disconnectAndCleanConnection();
+
+ fireRegistrationStateChanged(getRegistrationState(),
+ RegistrationState.CONNECTION_FAILED,
+ RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
+ exception.getMessage());
+ }
+
+ /**
+ * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt>
+ *
+ * @param i delay in seconds for reconnection.
+ */
+ public void reconnectingIn(int i)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("reconnectingIn " + i);
+ }
+
+ /**
+ * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt>
+ */
+ public void reconnectionSuccessful()
+ {
+ if (logger.isInfoEnabled())
+ logger.info("reconnectionSuccessful");
+ }
+
+ /**
+ * Implements <tt>reconnectionFailed</tt> from
+ * <tt>ConnectionListener</tt>.
+ *
+ * @param exception description of the failure
+ */
+ public void reconnectionFailed(Exception exception)
+ {
+ if (logger.isInfoEnabled())
+ logger.info("reconnectionFailed ", exception);
+ }
+ }
+
+ /**
+ * Returns the jabber protocol icon.
+ * @return the jabber protocol icon
+ */
+ public ProtocolIcon getProtocolIcon()
+ {
+ return jabberIcon;
+ }
+
+ /**
+ * Returns the current instance of <tt>JabberStatusEnum</tt>.
+ *
+ * @return the current instance of <tt>JabberStatusEnum</tt>.
+ */
+ JabberStatusEnum getJabberStatusEnum()
+ {
+ return jabberStatusEnum;
+ }
+
+ /**
+ * Determines if the given list of <tt>ext features</tt> is supported by the
+ * specified jabber id.
+ *
+ * @param jid the jabber id for which to check
+ * @param extFeatures the list of ext features to check for
+ *
+ * @return <tt>true</tt> if the list of ext features is supported;
+ * otherwise, <tt>false</tt>
+ */
+ public boolean isExtFeatureListSupported(String jid, String... extFeatures)
+ {
+ EntityCapsManager capsManager = discoveryManager.getCapsManager();
+ EntityCapsManager.Caps caps = capsManager.getCapsByUser(jid);
+
+ String bypassDomain = accountID.getAccountPropertyString(
+ "TELEPHONY_BYPASS_GTALK_CAPS");
+ String domain = StringUtils.parseServer(jid);
+ boolean domainEquals = domain.equals(bypassDomain);
+
+ if(caps != null && caps.ext != null)
+ {
+ String exts[] = caps.ext.split(" ");
+ boolean found = false;
+
+ for(String extFeature : extFeatures)
+ {
+ // in case we have a domain that have to bypass GTalk caps
+ if(extFeature.equals(CAPS_GTALK_WEB_VOICE) && domainEquals)
+ {
+ return true;
+ }
+
+ found = false;
+ for(String ext : exts)
+ {
+ if(ext.equals(extFeature))
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if(!found)
+ {
+ break;
+ }
+ }
+
+ return found;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines if the given list of <tt>features</tt> is supported by the
+ * specified jabber id.
+ *
+ * @param jid the jabber id for which to check
+ * @param features the list of features to check for
+ *
+ * @return <tt>true</tt> if the list of features is supported; otherwise,
+ * <tt>false</tt>
+ */
+ public boolean isFeatureListSupported(String jid, String... features)
+ {
+ boolean isFeatureListSupported = true;
+
+ try
+ {
+ if(discoveryManager == null)
+ return isFeatureListSupported;
+
+ DiscoverInfo featureInfo =
+ discoveryManager.discoverInfoNonBlocking(jid);
+
+ if(featureInfo == null)
+ return isFeatureListSupported;
+
+ for (String feature : features)
+ {
+ if (!featureInfo.containsFeature(feature))
+ {
+ // If one is not supported we return false and don't check
+ // the others.
+ isFeatureListSupported = false;
+ break;
+ }
+ }
+ }
+ catch (XMPPException e)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Failed to retrive discovery info.", e);
+ }
+ return isFeatureListSupported;
+ }
+
+ /**
+ * Determines if the given list of <tt>features</tt> is supported by the
+ * specified jabber id.
+ *
+ * @param jid the jabber id that we'd like to get information about
+ * @param feature the feature to check for
+ *
+ * @return <tt>true</tt> if the list of features is supported, otherwise
+ * returns <tt>false</tt>
+ */
+ public boolean isFeatureSupported(String jid, String feature)
+ {
+ return isFeatureListSupported(jid, feature);
+ }
+
+ /**
+ * Returns the full jabber id (jid) corresponding to the given contact. If
+ * the provider is not connected returns null.
+ *
+ * @param contact the contact, for which we're looking for a jid
+ * @return the jid of the specified contact or null if the provider is not
+ * yet connected;
+ */
+ public String getFullJid(Contact contact)
+ {
+ return getFullJid(contact.getAddress());
+ }
+
+ /**
+ * Returns the full jabber id (jid) corresponding to the given bare jid. If
+ * the provider is not connected returns null.
+ *
+ * @param bareJid the bare contact address (i.e. no resource) whose full
+ * jid we are looking for.
+ * @return the jid of the specified contact or null if the provider is not
+ * yet connected;
+ */
+ public String getFullJid(String bareJid)
+ {
+ XMPPConnection connection = getConnection();
+
+ // when we are not connected there is no full jid
+ if (connection != null && connection.isConnected())
+ {
+ Roster roster = connection.getRoster();
+
+ if (roster != null)
+ return roster.getPresence(bareJid).getFrom();
+ }
+ return null;
+ }
+
+ /**
+ * The trust manager which asks the client whether to trust particular
+ * certificate which is not globally trusted.
+ */
+ private class HostTrustManager
+ implements X509TrustManager
+ {
+ /**
+ * The default trust manager.
+ */
+ private final X509TrustManager tm;
+
+ /**
+ * Creates the custom trust manager.
+ * @param tm the default trust manager.
+ */
+ HostTrustManager(X509TrustManager tm)
+ {
+ this.tm = tm;
+ }
+
+ /**
+ * Not used.
+ *
+ * @return nothing.
+ */
+ public X509Certificate[] getAcceptedIssuers()
+ {
+ return new X509Certificate[0];
+ }
+
+ /**
+ * Not used.
+ * @param chain the cert chain.
+ * @param authType authentication type like: RSA.
+ * @throws CertificateException never
+ * @throws UnsupportedOperationException always
+ */
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException, UnsupportedOperationException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Check whether a certificate is trusted, if not as user whether he
+ * trust it.
+ * @param chain the certificate chain.
+ * @param authType authentication type like: RSA.
+ * @throws CertificateException not trusted.
+ */
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException
+ {
+ abortConnecting = true;
+ try
+ {
+ tm.checkServerTrusted(chain, authType);
+ }
+ catch(CertificateException e)
+ {
+ fireRegistrationStateChanged(getRegistrationState(),
+ RegistrationState.UNREGISTERED,
+ RegistrationStateChangeEvent.REASON_USER_REQUEST,
+ "Not trusted certificate");
+ throw e;
+ }
+
+ if(abortConnecting)
+ {
+ // connect hasn't finished we will continue normally
+ abortConnecting = false;
+ return;
+ }
+ else
+ {
+ // in this situation connect method has finished
+ // and it was disconnected so we wont to connect.
+
+ // register.connect in new thread so we can release the
+ // current connecting thread, otherwise this blocks
+ // jabber
+ new Thread(new Runnable()
+ {
+ public void run()
+ {
+ reregister(SecurityAuthority.CONNECTION_FAILED);
+ }
+ }).start();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Returns the currently valid {@link ScServiceDiscoveryManager}.
+ *
+ * @return the currently valid {@link ScServiceDiscoveryManager}.
+ */
+ public ScServiceDiscoveryManager getDiscoveryManager()
+ {
+ return discoveryManager;
+ }
+
+ /**
+ * Returns our own Jabber ID.
+ *
+ * @return our own Jabber ID.
+ */
+ public String getOurJID()
+ {
+ String jid = null;
+
+ if (connection != null)
+ jid = connection.getUser();
+
+ if (jid == null)
+ {
+ // seems like the connection is not yet initialized so lets try to
+ // construct our jid ourselves.
+ String accountIDUserID = getAccountID().getUserID();
+ String userID = StringUtils.parseName(accountIDUserID);
+ String serviceName = StringUtils.parseServer(accountIDUserID);
+
+ jid = userID + "@" + serviceName;
+ }
+
+ return jid;
+ }
+
+ /**
+ * Returns the <tt>InetAddress</tt> that is most likely to be to be used
+ * as a next hop when contacting our XMPP server. This is an utility method
+ * that is used whenever we have to choose one of our local addresses (e.g.
+ * when trying to pick a best candidate for raw udp). It is based on the
+ * assumption that, in absence of any more specific details, chances are
+ * that we will be accessing remote destinations via the same interface
+ * that we are using to access our jabber server.
+ *
+ * @return the <tt>InetAddress</tt> that is most likely to be to be used
+ * as a next hop when contacting our server.
+ *
+ * @throws IllegalArgumentException if we don't have a valid server.
+ */
+ public InetAddress getNextHop()
+ throws IllegalArgumentException
+ {
+ InetAddress nextHop = null;
+ String nextHopStr = null;
+
+ if ( proxy != null
+ && proxy.getProxyType()
+ != org.jivesoftware.smack.proxy.ProxyInfo.ProxyType.NONE)
+ {
+ nextHopStr = proxy.getProxyAddress();
+ }
+ else
+ {
+ nextHopStr = getConnection().getHost();
+ }
+
+ try
+ {
+ nextHop = NetworkUtils.getInetAddress(nextHopStr);
+ }
+ catch (UnknownHostException ex)
+ {
+ throw new IllegalArgumentException(
+ "seems we don't have a valid next hop.", ex);
+ }
+
+ if(logger.isDebugEnabled())
+ logger.debug("Returning address " + nextHop + " as next hop.");
+
+ return nextHop;
+ }
+
+ /**
+ * Start auto-discovery of JingleNodes tracker/relays.
+ */
+ public void startJingleNodesDiscovery()
+ {
+ // Jingle Nodes Service Initialization
+ final JabberAccountIDImpl accID = (JabberAccountIDImpl)getAccountID();
+ final SmackServiceNode service = new SmackServiceNode(connection,
+ 60000);
+ // make sure SmackServiceNode will clean up when connection is closed
+ connection.addConnectionListener(service);
+
+ for(JingleNodeDescriptor desc : accID.getJingleNodes())
+ {
+ TrackerEntry entry = new TrackerEntry(
+ desc.isRelaySupported() ? TrackerEntry.Type.relay :
+ TrackerEntry.Type.tracker,
+ TrackerEntry.Policy._public,
+ desc.getJID(),
+ JingleChannelIQ.UDP);
+
+ service.addTrackerEntry(entry);
+ }
+
+ new Thread(new JingleNodesServiceDiscovery(
+ service,
+ connection,
+ accID,
+ jingleNodesSyncRoot))
+ .start();
+
+ jingleNodesServiceNode = service;
+ }
+
+ /**
+ * Get the Jingle Nodes service. Note that this method will block until
+ * Jingle Nodes auto discovery (if enabled) finished.
+ *
+ * @return Jingle Nodes service
+ */
+ public SmackServiceNode getJingleNodesServiceNode()
+ {
+ synchronized(jingleNodesSyncRoot)
+ {
+ return jingleNodesServiceNode;
+ }
+ }
+
+ /**
+ * Logs a specific message and associated <tt>Throwable</tt> cause as an
+ * error using the current <tt>Logger</tt> and then throws a new
+ * <tt>OperationFailedException</tt> with the message, a specific error code
+ * and the cause.
+ *
+ * @param message the message to be logged and then wrapped in a new
+ * <tt>OperationFailedException</tt>
+ * @param errorCode the error code to be assigned to the new
+ * <tt>OperationFailedException</tt>
+ * @param cause the <tt>Throwable</tt> that has caused the necessity to log
+ * an error and have a new <tt>OperationFailedException</tt> thrown
+ * @param logger the logger that we'd like to log the error <tt>message</tt>
+ * and <tt>cause</tt>.
+ *
+ * @throws OperationFailedException the exception that we wanted this method
+ * to throw.
+ */
+ public static void throwOperationFailedException( String message,
+ int errorCode,
+ Throwable cause,
+ Logger logger)
+ throws OperationFailedException
+ {
+ logger.error(message, cause);
+
+ if(cause == null)
+ throw new OperationFailedException(message, errorCode);
+ else
+ throw new OperationFailedException(message, errorCode, cause);
+ }
+
+ /**
+ * Used when we need to re-register or someone needs to obtain credentials.
+ * @return the SecurityAuthority.
+ */
+ public SecurityAuthority getAuthority()
+ {
+ return authority;
+ }
+
+ /**
+ * Returns true if gtalktesting is enabled, false otherwise.
+ *
+ * @return true if gtalktesting is enabled, false otherwise.
+ */
+ public boolean isGTalkTesting()
+ {
+ return
+ Boolean.getBoolean("gtalktesting")
+ || JabberActivator.getConfigurationService().getBoolean(
+ "net.java.sip.communicator.impl.protocol.jabber"
+ + ".gtalktesting",
+ false)
+ || accountID.getAccountPropertyBoolean(
+ ProtocolProviderFactory.IS_USE_GOOGLE_ICE,
+ true);
+ }
+
+ UserCredentials getUserCredentials()
+ {
+ return userCredentials;
+ }
+
+ /**
+ * Returns true if our account is a Gmail or a Google Apps ones.
+ *
+ * @return true if our account is a Gmail or a Google Apps ones.
+ */
+ public boolean isGmailOrGoogleAppsAccount()
+ {
+ String domain = StringUtils.parseServer(
+ getAccountID().getUserID());
+ return isGmailOrGoogleAppsAccount(domain);
+ }
+
+ /**
+ * Returns true if our account is a Gmail or a Google Apps ones.
+ *
+ * @param domain domain to check
+ * @return true if our account is a Gmail or a Google Apps ones.
+ */
+ public static boolean isGmailOrGoogleAppsAccount(String domain)
+ {
+ SRVRecord srvRecords[] = null;
+
+ try
+ {
+ srvRecords = NetworkUtils.getSRVRecords("xmpp-client", "tcp",
+ domain);
+ }
+ catch (ParseException e)
+ {
+ logger.info("Failed to get SRV records for XMPP domain");
+ return false;
+ }
+ catch (DnssecException e)
+ {
+ logger.error("DNSSEC failure while checking for google domains", e);
+ return false;
+ }
+
+ if(srvRecords == null)
+ {
+ return false;
+ }
+
+ for(SRVRecord srv : srvRecords)
+ {
+ if(srv.getTarget().endsWith("google.com") ||
+ srv.getTarget().endsWith("google.com."))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the traffic class for the XMPP signalling socket.
+ */
+ private void setTrafficClass()
+ {
+ Socket s = connection.getSocket();
+
+ if(s != null)
+ {
+ ConfigurationService configService =
+ JabberActivator.getConfigurationService();
+ String dscp = configService.getString(XMPP_DSCP_PROPERTY);
+
+ if(dscp != null)
+ {
+ try
+ {
+ int dscpInt = Integer.parseInt(dscp) << 2;
+
+ if(dscpInt > 0)
+ s.setTrafficClass(dscpInt);
+ }
+ catch (Exception e)
+ {
+ logger.info("Failed to set trafficClass", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the entity ID of the first Jitsi Videobridge associated with
+ * {@link #connection} i.e. provided by the <tt>serviceName</tt> of
+ * <tt>connection</tt>.
+ *
+ * @return the entity ID of the first Jitsi Videobridge associated with
+ * <tt>connection</tt>
+ */
+ public String getJitsiVideobridge()
+ {
+ XMPPConnection connection = getConnection();
+
+ if (connection != null)
+ {
+ ScServiceDiscoveryManager discoveryManager = getDiscoveryManager();
+ String serviceName = connection.getServiceName();
+ DiscoverItems discoverItems = null;
+
+ try
+ {
+ discoverItems = discoveryManager.discoverItems(serviceName);
+ }
+ catch (XMPPException xmppe)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Failed to discover the items associated with"
+ + " Jabber entity: " + serviceName,
+ xmppe);
+ }
+ }
+ if (discoverItems != null)
+ {
+ Iterator<DiscoverItems.Item> discoverItemIter
+ = discoverItems.getItems();
+
+ while (discoverItemIter.hasNext())
+ {
+ DiscoverItems.Item discoverItem = discoverItemIter.next();
+ String entityID = discoverItem.getEntityID();
+ DiscoverInfo discoverInfo = null;
+
+ try
+ {
+ discoverInfo = discoveryManager.discoverInfo(entityID);
+ }
+ catch (XMPPException xmppe)
+ {
+ logger.warn(
+ "Failed to discover information about Jabber"
+ + " entity: " + entityID,
+ xmppe);
+ }
+ if ((discoverInfo != null)
+ && discoverInfo.containsFeature(
+ ColibriConferenceIQ.NAMESPACE))
+ {
+ return entityID;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Load jabber service class, their static context will register
+ * what is needed. Used in android as when using the other jars
+ * these services are loaded from the jar manifest.
+ */
+ private static void loadJabberServiceClasses()
+ {
+ if(!OSUtils.IS_ANDROID)
+ return;
+
+ try
+ {
+ // pre-configure smack in android
+ // just to load class to init their static blocks
+ SmackConfiguration.getVersion();
+ Class.forName(ServiceDiscoveryManager.class.getName());
+
+ Class.forName(DelayInformation.class.getName());
+ Class.forName(org.jivesoftware.smackx
+ .provider.DelayInformationProvider.class.getName());
+ Class.forName(org.jivesoftware.smackx
+ .bytestreams.socks5.Socks5BytestreamManager.class.getName());
+ Class.forName(XHTMLManager.class.getName());
+ Class.forName(org.jivesoftware.smackx
+ .bytestreams.ibb.InBandBytestreamManager.class.getName());
+
+ }
+ catch(ClassNotFoundException e)
+ {
+ logger.error("Error loading classes in smack", e);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriStreamConnector.java b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriStreamConnector.java index e91360c..4350dd3 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriStreamConnector.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriStreamConnector.java @@ -1,77 +1,77 @@ -/* - * 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.impl.protocol.jabber.extensions.colibri; - -import org.jitsi.service.neomedia.*; - -/** - * Implements a <tt>StreamConnector</tt> which allows sharing a specific - * <tt>StreamConnector</tt> instance among multiple <tt>TransportManager</tt>s - * for the purposes of the Jitsi VideoBridge. - * - * @author Lyubomir Marinov - */ -public class ColibriStreamConnector - extends StreamConnectorDelegate<StreamConnector> -{ - /** - * Initializes a new <tt>ColibriStreamConnector</tt> instance which is to - * share a specific <tt>StreamConnector</tt> instance among multiple - * <tt>TransportManager</tt>s for the purposes of the Jitsi VideoBridge. - * - * @param streamConnector the <tt>StreamConnector</tt> instance to be shared - * by the new instance among multiple <tt>TransportManager</tt>s for the - * purposes of the Jitsi VideoBridge - */ - public ColibriStreamConnector(StreamConnector streamConnector) - { - super(streamConnector); - } - - /** - * {@inheritDoc} - * - * Overrides {@link StreamConnectorDelegate#close()} in order to prevent the - * closing of the <tt>StreamConnector</tt> wrapped by this instance because - * the latter is shared and it is not clear whether no - * <tt>TransportManager</tt> is using it. - */ - @Override - public void close() - { - /* - * Do not close the shared StreamConnector because it is not clear - * whether no TransportManager is using it. - */ - } - - /** - * {@inheritDoc} - * - * Invokes {@link #close()} on this instance when it is clear that no - * <tt>TransportManager</tt> is using it in order to release the resources - * allocated by this instance throughout its life time (that need explicit - * disposal). - */ - @Override - protected void finalize() - throws Throwable - { - try - { - /* - * Close the shared StreamConnector because it is clear that no - * TrasportManager is using it. - */ - super.close(); - } - finally - { - super.finalize(); - } - } -} +/*
+ * 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.impl.protocol.jabber.extensions.colibri;
+
+import org.jitsi.service.neomedia.*;
+
+/**
+ * Implements a <tt>StreamConnector</tt> which allows sharing a specific
+ * <tt>StreamConnector</tt> instance among multiple <tt>TransportManager</tt>s
+ * for the purposes of the Jitsi Videobridge.
+ *
+ * @author Lyubomir Marinov
+ */
+public class ColibriStreamConnector
+ extends StreamConnectorDelegate<StreamConnector>
+{
+ /**
+ * Initializes a new <tt>ColibriStreamConnector</tt> instance which is to
+ * share a specific <tt>StreamConnector</tt> instance among multiple
+ * <tt>TransportManager</tt>s for the purposes of the Jitsi Videobridge.
+ *
+ * @param streamConnector the <tt>StreamConnector</tt> instance to be shared
+ * by the new instance among multiple <tt>TransportManager</tt>s for the
+ * purposes of the Jitsi Videobridge
+ */
+ public ColibriStreamConnector(StreamConnector streamConnector)
+ {
+ super(streamConnector);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Overrides {@link StreamConnectorDelegate#close()} in order to prevent the
+ * closing of the <tt>StreamConnector</tt> wrapped by this instance because
+ * the latter is shared and it is not clear whether no
+ * <tt>TransportManager</tt> is using it.
+ */
+ @Override
+ public void close()
+ {
+ /*
+ * Do not close the shared StreamConnector because it is not clear
+ * whether no TransportManager is using it.
+ */
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes {@link #close()} on this instance when it is clear that no
+ * <tt>TransportManager</tt> is using it in order to release the resources
+ * allocated by this instance throughout its life time (that need explicit
+ * disposal).
+ */
+ @Override
+ protected void finalize()
+ throws Throwable
+ {
+ try
+ {
+ /*
+ * Close the shared StreamConnector because it is clear that no
+ * TrasportManager is using it.
+ */
+ super.close();
+ }
+ finally
+ {
+ super.finalize();
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/CallConference.java b/src/net/java/sip/communicator/service/protocol/CallConference.java index bc0cc15..443aeb0 100644 --- a/src/net/java/sip/communicator/service/protocol/CallConference.java +++ b/src/net/java/sip/communicator/service/protocol/CallConference.java @@ -1,931 +1,931 @@ -/* - * 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.util.*; - -import net.java.sip.communicator.service.protocol.event.*; - -import org.jitsi.util.event.*; - -/** - * Represents the telephony conference-related state of a <tt>Call</tt>. - * Multiple <tt>Call</tt> instances share a single <tt>CallConference</tt> - * instance when the former are into a telephony conference i.e. the local - * peer/user is the conference focus. <tt>CallConference</tt> is - * protocol-agnostic and thus enables cross-protocol conferences. Since a - * non-conference <tt>Call</tt> may be converted into a conference <tt>Call</tt> - * at any time, every <tt>Call</tt> instance maintains a <tt>CallConference</tt> - * instance regardless of whether the <tt>Call</tt> in question is participating - * in a telephony conference. - * - * @author Lyubomir Marinov - */ -public class CallConference - extends PropertyChangeNotifier -{ - /** - * The name of the <tt>CallConference</tt> property which specifies the list - * of <tt>Call</tt>s participating in a telephony conference. A change in - * the value of the property is delivered in the form of a - * <tt>PropertyChangeEvent</tt> which has its <tt>oldValue</tt> or - * <tt>newValue</tt> set to the <tt>Call</tt> which has been removed or - * added to the list of <tt>Call</tt>s participating in the telephony - * conference. - */ - public static final String CALLS = "calls"; - - /** - * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in the telephony conference-related state of a specific - * <tt>Call</tt>. - * - * @param call the <tt>Call</tt> for which the number of <tt>CallPeer</tt>s - * associated with the <tt>Call</tt>s participating in its associated - * telephony conference-related state - * @return the number of <tt>CallPeer</tt>s associated with the - * <tt>Call</tt>s participating in the telephony conference-related state - * of the specified <tt>Call</tt> - */ - public static int getCallPeerCount(Call call) - { - CallConference conference = call.getConference(); - - /* - * A Call instance is supposed to always maintain a CallConference - * instance. Anyway, if it turns out that it is not the case, we will - * consider the Call as a representation of a telephony conference. - */ - return - (conference == null) - ? call.getCallPeerCount() - : conference.getCallPeerCount(); - } - - /** - * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in the telephony conference in which a specific - * <tt>Call</tt> is participating. - * - * @param call the <tt>Call</tt> which specifies the telephony conference - * the <tt>CallPeer</tt>s of which are to be retrieved - * @return a list of the <tt>CallPeer</tt>s associated with the - * <tt>Call</tt>s participating in the telephony conference in which the - * specified <tt>call</tt> is participating - */ - public static List<CallPeer> getCallPeers(Call call) - { - CallConference conference = call.getConference(); - List<CallPeer> callPeers = new ArrayList<CallPeer>(); - - if (conference == null) - { - Iterator<? extends CallPeer> callPeerIt = call.getCallPeers(); - - while (callPeerIt.hasNext()) - callPeers.add(callPeerIt.next()); - } - else - conference.getCallPeers(callPeers); - return callPeers; - } - - /** - * Gets the list of <tt>Call</tt>s participating in the telephony conference - * in which a specific <tt>Call</tt> is participating. - * - * @param call the <tt>Call</tt> which participates in the telephony - * conference the list of participating <tt>Call</tt>s of which is to be - * returned - * @return the list of <tt>Call</tt>s participating in the telephony - * conference in which the specified <tt>call</tt> is participating - */ - public static List<Call> getCalls(Call call) - { - CallConference conference = call.getConference(); - List<Call> calls; - - if (conference == null) - calls = Collections.emptyList(); - else - calls = conference.getCalls(); - return calls; - } - - /** - * Determines whether a <tt>CallConference</tt> is to report the local - * peer/user as a conference focus judging by a specific list of - * <tt>Call</tt>s. - * - * @param calls the list of <tt>Call</tt> which are to be judged whether - * the local peer/user that they represent is to be considered as a - * conference focus - * @return <tt>true</tt> if the local peer/user represented by the specified - * <tt>calls</tt> is judged to be a conference focus; otherwise, - * <tt>false</tt> - */ - private static boolean isConferenceFocus(List<Call> calls) - { - int callCount = calls.size(); - boolean conferenceFocus; - - if (callCount < 1) - conferenceFocus = false; - else if (callCount > 1) - conferenceFocus = true; - else - conferenceFocus = (calls.get(0).getCallPeerCount() > 1); - return conferenceFocus; - } - - /** - * The <tt>CallChangeListener</tt> which listens to changes in the - * <tt>Call</tt>s participating in this telephony conference. - */ - private final CallChangeListener callChangeListener - = new CallChangeListener() - { - public void callPeerAdded(CallPeerEvent ev) - { - CallConference.this.onCallPeerEvent(ev); - } - - public void callPeerRemoved(CallPeerEvent ev) - { - CallConference.this.onCallPeerEvent(ev); - } - - public void callStateChanged(CallChangeEvent ev) - { - CallConference.this.callStateChanged(ev); - } - }; - - /** - * The list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s - * participating in this telephony conference via - * {@link #addCallChangeListener(CallChangeListener)}. - */ - private final List<CallChangeListener> callChangeListeners - = new LinkedList<CallChangeListener>(); - - /** - * The <tt>CallPeerConferenceListener</tt> which listens to the - * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in - * this telephony conference. - */ - private final CallPeerConferenceListener callPeerConferenceListener - = new CallPeerConferenceAdapter() - { - /** - * {@inheritDoc} - * - * Invokes - * {@link CallConference#onCallPeerConferenceEvent( - * CallPeerConferenceEvent)}. - */ - @Override - protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) - { - CallConference.this.onCallPeerConferenceEvent(ev); - } - - /** - * {@inheritDoc} - * - * Invokes - * {@link CallConference#onCallPeerConferenceEvent( - * CallPeerConferenceEvent)}. - */ - @Override - public void conferenceMemberErrorReceived( - CallPeerConferenceEvent ev) - { - CallConference.this.onCallPeerConferenceEvent(ev); - } - }; - - /** - * The list of <tt>CallPeerConferenceListener</tt>s added to the - * <tt>CallPeer</tt>s associated with the <tt>CallPeer</tt>s participating - * in this telephony conference via - * {@link #addCallPeerConferenceListener}. - */ - private final List<CallPeerConferenceListener> callPeerConferenceListeners - = new LinkedList<CallPeerConferenceListener>(); - - /** - * The synchronization root/<tt>Object</tt> which protects the access to - * {@link #immutableCalls} and {@link #mutableCalls}. - */ - private final Object callsSyncRoot = new Object(); - - /** - * The indicator which determines whether the local peer represented by this - * instance and the <tt>Call</tt>s participating in it is acting as a - * conference focus. The SIP protocol, for example, will add the - * "isfocus" parameter to the Contact headers of its outgoing - * signaling if <tt>true</tt>. - */ - private boolean conferenceFocus = false; - - /** - * The list of <tt>Call</tt>s participating in this telephony conference as - * an immutable <tt>List</tt> which can be exposed out of this instance - * without the need to make a copy. In other words, it is an unmodifiable - * view of {@link #mutableCalls}. - */ - private List<Call> immutableCalls; - - /** - * The indicator which determines whether the telephony conference - * represented by this instance is utilizing the Jitsi VideoBridge - * server-side telephony conferencing technology. - */ - private final boolean jitsiVideoBridge; - - /** - * The list of <tt>Call</tt>s participating in this telephony conference as - * a mutable <tt>List</tt> which should not be exposed out of this instance. - */ - private List<Call> mutableCalls; - - /** - * Initializes a new <tt>CallConference</tt> instance. - */ - public CallConference() - { - this(false); - } - - /** - * Initializes a new <tt>CallConference</tt> instance which is to optionally - * utilize the Jitsi VideoBridge server-side telephony conferencing - * technology. - * - * @param jitsiVideoBridge <tt>true</tt> if the telephony conference - * represented by the new instance is to utilize the Jitsi VideoBridge - * server-side telephony conferencing technology; otherwise, <tt>false</tt> - */ - public CallConference(boolean jitsiVideoBridge) - { - this.jitsiVideoBridge = jitsiVideoBridge; - - mutableCalls = new ArrayList<Call>(); - immutableCalls = Collections.unmodifiableList(mutableCalls); - } - - /** - * Adds a specific <tt>Call</tt> to the list of <tt>Call</tt>s participating - * in this telephony conference. - * - * @param call the <tt>Call</tt> to add to the list of <tt>Call</tt>s - * participating in this telephony conference - * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this - * telephony conference changed as a result of the method call; otherwise, - * <tt>false</tt> - * @throws NullPointerException if <tt>call</tt> is <tt>null</tt> - */ - boolean addCall(Call call) - { - if (call == null) - throw new NullPointerException("call"); - - synchronized (callsSyncRoot) - { - if (mutableCalls.contains(call)) - return false; - - /* - * Implement the List of Calls participating in this telephony - * conference as a copy-on-write storage in order to optimize the - * getCalls method which is likely to be executed much more often - * than the addCall and removeCall methods. - */ - List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls); - - if (newMutableCalls.add(call)) - { - mutableCalls = newMutableCalls; - immutableCalls = Collections.unmodifiableList(mutableCalls); - } - else - return false; - } - - callAdded(call); - return true; - } - - /** - * Adds a <tt>CallChangeListener</tt> to the <tt>Call</tt>s participating in - * this telephony conference. The method is a convenience that takes on the - * responsibility of tracking the <tt>Call</tt>s that get added/removed - * to/from this telephony conference. - * - * @param listener the <tt>CallChangeListner</tt> to be added to the - * <tt>Call</tt>s participating in this telephony conference - * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> - */ - public void addCallChangeListener(CallChangeListener listener) - { - if (listener == null) - throw new NullPointerException("listener"); - else - { - synchronized (callChangeListeners) - { - if (!callChangeListeners.contains(listener)) - callChangeListeners.add(listener); - } - } - } - - /** - * Adds {@link #callPeerConferenceListener} to the <tt>CallPeer</tt>s - * associated with a specific <tt>Call</tt>. - * - * @param call the <tt>Call</tt> to whose associated <tt>CallPeer</tt>s - * <tt>callPeerConferenceListener</tt> is to be added - */ - private void addCallPeerConferenceListener(Call call) - { - Iterator<? extends CallPeer> callPeerIter = call.getCallPeers(); - - while (callPeerIter.hasNext()) - { - callPeerIter.next().addCallPeerConferenceListener( - callPeerConferenceListener); - } - } - - /** - * Adds a <tt>CallPeerConferenceListener</tt> to the <tt>CallPeer</tt>s - * associated with the <tt>Call</tt>s participating in this telephony - * conference. The method is a convenience that takes on the responsibility - * of tracking the <tt>Call</tt>s that get added/removed to/from this - * telephony conference and the <tt>CallPeer</tt> that get added/removed - * to/from these <tt>Call</tt>s. - * - * @param listener the <tt>CallPeerConferenceListener</tt> to be added to - * the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating - * in this telephony conference - * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> - */ - public void addCallPeerConferenceListener( - CallPeerConferenceListener listener) - { - if (listener == null) - throw new NullPointerException("listener"); - else - { - synchronized (callPeerConferenceListeners) - { - if (!callPeerConferenceListeners.contains(listener)) - callPeerConferenceListeners.add(listener); - } - } - } - - /** - * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has - * been added to the list of <tt>Call</tt>s participating in this telephony - * conference. - * - * @param call the <tt>Call</tt> which has been added to the list of - * <tt>Call</tt>s participating in this telephony conference - */ - protected void callAdded(Call call) - { - call.addCallChangeListener(callChangeListener); - addCallPeerConferenceListener(call); - - /* - * Update the conferenceFocus state. Because the public - * setConferenceFocus method allows forcing a specific value on the - * state in question and because it does not sound right to have the - * adding of a Call set conferenceFocus to false, only update it if the - * new conferenceFocus value is true, - */ - boolean conferenceFocus = isConferenceFocus(getCalls()); - - if (conferenceFocus) - setConferenceFocus(conferenceFocus); - - firePropertyChange(CALLS, null, call); - } - - /** - * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has - * been removed from the list of <tt>Call</tt>s participating in this - * telephony conference. - * - * @param call the <tt>Call</tt> which has been removed from the list of - * <tt>Call</tt>s participating in this telephony conference - */ - protected void callRemoved(Call call) - { - call.removeCallChangeListener(callChangeListener); - removeCallPeerConferenceListener(call); - - /* - * Update the conferenceFocus state. Following the line of thinking - * expressed in the callAdded method, only update it if the new - * conferenceFocus value is false. - */ - boolean conferenceFocus = isConferenceFocus(getCalls()); - - if (!conferenceFocus) - setConferenceFocus(conferenceFocus); - - firePropertyChange(CALLS, call, null); - } - - /** - * Notifies this telephony conference that the <tt>CallState</tt> of a - * <tt>Call</tt> has changed. - * - * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt> - * which had its <tt>CallState</tt> changed and the old and new - * <tt>CallState</tt>s of that <tt>Call</tt> - */ - private void callStateChanged(CallChangeEvent ev) - { - Call call = ev.getSourceCall(); - - if (containsCall(call)) - { - try - { - // Forward the CallChangeEvent to the callChangeListeners. - for (CallChangeListener l : getCallChangeListeners()) - l.callStateChanged(ev); - } - finally - { - if (CallChangeEvent.CALL_STATE_CHANGE - .equals(ev.getPropertyName()) - && CallState.CALL_ENDED.equals(ev.getNewValue())) - { - /* - * Should not be vital because Call will remove itself. - * Anyway, do it for the sake of completeness. - */ - removeCall(call); - } - } - } - } - - /** - * Notifies this <tt>CallConference</tt> that the value of its - * <tt>conferenceFocus</tt> property has changed from a specific old value - * to a specific new value. - * - * @param oldValue the value of the <tt>conferenceFocus</tt> property of - * this instance before the change - * @param newValue the value of the <tt>conferenceFocus</tt> property of - * this instance after the change - */ - protected void conferenceFocusChanged(boolean oldValue, boolean newValue) - { - firePropertyChange(Call.CONFERENCE_FOCUS, oldValue, newValue); - } - - /** - * Determines whether a specific <tt>Call</tt> is participating in this - * telephony conference. - * - * @param call the <tt>Call</tt> which is to be checked whether it is - * participating in this telephony conference - * @return <tt>true</tt> if the specified <tt>call</tt> is participating in - * this telephony conference - */ - public boolean containsCall(Call call) - { - synchronized (callsSyncRoot) - { - return mutableCalls.contains(call); - } - } - - /** - * Gets the list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s - * participating in this telephony conference via - * {@link #addCallChangeListener(CallChangeListener)}. - * - * @return the list of <tt>CallChangeListener</tt>s added to the - * <tt>Call</tt>s participating in this telephony conference via - * {@link #addCallChangeListener(CallChangeListener)} - */ - private CallChangeListener[] getCallChangeListeners() - { - synchronized (callChangeListeners) - { - return - callChangeListeners.toArray( - new CallChangeListener[callChangeListeners.size()]); - } - } - - /** - * Gets the number of <tt>Call</tt>s that are participating in this - * telephony conference. - * - * @return the number of <tt>Call</tt>s that are participating in this - * telephony conference - */ - public int getCallCount() - { - synchronized (callsSyncRoot) - { - return mutableCalls.size(); - } - } - - /** - * Gets the list of <tt>CallPeerConferenceListener</tt>s added to the - * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in - * this telephony conference via - * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}. - * - * @return the list of <tt>CallPeerConferenceListener</tt>s added to the - * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in - * this telephony conference via - * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)} - */ - private CallPeerConferenceListener[] getCallPeerConferenceListeners() - { - synchronized (callPeerConferenceListeners) - { - return - callPeerConferenceListeners.toArray( - new CallPeerConferenceListener[ - callPeerConferenceListeners.size()]); - } - } - - /** - * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in this telephony conference. - * - * @return the number of <tt>CallPeer</tt>s associated with the - * <tt>Call</tt>s participating in this telephony conference - */ - public int getCallPeerCount() - { - int callPeerCount = 0; - - for (Call call : getCalls()) - callPeerCount += call.getCallPeerCount(); - return callPeerCount; - } - - /** - * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in this telephony conference. - * - * @return a list of the <tt>CallPeer</tt>s associated with the - * <tt>Call</tt>s participating in this telephony conference - */ - public List<CallPeer> getCallPeers() - { - List<CallPeer> callPeers = new ArrayList<CallPeer>(); - - getCallPeers(callPeers); - return callPeers; - } - - /** - * Adds the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in this telephony conference into a specific <tt>List</tt>. - * - * @param callPeers a <tt>List</tt> into which the <tt>CallPeer</tt>s - * associated with the <tt>Call</tt>s participating in this telephony - * conference are to be added - */ - protected void getCallPeers(List<CallPeer> callPeers) - { - for (Call call : getCalls()) - { - Iterator<? extends CallPeer> callPeerIt = call.getCallPeers(); - - while (callPeerIt.hasNext()) - callPeers.add(callPeerIt.next()); - } - } - - /** - * Gets the list of <tt>Call</tt> participating in this telephony - * conference. - * - * @return the list of <tt>Call</tt>s participating in this telephony - * conference. An empty array of <tt>Call</tt> element type is returned if - * there are no <tt>Call</tt>s in this telephony conference-related state. - */ - public List<Call> getCalls() - { - synchronized (callsSyncRoot) - { - return immutableCalls; - } - } - - /** - * Determines whether the local peer/user associated with this instance and - * represented by the <tt>Call</tt>s participating into it is acting as a - * conference focus. - * - * @return <tt>true</tt> if the local peer/user associated by this instance - * is acting as a conference focus; otherwise, <tt>false</tt> - */ - public boolean isConferenceFocus() - { - return conferenceFocus; - } - - /** - * Determines whether the current state of this instance suggests that the - * telephony conference it represents has ended. Iterates over the - * <tt>Call</tt>s participating in this telephony conference and looks for a - * <tt>Call</tt> which is not in the {@link CallState#CALL_ENDED} state. - * - * @return <tt>true</tt> if the current state of this instance suggests that - * the telephony conference it represents has ended; otherwise, - * <tt>false</tt> - */ - public boolean isEnded() - { - for (Call call : getCalls()) - { - if (!CallState.CALL_ENDED.equals(call.getCallState())) - return false; - } - return true; - } - - /** - * Determines whether the telephony conference represented by this instance - * is utilizing the Jitsi VideoBridge server-side telephony conferencing - * technology. - * - * @return <tt>true</tt> if the telephony conference represented by this - * instance is utilizing the Jitsi VideoBridge server-side telephony - * conferencing technology - */ - public boolean isJitsiVideoBridge() - { - return jitsiVideoBridge; - } - - /** - * Notifies this telephony conference that a - * <tt>CallPeerConferenceEvent</tt> was fired by a <tt>CallPeer</tt> - * associated with a <tt>Call</tt> participating in this telephony - * conference. Forwards the specified <tt>CallPeerConferenceEvent</tt> to - * {@link #callPeerConferenceListeners}. - * - * @param ev the <tt>CallPeerConferenceEvent</tt> which was fired - */ - private void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) - { - int eventID = ev.getEventID(); - - for (CallPeerConferenceListener l : getCallPeerConferenceListeners()) - { - switch (eventID) - { - case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED: - l.conferenceFocusChanged(ev); - break; - case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED: - l.conferenceMemberAdded(ev); - break; - case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED: - l.conferenceMemberRemoved(ev); - break; - case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED: - l.conferenceMemberErrorReceived(ev); - break; - default: - throw new UnsupportedOperationException( - "Unsupported CallPeerConferenceEvent eventID."); - } - } - } - - /** - * Notifies this telephony conference about a specific - * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was either added to - * or removed from a <tt>Call</tt>. - * - * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt> - * which was added or removed and the <tt>Call</tt> to which it was added or - * from which is was removed - */ - private void onCallPeerEvent(CallPeerEvent ev) - { - Call call = ev.getSourceCall(); - - if (containsCall(call)) - { - /* - * Update the conferenceFocus state. Following the line of thinking - * expressed in the callAdded and callRemoved methods, only update - * it if the new conferenceFocus value is in accord with the - * expectations. - */ - int eventID = ev.getEventID(); - boolean conferenceFocus = isConferenceFocus(getCalls()); - - switch (eventID) - { - case CallPeerEvent.CALL_PEER_ADDED: - if (conferenceFocus) - setConferenceFocus(conferenceFocus); - break; - case CallPeerEvent.CALL_PEER_REMOVED: - if (!conferenceFocus) - setConferenceFocus(conferenceFocus); - break; - default: - /* - * We're interested in the adding and removing of CallPeers - * only. - */ - break; - } - - try - { - // Forward the CallPeerEvent to the callChangeListeners. - for (CallChangeListener l : getCallChangeListeners()) - { - switch (eventID) - { - case CallPeerEvent.CALL_PEER_ADDED: - l.callPeerAdded(ev); - break; - case CallPeerEvent.CALL_PEER_REMOVED: - l.callPeerRemoved(ev); - break; - default: - break; - } - } - } - finally - { - /* - * Add/remove the callPeerConferenceListener to/from the source - * CallPeer (for the purposes of the - * addCallPeerConferenceListener method of this CallConference). - */ - CallPeer callPeer = ev.getSourceCallPeer(); - - switch (eventID) - { - case CallPeerEvent.CALL_PEER_ADDED: - callPeer.addCallPeerConferenceListener( - callPeerConferenceListener); - break; - case CallPeerEvent.CALL_PEER_REMOVED: - callPeer.removeCallPeerConferenceListener( - callPeerConferenceListener); - break; - default: - break; - } - } - } - } - - /** - * Removes a specific <tt>Call</tt> from the list of <tt>Call</tt>s - * participating in this telephony conference. - * - * @param call the <tt>Call</tt> to remove from the list of <tt>Call</tt>s - * participating in this telephony conference - * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this - * telephony conference changed as a result of the method call; otherwise, - * <tt>false</tt> - */ - boolean removeCall(Call call) - { - if (call == null) - return false; - - synchronized (callsSyncRoot) - { - if (!mutableCalls.contains(call)) - return false; - - /* - * Implement the List of Calls participating in this telephony - * conference as a copy-on-write storage in order to optimize the - * getCalls method which is likely to be executed much more often - * than the addCall and removeCall methods. - */ - List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls); - - if (newMutableCalls.remove(call)) - { - mutableCalls = newMutableCalls; - immutableCalls = Collections.unmodifiableList(mutableCalls); - } - else - return false; - } - - callRemoved(call); - return true; - } - - /** - * Removes a <tt>CallChangeListener</tt> from the <tt>Call</tt>s - * participating in this telephony conference. - * - * @param listener the <tt>CallChangeListener</tt> to be removed from the - * <tt>Call</tt>s participating in this telephony conference - * @see #addCallChangeListener(CallChangeListener) - */ - public void removeCallChangeListener(CallChangeListener listener) - { - if (listener != null) - { - synchronized (callChangeListeners) - { - callChangeListeners.remove(listener); - } - } - } - - /** - * Removes {@link #callPeerConferenceListener} from the <tt>CallPeer</tt>s - * associated with a specific <tt>Call</tt>. - * - * @param call the <tt>Call</tt> from whose associated <tt>CallPeer</tt>s - * <tt>callPeerConferenceListener</tt> is to be removed - */ - private void removeCallPeerConferenceListener(Call call) - { - Iterator<? extends CallPeer> callPeerIter = call.getCallPeers(); - - while (callPeerIter.hasNext()) - { - callPeerIter.next().removeCallPeerConferenceListener( - callPeerConferenceListener); - } - } - - /** - * Removes a <tt>CallPeerConferenceListener</tt> from the <tt>CallPeer</tt>s - * associated with the <tt>Call</tt>s participating in this telephony - * conference. - * - * @param listener the <tt>CallPeerConferenceListener</tt> to be removed - * from the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s - * participating in this telephony conference - * @see #addCallPeerConferenceListener(CallPeerConferenceListener) - */ - public void removeCallPeerConferenceListener( - CallPeerConferenceListener listener) - { - if (listener != null) - { - synchronized (callPeerConferenceListeners) - { - callPeerConferenceListeners.remove(listener); - } - } - } - - /** - * Sets the indicator which determines whether the local peer represented by - * this instance and the <tt>Call</tt>s participating in it is acting as a - * conference focus (and thus may, for example, need to send the - * corresponding parameters in its outgoing signaling). - * - * @param conferenceFocus <tt>true</tt> if the local peer represented by - * this instance and the <tt>Call</tt>s participating in it is to act as a - * conference focus; otherwise, <tt>false</tt> - */ - public void setConferenceFocus(boolean conferenceFocus) - { - if (this.conferenceFocus != conferenceFocus) - { - boolean oldValue = isConferenceFocus(); - - this.conferenceFocus = conferenceFocus; - - boolean newValue = isConferenceFocus(); - - if (oldValue != newValue) - conferenceFocusChanged(oldValue, newValue); - } - } -} +/*
+ * 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.util.*;
+
+import net.java.sip.communicator.service.protocol.event.*;
+
+import org.jitsi.util.event.*;
+
+/**
+ * Represents the telephony conference-related state of a <tt>Call</tt>.
+ * Multiple <tt>Call</tt> instances share a single <tt>CallConference</tt>
+ * instance when the former are into a telephony conference i.e. the local
+ * peer/user is the conference focus. <tt>CallConference</tt> is
+ * protocol-agnostic and thus enables cross-protocol conferences. Since a
+ * non-conference <tt>Call</tt> may be converted into a conference <tt>Call</tt>
+ * at any time, every <tt>Call</tt> instance maintains a <tt>CallConference</tt>
+ * instance regardless of whether the <tt>Call</tt> in question is participating
+ * in a telephony conference.
+ *
+ * @author Lyubomir Marinov
+ */
+public class CallConference
+ extends PropertyChangeNotifier
+{
+ /**
+ * The name of the <tt>CallConference</tt> property which specifies the list
+ * of <tt>Call</tt>s participating in a telephony conference. A change in
+ * the value of the property is delivered in the form of a
+ * <tt>PropertyChangeEvent</tt> which has its <tt>oldValue</tt> or
+ * <tt>newValue</tt> set to the <tt>Call</tt> which has been removed or
+ * added to the list of <tt>Call</tt>s participating in the telephony
+ * conference.
+ */
+ public static final String CALLS = "calls";
+
+ /**
+ * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in the telephony conference-related state of a specific
+ * <tt>Call</tt>.
+ *
+ * @param call the <tt>Call</tt> for which the number of <tt>CallPeer</tt>s
+ * associated with the <tt>Call</tt>s participating in its associated
+ * telephony conference-related state
+ * @return the number of <tt>CallPeer</tt>s associated with the
+ * <tt>Call</tt>s participating in the telephony conference-related state
+ * of the specified <tt>Call</tt>
+ */
+ public static int getCallPeerCount(Call call)
+ {
+ CallConference conference = call.getConference();
+
+ /*
+ * A Call instance is supposed to always maintain a CallConference
+ * instance. Anyway, if it turns out that it is not the case, we will
+ * consider the Call as a representation of a telephony conference.
+ */
+ return
+ (conference == null)
+ ? call.getCallPeerCount()
+ : conference.getCallPeerCount();
+ }
+
+ /**
+ * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in the telephony conference in which a specific
+ * <tt>Call</tt> is participating.
+ *
+ * @param call the <tt>Call</tt> which specifies the telephony conference
+ * the <tt>CallPeer</tt>s of which are to be retrieved
+ * @return a list of the <tt>CallPeer</tt>s associated with the
+ * <tt>Call</tt>s participating in the telephony conference in which the
+ * specified <tt>call</tt> is participating
+ */
+ public static List<CallPeer> getCallPeers(Call call)
+ {
+ CallConference conference = call.getConference();
+ List<CallPeer> callPeers = new ArrayList<CallPeer>();
+
+ if (conference == null)
+ {
+ Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();
+
+ while (callPeerIt.hasNext())
+ callPeers.add(callPeerIt.next());
+ }
+ else
+ conference.getCallPeers(callPeers);
+ return callPeers;
+ }
+
+ /**
+ * Gets the list of <tt>Call</tt>s participating in the telephony conference
+ * in which a specific <tt>Call</tt> is participating.
+ *
+ * @param call the <tt>Call</tt> which participates in the telephony
+ * conference the list of participating <tt>Call</tt>s of which is to be
+ * returned
+ * @return the list of <tt>Call</tt>s participating in the telephony
+ * conference in which the specified <tt>call</tt> is participating
+ */
+ public static List<Call> getCalls(Call call)
+ {
+ CallConference conference = call.getConference();
+ List<Call> calls;
+
+ if (conference == null)
+ calls = Collections.emptyList();
+ else
+ calls = conference.getCalls();
+ return calls;
+ }
+
+ /**
+ * Determines whether a <tt>CallConference</tt> is to report the local
+ * peer/user as a conference focus judging by a specific list of
+ * <tt>Call</tt>s.
+ *
+ * @param calls the list of <tt>Call</tt> which are to be judged whether
+ * the local peer/user that they represent is to be considered as a
+ * conference focus
+ * @return <tt>true</tt> if the local peer/user represented by the specified
+ * <tt>calls</tt> is judged to be a conference focus; otherwise,
+ * <tt>false</tt>
+ */
+ private static boolean isConferenceFocus(List<Call> calls)
+ {
+ int callCount = calls.size();
+ boolean conferenceFocus;
+
+ if (callCount < 1)
+ conferenceFocus = false;
+ else if (callCount > 1)
+ conferenceFocus = true;
+ else
+ conferenceFocus = (calls.get(0).getCallPeerCount() > 1);
+ return conferenceFocus;
+ }
+
+ /**
+ * The <tt>CallChangeListener</tt> which listens to changes in the
+ * <tt>Call</tt>s participating in this telephony conference.
+ */
+ private final CallChangeListener callChangeListener
+ = new CallChangeListener()
+ {
+ public void callPeerAdded(CallPeerEvent ev)
+ {
+ CallConference.this.onCallPeerEvent(ev);
+ }
+
+ public void callPeerRemoved(CallPeerEvent ev)
+ {
+ CallConference.this.onCallPeerEvent(ev);
+ }
+
+ public void callStateChanged(CallChangeEvent ev)
+ {
+ CallConference.this.callStateChanged(ev);
+ }
+ };
+
+ /**
+ * The list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
+ * participating in this telephony conference via
+ * {@link #addCallChangeListener(CallChangeListener)}.
+ */
+ private final List<CallChangeListener> callChangeListeners
+ = new LinkedList<CallChangeListener>();
+
+ /**
+ * The <tt>CallPeerConferenceListener</tt> which listens to the
+ * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
+ * this telephony conference.
+ */
+ private final CallPeerConferenceListener callPeerConferenceListener
+ = new CallPeerConferenceAdapter()
+ {
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes
+ * {@link CallConference#onCallPeerConferenceEvent(
+ * CallPeerConferenceEvent)}.
+ */
+ @Override
+ protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
+ {
+ CallConference.this.onCallPeerConferenceEvent(ev);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Invokes
+ * {@link CallConference#onCallPeerConferenceEvent(
+ * CallPeerConferenceEvent)}.
+ */
+ @Override
+ public void conferenceMemberErrorReceived(
+ CallPeerConferenceEvent ev)
+ {
+ CallConference.this.onCallPeerConferenceEvent(ev);
+ }
+ };
+
+ /**
+ * The list of <tt>CallPeerConferenceListener</tt>s added to the
+ * <tt>CallPeer</tt>s associated with the <tt>CallPeer</tt>s participating
+ * in this telephony conference via
+ * {@link #addCallPeerConferenceListener}.
+ */
+ private final List<CallPeerConferenceListener> callPeerConferenceListeners
+ = new LinkedList<CallPeerConferenceListener>();
+
+ /**
+ * The synchronization root/<tt>Object</tt> which protects the access to
+ * {@link #immutableCalls} and {@link #mutableCalls}.
+ */
+ private final Object callsSyncRoot = new Object();
+
+ /**
+ * The indicator which determines whether the local peer represented by this
+ * instance and the <tt>Call</tt>s participating in it is acting as a
+ * conference focus. The SIP protocol, for example, will add the
+ * "isfocus" parameter to the Contact headers of its outgoing
+ * signaling if <tt>true</tt>.
+ */
+ private boolean conferenceFocus = false;
+
+ /**
+ * The list of <tt>Call</tt>s participating in this telephony conference as
+ * an immutable <tt>List</tt> which can be exposed out of this instance
+ * without the need to make a copy. In other words, it is an unmodifiable
+ * view of {@link #mutableCalls}.
+ */
+ private List<Call> immutableCalls;
+
+ /**
+ * The indicator which determines whether the telephony conference
+ * represented by this instance is utilizing the Jitsi Videobridge
+ * server-side telephony conferencing technology.
+ */
+ private final boolean jitsiVideobridge;
+
+ /**
+ * The list of <tt>Call</tt>s participating in this telephony conference as
+ * a mutable <tt>List</tt> which should not be exposed out of this instance.
+ */
+ private List<Call> mutableCalls;
+
+ /**
+ * Initializes a new <tt>CallConference</tt> instance.
+ */
+ public CallConference()
+ {
+ this(false);
+ }
+
+ /**
+ * Initializes a new <tt>CallConference</tt> instance which is to optionally
+ * utilize the Jitsi Videobridge server-side telephony conferencing
+ * technology.
+ *
+ * @param jitsiVideobridge <tt>true</tt> if the telephony conference
+ * represented by the new instance is to utilize the Jitsi Videobridge
+ * server-side telephony conferencing technology; otherwise, <tt>false</tt>
+ */
+ public CallConference(boolean jitsiVideobridge)
+ {
+ this.jitsiVideobridge = jitsiVideobridge;
+
+ mutableCalls = new ArrayList<Call>();
+ immutableCalls = Collections.unmodifiableList(mutableCalls);
+ }
+
+ /**
+ * Adds a specific <tt>Call</tt> to the list of <tt>Call</tt>s participating
+ * in this telephony conference.
+ *
+ * @param call the <tt>Call</tt> to add to the list of <tt>Call</tt>s
+ * participating in this telephony conference
+ * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
+ * telephony conference changed as a result of the method call; otherwise,
+ * <tt>false</tt>
+ * @throws NullPointerException if <tt>call</tt> is <tt>null</tt>
+ */
+ boolean addCall(Call call)
+ {
+ if (call == null)
+ throw new NullPointerException("call");
+
+ synchronized (callsSyncRoot)
+ {
+ if (mutableCalls.contains(call))
+ return false;
+
+ /*
+ * Implement the List of Calls participating in this telephony
+ * conference as a copy-on-write storage in order to optimize the
+ * getCalls method which is likely to be executed much more often
+ * than the addCall and removeCall methods.
+ */
+ List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);
+
+ if (newMutableCalls.add(call))
+ {
+ mutableCalls = newMutableCalls;
+ immutableCalls = Collections.unmodifiableList(mutableCalls);
+ }
+ else
+ return false;
+ }
+
+ callAdded(call);
+ return true;
+ }
+
+ /**
+ * Adds a <tt>CallChangeListener</tt> to the <tt>Call</tt>s participating in
+ * this telephony conference. The method is a convenience that takes on the
+ * responsibility of tracking the <tt>Call</tt>s that get added/removed
+ * to/from this telephony conference.
+ *
+ * @param listener the <tt>CallChangeListner</tt> to be added to the
+ * <tt>Call</tt>s participating in this telephony conference
+ * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
+ */
+ public void addCallChangeListener(CallChangeListener listener)
+ {
+ if (listener == null)
+ throw new NullPointerException("listener");
+ else
+ {
+ synchronized (callChangeListeners)
+ {
+ if (!callChangeListeners.contains(listener))
+ callChangeListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Adds {@link #callPeerConferenceListener} to the <tt>CallPeer</tt>s
+ * associated with a specific <tt>Call</tt>.
+ *
+ * @param call the <tt>Call</tt> to whose associated <tt>CallPeer</tt>s
+ * <tt>callPeerConferenceListener</tt> is to be added
+ */
+ private void addCallPeerConferenceListener(Call call)
+ {
+ Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();
+
+ while (callPeerIter.hasNext())
+ {
+ callPeerIter.next().addCallPeerConferenceListener(
+ callPeerConferenceListener);
+ }
+ }
+
+ /**
+ * Adds a <tt>CallPeerConferenceListener</tt> to the <tt>CallPeer</tt>s
+ * associated with the <tt>Call</tt>s participating in this telephony
+ * conference. The method is a convenience that takes on the responsibility
+ * of tracking the <tt>Call</tt>s that get added/removed to/from this
+ * telephony conference and the <tt>CallPeer</tt> that get added/removed
+ * to/from these <tt>Call</tt>s.
+ *
+ * @param listener the <tt>CallPeerConferenceListener</tt> to be added to
+ * the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating
+ * in this telephony conference
+ * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
+ */
+ public void addCallPeerConferenceListener(
+ CallPeerConferenceListener listener)
+ {
+ if (listener == null)
+ throw new NullPointerException("listener");
+ else
+ {
+ synchronized (callPeerConferenceListeners)
+ {
+ if (!callPeerConferenceListeners.contains(listener))
+ callPeerConferenceListeners.add(listener);
+ }
+ }
+ }
+
+ /**
+ * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
+ * been added to the list of <tt>Call</tt>s participating in this telephony
+ * conference.
+ *
+ * @param call the <tt>Call</tt> which has been added to the list of
+ * <tt>Call</tt>s participating in this telephony conference
+ */
+ protected void callAdded(Call call)
+ {
+ call.addCallChangeListener(callChangeListener);
+ addCallPeerConferenceListener(call);
+
+ /*
+ * Update the conferenceFocus state. Because the public
+ * setConferenceFocus method allows forcing a specific value on the
+ * state in question and because it does not sound right to have the
+ * adding of a Call set conferenceFocus to false, only update it if the
+ * new conferenceFocus value is true,
+ */
+ boolean conferenceFocus = isConferenceFocus(getCalls());
+
+ if (conferenceFocus)
+ setConferenceFocus(conferenceFocus);
+
+ firePropertyChange(CALLS, null, call);
+ }
+
+ /**
+ * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
+ * been removed from the list of <tt>Call</tt>s participating in this
+ * telephony conference.
+ *
+ * @param call the <tt>Call</tt> which has been removed from the list of
+ * <tt>Call</tt>s participating in this telephony conference
+ */
+ protected void callRemoved(Call call)
+ {
+ call.removeCallChangeListener(callChangeListener);
+ removeCallPeerConferenceListener(call);
+
+ /*
+ * Update the conferenceFocus state. Following the line of thinking
+ * expressed in the callAdded method, only update it if the new
+ * conferenceFocus value is false.
+ */
+ boolean conferenceFocus = isConferenceFocus(getCalls());
+
+ if (!conferenceFocus)
+ setConferenceFocus(conferenceFocus);
+
+ firePropertyChange(CALLS, call, null);
+ }
+
+ /**
+ * Notifies this telephony conference that the <tt>CallState</tt> of a
+ * <tt>Call</tt> has changed.
+ *
+ * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt>
+ * which had its <tt>CallState</tt> changed and the old and new
+ * <tt>CallState</tt>s of that <tt>Call</tt>
+ */
+ private void callStateChanged(CallChangeEvent ev)
+ {
+ Call call = ev.getSourceCall();
+
+ if (containsCall(call))
+ {
+ try
+ {
+ // Forward the CallChangeEvent to the callChangeListeners.
+ for (CallChangeListener l : getCallChangeListeners())
+ l.callStateChanged(ev);
+ }
+ finally
+ {
+ if (CallChangeEvent.CALL_STATE_CHANGE
+ .equals(ev.getPropertyName())
+ && CallState.CALL_ENDED.equals(ev.getNewValue()))
+ {
+ /*
+ * Should not be vital because Call will remove itself.
+ * Anyway, do it for the sake of completeness.
+ */
+ removeCall(call);
+ }
+ }
+ }
+ }
+
+ /**
+ * Notifies this <tt>CallConference</tt> that the value of its
+ * <tt>conferenceFocus</tt> property has changed from a specific old value
+ * to a specific new value.
+ *
+ * @param oldValue the value of the <tt>conferenceFocus</tt> property of
+ * this instance before the change
+ * @param newValue the value of the <tt>conferenceFocus</tt> property of
+ * this instance after the change
+ */
+ protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
+ {
+ firePropertyChange(Call.CONFERENCE_FOCUS, oldValue, newValue);
+ }
+
+ /**
+ * Determines whether a specific <tt>Call</tt> is participating in this
+ * telephony conference.
+ *
+ * @param call the <tt>Call</tt> which is to be checked whether it is
+ * participating in this telephony conference
+ * @return <tt>true</tt> if the specified <tt>call</tt> is participating in
+ * this telephony conference
+ */
+ public boolean containsCall(Call call)
+ {
+ synchronized (callsSyncRoot)
+ {
+ return mutableCalls.contains(call);
+ }
+ }
+
+ /**
+ * Gets the list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
+ * participating in this telephony conference via
+ * {@link #addCallChangeListener(CallChangeListener)}.
+ *
+ * @return the list of <tt>CallChangeListener</tt>s added to the
+ * <tt>Call</tt>s participating in this telephony conference via
+ * {@link #addCallChangeListener(CallChangeListener)}
+ */
+ private CallChangeListener[] getCallChangeListeners()
+ {
+ synchronized (callChangeListeners)
+ {
+ return
+ callChangeListeners.toArray(
+ new CallChangeListener[callChangeListeners.size()]);
+ }
+ }
+
+ /**
+ * Gets the number of <tt>Call</tt>s that are participating in this
+ * telephony conference.
+ *
+ * @return the number of <tt>Call</tt>s that are participating in this
+ * telephony conference
+ */
+ public int getCallCount()
+ {
+ synchronized (callsSyncRoot)
+ {
+ return mutableCalls.size();
+ }
+ }
+
+ /**
+ * Gets the list of <tt>CallPeerConferenceListener</tt>s added to the
+ * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
+ * this telephony conference via
+ * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}.
+ *
+ * @return the list of <tt>CallPeerConferenceListener</tt>s added to the
+ * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
+ * this telephony conference via
+ * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}
+ */
+ private CallPeerConferenceListener[] getCallPeerConferenceListeners()
+ {
+ synchronized (callPeerConferenceListeners)
+ {
+ return
+ callPeerConferenceListeners.toArray(
+ new CallPeerConferenceListener[
+ callPeerConferenceListeners.size()]);
+ }
+ }
+
+ /**
+ * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in this telephony conference.
+ *
+ * @return the number of <tt>CallPeer</tt>s associated with the
+ * <tt>Call</tt>s participating in this telephony conference
+ */
+ public int getCallPeerCount()
+ {
+ int callPeerCount = 0;
+
+ for (Call call : getCalls())
+ callPeerCount += call.getCallPeerCount();
+ return callPeerCount;
+ }
+
+ /**
+ * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in this telephony conference.
+ *
+ * @return a list of the <tt>CallPeer</tt>s associated with the
+ * <tt>Call</tt>s participating in this telephony conference
+ */
+ public List<CallPeer> getCallPeers()
+ {
+ List<CallPeer> callPeers = new ArrayList<CallPeer>();
+
+ getCallPeers(callPeers);
+ return callPeers;
+ }
+
+ /**
+ * Adds the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in this telephony conference into a specific <tt>List</tt>.
+ *
+ * @param callPeers a <tt>List</tt> into which the <tt>CallPeer</tt>s
+ * associated with the <tt>Call</tt>s participating in this telephony
+ * conference are to be added
+ */
+ protected void getCallPeers(List<CallPeer> callPeers)
+ {
+ for (Call call : getCalls())
+ {
+ Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();
+
+ while (callPeerIt.hasNext())
+ callPeers.add(callPeerIt.next());
+ }
+ }
+
+ /**
+ * Gets the list of <tt>Call</tt> participating in this telephony
+ * conference.
+ *
+ * @return the list of <tt>Call</tt>s participating in this telephony
+ * conference. An empty array of <tt>Call</tt> element type is returned if
+ * there are no <tt>Call</tt>s in this telephony conference-related state.
+ */
+ public List<Call> getCalls()
+ {
+ synchronized (callsSyncRoot)
+ {
+ return immutableCalls;
+ }
+ }
+
+ /**
+ * Determines whether the local peer/user associated with this instance and
+ * represented by the <tt>Call</tt>s participating into it is acting as a
+ * conference focus.
+ *
+ * @return <tt>true</tt> if the local peer/user associated by this instance
+ * is acting as a conference focus; otherwise, <tt>false</tt>
+ */
+ public boolean isConferenceFocus()
+ {
+ return conferenceFocus;
+ }
+
+ /**
+ * Determines whether the current state of this instance suggests that the
+ * telephony conference it represents has ended. Iterates over the
+ * <tt>Call</tt>s participating in this telephony conference and looks for a
+ * <tt>Call</tt> which is not in the {@link CallState#CALL_ENDED} state.
+ *
+ * @return <tt>true</tt> if the current state of this instance suggests that
+ * the telephony conference it represents has ended; otherwise,
+ * <tt>false</tt>
+ */
+ public boolean isEnded()
+ {
+ for (Call call : getCalls())
+ {
+ if (!CallState.CALL_ENDED.equals(call.getCallState()))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether the telephony conference represented by this instance
+ * is utilizing the Jitsi Videobridge server-side telephony conferencing
+ * technology.
+ *
+ * @return <tt>true</tt> if the telephony conference represented by this
+ * instance is utilizing the Jitsi Videobridge server-side telephony
+ * conferencing technology
+ */
+ public boolean isJitsiVideobridge()
+ {
+ return jitsiVideobridge;
+ }
+
+ /**
+ * Notifies this telephony conference that a
+ * <tt>CallPeerConferenceEvent</tt> was fired by a <tt>CallPeer</tt>
+ * associated with a <tt>Call</tt> participating in this telephony
+ * conference. Forwards the specified <tt>CallPeerConferenceEvent</tt> to
+ * {@link #callPeerConferenceListeners}.
+ *
+ * @param ev the <tt>CallPeerConferenceEvent</tt> which was fired
+ */
+ private void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
+ {
+ int eventID = ev.getEventID();
+
+ for (CallPeerConferenceListener l : getCallPeerConferenceListeners())
+ {
+ switch (eventID)
+ {
+ case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED:
+ l.conferenceFocusChanged(ev);
+ break;
+ case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED:
+ l.conferenceMemberAdded(ev);
+ break;
+ case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED:
+ l.conferenceMemberRemoved(ev);
+ break;
+ case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED:
+ l.conferenceMemberErrorReceived(ev);
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported CallPeerConferenceEvent eventID.");
+ }
+ }
+ }
+
+ /**
+ * Notifies this telephony conference about a specific
+ * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was either added to
+ * or removed from a <tt>Call</tt>.
+ *
+ * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt>
+ * which was added or removed and the <tt>Call</tt> to which it was added or
+ * from which is was removed
+ */
+ private void onCallPeerEvent(CallPeerEvent ev)
+ {
+ Call call = ev.getSourceCall();
+
+ if (containsCall(call))
+ {
+ /*
+ * Update the conferenceFocus state. Following the line of thinking
+ * expressed in the callAdded and callRemoved methods, only update
+ * it if the new conferenceFocus value is in accord with the
+ * expectations.
+ */
+ int eventID = ev.getEventID();
+ boolean conferenceFocus = isConferenceFocus(getCalls());
+
+ switch (eventID)
+ {
+ case CallPeerEvent.CALL_PEER_ADDED:
+ if (conferenceFocus)
+ setConferenceFocus(conferenceFocus);
+ break;
+ case CallPeerEvent.CALL_PEER_REMOVED:
+ if (!conferenceFocus)
+ setConferenceFocus(conferenceFocus);
+ break;
+ default:
+ /*
+ * We're interested in the adding and removing of CallPeers
+ * only.
+ */
+ break;
+ }
+
+ try
+ {
+ // Forward the CallPeerEvent to the callChangeListeners.
+ for (CallChangeListener l : getCallChangeListeners())
+ {
+ switch (eventID)
+ {
+ case CallPeerEvent.CALL_PEER_ADDED:
+ l.callPeerAdded(ev);
+ break;
+ case CallPeerEvent.CALL_PEER_REMOVED:
+ l.callPeerRemoved(ev);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ finally
+ {
+ /*
+ * Add/remove the callPeerConferenceListener to/from the source
+ * CallPeer (for the purposes of the
+ * addCallPeerConferenceListener method of this CallConference).
+ */
+ CallPeer callPeer = ev.getSourceCallPeer();
+
+ switch (eventID)
+ {
+ case CallPeerEvent.CALL_PEER_ADDED:
+ callPeer.addCallPeerConferenceListener(
+ callPeerConferenceListener);
+ break;
+ case CallPeerEvent.CALL_PEER_REMOVED:
+ callPeer.removeCallPeerConferenceListener(
+ callPeerConferenceListener);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a specific <tt>Call</tt> from the list of <tt>Call</tt>s
+ * participating in this telephony conference.
+ *
+ * @param call the <tt>Call</tt> to remove from the list of <tt>Call</tt>s
+ * participating in this telephony conference
+ * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
+ * telephony conference changed as a result of the method call; otherwise,
+ * <tt>false</tt>
+ */
+ boolean removeCall(Call call)
+ {
+ if (call == null)
+ return false;
+
+ synchronized (callsSyncRoot)
+ {
+ if (!mutableCalls.contains(call))
+ return false;
+
+ /*
+ * Implement the List of Calls participating in this telephony
+ * conference as a copy-on-write storage in order to optimize the
+ * getCalls method which is likely to be executed much more often
+ * than the addCall and removeCall methods.
+ */
+ List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);
+
+ if (newMutableCalls.remove(call))
+ {
+ mutableCalls = newMutableCalls;
+ immutableCalls = Collections.unmodifiableList(mutableCalls);
+ }
+ else
+ return false;
+ }
+
+ callRemoved(call);
+ return true;
+ }
+
+ /**
+ * Removes a <tt>CallChangeListener</tt> from the <tt>Call</tt>s
+ * participating in this telephony conference.
+ *
+ * @param listener the <tt>CallChangeListener</tt> to be removed from the
+ * <tt>Call</tt>s participating in this telephony conference
+ * @see #addCallChangeListener(CallChangeListener)
+ */
+ public void removeCallChangeListener(CallChangeListener listener)
+ {
+ if (listener != null)
+ {
+ synchronized (callChangeListeners)
+ {
+ callChangeListeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Removes {@link #callPeerConferenceListener} from the <tt>CallPeer</tt>s
+ * associated with a specific <tt>Call</tt>.
+ *
+ * @param call the <tt>Call</tt> from whose associated <tt>CallPeer</tt>s
+ * <tt>callPeerConferenceListener</tt> is to be removed
+ */
+ private void removeCallPeerConferenceListener(Call call)
+ {
+ Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();
+
+ while (callPeerIter.hasNext())
+ {
+ callPeerIter.next().removeCallPeerConferenceListener(
+ callPeerConferenceListener);
+ }
+ }
+
+ /**
+ * Removes a <tt>CallPeerConferenceListener</tt> from the <tt>CallPeer</tt>s
+ * associated with the <tt>Call</tt>s participating in this telephony
+ * conference.
+ *
+ * @param listener the <tt>CallPeerConferenceListener</tt> to be removed
+ * from the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
+ * participating in this telephony conference
+ * @see #addCallPeerConferenceListener(CallPeerConferenceListener)
+ */
+ public void removeCallPeerConferenceListener(
+ CallPeerConferenceListener listener)
+ {
+ if (listener != null)
+ {
+ synchronized (callPeerConferenceListeners)
+ {
+ callPeerConferenceListeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Sets the indicator which determines whether the local peer represented by
+ * this instance and the <tt>Call</tt>s participating in it is acting as a
+ * conference focus (and thus may, for example, need to send the
+ * corresponding parameters in its outgoing signaling).
+ *
+ * @param conferenceFocus <tt>true</tt> if the local peer represented by
+ * this instance and the <tt>Call</tt>s participating in it is to act as a
+ * conference focus; otherwise, <tt>false</tt>
+ */
+ public void setConferenceFocus(boolean conferenceFocus)
+ {
+ if (this.conferenceFocus != conferenceFocus)
+ {
+ boolean oldValue = isConferenceFocus();
+
+ this.conferenceFocus = conferenceFocus;
+
+ boolean newValue = isConferenceFocus();
+
+ if (oldValue != newValue)
+ conferenceFocusChanged(oldValue, newValue);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java index 010dc16..0530b35 100644 --- a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java +++ b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java @@ -1,1259 +1,1259 @@ -/* - * 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.lang.reflect.*; -import java.util.*; - -import net.java.sip.communicator.service.credentialsstorage.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.configuration.*; -import org.osgi.framework.*; - -/** - * The ProtocolProviderFactory is what actually creates instances of a - * ProtocolProviderService implementation. A provider factory would register, - * persistently store, and remove when necessary, ProtocolProviders. The way - * things are in the SIP Communicator, a user account is represented (in a 1:1 - * relationship) by an AccountID and a ProtocolProvider. In other words - one - * would have as many protocol providers installed in a given moment as they - * would user account registered through the various services. - * - * @author Emil Ivov - * @author Lubomir Marinov - */ -public abstract class ProtocolProviderFactory -{ - /** - * The <tt>Logger</tt> used by the <tt>ProtocolProviderFactory</tt> class - * and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(ProtocolProviderFactory.class); - - /** - * Then name of a property which represents a password. - */ - public static final String PASSWORD = "PASSWORD"; - - /** - * The name of a property representing the name of the protocol for an - * ProtocolProviderFactory. - */ - public static final String PROTOCOL = "PROTOCOL_NAME"; - - /** - * The name of a property representing the path to protocol icons. - */ - public static final String PROTOCOL_ICON_PATH = "PROTOCOL_ICON_PATH"; - - /** - * The name of a property representing the path to the account icon to - * be used in the user interface, when the protocol provider service is not - * available. - */ - public static final String ACCOUNT_ICON_PATH = "ACCOUNT_ICON_PATH"; - - /** - * The name of a property which represents the AccountID of a - * ProtocolProvider and that, together with a password is used to login - * on the protocol network.. - */ - public static final String USER_ID = "USER_ID"; - - /** - * The name that should be displayed to others when we are calling or - * writing them. - */ - public static final String DISPLAY_NAME = "DISPLAY_NAME"; - - /** - * The name that should be displayed to the user on call via and chat via - * lists. - */ - public static final String ACCOUNT_DISPLAY_NAME = "ACCOUNT_DISPLAY_NAME"; - - /** - * The name of the property under which we store protocol AccountID-s. - */ - public static final String ACCOUNT_UID = "ACCOUNT_UID"; - - /** - * The name of the property under which we store protocol the address of - * a protocol centric entity (any protocol server). - */ - public static final String SERVER_ADDRESS = "SERVER_ADDRESS"; - - /** - * The name of the property under which we store the number of the port - * where the server stored against the SERVER_ADDRESS property is expecting - * connections to be made via this protocol. - */ - public static final String SERVER_PORT = "SERVER_PORT"; - - /** - * The name of the property under which we store the name of the transport - * protocol that needs to be used to access the server. - */ - public static final String SERVER_TRANSPORT = "SERVER_TRANSPORT"; - - /** - * The name of the property under which we store protocol the address of - * a protocol proxy. - */ - public static final String PROXY_ADDRESS = "PROXY_ADDRESS"; - - /** - * The name of the property under which we store the number of the port - * where the proxy stored against the PROXY_ADDRESS property is expecting - * connections to be made via this protocol. - */ - public static final String PROXY_PORT = "PROXY_PORT"; - - /** - * The name of the property which defines whether proxy is auto configured - * by the protocol by using known methods such as specific DNS queries. - */ - public static final String PROXY_AUTO_CONFIG = "PROXY_AUTO_CONFIG"; - - /** - * The property indicating the preferred UDP and TCP - * port to bind to for clear communications. - */ - public static final String PREFERRED_CLEAR_PORT_PROPERTY_NAME - = "net.java.sip.communicator.SIP_PREFERRED_CLEAR_PORT"; - - /** - * The property indicating the preferred TLS (TCP) - * port to bind to for secure communications. - */ - public static final String PREFERRED_SECURE_PORT_PROPERTY_NAME - = "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT"; - - /** - * The name of the property under which we store the the type of the proxy - * stored against the PROXY_ADDRESS property. Exact type values depend on - * protocols and among them are socks4, socks5, http and possibly others. - */ - public static final String PROXY_TYPE = "PROXY_TYPE"; - - /** - * The name of the property under which we store the the username for the - * proxy stored against the PROXY_ADDRESS property. - */ - public static final String PROXY_USERNAME = "PROXY_USERNAME"; - - /** - * The name of the property under which we store the the authorization name - * for the proxy stored against the PROXY_ADDRESS property. - */ - public static final String AUTHORIZATION_NAME = "AUTHORIZATION_NAME"; - - /** - * The name of the property under which we store the password for the proxy - * stored against the PROXY_ADDRESS property. - */ - public static final String PROXY_PASSWORD = "PROXY_PASSWORD"; - - /** - * The name of the property under which we store the name of the transport - * protocol that needs to be used to access the proxy. - */ - public static final String PROXY_TRANSPORT = "PROXY_TRANSPORT"; - - /** - * The name of the property that indicates whether loose routing should be - * forced for all traffic in an account, rather than routing through an - * outbound proxy which is the default for Jitsi. - */ - public static final String FORCE_PROXY_BYPASS = "FORCE_PROXY_BYPASS"; - - /** - * The name of the property under which we store the user preference for a - * transport protocol to use (i.e. tcp or udp). - */ - public static final String PREFERRED_TRANSPORT = "PREFERRED_TRANSPORT"; - - /** - * The name of the property under which we store whether we generate - * resource values or we just use the stored one. - */ - public static final String AUTO_GENERATE_RESOURCE = "AUTO_GENERATE_RESOURCE"; - - /** - * The name of the property under which we store resources such as the - * jabber resource property. - */ - public static final String RESOURCE = "RESOURCE"; - - /** - * The name of the property under which we store resource priority. - */ - public static final String RESOURCE_PRIORITY = "RESOURCE_PRIORITY"; - - /** - * The name of the property which defines that the call is encrypted by - * default - */ - public static final String DEFAULT_ENCRYPTION = "DEFAULT_ENCRYPTION"; - - /** - * The name of the property that indicates the encryption protocols for this - * account. - */ - public static final String ENCRYPTION_PROTOCOL = "ENCRYPTION_PROTOCOL"; - - /** - * The name of the property that indicates the status (enabed or disabled) - * encryption protocols for this account. - */ - public static final String ENCRYPTION_PROTOCOL_STATUS - = "ENCRYPTION_PROTOCOL_STATUS"; - - /** - * The name of the property which defines if to include the ZRTP attribute - * to SIP/SDP - */ - public static final String DEFAULT_SIPZRTP_ATTRIBUTE = - "DEFAULT_SIPZRTP_ATTRIBUTE"; - - /** - * The name of the property which defines the ID of the client TLS - * certificate configuration entry. - */ - public static final String CLIENT_TLS_CERTIFICATE = - "CLIENT_TLS_CERTIFICATE"; - - /** - * The name of the property under which we store the boolean value - * indicating if the user name should be automatically changed if the - * specified name already exists. This property is meant to be used by IRC - * implementations. - */ - public static final String AUTO_CHANGE_USER_NAME = "AUTO_CHANGE_USER_NAME"; - - /** - * The name of the property under which we store the boolean value - * indicating if a password is required. Initially this property is meant to - * be used by IRC implementations. - */ - public static final String NO_PASSWORD_REQUIRED = "NO_PASSWORD_REQUIRED"; - - /** - * The name of the property under which we store if the presence is enabled. - */ - public static final String IS_PRESENCE_ENABLED = "IS_PRESENCE_ENABLED"; - - /** - * The name of the property under which we store if the p2p mode for SIMPLE - * should be forced. - */ - public static final String FORCE_P2P_MODE = "FORCE_P2P_MODE"; - - /** - * The name of the property under which we store the offline contact polling - * period for SIMPLE. - */ - public static final String POLLING_PERIOD = "POLLING_PERIOD"; - - /** - * The name of the property under which we store the chosen default - * subscription expiration value for SIMPLE. - */ - public static final String SUBSCRIPTION_EXPIRATION - = "SUBSCRIPTION_EXPIRATION"; - - /** - * Indicates if the server address has been validated. - */ - public static final String SERVER_ADDRESS_VALIDATED - = "SERVER_ADDRESS_VALIDATED"; - - /** - * Indicates if the server settings are over - */ - public static final String IS_SERVER_OVERRIDDEN - = "IS_SERVER_OVERRIDDEN"; - /** - * Indicates if the proxy address has been validated. - */ - public static final String PROXY_ADDRESS_VALIDATED - = "PROXY_ADDRESS_VALIDATED"; - - /** - * Indicates the search strategy chosen for the DICT protocole. - */ - public static final String STRATEGY = "STRATEGY"; - - /** - * Indicates a protocol that would not be shown in the user interface as an - * account. - */ - public static final String IS_PROTOCOL_HIDDEN = "IS_PROTOCOL_HIDDEN"; - - /** - * Indicates if the given account is the preferred account. - */ - public static final String IS_PREFERRED_PROTOCOL = "IS_PREFERRED_PROTOCOL"; - - /** - * The name of the property that would indicate if a given account is - * currently enabled or disabled. - */ - public static final String IS_ACCOUNT_DISABLED = "IS_ACCOUNT_DISABLED"; - - /** - * Indicates if ICE should be used. - */ - public static final String IS_USE_ICE = "ICE_ENABLED"; - - /** - * Indicates if Google ICE should be used. - */ - public static final String IS_USE_GOOGLE_ICE = "GTALK_ICE_ENABLED"; - - /** - * Indicates if STUN server should be automatically discovered. - */ - public static final String AUTO_DISCOVER_STUN = "AUTO_DISCOVER_STUN"; - - /** - * Indicates if default STUN server would be used if no other STUN/TURN - * server are available. - */ - public static final String USE_DEFAULT_STUN_SERVER - = "USE_DEFAULT_STUN_SERVER"; - - /** - * The name of the boolean account property which indicates whether Jitsi - * VideoBridge is to be used, if available and supported, for conference - * calls. - */ - public static final String USE_JITSI_VIDEO_BRIDGE - = "USE_JITSI_VIDEO_BRIDGE"; - - /** - * The property name prefix for all stun server properties. We generally use - * this prefix in conjunction with an index which is how we store multiple - * servers. - */ - public static final String STUN_PREFIX = "STUN"; - - /** - * The base property name for address of additional STUN servers specified. - */ - public static final String STUN_ADDRESS = "ADDRESS"; - - /** - * The base property name for port of additional STUN servers specified. - */ - public static final String STUN_PORT = "PORT"; - - /** - * The base property name for username of additional STUN servers specified. - */ - public static final String STUN_USERNAME = "USERNAME"; - - /** - * The base property name for password of additional STUN servers specified. - */ - public static final String STUN_PASSWORD = "PASSWORD"; - - /** - * The base property name for the turn supported property of additional - * STUN servers specified. - */ - public static final String STUN_IS_TURN_SUPPORTED = "IS_TURN_SUPPORTED"; - - /** - * Indicates if JingleNodes should be used with ICE. - */ - public static final String IS_USE_JINGLE_NODES = "JINGLE_NODES_ENABLED"; - - /** - * Indicates if JingleNodes should be used with ICE. - */ - public static final String AUTO_DISCOVER_JINGLE_NODES - = "AUTO_DISCOVER_JINGLE_NODES"; - - /** - * Indicates if JingleNodes should use buddies to search for nodes. - */ - public static final String JINGLE_NODES_SEARCH_BUDDIES - = "JINGLE_NODES_SEARCH_BUDDIES"; - - /** - * Indicates if UPnP should be used with ICE. - */ - public static final String IS_USE_UPNP = "UPNP_ENABLED"; - - /** - * Indicates if we allow non-TLS connection. - */ - public static final String IS_ALLOW_NON_SECURE = "ALLOW_NON_SECURE"; - - /** - * Enable notifications for new voicemail messages. - */ - public static final String VOICEMAIL_ENABLED = "VOICEMAIL_ENABLED"; - - /** - * Address used to reach voicemail box, by services able to - * subscribe for voicemail new messages notifications. - */ - public static final String VOICEMAIL_URI = "VOICEMAIL_URI"; - - /** - * Address used to call to hear your messages stored on the server - * for your voicemail. - */ - public static final String VOICEMAIL_CHECK_URI = "VOICEMAIL_CHECK_URI"; - - /** - * Indicates if calling is disabled for a certain account. - */ - public static final String IS_CALLING_DISABLED_FOR_ACCOUNT - = "CALLING_DISABLED"; - - /** - * Indicates if desktop streaming/sharing is disabled for a certain account. - */ - public static final String IS_DESKTOP_STREAMING_DISABLED - = "DESKTOP_STREAMING_DISABLED"; - - /** - * The sms default server address. - */ - public static final String SMS_SERVER_ADDRESS = "SMS_SERVER_ADDRESS"; - - /** - * Keep-alive method used by the protocol. - */ - public static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD"; - - /** - * The interval for keep-alives if any. - */ - public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL"; - - /** - * The name of the property holding DTMF method. - */ - public static final String DTMF_METHOD = "DTMF_METHOD"; - - /** - * The minimal DTMF tone duration. - */ - public static final String DTMF_MINIMAL_TONE_DURATION - = "DTMF_MINIMAL_TONE_DURATION"; - - /** - * Paranoia mode when turned on requires all calls to be secure and - * indicated as such. - */ - public static final String MODE_PARANOIA = "MODE_PARANOIA"; - - /** - * The name of the "override encodings" property - */ - public static final String OVERRIDE_ENCODINGS = "OVERRIDE_ENCODINGS"; - - /** - * The prefix used to store account encoding properties - */ - public static final String ENCODING_PROP_PREFIX = "Encodings"; - - /** - * An account property to provide a connected account to check for - * its status. Used when the current provider need to reject calls - * but is missing presence operation set and need to check other - * provider for status. - */ - public static final String CUSAX_PROVIDER_ACCOUNT_PROP - = "cusax.XMPP_ACCOUNT_ID"; - - /** - * The <code>BundleContext</code> containing (or to contain) the service - * registration of this factory. - */ - private final BundleContext bundleContext; - - /** - * The name of the protocol this factory registers its - * <code>ProtocolProviderService</code>s with and to be placed in the - * properties of the accounts created by this factory. - */ - private final String protocolName; - - /** - * The table that we store our accounts in. - * <p> - * TODO Synchronize the access to the field which may in turn be better - * achieved by also hiding it from protected into private access. - * </p> - */ - protected final Hashtable<AccountID, ServiceRegistration> registeredAccounts - = new Hashtable<AccountID, ServiceRegistration>(); - - /** - * The name of the property that indicates the AVP type. - * <ul> - * <li>{@link #SAVP_OFF}</li> - * <li>{@link #SAVP_MANDATORY}</li> - * <li>{@link #SAVP_OPTIONAL}</li> - * </ul> - */ - public static final String SAVP_OPTION = "SAVP_OPTION"; - - /** - * Always use RTP/AVP - */ - public static final int SAVP_OFF = 0; - - /** - * Always use RTP/SAVP - */ - public static final int SAVP_MANDATORY = 1; - - /** - * Sends two media description, with RTP/SAVP being first. - */ - public static final int SAVP_OPTIONAL = 2; - - /** - * The name of the property that defines the enabled SDES cipher suites. - * Enabled suites are listed as CSV by their RFC name. - */ - public static final String SDES_CIPHER_SUITES = "SDES_CIPHER_SUITES"; - - /** - * Creates a new <tt>ProtocolProviderFactory</tt>. - * - * @param bundleContext the bundle context reference of the service - * @param protocolName the name of the protocol - */ - protected ProtocolProviderFactory(BundleContext bundleContext, - String protocolName) - { - this.bundleContext = bundleContext; - this.protocolName = protocolName; - } - - /** - * Gets the <code>BundleContext</code> containing (or to contain) the - * service registration of this factory. - * - * @return the <code>BundleContext</code> containing (or to contain) the - * service registration of this factory - */ - public BundleContext getBundleContext() - { - return bundleContext; - } - - /** - * Initializes and creates an account corresponding to the specified - * accountProperties and registers the resulting ProtocolProvider in the - * <tt>context</tt> BundleContext parameter. Note that account - * registration is persistent and accounts that are registered during - * a particular sip-communicator session would be automatically reloaded - * during all following sessions until they are removed through the - * removeAccount method. - * - * @param userID the user identifier uniquely representing the newly - * created account within the protocol namespace. - * @param accountProperties a set of protocol (or implementation) specific - * properties defining the new account. - * @return the AccountID of the newly created account. - * @throws java.lang.IllegalArgumentException if userID does not correspond - * to an identifier in the context of the underlying protocol or if - * accountProperties does not contain a complete set of account installation - * properties. - * @throws java.lang.IllegalStateException if the account has already been - * installed. - * @throws java.lang.NullPointerException if any of the arguments is null. - */ - public abstract AccountID installAccount(String userID, - Map<String, String> accountProperties) - throws IllegalArgumentException, - IllegalStateException, - NullPointerException; - - - /** - * Modifies the account corresponding to the specified accountID. This - * method is meant to be used to change properties of already existing - * accounts. Note that if the given accountID doesn't correspond to any - * registered account this method would do nothing. - * - * @param protocolProvider the protocol provider service corresponding to - * the modified account. - * @param accountProperties a set of protocol (or implementation) specific - * properties defining the new account. - * - * @throws java.lang.NullPointerException if any of the arguments is null. - */ - public abstract void modifyAccount( - ProtocolProviderService protocolProvider, - Map<String, String> accountProperties) - throws NullPointerException; - - /** - * Returns a copy of the list containing the <tt>AccountID</tt>s of all - * accounts currently registered in this protocol provider. - * @return a copy of the list containing the <tt>AccountID</tt>s of all - * accounts currently registered in this protocol provider. - */ - public ArrayList<AccountID> getRegisteredAccounts() - { - synchronized (registeredAccounts) - { - return new ArrayList<AccountID>(registeredAccounts.keySet()); - } - } - - /** - * Returns the ServiceReference for the protocol provider corresponding to - * the specified accountID or null if the accountID is unknown. - * @param accountID the accountID of the protocol provider we'd like to get - * @return a ServiceReference object to the protocol provider with the - * specified account id and null if the account id is unknown to the - * provider factory. - */ - public ServiceReference getProviderForAccount(AccountID accountID) - { - ServiceRegistration registration; - - synchronized (registeredAccounts) - { - registration = - registeredAccounts.get(accountID); - } - - try - { - return (registration == null) ? null : registration.getReference(); - } - catch (IllegalStateException ise) - { - synchronized (registeredAccounts) - { - registeredAccounts.remove(accountID); - } - } - - return null; - } - - /** - * Removes the specified account from the list of accounts that this - * provider factory is handling. If the specified accountID is unknown to - * the ProtocolProviderFactory, the call has no effect and false is - * returned. This method is persistent in nature and once called the account - * corresponding to the specified ID will not be loaded during future runs - * of the project. - * - * @param accountID the ID of the account to remove. - * @return true if an account with the specified ID existed and was removed - * and false otherwise. - */ - public boolean uninstallAccount(AccountID accountID) - { - // Unregister the protocol provider. - ServiceReference serRef = getProviderForAccount(accountID); - - boolean wasAccountExisting = false; - - // If the protocol provider service is registered, first unregister the - // service. - if (serRef != null) - { - BundleContext bundleContext = getBundleContext(); - ProtocolProviderService protocolProvider = - (ProtocolProviderService) bundleContext.getService(serRef); - - try - { - protocolProvider.unregister(); - } - catch (OperationFailedException ex) - { - logger - .error("Failed to unregister protocol provider for account: " - + accountID + " caused by: " + ex); - } - } - - ServiceRegistration registration; - - synchronized (registeredAccounts) - { - registration = registeredAccounts.remove(accountID); - } - - // first remove the stored account so when PP is unregistered we can - // distinguish between deleted or just disabled account - wasAccountExisting = removeStoredAccount(accountID); - - if (registration != null) - { - // Kill the service. - registration.unregister(); - } - - return wasAccountExisting; - } - - /** - * The method stores the specified account in the configuration service - * under the package name of the source factory. The restore and remove - * account methods are to be used to obtain access to and control the stored - * accounts. - * <p> - * In order to store all account properties, the method would create an - * entry in the configuration service corresponding (beginning with) the - * <tt>sourceFactory</tt>'s package name and add to it a unique identifier - * (e.g. the current miliseconds.) - * </p> - * - * @param accountID the AccountID corresponding to the account that we would - * like to store. - */ - protected void storeAccount(AccountID accountID) - { - this.storeAccount(accountID, true); - } - - /** - * The method stores the specified account in the configuration service - * under the package name of the source factory. The restore and remove - * account methods are to be used to obtain access to and control the stored - * accounts. - * <p> - * In order to store all account properties, the method would create an - * entry in the configuration service corresponding (beginning with) the - * <tt>sourceFactory</tt>'s package name and add to it a unique identifier - * (e.g. the current miliseconds.) - * </p> - * - * @param accountID the AccountID corresponding to the account that we would - * like to store. - * @param isModification if <tt>false</tt> there must be no such already - * loaded account, it <tt>true</tt> ist modification of an existing account. - * Usually we use this method with <tt>false</tt> in method installAccount - * and with <tt>true</tt> or the overridden method in method - * modifyAccount. - */ - protected void storeAccount(AccountID accountID, boolean isModification) - { - if(!isModification - && getAccountManager().getStoredAccounts().contains(accountID)) - { - throw new IllegalStateException( - "An account for id " + accountID.getUserID() - + " was already loaded!"); - } - - try - { - getAccountManager().storeAccount(this, accountID); - } - catch (OperationFailedException ofex) - { - throw new UndeclaredThrowableException(ofex); - } - } - - /** - * Saves the password for the specified account after scrambling it a bit so - * that it is not visible from first sight. (The method remains highly - * insecure). - * - * @param accountID the AccountID for the account whose password we're - * storing - * @param password the password itself - * - * @throws IllegalArgumentException if no account corresponding to - * <code>accountID</code> has been previously stored - */ - public void storePassword(AccountID accountID, String password) - throws IllegalArgumentException - { - try - { - storePassword(getBundleContext(), accountID, password); - } - catch (OperationFailedException ofex) - { - throw new UndeclaredThrowableException(ofex); - } - } - - /** - * Saves the password for the specified account after scrambling it a bit - * so that it is not visible from first sight (Method remains highly - * insecure). - * <p> - * TODO Delegate the implementation to {@link AccountManager} because it - * knows the format in which the password (among the other account - * properties) is to be saved. - * </p> - * - * @param bundleContext a currently valid bundle context. - * @param accountID the <tt>AccountID</tt> of the account whose password is - * to be stored - * @param password the password to be stored - * - * @throws IllegalArgumentException if no account corresponding to - * <tt>accountID</tt> has been previously stored. - * @throws OperationFailedException if anything goes wrong while storing the - * specified <tt>password</tt> - */ - protected void storePassword(BundleContext bundleContext, - AccountID accountID, - String password) - throws IllegalArgumentException, - OperationFailedException - { - String accountPrefix - = findAccountPrefix( - bundleContext, - accountID, - getFactoryImplPackageName()); - - if (accountPrefix == null) - { - throw - new IllegalArgumentException( - "No previous records found for account ID: " - + accountID.getAccountUniqueID() - + " in package" - + getFactoryImplPackageName()); - } - - CredentialsStorageService credentialsStorage - = ServiceUtils.getService( - bundleContext, - CredentialsStorageService.class); - - if (!credentialsStorage.storePassword(accountPrefix, password)) - { - throw - new OperationFailedException( - "CredentialsStorageService failed to storePassword", - OperationFailedException.GENERAL_ERROR); - } - - // Update password property also in the AccountID - // to prevent it from being removed during account reload - // in some cases. - accountID.setPassword(password); - - } - - /** - * Returns the password last saved for the specified account. - * - * @param accountID the AccountID for the account whose password we're - * looking for - * - * @return a String containing the password for the specified accountID - */ - public String loadPassword(AccountID accountID) - { - return loadPassword(getBundleContext(), accountID); - } - - /** - * Returns the password last saved for the specified account. - * <p> - * TODO Delegate the implementation to {@link AccountManager} because it - * knows the format in which the password (among the other account - * properties) was saved. - * </p> - * - * @param bundleContext a currently valid bundle context. - * @param accountID the AccountID for the account whose password we're - * looking for.. - * - * @return a String containing the password for the specified accountID. - */ - protected String loadPassword(BundleContext bundleContext, - AccountID accountID) - { - String accountPrefix = findAccountPrefix( - bundleContext, accountID, getFactoryImplPackageName()); - - if (accountPrefix == null) - return null; - - CredentialsStorageService credentialsStorage - = ServiceUtils.getService( - bundleContext, - CredentialsStorageService.class); - - return credentialsStorage.loadPassword(accountPrefix); - } - - /** - * Initializes and creates an account corresponding to the specified - * accountProperties and registers the resulting ProtocolProvider in the - * <tt>context</tt> BundleContext parameter. This method has a persistent - * effect. Once created the resulting account will remain installed until - * removed through the uninstallAccount method. - * - * @param accountProperties a set of protocol (or implementation) specific - * properties defining the new account. - * @return the AccountID of the newly loaded account - */ - public AccountID loadAccount(Map<String, String> accountProperties) - { - AccountID accountID = createAccount(accountProperties); - - loadAccount(accountID); - - return accountID; - } - - /** - * Creates a protocol provider for the given <tt>accountID</tt> and - * registers it in the bundle context. This method has a persistent - * effect. Once created the resulting account will remain installed until - * removed through the uninstallAccount method. - * - * @param accountID the account identifier - * @return <tt>true</tt> if the account with the given <tt>accountID</tt> is - * successfully loaded, otherwise returns <tt>false</tt> - */ - public boolean loadAccount(AccountID accountID) - { - // Need to obtain the original user id property, instead of calling - // accountID.getUserID(), because this method could return a modified - // version of the user id property. - String userID = accountID - .getAccountPropertyString(ProtocolProviderFactory.USER_ID); - - ProtocolProviderService service = createService(userID, accountID); - - Dictionary<String, String> properties = new Hashtable<String, String>(); - properties.put(PROTOCOL, protocolName); - properties.put(USER_ID, userID); - - ServiceRegistration serviceRegistration = - bundleContext.registerService(ProtocolProviderService.class - .getName(), service, properties); - - if (serviceRegistration == null) - return false; - else - { - synchronized (registeredAccounts) - { - registeredAccounts.put(accountID, serviceRegistration); - } - return true; - } - } - - /** - * Unloads the account corresponding to the given <tt>accountID</tt>. - * Unregisters the corresponding protocol provider, but keeps the account in - * contrast to the uninstallAccount method. - * - * @param accountID the account identifier - * @return true if an account with the specified ID existed and was unloaded - * and false otherwise. - */ - public boolean unloadAccount(AccountID accountID) - { - // Unregister the protocol provider. - ServiceReference serRef = getProviderForAccount(accountID); - - if (serRef == null) - { - return false; - } - - BundleContext bundleContext = getBundleContext(); - ProtocolProviderService protocolProvider = - (ProtocolProviderService) bundleContext.getService(serRef); - - try - { - protocolProvider.unregister(); - } - catch (OperationFailedException ex) - { - logger - .error("Failed to unregister protocol provider for account : " - + accountID + " caused by: " + ex); - } - - ServiceRegistration registration; - - synchronized (registeredAccounts) - { - registration = registeredAccounts.remove(accountID); - } - if (registration == null) - { - return false; - } - - // Kill the service. - registration.unregister(); - - return true; - } - - /** - * Initializes and creates an account corresponding to the specified - * accountProperties. - * - * @param accountProperties a set of protocol (or implementation) specific - * properties defining the new account. - * @return the AccountID of the newly created account - */ - public AccountID createAccount(Map<String, String> accountProperties) - { - BundleContext bundleContext = getBundleContext(); - if (bundleContext == null) - throw new NullPointerException( - "The specified BundleContext was null"); - - if (accountProperties == null) - throw new NullPointerException( - "The specified property map was null"); - - String userID = accountProperties.get(USER_ID); - if (userID == null) - throw new NullPointerException( - "The account properties contained no user id."); - - String protocolName = getProtocolName(); - if (!accountProperties.containsKey(PROTOCOL)) - accountProperties.put(PROTOCOL, protocolName); - - return createAccountID(userID, accountProperties); - } - - /** - * Creates a new <code>AccountID</code> instance with a specific user ID to - * represent a given set of account properties. - * <p> - * The method is a pure factory allowing implementers to specify the runtime - * type of the created <code>AccountID</code> and customize the instance. - * The returned <code>AccountID</code> will later be associated with a - * <code>ProtocolProviderService</code> by the caller (e.g. using - * {@link #createService(String, AccountID)}). - * </p> - * - * @param userID the user ID of the new instance - * @param accountProperties the set of properties to be represented by the - * new instance - * @return a new <code>AccountID</code> instance with the specified user ID - * representing the given set of account properties - */ - protected abstract AccountID createAccountID( - String userID, Map<String, String> accountProperties); - - /** - * Gets the name of the protocol this factory registers its - * <code>ProtocolProviderService</code>s with and to be placed in the - * properties of the accounts created by this factory. - * - * @return the name of the protocol this factory registers its - * <code>ProtocolProviderService</code>s with and to be placed in - * the properties of the accounts created by this factory - */ - public String getProtocolName() - { - return protocolName; - } - - /** - * Initializes a new <code>ProtocolProviderService</code> instance with a - * specific user ID to represent a specific <code>AccountID</code>. - * <p> - * The method is a pure factory allowing implementers to specify the runtime - * type of the created <code>ProtocolProviderService</code> and customize - * the instance. The caller will later register the returned service with - * the <code>BundleContext</code> of this factory. - * </p> - * - * @param userID the user ID to initialize the new instance with - * @param accountID the <code>AccountID</code> to be represented by the new - * instance - * @return a new <code>ProtocolProviderService</code> instance with the - * specific user ID representing the specified - * <code>AccountID</code> - */ - protected abstract ProtocolProviderService createService(String userID, - AccountID accountID); - - /** - * Removes the account with <tt>accountID</tt> from the set of accounts - * that are persistently stored inside the configuration service. - * - * @param accountID the AccountID of the account to remove. - * - * @return true if an account has been removed and false otherwise. - */ - protected boolean removeStoredAccount(AccountID accountID) - { - return getAccountManager().removeStoredAccount(this, accountID); - } - - /** - * Returns the prefix for all persistently stored properties of the account - * with the specified id. - * @param bundleContext a currently valid bundle context. - * @param accountID the AccountID of the account whose properties we're - * looking for. - * @param sourcePackageName a String containing the package name of the - * concrete factory class that extends us. - * @return a String indicating the ConfigurationService property name - * prefix under which all account properties are stored or null if no - * account corresponding to the specified id was found. - */ - public static String findAccountPrefix(BundleContext bundleContext, - AccountID accountID, - String sourcePackageName) - { - ServiceReference confReference - = bundleContext.getServiceReference( - ConfigurationService.class.getName()); - ConfigurationService configurationService - = (ConfigurationService) bundleContext.getService(confReference); - - //first retrieve all accounts that we've registered - List<String> storedAccounts = - configurationService.getPropertyNamesByPrefix(sourcePackageName, - true); - - //find an account with the corresponding id. - for (String accountRootPropertyName : storedAccounts) - { - //unregister the account in the configuration service. - //all the properties must have been registered in the following - //hierarchy: - //net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME - String accountUID = configurationService.getString( - accountRootPropertyName //node id - + "." + ACCOUNT_UID); // propname - - if (accountID.getAccountUniqueID().equals(accountUID)) - { - return accountRootPropertyName; - } - } - return null; - } - - /** - * Returns the name of the package that we're currently running in (i.e. - * the name of the package containing the proto factory that extends us). - * - * @return a String containing the package name of the concrete factory - * class that extends us. - */ - private String getFactoryImplPackageName() - { - String className = getClass().getName(); - - return className.substring(0, className.lastIndexOf('.')); - } - - /** - * Prepares the factory for bundle shutdown. - */ - public void stop() - { - if (logger.isTraceEnabled()) - logger.trace("Preparing to stop all protocol providers of" + this); - - synchronized (registeredAccounts) - { - for (Enumeration<ServiceRegistration> registrations = - registeredAccounts.elements(); registrations.hasMoreElements();) - { - ServiceRegistration reg = registrations.nextElement(); - - stop(reg); - - reg.unregister(); - } - - registeredAccounts.clear(); - } - } - - /** - * Shuts down the <code>ProtocolProviderService</code> representing an - * account registered with this factory. - * - * @param registeredAccount the <code>ServiceRegistration</code> of the - * <code>ProtocolProviderService</code> representing an account - * registered with this factory - */ - protected void stop(ServiceRegistration registeredAccount) - { - ProtocolProviderService protocolProviderService = - (ProtocolProviderService) getBundleContext().getService( - registeredAccount.getReference()); - - protocolProviderService.shutdown(); - } - - /** - * Get the <tt>AccountManager</tt> of the protocol. - * - * @return <tt>AccountManager</tt> of the protocol - */ - private AccountManager getAccountManager() - { - BundleContext bundleContext = getBundleContext(); - ServiceReference serviceReference = - bundleContext.getServiceReference(AccountManager.class.getName()); - - return (AccountManager) bundleContext.getService(serviceReference); - } - - - /** - * Finds registered <tt>ProtocolProviderFactory</tt> for given - * <tt>protocolName</tt>. - * @param bundleContext the OSGI bundle context that will be used. - * @param protocolName the protocol name. - * @return Registered <tt>ProtocolProviderFactory</tt> for given protocol - * name or <tt>null</tt> if no provider was found. - */ - static public ProtocolProviderFactory getProtocolProviderFactory( - BundleContext bundleContext, - String protocolName) - { - ServiceReference[] serRefs = null; - - String osgiFilter = - "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")"; - - try - { - serRefs = - bundleContext.getServiceReferences( - ProtocolProviderFactory.class.getName(), osgiFilter); - } - catch (InvalidSyntaxException ex) - { - logger.error(ex); - return null; - } - - return (ProtocolProviderFactory) bundleContext.getService(serRefs[0]); - } -} +/*
+ * 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.lang.reflect.*;
+import java.util.*;
+
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.util.*;
+
+import org.jitsi.service.configuration.*;
+import org.osgi.framework.*;
+
+/**
+ * The ProtocolProviderFactory is what actually creates instances of a
+ * ProtocolProviderService implementation. A provider factory would register,
+ * persistently store, and remove when necessary, ProtocolProviders. The way
+ * things are in the SIP Communicator, a user account is represented (in a 1:1
+ * relationship) by an AccountID and a ProtocolProvider. In other words - one
+ * would have as many protocol providers installed in a given moment as they
+ * would user account registered through the various services.
+ *
+ * @author Emil Ivov
+ * @author Lubomir Marinov
+ */
+public abstract class ProtocolProviderFactory
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>ProtocolProviderFactory</tt> class
+ * and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(ProtocolProviderFactory.class);
+
+ /**
+ * Then name of a property which represents a password.
+ */
+ public static final String PASSWORD = "PASSWORD";
+
+ /**
+ * The name of a property representing the name of the protocol for an
+ * ProtocolProviderFactory.
+ */
+ public static final String PROTOCOL = "PROTOCOL_NAME";
+
+ /**
+ * The name of a property representing the path to protocol icons.
+ */
+ public static final String PROTOCOL_ICON_PATH = "PROTOCOL_ICON_PATH";
+
+ /**
+ * The name of a property representing the path to the account icon to
+ * be used in the user interface, when the protocol provider service is not
+ * available.
+ */
+ public static final String ACCOUNT_ICON_PATH = "ACCOUNT_ICON_PATH";
+
+ /**
+ * The name of a property which represents the AccountID of a
+ * ProtocolProvider and that, together with a password is used to login
+ * on the protocol network..
+ */
+ public static final String USER_ID = "USER_ID";
+
+ /**
+ * The name that should be displayed to others when we are calling or
+ * writing them.
+ */
+ public static final String DISPLAY_NAME = "DISPLAY_NAME";
+
+ /**
+ * The name that should be displayed to the user on call via and chat via
+ * lists.
+ */
+ public static final String ACCOUNT_DISPLAY_NAME = "ACCOUNT_DISPLAY_NAME";
+
+ /**
+ * The name of the property under which we store protocol AccountID-s.
+ */
+ public static final String ACCOUNT_UID = "ACCOUNT_UID";
+
+ /**
+ * The name of the property under which we store protocol the address of
+ * a protocol centric entity (any protocol server).
+ */
+ public static final String SERVER_ADDRESS = "SERVER_ADDRESS";
+
+ /**
+ * The name of the property under which we store the number of the port
+ * where the server stored against the SERVER_ADDRESS property is expecting
+ * connections to be made via this protocol.
+ */
+ public static final String SERVER_PORT = "SERVER_PORT";
+
+ /**
+ * The name of the property under which we store the name of the transport
+ * protocol that needs to be used to access the server.
+ */
+ public static final String SERVER_TRANSPORT = "SERVER_TRANSPORT";
+
+ /**
+ * The name of the property under which we store protocol the address of
+ * a protocol proxy.
+ */
+ public static final String PROXY_ADDRESS = "PROXY_ADDRESS";
+
+ /**
+ * The name of the property under which we store the number of the port
+ * where the proxy stored against the PROXY_ADDRESS property is expecting
+ * connections to be made via this protocol.
+ */
+ public static final String PROXY_PORT = "PROXY_PORT";
+
+ /**
+ * The name of the property which defines whether proxy is auto configured
+ * by the protocol by using known methods such as specific DNS queries.
+ */
+ public static final String PROXY_AUTO_CONFIG = "PROXY_AUTO_CONFIG";
+
+ /**
+ * The property indicating the preferred UDP and TCP
+ * port to bind to for clear communications.
+ */
+ public static final String PREFERRED_CLEAR_PORT_PROPERTY_NAME
+ = "net.java.sip.communicator.SIP_PREFERRED_CLEAR_PORT";
+
+ /**
+ * The property indicating the preferred TLS (TCP)
+ * port to bind to for secure communications.
+ */
+ public static final String PREFERRED_SECURE_PORT_PROPERTY_NAME
+ = "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT";
+
+ /**
+ * The name of the property under which we store the the type of the proxy
+ * stored against the PROXY_ADDRESS property. Exact type values depend on
+ * protocols and among them are socks4, socks5, http and possibly others.
+ */
+ public static final String PROXY_TYPE = "PROXY_TYPE";
+
+ /**
+ * The name of the property under which we store the the username for the
+ * proxy stored against the PROXY_ADDRESS property.
+ */
+ public static final String PROXY_USERNAME = "PROXY_USERNAME";
+
+ /**
+ * The name of the property under which we store the the authorization name
+ * for the proxy stored against the PROXY_ADDRESS property.
+ */
+ public static final String AUTHORIZATION_NAME = "AUTHORIZATION_NAME";
+
+ /**
+ * The name of the property under which we store the password for the proxy
+ * stored against the PROXY_ADDRESS property.
+ */
+ public static final String PROXY_PASSWORD = "PROXY_PASSWORD";
+
+ /**
+ * The name of the property under which we store the name of the transport
+ * protocol that needs to be used to access the proxy.
+ */
+ public static final String PROXY_TRANSPORT = "PROXY_TRANSPORT";
+
+ /**
+ * The name of the property that indicates whether loose routing should be
+ * forced for all traffic in an account, rather than routing through an
+ * outbound proxy which is the default for Jitsi.
+ */
+ public static final String FORCE_PROXY_BYPASS = "FORCE_PROXY_BYPASS";
+
+ /**
+ * The name of the property under which we store the user preference for a
+ * transport protocol to use (i.e. tcp or udp).
+ */
+ public static final String PREFERRED_TRANSPORT = "PREFERRED_TRANSPORT";
+
+ /**
+ * The name of the property under which we store whether we generate
+ * resource values or we just use the stored one.
+ */
+ public static final String AUTO_GENERATE_RESOURCE = "AUTO_GENERATE_RESOURCE";
+
+ /**
+ * The name of the property under which we store resources such as the
+ * jabber resource property.
+ */
+ public static final String RESOURCE = "RESOURCE";
+
+ /**
+ * The name of the property under which we store resource priority.
+ */
+ public static final String RESOURCE_PRIORITY = "RESOURCE_PRIORITY";
+
+ /**
+ * The name of the property which defines that the call is encrypted by
+ * default
+ */
+ public static final String DEFAULT_ENCRYPTION = "DEFAULT_ENCRYPTION";
+
+ /**
+ * The name of the property that indicates the encryption protocols for this
+ * account.
+ */
+ public static final String ENCRYPTION_PROTOCOL = "ENCRYPTION_PROTOCOL";
+
+ /**
+ * The name of the property that indicates the status (enabed or disabled)
+ * encryption protocols for this account.
+ */
+ public static final String ENCRYPTION_PROTOCOL_STATUS
+ = "ENCRYPTION_PROTOCOL_STATUS";
+
+ /**
+ * The name of the property which defines if to include the ZRTP attribute
+ * to SIP/SDP
+ */
+ public static final String DEFAULT_SIPZRTP_ATTRIBUTE =
+ "DEFAULT_SIPZRTP_ATTRIBUTE";
+
+ /**
+ * The name of the property which defines the ID of the client TLS
+ * certificate configuration entry.
+ */
+ public static final String CLIENT_TLS_CERTIFICATE =
+ "CLIENT_TLS_CERTIFICATE";
+
+ /**
+ * The name of the property under which we store the boolean value
+ * indicating if the user name should be automatically changed if the
+ * specified name already exists. This property is meant to be used by IRC
+ * implementations.
+ */
+ public static final String AUTO_CHANGE_USER_NAME = "AUTO_CHANGE_USER_NAME";
+
+ /**
+ * The name of the property under which we store the boolean value
+ * indicating if a password is required. Initially this property is meant to
+ * be used by IRC implementations.
+ */
+ public static final String NO_PASSWORD_REQUIRED = "NO_PASSWORD_REQUIRED";
+
+ /**
+ * The name of the property under which we store if the presence is enabled.
+ */
+ public static final String IS_PRESENCE_ENABLED = "IS_PRESENCE_ENABLED";
+
+ /**
+ * The name of the property under which we store if the p2p mode for SIMPLE
+ * should be forced.
+ */
+ public static final String FORCE_P2P_MODE = "FORCE_P2P_MODE";
+
+ /**
+ * The name of the property under which we store the offline contact polling
+ * period for SIMPLE.
+ */
+ public static final String POLLING_PERIOD = "POLLING_PERIOD";
+
+ /**
+ * The name of the property under which we store the chosen default
+ * subscription expiration value for SIMPLE.
+ */
+ public static final String SUBSCRIPTION_EXPIRATION
+ = "SUBSCRIPTION_EXPIRATION";
+
+ /**
+ * Indicates if the server address has been validated.
+ */
+ public static final String SERVER_ADDRESS_VALIDATED
+ = "SERVER_ADDRESS_VALIDATED";
+
+ /**
+ * Indicates if the server settings are over
+ */
+ public static final String IS_SERVER_OVERRIDDEN
+ = "IS_SERVER_OVERRIDDEN";
+ /**
+ * Indicates if the proxy address has been validated.
+ */
+ public static final String PROXY_ADDRESS_VALIDATED
+ = "PROXY_ADDRESS_VALIDATED";
+
+ /**
+ * Indicates the search strategy chosen for the DICT protocole.
+ */
+ public static final String STRATEGY = "STRATEGY";
+
+ /**
+ * Indicates a protocol that would not be shown in the user interface as an
+ * account.
+ */
+ public static final String IS_PROTOCOL_HIDDEN = "IS_PROTOCOL_HIDDEN";
+
+ /**
+ * Indicates if the given account is the preferred account.
+ */
+ public static final String IS_PREFERRED_PROTOCOL = "IS_PREFERRED_PROTOCOL";
+
+ /**
+ * The name of the property that would indicate if a given account is
+ * currently enabled or disabled.
+ */
+ public static final String IS_ACCOUNT_DISABLED = "IS_ACCOUNT_DISABLED";
+
+ /**
+ * Indicates if ICE should be used.
+ */
+ public static final String IS_USE_ICE = "ICE_ENABLED";
+
+ /**
+ * Indicates if Google ICE should be used.
+ */
+ public static final String IS_USE_GOOGLE_ICE = "GTALK_ICE_ENABLED";
+
+ /**
+ * Indicates if STUN server should be automatically discovered.
+ */
+ public static final String AUTO_DISCOVER_STUN = "AUTO_DISCOVER_STUN";
+
+ /**
+ * Indicates if default STUN server would be used if no other STUN/TURN
+ * server are available.
+ */
+ public static final String USE_DEFAULT_STUN_SERVER
+ = "USE_DEFAULT_STUN_SERVER";
+
+ /**
+ * The name of the boolean account property which indicates whether Jitsi
+ * Videobridge is to be used, if available and supported, for conference
+ * calls.
+ */
+ public static final String USE_JITSI_VIDEO_BRIDGE
+ = "USE_JITSI_VIDEO_BRIDGE";
+
+ /**
+ * The property name prefix for all stun server properties. We generally use
+ * this prefix in conjunction with an index which is how we store multiple
+ * servers.
+ */
+ public static final String STUN_PREFIX = "STUN";
+
+ /**
+ * The base property name for address of additional STUN servers specified.
+ */
+ public static final String STUN_ADDRESS = "ADDRESS";
+
+ /**
+ * The base property name for port of additional STUN servers specified.
+ */
+ public static final String STUN_PORT = "PORT";
+
+ /**
+ * The base property name for username of additional STUN servers specified.
+ */
+ public static final String STUN_USERNAME = "USERNAME";
+
+ /**
+ * The base property name for password of additional STUN servers specified.
+ */
+ public static final String STUN_PASSWORD = "PASSWORD";
+
+ /**
+ * The base property name for the turn supported property of additional
+ * STUN servers specified.
+ */
+ public static final String STUN_IS_TURN_SUPPORTED = "IS_TURN_SUPPORTED";
+
+ /**
+ * Indicates if JingleNodes should be used with ICE.
+ */
+ public static final String IS_USE_JINGLE_NODES = "JINGLE_NODES_ENABLED";
+
+ /**
+ * Indicates if JingleNodes should be used with ICE.
+ */
+ public static final String AUTO_DISCOVER_JINGLE_NODES
+ = "AUTO_DISCOVER_JINGLE_NODES";
+
+ /**
+ * Indicates if JingleNodes should use buddies to search for nodes.
+ */
+ public static final String JINGLE_NODES_SEARCH_BUDDIES
+ = "JINGLE_NODES_SEARCH_BUDDIES";
+
+ /**
+ * Indicates if UPnP should be used with ICE.
+ */
+ public static final String IS_USE_UPNP = "UPNP_ENABLED";
+
+ /**
+ * Indicates if we allow non-TLS connection.
+ */
+ public static final String IS_ALLOW_NON_SECURE = "ALLOW_NON_SECURE";
+
+ /**
+ * Enable notifications for new voicemail messages.
+ */
+ public static final String VOICEMAIL_ENABLED = "VOICEMAIL_ENABLED";
+
+ /**
+ * Address used to reach voicemail box, by services able to
+ * subscribe for voicemail new messages notifications.
+ */
+ public static final String VOICEMAIL_URI = "VOICEMAIL_URI";
+
+ /**
+ * Address used to call to hear your messages stored on the server
+ * for your voicemail.
+ */
+ public static final String VOICEMAIL_CHECK_URI = "VOICEMAIL_CHECK_URI";
+
+ /**
+ * Indicates if calling is disabled for a certain account.
+ */
+ public static final String IS_CALLING_DISABLED_FOR_ACCOUNT
+ = "CALLING_DISABLED";
+
+ /**
+ * Indicates if desktop streaming/sharing is disabled for a certain account.
+ */
+ public static final String IS_DESKTOP_STREAMING_DISABLED
+ = "DESKTOP_STREAMING_DISABLED";
+
+ /**
+ * The sms default server address.
+ */
+ public static final String SMS_SERVER_ADDRESS = "SMS_SERVER_ADDRESS";
+
+ /**
+ * Keep-alive method used by the protocol.
+ */
+ public static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD";
+
+ /**
+ * The interval for keep-alives if any.
+ */
+ public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL";
+
+ /**
+ * The name of the property holding DTMF method.
+ */
+ public static final String DTMF_METHOD = "DTMF_METHOD";
+
+ /**
+ * The minimal DTMF tone duration.
+ */
+ public static final String DTMF_MINIMAL_TONE_DURATION
+ = "DTMF_MINIMAL_TONE_DURATION";
+
+ /**
+ * Paranoia mode when turned on requires all calls to be secure and
+ * indicated as such.
+ */
+ public static final String MODE_PARANOIA = "MODE_PARANOIA";
+
+ /**
+ * The name of the "override encodings" property
+ */
+ public static final String OVERRIDE_ENCODINGS = "OVERRIDE_ENCODINGS";
+
+ /**
+ * The prefix used to store account encoding properties
+ */
+ public static final String ENCODING_PROP_PREFIX = "Encodings";
+
+ /**
+ * An account property to provide a connected account to check for
+ * its status. Used when the current provider need to reject calls
+ * but is missing presence operation set and need to check other
+ * provider for status.
+ */
+ public static final String CUSAX_PROVIDER_ACCOUNT_PROP
+ = "cusax.XMPP_ACCOUNT_ID";
+
+ /**
+ * The <code>BundleContext</code> containing (or to contain) the service
+ * registration of this factory.
+ */
+ private final BundleContext bundleContext;
+
+ /**
+ * The name of the protocol this factory registers its
+ * <code>ProtocolProviderService</code>s with and to be placed in the
+ * properties of the accounts created by this factory.
+ */
+ private final String protocolName;
+
+ /**
+ * The table that we store our accounts in.
+ * <p>
+ * TODO Synchronize the access to the field which may in turn be better
+ * achieved by also hiding it from protected into private access.
+ * </p>
+ */
+ protected final Hashtable<AccountID, ServiceRegistration> registeredAccounts
+ = new Hashtable<AccountID, ServiceRegistration>();
+
+ /**
+ * The name of the property that indicates the AVP type.
+ * <ul>
+ * <li>{@link #SAVP_OFF}</li>
+ * <li>{@link #SAVP_MANDATORY}</li>
+ * <li>{@link #SAVP_OPTIONAL}</li>
+ * </ul>
+ */
+ public static final String SAVP_OPTION = "SAVP_OPTION";
+
+ /**
+ * Always use RTP/AVP
+ */
+ public static final int SAVP_OFF = 0;
+
+ /**
+ * Always use RTP/SAVP
+ */
+ public static final int SAVP_MANDATORY = 1;
+
+ /**
+ * Sends two media description, with RTP/SAVP being first.
+ */
+ public static final int SAVP_OPTIONAL = 2;
+
+ /**
+ * The name of the property that defines the enabled SDES cipher suites.
+ * Enabled suites are listed as CSV by their RFC name.
+ */
+ public static final String SDES_CIPHER_SUITES = "SDES_CIPHER_SUITES";
+
+ /**
+ * Creates a new <tt>ProtocolProviderFactory</tt>.
+ *
+ * @param bundleContext the bundle context reference of the service
+ * @param protocolName the name of the protocol
+ */
+ protected ProtocolProviderFactory(BundleContext bundleContext,
+ String protocolName)
+ {
+ this.bundleContext = bundleContext;
+ this.protocolName = protocolName;
+ }
+
+ /**
+ * Gets the <code>BundleContext</code> containing (or to contain) the
+ * service registration of this factory.
+ *
+ * @return the <code>BundleContext</code> containing (or to contain) the
+ * service registration of this factory
+ */
+ public BundleContext getBundleContext()
+ {
+ return bundleContext;
+ }
+
+ /**
+ * Initializes and creates an account corresponding to the specified
+ * accountProperties and registers the resulting ProtocolProvider in the
+ * <tt>context</tt> BundleContext parameter. Note that account
+ * registration is persistent and accounts that are registered during
+ * a particular sip-communicator session would be automatically reloaded
+ * during all following sessions until they are removed through the
+ * removeAccount method.
+ *
+ * @param userID the user identifier uniquely representing the newly
+ * created account within the protocol namespace.
+ * @param accountProperties a set of protocol (or implementation) specific
+ * properties defining the new account.
+ * @return the AccountID of the newly created account.
+ * @throws java.lang.IllegalArgumentException if userID does not correspond
+ * to an identifier in the context of the underlying protocol or if
+ * accountProperties does not contain a complete set of account installation
+ * properties.
+ * @throws java.lang.IllegalStateException if the account has already been
+ * installed.
+ * @throws java.lang.NullPointerException if any of the arguments is null.
+ */
+ public abstract AccountID installAccount(String userID,
+ Map<String, String> accountProperties)
+ throws IllegalArgumentException,
+ IllegalStateException,
+ NullPointerException;
+
+
+ /**
+ * Modifies the account corresponding to the specified accountID. This
+ * method is meant to be used to change properties of already existing
+ * accounts. Note that if the given accountID doesn't correspond to any
+ * registered account this method would do nothing.
+ *
+ * @param protocolProvider the protocol provider service corresponding to
+ * the modified account.
+ * @param accountProperties a set of protocol (or implementation) specific
+ * properties defining the new account.
+ *
+ * @throws java.lang.NullPointerException if any of the arguments is null.
+ */
+ public abstract void modifyAccount(
+ ProtocolProviderService protocolProvider,
+ Map<String, String> accountProperties)
+ throws NullPointerException;
+
+ /**
+ * Returns a copy of the list containing the <tt>AccountID</tt>s of all
+ * accounts currently registered in this protocol provider.
+ * @return a copy of the list containing the <tt>AccountID</tt>s of all
+ * accounts currently registered in this protocol provider.
+ */
+ public ArrayList<AccountID> getRegisteredAccounts()
+ {
+ synchronized (registeredAccounts)
+ {
+ return new ArrayList<AccountID>(registeredAccounts.keySet());
+ }
+ }
+
+ /**
+ * Returns the ServiceReference for the protocol provider corresponding to
+ * the specified accountID or null if the accountID is unknown.
+ * @param accountID the accountID of the protocol provider we'd like to get
+ * @return a ServiceReference object to the protocol provider with the
+ * specified account id and null if the account id is unknown to the
+ * provider factory.
+ */
+ public ServiceReference getProviderForAccount(AccountID accountID)
+ {
+ ServiceRegistration registration;
+
+ synchronized (registeredAccounts)
+ {
+ registration =
+ registeredAccounts.get(accountID);
+ }
+
+ try
+ {
+ return (registration == null) ? null : registration.getReference();
+ }
+ catch (IllegalStateException ise)
+ {
+ synchronized (registeredAccounts)
+ {
+ registeredAccounts.remove(accountID);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Removes the specified account from the list of accounts that this
+ * provider factory is handling. If the specified accountID is unknown to
+ * the ProtocolProviderFactory, the call has no effect and false is
+ * returned. This method is persistent in nature and once called the account
+ * corresponding to the specified ID will not be loaded during future runs
+ * of the project.
+ *
+ * @param accountID the ID of the account to remove.
+ * @return true if an account with the specified ID existed and was removed
+ * and false otherwise.
+ */
+ public boolean uninstallAccount(AccountID accountID)
+ {
+ // Unregister the protocol provider.
+ ServiceReference serRef = getProviderForAccount(accountID);
+
+ boolean wasAccountExisting = false;
+
+ // If the protocol provider service is registered, first unregister the
+ // service.
+ if (serRef != null)
+ {
+ BundleContext bundleContext = getBundleContext();
+ ProtocolProviderService protocolProvider =
+ (ProtocolProviderService) bundleContext.getService(serRef);
+
+ try
+ {
+ protocolProvider.unregister();
+ }
+ catch (OperationFailedException ex)
+ {
+ logger
+ .error("Failed to unregister protocol provider for account: "
+ + accountID + " caused by: " + ex);
+ }
+ }
+
+ ServiceRegistration registration;
+
+ synchronized (registeredAccounts)
+ {
+ registration = registeredAccounts.remove(accountID);
+ }
+
+ // first remove the stored account so when PP is unregistered we can
+ // distinguish between deleted or just disabled account
+ wasAccountExisting = removeStoredAccount(accountID);
+
+ if (registration != null)
+ {
+ // Kill the service.
+ registration.unregister();
+ }
+
+ return wasAccountExisting;
+ }
+
+ /**
+ * The method stores the specified account in the configuration service
+ * under the package name of the source factory. The restore and remove
+ * account methods are to be used to obtain access to and control the stored
+ * accounts.
+ * <p>
+ * In order to store all account properties, the method would create an
+ * entry in the configuration service corresponding (beginning with) the
+ * <tt>sourceFactory</tt>'s package name and add to it a unique identifier
+ * (e.g. the current miliseconds.)
+ * </p>
+ *
+ * @param accountID the AccountID corresponding to the account that we would
+ * like to store.
+ */
+ protected void storeAccount(AccountID accountID)
+ {
+ this.storeAccount(accountID, true);
+ }
+
+ /**
+ * The method stores the specified account in the configuration service
+ * under the package name of the source factory. The restore and remove
+ * account methods are to be used to obtain access to and control the stored
+ * accounts.
+ * <p>
+ * In order to store all account properties, the method would create an
+ * entry in the configuration service corresponding (beginning with) the
+ * <tt>sourceFactory</tt>'s package name and add to it a unique identifier
+ * (e.g. the current miliseconds.)
+ * </p>
+ *
+ * @param accountID the AccountID corresponding to the account that we would
+ * like to store.
+ * @param isModification if <tt>false</tt> there must be no such already
+ * loaded account, it <tt>true</tt> ist modification of an existing account.
+ * Usually we use this method with <tt>false</tt> in method installAccount
+ * and with <tt>true</tt> or the overridden method in method
+ * modifyAccount.
+ */
+ protected void storeAccount(AccountID accountID, boolean isModification)
+ {
+ if(!isModification
+ && getAccountManager().getStoredAccounts().contains(accountID))
+ {
+ throw new IllegalStateException(
+ "An account for id " + accountID.getUserID()
+ + " was already loaded!");
+ }
+
+ try
+ {
+ getAccountManager().storeAccount(this, accountID);
+ }
+ catch (OperationFailedException ofex)
+ {
+ throw new UndeclaredThrowableException(ofex);
+ }
+ }
+
+ /**
+ * Saves the password for the specified account after scrambling it a bit so
+ * that it is not visible from first sight. (The method remains highly
+ * insecure).
+ *
+ * @param accountID the AccountID for the account whose password we're
+ * storing
+ * @param password the password itself
+ *
+ * @throws IllegalArgumentException if no account corresponding to
+ * <code>accountID</code> has been previously stored
+ */
+ public void storePassword(AccountID accountID, String password)
+ throws IllegalArgumentException
+ {
+ try
+ {
+ storePassword(getBundleContext(), accountID, password);
+ }
+ catch (OperationFailedException ofex)
+ {
+ throw new UndeclaredThrowableException(ofex);
+ }
+ }
+
+ /**
+ * Saves the password for the specified account after scrambling it a bit
+ * so that it is not visible from first sight (Method remains highly
+ * insecure).
+ * <p>
+ * TODO Delegate the implementation to {@link AccountManager} because it
+ * knows the format in which the password (among the other account
+ * properties) is to be saved.
+ * </p>
+ *
+ * @param bundleContext a currently valid bundle context.
+ * @param accountID the <tt>AccountID</tt> of the account whose password is
+ * to be stored
+ * @param password the password to be stored
+ *
+ * @throws IllegalArgumentException if no account corresponding to
+ * <tt>accountID</tt> has been previously stored.
+ * @throws OperationFailedException if anything goes wrong while storing the
+ * specified <tt>password</tt>
+ */
+ protected void storePassword(BundleContext bundleContext,
+ AccountID accountID,
+ String password)
+ throws IllegalArgumentException,
+ OperationFailedException
+ {
+ String accountPrefix
+ = findAccountPrefix(
+ bundleContext,
+ accountID,
+ getFactoryImplPackageName());
+
+ if (accountPrefix == null)
+ {
+ throw
+ new IllegalArgumentException(
+ "No previous records found for account ID: "
+ + accountID.getAccountUniqueID()
+ + " in package"
+ + getFactoryImplPackageName());
+ }
+
+ CredentialsStorageService credentialsStorage
+ = ServiceUtils.getService(
+ bundleContext,
+ CredentialsStorageService.class);
+
+ if (!credentialsStorage.storePassword(accountPrefix, password))
+ {
+ throw
+ new OperationFailedException(
+ "CredentialsStorageService failed to storePassword",
+ OperationFailedException.GENERAL_ERROR);
+ }
+
+ // Update password property also in the AccountID
+ // to prevent it from being removed during account reload
+ // in some cases.
+ accountID.setPassword(password);
+
+ }
+
+ /**
+ * Returns the password last saved for the specified account.
+ *
+ * @param accountID the AccountID for the account whose password we're
+ * looking for
+ *
+ * @return a String containing the password for the specified accountID
+ */
+ public String loadPassword(AccountID accountID)
+ {
+ return loadPassword(getBundleContext(), accountID);
+ }
+
+ /**
+ * Returns the password last saved for the specified account.
+ * <p>
+ * TODO Delegate the implementation to {@link AccountManager} because it
+ * knows the format in which the password (among the other account
+ * properties) was saved.
+ * </p>
+ *
+ * @param bundleContext a currently valid bundle context.
+ * @param accountID the AccountID for the account whose password we're
+ * looking for..
+ *
+ * @return a String containing the password for the specified accountID.
+ */
+ protected String loadPassword(BundleContext bundleContext,
+ AccountID accountID)
+ {
+ String accountPrefix = findAccountPrefix(
+ bundleContext, accountID, getFactoryImplPackageName());
+
+ if (accountPrefix == null)
+ return null;
+
+ CredentialsStorageService credentialsStorage
+ = ServiceUtils.getService(
+ bundleContext,
+ CredentialsStorageService.class);
+
+ return credentialsStorage.loadPassword(accountPrefix);
+ }
+
+ /**
+ * Initializes and creates an account corresponding to the specified
+ * accountProperties and registers the resulting ProtocolProvider in the
+ * <tt>context</tt> BundleContext parameter. This method has a persistent
+ * effect. Once created the resulting account will remain installed until
+ * removed through the uninstallAccount method.
+ *
+ * @param accountProperties a set of protocol (or implementation) specific
+ * properties defining the new account.
+ * @return the AccountID of the newly loaded account
+ */
+ public AccountID loadAccount(Map<String, String> accountProperties)
+ {
+ AccountID accountID = createAccount(accountProperties);
+
+ loadAccount(accountID);
+
+ return accountID;
+ }
+
+ /**
+ * Creates a protocol provider for the given <tt>accountID</tt> and
+ * registers it in the bundle context. This method has a persistent
+ * effect. Once created the resulting account will remain installed until
+ * removed through the uninstallAccount method.
+ *
+ * @param accountID the account identifier
+ * @return <tt>true</tt> if the account with the given <tt>accountID</tt> is
+ * successfully loaded, otherwise returns <tt>false</tt>
+ */
+ public boolean loadAccount(AccountID accountID)
+ {
+ // Need to obtain the original user id property, instead of calling
+ // accountID.getUserID(), because this method could return a modified
+ // version of the user id property.
+ String userID = accountID
+ .getAccountPropertyString(ProtocolProviderFactory.USER_ID);
+
+ ProtocolProviderService service = createService(userID, accountID);
+
+ Dictionary<String, String> properties = new Hashtable<String, String>();
+ properties.put(PROTOCOL, protocolName);
+ properties.put(USER_ID, userID);
+
+ ServiceRegistration serviceRegistration =
+ bundleContext.registerService(ProtocolProviderService.class
+ .getName(), service, properties);
+
+ if (serviceRegistration == null)
+ return false;
+ else
+ {
+ synchronized (registeredAccounts)
+ {
+ registeredAccounts.put(accountID, serviceRegistration);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Unloads the account corresponding to the given <tt>accountID</tt>.
+ * Unregisters the corresponding protocol provider, but keeps the account in
+ * contrast to the uninstallAccount method.
+ *
+ * @param accountID the account identifier
+ * @return true if an account with the specified ID existed and was unloaded
+ * and false otherwise.
+ */
+ public boolean unloadAccount(AccountID accountID)
+ {
+ // Unregister the protocol provider.
+ ServiceReference serRef = getProviderForAccount(accountID);
+
+ if (serRef == null)
+ {
+ return false;
+ }
+
+ BundleContext bundleContext = getBundleContext();
+ ProtocolProviderService protocolProvider =
+ (ProtocolProviderService) bundleContext.getService(serRef);
+
+ try
+ {
+ protocolProvider.unregister();
+ }
+ catch (OperationFailedException ex)
+ {
+ logger
+ .error("Failed to unregister protocol provider for account : "
+ + accountID + " caused by: " + ex);
+ }
+
+ ServiceRegistration registration;
+
+ synchronized (registeredAccounts)
+ {
+ registration = registeredAccounts.remove(accountID);
+ }
+ if (registration == null)
+ {
+ return false;
+ }
+
+ // Kill the service.
+ registration.unregister();
+
+ return true;
+ }
+
+ /**
+ * Initializes and creates an account corresponding to the specified
+ * accountProperties.
+ *
+ * @param accountProperties a set of protocol (or implementation) specific
+ * properties defining the new account.
+ * @return the AccountID of the newly created account
+ */
+ public AccountID createAccount(Map<String, String> accountProperties)
+ {
+ BundleContext bundleContext = getBundleContext();
+ if (bundleContext == null)
+ throw new NullPointerException(
+ "The specified BundleContext was null");
+
+ if (accountProperties == null)
+ throw new NullPointerException(
+ "The specified property map was null");
+
+ String userID = accountProperties.get(USER_ID);
+ if (userID == null)
+ throw new NullPointerException(
+ "The account properties contained no user id.");
+
+ String protocolName = getProtocolName();
+ if (!accountProperties.containsKey(PROTOCOL))
+ accountProperties.put(PROTOCOL, protocolName);
+
+ return createAccountID(userID, accountProperties);
+ }
+
+ /**
+ * Creates a new <code>AccountID</code> instance with a specific user ID to
+ * represent a given set of account properties.
+ * <p>
+ * The method is a pure factory allowing implementers to specify the runtime
+ * type of the created <code>AccountID</code> and customize the instance.
+ * The returned <code>AccountID</code> will later be associated with a
+ * <code>ProtocolProviderService</code> by the caller (e.g. using
+ * {@link #createService(String, AccountID)}).
+ * </p>
+ *
+ * @param userID the user ID of the new instance
+ * @param accountProperties the set of properties to be represented by the
+ * new instance
+ * @return a new <code>AccountID</code> instance with the specified user ID
+ * representing the given set of account properties
+ */
+ protected abstract AccountID createAccountID(
+ String userID, Map<String, String> accountProperties);
+
+ /**
+ * Gets the name of the protocol this factory registers its
+ * <code>ProtocolProviderService</code>s with and to be placed in the
+ * properties of the accounts created by this factory.
+ *
+ * @return the name of the protocol this factory registers its
+ * <code>ProtocolProviderService</code>s with and to be placed in
+ * the properties of the accounts created by this factory
+ */
+ public String getProtocolName()
+ {
+ return protocolName;
+ }
+
+ /**
+ * Initializes a new <code>ProtocolProviderService</code> instance with a
+ * specific user ID to represent a specific <code>AccountID</code>.
+ * <p>
+ * The method is a pure factory allowing implementers to specify the runtime
+ * type of the created <code>ProtocolProviderService</code> and customize
+ * the instance. The caller will later register the returned service with
+ * the <code>BundleContext</code> of this factory.
+ * </p>
+ *
+ * @param userID the user ID to initialize the new instance with
+ * @param accountID the <code>AccountID</code> to be represented by the new
+ * instance
+ * @return a new <code>ProtocolProviderService</code> instance with the
+ * specific user ID representing the specified
+ * <code>AccountID</code>
+ */
+ protected abstract ProtocolProviderService createService(String userID,
+ AccountID accountID);
+
+ /**
+ * Removes the account with <tt>accountID</tt> from the set of accounts
+ * that are persistently stored inside the configuration service.
+ *
+ * @param accountID the AccountID of the account to remove.
+ *
+ * @return true if an account has been removed and false otherwise.
+ */
+ protected boolean removeStoredAccount(AccountID accountID)
+ {
+ return getAccountManager().removeStoredAccount(this, accountID);
+ }
+
+ /**
+ * Returns the prefix for all persistently stored properties of the account
+ * with the specified id.
+ * @param bundleContext a currently valid bundle context.
+ * @param accountID the AccountID of the account whose properties we're
+ * looking for.
+ * @param sourcePackageName a String containing the package name of the
+ * concrete factory class that extends us.
+ * @return a String indicating the ConfigurationService property name
+ * prefix under which all account properties are stored or null if no
+ * account corresponding to the specified id was found.
+ */
+ public static String findAccountPrefix(BundleContext bundleContext,
+ AccountID accountID,
+ String sourcePackageName)
+ {
+ ServiceReference confReference
+ = bundleContext.getServiceReference(
+ ConfigurationService.class.getName());
+ ConfigurationService configurationService
+ = (ConfigurationService) bundleContext.getService(confReference);
+
+ //first retrieve all accounts that we've registered
+ List<String> storedAccounts =
+ configurationService.getPropertyNamesByPrefix(sourcePackageName,
+ true);
+
+ //find an account with the corresponding id.
+ for (String accountRootPropertyName : storedAccounts)
+ {
+ //unregister the account in the configuration service.
+ //all the properties must have been registered in the following
+ //hierarchy:
+ //net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME
+ String accountUID = configurationService.getString(
+ accountRootPropertyName //node id
+ + "." + ACCOUNT_UID); // propname
+
+ if (accountID.getAccountUniqueID().equals(accountUID))
+ {
+ return accountRootPropertyName;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the name of the package that we're currently running in (i.e.
+ * the name of the package containing the proto factory that extends us).
+ *
+ * @return a String containing the package name of the concrete factory
+ * class that extends us.
+ */
+ private String getFactoryImplPackageName()
+ {
+ String className = getClass().getName();
+
+ return className.substring(0, className.lastIndexOf('.'));
+ }
+
+ /**
+ * Prepares the factory for bundle shutdown.
+ */
+ public void stop()
+ {
+ if (logger.isTraceEnabled())
+ logger.trace("Preparing to stop all protocol providers of" + this);
+
+ synchronized (registeredAccounts)
+ {
+ for (Enumeration<ServiceRegistration> registrations =
+ registeredAccounts.elements(); registrations.hasMoreElements();)
+ {
+ ServiceRegistration reg = registrations.nextElement();
+
+ stop(reg);
+
+ reg.unregister();
+ }
+
+ registeredAccounts.clear();
+ }
+ }
+
+ /**
+ * Shuts down the <code>ProtocolProviderService</code> representing an
+ * account registered with this factory.
+ *
+ * @param registeredAccount the <code>ServiceRegistration</code> of the
+ * <code>ProtocolProviderService</code> representing an account
+ * registered with this factory
+ */
+ protected void stop(ServiceRegistration registeredAccount)
+ {
+ ProtocolProviderService protocolProviderService =
+ (ProtocolProviderService) getBundleContext().getService(
+ registeredAccount.getReference());
+
+ protocolProviderService.shutdown();
+ }
+
+ /**
+ * Get the <tt>AccountManager</tt> of the protocol.
+ *
+ * @return <tt>AccountManager</tt> of the protocol
+ */
+ private AccountManager getAccountManager()
+ {
+ BundleContext bundleContext = getBundleContext();
+ ServiceReference serviceReference =
+ bundleContext.getServiceReference(AccountManager.class.getName());
+
+ return (AccountManager) bundleContext.getService(serviceReference);
+ }
+
+
+ /**
+ * Finds registered <tt>ProtocolProviderFactory</tt> for given
+ * <tt>protocolName</tt>.
+ * @param bundleContext the OSGI bundle context that will be used.
+ * @param protocolName the protocol name.
+ * @return Registered <tt>ProtocolProviderFactory</tt> for given protocol
+ * name or <tt>null</tt> if no provider was found.
+ */
+ static public ProtocolProviderFactory getProtocolProviderFactory(
+ BundleContext bundleContext,
+ String protocolName)
+ {
+ ServiceReference[] serRefs = null;
+
+ String osgiFilter =
+ "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")";
+
+ try
+ {
+ serRefs =
+ bundleContext.getServiceReferences(
+ ProtocolProviderFactory.class.getName(), osgiFilter);
+ }
+ catch (InvalidSyntaxException ex)
+ {
+ logger.error(ex);
+ return null;
+ }
+
+ return (ProtocolProviderFactory) bundleContext.getService(serRefs[0]);
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java index 2b33ea2..9acb782 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallConference.java @@ -1,616 +1,616 @@ -/* - * 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.media; - -import java.beans.*; -import java.lang.ref.*; -import java.util.*; - -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.device.*; -import org.jitsi.util.*; -import org.jitsi.util.event.*; - -import net.java.sip.communicator.service.protocol.*; - -/** - * Extends <tt>CallConference</tt> to represent the media-specific information - * associated with the telephony conference-related state of a - * <tt>MediaAwareCall</tt>. - * - * @author Lyubomir Marinov - */ -public class MediaAwareCallConference - extends CallConference -{ - /** - * The <tt>PropertyChangeListener</tt> which will listen to the - * <tt>MediaService</tt> about <tt>PropertyChangeEvent</tt>s. - */ - private static WeakPropertyChangeListener - mediaServicePropertyChangeListener; - - /** - * The <tt>MediaDevice</tt>s indexed by <tt>MediaType</tt> ordinal which are - * to be used by this telephony conference for media capture and/or - * playback. If the <tt>MediaDevice</tt> for a specific <tt>MediaType</tt> - * is <tt>null</tt>, - * {@link MediaService#getDefaultDevice(MediaType, MediaUseCase)} is called. - */ - private final MediaDevice[] devices; - - /** - * The <tt>MediaDevice</tt>s which implement media mixing on the respective - * <tt>MediaDevice</tt> in {@link #devices} for the purposes of this - * telephony conference. - */ - private final MediaDevice[] mixers; - - /** - * The <tt>VolumeControl</tt> implementation which is to control the volume - * (level) of the audio played back the telephony conference represented by - * this instance. - */ - private final VolumeControl outputVolumeControl - = new BasicVolumeControl( - VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME); - - /** - * The <tt>PropertyChangeListener</tt> which listens to sources of - * <tt>PropertyChangeEvent</tt>s on behalf of this instance. - */ - private final PropertyChangeListener propertyChangeListener - = new PropertyChangeListener() - { - public void propertyChange(PropertyChangeEvent ev) - { - MediaAwareCallConference.this.propertyChange(ev); - } - }; - - /** - * The <tt>RTPTranslator</tt> which forwards video RTP and RTCP traffic - * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in - * this telephony conference when the local peer is acting as a conference - * focus. - */ - private RTPTranslator videoRTPTranslator; - - /** - * Initializes a new <tt>MediaAwareCallConference</tt> instance. - */ - public MediaAwareCallConference() - { - this(false); - } - - /** - * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to - * optionally utilize the Jitsi VideoBridge server-side telephony - * conferencing technology. - * - * @param jitsiVideoBridge <tt>true</tt> if the telephony conference - * represented by the new instance is to utilize the Jitsi VideoBridge - * server-side telephony conferencing technology; otherwise, <tt>false</tt> - */ - public MediaAwareCallConference(boolean jitsiVideoBridge) - { - super(jitsiVideoBridge); - - int mediaTypeCount = MediaType.values().length; - - devices = new MediaDevice[mediaTypeCount]; - mixers = new MediaDevice[mediaTypeCount]; - - /* - * Listen to the MediaService in order to reflect changes in the user's - * selection with respect to the default media device. - */ - addMediaServicePropertyChangeListener(propertyChangeListener); - } - - /** - * Adds a specific <tt>PropertyChangeListener</tt> to be notified about - * <tt>PropertyChangeEvent</tt>s fired by the current <tt>MediaService</tt> - * implementation. The implementation adds a <tt>WeakReference</tt> to the - * specified <tt>listener</tt> because <tt>MediaAwareCallConference</tt> - * is unable to determine when the <tt>PropertyChangeListener</tt> is to be - * removed. - * - * @param listener the <tt>PropertyChangeListener</tt> to add - */ - private static synchronized void addMediaServicePropertyChangeListener( - PropertyChangeListener listener) - { - if (mediaServicePropertyChangeListener == null) - { - final MediaService mediaService - = ProtocolMediaActivator.getMediaService(); - - if (mediaService != null) - { - mediaServicePropertyChangeListener - = new WeakPropertyChangeListener() - { - @Override - protected void addThisToNotifier() - { - mediaService.addPropertyChangeListener(this); - } - - @Override - protected void removeThisFromNotifier() - { - mediaService.removePropertyChangeListener(this); - } - }; - } - } - if (mediaServicePropertyChangeListener != null) - { - mediaServicePropertyChangeListener.addPropertyChangeListener( - listener); - } - } - - /** - * {@inheritDoc} - * - * If this telephony conference switches from being a conference focus to - * not being such, disposes of the mixers used by this instance when it was - * a conference focus - */ - @Override - protected void conferenceFocusChanged(boolean oldValue, boolean newValue) - { - /* - * If this telephony conference switches from being a conference - * focus to not being one, dispose of the mixers used when it was a - * conference focus. - */ - if (oldValue && !newValue) - { - Arrays.fill(mixers, null); - if (videoRTPTranslator != null) - { - videoRTPTranslator.dispose(); - videoRTPTranslator = null; - } - } - - super.conferenceFocusChanged(oldValue, newValue); - } - - /** - * {@inheritDoc} - * - * Disposes of <tt>this.videoRTPTranslator</tt> if the removed <tt>Call</tt> - * was the last <tt>Call</tt> in this <tt>CallConference</tt>. - * - * @param call the <tt>Call</tt> which has been removed from the list of - * <tt>Call</tt>s participating in this telephony conference. - */ - @Override - protected void callRemoved(Call call) - { - super.callRemoved(call); - - if (getCallCount() == 0 && (videoRTPTranslator != null)) - { - videoRTPTranslator.dispose(); - videoRTPTranslator = null; - } - } - - /** - * Gets a <tt>MediaDevice</tt> which is capable of capture and/or playback - * of media of the specified <tt>MediaType</tt> and is the default choice of - * the user with respect to such a <tt>MediaDevice</tt>. - * - * @param mediaType the <tt>MediaType</tt> in which the retrieved - * <tt>MediaDevice</tt> is to capture and/or play back media - * @param useCase the <tt>MediaUseCase</tt> associated with the intended - * utilization of the <tt>MediaDevice</tt> to be retrieved - * @return a <tt>MediaDevice</tt> which is capable of capture and/or - * playback of media of the specified <tt>mediaType</tt> and is the default - * choice of the user with respect to such a <tt>MediaDevice</tt> - */ - public MediaDevice getDefaultDevice( - MediaType mediaType, - MediaUseCase useCase) - { - int mediaTypeIndex = mediaType.ordinal(); - MediaDevice device = devices[mediaTypeIndex]; - MediaService mediaService = ProtocolMediaActivator.getMediaService(); - - if (device == null) - device = mediaService.getDefaultDevice(mediaType, useCase); - - /* - * Make sure that the device is capable of mixing in order to support - * conferencing and call recording. - */ - if (device != null) - { - MediaDevice mixer = mixers[mediaTypeIndex]; - - if (mixer == null) - { - switch (mediaType) - { - case AUDIO: - /* - * TODO AudioMixer leads to very poor audio quality on - * Android so do not use it unless it is really really - * necessary. - */ - if ((!OSUtils.IS_ANDROID || isConferenceFocus()) - /* - * We can use the AudioMixer only if the device is - * able to capture (because the AudioMixer will push - * when the capture device pushes). - */ - && device.getDirection().allowsSending()) - { - mixer = mediaService.createMixer(device); - } - break; - - case VIDEO: - if (isConferenceFocus()) - mixer = mediaService.createMixer(device); - break; - } - - mixers[mediaTypeIndex] = mixer; - } - - if (mixer != null) - device = mixer; - } - - return device; - } - - /** - * Gets the <tt>VolumeControl</tt> which controls the volume (level) of the - * audio played back in the telephony conference represented by this - * instance. - * - * @return the <tt>VolumeControl</tt> which controls the volume (level) of - * the audio played back in the telephony conference represented by this - * instance - */ - public VolumeControl getOutputVolumeControl() - { - return outputVolumeControl; - } - - /** - * Gets the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic - * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in - * this telephony conference when the local peer is acting as a conference - * focus. - * - * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which - * RTP and RTCP traffic is to be forwarded between - * @return the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic - * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in - * this telephony conference when the local peer is acting as a conference - * focus - */ - public RTPTranslator getRTPTranslator(MediaType mediaType) - { - RTPTranslator rtpTranslator = null; - - /* - * XXX A mixer is created for audio even when the local peer is not a - * conference focus in order to enable additional functionality. - * Similarly, the videoRTPTranslator is created even when the local peer - * is not a conference focus in order to enable the local peer to turn - * into a conference focus at a later time. More specifically, - * MediaStreamImpl is unable to accommodate an RTPTranslator after it - * has created its RTPManager. Yet again like the audio mixer, we'd - * better not try to use it on Android at this time because of - * performance issues that might arise. - */ - if (MediaType.VIDEO.equals(mediaType) - && (!OSUtils.IS_ANDROID || isConferenceFocus())) - { - if (videoRTPTranslator == null) - { - videoRTPTranslator - = ProtocolMediaActivator - .getMediaService() - .createRTPTranslator(); - } - rtpTranslator = videoRTPTranslator; - } - return rtpTranslator; - } - - /** - * Notifies this <tt>MediaAwareCallConference</tt> about changes in the - * values of the properties of sources of <tt>PropertyChangeEvent</tt>s. For - * example, this instance listens to changes of the value of - * {@link MediaService#DEFAULT_DEVICE} which represents the user's choice - * with respect to the default audio device. - * - * @param ev a <tt>PropertyChangeEvent</tt> which specifies the name of the - * property which had its value changed and the old and new values of that - * property - */ - private void propertyChange(PropertyChangeEvent ev) - { - String propertyName = ev.getPropertyName(); - - if (MediaService.DEFAULT_DEVICE.equals(propertyName)) - { - Object source = ev.getSource(); - - if (source instanceof MediaService) - { - /* - * XXX We only support changing the default audio device at the - * time of this writing. - */ - int mediaTypeIndex = MediaType.AUDIO.ordinal(); - MediaDevice mixer = mixers[mediaTypeIndex]; - MediaDevice oldValue - = (mixer instanceof MediaDeviceWrapper) - ? ((MediaDeviceWrapper) mixer).getWrappedDevice() - : null; - MediaDevice newValue = devices[mediaTypeIndex]; - - if (newValue == null) - { - newValue - = ProtocolMediaActivator - .getMediaService() - .getDefaultDevice( - MediaType.AUDIO, - MediaUseCase.ANY); - } - - /* - * XXX If MediaService#getDefaultDevice(MediaType, MediaUseCase) - * above returns null and its earlier return value was not null, - * we will not notify of an actual change in the value of the - * user's choice with respect to the default audio device. - */ - if (oldValue != newValue) - { - mixers[mediaTypeIndex] = null; - firePropertyChange( - MediaAwareCall.DEFAULT_DEVICE, - oldValue, newValue); - } - } - } - } - - /** - * Sets the <tt>MediaDevice</tt> to be used by this telephony conference for - * capture and/or playback of media of a specific <tt>MediaType</tt>. - * - * @param mediaType the <tt>MediaType</tt> of the media which is to be - * captured and/or played back by the specified <tt>device</tt> - * @param device the <tt>MediaDevice</tt> to be used by this telephony - * conference for capture and/or playback of media of the specified - * <tt>mediaType</tt> - */ - void setDevice(MediaType mediaType, MediaDevice device) - { - int mediaTypeIndex = mediaType.ordinal(); - MediaDevice oldValue = devices[mediaTypeIndex]; - - /* - * XXX While we know the old and the new master/wrapped devices, we - * are not sure whether the mixer has been used. Anyway, we have to - * report different values in order to have PropertyChangeSupport - * really fire an event. - */ - MediaDevice mixer = mixers[mediaTypeIndex]; - - if (mixer instanceof MediaDeviceWrapper) - oldValue = ((MediaDeviceWrapper) mixer).getWrappedDevice(); - - MediaDevice newValue = devices[mediaTypeIndex] = device; - - if (oldValue != newValue) - { - mixers[mediaTypeIndex] = null; - firePropertyChange( - MediaAwareCall.DEFAULT_DEVICE, - oldValue, newValue); - } - } - - /** - * Implements a <tt>PropertyChangeListener</tt> which weakly references and - * delegates to specific <tt>PropertyChangeListener</tt>s and automatically - * adds itself to and removes itself from a specific - * <tt>PropertyChangeNotifier</tt> depending on whether there are - * <tt>PropertyChangeListener</tt>s to delegate to. Thus enables listening - * to a <tt>PropertyChangeNotifier</tt> by invoking - * {@link PropertyChangeNotifier#addPropertyChangeListener( - * PropertyChangeListener)} without - * {@link PropertyChangeNotifier#removePropertyChangeListener( - * PropertyChangeListener)}. - */ - private static class WeakPropertyChangeListener - implements PropertyChangeListener - { - /** - * The indicator which determines whether this - * <tt>PropertyChangeListener</tt> has been added to {@link #notifier}. - */ - private boolean added = false; - - /** - * The list of <tt>PropertyChangeListener</tt>s which are to be notified - * about <tt>PropertyChangeEvent</tt>s fired by {@link #notifier}. - */ - private final List<WeakReference<PropertyChangeListener>> listeners - = new LinkedList<WeakReference<PropertyChangeListener>>(); - - /** - * The <tt>PropertyChangeNotifier</tt> this instance is to listen to - * about <tt>PropertyChangeEvent</tt>s which are to be forwarded to - * {@link #listeners}. - */ - private final PropertyChangeNotifier notifier; - - /** - * Initializes a new <tt>WeakPropertyChangeListener</tt> instance. - */ - protected WeakPropertyChangeListener() - { - this(null); - } - - /** - * Initializes a new <tt>WeakPropertyChangeListener</tt> instance which - * is to listen to a specific <tt>PropertyChangeNotifier</tt>. - * - * @param notifier the <tt>PropertyChangeNotifier</tt> the new instance - * is to listen to - */ - public WeakPropertyChangeListener(PropertyChangeNotifier notifier) - { - this.notifier = notifier; - } - - /** - * Adds a specific <tt>PropertyChangeListener</tt> to the list of - * <tt>PropertyChangeListener</tt>s to be notified about - * <tt>PropertyChangeEvent</tt>s fired by the - * <tt>PropertyChangeNotifier</tt> associated with this instance. - * - * @param listener the <tt>PropertyChangeListener</tt> to add - */ - public synchronized void addPropertyChangeListener( - PropertyChangeListener listener) - { - Iterator<WeakReference<PropertyChangeListener>> i - = listeners.iterator(); - boolean add = true; - - while (i.hasNext()) - { - PropertyChangeListener l = i.next().get(); - - if (l == null) - i.remove(); - else if (l.equals(listener)) - add = false; - } - if (add - && listeners.add( - new WeakReference<PropertyChangeListener>(listener)) - && !this.added) - { - addThisToNotifier(); - this.added = true; - } - } - - /** - * Adds this as a <tt>PropertyChangeListener</tt> to {@link #notifier}. - */ - protected void addThisToNotifier() - { - if (notifier != null) - notifier.addPropertyChangeListener(this); - } - - /** - * {@inheritDoc} - * - * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by - * {@link #notifier}. - */ - public void propertyChange(PropertyChangeEvent ev) - { - PropertyChangeListener[] ls; - int n; - - synchronized (this) - { - Iterator<WeakReference<PropertyChangeListener>> i - = listeners.iterator(); - - ls = new PropertyChangeListener[listeners.size()]; - n = 0; - while (i.hasNext()) - { - PropertyChangeListener l = i.next().get(); - - if (l == null) - i.remove(); - else - ls[n++] = l; - } - if ((n == 0) && this.added) - { - removeThisFromNotifier(); - this.added = false; - } - } - - if (n != 0) - { - for (PropertyChangeListener l : ls) - { - if (l == null) - break; - else - l.propertyChange(ev); - } - } - } - - /** - * Removes a specific <tt>PropertyChangeListener</tt> from the list of - * <tt>PropertyChangeListener</tt>s to be notified about - * <tt>PropertyChangeEvent</tt>s fired by the - * <tt>PropertyChangeNotifier</tt> associated with this instance. - * - * @param listener the <tt>PropertyChangeListener</tt> to remove - */ - @SuppressWarnings("unused") - public synchronized void removePropertyChangeListener( - PropertyChangeListener listener) - { - Iterator<WeakReference<PropertyChangeListener>> i - = listeners.iterator(); - - while (i.hasNext()) - { - PropertyChangeListener l = i.next().get(); - - if ((l == null) || l.equals(listener)) - i.remove(); - } - if (this.added && (listeners.size() == 0)) - { - removeThisFromNotifier(); - this.added = false; - } - } - - /** - * Removes this as a <tt>PropertyChangeListener</tt> from - * {@link #notifier}. - */ - protected void removeThisFromNotifier() - { - if (notifier != null) - notifier.removePropertyChangeListener(this); - } - } -} +/*
+ * 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.media;
+
+import java.beans.*;
+import java.lang.ref.*;
+import java.util.*;
+
+import org.jitsi.service.neomedia.*;
+import org.jitsi.service.neomedia.device.*;
+import org.jitsi.util.*;
+import org.jitsi.util.event.*;
+
+import net.java.sip.communicator.service.protocol.*;
+
+/**
+ * Extends <tt>CallConference</tt> to represent the media-specific information
+ * associated with the telephony conference-related state of a
+ * <tt>MediaAwareCall</tt>.
+ *
+ * @author Lyubomir Marinov
+ */
+public class MediaAwareCallConference
+ extends CallConference
+{
+ /**
+ * The <tt>PropertyChangeListener</tt> which will listen to the
+ * <tt>MediaService</tt> about <tt>PropertyChangeEvent</tt>s.
+ */
+ private static WeakPropertyChangeListener
+ mediaServicePropertyChangeListener;
+
+ /**
+ * The <tt>MediaDevice</tt>s indexed by <tt>MediaType</tt> ordinal which are
+ * to be used by this telephony conference for media capture and/or
+ * playback. If the <tt>MediaDevice</tt> for a specific <tt>MediaType</tt>
+ * is <tt>null</tt>,
+ * {@link MediaService#getDefaultDevice(MediaType, MediaUseCase)} is called.
+ */
+ private final MediaDevice[] devices;
+
+ /**
+ * The <tt>MediaDevice</tt>s which implement media mixing on the respective
+ * <tt>MediaDevice</tt> in {@link #devices} for the purposes of this
+ * telephony conference.
+ */
+ private final MediaDevice[] mixers;
+
+ /**
+ * The <tt>VolumeControl</tt> implementation which is to control the volume
+ * (level) of the audio played back the telephony conference represented by
+ * this instance.
+ */
+ private final VolumeControl outputVolumeControl
+ = new BasicVolumeControl(
+ VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME);
+
+ /**
+ * The <tt>PropertyChangeListener</tt> which listens to sources of
+ * <tt>PropertyChangeEvent</tt>s on behalf of this instance.
+ */
+ private final PropertyChangeListener propertyChangeListener
+ = new PropertyChangeListener()
+ {
+ public void propertyChange(PropertyChangeEvent ev)
+ {
+ MediaAwareCallConference.this.propertyChange(ev);
+ }
+ };
+
+ /**
+ * The <tt>RTPTranslator</tt> which forwards video RTP and RTCP traffic
+ * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
+ * this telephony conference when the local peer is acting as a conference
+ * focus.
+ */
+ private RTPTranslator videoRTPTranslator;
+
+ /**
+ * Initializes a new <tt>MediaAwareCallConference</tt> instance.
+ */
+ public MediaAwareCallConference()
+ {
+ this(false);
+ }
+
+ /**
+ * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to
+ * optionally utilize the Jitsi Videobridge server-side telephony
+ * conferencing technology.
+ *
+ * @param jitsiVideobridge <tt>true</tt> if the telephony conference
+ * represented by the new instance is to utilize the Jitsi Videobridge
+ * server-side telephony conferencing technology; otherwise, <tt>false</tt>
+ */
+ public MediaAwareCallConference(boolean jitsiVideobridge)
+ {
+ super(jitsiVideobridge);
+
+ int mediaTypeCount = MediaType.values().length;
+
+ devices = new MediaDevice[mediaTypeCount];
+ mixers = new MediaDevice[mediaTypeCount];
+
+ /*
+ * Listen to the MediaService in order to reflect changes in the user's
+ * selection with respect to the default media device.
+ */
+ addMediaServicePropertyChangeListener(propertyChangeListener);
+ }
+
+ /**
+ * Adds a specific <tt>PropertyChangeListener</tt> to be notified about
+ * <tt>PropertyChangeEvent</tt>s fired by the current <tt>MediaService</tt>
+ * implementation. The implementation adds a <tt>WeakReference</tt> to the
+ * specified <tt>listener</tt> because <tt>MediaAwareCallConference</tt>
+ * is unable to determine when the <tt>PropertyChangeListener</tt> is to be
+ * removed.
+ *
+ * @param listener the <tt>PropertyChangeListener</tt> to add
+ */
+ private static synchronized void addMediaServicePropertyChangeListener(
+ PropertyChangeListener listener)
+ {
+ if (mediaServicePropertyChangeListener == null)
+ {
+ final MediaService mediaService
+ = ProtocolMediaActivator.getMediaService();
+
+ if (mediaService != null)
+ {
+ mediaServicePropertyChangeListener
+ = new WeakPropertyChangeListener()
+ {
+ @Override
+ protected void addThisToNotifier()
+ {
+ mediaService.addPropertyChangeListener(this);
+ }
+
+ @Override
+ protected void removeThisFromNotifier()
+ {
+ mediaService.removePropertyChangeListener(this);
+ }
+ };
+ }
+ }
+ if (mediaServicePropertyChangeListener != null)
+ {
+ mediaServicePropertyChangeListener.addPropertyChangeListener(
+ listener);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * If this telephony conference switches from being a conference focus to
+ * not being such, disposes of the mixers used by this instance when it was
+ * a conference focus
+ */
+ @Override
+ protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
+ {
+ /*
+ * If this telephony conference switches from being a conference
+ * focus to not being one, dispose of the mixers used when it was a
+ * conference focus.
+ */
+ if (oldValue && !newValue)
+ {
+ Arrays.fill(mixers, null);
+ if (videoRTPTranslator != null)
+ {
+ videoRTPTranslator.dispose();
+ videoRTPTranslator = null;
+ }
+ }
+
+ super.conferenceFocusChanged(oldValue, newValue);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Disposes of <tt>this.videoRTPTranslator</tt> if the removed <tt>Call</tt>
+ * was the last <tt>Call</tt> in this <tt>CallConference</tt>.
+ *
+ * @param call the <tt>Call</tt> which has been removed from the list of
+ * <tt>Call</tt>s participating in this telephony conference.
+ */
+ @Override
+ protected void callRemoved(Call call)
+ {
+ super.callRemoved(call);
+
+ if (getCallCount() == 0 && (videoRTPTranslator != null))
+ {
+ videoRTPTranslator.dispose();
+ videoRTPTranslator = null;
+ }
+ }
+
+ /**
+ * Gets a <tt>MediaDevice</tt> which is capable of capture and/or playback
+ * of media of the specified <tt>MediaType</tt> and is the default choice of
+ * the user with respect to such a <tt>MediaDevice</tt>.
+ *
+ * @param mediaType the <tt>MediaType</tt> in which the retrieved
+ * <tt>MediaDevice</tt> is to capture and/or play back media
+ * @param useCase the <tt>MediaUseCase</tt> associated with the intended
+ * utilization of the <tt>MediaDevice</tt> to be retrieved
+ * @return a <tt>MediaDevice</tt> which is capable of capture and/or
+ * playback of media of the specified <tt>mediaType</tt> and is the default
+ * choice of the user with respect to such a <tt>MediaDevice</tt>
+ */
+ public MediaDevice getDefaultDevice(
+ MediaType mediaType,
+ MediaUseCase useCase)
+ {
+ int mediaTypeIndex = mediaType.ordinal();
+ MediaDevice device = devices[mediaTypeIndex];
+ MediaService mediaService = ProtocolMediaActivator.getMediaService();
+
+ if (device == null)
+ device = mediaService.getDefaultDevice(mediaType, useCase);
+
+ /*
+ * Make sure that the device is capable of mixing in order to support
+ * conferencing and call recording.
+ */
+ if (device != null)
+ {
+ MediaDevice mixer = mixers[mediaTypeIndex];
+
+ if (mixer == null)
+ {
+ switch (mediaType)
+ {
+ case AUDIO:
+ /*
+ * TODO AudioMixer leads to very poor audio quality on
+ * Android so do not use it unless it is really really
+ * necessary.
+ */
+ if ((!OSUtils.IS_ANDROID || isConferenceFocus())
+ /*
+ * We can use the AudioMixer only if the device is
+ * able to capture (because the AudioMixer will push
+ * when the capture device pushes).
+ */
+ && device.getDirection().allowsSending())
+ {
+ mixer = mediaService.createMixer(device);
+ }
+ break;
+
+ case VIDEO:
+ if (isConferenceFocus())
+ mixer = mediaService.createMixer(device);
+ break;
+ }
+
+ mixers[mediaTypeIndex] = mixer;
+ }
+
+ if (mixer != null)
+ device = mixer;
+ }
+
+ return device;
+ }
+
+ /**
+ * Gets the <tt>VolumeControl</tt> which controls the volume (level) of the
+ * audio played back in the telephony conference represented by this
+ * instance.
+ *
+ * @return the <tt>VolumeControl</tt> which controls the volume (level) of
+ * the audio played back in the telephony conference represented by this
+ * instance
+ */
+ public VolumeControl getOutputVolumeControl()
+ {
+ return outputVolumeControl;
+ }
+
+ /**
+ * Gets the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic
+ * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
+ * this telephony conference when the local peer is acting as a conference
+ * focus.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which
+ * RTP and RTCP traffic is to be forwarded between
+ * @return the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic
+ * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in
+ * this telephony conference when the local peer is acting as a conference
+ * focus
+ */
+ public RTPTranslator getRTPTranslator(MediaType mediaType)
+ {
+ RTPTranslator rtpTranslator = null;
+
+ /*
+ * XXX A mixer is created for audio even when the local peer is not a
+ * conference focus in order to enable additional functionality.
+ * Similarly, the videoRTPTranslator is created even when the local peer
+ * is not a conference focus in order to enable the local peer to turn
+ * into a conference focus at a later time. More specifically,
+ * MediaStreamImpl is unable to accommodate an RTPTranslator after it
+ * has created its RTPManager. Yet again like the audio mixer, we'd
+ * better not try to use it on Android at this time because of
+ * performance issues that might arise.
+ */
+ if (MediaType.VIDEO.equals(mediaType)
+ && (!OSUtils.IS_ANDROID || isConferenceFocus()))
+ {
+ if (videoRTPTranslator == null)
+ {
+ videoRTPTranslator
+ = ProtocolMediaActivator
+ .getMediaService()
+ .createRTPTranslator();
+ }
+ rtpTranslator = videoRTPTranslator;
+ }
+ return rtpTranslator;
+ }
+
+ /**
+ * Notifies this <tt>MediaAwareCallConference</tt> about changes in the
+ * values of the properties of sources of <tt>PropertyChangeEvent</tt>s. For
+ * example, this instance listens to changes of the value of
+ * {@link MediaService#DEFAULT_DEVICE} which represents the user's choice
+ * with respect to the default audio device.
+ *
+ * @param ev a <tt>PropertyChangeEvent</tt> which specifies the name of the
+ * property which had its value changed and the old and new values of that
+ * property
+ */
+ private void propertyChange(PropertyChangeEvent ev)
+ {
+ String propertyName = ev.getPropertyName();
+
+ if (MediaService.DEFAULT_DEVICE.equals(propertyName))
+ {
+ Object source = ev.getSource();
+
+ if (source instanceof MediaService)
+ {
+ /*
+ * XXX We only support changing the default audio device at the
+ * time of this writing.
+ */
+ int mediaTypeIndex = MediaType.AUDIO.ordinal();
+ MediaDevice mixer = mixers[mediaTypeIndex];
+ MediaDevice oldValue
+ = (mixer instanceof MediaDeviceWrapper)
+ ? ((MediaDeviceWrapper) mixer).getWrappedDevice()
+ : null;
+ MediaDevice newValue = devices[mediaTypeIndex];
+
+ if (newValue == null)
+ {
+ newValue
+ = ProtocolMediaActivator
+ .getMediaService()
+ .getDefaultDevice(
+ MediaType.AUDIO,
+ MediaUseCase.ANY);
+ }
+
+ /*
+ * XXX If MediaService#getDefaultDevice(MediaType, MediaUseCase)
+ * above returns null and its earlier return value was not null,
+ * we will not notify of an actual change in the value of the
+ * user's choice with respect to the default audio device.
+ */
+ if (oldValue != newValue)
+ {
+ mixers[mediaTypeIndex] = null;
+ firePropertyChange(
+ MediaAwareCall.DEFAULT_DEVICE,
+ oldValue, newValue);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the <tt>MediaDevice</tt> to be used by this telephony conference for
+ * capture and/or playback of media of a specific <tt>MediaType</tt>.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the media which is to be
+ * captured and/or played back by the specified <tt>device</tt>
+ * @param device the <tt>MediaDevice</tt> to be used by this telephony
+ * conference for capture and/or playback of media of the specified
+ * <tt>mediaType</tt>
+ */
+ void setDevice(MediaType mediaType, MediaDevice device)
+ {
+ int mediaTypeIndex = mediaType.ordinal();
+ MediaDevice oldValue = devices[mediaTypeIndex];
+
+ /*
+ * XXX While we know the old and the new master/wrapped devices, we
+ * are not sure whether the mixer has been used. Anyway, we have to
+ * report different values in order to have PropertyChangeSupport
+ * really fire an event.
+ */
+ MediaDevice mixer = mixers[mediaTypeIndex];
+
+ if (mixer instanceof MediaDeviceWrapper)
+ oldValue = ((MediaDeviceWrapper) mixer).getWrappedDevice();
+
+ MediaDevice newValue = devices[mediaTypeIndex] = device;
+
+ if (oldValue != newValue)
+ {
+ mixers[mediaTypeIndex] = null;
+ firePropertyChange(
+ MediaAwareCall.DEFAULT_DEVICE,
+ oldValue, newValue);
+ }
+ }
+
+ /**
+ * Implements a <tt>PropertyChangeListener</tt> which weakly references and
+ * delegates to specific <tt>PropertyChangeListener</tt>s and automatically
+ * adds itself to and removes itself from a specific
+ * <tt>PropertyChangeNotifier</tt> depending on whether there are
+ * <tt>PropertyChangeListener</tt>s to delegate to. Thus enables listening
+ * to a <tt>PropertyChangeNotifier</tt> by invoking
+ * {@link PropertyChangeNotifier#addPropertyChangeListener(
+ * PropertyChangeListener)} without
+ * {@link PropertyChangeNotifier#removePropertyChangeListener(
+ * PropertyChangeListener)}.
+ */
+ private static class WeakPropertyChangeListener
+ implements PropertyChangeListener
+ {
+ /**
+ * The indicator which determines whether this
+ * <tt>PropertyChangeListener</tt> has been added to {@link #notifier}.
+ */
+ private boolean added = false;
+
+ /**
+ * The list of <tt>PropertyChangeListener</tt>s which are to be notified
+ * about <tt>PropertyChangeEvent</tt>s fired by {@link #notifier}.
+ */
+ private final List<WeakReference<PropertyChangeListener>> listeners
+ = new LinkedList<WeakReference<PropertyChangeListener>>();
+
+ /**
+ * The <tt>PropertyChangeNotifier</tt> this instance is to listen to
+ * about <tt>PropertyChangeEvent</tt>s which are to be forwarded to
+ * {@link #listeners}.
+ */
+ private final PropertyChangeNotifier notifier;
+
+ /**
+ * Initializes a new <tt>WeakPropertyChangeListener</tt> instance.
+ */
+ protected WeakPropertyChangeListener()
+ {
+ this(null);
+ }
+
+ /**
+ * Initializes a new <tt>WeakPropertyChangeListener</tt> instance which
+ * is to listen to a specific <tt>PropertyChangeNotifier</tt>.
+ *
+ * @param notifier the <tt>PropertyChangeNotifier</tt> the new instance
+ * is to listen to
+ */
+ public WeakPropertyChangeListener(PropertyChangeNotifier notifier)
+ {
+ this.notifier = notifier;
+ }
+
+ /**
+ * Adds a specific <tt>PropertyChangeListener</tt> to the list of
+ * <tt>PropertyChangeListener</tt>s to be notified about
+ * <tt>PropertyChangeEvent</tt>s fired by the
+ * <tt>PropertyChangeNotifier</tt> associated with this instance.
+ *
+ * @param listener the <tt>PropertyChangeListener</tt> to add
+ */
+ public synchronized void addPropertyChangeListener(
+ PropertyChangeListener listener)
+ {
+ Iterator<WeakReference<PropertyChangeListener>> i
+ = listeners.iterator();
+ boolean add = true;
+
+ while (i.hasNext())
+ {
+ PropertyChangeListener l = i.next().get();
+
+ if (l == null)
+ i.remove();
+ else if (l.equals(listener))
+ add = false;
+ }
+ if (add
+ && listeners.add(
+ new WeakReference<PropertyChangeListener>(listener))
+ && !this.added)
+ {
+ addThisToNotifier();
+ this.added = true;
+ }
+ }
+
+ /**
+ * Adds this as a <tt>PropertyChangeListener</tt> to {@link #notifier}.
+ */
+ protected void addThisToNotifier()
+ {
+ if (notifier != null)
+ notifier.addPropertyChangeListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by
+ * {@link #notifier}.
+ */
+ public void propertyChange(PropertyChangeEvent ev)
+ {
+ PropertyChangeListener[] ls;
+ int n;
+
+ synchronized (this)
+ {
+ Iterator<WeakReference<PropertyChangeListener>> i
+ = listeners.iterator();
+
+ ls = new PropertyChangeListener[listeners.size()];
+ n = 0;
+ while (i.hasNext())
+ {
+ PropertyChangeListener l = i.next().get();
+
+ if (l == null)
+ i.remove();
+ else
+ ls[n++] = l;
+ }
+ if ((n == 0) && this.added)
+ {
+ removeThisFromNotifier();
+ this.added = false;
+ }
+ }
+
+ if (n != 0)
+ {
+ for (PropertyChangeListener l : ls)
+ {
+ if (l == null)
+ break;
+ else
+ l.propertyChange(ev);
+ }
+ }
+ }
+
+ /**
+ * Removes a specific <tt>PropertyChangeListener</tt> from the list of
+ * <tt>PropertyChangeListener</tt>s to be notified about
+ * <tt>PropertyChangeEvent</tt>s fired by the
+ * <tt>PropertyChangeNotifier</tt> associated with this instance.
+ *
+ * @param listener the <tt>PropertyChangeListener</tt> to remove
+ */
+ @SuppressWarnings("unused")
+ public synchronized void removePropertyChangeListener(
+ PropertyChangeListener listener)
+ {
+ Iterator<WeakReference<PropertyChangeListener>> i
+ = listeners.iterator();
+
+ while (i.hasNext())
+ {
+ PropertyChangeListener l = i.next().get();
+
+ if ((l == null) || l.equals(listener))
+ i.remove();
+ }
+ if (this.added && (listeners.size() == 0))
+ {
+ removeThisFromNotifier();
+ this.added = false;
+ }
+ }
+
+ /**
+ * Removes this as a <tt>PropertyChangeListener</tt> from
+ * {@link #notifier}.
+ */
+ protected void removeThisFromNotifier()
+ {
+ if (notifier != null)
+ notifier.removePropertyChangeListener(this);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java index 59c885c..59b8798 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCallPeer.java @@ -1,1251 +1,1251 @@ -/* - * 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.media; - -import java.beans.*; -import java.util.*; - -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.event.*; -import org.jitsi.service.protocol.event.*; - -/** - * A utility class implementing media control code shared between current - * telephony implementations. This class is only meant for use by protocol - * implementations and should/could not be accessed by bundles that are simply - * using the telephony functionalities. - * - * @param <T> the peer extension class like for example <tt>CallSipImpl</tt> - * or <tt>CallJabberImpl</tt> - * @param <U> the media handler extension class like for example - * <tt>CallPeerMediaHandlerSipImpl</tt> or - * <tt>CallPeerMediaHandlerJabberImpl</tt> - * @param <V> the provider extension class like for example - * <tt>ProtocolProviderServiceSipImpl</tt> or - * <tt>ProtocolProviderServiceJabberImpl</tt> - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Boris Grozev - */ -public abstract class MediaAwareCallPeer - <T extends MediaAwareCall<?, ?, V>, - U extends CallPeerMediaHandler<?>, - V extends ProtocolProviderService> - extends AbstractCallPeer<T, V> - implements SrtpListener, - CallPeerConferenceListener, - CsrcAudioLevelListener, - SimpleAudioLevelListener -{ - /** - * The <tt>Logger</tt> used by the <tt>MediaAwareCallPeer</tt> class and its - * instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(MediaAwareCallPeer.class); - - /** - * The call this peer belongs to. - */ - private T call; - - /** - * The listeners registered for level changes in the audio of participants - * that this peer might be mixing and that we are not directly communicating - * with. - */ - private final List<ConferenceMembersSoundLevelListener> - conferenceMembersSoundLevelListeners - = new ArrayList<ConferenceMembersSoundLevelListener>(); - - /** - * A byte array containing the image/photo representing the call peer. - */ - private byte[] image; - - /** - * The media handler class handles all media management for a single - * <tt>CallPeer</tt>. This includes initializing and configuring streams, - * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>always - * corresponds to exactly one instance of <tt>CallPeerMediaHandler</tt> and - * both classes are only separated for reasons of readability. - */ - private U mediaHandler; - - /** - * The <tt>PropertyChangeListener</tt> which listens to - * {@link CallPeerMediaHandler} for changes in the values of its properties. - */ - private PropertyChangeListener mediaHandlerPropertyChangeListener; - - /** - * A string uniquely identifying the peer. - */ - private String peerID; - - /** - * The protocol provider that this peer belongs to. - */ - private final V protocolProvider; - - /** - * The list of <tt>SoundLevelListener</tt>s interested in level changes in - * the audio we are getting from the remote peer. - * <p> - * It is implemented as a copy-on-write storage because the number of - * additions and removals of <tt>SoundLevelListener</tt>s is expected to be - * far smaller than the number of audio level changes. The access to it is - * to be synchronized using {@link #streamSoundLevelListenersSyncRoot}. - * </p> - */ - private List<SoundLevelListener> streamSoundLevelListeners; - - /** - * The <tt>Object</tt> to synchronize the access to - * {@link #streamSoundLevelListeners}. - */ - private final Object streamSoundLevelListenersSyncRoot = new Object(); - - /** - * The <tt>List</tt> of <tt>PropertyChangeListener</tt>s listening to this - * <tt>CallPeer</tt> for changes in the values of its properties related to - * video. - */ - private final List<PropertyChangeListener> videoPropertyChangeListeners - = new LinkedList<PropertyChangeListener>(); - - /** - * Represents the last Conference Information (RFC4575) document sent to - * this <tt>CallPeer</tt>. This is always a document with state "full", even - * if the last document actually sent was a "partial" - */ - private ConferenceInfoDocument lastConferenceInfoSent = null; - - /** - * The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which - * a Conference Information (RFC4575) document was last sent to this - * <tt>CallPeer</tt>. - */ - private long lastConferenceInfoSentTimestamp = -1; - - /** - * The last Conference Information (RFC4575) document sent to us by this - * <tt>CallPeer</tt>. This is always a document with state "full", which is - * only gets updated by "partial" or "deleted" documents. - */ - private ConferenceInfoDocument lastConferenceInfoReceived = null; - - /** - * Whether a conference-info document has been scheduled to be sent to this - * <tt>CallPeer</tt> - */ - private boolean confInfoScheduled = false; - - /** - * Synchronization object for confInfoScheduled - */ - private final Object confInfoScheduledSyncRoot = new Object(); - - /** - * Creates a new call peer with address <tt>peerAddress</tt>. - * - * @param owningCall the call that contains this call peer. - */ - public MediaAwareCallPeer(T owningCall) - { - this.call = owningCall; - this.protocolProvider = owningCall.getProtocolProvider(); - - // create the uid - this.peerID - = String.valueOf(System.currentTimeMillis()) - + String.valueOf(hashCode()); - - // we listen for events when the call will become focus or not - // of a conference so we will add or remove our sound level listeners - super.addCallPeerConferenceListener(this); - } - - /** - * Adds a specific <tt>ConferenceMembersSoundLevelListener</tt> to the list - * of listeners interested in and notified about changes in conference - * members sound level. - * - * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to add - * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt> - */ - public void addConferenceMembersSoundLevelListener( - ConferenceMembersSoundLevelListener listener) - { - /* - * XXX The uses of the method at the time of this writing rely on being - * able to add a null listener so make it a no-op here. - */ - if (listener == null) - return; - - synchronized (conferenceMembersSoundLevelListeners) - { - if (conferenceMembersSoundLevelListeners.size() == 0) - { - // if this is the first listener that's being registered with - // us, we also need to register ourselves as a CSRC audio level - // listener with the media handler. - getMediaHandler().setCsrcAudioLevelListener(this); - } - conferenceMembersSoundLevelListeners.add(listener); - } - } - - /** - * Adds a specific <tt>SoundLevelListener</tt> to the list of listeners - * interested in and notified about changes in the sound level of the audio - * sent by the remote party. When the first listener is being registered - * the method also registers its single listener with the media handler so - * that it would receive level change events and delegate them to the - * listeners that have registered with us. - * - * @param listener the <tt>SoundLevelListener</tt> to add - */ - public void addStreamSoundLevelListener(SoundLevelListener listener) - { - synchronized (streamSoundLevelListenersSyncRoot) - { - if ((streamSoundLevelListeners == null) - || streamSoundLevelListeners.isEmpty()) - { - CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); - - if (isJitsiVideoBridge()) - { - /* - * When the local user/peer has organized a telephony - * conference utilizing the Jitsi VideoBridge server-side - * technology, the server will calculate the audio levels - * and not the client. - */ - mediaHandler.setCsrcAudioLevelListener(this); - } - else - { - /* - * If this is the first listener that's being registered - * with us, we also need to register ourselves as an audio - * level listener with the media handler. We do this so that - * audio levels would only be calculated if anyone is - * interested in receiving them. - */ - mediaHandler.setStreamAudioLevelListener(this); - } - } - - /* - * Implement streamAudioLevelListeners as a copy-on-write storage so - * that iterators over it can iterate without - * ConcurrentModificationExceptions. - */ - streamSoundLevelListeners - = (streamSoundLevelListeners == null) - ? new ArrayList<SoundLevelListener>() - : new ArrayList<SoundLevelListener>( - streamSoundLevelListeners); - streamSoundLevelListeners.add(listener); - } - } - - /** - * Adds a specific <tt>PropertyChangeListener</tt> to the list of - * listeners which get notified when the properties (e.g. - * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change - * their values. - * - * @param listener the <tt>PropertyChangeListener</tt> to be notified - * when the properties associated with the specified <tt>Call</tt> change - * their values - */ - public void addVideoPropertyChangeListener(PropertyChangeListener listener) - { - if (listener == null) - throw new NullPointerException("listener"); - - synchronized (videoPropertyChangeListeners) - { - /* - * The video is part of the media-related functionality and thus it - * is the responsibility of mediaHandler. So listen to mediaHandler - * for video-related property changes and re-fire them as - * originating from this instance. - */ - if (!videoPropertyChangeListeners.contains(listener) - && videoPropertyChangeListeners.add(listener) - && (mediaHandlerPropertyChangeListener == null)) - { - mediaHandlerPropertyChangeListener - = new PropertyChangeListener() - { - public void propertyChange(PropertyChangeEvent event) - { - Iterable<PropertyChangeListener> listeners; - - synchronized (videoPropertyChangeListeners) - { - listeners - = new LinkedList<PropertyChangeListener>( - videoPropertyChangeListeners); - } - - PropertyChangeEvent thisEvent - = new PropertyChangeEvent( - this, - event.getPropertyName(), - event.getOldValue(), - event.getNewValue()); - - for (PropertyChangeListener listener : listeners) - listener.propertyChange(thisEvent); - } - }; - getMediaHandler() - .addPropertyChangeListener( - mediaHandlerPropertyChangeListener); - } - } - } - - /** - * Notified by its very majesty the media service about changes in the audio - * level of the stream coming from this peer, the method generates the - * corresponding events and delivers them to the listeners that have - * registered here. - * - * @param newLevel the new audio level of the audio stream received from the - * remote peer - */ - public void audioLevelChanged(int newLevel) - { - /* - * If we're in a conference in which this CallPeer is the focus and - * we're the only member in it besides the focus, we will not receive - * audio levels in the RTP and our media will instead measure the audio - * levels of the received media. In order to make the UI oblivious of - * the difference, we have to translate the event to the appropriate - * type of listener. - * - * We may end up in a conference call with 0 members if the server - * for some reason doesn't support sip conference (our subscribes - * doesn't go to the focus of the conference) and so we must - * pass the sound levels measured on the stream so we can see - * the stream activity of the call. - */ - int conferenceMemberCount = getConferenceMemberCount(); - - if ((conferenceMemberCount > 0) && (conferenceMemberCount < 3)) - { - long audioRemoteSSRC - = getMediaHandler().getRemoteSSRC(MediaType.AUDIO); - - if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN) - { - audioLevelsReceived(new long[] { audioRemoteSSRC, newLevel }); - return; - } - } - - fireStreamSoundLevelChanged(newLevel); - } - - - /** - * Implements {@link CsrcAudioLevelListener#audioLevelsReceived(long[])}. - * Delivers the received audio levels to the - * {@link ConferenceMembersSoundLevelListener}s registered with this - * <tt>MediaAwareCallPeer</tt>.. - * - * @param audioLevels the levels that we need to dispatch to all registered - * <tt>ConferenceMemberSoundLevelListeners</tt>. - */ - public void audioLevelsReceived(long[] audioLevels) - { - /* - * When the local user/peer has organized a telephony conference - * utilizing the Jitsi VideoBridge server-side technology, the server - * will calculate the audio levels and not the client. - */ - if (isJitsiVideoBridge()) - { - long audioRemoteSSRC - = getMediaHandler().getRemoteSSRC(MediaType.AUDIO); - - if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN) - { - for (int i = 0; i < audioLevels.length; i += 2) - { - if (audioLevels[i] == audioRemoteSSRC) - { - fireStreamSoundLevelChanged((int) audioLevels[i + 1]); - break; - } - } - } - } - - if (getConferenceMemberCount() == 0) - return; - - Map<ConferenceMember, Integer> levelsMap - = new HashMap<ConferenceMember, Integer>(); - - for (int i = 0; i < audioLevels.length; i += 2) - { - ConferenceMember mmbr = findConferenceMember(audioLevels[i]); - - if (mmbr != null) - levelsMap.put(mmbr, (int) audioLevels[i + 1]); - } - - synchronized (conferenceMembersSoundLevelListeners) - { - int conferenceMemberSoundLevelListenerCount - = conferenceMembersSoundLevelListeners.size(); - - if (conferenceMemberSoundLevelListenerCount > 0) - { - ConferenceMembersSoundLevelEvent ev - = new ConferenceMembersSoundLevelEvent(this, levelsMap); - - for (int i = 0; - i < conferenceMemberSoundLevelListenerCount; - i++) - { - conferenceMembersSoundLevelListeners - .get(i) - .soundLevelChanged(ev); - } - } - } - } - - /** - * Does nothing. - * @param evt the event. - */ - public void callPeerAdded(CallPeerEvent evt) {} - - /** - * Does nothing. - * @param evt the event. - */ - public void callPeerRemoved(CallPeerEvent evt) {} - - /** - * Dummy implementation of {@link CallPeerConferenceListener - * #conferenceFocusChanged(CallPeerConferenceEvent)}. - * - * @param evt ignored - */ - public void conferenceFocusChanged(CallPeerConferenceEvent evt) - { - } - - /** - * Called when this peer becomes a mixer. The method add removes this class - * as the stream audio level listener for the media coming from this peer - * because the levels it delivers no longer represent the level of a - * particular member. The method also adds this class as a member (CSRC) - * audio level listener. - * - * @param conferenceEvent the event containing information (that we don't - * really use) on the newly add member. - */ - public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent) - { - if (getConferenceMemberCount() > 2) - { - /* - * This peer is now a conference focus with more than three - * participants. It means that this peer is mixing and sending us - * audio for at least two separate participants. We therefore need - * to switch from stream to CSRC level listening. - */ - CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); - - mediaHandler.setStreamAudioLevelListener(null); - mediaHandler.setCsrcAudioLevelListener(this); - } - } - - /** - * Dummy implementation of {@link CallPeerConferenceListener - * #conferenceMemberErrorReceived(CallPeerConferenceEvent)}. - * - * @param ev the event - */ - public void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {}; - - /** - * Called when this peer stops being a mixer. The method add removes this - * class as the stream audio level listener for the media coming from this - * peer because the levels it delivers no longer represent the level of a - * particular member. The method also adds this class as a member (CSRC) - * audio level listener. - * - * @param conferenceEvent the event containing information (that we don't - * really use) on the freshly removed member. - */ - public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent) - { - if (getConferenceMemberCount() < 3) - { - /* - * This call peer is no longer mixing audio from multiple sources - * since there's only us and her in the call. We therefore need to - * switch from CSRC to stream level listening. - */ - CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); - - mediaHandler.setStreamAudioLevelListener(this); - mediaHandler.setCsrcAudioLevelListener(null); - } - } - - /** - * Invokes {@link SoundLevelListener#soundLevelChanged(Object, int) on - * the <tt>SoundLevelListener</tt>s interested in the changes of the audio - * stream received from the remote peer i.e. in - * {@link #streamSoundLevelListeners}. - * - * @param newLevel the new value of the sound level to notify - * <tt>streamSoundLevelListeners</tt> about - */ - private void fireStreamSoundLevelChanged(int newLevel) - { - List<SoundLevelListener> streamSoundLevelListeners; - - synchronized (streamSoundLevelListenersSyncRoot) - { - /* - * Since the streamAudioLevelListeners field of this - * MediaAwareCallPeer is implemented as a copy-on-write storage, - * just get a reference to it and it should be safe to iterate over it - * without ConcurrentModificationExceptions. - */ - streamSoundLevelListeners = this.streamSoundLevelListeners; - } - - if (streamSoundLevelListeners != null) - { - /* - * Iterate over streamAudioLevelListeners using an index rather than - * an Iterator in order to try to reduce the number of allocations - * (as the number of audio level changes is expected to be very - * large). - */ - int streamSoundLevelListenerCount - = streamSoundLevelListeners.size(); - - for(int i = 0; i < streamSoundLevelListenerCount; i++) - { - streamSoundLevelListeners.get(i).soundLevelChanged( - this, - newLevel); - } - } - } - - /** - * Returns a reference to the call that this peer belongs to. Calls - * are created by underlying telephony protocol implementations. - * - * @return a reference to the call containing this peer. - */ - @Override - public T getCall() - { - return call; - } - - /** - * The method returns an image representation of the call peer if one is - * available. - * - * @return byte[] a byte array containing the image or null if no image is - * available. - */ - public byte[] getImage() - { - return image; - } - - /** - * Returns a reference to the <tt>CallPeerMediaHandler</tt> used by this - * peer. The media handler class handles all media management for a single - * <tt>CallPeer</tt>. This includes initializing and configuring streams, - * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt> - * always corresponds to exactly one instance of - * <tt>CallPeerMediaHandler</tt> and both classes are only separated for - * reasons of readability. - * - * @return a reference to the <tt>CallPeerMediaHandler</tt> instance that - * this peer uses for media related tips and tricks. - */ - public U getMediaHandler() - { - return mediaHandler; - } - - /** - * Returns a unique identifier representing this peer. - * - * @return an identifier representing this call peer. - */ - public String getPeerID() - { - return peerID; - } - - /** - * Returns the protocol provider that this peer belongs to. - * - * @return a reference to the <tt>ProtocolProviderService</tt> that this - * peer belongs to. - */ - @Override - public V getProtocolProvider() - { - return protocolProvider; - } - - /** - * Determines whether this <tt>CallPeer</tt> is participating in a telephony - * conference organized by the local user/peer utilizing the Jitsi - * VideoBridge server-side technology. - * - * @return <tt>true</tt> if this <tt>CallPeer</tt> is participating in a - * telephony conference organized by the local user/peer utilizing the Jitsi - * VideoBridge server-side technology; otherwise, <tt>false</tt> - */ - public final boolean isJitsiVideoBridge() - { - Call call = getCall(); - - if (call != null) - { - CallConference conference = call.getConference(); - - if (conference != null) - return conference.isJitsiVideoBridge(); - } - return false; - } - - /** - * Determines whether we are currently streaming video toward whoever this - * <tt>MediaAwareCallPeer</tt> represents. - * - * @return <tt>true</tt> if we are currently streaming video toward this - * <tt>CallPeer</tt> and <tt>false</tt> otherwise. - */ - public boolean isLocalVideoStreaming() - { - return getMediaHandler().isLocalVideoTransmissionEnabled(); - } - - /** - * Determines whether the audio stream (if any) being sent to this - * peer is mute. - * - * @return <tt>true</tt> if an audio stream is being sent to this - * peer and it is currently mute; <tt>false</tt>, otherwise - */ - @Override - public boolean isMute() - { - return getMediaHandler().isMute(); - } - - /** - * Logs <tt>message</tt> and <tt>cause</tt> and sets this <tt>peer</tt>'s - * state to <tt>CallPeerState.FAILED</tt> - * - * @param message a message to log and display to the user. - * @param throwable the exception that cause the error we are logging - */ - public void logAndFail(String message, Throwable throwable) - { - logger.error(message, throwable); - setState(CallPeerState.FAILED, message); - } - - /** - * Updates the state of this <tt>CallPeer</tt> to match the locally-on-hold - * status of our media handler. - */ - public void reevalLocalHoldStatus() - { - CallPeerState state = getState(); - boolean locallyOnHold = getMediaHandler().isLocallyOnHold(); - - if (CallPeerState.ON_HOLD_LOCALLY.equals(state)) - { - if (!locallyOnHold) - setState(CallPeerState.CONNECTED); - } - else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state)) - { - if (!locallyOnHold) - setState(CallPeerState.ON_HOLD_REMOTELY); - } - else if (CallPeerState.ON_HOLD_REMOTELY.equals(state)) - { - if (locallyOnHold) - setState(CallPeerState.ON_HOLD_MUTUALLY); - } - else if (locallyOnHold) - { - setState(CallPeerState.ON_HOLD_LOCALLY); - } - } - - /** - * Updates the state of this <tt>CallPeer</tt> to match the remotely-on-hold - * status of our media handler. - */ - public void reevalRemoteHoldStatus() - { - boolean remotelyOnHold = getMediaHandler().isRemotelyOnHold(); - - CallPeerState state = getState(); - if (CallPeerState.ON_HOLD_LOCALLY.equals(state)) - { - if (remotelyOnHold) - setState(CallPeerState.ON_HOLD_MUTUALLY); - } - else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state)) - { - if (!remotelyOnHold) - setState(CallPeerState.ON_HOLD_LOCALLY); - } - else if (CallPeerState.ON_HOLD_REMOTELY.equals(state)) - { - if (!remotelyOnHold) - setState(CallPeerState.CONNECTED); - } - else if (remotelyOnHold) - { - setState(CallPeerState.ON_HOLD_REMOTELY); - } - } - - /** - * Removes a specific <tt>ConferenceMembersSoundLevelListener</tt> of the - * list of listeners interested in and notified about changes in conference - * members sound level. - * - * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to - * remove - */ - public void removeConferenceMembersSoundLevelListener( - ConferenceMembersSoundLevelListener listener) - { - synchronized (conferenceMembersSoundLevelListeners) - { - if (conferenceMembersSoundLevelListeners.remove(listener) - && (conferenceMembersSoundLevelListeners.size() == 0)) - { - // if this was the last listener then we also remove ourselves - // as a CSRC audio level listener from the handler so that we - // don't have to create new events and maps for something no one - // is interested in. - getMediaHandler().setCsrcAudioLevelListener(null); - } - } - } - - /** - * Removes a specific <tt>SoundLevelListener</tt> of the list of - * listeners interested in and notified about changes in stream sound level - * related information. - * - * @param listener the <tt>SoundLevelListener</tt> to remove - */ - public void removeStreamSoundLevelListener(SoundLevelListener listener) - { - synchronized (streamSoundLevelListenersSyncRoot) - { - /* - * Implement streamAudioLevelListeners as a copy-on-write storage so - * that iterators over it can iterate over it without - * ConcurrentModificationExceptions. - */ - if (streamSoundLevelListeners != null) - { - streamSoundLevelListeners - = new ArrayList<SoundLevelListener>( - streamSoundLevelListeners); - if (streamSoundLevelListeners.remove(listener) - && streamSoundLevelListeners.isEmpty()) - streamSoundLevelListeners = null; - } - - if ((streamSoundLevelListeners == null) - || streamSoundLevelListeners.isEmpty()) - { - // if this was the last listener then we also need to remove - // ourselves as an audio level so that audio levels would only - // be calculated if anyone is interested in receiving them. - getMediaHandler().setStreamAudioLevelListener(null); - } - } - } - - /** - * Removes a specific <tt>PropertyChangeListener</tt> from the list of - * listeners which get notified when the properties (e.g. - * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change - * their values. - * - * @param listener the <tt>PropertyChangeListener</tt> to no longer be - * notified when the properties associated with the specified <tt>Call</tt> - * change their values - */ - public void removeVideoPropertyChangeListener( - PropertyChangeListener listener) - { - if (listener != null) - synchronized (videoPropertyChangeListeners) - { - /* - * The video is part of the media-related functionality and thus - * it is the responsibility of mediaHandler. So we're listening - * to mediaHandler for video-related property changes and w're - * re-firing them as originating from this instance. Make sure - * that we're not listening to mediaHandler if noone is - * interested in video-related property changes originating from - * this instance. - */ - if (videoPropertyChangeListeners.remove(listener) - && videoPropertyChangeListeners.isEmpty() - && (mediaHandlerPropertyChangeListener != null)) - { -// getMediaHandler() -// .removePropertyChangeListener( -// mediaHandlerPropertyChangeListener); - mediaHandlerPropertyChangeListener = null; - } - } - } - - /** - * Sets the security message associated with a failure/warning or - * information coming from the encryption protocol. - * - * @param messageType the type of the message. - * @param i18nMessage the message - * @param severity severity level - */ - public void securityMessageReceived( - String messageType, - String i18nMessage, - int severity) - { - fireCallPeerSecurityMessageEvent(messageType, - i18nMessage, - severity); - } - - /** - * Indicates that the other party has timeouted replying to our - * offer to secure the connection. - * - * @param mediaType the <tt>MediaType</tt> of the call session - * @param sender the security controller that caused the event - */ - public void securityNegotiationStarted( - MediaType mediaType, - SrtpControl sender) - { - fireCallPeerSecurityNegotiationStartedEvent( - new CallPeerSecurityNegotiationStartedEvent( - this, - toSessionType(mediaType), - sender)); - } - - /** - * Indicates that the other party has timeouted replying to our - * offer to secure the connection. - * - * @param mediaType the <tt>MediaType</tt> of the call session - */ - public void securityTimeout(MediaType mediaType) - { - fireCallPeerSecurityTimeoutEvent( - new CallPeerSecurityTimeoutEvent( - this, - toSessionType(mediaType))); - } - - /** - * Sets the security status to OFF for this call peer. - * - * @param mediaType the <tt>MediaType</tt> of the call session - */ - public void securityTurnedOff(MediaType mediaType) - { - // If this event has been triggered because of a call end event and the - // call is already ended we don't need to alert the user for - // security off. - if((call != null) && !call.getCallState().equals(CallState.CALL_ENDED)) - { - fireCallPeerSecurityOffEvent( - new CallPeerSecurityOffEvent( - this, - toSessionType(mediaType))); - } - } - - /** - * Sets the security status to ON for this call peer. - * - * @param mediaType the <tt>MediaType</tt> of the call session - * @param cipher the cipher - * @param sender the security controller that caused the event - */ - public void securityTurnedOn( - MediaType mediaType, - String cipher, - SrtpControl sender) - { - getMediaHandler().startSrtpMultistream(sender); - fireCallPeerSecurityOnEvent( - new CallPeerSecurityOnEvent( - this, - toSessionType(mediaType), - cipher, - sender)); - } - - /** - * Sets the call containing this peer. - * - * @param call the call that this call peer is participating in. - */ - public void setCall(T call) - { - this.call = call; - } - - /** - * Sets the byte array containing an image representation (photo or picture) - * of the call peer. - * - * @param image a byte array containing the image - */ - public void setImage(byte[] image) - { - byte[] oldImage = getImage(); - this.image = image; - - //Fire the Event - fireCallPeerChangeEvent( - CallPeerChangeEvent.CALL_PEER_IMAGE_CHANGE, - oldImage, - image); - } - - /** - * Modifies the local media setup to reflect the requested setting for the - * streaming of the local video and then re-invites the peer represented by - * this class using a corresponding SDP description.. - * - * @param allowed <tt>true</tt> if local video transmission is allowed and - * <tt>false</tt> otherwise. - * - * @throws OperationFailedException if video initialization fails. - */ - public void setLocalVideoAllowed(boolean allowed) - throws OperationFailedException - { - CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); - - if(mediaHandler.isLocalVideoTransmissionEnabled() != allowed) - { - // Modify the local media setup to reflect the requested setting for - // the streaming of the local video. - mediaHandler.setLocalVideoTransmissionEnabled(allowed); - } - } - - /** - * Sets a reference to the <tt>CallPeerMediaHandler</tt> used by this - * peer. The media handler class handles all media management for a single - * <tt>CallPeer</tt>. This includes initializing and configuring streams, - * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt> - * always corresponds to exactly one instance of - * <tt>CallPeerMediaHandler</tt> and both classes are only separated for - * reasons of readability. - * - * @param mediaHandler a reference to the <tt>CallPeerMediaHandler</tt> - * instance that this peer uses for media related tips and tricks. - */ - protected void setMediaHandler(U mediaHandler) - { - this.mediaHandler = mediaHandler; - } - - /** - * Sets the mute property for this call peer. - * - * @param newMuteValue the new value of the mute property for this call peer - */ - @Override - public void setMute(boolean newMuteValue) - { - getMediaHandler().setMute(newMuteValue); - super.setMute(newMuteValue); - } - - /** - * Sets the String that serves as a unique identifier of this - * CallPeer. - * @param peerID the ID of this call peer. - */ - public void setPeerID(String peerID) - { - this.peerID = peerID; - } - - /** - * Overrides the parent set state method in order to make sure that we - * close our media handler whenever we enter a disconnected state. - * - * @param newState the <tt>CallPeerState</tt> that we are about to enter and - * that we pass to our predecessor. - * @param reason a reason phrase explaining the state (e.g. if newState - * indicates a failure) and that we pass to our predecessor. - * @param reasonCode the code for the reason of the state change. - */ - @Override - public void setState(CallPeerState newState, String reason, int reasonCode) - { - // synchronized to mediaHandler if there are currently jobs of - // initializing, configuring and starting streams (method processAnswer - // of CallPeerMediaHandler) we won't set and fire the current state - // to Disconnected. Before closing the mediaHandler is setting the state - // in order to deliver states as quick as possible. - CallPeerMediaHandler<?> mediaHandler = getMediaHandler(); - - synchronized(mediaHandler) - { - try - { - super.setState(newState, reason, reasonCode); - } - finally - { - // make sure whatever happens to close the media - if (CallPeerState.DISCONNECTED.equals(newState) - || CallPeerState.FAILED.equals(newState)) - mediaHandler.close(); - } - } - } - - /** - * Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this - * <tt>CallPeer</tt>. It is a document with state <tt>full</tt> - * @return the last <tt>ConferenceInfoDocument</tt> sent by us to this - * <tt>CallPeer</tt>. It is a document with state <tt>full</tt> - */ - public ConferenceInfoDocument getLastConferenceInfoSent() - { - return lastConferenceInfoSent; - } - - /** - * Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this - * <tt>CallPeer</tt>. - * @param confInfo the document to set. - */ - public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo) - { - lastConferenceInfoSent = confInfo; - } - - /** - * Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>) - * at which we last sent a <tt>ConferenceInfoDocument</tt> to this - * <tt>CallPeer</tt>. - * @return the time (as obtained by <tt>System.currentTimeMillis()</tt>) - * at which we last sent a <tt>ConferenceInfoDocument</tt> to this - * <tt>CallPeer</tt>. - */ - public long getLastConferenceInfoSentTimestamp() - { - return lastConferenceInfoSentTimestamp; - } - - /** - * Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>) - * at which we last sent a <tt>ConferenceInfoDocument</tt> to this - * <tt>CallPeer</tt>. - * @param newTimestamp the time to set - */ - public void setLastConferenceInfoSentTimestamp(long newTimestamp) - { - lastConferenceInfoSentTimestamp = newTimestamp; - } - - /** - * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this - * <tt>CallPeer</tt>. - * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this - * <tt>CallPeer</tt>. - */ - public ConferenceInfoDocument getLastConferenceInfoReceived() - { - return lastConferenceInfoReceived; - } - - /** - * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this - * <tt>CallPeer</tt>. - * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this - * <tt>CallPeer</tt>. - */ - public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo) - { - lastConferenceInfoReceived = confInfo; - } - - /** - * Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt> - * sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received - * a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>. - * @return - */ - public int getLastConferenceInfoReceivedVersion() - { - return (lastConferenceInfoReceived == null) - ? -1 - : lastConferenceInfoReceived.getVersion(); - } - - /** - * Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when - * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the - * <tt>entity</tt> key attribute which to use for the <tt>user</tt> - * element corresponding to this <tt>CallPeer</tt>) - * - * @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when - * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the - * <tt>entity</tt> key attribute which to use for the <tt>user</tt> - * element corresponding to this <tt>CallPeer</tt>) - */ - public abstract String getEntity(); - - /** - * Check whether a conference-info document is scheduled to be sent to - * this <tt>CallPeer</tt> (i.e. there is a thread which will eventually - * (after sleeping a certain amount of time) trigger a document to be sent) - * @return <tt>true</tt> if there is a conference-info document scheduled - * to be sent to this <tt>CallPeer</tt> and <tt>false</tt> otherwise. - */ - public boolean isConfInfoScheduled() - { - synchronized (confInfoScheduledSyncRoot) - { - return confInfoScheduled; - } - } - - /** - * Sets the property which indicates whether a conference-info document - * is scheduled to be sent to this <tt>CallPeer</tt>. - * @param confInfoScheduled - */ - public void setConfInfoScheduled(boolean confInfoScheduled) - { - synchronized (confInfoScheduledSyncRoot) - { - this.confInfoScheduled = confInfoScheduled; - } - } - - /** - * Returns the direction of the session for media of type <tt>mediaType</tt> - * that we have with this <tt>CallPeer</tt>. This is the direction of the - * session negotiated in the signaling protocol, and it may or may not - * coincide with the direction of the media stream. - * For example, if we are the focus of a videobridge conference and another - * peer is sending video to us, we have a <tt>RECVONLY</tt> video stream, - * but <tt>SENDONLY</tt> or <tt>SENDRECV</tt> (Jingle) sessions with the - * rest of the conference members. - * Should always return non-null. - * - * @param mediaType the <tt>MediaType</tt> to use - * @return Returns the direction of the session for media of type - * <tt>mediaType</tt> that we have with this <tt>CallPeer</tt>. - */ - public abstract MediaDirection getDirection(MediaType mediaType); - - /** - * {@inheritDoc} - * - * When a <tt>ConferenceMember</tt> is removed from a conference with a - * Jitsi-videobridge, an RTCP BYE packet is not always sent. Therefore, - * if the <tt>ConferenceMember</tt> had an associated video SSRC, the stream - * isn't be removed until it times out, leaving a blank video container in - * the interface for a few seconds. - * TODO: This works around the problem by removing the - * <tt>ConferenceMember</tt>'s <tt>ReceiveStream</tt> when the - * <tt>ConferenceMember</tt> is removed. The proper solution is to ensure - * that RTCP BYEs are sent whenever necessary, and when it is deployed this - * code should be removed. - * - * @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 - */ - @Override - public void removeConferenceMember(ConferenceMember conferenceMember) - { - MediaStream videoStream = getMediaHandler().getStream(MediaType.VIDEO); - if (videoStream != null) - videoStream.removeReceiveStreamForSsrc( - conferenceMember.getVideoSsrc()); - - super.removeConferenceMember(conferenceMember); - } - - /** - * Converts a specific <tt>MediaType</tt> into a <tt>sessionType</tt> value - * in the terms of the <tt>CallPeerSecurityStatusEvent</tt> class. - * - * @param mediaType the <tt>MediaType</tt> to be converted - * @return the <tt>sessionType</tt> value in the terms of the - * <tt>CallPeerSecurityStatusEvent</tt> class that is equivalent to the - * specified <tt>mediaType</tt> - */ - private static int toSessionType(MediaType mediaType) - { - switch (mediaType) - { - case AUDIO: - return CallPeerSecurityStatusEvent.AUDIO_SESSION; - case VIDEO: - return CallPeerSecurityStatusEvent.VIDEO_SESSION; - default: - throw new IllegalArgumentException("mediaType"); - } - } -} +/*
+ * 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.media;
+
+import java.beans.*;
+import java.util.*;
+
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+
+import org.jitsi.service.neomedia.*;
+import org.jitsi.service.neomedia.event.*;
+import org.jitsi.service.protocol.event.*;
+
+/**
+ * A utility class implementing media control code shared between current
+ * telephony implementations. This class is only meant for use by protocol
+ * implementations and should/could not be accessed by bundles that are simply
+ * using the telephony functionalities.
+ *
+ * @param <T> the peer extension class like for example <tt>CallSipImpl</tt>
+ * or <tt>CallJabberImpl</tt>
+ * @param <U> the media handler extension class like for example
+ * <tt>CallPeerMediaHandlerSipImpl</tt> or
+ * <tt>CallPeerMediaHandlerJabberImpl</tt>
+ * @param <V> the provider extension class like for example
+ * <tt>ProtocolProviderServiceSipImpl</tt> or
+ * <tt>ProtocolProviderServiceJabberImpl</tt>
+ *
+ * @author Emil Ivov
+ * @author Lyubomir Marinov
+ * @author Boris Grozev
+ */
+public abstract class MediaAwareCallPeer
+ <T extends MediaAwareCall<?, ?, V>,
+ U extends CallPeerMediaHandler<?>,
+ V extends ProtocolProviderService>
+ extends AbstractCallPeer<T, V>
+ implements SrtpListener,
+ CallPeerConferenceListener,
+ CsrcAudioLevelListener,
+ SimpleAudioLevelListener
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>MediaAwareCallPeer</tt> class and its
+ * instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(MediaAwareCallPeer.class);
+
+ /**
+ * The call this peer belongs to.
+ */
+ private T call;
+
+ /**
+ * The listeners registered for level changes in the audio of participants
+ * that this peer might be mixing and that we are not directly communicating
+ * with.
+ */
+ private final List<ConferenceMembersSoundLevelListener>
+ conferenceMembersSoundLevelListeners
+ = new ArrayList<ConferenceMembersSoundLevelListener>();
+
+ /**
+ * A byte array containing the image/photo representing the call peer.
+ */
+ private byte[] image;
+
+ /**
+ * The media handler class handles all media management for a single
+ * <tt>CallPeer</tt>. This includes initializing and configuring streams,
+ * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>always
+ * corresponds to exactly one instance of <tt>CallPeerMediaHandler</tt> and
+ * both classes are only separated for reasons of readability.
+ */
+ private U mediaHandler;
+
+ /**
+ * The <tt>PropertyChangeListener</tt> which listens to
+ * {@link CallPeerMediaHandler} for changes in the values of its properties.
+ */
+ private PropertyChangeListener mediaHandlerPropertyChangeListener;
+
+ /**
+ * A string uniquely identifying the peer.
+ */
+ private String peerID;
+
+ /**
+ * The protocol provider that this peer belongs to.
+ */
+ private final V protocolProvider;
+
+ /**
+ * The list of <tt>SoundLevelListener</tt>s interested in level changes in
+ * the audio we are getting from the remote peer.
+ * <p>
+ * It is implemented as a copy-on-write storage because the number of
+ * additions and removals of <tt>SoundLevelListener</tt>s is expected to be
+ * far smaller than the number of audio level changes. The access to it is
+ * to be synchronized using {@link #streamSoundLevelListenersSyncRoot}.
+ * </p>
+ */
+ private List<SoundLevelListener> streamSoundLevelListeners;
+
+ /**
+ * The <tt>Object</tt> to synchronize the access to
+ * {@link #streamSoundLevelListeners}.
+ */
+ private final Object streamSoundLevelListenersSyncRoot = new Object();
+
+ /**
+ * The <tt>List</tt> of <tt>PropertyChangeListener</tt>s listening to this
+ * <tt>CallPeer</tt> for changes in the values of its properties related to
+ * video.
+ */
+ private final List<PropertyChangeListener> videoPropertyChangeListeners
+ = new LinkedList<PropertyChangeListener>();
+
+ /**
+ * Represents the last Conference Information (RFC4575) document sent to
+ * this <tt>CallPeer</tt>. This is always a document with state "full", even
+ * if the last document actually sent was a "partial"
+ */
+ private ConferenceInfoDocument lastConferenceInfoSent = null;
+
+ /**
+ * The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which
+ * a Conference Information (RFC4575) document was last sent to this
+ * <tt>CallPeer</tt>.
+ */
+ private long lastConferenceInfoSentTimestamp = -1;
+
+ /**
+ * The last Conference Information (RFC4575) document sent to us by this
+ * <tt>CallPeer</tt>. This is always a document with state "full", which is
+ * only gets updated by "partial" or "deleted" documents.
+ */
+ private ConferenceInfoDocument lastConferenceInfoReceived = null;
+
+ /**
+ * Whether a conference-info document has been scheduled to be sent to this
+ * <tt>CallPeer</tt>
+ */
+ private boolean confInfoScheduled = false;
+
+ /**
+ * Synchronization object for confInfoScheduled
+ */
+ private final Object confInfoScheduledSyncRoot = new Object();
+
+ /**
+ * Creates a new call peer with address <tt>peerAddress</tt>.
+ *
+ * @param owningCall the call that contains this call peer.
+ */
+ public MediaAwareCallPeer(T owningCall)
+ {
+ this.call = owningCall;
+ this.protocolProvider = owningCall.getProtocolProvider();
+
+ // create the uid
+ this.peerID
+ = String.valueOf(System.currentTimeMillis())
+ + String.valueOf(hashCode());
+
+ // we listen for events when the call will become focus or not
+ // of a conference so we will add or remove our sound level listeners
+ super.addCallPeerConferenceListener(this);
+ }
+
+ /**
+ * Adds a specific <tt>ConferenceMembersSoundLevelListener</tt> to the list
+ * of listeners interested in and notified about changes in conference
+ * members sound level.
+ *
+ * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to add
+ * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
+ */
+ public void addConferenceMembersSoundLevelListener(
+ ConferenceMembersSoundLevelListener listener)
+ {
+ /*
+ * XXX The uses of the method at the time of this writing rely on being
+ * able to add a null listener so make it a no-op here.
+ */
+ if (listener == null)
+ return;
+
+ synchronized (conferenceMembersSoundLevelListeners)
+ {
+ if (conferenceMembersSoundLevelListeners.size() == 0)
+ {
+ // if this is the first listener that's being registered with
+ // us, we also need to register ourselves as a CSRC audio level
+ // listener with the media handler.
+ getMediaHandler().setCsrcAudioLevelListener(this);
+ }
+ conferenceMembersSoundLevelListeners.add(listener);
+ }
+ }
+
+ /**
+ * Adds a specific <tt>SoundLevelListener</tt> to the list of listeners
+ * interested in and notified about changes in the sound level of the audio
+ * sent by the remote party. When the first listener is being registered
+ * the method also registers its single listener with the media handler so
+ * that it would receive level change events and delegate them to the
+ * listeners that have registered with us.
+ *
+ * @param listener the <tt>SoundLevelListener</tt> to add
+ */
+ public void addStreamSoundLevelListener(SoundLevelListener listener)
+ {
+ synchronized (streamSoundLevelListenersSyncRoot)
+ {
+ if ((streamSoundLevelListeners == null)
+ || streamSoundLevelListeners.isEmpty())
+ {
+ CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
+
+ if (isJitsiVideobridge())
+ {
+ /*
+ * When the local user/peer has organized a telephony
+ * conference utilizing the Jitsi Videobridge server-side
+ * technology, the server will calculate the audio levels
+ * and not the client.
+ */
+ mediaHandler.setCsrcAudioLevelListener(this);
+ }
+ else
+ {
+ /*
+ * If this is the first listener that's being registered
+ * with us, we also need to register ourselves as an audio
+ * level listener with the media handler. We do this so that
+ * audio levels would only be calculated if anyone is
+ * interested in receiving them.
+ */
+ mediaHandler.setStreamAudioLevelListener(this);
+ }
+ }
+
+ /*
+ * Implement streamAudioLevelListeners as a copy-on-write storage so
+ * that iterators over it can iterate without
+ * ConcurrentModificationExceptions.
+ */
+ streamSoundLevelListeners
+ = (streamSoundLevelListeners == null)
+ ? new ArrayList<SoundLevelListener>()
+ : new ArrayList<SoundLevelListener>(
+ streamSoundLevelListeners);
+ streamSoundLevelListeners.add(listener);
+ }
+ }
+
+ /**
+ * Adds a specific <tt>PropertyChangeListener</tt> to the list of
+ * listeners which get notified when the properties (e.g.
+ * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
+ * their values.
+ *
+ * @param listener the <tt>PropertyChangeListener</tt> to be notified
+ * when the properties associated with the specified <tt>Call</tt> change
+ * their values
+ */
+ public void addVideoPropertyChangeListener(PropertyChangeListener listener)
+ {
+ if (listener == null)
+ throw new NullPointerException("listener");
+
+ synchronized (videoPropertyChangeListeners)
+ {
+ /*
+ * The video is part of the media-related functionality and thus it
+ * is the responsibility of mediaHandler. So listen to mediaHandler
+ * for video-related property changes and re-fire them as
+ * originating from this instance.
+ */
+ if (!videoPropertyChangeListeners.contains(listener)
+ && videoPropertyChangeListeners.add(listener)
+ && (mediaHandlerPropertyChangeListener == null))
+ {
+ mediaHandlerPropertyChangeListener
+ = new PropertyChangeListener()
+ {
+ public void propertyChange(PropertyChangeEvent event)
+ {
+ Iterable<PropertyChangeListener> listeners;
+
+ synchronized (videoPropertyChangeListeners)
+ {
+ listeners
+ = new LinkedList<PropertyChangeListener>(
+ videoPropertyChangeListeners);
+ }
+
+ PropertyChangeEvent thisEvent
+ = new PropertyChangeEvent(
+ this,
+ event.getPropertyName(),
+ event.getOldValue(),
+ event.getNewValue());
+
+ for (PropertyChangeListener listener : listeners)
+ listener.propertyChange(thisEvent);
+ }
+ };
+ getMediaHandler()
+ .addPropertyChangeListener(
+ mediaHandlerPropertyChangeListener);
+ }
+ }
+ }
+
+ /**
+ * Notified by its very majesty the media service about changes in the audio
+ * level of the stream coming from this peer, the method generates the
+ * corresponding events and delivers them to the listeners that have
+ * registered here.
+ *
+ * @param newLevel the new audio level of the audio stream received from the
+ * remote peer
+ */
+ public void audioLevelChanged(int newLevel)
+ {
+ /*
+ * If we're in a conference in which this CallPeer is the focus and
+ * we're the only member in it besides the focus, we will not receive
+ * audio levels in the RTP and our media will instead measure the audio
+ * levels of the received media. In order to make the UI oblivious of
+ * the difference, we have to translate the event to the appropriate
+ * type of listener.
+ *
+ * We may end up in a conference call with 0 members if the server
+ * for some reason doesn't support sip conference (our subscribes
+ * doesn't go to the focus of the conference) and so we must
+ * pass the sound levels measured on the stream so we can see
+ * the stream activity of the call.
+ */
+ int conferenceMemberCount = getConferenceMemberCount();
+
+ if ((conferenceMemberCount > 0) && (conferenceMemberCount < 3))
+ {
+ long audioRemoteSSRC
+ = getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
+
+ if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
+ {
+ audioLevelsReceived(new long[] { audioRemoteSSRC, newLevel });
+ return;
+ }
+ }
+
+ fireStreamSoundLevelChanged(newLevel);
+ }
+
+
+ /**
+ * Implements {@link CsrcAudioLevelListener#audioLevelsReceived(long[])}.
+ * Delivers the received audio levels to the
+ * {@link ConferenceMembersSoundLevelListener}s registered with this
+ * <tt>MediaAwareCallPeer</tt>..
+ *
+ * @param audioLevels the levels that we need to dispatch to all registered
+ * <tt>ConferenceMemberSoundLevelListeners</tt>.
+ */
+ public void audioLevelsReceived(long[] audioLevels)
+ {
+ /*
+ * When the local user/peer has organized a telephony conference
+ * utilizing the Jitsi Videobridge server-side technology, the server
+ * will calculate the audio levels and not the client.
+ */
+ if (isJitsiVideobridge())
+ {
+ long audioRemoteSSRC
+ = getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
+
+ if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
+ {
+ for (int i = 0; i < audioLevels.length; i += 2)
+ {
+ if (audioLevels[i] == audioRemoteSSRC)
+ {
+ fireStreamSoundLevelChanged((int) audioLevels[i + 1]);
+ break;
+ }
+ }
+ }
+ }
+
+ if (getConferenceMemberCount() == 0)
+ return;
+
+ Map<ConferenceMember, Integer> levelsMap
+ = new HashMap<ConferenceMember, Integer>();
+
+ for (int i = 0; i < audioLevels.length; i += 2)
+ {
+ ConferenceMember mmbr = findConferenceMember(audioLevels[i]);
+
+ if (mmbr != null)
+ levelsMap.put(mmbr, (int) audioLevels[i + 1]);
+ }
+
+ synchronized (conferenceMembersSoundLevelListeners)
+ {
+ int conferenceMemberSoundLevelListenerCount
+ = conferenceMembersSoundLevelListeners.size();
+
+ if (conferenceMemberSoundLevelListenerCount > 0)
+ {
+ ConferenceMembersSoundLevelEvent ev
+ = new ConferenceMembersSoundLevelEvent(this, levelsMap);
+
+ for (int i = 0;
+ i < conferenceMemberSoundLevelListenerCount;
+ i++)
+ {
+ conferenceMembersSoundLevelListeners
+ .get(i)
+ .soundLevelChanged(ev);
+ }
+ }
+ }
+ }
+
+ /**
+ * Does nothing.
+ * @param evt the event.
+ */
+ public void callPeerAdded(CallPeerEvent evt) {}
+
+ /**
+ * Does nothing.
+ * @param evt the event.
+ */
+ public void callPeerRemoved(CallPeerEvent evt) {}
+
+ /**
+ * Dummy implementation of {@link CallPeerConferenceListener
+ * #conferenceFocusChanged(CallPeerConferenceEvent)}.
+ *
+ * @param evt ignored
+ */
+ public void conferenceFocusChanged(CallPeerConferenceEvent evt)
+ {
+ }
+
+ /**
+ * Called when this peer becomes a mixer. The method add removes this class
+ * as the stream audio level listener for the media coming from this peer
+ * because the levels it delivers no longer represent the level of a
+ * particular member. The method also adds this class as a member (CSRC)
+ * audio level listener.
+ *
+ * @param conferenceEvent the event containing information (that we don't
+ * really use) on the newly add member.
+ */
+ public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent)
+ {
+ if (getConferenceMemberCount() > 2)
+ {
+ /*
+ * This peer is now a conference focus with more than three
+ * participants. It means that this peer is mixing and sending us
+ * audio for at least two separate participants. We therefore need
+ * to switch from stream to CSRC level listening.
+ */
+ CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
+
+ mediaHandler.setStreamAudioLevelListener(null);
+ mediaHandler.setCsrcAudioLevelListener(this);
+ }
+ }
+
+ /**
+ * Dummy implementation of {@link CallPeerConferenceListener
+ * #conferenceMemberErrorReceived(CallPeerConferenceEvent)}.
+ *
+ * @param ev the event
+ */
+ public void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {};
+
+ /**
+ * Called when this peer stops being a mixer. The method add removes this
+ * class as the stream audio level listener for the media coming from this
+ * peer because the levels it delivers no longer represent the level of a
+ * particular member. The method also adds this class as a member (CSRC)
+ * audio level listener.
+ *
+ * @param conferenceEvent the event containing information (that we don't
+ * really use) on the freshly removed member.
+ */
+ public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent)
+ {
+ if (getConferenceMemberCount() < 3)
+ {
+ /*
+ * This call peer is no longer mixing audio from multiple sources
+ * since there's only us and her in the call. We therefore need to
+ * switch from CSRC to stream level listening.
+ */
+ CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
+
+ mediaHandler.setStreamAudioLevelListener(this);
+ mediaHandler.setCsrcAudioLevelListener(null);
+ }
+ }
+
+ /**
+ * Invokes {@link SoundLevelListener#soundLevelChanged(Object, int) on
+ * the <tt>SoundLevelListener</tt>s interested in the changes of the audio
+ * stream received from the remote peer i.e. in
+ * {@link #streamSoundLevelListeners}.
+ *
+ * @param newLevel the new value of the sound level to notify
+ * <tt>streamSoundLevelListeners</tt> about
+ */
+ private void fireStreamSoundLevelChanged(int newLevel)
+ {
+ List<SoundLevelListener> streamSoundLevelListeners;
+
+ synchronized (streamSoundLevelListenersSyncRoot)
+ {
+ /*
+ * Since the streamAudioLevelListeners field of this
+ * MediaAwareCallPeer is implemented as a copy-on-write storage,
+ * just get a reference to it and it should be safe to iterate over it
+ * without ConcurrentModificationExceptions.
+ */
+ streamSoundLevelListeners = this.streamSoundLevelListeners;
+ }
+
+ if (streamSoundLevelListeners != null)
+ {
+ /*
+ * Iterate over streamAudioLevelListeners using an index rather than
+ * an Iterator in order to try to reduce the number of allocations
+ * (as the number of audio level changes is expected to be very
+ * large).
+ */
+ int streamSoundLevelListenerCount
+ = streamSoundLevelListeners.size();
+
+ for(int i = 0; i < streamSoundLevelListenerCount; i++)
+ {
+ streamSoundLevelListeners.get(i).soundLevelChanged(
+ this,
+ newLevel);
+ }
+ }
+ }
+
+ /**
+ * Returns a reference to the call that this peer belongs to. Calls
+ * are created by underlying telephony protocol implementations.
+ *
+ * @return a reference to the call containing this peer.
+ */
+ @Override
+ public T getCall()
+ {
+ return call;
+ }
+
+ /**
+ * The method returns an image representation of the call peer if one is
+ * available.
+ *
+ * @return byte[] a byte array containing the image or null if no image is
+ * available.
+ */
+ public byte[] getImage()
+ {
+ return image;
+ }
+
+ /**
+ * Returns a reference to the <tt>CallPeerMediaHandler</tt> used by this
+ * peer. The media handler class handles all media management for a single
+ * <tt>CallPeer</tt>. This includes initializing and configuring streams,
+ * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
+ * always corresponds to exactly one instance of
+ * <tt>CallPeerMediaHandler</tt> and both classes are only separated for
+ * reasons of readability.
+ *
+ * @return a reference to the <tt>CallPeerMediaHandler</tt> instance that
+ * this peer uses for media related tips and tricks.
+ */
+ public U getMediaHandler()
+ {
+ return mediaHandler;
+ }
+
+ /**
+ * Returns a unique identifier representing this peer.
+ *
+ * @return an identifier representing this call peer.
+ */
+ public String getPeerID()
+ {
+ return peerID;
+ }
+
+ /**
+ * Returns the protocol provider that this peer belongs to.
+ *
+ * @return a reference to the <tt>ProtocolProviderService</tt> that this
+ * peer belongs to.
+ */
+ @Override
+ public V getProtocolProvider()
+ {
+ return protocolProvider;
+ }
+
+ /**
+ * Determines whether this <tt>CallPeer</tt> is participating in a telephony
+ * conference organized by the local user/peer utilizing the Jitsi
+ * Videobridge server-side technology.
+ *
+ * @return <tt>true</tt> if this <tt>CallPeer</tt> is participating in a
+ * telephony conference organized by the local user/peer utilizing the Jitsi
+ * Videobridge server-side technology; otherwise, <tt>false</tt>
+ */
+ public final boolean isJitsiVideobridge()
+ {
+ Call call = getCall();
+
+ if (call != null)
+ {
+ CallConference conference = call.getConference();
+
+ if (conference != null)
+ return conference.isJitsiVideobridge();
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether we are currently streaming video toward whoever this
+ * <tt>MediaAwareCallPeer</tt> represents.
+ *
+ * @return <tt>true</tt> if we are currently streaming video toward this
+ * <tt>CallPeer</tt> and <tt>false</tt> otherwise.
+ */
+ public boolean isLocalVideoStreaming()
+ {
+ return getMediaHandler().isLocalVideoTransmissionEnabled();
+ }
+
+ /**
+ * Determines whether the audio stream (if any) being sent to this
+ * peer is mute.
+ *
+ * @return <tt>true</tt> if an audio stream is being sent to this
+ * peer and it is currently mute; <tt>false</tt>, otherwise
+ */
+ @Override
+ public boolean isMute()
+ {
+ return getMediaHandler().isMute();
+ }
+
+ /**
+ * Logs <tt>message</tt> and <tt>cause</tt> and sets this <tt>peer</tt>'s
+ * state to <tt>CallPeerState.FAILED</tt>
+ *
+ * @param message a message to log and display to the user.
+ * @param throwable the exception that cause the error we are logging
+ */
+ public void logAndFail(String message, Throwable throwable)
+ {
+ logger.error(message, throwable);
+ setState(CallPeerState.FAILED, message);
+ }
+
+ /**
+ * Updates the state of this <tt>CallPeer</tt> to match the locally-on-hold
+ * status of our media handler.
+ */
+ public void reevalLocalHoldStatus()
+ {
+ CallPeerState state = getState();
+ boolean locallyOnHold = getMediaHandler().isLocallyOnHold();
+
+ if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
+ {
+ if (!locallyOnHold)
+ setState(CallPeerState.CONNECTED);
+ }
+ else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
+ {
+ if (!locallyOnHold)
+ setState(CallPeerState.ON_HOLD_REMOTELY);
+ }
+ else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
+ {
+ if (locallyOnHold)
+ setState(CallPeerState.ON_HOLD_MUTUALLY);
+ }
+ else if (locallyOnHold)
+ {
+ setState(CallPeerState.ON_HOLD_LOCALLY);
+ }
+ }
+
+ /**
+ * Updates the state of this <tt>CallPeer</tt> to match the remotely-on-hold
+ * status of our media handler.
+ */
+ public void reevalRemoteHoldStatus()
+ {
+ boolean remotelyOnHold = getMediaHandler().isRemotelyOnHold();
+
+ CallPeerState state = getState();
+ if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
+ {
+ if (remotelyOnHold)
+ setState(CallPeerState.ON_HOLD_MUTUALLY);
+ }
+ else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
+ {
+ if (!remotelyOnHold)
+ setState(CallPeerState.ON_HOLD_LOCALLY);
+ }
+ else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
+ {
+ if (!remotelyOnHold)
+ setState(CallPeerState.CONNECTED);
+ }
+ else if (remotelyOnHold)
+ {
+ setState(CallPeerState.ON_HOLD_REMOTELY);
+ }
+ }
+
+ /**
+ * Removes a specific <tt>ConferenceMembersSoundLevelListener</tt> of the
+ * list of listeners interested in and notified about changes in conference
+ * members sound level.
+ *
+ * @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to
+ * remove
+ */
+ public void removeConferenceMembersSoundLevelListener(
+ ConferenceMembersSoundLevelListener listener)
+ {
+ synchronized (conferenceMembersSoundLevelListeners)
+ {
+ if (conferenceMembersSoundLevelListeners.remove(listener)
+ && (conferenceMembersSoundLevelListeners.size() == 0))
+ {
+ // if this was the last listener then we also remove ourselves
+ // as a CSRC audio level listener from the handler so that we
+ // don't have to create new events and maps for something no one
+ // is interested in.
+ getMediaHandler().setCsrcAudioLevelListener(null);
+ }
+ }
+ }
+
+ /**
+ * Removes a specific <tt>SoundLevelListener</tt> of the list of
+ * listeners interested in and notified about changes in stream sound level
+ * related information.
+ *
+ * @param listener the <tt>SoundLevelListener</tt> to remove
+ */
+ public void removeStreamSoundLevelListener(SoundLevelListener listener)
+ {
+ synchronized (streamSoundLevelListenersSyncRoot)
+ {
+ /*
+ * Implement streamAudioLevelListeners as a copy-on-write storage so
+ * that iterators over it can iterate over it without
+ * ConcurrentModificationExceptions.
+ */
+ if (streamSoundLevelListeners != null)
+ {
+ streamSoundLevelListeners
+ = new ArrayList<SoundLevelListener>(
+ streamSoundLevelListeners);
+ if (streamSoundLevelListeners.remove(listener)
+ && streamSoundLevelListeners.isEmpty())
+ streamSoundLevelListeners = null;
+ }
+
+ if ((streamSoundLevelListeners == null)
+ || streamSoundLevelListeners.isEmpty())
+ {
+ // if this was the last listener then we also need to remove
+ // ourselves as an audio level so that audio levels would only
+ // be calculated if anyone is interested in receiving them.
+ getMediaHandler().setStreamAudioLevelListener(null);
+ }
+ }
+ }
+
+ /**
+ * Removes a specific <tt>PropertyChangeListener</tt> from the list of
+ * listeners which get notified when the properties (e.g.
+ * LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
+ * their values.
+ *
+ * @param listener the <tt>PropertyChangeListener</tt> to no longer be
+ * notified when the properties associated with the specified <tt>Call</tt>
+ * change their values
+ */
+ public void removeVideoPropertyChangeListener(
+ PropertyChangeListener listener)
+ {
+ if (listener != null)
+ synchronized (videoPropertyChangeListeners)
+ {
+ /*
+ * The video is part of the media-related functionality and thus
+ * it is the responsibility of mediaHandler. So we're listening
+ * to mediaHandler for video-related property changes and w're
+ * re-firing them as originating from this instance. Make sure
+ * that we're not listening to mediaHandler if noone is
+ * interested in video-related property changes originating from
+ * this instance.
+ */
+ if (videoPropertyChangeListeners.remove(listener)
+ && videoPropertyChangeListeners.isEmpty()
+ && (mediaHandlerPropertyChangeListener != null))
+ {
+// getMediaHandler()
+// .removePropertyChangeListener(
+// mediaHandlerPropertyChangeListener);
+ mediaHandlerPropertyChangeListener = null;
+ }
+ }
+ }
+
+ /**
+ * Sets the security message associated with a failure/warning or
+ * information coming from the encryption protocol.
+ *
+ * @param messageType the type of the message.
+ * @param i18nMessage the message
+ * @param severity severity level
+ */
+ public void securityMessageReceived(
+ String messageType,
+ String i18nMessage,
+ int severity)
+ {
+ fireCallPeerSecurityMessageEvent(messageType,
+ i18nMessage,
+ severity);
+ }
+
+ /**
+ * Indicates that the other party has timeouted replying to our
+ * offer to secure the connection.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the call session
+ * @param sender the security controller that caused the event
+ */
+ public void securityNegotiationStarted(
+ MediaType mediaType,
+ SrtpControl sender)
+ {
+ fireCallPeerSecurityNegotiationStartedEvent(
+ new CallPeerSecurityNegotiationStartedEvent(
+ this,
+ toSessionType(mediaType),
+ sender));
+ }
+
+ /**
+ * Indicates that the other party has timeouted replying to our
+ * offer to secure the connection.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the call session
+ */
+ public void securityTimeout(MediaType mediaType)
+ {
+ fireCallPeerSecurityTimeoutEvent(
+ new CallPeerSecurityTimeoutEvent(
+ this,
+ toSessionType(mediaType)));
+ }
+
+ /**
+ * Sets the security status to OFF for this call peer.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the call session
+ */
+ public void securityTurnedOff(MediaType mediaType)
+ {
+ // If this event has been triggered because of a call end event and the
+ // call is already ended we don't need to alert the user for
+ // security off.
+ if((call != null) && !call.getCallState().equals(CallState.CALL_ENDED))
+ {
+ fireCallPeerSecurityOffEvent(
+ new CallPeerSecurityOffEvent(
+ this,
+ toSessionType(mediaType)));
+ }
+ }
+
+ /**
+ * Sets the security status to ON for this call peer.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the call session
+ * @param cipher the cipher
+ * @param sender the security controller that caused the event
+ */
+ public void securityTurnedOn(
+ MediaType mediaType,
+ String cipher,
+ SrtpControl sender)
+ {
+ getMediaHandler().startSrtpMultistream(sender);
+ fireCallPeerSecurityOnEvent(
+ new CallPeerSecurityOnEvent(
+ this,
+ toSessionType(mediaType),
+ cipher,
+ sender));
+ }
+
+ /**
+ * Sets the call containing this peer.
+ *
+ * @param call the call that this call peer is participating in.
+ */
+ public void setCall(T call)
+ {
+ this.call = call;
+ }
+
+ /**
+ * Sets the byte array containing an image representation (photo or picture)
+ * of the call peer.
+ *
+ * @param image a byte array containing the image
+ */
+ public void setImage(byte[] image)
+ {
+ byte[] oldImage = getImage();
+ this.image = image;
+
+ //Fire the Event
+ fireCallPeerChangeEvent(
+ CallPeerChangeEvent.CALL_PEER_IMAGE_CHANGE,
+ oldImage,
+ image);
+ }
+
+ /**
+ * Modifies the local media setup to reflect the requested setting for the
+ * streaming of the local video and then re-invites the peer represented by
+ * this class using a corresponding SDP description..
+ *
+ * @param allowed <tt>true</tt> if local video transmission is allowed and
+ * <tt>false</tt> otherwise.
+ *
+ * @throws OperationFailedException if video initialization fails.
+ */
+ public void setLocalVideoAllowed(boolean allowed)
+ throws OperationFailedException
+ {
+ CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
+
+ if(mediaHandler.isLocalVideoTransmissionEnabled() != allowed)
+ {
+ // Modify the local media setup to reflect the requested setting for
+ // the streaming of the local video.
+ mediaHandler.setLocalVideoTransmissionEnabled(allowed);
+ }
+ }
+
+ /**
+ * Sets a reference to the <tt>CallPeerMediaHandler</tt> used by this
+ * peer. The media handler class handles all media management for a single
+ * <tt>CallPeer</tt>. This includes initializing and configuring streams,
+ * generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
+ * always corresponds to exactly one instance of
+ * <tt>CallPeerMediaHandler</tt> and both classes are only separated for
+ * reasons of readability.
+ *
+ * @param mediaHandler a reference to the <tt>CallPeerMediaHandler</tt>
+ * instance that this peer uses for media related tips and tricks.
+ */
+ protected void setMediaHandler(U mediaHandler)
+ {
+ this.mediaHandler = mediaHandler;
+ }
+
+ /**
+ * Sets the mute property for this call peer.
+ *
+ * @param newMuteValue the new value of the mute property for this call peer
+ */
+ @Override
+ public void setMute(boolean newMuteValue)
+ {
+ getMediaHandler().setMute(newMuteValue);
+ super.setMute(newMuteValue);
+ }
+
+ /**
+ * Sets the String that serves as a unique identifier of this
+ * CallPeer.
+ * @param peerID the ID of this call peer.
+ */
+ public void setPeerID(String peerID)
+ {
+ this.peerID = peerID;
+ }
+
+ /**
+ * Overrides the parent set state method in order to make sure that we
+ * close our media handler whenever we enter a disconnected state.
+ *
+ * @param newState the <tt>CallPeerState</tt> that we are about to enter and
+ * that we pass to our predecessor.
+ * @param reason a reason phrase explaining the state (e.g. if newState
+ * indicates a failure) and that we pass to our predecessor.
+ * @param reasonCode the code for the reason of the state change.
+ */
+ @Override
+ public void setState(CallPeerState newState, String reason, int reasonCode)
+ {
+ // synchronized to mediaHandler if there are currently jobs of
+ // initializing, configuring and starting streams (method processAnswer
+ // of CallPeerMediaHandler) we won't set and fire the current state
+ // to Disconnected. Before closing the mediaHandler is setting the state
+ // in order to deliver states as quick as possible.
+ CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
+
+ synchronized(mediaHandler)
+ {
+ try
+ {
+ super.setState(newState, reason, reasonCode);
+ }
+ finally
+ {
+ // make sure whatever happens to close the media
+ if (CallPeerState.DISCONNECTED.equals(newState)
+ || CallPeerState.FAILED.equals(newState))
+ mediaHandler.close();
+ }
+ }
+ }
+
+ /**
+ * Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
+ * @return the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
+ */
+ public ConferenceInfoDocument getLastConferenceInfoSent()
+ {
+ return lastConferenceInfoSent;
+ }
+
+ /**
+ * Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this
+ * <tt>CallPeer</tt>.
+ * @param confInfo the document to set.
+ */
+ public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo)
+ {
+ lastConferenceInfoSent = confInfo;
+ }
+
+ /**
+ * Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ * @return the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ */
+ public long getLastConferenceInfoSentTimestamp()
+ {
+ return lastConferenceInfoSentTimestamp;
+ }
+
+ /**
+ * Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
+ * at which we last sent a <tt>ConferenceInfoDocument</tt> to this
+ * <tt>CallPeer</tt>.
+ * @param newTimestamp the time to set
+ */
+ public void setLastConferenceInfoSentTimestamp(long newTimestamp)
+ {
+ lastConferenceInfoSentTimestamp = newTimestamp;
+ }
+
+ /**
+ * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ */
+ public ConferenceInfoDocument getLastConferenceInfoReceived()
+ {
+ return lastConferenceInfoReceived;
+ }
+
+ /**
+ * Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ * @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
+ * <tt>CallPeer</tt>.
+ */
+ public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo)
+ {
+ lastConferenceInfoReceived = confInfo;
+ }
+
+ /**
+ * Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt>
+ * sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received
+ * a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>.
+ * @return
+ */
+ public int getLastConferenceInfoReceivedVersion()
+ {
+ return (lastConferenceInfoReceived == null)
+ ? -1
+ : lastConferenceInfoReceived.getVersion();
+ }
+
+ /**
+ * Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
+ * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
+ * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
+ * element corresponding to this <tt>CallPeer</tt>)
+ *
+ * @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
+ * we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
+ * <tt>entity</tt> key attribute which to use for the <tt>user</tt>
+ * element corresponding to this <tt>CallPeer</tt>)
+ */
+ public abstract String getEntity();
+
+ /**
+ * Check whether a conference-info document is scheduled to be sent to
+ * this <tt>CallPeer</tt> (i.e. there is a thread which will eventually
+ * (after sleeping a certain amount of time) trigger a document to be sent)
+ * @return <tt>true</tt> if there is a conference-info document scheduled
+ * to be sent to this <tt>CallPeer</tt> and <tt>false</tt> otherwise.
+ */
+ public boolean isConfInfoScheduled()
+ {
+ synchronized (confInfoScheduledSyncRoot)
+ {
+ return confInfoScheduled;
+ }
+ }
+
+ /**
+ * Sets the property which indicates whether a conference-info document
+ * is scheduled to be sent to this <tt>CallPeer</tt>.
+ * @param confInfoScheduled
+ */
+ public void setConfInfoScheduled(boolean confInfoScheduled)
+ {
+ synchronized (confInfoScheduledSyncRoot)
+ {
+ this.confInfoScheduled = confInfoScheduled;
+ }
+ }
+
+ /**
+ * Returns the direction of the session for media of type <tt>mediaType</tt>
+ * that we have with this <tt>CallPeer</tt>. This is the direction of the
+ * session negotiated in the signaling protocol, and it may or may not
+ * coincide with the direction of the media stream.
+ * For example, if we are the focus of a videobridge conference and another
+ * peer is sending video to us, we have a <tt>RECVONLY</tt> video stream,
+ * but <tt>SENDONLY</tt> or <tt>SENDRECV</tt> (Jingle) sessions with the
+ * rest of the conference members.
+ * Should always return non-null.
+ *
+ * @param mediaType the <tt>MediaType</tt> to use
+ * @return Returns the direction of the session for media of type
+ * <tt>mediaType</tt> that we have with this <tt>CallPeer</tt>.
+ */
+ public abstract MediaDirection getDirection(MediaType mediaType);
+
+ /**
+ * {@inheritDoc}
+ *
+ * When a <tt>ConferenceMember</tt> is removed from a conference with a
+ * Jitsi-videobridge, an RTCP BYE packet is not always sent. Therefore,
+ * if the <tt>ConferenceMember</tt> had an associated video SSRC, the stream
+ * isn't be removed until it times out, leaving a blank video container in
+ * the interface for a few seconds.
+ * TODO: This works around the problem by removing the
+ * <tt>ConferenceMember</tt>'s <tt>ReceiveStream</tt> when the
+ * <tt>ConferenceMember</tt> is removed. The proper solution is to ensure
+ * that RTCP BYEs are sent whenever necessary, and when it is deployed this
+ * code should be removed.
+ *
+ * @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
+ */
+ @Override
+ public void removeConferenceMember(ConferenceMember conferenceMember)
+ {
+ MediaStream videoStream = getMediaHandler().getStream(MediaType.VIDEO);
+ if (videoStream != null)
+ videoStream.removeReceiveStreamForSsrc(
+ conferenceMember.getVideoSsrc());
+
+ super.removeConferenceMember(conferenceMember);
+ }
+
+ /**
+ * Converts a specific <tt>MediaType</tt> into a <tt>sessionType</tt> value
+ * in the terms of the <tt>CallPeerSecurityStatusEvent</tt> class.
+ *
+ * @param mediaType the <tt>MediaType</tt> to be converted
+ * @return the <tt>sessionType</tt> value in the terms of the
+ * <tt>CallPeerSecurityStatusEvent</tt> class that is equivalent to the
+ * specified <tt>mediaType</tt>
+ */
+ private static int toSessionType(MediaType mediaType)
+ {
+ switch (mediaType)
+ {
+ case AUDIO:
+ return CallPeerSecurityStatusEvent.AUDIO_SESSION;
+ case VIDEO:
+ return CallPeerSecurityStatusEvent.VIDEO_SESSION;
+ default:
+ throw new IllegalArgumentException("mediaType");
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/media/TransportManager.java b/src/net/java/sip/communicator/service/protocol/media/TransportManager.java index 31698a4..5bb4205 100644 --- a/src/net/java/sip/communicator/service/protocol/media/TransportManager.java +++ b/src/net/java/sip/communicator/service/protocol/media/TransportManager.java @@ -1,765 +1,778 @@ -/* - * 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.media; - -import java.net.*; - -import net.java.sip.communicator.service.netaddr.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; - -import org.ice4j.ice.*; -import org.jitsi.service.configuration.*; -import org.jitsi.service.neomedia.*; - -/** - * <tt>TransportManager</tt>s are responsible for allocating ports, gathering - * local candidates and managing ICE whenever we are using it. - * - * @param <U> the peer extension class like for example <tt>CallPeerSipImpl</tt> - * or <tt>CallPeerJabberImpl</tt> - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Sebastien Vincent - */ -public abstract class TransportManager<U extends MediaAwareCallPeer<?, ?, ?>> -{ - /** - * The <tt>Logger</tt> used by the <tt>TransportManager</tt> - * class and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(TransportManager.class); - - /** - * The port tracker that we should use when binding generic media streams. - * <p> - * Initialized by {@link #initializePortNumbers()}. - * </p> - */ - private static PortTracker defaultPortTracker = new PortTracker(5000, 6000); - - /** - * The port tracker that we should use when binding video media streams. - * <p> - * Potentially initialized by {@link #initializePortNumbers()} if the - * necessary properties are set. - * </p> - */ - private static PortTracker videoPortTracker = null; - - /** - * The port tracker that we should use when binding data channels. - * <p> - * Potentially initialized by {@link #initializePortNumbers()} if the - * necessary properties are set - * </p> - */ - private static PortTracker dataChannelPortTracker = null; - - /** - * The port tracker that we should use when binding data media streams. - * <p> - * Potentially initialized by {@link #initializePortNumbers()} if the - * necessary properties are set - * </p> - */ - private static PortTracker audioPortTracker = null; - - /** - * RTP audio DSCP configuration property name. - */ - private static final String RTP_AUDIO_DSCP_PROPERTY = - "net.java.sip.communicator.impl.protocol.RTP_AUDIO_DSCP"; - - /** - * RTP video DSCP configuration property name. - */ - private static final String RTP_VIDEO_DSCP_PROPERTY = - "net.java.sip.communicator.impl.protocol.RTP_VIDEO_DSCP"; - - /** - * Number of empty UDP packets to send for NAT hole punching. - */ - private static final String HOLE_PUNCH_PKT_COUNT_PROPERTY = - "net.java.sip.communicator.impl.protocol.HOLE_PUNCH_PKT_COUNT"; - - /** - * Number of empty UDP packets to send for NAT hole punching. - */ - private static final int DEFAULT_HOLE_PUNCH_PKT_COUNT = 3; - - /** - * The {@link MediaAwareCallPeer} whose traffic we will be taking care of. - */ - private U callPeer; - - /** - * The RTP/RTCP socket couples that this <tt>TransportManager</tt> uses to - * send and receive media flows through indexed by <tt>MediaType</tt> - * (ordinal). - */ - private final StreamConnector[] streamConnectors - = new StreamConnector[MediaType.values().length]; - - /** - * Creates a new instance of this transport manager, binding it to the - * specified peer. - * - * @param callPeer the {@link MediaAwareCallPeer} whose traffic we will be - * taking care of. - */ - protected TransportManager(U callPeer) - { - this.callPeer = callPeer; - } - - /** - * Returns the <tt>StreamConnector</tt> instance that this media handler - * should use for streams of the specified <tt>mediaType</tt>. The method - * would also create a new <tt>StreamConnector</tt> if no connector has - * been initialized for this <tt>mediaType</tt> yet or in case one - * of its underlying sockets has been closed. - * - * @param mediaType the <tt>MediaType</tt> that we'd like to create a - * connector for. - * @return this media handler's <tt>StreamConnector</tt> for the specified - * <tt>mediaType</tt>. - * - * @throws OperationFailedException in case we failed to initialize our - * connector. - */ - public StreamConnector getStreamConnector(MediaType mediaType) - throws OperationFailedException - { - int streamConnectorIndex = mediaType.ordinal(); - StreamConnector streamConnector - = streamConnectors[streamConnectorIndex]; - - if((streamConnector == null) - || (streamConnector.getProtocol() == StreamConnector.Protocol.UDP)) - { - DatagramSocket controlSocket; - - if((streamConnector == null) - || streamConnector.getDataSocket().isClosed() - || (((controlSocket = streamConnector.getControlSocket()) - != null) - && controlSocket.isClosed())) - { - streamConnectors[streamConnectorIndex] - = streamConnector - = createStreamConnector(mediaType); - } - } - else if(streamConnector.getProtocol() == StreamConnector.Protocol.TCP) - { - Socket controlTCPSocket; - - if(streamConnector.getDataTCPSocket().isClosed() - || (((controlTCPSocket = streamConnector.getControlTCPSocket()) - != null) - && controlTCPSocket.isClosed())) - { - streamConnectors[streamConnectorIndex] - = streamConnector - = createStreamConnector(mediaType); - } - } - return streamConnector; - } - - /** - * Closes the existing <tt>StreamConnector</tt>, if any, associated with a - * specific <tt>MediaType</tt> and removes its reference from this - * <tt>TransportManager</tt>. - * - * @param mediaType the <tt>MediaType</tt> associated with the - * <tt>StreamConnector</tt> to close - */ - public void closeStreamConnector(MediaType mediaType) - { - int index = mediaType.ordinal(); - StreamConnector streamConnector = streamConnectors[index]; - - if (streamConnector != null) - { - closeStreamConnector(mediaType, streamConnector); - streamConnectors[index] = null; - } - } - - /** - * Closes a specific <tt>StreamConnector</tt> associated with a specific - * <tt>MediaType</tt>. If this <tt>TransportManager</tt> has a reference to - * the specified <tt>streamConnector</tt>, it remains. Allows extenders to - * override and perform additional customizations to the closing of the - * specified <tt>streamConnector</tt>. - * - * @param mediaType the <tt>MediaType</tt> associated with the specified - * <tt>streamConnector</tt> - * @param streamConnector the <tt>StreamConnector</tt> to be closed - * @see #closeStreamConnector(MediaType) - */ - protected void closeStreamConnector( - MediaType mediaType, - StreamConnector streamConnector) - { - /* - * XXX The connected owns the sockets so it is important that it - * decides whether to close them i.e. this TransportManager is not - * allowed to explicitly close the sockets by itself. - */ - streamConnector.close(); - } - - /** - * Creates a media <tt>StreamConnector</tt>. The method takes into account - * the minimum and maximum media port boundaries. - * - * @param mediaType the <tt>MediaType</tt> of the stream for which a new - * <tt>StreamConnector</tt> is to be created - * @return a new <tt>StreamConnector</tt>. - * - * @throws OperationFailedException if the binding of the sockets fails. - */ - protected StreamConnector createStreamConnector(MediaType mediaType) - throws OperationFailedException - { - NetworkAddressManagerService nam - = ProtocolMediaActivator.getNetworkAddressManagerService(); - InetAddress intendedDestination = getIntendedDestination(getCallPeer()); - InetAddress localHostForPeer = nam.getLocalHost(intendedDestination); - - //make sure our port numbers reflect the configuration service settings - initializePortNumbers(); - - PortTracker portTracker = getPortTracker(mediaType); - - //create the RTP socket. - DatagramSocket rtpSocket = null; - try - { - rtpSocket = nam.createDatagramSocket( - localHostForPeer, portTracker.getPort(), - portTracker.getMinPort(), portTracker.getMaxPort()); - } - catch (Exception exc) - { - throw new OperationFailedException( - "Failed to allocate the network ports necessary for the call.", - OperationFailedException.INTERNAL_ERROR, exc); - } - - //make sure that next time we don't try to bind on occupied ports - //also, refuse validation in case someone set the tracker range to 1 - portTracker.setNextPort( rtpSocket.getLocalPort() + 1, false); - - //create the RTCP socket, preferably on the port following our RTP one. - DatagramSocket rtcpSocket = null; - try - { - rtcpSocket = nam.createDatagramSocket( - localHostForPeer, portTracker.getPort(), - portTracker.getMinPort(), portTracker.getMaxPort()); - } - catch (Exception exc) - { - throw new OperationFailedException( - "Failed to allocate the network ports necessary for the call.", - OperationFailedException.INTERNAL_ERROR, - exc); - } - - //make sure that next time we don't try to bind on occupied ports - portTracker.setNextPort( rtcpSocket.getLocalPort() + 1); - - return new DefaultStreamConnector(rtpSocket, rtcpSocket); - } - - /** - * (Re)Sets the all the port allocators to reflect current values specified - * in the <tt>ConfigurationService</tt>. Calling this method may very well - * result in creating new port allocators or destroying exising ones. - */ - protected static void initializePortNumbers() - { - //try the default tracker first - ConfigurationService configuration - = ProtocolMediaActivator.getConfigurationService(); - String minPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MIN_MEDIA_PORT_NUMBER_PROPERTY_NAME); - - String maxPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MAX_MEDIA_PORT_NUMBER_PROPERTY_NAME); - - //try to send the specified range. If there's no specified range in - //configuration, we'll just leave the tracker as it is: [5000 to 6000] - defaultPortTracker.tryRange(minPortNumberStr, maxPortNumberStr); - - - //try the VIDEO tracker - minPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MIN_VIDEO_PORT_NUMBER_PROPERTY_NAME); - - maxPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MAX_VIDEO_PORT_NUMBER_PROPERTY_NAME); - - //try to send the specified range. If there's no specified range in - //configuration, we'll just leave this tracker to null - videoPortTracker - = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr); - - - //try the AUDIO tracker - minPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MIN_AUDIO_PORT_NUMBER_PROPERTY_NAME); - - maxPortNumberStr = configuration.getString( - OperationSetBasicTelephony.MAX_AUDIO_PORT_NUMBER_PROPERTY_NAME); - - //try to send the specified range. If there's no specified range in - //configuration, we'll just leave this tracker to null - audioPortTracker - = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr); - - //try the DATA CHANNEL tracker - minPortNumberStr = configuration.getString(OperationSetBasicTelephony - .MIN_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME); - - maxPortNumberStr = configuration.getString(OperationSetBasicTelephony - .MAX_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME); - - //try to send the specified range. If there's no specified range in - //configuration, we'll just leave this tracker to null - dataChannelPortTracker - = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr); - } - - /** - * Returns the <tt>InetAddress</tt> that we are using in one of our - * <tt>StreamConnector</tt>s or, in case we don't have any connectors yet - * the address returned by the our network address manager as the best local - * address to use when contacting the <tt>CallPeer</tt> associated with this - * <tt>MediaHandler</tt>. This method is primarily meant for use with the - * o= and c= fields of a newly created session description. The point is - * that we create our <tt>StreamConnector</tt>s when constructing the media - * descriptions so we already have a specific local address assigned to them - * at the time we get ready to create the c= and o= fields. It is therefore - * better to try and return one of these addresses before trying the net - * address manager again and running the slight risk of getting a different - * address. - * - * @return an <tt>InetAddress</tt> that we use in one of the - * <tt>StreamConnector</tt>s in this class. - */ - public InetAddress getLastUsedLocalHost() - { - for (MediaType mediaType : MediaType.values()) - { - StreamConnector streamConnector - = streamConnectors[mediaType.ordinal()]; - - if (streamConnector != null) - return streamConnector.getDataSocket().getLocalAddress(); - } - - NetworkAddressManagerService nam - = ProtocolMediaActivator.getNetworkAddressManagerService(); - InetAddress intendedDestination = getIntendedDestination(getCallPeer()); - - return nam.getLocalHost(intendedDestination); - } - - /** - * Send empty UDP packets to target destination data/control ports - * in order to open ports on NATs or and help RTP proxies latch onto our - * RTP ports. - * - * @param target <tt>MediaStreamTarget</tt> - * @param type the {@link MediaType} of the connector we'd like to send - * the hole punching packet through. - */ - public void sendHolePunchPacket(MediaStreamTarget target, MediaType type) - { - logger.info("Send NAT hole punch packets"); - - //check how many hole punch packets we would be supposed to send: - int packetCount = ProtocolMediaActivator.getConfigurationService() - .getInt( HOLE_PUNCH_PKT_COUNT_PROPERTY, - DEFAULT_HOLE_PUNCH_PKT_COUNT); - - if (packetCount < 0) - packetCount = DEFAULT_HOLE_PUNCH_PKT_COUNT; - - try - { - StreamConnector connector = getStreamConnector(type); - - synchronized(connector) - { - if(connector.getProtocol() == StreamConnector.Protocol.TCP) - return; - - DatagramSocket socket; - - //we may want to send more than one packet in case they get lost - for(int i=0; i < packetCount; i++) - { - /* data port (RTP) */ - if((socket = connector.getDataSocket()) != null) - { - socket.send( - new DatagramPacket( - new byte[0], - 0, - target.getDataAddress().getAddress(), - target.getDataAddress().getPort())); - } - - /* control port (RTCP) */ - if((socket = connector.getControlSocket()) != null) - { - socket.send( - new DatagramPacket( - new byte[0], - 0, - target.getControlAddress().getAddress(), - target.getControlAddress().getPort())); - } - } - } - } - catch(Exception e) - { - logger.error("Error cannot send to remote peer", e); - } - } - - /** - * Set traffic class (QoS) for the RTP socket. - * - * @param target <tt>MediaStreamTarget</tt> - * @param type the {@link MediaType} of the connector we'd like to set - * traffic class - */ - protected void setTrafficClass(MediaStreamTarget target, MediaType type) - { - // get traffic class value for RTP audio/video - int trafficClass = getDSCP(type); - - if(trafficClass <= 0) - return; - - if (logger.isInfoEnabled()) - logger.info( - "Set traffic class for " + type + " to " + trafficClass); - try - { - StreamConnector connector = getStreamConnector(type); - - synchronized(connector) - { - if(connector.getProtocol() == StreamConnector.Protocol.TCP) - { - connector.getDataTCPSocket().setTrafficClass(trafficClass); - - Socket controlTCPSocket = connector.getControlTCPSocket(); - - if (controlTCPSocket != null) - controlTCPSocket.setTrafficClass(trafficClass); - } - else - { - /* data port (RTP) */ - connector.getDataSocket().setTrafficClass(trafficClass); - - /* control port (RTCP) */ - DatagramSocket controlSocket = connector.getControlSocket(); - - if (controlSocket != null) - controlSocket.setTrafficClass(trafficClass); - } - } - } - catch(Exception e) - { - logger.error( - "Failed to set traffic class for " + type + " to " - + trafficClass, - e); - } - } - - /** - * Gets the SIP traffic class associated with a specific <tt>MediaType</tt> - * from the configuration. - * - * @param type the <tt>MediaType</tt> to get the associated SIP traffic - * class of - * @return the SIP traffic class associated with the specified - * <tt>MediaType</tt> or <tt>0</tt> if not configured - */ - private int getDSCP(MediaType type) - { - String dscpPropertyName; - - switch (type) - { - case AUDIO: - dscpPropertyName = RTP_AUDIO_DSCP_PROPERTY; - break; - case VIDEO: - dscpPropertyName = RTP_VIDEO_DSCP_PROPERTY; - break; - default: - dscpPropertyName = null; - break; - } - - return - (dscpPropertyName == null) - ? 0 - : (ProtocolMediaActivator.getConfigurationService().getInt( - dscpPropertyName, - 0) - << 2); - } - - /** - * Returns the <tt>InetAddress</tt> that is most likely to be used as a - * next hop when contacting the specified <tt>destination</tt>. This is - * an utility method that is used whenever we have to choose one of our - * local addresses to put in the Via, Contact or (in the case of no - * registrar accounts) From headers. - * - * @param peer the CallPeer that we would contact. - * - * @return the <tt>InetAddress</tt> that is most likely to be to be used - * as a next hop when contacting the specified <tt>destination</tt>. - * - * @throws IllegalArgumentException if <tt>destination</tt> is not a valid - * host/ip/fqdn - */ - protected abstract InetAddress getIntendedDestination(U peer); - - /** - * Returns the {@link MediaAwareCallPeer} that this transport manager is - * serving. - * - * @return the {@link MediaAwareCallPeer} that this transport manager is - * serving. - */ - public U getCallPeer() - { - return callPeer; - } - - /** - * Returns the port tracker that we are supposed to use when binding ports - * for the specified {@link MediaType}. - * - * @param mediaType the media type that we want to obtain a locator for. - * - * @return the port tracker that we are supposed to use when binding ports - * for the specified {@link MediaType}. - */ - protected static PortTracker getPortTracker(MediaType mediaType) - { - if(MediaType.AUDIO == mediaType && audioPortTracker != null) - return audioPortTracker; - - if(MediaType.VIDEO == mediaType && videoPortTracker != null) - return videoPortTracker; - - return defaultPortTracker; - } - - /** - * Returns the port tracker that we are supposed to use when binding ports - * for the {@link MediaType} indicated by the string param. If we do not - * recognize the string as a valid media type, we simply return the default - * port tracker. - * - * @param mediaTypeStr the name of the media type that we want to obtain a - * locator for. - * - * @return the port tracker that we are supposed to use when binding ports - * for the {@link MediaType} with the specified name or the default tracker - * in case the name doesn't ring a bell. - */ - protected static PortTracker getPortTracker(String mediaTypeStr) - { - try - { - MediaType mediaType = MediaType.parseString(mediaTypeStr); - - return getPortTracker(mediaType); - } - catch (Exception exc) - { - logger.info( - "Returning default port tracker for unrecognized media type: " - + mediaTypeStr); - - return defaultPortTracker; - } - } - - /** - * Returns the extended type of the candidate selected if this transport - * manager is using ICE. - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return The extended type of the candidate selected if this transport - * manager is using ICE. Otherwise, returns null. - */ - public abstract String getICECandidateExtendedType(String streamName); - - /** - * Returns the current state of ICE processing. - * - * @return the current state of ICE processing if this transport - * manager is using ICE. Otherwise, returns null. - */ - public abstract String getICEState(); - - /** - * Returns the ICE local host address. - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE local host address if this transport - * manager is using ICE. Otherwise, returns null. - */ - public abstract InetSocketAddress getICELocalHostAddress(String streamName); - - /** - * Returns the ICE remote host address. - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE remote host address if this transport - * manager is using ICE. Otherwise, returns null. - */ - public abstract InetSocketAddress getICERemoteHostAddress( - String streamName); - - /** - * Returns the ICE local reflexive address (server or peer reflexive). - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE local reflexive address. May be null if this transport - * manager is not using ICE or if there is no reflexive address for the - * local candidate used. - */ - public abstract InetSocketAddress getICELocalReflexiveAddress( - String streamName); - - /** - * Returns the ICE remote reflexive address (server or peer reflexive). - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE remote reflexive address. May be null if this transport - * manager is not using ICE or if there is no reflexive address for the - * remote candidate used. - */ - public abstract InetSocketAddress getICERemoteReflexiveAddress( - String streamName); - - /** - * Returns the ICE local relayed address (server or peer relayed). - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE local relayed address. May be null if this transport - * manager is not using ICE or if there is no relayed address for the - * local candidate used. - */ - public abstract InetSocketAddress getICELocalRelayedAddress( - String streamName); - - /** - * Returns the ICE remote relayed address (server or peer relayed). - * - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return the ICE remote relayed address. May be null if this transport - * manager is not using ICE or if there is no relayed address for the - * remote candidate used. - */ - public abstract InetSocketAddress getICERemoteRelayedAddress( - String streamName); - - /** - * Returns the total harvesting time (in ms) for all harvesters. - * - * @return The total harvesting time (in ms) for all the harvesters. 0 if - * the ICE agent is null, or if the agent has nevers harvested. - */ - public abstract long getTotalHarvestingTime(); - - /** - * Returns the harvesting time (in ms) for the harvester given in parameter. - * - * @param harvesterName The class name if the harvester. - * - * @return The harvesting time (in ms) for the harvester given in parameter. - * 0 if this harvester does not exists, if the ICE agent is null, or if the - * agent has never harvested with this harvester. - */ - public abstract long getHarvestingTime(String harvesterName); - - /** - * Returns the number of harvesting for this agent. - * - * @return The number of harvesting for this agent. - */ - public abstract int getNbHarvesting(); - - /** - * Returns the number of harvesting time for the harvester given in - * parameter. - * - * @param harvesterName The class name if the harvester. - * - * @return The number of harvesting time for the harvester given in - * parameter. - */ - public abstract int getNbHarvesting(String harvesterName); - - /** - * Returns the ICE candidate extended type selected by the given agent. - * - * @param iceAgent The ICE agent managing the ICE offer/answer exchange, - * collecting and selecting the candidate. - * @param streamName The stream name (AUDIO, VIDEO); - * - * @return The ICE candidate extended type selected by the given agent. null - * if the iceAgent is null or if there is no candidate selected or - * available. - */ - public static String getICECandidateExtendedType( - Agent iceAgent, - String streamName) - { - if(iceAgent != null) - { - LocalCandidate localCandidate = - iceAgent.getSelectedLocalCandidate(streamName); - if(localCandidate != null) - { - return localCandidate.getExtendedType().toString(); - } - } - return null; - } -} +/*
+ * 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.media;
+
+import java.net.*;
+
+import net.java.sip.communicator.service.netaddr.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.util.*;
+
+import org.ice4j.ice.*;
+import org.jitsi.service.configuration.*;
+import org.jitsi.service.neomedia.*;
+
+/**
+ * <tt>TransportManager</tt>s are responsible for allocating ports, gathering
+ * local candidates and managing ICE whenever we are using it.
+ *
+ * @param <U> the peer extension class like for example <tt>CallPeerSipImpl</tt>
+ * or <tt>CallPeerJabberImpl</tt>
+ *
+ * @author Emil Ivov
+ * @author Lyubomir Marinov
+ * @author Sebastien Vincent
+ */
+public abstract class TransportManager<U extends MediaAwareCallPeer<?, ?, ?>>
+{
+ /**
+ * The <tt>Logger</tt> used by the <tt>TransportManager</tt>
+ * class and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(TransportManager.class);
+
+ /**
+ * The port tracker that we should use when binding generic media streams.
+ * <p>
+ * Initialized by {@link #initializePortNumbers()}.
+ * </p>
+ */
+ private static PortTracker defaultPortTracker = new PortTracker(5000, 6000);
+
+ /**
+ * The port tracker that we should use when binding video media streams.
+ * <p>
+ * Potentially initialized by {@link #initializePortNumbers()} if the
+ * necessary properties are set.
+ * </p>
+ */
+ private static PortTracker videoPortTracker = null;
+
+ /**
+ * The port tracker that we should use when binding data channels.
+ * <p>
+ * Potentially initialized by {@link #initializePortNumbers()} if the
+ * necessary properties are set
+ * </p>
+ */
+ private static PortTracker dataChannelPortTracker = null;
+
+ /**
+ * The port tracker that we should use when binding data media streams.
+ * <p>
+ * Potentially initialized by {@link #initializePortNumbers()} if the
+ * necessary properties are set
+ * </p>
+ */
+ private static PortTracker audioPortTracker = null;
+
+ /**
+ * RTP audio DSCP configuration property name.
+ */
+ private static final String RTP_AUDIO_DSCP_PROPERTY =
+ "net.java.sip.communicator.impl.protocol.RTP_AUDIO_DSCP";
+
+ /**
+ * RTP video DSCP configuration property name.
+ */
+ private static final String RTP_VIDEO_DSCP_PROPERTY =
+ "net.java.sip.communicator.impl.protocol.RTP_VIDEO_DSCP";
+
+ /**
+ * Number of empty UDP packets to send for NAT hole punching.
+ */
+ private static final String HOLE_PUNCH_PKT_COUNT_PROPERTY =
+ "net.java.sip.communicator.impl.protocol.HOLE_PUNCH_PKT_COUNT";
+
+ /**
+ * Number of empty UDP packets to send for NAT hole punching.
+ */
+ private static final int DEFAULT_HOLE_PUNCH_PKT_COUNT = 3;
+
+ /**
+ * The {@link MediaAwareCallPeer} whose traffic we will be taking care of.
+ */
+ private U callPeer;
+
+ /**
+ * The RTP/RTCP socket couples that this <tt>TransportManager</tt> uses to
+ * send and receive media flows through indexed by <tt>MediaType</tt>
+ * (ordinal).
+ */
+ private final StreamConnector[] streamConnectors
+ = new StreamConnector[MediaType.values().length];
+
+ /**
+ * Creates a new instance of this transport manager, binding it to the
+ * specified peer.
+ *
+ * @param callPeer the {@link MediaAwareCallPeer} whose traffic we will be
+ * taking care of.
+ */
+ protected TransportManager(U callPeer)
+ {
+ this.callPeer = callPeer;
+ }
+
+ /**
+ * Returns the <tt>StreamConnector</tt> instance that this media handler
+ * should use for streams of the specified <tt>mediaType</tt>. The method
+ * would also create a new <tt>StreamConnector</tt> if no connector has
+ * been initialized for this <tt>mediaType</tt> yet or in case one
+ * of its underlying sockets has been closed.
+ *
+ * @param mediaType the <tt>MediaType</tt> that we'd like to create a
+ * connector for.
+ * @return this media handler's <tt>StreamConnector</tt> for the specified
+ * <tt>mediaType</tt>.
+ *
+ * @throws OperationFailedException in case we failed to initialize our
+ * connector.
+ */
+ public StreamConnector getStreamConnector(MediaType mediaType)
+ throws OperationFailedException
+ {
+ int streamConnectorIndex = mediaType.ordinal();
+ StreamConnector streamConnector
+ = streamConnectors[streamConnectorIndex];
+
+ if((streamConnector == null)
+ || (streamConnector.getProtocol() == StreamConnector.Protocol.UDP))
+ {
+ DatagramSocket controlSocket;
+
+ if((streamConnector == null)
+ || streamConnector.getDataSocket().isClosed()
+ || (((controlSocket = streamConnector.getControlSocket())
+ != null)
+ && controlSocket.isClosed()))
+ {
+ streamConnectors[streamConnectorIndex]
+ = streamConnector
+ = createStreamConnector(mediaType);
+ }
+ }
+ else if(streamConnector.getProtocol() == StreamConnector.Protocol.TCP)
+ {
+ Socket controlTCPSocket;
+
+ if(streamConnector.getDataTCPSocket().isClosed()
+ || (((controlTCPSocket = streamConnector.getControlTCPSocket())
+ != null)
+ && controlTCPSocket.isClosed()))
+ {
+ streamConnectors[streamConnectorIndex]
+ = streamConnector
+ = createStreamConnector(mediaType);
+ }
+ }
+ return streamConnector;
+ }
+
+ /**
+ * Closes the existing <tt>StreamConnector</tt>, if any, associated with a
+ * specific <tt>MediaType</tt> and removes its reference from this
+ * <tt>TransportManager</tt>.
+ *
+ * @param mediaType the <tt>MediaType</tt> associated with the
+ * <tt>StreamConnector</tt> to close
+ */
+ public void closeStreamConnector(MediaType mediaType)
+ {
+ int index = mediaType.ordinal();
+ StreamConnector streamConnector = streamConnectors[index];
+
+ if (streamConnector != null)
+ {
+ closeStreamConnector(mediaType, streamConnector);
+ streamConnectors[index] = null;
+ }
+ }
+
+ /**
+ * Closes a specific <tt>StreamConnector</tt> associated with a specific
+ * <tt>MediaType</tt>. If this <tt>TransportManager</tt> has a reference to
+ * the specified <tt>streamConnector</tt>, it remains. Allows extenders to
+ * override and perform additional customizations to the closing of the
+ * specified <tt>streamConnector</tt>.
+ *
+ * @param mediaType the <tt>MediaType</tt> associated with the specified
+ * <tt>streamConnector</tt>
+ * @param streamConnector the <tt>StreamConnector</tt> to be closed
+ * @see #closeStreamConnector(MediaType)
+ */
+ protected void closeStreamConnector(
+ MediaType mediaType,
+ StreamConnector streamConnector)
+ {
+ /*
+ * XXX The connected owns the sockets so it is important that it
+ * decides whether to close them i.e. this TransportManager is not
+ * allowed to explicitly close the sockets by itself.
+ */
+ streamConnector.close();
+ }
+
+ /**
+ * Creates a media <tt>StreamConnector</tt> for a stream of a specific
+ * <tt>MediaType</tt>. The minimum and maximum of the media port boundaries
+ * are taken into account.
+ *
+ * @param mediaType the <tt>MediaType</tt> of the stream for which a
+ * <tt>StreamConnector</tt> is to be created
+ * @return a <tt>StreamConnector</tt> for the stream of the specified
+ * <tt>mediaType</tt>
+ * @throws OperationFailedException if the binding of the sockets fails
+ */
+ protected StreamConnector createStreamConnector(MediaType mediaType)
+ throws OperationFailedException
+ {
+ NetworkAddressManagerService nam
+ = ProtocolMediaActivator.getNetworkAddressManagerService();
+ InetAddress intendedDestination = getIntendedDestination(getCallPeer());
+ InetAddress localHostForPeer = nam.getLocalHost(intendedDestination);
+
+ //make sure our port numbers reflect the configuration service settings
+ initializePortNumbers();
+
+ PortTracker portTracker = getPortTracker(mediaType);
+
+ //create the RTP socket.
+ DatagramSocket rtpSocket = null;
+ try
+ {
+ rtpSocket = nam.createDatagramSocket(
+ localHostForPeer, portTracker.getPort(),
+ portTracker.getMinPort(), portTracker.getMaxPort());
+ }
+ catch (Exception exc)
+ {
+ throw new OperationFailedException(
+ "Failed to allocate the network ports necessary for the call.",
+ OperationFailedException.INTERNAL_ERROR, exc);
+ }
+
+ //make sure that next time we don't try to bind on occupied ports
+ //also, refuse validation in case someone set the tracker range to 1
+ portTracker.setNextPort( rtpSocket.getLocalPort() + 1, false);
+
+ //create the RTCP socket, preferably on the port following our RTP one.
+ DatagramSocket rtcpSocket = null;
+ try
+ {
+ rtcpSocket = nam.createDatagramSocket(
+ localHostForPeer, portTracker.getPort(),
+ portTracker.getMinPort(), portTracker.getMaxPort());
+ }
+ catch (Exception exc)
+ {
+ throw new OperationFailedException(
+ "Failed to allocate the network ports necessary for the call.",
+ OperationFailedException.INTERNAL_ERROR,
+ exc);
+ }
+
+ //make sure that next time we don't try to bind on occupied ports
+ portTracker.setNextPort( rtcpSocket.getLocalPort() + 1);
+
+ return new DefaultStreamConnector(rtpSocket, rtcpSocket);
+ }
+
+ /**
+ * (Re)Sets the all the port allocators to reflect current values specified
+ * in the <tt>ConfigurationService</tt>. Calling this method may very well
+ * result in creating new port allocators or destroying existing ones.
+ */
+ protected static void initializePortNumbers()
+ {
+ //try the default tracker first
+ ConfigurationService cfg
+ = ProtocolMediaActivator.getConfigurationService();
+ String minPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MIN_MEDIA_PORT_NUMBER_PROPERTY_NAME);
+ String maxPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MAX_MEDIA_PORT_NUMBER_PROPERTY_NAME);
+
+ //try to send the specified range. If there's no specified range in
+ //configuration, we'll just leave the tracker as it is: [5000 to 6000]
+ defaultPortTracker.tryRange(minPortNumberStr, maxPortNumberStr);
+
+ //try the VIDEO tracker
+ minPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MIN_VIDEO_PORT_NUMBER_PROPERTY_NAME);
+ maxPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MAX_VIDEO_PORT_NUMBER_PROPERTY_NAME);
+ //try to send the specified range. If there's no specified range in
+ //configuration, we'll just leave this tracker to null
+ videoPortTracker
+ = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr);
+
+ //try the AUDIO tracker
+ minPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MIN_AUDIO_PORT_NUMBER_PROPERTY_NAME);
+ maxPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MAX_AUDIO_PORT_NUMBER_PROPERTY_NAME);
+ //try to send the specified range. If there's no specified range in
+ //configuration, we'll just leave this tracker to null
+ audioPortTracker
+ = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr);
+
+ //try the DATA CHANNEL tracker
+ minPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MIN_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
+ maxPortNumberStr
+ = cfg.getString(
+ OperationSetBasicTelephony
+ .MAX_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
+
+ //try to send the specified range. If there's no specified range in
+ //configuration, we'll just leave this tracker to null
+ dataChannelPortTracker
+ = PortTracker.createTracker(minPortNumberStr, maxPortNumberStr);
+ }
+
+ /**
+ * Returns the <tt>InetAddress</tt> that we are using in one of our
+ * <tt>StreamConnector</tt>s or, in case we don't have any connectors yet
+ * the address returned by the our network address manager as the best local
+ * address to use when contacting the <tt>CallPeer</tt> associated with this
+ * <tt>MediaHandler</tt>. This method is primarily meant for use with the
+ * o= and c= fields of a newly created session description. The point is
+ * that we create our <tt>StreamConnector</tt>s when constructing the media
+ * descriptions so we already have a specific local address assigned to them
+ * at the time we get ready to create the c= and o= fields. It is therefore
+ * better to try and return one of these addresses before trying the net
+ * address manager again and running the slight risk of getting a different
+ * address.
+ *
+ * @return an <tt>InetAddress</tt> that we use in one of the
+ * <tt>StreamConnector</tt>s in this class.
+ */
+ public InetAddress getLastUsedLocalHost()
+ {
+ for (MediaType mediaType : MediaType.values())
+ {
+ StreamConnector streamConnector
+ = streamConnectors[mediaType.ordinal()];
+
+ if (streamConnector != null)
+ return streamConnector.getDataSocket().getLocalAddress();
+ }
+
+ NetworkAddressManagerService nam
+ = ProtocolMediaActivator.getNetworkAddressManagerService();
+ InetAddress intendedDestination = getIntendedDestination(getCallPeer());
+
+ return nam.getLocalHost(intendedDestination);
+ }
+
+ /**
+ * Send empty UDP packets to target destination data/control ports
+ * in order to open ports on NATs or and help RTP proxies latch onto our
+ * RTP ports.
+ *
+ * @param target <tt>MediaStreamTarget</tt>
+ * @param type the {@link MediaType} of the connector we'd like to send
+ * the hole punching packet through.
+ */
+ public void sendHolePunchPacket(MediaStreamTarget target, MediaType type)
+ {
+ logger.info("Send NAT hole punch packets");
+
+ //check how many hole punch packets we would be supposed to send:
+ int packetCount = ProtocolMediaActivator.getConfigurationService()
+ .getInt( HOLE_PUNCH_PKT_COUNT_PROPERTY,
+ DEFAULT_HOLE_PUNCH_PKT_COUNT);
+
+ if (packetCount < 0)
+ packetCount = DEFAULT_HOLE_PUNCH_PKT_COUNT;
+
+ try
+ {
+ StreamConnector connector = getStreamConnector(type);
+
+ synchronized(connector)
+ {
+ if(connector.getProtocol() == StreamConnector.Protocol.TCP)
+ return;
+
+ DatagramSocket socket;
+
+ //we may want to send more than one packet in case they get lost
+ for(int i=0; i < packetCount; i++)
+ {
+ /* data port (RTP) */
+ if((socket = connector.getDataSocket()) != null)
+ {
+ socket.send(
+ new DatagramPacket(
+ new byte[0],
+ 0,
+ target.getDataAddress().getAddress(),
+ target.getDataAddress().getPort()));
+ }
+
+ /* control port (RTCP) */
+ if((socket = connector.getControlSocket()) != null)
+ {
+ socket.send(
+ new DatagramPacket(
+ new byte[0],
+ 0,
+ target.getControlAddress().getAddress(),
+ target.getControlAddress().getPort()));
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error("Error cannot send to remote peer", e);
+ }
+ }
+
+ /**
+ * Set traffic class (QoS) for the RTP socket.
+ *
+ * @param target <tt>MediaStreamTarget</tt>
+ * @param type the {@link MediaType} of the connector we'd like to set
+ * traffic class
+ */
+ protected void setTrafficClass(MediaStreamTarget target, MediaType type)
+ {
+ // get traffic class value for RTP audio/video
+ int trafficClass = getDSCP(type);
+
+ if(trafficClass <= 0)
+ return;
+
+ if (logger.isInfoEnabled())
+ logger.info(
+ "Set traffic class for " + type + " to " + trafficClass);
+ try
+ {
+ StreamConnector connector = getStreamConnector(type);
+
+ synchronized(connector)
+ {
+ if(connector.getProtocol() == StreamConnector.Protocol.TCP)
+ {
+ connector.getDataTCPSocket().setTrafficClass(trafficClass);
+
+ Socket controlTCPSocket = connector.getControlTCPSocket();
+
+ if (controlTCPSocket != null)
+ controlTCPSocket.setTrafficClass(trafficClass);
+ }
+ else
+ {
+ /* data port (RTP) */
+ connector.getDataSocket().setTrafficClass(trafficClass);
+
+ /* control port (RTCP) */
+ DatagramSocket controlSocket = connector.getControlSocket();
+
+ if (controlSocket != null)
+ controlSocket.setTrafficClass(trafficClass);
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ logger.error(
+ "Failed to set traffic class for " + type + " to "
+ + trafficClass,
+ e);
+ }
+ }
+
+ /**
+ * Gets the SIP traffic class associated with a specific <tt>MediaType</tt>
+ * from the configuration.
+ *
+ * @param type the <tt>MediaType</tt> to get the associated SIP traffic
+ * class of
+ * @return the SIP traffic class associated with the specified
+ * <tt>MediaType</tt> or <tt>0</tt> if not configured
+ */
+ private int getDSCP(MediaType type)
+ {
+ String dscpPropertyName;
+
+ switch (type)
+ {
+ case AUDIO:
+ dscpPropertyName = RTP_AUDIO_DSCP_PROPERTY;
+ break;
+ case VIDEO:
+ dscpPropertyName = RTP_VIDEO_DSCP_PROPERTY;
+ break;
+ default:
+ dscpPropertyName = null;
+ break;
+ }
+
+ return
+ (dscpPropertyName == null)
+ ? 0
+ : (ProtocolMediaActivator.getConfigurationService().getInt(
+ dscpPropertyName,
+ 0)
+ << 2);
+ }
+
+ /**
+ * Returns the <tt>InetAddress</tt> that is most likely to be used as a
+ * next hop when contacting the specified <tt>destination</tt>. This is
+ * an utility method that is used whenever we have to choose one of our
+ * local addresses to put in the Via, Contact or (in the case of no
+ * registrar accounts) From headers.
+ *
+ * @param peer the CallPeer that we would contact.
+ *
+ * @return the <tt>InetAddress</tt> that is most likely to be to be used
+ * as a next hop when contacting the specified <tt>destination</tt>.
+ *
+ * @throws IllegalArgumentException if <tt>destination</tt> is not a valid
+ * host/ip/fqdn
+ */
+ protected abstract InetAddress getIntendedDestination(U peer);
+
+ /**
+ * Returns the {@link MediaAwareCallPeer} that this transport manager is
+ * serving.
+ *
+ * @return the {@link MediaAwareCallPeer} that this transport manager is
+ * serving.
+ */
+ public U getCallPeer()
+ {
+ return callPeer;
+ }
+
+ /**
+ * Returns the port tracker that we are supposed to use when binding ports
+ * for the specified {@link MediaType}.
+ *
+ * @param mediaType the media type that we want to obtain a locator for.
+ *
+ * @return the port tracker that we are supposed to use when binding ports
+ * for the specified {@link MediaType}.
+ */
+ protected static PortTracker getPortTracker(MediaType mediaType)
+ {
+ if (MediaType.AUDIO == mediaType)
+ {
+ if (audioPortTracker != null)
+ return audioPortTracker;
+ }
+ else if (MediaType.VIDEO == mediaType)
+ {
+ if (videoPortTracker != null)
+ return videoPortTracker;
+ }
+
+ return defaultPortTracker;
+ }
+
+ /**
+ * Returns the port tracker that we are supposed to use when binding ports
+ * for the {@link MediaType} indicated by the string param. If we do not
+ * recognize the string as a valid media type, we simply return the default
+ * port tracker.
+ *
+ * @param mediaTypeStr the name of the media type that we want to obtain a
+ * locator for.
+ *
+ * @return the port tracker that we are supposed to use when binding ports
+ * for the {@link MediaType} with the specified name or the default tracker
+ * in case the name doesn't ring a bell.
+ */
+ protected static PortTracker getPortTracker(String mediaTypeStr)
+ {
+ try
+ {
+ MediaType mediaType = MediaType.parseString(mediaTypeStr);
+
+ return getPortTracker(mediaType);
+ }
+ catch (Exception exc)
+ {
+ logger.info(
+ "Returning default port tracker for unrecognized media type: "
+ + mediaTypeStr);
+
+ return defaultPortTracker;
+ }
+ }
+
+ /**
+ * Returns the extended type of the candidate selected if this transport
+ * manager is using ICE.
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return The extended type of the candidate selected if this transport
+ * manager is using ICE. Otherwise, returns null.
+ */
+ public abstract String getICECandidateExtendedType(String streamName);
+
+ /**
+ * Returns the current state of ICE processing.
+ *
+ * @return the current state of ICE processing if this transport
+ * manager is using ICE. Otherwise, returns null.
+ */
+ public abstract String getICEState();
+
+ /**
+ * Returns the ICE local host address.
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE local host address if this transport
+ * manager is using ICE. Otherwise, returns null.
+ */
+ public abstract InetSocketAddress getICELocalHostAddress(String streamName);
+
+ /**
+ * Returns the ICE remote host address.
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE remote host address if this transport
+ * manager is using ICE. Otherwise, returns null.
+ */
+ public abstract InetSocketAddress getICERemoteHostAddress(
+ String streamName);
+
+ /**
+ * Returns the ICE local reflexive address (server or peer reflexive).
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE local reflexive address. May be null if this transport
+ * manager is not using ICE or if there is no reflexive address for the
+ * local candidate used.
+ */
+ public abstract InetSocketAddress getICELocalReflexiveAddress(
+ String streamName);
+
+ /**
+ * Returns the ICE remote reflexive address (server or peer reflexive).
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE remote reflexive address. May be null if this transport
+ * manager is not using ICE or if there is no reflexive address for the
+ * remote candidate used.
+ */
+ public abstract InetSocketAddress getICERemoteReflexiveAddress(
+ String streamName);
+
+ /**
+ * Returns the ICE local relayed address (server or peer relayed).
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE local relayed address. May be null if this transport
+ * manager is not using ICE or if there is no relayed address for the
+ * local candidate used.
+ */
+ public abstract InetSocketAddress getICELocalRelayedAddress(
+ String streamName);
+
+ /**
+ * Returns the ICE remote relayed address (server or peer relayed).
+ *
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return the ICE remote relayed address. May be null if this transport
+ * manager is not using ICE or if there is no relayed address for the
+ * remote candidate used.
+ */
+ public abstract InetSocketAddress getICERemoteRelayedAddress(
+ String streamName);
+
+ /**
+ * Returns the total harvesting time (in ms) for all harvesters.
+ *
+ * @return The total harvesting time (in ms) for all the harvesters. 0 if
+ * the ICE agent is null, or if the agent has nevers harvested.
+ */
+ public abstract long getTotalHarvestingTime();
+
+ /**
+ * Returns the harvesting time (in ms) for the harvester given in parameter.
+ *
+ * @param harvesterName The class name if the harvester.
+ *
+ * @return The harvesting time (in ms) for the harvester given in parameter.
+ * 0 if this harvester does not exists, if the ICE agent is null, or if the
+ * agent has never harvested with this harvester.
+ */
+ public abstract long getHarvestingTime(String harvesterName);
+
+ /**
+ * Returns the number of harvesting for this agent.
+ *
+ * @return The number of harvesting for this agent.
+ */
+ public abstract int getNbHarvesting();
+
+ /**
+ * Returns the number of harvesting time for the harvester given in
+ * parameter.
+ *
+ * @param harvesterName The class name if the harvester.
+ *
+ * @return The number of harvesting time for the harvester given in
+ * parameter.
+ */
+ public abstract int getNbHarvesting(String harvesterName);
+
+ /**
+ * Returns the ICE candidate extended type selected by the given agent.
+ *
+ * @param iceAgent The ICE agent managing the ICE offer/answer exchange,
+ * collecting and selecting the candidate.
+ * @param streamName The stream name (AUDIO, VIDEO);
+ *
+ * @return The ICE candidate extended type selected by the given agent. null
+ * if the iceAgent is null or if there is no candidate selected or
+ * available.
+ */
+ public static String getICECandidateExtendedType(
+ Agent iceAgent,
+ String streamName)
+ {
+ if(iceAgent != null)
+ {
+ LocalCandidate localCandidate
+ = iceAgent.getSelectedLocalCandidate(streamName);
+
+ if(localCandidate != null)
+ return localCandidate.getExtendedType().toString();
+ }
+ return null;
+ }
+}
|