/*
* 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.event.*;
import java.util.*;
import java.util.List;
import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.service.gui.call.*;
import net.java.sip.communicator.service.notification.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.resources.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.protocol.*;
/**
* Handles DTMF sending and playing sound notifications for that.
*
* @author Damian Minkov
* @author Lyubomir Marinov
*/
public class DTMFHandler
implements KeyEventDispatcher
{
/**
* All available DTMF tones and their properties such as images for buttons
* and sounds to be played during send.
*/
public static final DTMFToneInfo[] AVAILABLE_TONES
= new DTMFToneInfo[]
{
new DTMFToneInfo(
DTMFTone.DTMF_1,
KeyEvent.VK_1,
'1',
ImageLoader.ONE_DIAL_BUTTON,
ImageLoader.ONE_DIAL_BUTTON_PRESSED,
ImageLoader.ONE_DIAL_BUTTON_ROLLOVER,
ImageLoader.ONE_DIAL_BUTTON_MAC,
ImageLoader.ONE_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_ONE),
new DTMFToneInfo(
DTMFTone.DTMF_2,
KeyEvent.VK_2,
'2',
ImageLoader.TWO_DIAL_BUTTON,
ImageLoader.TWO_DIAL_BUTTON_PRESSED,
ImageLoader.TWO_DIAL_BUTTON_ROLLOVER,
ImageLoader.TWO_DIAL_BUTTON_MAC,
ImageLoader.TWO_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_TWO),
new DTMFToneInfo(
DTMFTone.DTMF_3,
KeyEvent.VK_3,
'3',
ImageLoader.THREE_DIAL_BUTTON,
ImageLoader.THREE_DIAL_BUTTON_PRESSED,
ImageLoader.THREE_DIAL_BUTTON_ROLLOVER,
ImageLoader.THREE_DIAL_BUTTON_MAC,
ImageLoader.THREE_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_THREE),
new DTMFToneInfo(
DTMFTone.DTMF_4,
KeyEvent.VK_4,
'4',
ImageLoader.FOUR_DIAL_BUTTON,
ImageLoader.FOUR_DIAL_BUTTON_PRESSED,
ImageLoader.FOUR_DIAL_BUTTON_ROLLOVER,
ImageLoader.FOUR_DIAL_BUTTON_MAC,
ImageLoader.FOUR_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_FOUR),
new DTMFToneInfo(
DTMFTone.DTMF_5,
KeyEvent.VK_5,
'5',
ImageLoader.FIVE_DIAL_BUTTON,
ImageLoader.FIVE_DIAL_BUTTON_PRESSED,
ImageLoader.FIVE_DIAL_BUTTON_ROLLOVER,
ImageLoader.FIVE_DIAL_BUTTON_MAC,
ImageLoader.FIVE_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_FIVE),
new DTMFToneInfo(
DTMFTone.DTMF_6,
KeyEvent.VK_6,
'6',
ImageLoader.SIX_DIAL_BUTTON,
ImageLoader.SIX_DIAL_BUTTON_PRESSED,
ImageLoader.SIX_DIAL_BUTTON_ROLLOVER,
ImageLoader.SIX_DIAL_BUTTON_MAC,
ImageLoader.SIX_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_SIX),
new DTMFToneInfo(
DTMFTone.DTMF_7,
KeyEvent.VK_7,
'7',
ImageLoader.SEVEN_DIAL_BUTTON,
ImageLoader.SEVEN_DIAL_BUTTON_PRESSED,
ImageLoader.SEVEN_DIAL_BUTTON_ROLLOVER,
ImageLoader.SEVEN_DIAL_BUTTON_MAC,
ImageLoader.SEVEN_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_SEVEN),
new DTMFToneInfo(
DTMFTone.DTMF_8,
KeyEvent.VK_8,
'8',
ImageLoader.EIGHT_DIAL_BUTTON,
ImageLoader.EIGHT_DIAL_BUTTON_PRESSED,
ImageLoader.EIGHT_DIAL_BUTTON_ROLLOVER,
ImageLoader.EIGHT_DIAL_BUTTON_MAC,
ImageLoader.EIGHT_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_EIGHT),
new DTMFToneInfo(
DTMFTone.DTMF_9,
KeyEvent.VK_9,
'9',
ImageLoader.NINE_DIAL_BUTTON,
ImageLoader.NINE_DIAL_BUTTON_PRESSED,
ImageLoader.NINE_DIAL_BUTTON_ROLLOVER,
ImageLoader.NINE_DIAL_BUTTON_MAC,
ImageLoader.NINE_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_NINE),
new DTMFToneInfo(
DTMFTone.DTMF_A,
KeyEvent.VK_A,
'a',
null,
null,
null,
null,
null,
null),
new DTMFToneInfo(
DTMFTone.DTMF_B,
KeyEvent.VK_B,
'b',
null,
null,
null,
null,
null,
null),
new DTMFToneInfo(
DTMFTone.DTMF_C,
KeyEvent.VK_C,
'c',
null,
null,
null,
null,
null,
null),
new DTMFToneInfo(
DTMFTone.DTMF_D,
KeyEvent.VK_D,
'd',
null,
null,
null,
null,
null,
null),
new DTMFToneInfo(
DTMFTone.DTMF_STAR,
KeyEvent.VK_ASTERISK,
'*',
ImageLoader.STAR_DIAL_BUTTON,
ImageLoader.STAR_DIAL_BUTTON_PRESSED,
ImageLoader.STAR_DIAL_BUTTON_ROLLOVER,
ImageLoader.STAR_DIAL_BUTTON_MAC,
ImageLoader.STAR_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_STAR),
new DTMFToneInfo(
DTMFTone.DTMF_0,
KeyEvent.VK_0,
'0',
ImageLoader.ZERO_DIAL_BUTTON,
ImageLoader.ZERO_DIAL_BUTTON_PRESSED,
ImageLoader.ZERO_DIAL_BUTTON_ROLLOVER,
ImageLoader.ZERO_DIAL_BUTTON_MAC,
ImageLoader.ZERO_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_ZERO),
new DTMFToneInfo(
DTMFTone.DTMF_SHARP,
KeyEvent.VK_NUMBER_SIGN,
'#',
ImageLoader.DIEZ_DIAL_BUTTON,
ImageLoader.DIEZ_DIAL_BUTTON_PRESSED,
ImageLoader.DIEZ_DIAL_BUTTON_ROLLOVER,
ImageLoader.DIEZ_DIAL_BUTTON_MAC,
ImageLoader.DIEZ_DIAL_BUTTON_MAC_ROLLOVER,
SoundProperties.DIAL_DIEZ)
};
/**
* Whether we have already loaded the defaults for DTMF tones.
*/
private static boolean defaultsLoaded = false;
/**
* The maximum number of milliseconds of idleness after which
* {@link #dtmfToneNotificationThread} should die.
*/
private static final long DTMF_TONE_NOTIFICATION_THREAD_IDLE_TIMEOUT
= 15 * 1000;
/**
* Default event type for DTMF tone.
*/
public static final String DTMF_TONE_PREFIX = "DTMFTone.";
/**
* The Logger used by the DTMFHandler class and its
* instances for logging output.
*/
private static final Logger logger = Logger.getLogger(DTMFHandler.class);
/**
* Load the defaults for DTMF tones.
*/
public static synchronized void loadDefaults()
{
if(defaultsLoaded)
return;
NotificationService notificationService
= GuiActivator.getNotificationService();
for(DTMFToneInfo info : AVAILABLE_TONES)
{
notificationService.registerDefaultNotificationForEvent(
DTMF_TONE_PREFIX + info.tone.getValue(),
new SoundNotificationAction(
info.sound,
0,
false, true, false));
}
defaultsLoaded = true;
}
/**
* The call dialog, where this handler is registered.
*/
private final CallPanel callContainer;
/**
* The list of audio DTMF tones to play.
*/
private final List dtmfToneNotifications
= new LinkedList();
/**
* The background/daemon Thread which plays the audio of
* {@link #dtmfToneNotifications} as sound notifications.
*/
private Thread dtmfToneNotificationThread;
/**
* The KeyboadFocusManager to which this instance is added as a
* KeyEventDispatcher.
*/
private KeyboardFocusManager keyboardFocusManager;
/**
* The Windows which this instance listens to for key presses and
* releases.
*/
private final List parents = new ArrayList();
/**
* Creates DTMF handler for a call.
*/
public DTMFHandler()
{
this(null);
}
/**
* Creates DTMF handler for a call.
*
* @param callContainer the CallContainer where this handler is
* registered
*/
public DTMFHandler(CallPanel callContainer)
{
this.callContainer = callContainer;
if (this.callContainer != null)
{
final Window parent = callContainer.getCallWindow().getFrame();
if (parent != null)
{
parent.addWindowListener(
new WindowAdapter()
{
@Override
public void windowClosed(WindowEvent e)
{
removeParent(parent);
}
@Override
public void windowOpened(WindowEvent e)
{
addParent(parent);
}
});
if (parent.isVisible())
addParent(parent);
}
}
}
/**
* Adds a Window on which key presses and releases are to be
* monitored for the purposes of this DTMFHandler.
*
* @param parent the Window on which key presses and releases are
* to be monitored for the purposes of this DTMFHandler
*/
public void addParent(Window parent)
{
synchronized (parents)
{
if (!parents.contains(parent)
&& parents.add(parent)
&& (keyboardFocusManager == null))
{
keyboardFocusManager
= KeyboardFocusManager.getCurrentKeyboardFocusManager();
keyboardFocusManager.addKeyEventDispatcher(this);
}
}
}
/**
* Dispatches a specific KeyEvent. If one of the parents
* registered with this DTMFHandler is focused, starts or stops
* sending a respective DTMF tone.
*
* @param e the KeyEvent to be dispatched
* @return true to stop dispatching the event or false to
* continue dispatching it. DTMFHandler always returns
* false
*/
@Override
public boolean dispatchKeyEvent(KeyEvent e)
{
if (e.getID() == KeyEvent.KEY_TYPED)
return false;
/*
* When the UI uses a single window and we do not have a callContainer,
* we do not seem to be able to deal with the situation.
*/
if ((GuiActivator.getUIService().getSingleWindowContainer() != null)
&& ((callContainer == null) || !callContainer.isFocusOwner()))
return false;
boolean dispatch = false;
synchronized (parents)
{
for (int i = 0, count = parents.size(); i < count; i++)
{
if (parents.get(i).isFocused())
{
dispatch = true;
break;
}
}
}
// If we are not focused, the KeyEvent was not meant for us.
if (dispatch)
{
for (int i = 0; i < AVAILABLE_TONES.length; i++)
{
DTMFToneInfo info = AVAILABLE_TONES[i];
if (info.keyChar == e.getKeyChar())
{
switch (e.getID())
{
case KeyEvent.KEY_PRESSED:
startSendingDtmfTone(info);
break;
case KeyEvent.KEY_RELEASED:
stopSendingDtmfTone();
break;
}
break;
}
}
}
return false;
}
/**
* Removes a Window on which key presses and releases are to no
* longer be monitored for the purposes of this DTMFHandler.
*
* @param parent the Window on which key presses and releases are
* to no longer be monitored for the purposes of this DTMFHandler
*/
public void removeParent(Window parent)
{
synchronized (parents)
{
if (parents.remove(parent)
&& parents.isEmpty()
&& (keyboardFocusManager != null))
{
keyboardFocusManager.removeKeyEventDispatcher(this);
keyboardFocusManager = null;
}
}
}
/**
* Runs in a background/daemon thread and consecutively plays each of the
* {@link #dtmfToneNotifications} through the current
* {@link NotificationService}.
*/
private void runInDTMFToneNotificationThread()
{
long idleStartTime = -1;
do
{
DTMFToneInfo toneToPlay;
synchronized (dtmfToneNotifications)
{
if (dtmfToneNotificationThread != Thread.currentThread())
break;
if (dtmfToneNotifications.isEmpty())
{
toneToPlay = null;
long now = System.currentTimeMillis();
if (idleStartTime == -1)
idleStartTime = now;
long timeout
= DTMF_TONE_NOTIFICATION_THREAD_IDLE_TIMEOUT
- (now - idleStartTime);
if (timeout <= 0)
{
break;
}
else
{
try
{
dtmfToneNotifications.wait(timeout);
}
catch (InterruptedException ie)
{
}
continue;
}
}
else
{
toneToPlay = dtmfToneNotifications.remove(0);
idleStartTime = -1;
}
}
// Play the DTMF tone as a sound notification.
if ((toneToPlay != null) && (toneToPlay.sound != null))
{
GuiActivator.getNotificationService().fireNotification(
DTMF_TONE_PREFIX + toneToPlay.tone.getValue());
}
}
while (true);
}
/**
* Initializes and starts {@link #dtmfToneNotificationThread} if it is
* null and {@link #dtmfToneNotifications} is not empty.
*/
private void startDTMFToneNotificationThreadIfNecessary()
{
synchronized (dtmfToneNotifications)
{
if((dtmfToneNotificationThread == null)
&& !dtmfToneNotifications.isEmpty())
{
Thread t
= new Thread()
{
@Override
public void run()
{
try
{
runInDTMFToneNotificationThread();
}
finally
{
synchronized (dtmfToneNotifications)
{
if (dtmfToneNotificationThread
== Thread.currentThread())
{
dtmfToneNotificationThread = null;
startDTMFToneNotificationThreadIfNecessary();
}
}
}
}
};
t.setDaemon(true);
t.setName("DTMFHandler: DTMF tone notification player");
boolean started = false;
dtmfToneNotificationThread = t;
try
{
t.start();
started = true;
}
finally
{
if (!started && (dtmfToneNotificationThread == t))
dtmfToneNotificationThread = null;
}
}
else
{
dtmfToneNotifications.notify();
}
}
}
/**
* Sends a DTMF tone to the current DTMF operation set of the given call.
*
* @param call The call to which we send DTMF-s.
* @param info The DTMF tone to send.
*/
private void startSendingDtmfTone(Call call, DTMFToneInfo info)
{
Iterator extends CallPeer> callPeers = call.getCallPeers();
try
{
while (callPeers.hasNext())
{
CallPeer peer = callPeers.next();
OperationSetDTMF dtmfOpSet
= peer.getProtocolProvider().getOperationSet(
OperationSetDTMF.class);
if (dtmfOpSet != null)
{
dtmfOpSet.startSendingDTMF(peer, info.tone);
CallPeerRenderer peerRenderer
= callContainer
.getCurrentCallRenderer()
.getCallPeerRenderer(peer);
if (peerRenderer != null)
peerRenderer.printDTMFTone(info.keyChar);
}
}
}
catch (Throwable t)
{
if (t instanceof InterruptedException)
Thread.currentThread().interrupt();
else if (t instanceof ThreadDeath)
throw (ThreadDeath) t;
else
logger.error("Failed to send a DTMF tone.", t);
}
}
/**
* Sends a DTMF tone to the current DTMF operation set.
*
* @param info The DTMF tone to send.
*/
private synchronized void startSendingDtmfTone(DTMFToneInfo info)
{
if(info.sound != null)
{
synchronized(dtmfToneNotifications)
{
dtmfToneNotifications.add(info);
startDTMFToneNotificationThreadIfNecessary();
}
}
Collection calls
= (callContainer == null)
? CallManager.getInProgressCalls()
: callContainer.getCallConference().getCalls();
if ((calls != null) && !calls.isEmpty())
{
for (Call call : calls)
startSendingDtmfTone(call, info);
}
}
/**
* Sends a DTMF tone to the current DTMF operation set.
*
* @param toneValue the value of the DTMF tone to send.
*/
public void startSendingDtmfTone(String toneValue)
{
for (int i = 0; i < AVAILABLE_TONES.length; i++)
{
DTMFToneInfo info = AVAILABLE_TONES[i];
if (info.tone.getValue().equals(toneValue))
{
startSendingDtmfTone(info);
return;
}
}
}
/**
* Stop sending DTMF tone.
*/
public synchronized void stopSendingDtmfTone()
{
Collection calls
= (callContainer == null)
? CallManager.getInProgressCalls()
: callContainer.getCallConference().getCalls();
if ((calls != null) && !calls.isEmpty())
{
for (Call call : calls)
stopSendingDtmfTone(call);
}
}
/**
* Stops sending DTMF tone to the given call.
*
* @param call The call to which we send DTMF-s.
*/
private void stopSendingDtmfTone(Call call)
{
Iterator extends CallPeer> callPeers = call.getCallPeers();
try
{
while (callPeers.hasNext())
{
CallPeer peer = callPeers.next();
OperationSetDTMF dtmfOpSet
= peer.getProtocolProvider().getOperationSet(
OperationSetDTMF.class);
if (dtmfOpSet != null)
dtmfOpSet.stopSendingDTMF(peer);
}
}
catch (Throwable t)
{
if (t instanceof InterruptedException)
Thread.currentThread().interrupt();
else if (t instanceof ThreadDeath)
throw (ThreadDeath) t;
else
logger.error("Failed to send a DTMF tone.", t);
}
}
/**
* DTMF extended information.
*/
public static class DTMFToneInfo
{
/**
* The image to display in buttons sending DTMFs.
*/
public final ImageID imageID;
/**
* The image to display in buttons sending DTMFs.
*/
public final ImageID imageIDPressed;
/**
* The image to display in buttons sending DTMFs.
*/
public final ImageID imageIDRollover;
/**
* The char associated with this DTMF tone.
*/
public final char keyChar;
/**
* The key code when entered from keyboard.
*/
public final int keyCode;
/**
* The image to display on Mac buttons.
*/
public final ImageID macImageID;
/**
* The id of the image to display on Mac buttons on rollover.
*/
public final ImageID macImageRolloverID;
/**
* The sound to play during send of this tone.
*/
public final String sound;
/**
* The tone itself
*/
public final DTMFTone tone;
/**
* Creates DTMF extended info.
* @param tone the tone.
* @param keyCode its key code.
* @param keyChar the char associated with the DTMF
* @param imageID the image if any.
* @param macImageID the Mac OS X-specific image if any.
* @param macImageRolloverID the Mac OS X-specific rollover image if any
* @param sound the sound if any.
*/
public DTMFToneInfo(
DTMFTone tone,
int keyCode, char keyChar,
ImageID imageID, ImageID imageIDPressed,ImageID imageIDRollover,
ImageID macImageID, ImageID macImageRolloverID,
String sound)
{
this.tone = tone;
this.keyCode = keyCode;
this.keyChar = keyChar;
this.imageID = imageID;
this.imageIDPressed = imageIDPressed;
this.imageIDRollover = imageIDRollover;
this.macImageID = macImageID;
this.macImageRolloverID = macImageRolloverID;
this.sound = sound;
}
}
}