/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.gui.main.call; import java.awt.*; import java.awt.Container; import java.awt.event.*; import java.beans.*; import java.io.*; 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 * 25 parkButton * 30 crmButton * 50 infoButton * 100 hangupButton * * @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 info button name. */ private static final String CRM_BUTTON = "CRM_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 enable the CRM button. */ private static final String SHOW_CRM_BUTON_PROP = "net.java.sip.communicator.impl.gui.main.call.SHOW_CRM_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 button, which shows/hides participants in video * conferences. */ private static final String HIDE_PEERS_LIST_BUTON_PROP = "net.java.sip.communicator.impl.gui.main.call.HIDE_PEERS_LIST_BUTTON"; /** * Indicates if the participants list in a video conference is visible by * default. */ private static final String PEERS_LIST_HIDDEN_PROP = "net.java.sip.communicator.impl.gui.main.call.PEERS_LIST_HIDDEN"; /** * 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; /** * CRM button. */ private CallToolBarButton crmButton; /** * Park button. */ private SIPCommButton parkButton; /** * 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 button, which shows / hides the participants list in a video * conference. */ private ShowHidePeersButton showHidePeersButton; /** * 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()); } else if (buttonName.equals(CRM_BUTTON)) { String command = GuiActivator.getConfigurationService().getString( "net.java.sip.communicator.impl.gui.main.call.CRM_COMMAND"); if (command == null) { return; } List<CallPeer> callPeers = callConference.getCallPeers(); if (callPeers.isEmpty()) { logger.info("No CallPeer for CRM application found."); return; } command = String.format(command, callPeers.get(0).getAddress()); try { logger.info("Launching CRM application: " + command); Runtime.getRuntime().exec(command); } catch (IOException e) { logger.error("Unable launch CRM application", e); } } } /** * 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); settingsPanel.remove(parkButton); } 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 we are not in conf call and we have the needed opset // add the button and enable it when call is connected if(!isConference) { OperationSetTelephonyPark opsetPark = pps.getOperationSet(OperationSetTelephonyPark.class); if(opsetPark != null) { settingsPanel.add(parkButton); parkButton.setEnabled(allCallsConnected); } } } 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()); } } if (showHidePeersButton != null) { showHidePeersButton.setVisible(isConference && CallManager.isVideoStreaming(callConference)); } // 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); if (showHidePeersButton != null) showHidePeersButton.setIndex(13); chatButton.setIndex(19); parkButton.setIndex(25); if (crmButton != null) crmButton.setIndex(30); if (infoButton != null) infoButton.setIndex(50); 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. Collection<ServiceReference<PluginComponentFactory>> serRefs; String osgiFilter = "(" + net.java.sip.communicator.service.gui.Container.CONTAINER_ID + "=" + net.java.sip.communicator.service.gui.Container .CONTAINER_CALL_DIALOG.getID() + ")"; try { serRefs = GuiActivator.bundleContext.getServiceReferences( PluginComponentFactory.class, osgiFilter); } catch (InvalidSyntaxException ise) { serRefs = null; logger.error("Could not obtain plugin reference.", ise); } if ((serRefs != null) && !serRefs.isEmpty()) { for (ServiceReference<PluginComponentFactory> serRef : serRefs) { PluginComponentFactory factory = 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(SHOW_CRM_BUTON_PROP)) { crmButton = new CallToolBarButton( ImageLoader.getImage(ImageLoader.CRM), CRM_BUTTON, GuiActivator.getResources().getI18NString( "service.gui.PRESS_TO_OPEN_CRM")); } 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); } if (isButtonEnabled(HIDE_PEERS_LIST_BUTON_PROP)) { // If the PEERS_LIST_HIDDEN_PROP isn't specified we show the list // by default. showHidePeersButton = new ShowHidePeersButton(this, isButtonEnabled(PEERS_LIST_HIDDEN_PROP)); } localLevel = new InputVolumeControlButton( callConference, ImageLoader.MICROPHONE, ImageLoader.MUTE_BUTTON, true, false); remoteLevel = new OutputVolumeControlButton( callConference, ImageLoader.VOLUME_CONTROL_BUTTON, false, true) .getComponent(); parkButton = new ParkCallButton(aCall); /* * 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 (crmButton != null) crmButton.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 (crmButton != null) settingsPanel.add(crmButton); 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 (transferCallButton != null) settingsPanel.add(transferCallButton); if (videoButton != null) settingsPanel.add(videoButton); if (showHidePeersButton != null) settingsPanel.add(showHidePeersButton); // 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); } /** * Shows/hides the thumbnails list in the case of video conference. * * @param show <tt>true</tt> to show the thumbnails list, <tt>false</tt> * to hide it */ public void showThumbnailsList(boolean show) { // This shouldn't happen, but if we aren't in a video conference we // have nothing to do here. if (!(callPanel instanceof VideoConferenceCallPanel)) return; ((VideoConferenceCallPanel) callPanel).showThumbnailsList(show); } /** * 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); } } } } } }