/* * 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 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.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; /** * A base implementation of a user interface <tt>Component</tt> which depicts a * <tt>CallConference</tt> and is contained in a <tt>CallPanel</tt>. * * @author Lyubomir Marinov * @author Hristo Terezov */ public abstract class BasicConferenceCallPanel extends JPanel implements SwingCallRenderer { /** * The <tt>CallPanel</tt> which has created this instance and uses it to * depict {@link #callConference}. */ protected final CallPanel callPanel; /** * The <tt>CallConference</tt> which is depicted by this * <tt>BasicConferenceCallPanel</tt> i.e. the model of this view. */ protected final CallConference callConference; /** * The listener which listens to the <tt>CallConference</tt> depicted by * this instance, the <tt>Call</tt>s participating in it, and the * <tt>CallPeer</tt>s associated with them. */ private final CallConferenceListener callConferenceListener = new CallConferenceListener(); /** * The <tt>ConferenceCallPeerRenderer</tt>s which depict/render * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in * the telephony conference depicted by this instance. */ private final Map<CallPeer, ConferenceCallPeerRenderer> callPeerPanels = new HashMap<CallPeer, ConferenceCallPeerRenderer>(); /** * List of call peers that should be removed with delay. */ private Map<CallPeer, Timer> delayedCallPeers = new HashMap<CallPeer, Timer>(); /** * 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; /** * List of conference peer panel listeners that will be notified for adding * or removing peer panels. */ private List<ConferencePeerViewListener> peerViewListeners = new ArrayList<ConferencePeerViewListener>(); /** * 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(); } }; /** * Initializes a new <tt>BasicConferenceCallPanel</tt> instance which is to * be used by a specific <tt>CallPanel</tt> to depict a specific * <tt>CallConference</tt>. * * @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 */ protected BasicConferenceCallPanel( CallPanel callPanel, CallConference callConference) { super(new BorderLayout()); this.callPanel = callPanel; this.callConference = callConference; } /** * Creates a timer for the call peer and adds the timer and the call peer to * <tt>delayedCallPeers</tt> list. * * @param peer the peer to be added. */ public void addDelayedCallPeer(final CallPeer peer) { Timer timer = new Timer( 5000, new ActionListener() { public void actionPerformed(ActionEvent event) { removeDelayedCallPeer(peer, false); updateViewFromModel(); } }); synchronized (delayedCallPeers) { delayedCallPeers.put(peer, timer); } timer.setRepeats(false); timer.start(); } /** * Adds new <tt>ConferencePeerViewListener</tt> listener if the listener * is not already added. * * @param listener the listener to be added. */ public void addPeerViewlListener(ConferencePeerViewListener listener) { if(!peerViewListeners.contains(listener)) peerViewListeners.add(listener); } /** * Notifies this instance that a <tt>CallPeer</tt> was added to a * <tt>Call</tt> participating in the telephony conference depicted by this * instance. * * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt> * which was added and the <tt>Call</tt> to which it was added */ protected void callPeerAdded(CallPeerEvent ev) { updateViewFromModel(); } /** * Notifies this instance that a <tt>CallPeer</tt> was removed from a * <tt>Call</tt> participating in the telephony conference depicted by this * instance. * * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt> * which was removed and the <tt>Call</tt> from which it was removed */ protected void callPeerRemoved(CallPeerEvent ev) { final CallPeer peer = ev.getSourceCallPeer(); if(ev.isDelayed()) { addDelayedCallPeer(peer); } else { if(delayedCallPeers.containsKey(peer)) { removeDelayedCallPeer(peer, false); } updateViewFromModel(); } } /** * Notifies this instance that there was a change in the <tt>CallState</tt> * of a <tt>Call</tt> participating in the telephony conference depicted by * this instance. * * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt> * whose <tt>CallState</tt> was changed and the old and new * <tt>CallState</tt>s */ protected void callStateChanged(CallChangeEvent ev) { updateViewFromModel(); } /** * Notifies this instance that a <tt>CallPeer</tt> associated with a * <tt>Call</tt> participating in the telephony conference depicted by this * instance changed its <tt>conferenceFocus</tt> state/property. * * @param ev a <tt>CallPeerConferenceEvent</tt> which specifies the * <tt>CallPeer</tt> which changed its <tt>conferenceFocus</tt> * state/property */ protected void conferenceFocusChanged(CallPeerConferenceEvent ev) { updateViewFromModel(); } /** * Notifies this instance that a <tt>CallPeer</tt> associated with a * <tt>Call</tt> participating in the telephony conference depicted by this * instance added a <tt>ConferenceMember</tt> (to its list). * * @param ev a <tt>CallPeerConferenceEvent</tt> which specifies the * <tt>ConferenceMember</tt> which was added and the <tt>CallPeer</tt> which * added that <tt>ConferenceMember</tt> (to its list) */ protected void conferenceMemberAdded(CallPeerConferenceEvent ev) { updateViewFromModel(); } /** * Notifies this instance that a <tt>CallPeer</tt> associated with a * <tt>Call</tt> participating in the telephony conference received a error * packet. * * @param ev a <tt>CallPeerConferenceEvent</tt> which specifies the * <tt>CallPeer</tt> which sent the error packet and an error message. */ protected void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) { CallPeer callPeer = ev.getSourceCallPeer(); callPeerPanels.get(callPeer).setErrorReason( GuiActivator.getResources().getI18NString( "service.gui.PROBLEMS_ENCOUNTERED")); GuiActivator.getAlertUIService().showAlertPopup( GuiActivator.getResources().getI18NString( "service.gui.ERROR_RECEIVED_FROM", new String[]{callPeer.getDisplayName()}), ev.getErrorString()); } /** * Notifies this instance that a <tt>CallPeer</tt> associated with a * <tt>Call</tt> participating in the telephony conference depicted by this * instance removed a <tt>ConferenceMember</tt> (from its list). * * @param ev a <tt>CallPeerConferenceEvent</tt> which specifies the * <tt>ConferenceMember</tt> which was removed and the <tt>CallPeer</tt> * which removed that <tt>ConferenceMember</tt> (from its list) */ protected void conferenceMemberRemoved(CallPeerConferenceEvent ev) { updateViewFromModel(); } /** * 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>CallPanel</tt> when it * determines that this <tt>BasicConferenceCallPanel</tt> is no longer * necessary. */ public void dispose() { disposed = true; callConference.removeCallChangeListener(callConferenceListener); callConference.removeCallPeerConferenceListener(callConferenceListener); for (ConferenceCallPeerRenderer callPeerPanel : callPeerPanels.values()) callPeerPanel.dispose(); } /** * Creates and fires <tt>ConferencePeerViewEvent</tt> event. The method * notifies all listeners added by {@link #addPeerViewlListener} method. * * @param eventID the ID of this event which may be * {@link ConferencePeerViewEvent#CONFERENCE_PEER_VIEW_ADDED} or * {@link ConferencePeerViewEvent#CONFERENCE_PEER_VIEW_REMOVED} * @param callPeer the call peer associated with the event. * @param callPeerView the peer view associated with the event. */ public void fireConferencePeerViewEvent(int eventID, CallPeer callPeer, ConferenceCallPeerRenderer callPeerView) { for(ConferencePeerViewListener listener : peerViewListeners) { listener.peerViewRemoved( new ConferencePeerViewEvent(eventID, callPeer, callPeerView)); } } /** * {@inheritDoc} * * Implements {@link SwingCallRenderer#getCallContainer()}. */ public CallPanel getCallContainer() { return callPanel; } /** * {@inheritDoc} * * Implements {@link SwingCallRenderer#getCallPeerRenderer(CallPeer)}. */ public SwingCallPeerRenderer getCallPeerRenderer(CallPeer callPeer) { return callPeerPanels.get(callPeer); } /** * Check if the list with the delayed call peers is empty. * * @return <tt>true</tt> if the list is not empty and <tt>false</tt> if the * list is empty. */ public boolean hasDelayedCallPeers() { return !delayedCallPeers.isEmpty(); } /** * Notifies this instance that it has been fully initialized and the view * that it implements is ready to be updated from its model. Allows * extenders to provide additional initialization in their constructors * before <tt>BasicConferenceCallPanel</tt> invokes * {@link #updateViewFromModel()}. */ protected void initializeComplete() { callConference.addCallChangeListener(callConferenceListener); callConference.addCallPeerConferenceListener(callConferenceListener); updateViewFromModel(); } /** * Returns <tt>true</tt> if {@link #dispose()} has already been invoked on * this instance; otherwise, <tt>false</tt>. * * @return <tt>true</tt> if <tt>dispose()</tt> has already been invoked on * this instance; otherwise, <tt>false</tt> */ protected final boolean isDisposed() { return disposed; } /** * Notifies this instance about a specific <tt>CallPeerConferenceEvent</tt> * fired in the telephony conference depicted by this instance. * * @param ev the <tt>CallPeerConferenceEvent</tt> this instance is notified * about */ protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) { switch (ev.getEventID()) { case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED: conferenceFocusChanged(ev); break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED: conferenceMemberAdded(ev); break; case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED: conferenceMemberRemoved(ev); break; default: throw new IllegalArgumentException( "CallPeerConferenceEvent.getEventID"); } } /** * Notifies this instance about a specific <tt>CallPeerEvent</tt> fired in * the telephony conference depicted by this instance. Depending on the * <tt>eventID</tt> of <tt>ev</tt>, calls * {@link #callPeerAdded(CallPeerEvent)} or * {@link #callPeerRemoved(CallPeerEvent)}. * * @param ev the <tt>CallPeerEvent</tt> this instance is notified about */ protected void onCallPeerEvent(CallPeerEvent ev) { switch (ev.getEventID()) { case CallPeerEvent.CALL_PEER_ADDED: callPeerAdded(ev); break; case CallPeerEvent.CALL_PEER_REMOVED: callPeerRemoved(ev); break; default: throw new IllegalArgumentException("CallPeerEvent.getEventID"); } } /** * Removes a call peer from <tt>delayedCallPeers</tt> list. * * @param peer a call peer to be removed. * @param stopTimer if <tt>true</tt> the timer for the peer will be stopped * before the removal. */ public void removeDelayedCallPeer(CallPeer peer, boolean stopTimer) { if(stopTimer) { Timer timer = delayedCallPeers.get(peer); if(timer != null) timer.stop(); } synchronized (delayedCallPeers) { delayedCallPeers.remove(peer); } } /** * Removes <tt>ConferencePeerViewListener</tt> listener. * * @param listener the listener to be removed. */ public void removePeerViewListener(ConferencePeerViewListener listener) { peerViewListeners.remove(listener); } /** * Updates this view i.e. <tt>BasicConferenceCallPanel</tt> so that it * depicts the current state of its model i.e. <tt>callConference</tt>. */ protected 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 the <tt>ConferenceCallPeerRenderer</tt> which is to depict a * specific <tt>CallPeer</tt>. Invoked by * {@link #updateViewFromModelInEventDispatchThread()} in the AWT event * dispatching thread. * * @param callPeer the <tt>CallPeer</tt> whose depicting * <tt>ConferenceCallPeerPanel</tt> is to be updated. The <tt>null</tt> * value is used to indicate the local peer. * @see #updateViewFromModel(ConferenceCallPeerRenderer, CallPeer) */ protected void updateViewFromModel(CallPeer callPeer) { ConferenceCallPeerRenderer oldCallPeerPanel = callPeerPanels.get(callPeer); ConferenceCallPeerRenderer newCallPeerPanel = updateViewFromModel(oldCallPeerPanel, callPeer); if (oldCallPeerPanel != newCallPeerPanel) { if (oldCallPeerPanel != null) { callPeerPanels.remove(callPeer); try { viewForModelRemoved(oldCallPeerPanel, callPeer); } finally { oldCallPeerPanel.dispose(); } } if (newCallPeerPanel != null) { callPeerPanels.put(callPeer, newCallPeerPanel); viewForModelAdded(newCallPeerPanel, callPeer); } } } /** * Updates the <tt>ConferenceCallPeerRenderer</tt> which is to depict a * specific <tt>CallPeer</tt>. The update is in the sense of making sure * that the existing <tt>callPeerPanel</tt> is of the right run-time type to * continue depicting the current state of <tt>callPeer</tt> and the * telephony conference in which it participates, replacing it with a new * <tt>ConferenceCallPeerRenderer</tt> if the existing one is no longer * appropriate, or creating a new <tt>ConferenceCallPeerRenderer</tt> if * there is no existing one to depict the specified <tt>callPeer</tt>. If * the existing <tt>callPeerPanel</tt> is still appropriate for the current * state of the specified <tt>callPeer</tt>, the update does not include * notifying the existing <tt>callPeerPanel</tt> that it should update its * view from its model. <tt>BasicConferenceCallPanel</tt> invokes the method * in the AWT event dispatching thread. * * @param callPeerPanel the <tt>ConferenceCallPeerRenderer</tt>, if any, * which currently depicts the specified <tt>CallPeer</tt> * @param callPeer the <tt>CallPeer</tt> whose depicting * <tt>ConferenceCallPeerPanel</tt> is to be updated. The <tt>null</tt> * value is used to indicate the local peer. * @return the <tt>ConferenceCallPeerRenderer</tt>, if any, which is to * depict the specified <tt>callPeer</tt>. If it is different from * <tt>callPeerPanel</tt> (and <tt>callPeerPanel</tt> is non-<tt>null</tt>), * <tt>callPeerPanel</tt> will be disposed of with a call to * {@link ConferenceCallPeerRenderer#dispose()}. */ protected abstract ConferenceCallPeerRenderer updateViewFromModel( ConferenceCallPeerRenderer callPeerPanel, CallPeer callPeer); /** * Updates this view i.e. <tt>BasicConferenceCallPanel</tt> so that it * depicts the current state of its model i.e. <tt>callConference</tt>. The * update is performed on the AWT event dispatching thread. */ protected 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; /* Update the view of the local peer/user. */ updateViewFromModel(null); List<CallPeer> callPeers = callConference.getCallPeers(); /* * Dispose of the callPeerPanels whose CallPeers are no longer in the * telephony conference depicted by this instance. */ for (Iterator<Map.Entry<CallPeer, ConferenceCallPeerRenderer>> entryIter = callPeerPanels.entrySet().iterator(); entryIter.hasNext();) { Map.Entry<CallPeer, ConferenceCallPeerRenderer> entry = entryIter.next(); CallPeer callPeer = entry.getKey(); if ((callPeer != null) && !callPeers.contains(callPeer) && !delayedCallPeers.containsKey(callPeer)) { ConferenceCallPeerRenderer callPeerPanel = entry.getValue(); entryIter.remove(); fireConferencePeerViewEvent( ConferencePeerViewEvent.CONFERENCE_PEER_VIEW_REMOVED, callPeer, callPeerPanel); try { viewForModelRemoved(callPeerPanel, callPeer); } finally { callPeerPanel.dispose(); } } } /* * Update the callPeerPanels whose CallPeers are still in the telephony * conference depicted by this instance. The update procedure includes * adding callPeerPanels for new CallPeers and replacing callPeerPanels * for existing CallPeers who require different callPeerPanels. */ for (CallPeer callPeer : callPeers) updateViewFromModel(callPeer); } /** * Notifies this instance that a <tt>ConferenceCallPeerRenderer</tt> was * added to depict a specific <tt>CallPeer</tt>. Implementers are expected * to add the AWT <tt>Component</tt> of the specified <tt>callPeerPanel</tt> * to their user interface hierarchy. * * @param callPeerPanel the <tt>ConferenceCallPeerRenderer</tt> which was * added to depict the specified <tt>callPeer</tt> * @param callPeer the <tt>CallPeer</tt> which is depicted by the specified * <tt>callPeerPanel</tt> */ protected abstract void viewForModelAdded( ConferenceCallPeerRenderer callPeerPanel, CallPeer callPeer); /** * Notifies this instance that a <tt>ConferenceCallPeerRenderer</tt> was * removed to no longer depict a specific <tt>CallPeer</tt>. Implementers * are expected to remove the AWT <tt>Component</tt> of the specified * <tt>callPeerPanel</tt> from their user interface hierarchy. * * @param callPeerPanel the <tt>ConferenceCallPeerRenderer</tt> which was * removed to no longer depict the specified <tt>callPeer</tt> * @param callPeer the <tt>CallPeer</tt> which is depicted by the specified * <tt>callPeerPanel</tt> */ protected abstract void viewForModelRemoved( ConferenceCallPeerRenderer callPeerPanel, CallPeer callPeer); /** * Implements the listeners which get notified about events related to the * telephony conference depicted by this <tt>BasicConferenceCallPanel</tt> * and which may cause a need to update this view from its model. */ private class CallConferenceListener extends CallPeerConferenceAdapter implements CallChangeListener { public void callPeerAdded(CallPeerEvent ev) { BasicConferenceCallPanel.this.onCallPeerEvent(ev); } public void callPeerRemoved(CallPeerEvent ev) { BasicConferenceCallPanel.this.onCallPeerEvent(ev); } public void callStateChanged(CallChangeEvent ev) { BasicConferenceCallPanel.this.callStateChanged(ev); } /** * Invokes * {@link BasicConferenceCallPanel#conferenceMemberErrorReceived( * CallPeerConferenceEvent)}. */ public void conferenceMemberErrorReceived( CallPeerConferenceEvent ev) { BasicConferenceCallPanel.this.conferenceMemberErrorReceived(ev); } /** * {@inheritDoc} * * Invokes * {@link BasicConferenceCallPanel#onCallPeerConferenceEvent( * CallPeerConferenceEvent)}. */ @Override protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev) { BasicConferenceCallPanel.this.onCallPeerConferenceEvent(ev); } } /** * Starts the timer that counts call duration. */ public void startCallTimer() { callPanel.startCallTimer(); } /** * Stops the timer that counts call duration. */ public void stopCallTimer() { callPanel.stopCallTimer(); } /** * 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 callPanel.isCallTimerStarted(); } /** * 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() { callPanel.updateHoldButtonState(); } }