/* * 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.conference; import java.awt.*; import java.awt.event.*; import java.util.*; 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.utils.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.service.globaldisplaydetails.*; 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.util.skin.*; import net.java.sip.communicator.util.call.CallPeerAdapter; import org.jitsi.service.neomedia.*; import org.jitsi.service.resources.*; /** * Depicts a single <tt>CallPeer</tt> who participates in a telephony conference * and is not a focus or the local user/peer (identified by a specific * <tt>Call</tt> instance). * * @author Yana Stamcheva * @author Lyubomir Marinov * @author Adam Netocny */ public class ConferencePeerPanel extends BasicConferenceParticipantPanel<Object> implements ConferenceCallPeerRenderer, Skinnable { /** * Serial version UID. */ private static final long serialVersionUID = 0L; /** * The <tt>Call</tt> which represents the local peer depicted by this * instance. If <tt>null</tt>, then this instance does not depict the local * peer and depicts {@link #callPeer}. */ private final Call call; /** * The <tt>CallPeer</tt> depicted by this instance. If <tt>null</tt>, then * this instance depicts the local peer represented by {@link #call}. */ private final CallPeer callPeer; /** * The <tt>CallPeerAdapter</tt> which implements common * <tt>CallPeer</tt>-related listeners on behalf of this instance. */ private final CallPeerAdapter callPeerAdapter; /** * The tools menu available for each peer. */ private CallPeerMenu callPeerMenu; /** * The DTMF label. */ private final JLabel dtmfLabel = new JLabel(); /** * The label showing whether the voice has been set to mute. */ private final JLabel holdStatusLabel = new JLabel(); /** * The label showing whether the voice has been set to mute. */ private final JLabel muteStatusLabel = new JLabel(); /** * The component showing the security details. */ private SecurityPanel<?> securityPanel; /** * The <tt>SoundLevelListener</tt> which listens to the changes in the * audio/sound level of the model of this instance. If {@link #callPeer} is * non-<tt>null</tt>, <tt>callPeer</tt> is the model of this instance i.e. * <tt>soundLevelListener</tt> will be added to the audio stream of * <tt>callPeer</tt> and will listen to local calculations of the audio * levels of the remote peer; otherwise, {@link #call} is the model of this * instance i.e. <tt>soundLevelListener</tt> will be added to <tt>call</tt> * and will listen to local calculations of the audio levels of the local * peer. */ private final SoundLevelListenerImpl soundLevelListener = new SoundLevelListenerImpl(); /** * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict the * local peer represented by a specific <tt>Call</tt> on behalf of a * specific <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>. * * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests * the initialization of the new instance and which will use the new * instance to depict the local peer represented by the specified * <tt>Call</tt> * @param call the <tt>Call</tt> which represents the local peer to be * depicted by the new instance * @param video <tt>true</tt> if the new instance will be associated with a * display of video (e.g. which will be streaming to the <tt>CallPeer</tt>s * associated with the specified <tt>call</tt>); otherwise, <tt>false</tt> * @throws NullPointerException if <tt>call</tt> is <tt>null</tt> */ public ConferencePeerPanel( BasicConferenceCallPanel callRenderer, Call call, boolean video) { super(callRenderer, call, video); if (call == null) throw new NullPointerException("call"); this.call = call; this.callPeer = null; callPeerAdapter = null; String globalDisplayName = null; // Try to set the same image as the one in the main window. This way // we improve our chances to have an image, instead of looking only at // the protocol provider avatar, which could be null, we look for any // image coming from one of our accounts. GlobalDisplayDetailsService displayDetailsService = GuiActivator.getGlobalDisplayDetailsService(); if(displayDetailsService != null) { byte[] globalAccountImage = displayDetailsService.getGlobalDisplayAvatar(); if((globalAccountImage != null) && (globalAccountImage.length > 0)) setPeerImage(globalAccountImage); globalDisplayName = displayDetailsService.getGlobalDisplayName(); } ResourceManagementService resources = GuiActivator.getResources(); setPeerName( (globalDisplayName != null && globalDisplayName.length() > 0) ? globalDisplayName + " (" + call.getProtocolProvider().getAccountID().getDisplayName() + ")" : call.getProtocolProvider().getAccountID().getDisplayName()); setTitleBackground( video ? Color.DARK_GRAY : new Color( resources.getColor( "service.gui.CALL_LOCAL_USER_BACKGROUND"))); if(isSoundLevelIndicatorEnabled()) call.addLocalUserSoundLevelListener(soundLevelListener); } /** * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict a * specific <tt>CallPeer</tt> on behalf of a specific * <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>. * * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests * the initialization of the new instance and which will use the new * instance to depict the specified <tt>CallPeer</tt> * @param callPeer the <tt>CallPeer</tt> to be depicted by the new instance */ public ConferencePeerPanel( BasicConferenceCallPanel callRenderer, CallPeer callPeer) { this(callRenderer, callPeer, false); } /** * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict a * specific <tt>CallPeer</tt> on behalf of a specific * <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>. * * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests * the initialization of the new instance and which will use the new * instance to depict the specified <tt>CallPeer</tt> * @param callPeer the <tt>CallPeer</tt> to be depicted by the new instance * @param video <tt>true</tt> if the new instance will be associated with a * display of video (e.g. which will be streaming from the specified * <tt>callPeer</tt>); otherwise, <tt>false</tt> * @throws NullPointerException if <tt>callPeer</tt> is <tt>null</tt> */ public ConferencePeerPanel( BasicConferenceCallPanel callRenderer, CallPeer callPeer, boolean video) { super(callRenderer, callPeer, video); if (callPeer == null) throw new NullPointerException("callPeer"); this.call = null; this.callPeer = callPeer; securityPanel = SecurityPanel.create(this, callPeer, null); setMute(callPeer.isMute()); setPeerImage(CallManager.getPeerImage(callPeer)); setPeerName(callPeer.getDisplayName()); // We initialize the status bar for call peers only. initStatusBar(callPeer); callPeerMenu = new CallPeerMenu(callPeer, callRenderer); SIPCommMenuBar menuBar = new SIPCommMenuBar(); menuBar.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); menuBar.add(callPeerMenu); addToNameBar(menuBar); setTitleBackground( video ? Color.DARK_GRAY : new Color( GuiActivator.getResources().getColor( "service.gui.CALL_PEER_NAME_BACKGROUND"))); initSecuritySettings(); callPeerAdapter = new CallPeerAdapter(this.callPeer, this); if(isSoundLevelIndicatorEnabled()) this.callPeer.addStreamSoundLevelListener(soundLevelListener); } /** * {@inheritDoc} */ public void dispose() { if (callPeerAdapter != null) callPeerAdapter.dispose(); if (callPeer != null) { callPeer.removeConferenceMembersSoundLevelListener( soundLevelListener); callPeer.removeStreamSoundLevelListener(soundLevelListener); } if (call != null) call.removeLocalUserSoundLevelListener(soundLevelListener); } /** * Gets the <tt>Call</tt> associated with this instance. If this instance * depicts the local peer, it was initialized with a specific <tt>Call</tt> * which represents the local peer. If this instance depicts an actual * <tt>CallPeer</tt>, its associated <tt>Call</tt> is returned. * * @return the <tt>Call</tt> associated with this instance */ public Call getCall() { return (callPeer == null) ? call : callPeer.getCall(); } /** * Returns <tt>CallPeer</tt> contact address. * * @return <tt>CallPeer</tt> contact address */ public String getCallPeerContactAddress() { return callPeer.getURI(); } /** * {@inheritDoc} * * Returns this instance. */ public Component getComponent() { return this; } /** * Initializes the security settings for this call peer. */ private void initSecuritySettings() { securityStatusLabel.setSecurityOff(); CallPeerSecurityStatusEvent securityEvent = callPeer.getCurrentSecuritySettings(); if (securityEvent != null && securityEvent instanceof CallPeerSecurityOnEvent) { CallPeerSecurityOnEvent securityOnEvt = (CallPeerSecurityOnEvent) securityEvent; securityOn(securityOnEvt); } securityStatusLabel.setBorder( BorderFactory.createEmptyBorder(2, 5, 2, 5)); securityStatusLabel.addMouseListener(new MouseAdapter() { /** * Invoked when a mouse button has been pressed on a component. */ @Override public void mousePressed(MouseEvent e) { setSecurityPanelVisible( !getCallPanel().getCallWindow().getFrame() .getGlassPane().isVisible()); } }); } /** * Initializes the status bar component for the given <tt>callPeer</tt>. * * @param callPeer the underlying peer, which status would be displayed */ private void initStatusBar(CallPeer callPeer) { initSecurityStatusLabel(); this.setParticipantState(callPeer.getState().getLocalizedStateString()); this.addToStatusBar(holdStatusLabel); this.addToStatusBar(muteStatusLabel); this.addToStatusBar(dtmfLabel); } /** * Indicates if the local video component is currently visible. * * @return <tt>true</tt> if the local video component is currently visible, * <tt>false</tt> - otherwise */ public boolean isLocalVideoVisible() { return false; } /** * Determines whether the indicator which depicts the sound/audio levels (of * the local or remote peer in a call) is to be enabled. For example, the * indicator may be disabled for performance-related reasons. * * @return <tt>true</tt> if the indicator which depicts the sound/audio * levels (of the local or remote peer in a call) is to be enabled; * otherwise, <tt>false</tt> */ static boolean isSoundLevelIndicatorEnabled() { return !GuiActivator.getConfigurationService().getBoolean( "net.java.sip.communicator.impl.gui.main.call" + ".DISABLE_SOUND_LEVEL_INDICATORS", false); } /** * Reloads style information. */ @Override public void loadSkin() { setTitleBackground( new Color( GuiActivator.getResources().getColor( "service.gui.CALL_LOCAL_USER_BACKGROUND"))); if(muteStatusLabel.getIcon() != null) { muteStatusLabel.setIcon( new ImageIcon( ImageLoader.getImage( ImageLoader.MUTE_STATUS_ICON))); } if(holdStatusLabel.getIcon() != null) { holdStatusLabel.setIcon( new ImageIcon( ImageLoader.getImage( ImageLoader.HOLD_STATUS_ICON))); } if(callPeerMenu != null) callPeerMenu.loadSkin(); } /** * Prints the given DTMG character through this <tt>CallPeerRenderer</tt>. * @param dtmfChar the DTMF char to print */ public void printDTMFTone(char dtmfChar) { dtmfLabel.setText(dtmfLabel.getText() + dtmfChar); } /** * Re-dispatches glass pane mouse events only in case they occur on the * security panel. * * @param glassPane the glass pane * @param e the mouse event in question */ private void redispatchMouseEvent(Component glassPane, MouseEvent e) { Point glassPanePoint = e.getPoint(); Point securityPanelPoint = SwingUtilities.convertPoint( glassPane, glassPanePoint, securityPanel); Component component; Point componentPoint; if (securityPanelPoint.y > 0) { component = securityPanel; componentPoint = securityPanelPoint; } else { Container contentPane = getCallPanel().getCallWindow().getFrame().getContentPane(); Point containerPoint = SwingUtilities.convertPoint( glassPane, glassPanePoint, contentPane); component = SwingUtilities.getDeepestComponentAt( contentPane, containerPoint.x, containerPoint.y); componentPoint = SwingUtilities.convertPoint( contentPane, glassPanePoint, component); } if (component != null) { component.dispatchEvent( new MouseEvent( component, e.getID(), e.getWhen(), e.getModifiers(), componentPoint.x, componentPoint.y, e.getClickCount(), e.isPopupTrigger())); } e.consume(); } /** * The handler for the security event received. The security event * for starting establish a secure connection. * * @param securityNegotiationStartedEvent * the security started event received */ public void securityNegotiationStarted( CallPeerSecurityNegotiationStartedEvent securityNegotiationStartedEvent) { } /** * Indicates that the security has gone off. * * @param evt Details about the event that caused this message. */ @Override public void securityOff(CallPeerSecurityOffEvent evt) { super.securityOff(evt); if(securityPanel != null) { securityPanel.securityOff(evt); } } /** * Indicates that the security is turned on. * <p> * Sets the secured status icon to the status panel and initializes/updates * the corresponding security details. * * @param evt Details about the event that caused this message. */ @Override public void securityOn(CallPeerSecurityOnEvent evt) { super.securityOn(evt); // If the securityOn is called without a specific event, we'll just call // the super and we'll return. if (evt == null) return; SrtpControl srtpControl = evt.getSecurityController(); // In case this is the local peer. if (securityPanel == null) return; // if we have some other panel, using other control if (securityPanel.getSecurityControl() == null || !srtpControl.getClass().isInstance( securityPanel.getSecurityControl())) { setSecurityPanelVisible(false); securityPanel = SecurityPanel.create(this, callPeer, srtpControl); } securityPanel.securityOn(evt); boolean isSecurityLowPriority = Boolean.parseBoolean( GuiActivator.getResources().getSettingsString( "impl.gui.I_DONT_CARE_THAT_MUCH_ABOUT_SECURITY")); if (srtpControl instanceof ZrtpControl && !((ZrtpControl) srtpControl).isSecurityVerified() && !isSecurityLowPriority) { setSecurityPanelVisible(true); } } /** * Indicates that the security status is pending confirmation. */ @Override public void securityPending() { super.securityPending(); } /** * Indicates that the security is timeouted, is not supported by the * other end. * @param evt Details about the event that caused this message. */ public void securityTimeout(CallPeerSecurityTimeoutEvent evt) { if(Boolean.parseBoolean(GuiActivator.getResources() .getSettingsString("impl.gui.PARANOIA_UI"))) { try { CallPeer peer = (CallPeer) evt.getSource(); OperationSetBasicTelephony<?> telephony = peer.getProtocolProvider().getOperationSet( OperationSetBasicTelephony.class); telephony.hangupCallPeer( peer, OperationSetBasicTelephony.HANGUP_REASON_ENCRYPTION_REQUIRED, "Encryption Required!"); } catch(OperationFailedException ex) { Logger.getLogger(getClass()) .error("Failed to hangup peer", ex); } } } /** * Sets the reason of a call failure if one occurs. The renderer should * display this reason to the user. * @param reason the reason to display */ @Override public void setErrorReason(String reason) { super.setErrorReason(reason); } /** * Shows/hides the local video component. * * @param isVisible <tt>true</tt> to show the local video, <tt>false</tt> - * otherwise */ public void setLocalVideoVisible(boolean isVisible) {} /** * Sets the mute status icon to the status panel. * * @param isMute indicates if the call with this peer is * muted */ public void setMute(boolean isMute) { if(isMute) muteStatusLabel.setIcon(new ImageIcon( ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON))); else muteStatusLabel.setIcon(null); this.revalidate(); this.repaint(); } /** * Sets the "on hold" property value. * @param isOnHold indicates if the call with this peer is put on hold */ public void setOnHold(boolean isOnHold) { if(isOnHold) holdStatusLabel.setIcon(new ImageIcon( ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON))); else holdStatusLabel.setIcon(null); this.revalidate(); this.repaint(); } /** * Sets the <tt>icon</tt> of the peer. * @param icon the icon to set */ public void setPeerImage(byte[] icon) { // If this is the local peer (i.e. us) or any peer, but the focus peer. if (callPeer == null || !callPeer.isConferenceFocus()) setParticipantImage(icon); } /** * Sets the name of the peer. * @param name the name of the peer */ public void setPeerName(String name) { setParticipantName(name); } /** * Sets the state of the contained call peer by specifying the * state name. * * @param oldState the previous state of the peer * @param newState the new state of the peer * @param stateString the state of the contained call peer */ public void setPeerState( CallPeerState oldState, CallPeerState newState, String stateString) { this.setParticipantState(stateString); } /** * Shows/hides the security panel. * * @param isVisible <tt>true</tt> to show the security panel, <tt>false</tt> * to hide it */ public void setSecurityPanelVisible(boolean isVisible) { final JFrame callFrame = getCallPanel().getCallWindow().getFrame(); final JPanel glassPane = (JPanel) callFrame.getGlassPane(); if (!isVisible) { // Need to hide the security panel explicitly in order to keep the // fade effect. securityPanel.setVisible(false); glassPane.setVisible(false); glassPane.removeAll(); } else { glassPane.setLayout(null); glassPane.addMouseListener( new MouseListener() { public void mouseClicked(MouseEvent e) { redispatchMouseEvent(glassPane, e); } public void mouseEntered(MouseEvent e) { redispatchMouseEvent(glassPane, e); } public void mouseExited(MouseEvent e) { redispatchMouseEvent(glassPane, e); } public void mousePressed(MouseEvent e) { redispatchMouseEvent(glassPane, e); } public void mouseReleased(MouseEvent e) { redispatchMouseEvent(glassPane, e); } }); Point securityLabelPoint = securityStatusLabel.getLocation(); Point newPoint = SwingUtilities.convertPoint( securityStatusLabel.getParent(), securityLabelPoint.x, securityLabelPoint.y, callFrame); securityPanel.setBeginPoint( new Point((int) newPoint.getX() + 15, 0)); securityPanel.setBounds( 0, (int) newPoint.getY() - 5, callFrame.getWidth(), 110); glassPane.add(securityPanel); // Need to show the security panel explicitly in order to keep the // fade effect. securityPanel.setVisible(true); glassPane.setVisible(true); glassPane.addComponentListener( new ComponentAdapter() { /** * Invoked when the component's size changes. */ @Override public void componentResized(ComponentEvent e) { if (glassPane.isVisible()) { glassPane.setVisible(false); callFrame.removeComponentListener(this); } } }); } } /** * Implements the various types of listeners which get notified about * changes in the sound/audio levels of the model of this * <tt>ConferencePeerPanel</tt> and updates its sound level indicator. */ private class SoundLevelListenerImpl implements ConferenceMembersSoundLevelListener, SoundLevelListener { /** * {@inheritDoc} */ public void soundLevelChanged(ConferenceMembersSoundLevelEvent ev) { /* * If the callPeer depicted by this ConferencePeerPanel instance is * represented as a ConferenceMember, update the sound level * indicator of this ConferencePeerPanel instance with the specified * sound level (value). */ for (Map.Entry<ConferenceMember,Integer> e : ev.getLevels().entrySet()) { if (CallManager.addressesAreEqual( e.getKey().getAddress(), callPeer.getAddress())) { updateSoundBar(e.getValue()); break; } } } /** * {@inheritDoc} */ public void soundLevelChanged(Object source, int level) { if (source.equals(participant)) { /* * If the remote peer is a conference focus and there is at * least one other member (i.e. different than the remote peer * and the local peer/user), we expect the remote peer to send * us CSRC-based audio levels. Otherwise, the stream-based audio * levels may conflict with the CSRC-based audio levels. */ if (callPeer != null) { int conferenceMemberCount = callPeer.getConferenceMemberCount(); if (conferenceMemberCount > 2) return; } updateSoundBar(level); } } } }