/* * 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.globalshortcut; import java.awt.*; import java.util.*; import java.util.List; import net.java.sip.communicator.service.globalshortcut.*; import net.java.sip.communicator.service.keybindings.*; 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.*; /** * Shortcut for call (take the call, hang up, ...). * * @author Sebastien Vincent * @author Vincent Lucas */ public class CallShortcut implements GlobalShortcutListener, CallListener { /** * The <tt>Logger</tt> used by the <tt>CallShortcut</tt> class and its * instances for logging output. */ private static final Logger logger = Logger.getLogger(CallShortcut.class); /** * Lists the call actions available: ANSWER or HANGUP. */ private enum CallAction { // Answers an incoming call. ANSWER, // Hangs up a call. HANGUP } /** * Keybindings service. */ private final KeybindingsService keybindingsService = GlobalShortcutActivator.getKeybindingsService(); /** * List of incoming calls not yet answered. */ private final List<Call> incomingCalls = new ArrayList<Call>(); /** * List of answered calls: active (off hold) or on hold. */ private final List<Call> answeredCalls = new ArrayList<Call>(); /** * Next mute state action. */ private boolean mute = true; /** * Push to talk state action. */ private boolean ptt_pressed = false; /** * Initializes a new <tt>CallShortcut</tt> instance. */ public CallShortcut() { } /** * Notifies this <tt>GlobalShortcutListener</tt> that a shortcut was * triggered. * * @param evt a <tt>GlobalShortcutEvent</tt> which describes the specifics * of the triggering of the shortcut */ public void shortcutReceived(GlobalShortcutEvent evt) { AWTKeyStroke keystroke = evt.getKeyStroke(); GlobalKeybindingSet set = keybindingsService.getGlobalBindings(); for(Map.Entry<String, List<AWTKeyStroke>> entry : set.getBindings().entrySet()) { for(AWTKeyStroke ks : entry.getValue()) { if(ks == null) continue; String entryKey = entry.getKey(); if(entryKey.equals("answer") && keystroke.getKeyCode() == ks.getKeyCode() && keystroke.getModifiers() == ks.getModifiers()) { // Try to answer the new incoming call, if there is any. manageNextIncomingCall(CallAction.ANSWER); } else if(entryKey.equals("hangup") && keystroke.getKeyCode() == ks.getKeyCode() && keystroke.getModifiers() == ks.getModifiers()) { // Try to hang up the new incoming call. if(!manageNextIncomingCall(CallAction.HANGUP)) { // There was no new incoming call. // Thus, we try to close all active calls. if(!closeAnsweredCalls(true)) { // There was no active call. // Thus, we close all answered (inactive, hold on) // calls. closeAnsweredCalls(false); } } } else if(entryKey.equals("answer_hangup") && keystroke.getKeyCode() == ks.getKeyCode() && keystroke.getModifiers() == ks.getModifiers()) { // Try to answer the new incoming call. if(!manageNextIncomingCall(CallAction.ANSWER)) { // There was no new incoming call. // Thus, we try to close all active calls. if(!closeAnsweredCalls(true)) { // There was no active call. // Thus, we close all answered (inactive, hold on) // calls. closeAnsweredCalls(false); } } } else if(entryKey.equals("mute") && keystroke.getKeyCode() == ks.getKeyCode() && keystroke.getModifiers() == ks.getModifiers()) { try { handleAllCallsMute(mute); } finally { // next action will revert change done here (mute or // unmute) mute = !mute; } } else if(entryKey.equals("push_to_talk") && keystroke.getKeyCode() == ks.getKeyCode() && keystroke.getModifiers() == ks.getModifiers() && evt.isReleased() == ptt_pressed ) { try { handleAllCallsMute(ptt_pressed); } finally { ptt_pressed = !ptt_pressed; } } } } } /** * Sets the mute state for all calls. * * @param mute the state to be set */ private void handleAllCallsMute(boolean mute) { synchronized(incomingCalls) { for(Call c : incomingCalls) handleMute(c, mute); } synchronized(answeredCalls) { for(Call c : answeredCalls) handleMute(c, mute); } } /** * This method is called by a protocol provider whenever an incoming call is * received. * * @param event a CallEvent instance describing the new incoming call */ public void incomingCallReceived(CallEvent event) { addCall(event.getSourceCall(), this.incomingCalls); } /** * This method is called by a protocol provider upon initiation of an * outgoing call. * <p> * * @param event a CalldEvent instance describing the new incoming call. */ public void outgoingCallCreated(CallEvent event) { addCall(event.getSourceCall(), this.answeredCalls); } /** * Adds a created call to the managed call list. * * @param call The call to add to the managed call list. * @param calls The managed call list. */ private static void addCall(Call call, List<Call> calls) { synchronized(calls) { if(!calls.contains(call)) calls.add(call); } } /** * Indicates that all peers have left the source call and that it has * been ended. The event may be considered redundant since there are already * events issued upon termination of a single call peer but we've * decided to keep it for listeners that are only interested in call * duration and don't want to follow other call details. * * @param event the <tt>CallEvent</tt> containing the source call. */ public void callEnded(CallEvent event) { Call sourceCall = event.getSourceCall(); removeCall(sourceCall, incomingCalls); removeCall(sourceCall, answeredCalls); } /** * Removes an ended call to the managed call list. * * @param call The call to remove from the managed call list. * @param calls The managed call list. */ private static void removeCall(Call call, List<Call> calls) { synchronized(calls) { if(calls.contains(call)) calls.remove(call); } } /** * Sets the mute state of a specific <tt>Call</tt> in accord with * {@link #mute}. * * @param call the <tt>Call</tt> to set the mute state of * @param mute indicates if the current state is mute or unmute */ private void handleMute(Call call, boolean mute) { // handle only established call if(call.getCallState() != CallState.CALL_IN_PROGRESS) return; // handle only connected peer (no on hold peer) if(call.getCallPeers().next().getState() != CallPeerState.CONNECTED) return; OperationSetBasicTelephony<?> basicTelephony = call.getProtocolProvider().getOperationSet( OperationSetBasicTelephony.class); if ((basicTelephony != null) && (mute != ((MediaAwareCall<?,?,?>) call).isMute())) { basicTelephony.setMute(call, mute); } } /** * Answers or puts on/off hold the given call. * * @param call The call to answer, to put on hold, or to put off hold. * @param callAction The action (ANSWER or HANGUP) to do. */ private static void doCallAction( final Call call, final CallAction callAction) { new Thread() { @Override public void run() { try { for (Call aCall : CallConference.getCalls(call)) { Iterator<? extends CallPeer> callPeers = aCall.getCallPeers(); OperationSetBasicTelephony<?> basicTelephony = aCall.getProtocolProvider().getOperationSet( OperationSetBasicTelephony.class); while(callPeers.hasNext()) { CallPeer callPeer = callPeers.next(); switch(callAction) { case ANSWER: if(callPeer.getState() == CallPeerState.INCOMING_CALL) { basicTelephony.answerCallPeer(callPeer); } break; case HANGUP: basicTelephony.hangupCallPeer(callPeer); break; } } } } catch(OperationFailedException ofe) { logger.error( "Failed to answer/hangup call via global shortcut", ofe); } } }.start(); } /** * Answers or hangs up the next incoming call if any. * * @param callAction The action (ANSWER or HANGUP) to do. * * @return True if the next incoming call has been answered/hanged up. False * if there is no incoming call remaining. */ private boolean manageNextIncomingCall(CallAction callAction) { synchronized(incomingCalls) { int i = incomingCalls.size(); while(i != 0) { --i; Call call = incomingCalls.get(i); // Either this incoming call is already answered, or we will // answered it. Thus, we switch it to the answered list. answeredCalls.add(call); incomingCalls.remove(i); // We find a call not answered yet. if(call.getCallState() == CallState.CALL_INITIALIZATION) { // Answer or hang up the ringing call. CallShortcut.doCallAction(call, callAction); return true; } } } return false; } /** * Closes only active calls, or all answered calls depending on the * closeOnlyActiveCalls parameter. * * @param closeOnlyActiveCalls Boolean which must be set to true to only * removes the active calls. Otherwise, the whole answered calls will be * closed. * * @return True if there was at least one call closed. False otherwise. */ private boolean closeAnsweredCalls(boolean closeOnlyActiveCalls) { boolean isAtLeastOneCallClosed = false; synchronized(answeredCalls) { int i = answeredCalls.size(); while(i != 0) { --i; Call call = answeredCalls.get(i); // If we are not limited to active call, then we close all // answered calls. Otherwise, we close only active calls (hold // off calls). if(!closeOnlyActiveCalls || CallShortcut.isActiveCall(call)) { CallShortcut.doCallAction(call, CallAction.HANGUP); answeredCalls.remove(i); isAtLeastOneCallClosed = true; } } } return isAtLeastOneCallClosed; } /** * Returns <tt>true</tt> if a specific <tt>Call</tt> is active - at least * one <tt>CallPeer</tt> is active (i.e. not on hold). * * @param call the <tt>Call</tt> to be determined whether it is active * @return <tt>true</tt> if the specified <tt>call</tt> is active; * <tt>false</tt>, otherwise. */ private static boolean isActiveCall(Call call) { for (Call conferenceCall : CallConference.getCalls(call)) { // If at least one CallPeer is active, the whole call is active. if (isAtLeastOneActiveCallPeer(conferenceCall.getCallPeers())) return true; } return false; } /** * Returns <tt>true</tt> if at least one <tt>CallPeer</tt> in a list of * <tt>CallPeer</tt>s is active i.e. is not on hold; <tt>false</tt>, * otherwise. * * @param callPeers the list of <tt>CallPeer</tt>s to check for at least one * active <tt>CallPeer</tt> * @return <tt>true</tt> if at least one <tt>CallPeer</tt> in * <tt>callPeers</tt> is active i.e. is not on hold; <tt>false</tt>, * otherwise */ private static boolean isAtLeastOneActiveCallPeer( Iterator<? extends CallPeer> callPeers) { while (callPeers.hasNext()) { CallPeer callPeer = callPeers.next(); if (!CallPeerState.isOnHold(callPeer.getState())) { // If at least one peer is active, then the call is active. return true; } } return false; } }