From 98fef59119c22bcb8e09fad6a4a1d787ea84b665 Mon Sep 17 00:00:00 2001 From: Lyubomir Marinov Date: Thu, 18 Oct 2012 14:06:40 +0000 Subject: [PATCH] Works on preventing sound notifications from playing forever. --- lib/installer-exclude/libjitsi.jar | Bin 996363 -> 996560 bytes .../impl/neomedia/NeomediaActivator.java | 24 +- .../PopupMessageNotificationHandlerImpl.java | 9 +- .../SoundNotificationHandlerImpl.java | 349 +-- .../loggingutils/LoggingConfigForm.java | 45 +- .../NotificationManager.java | 2289 +++++++++-------- .../ReconnectPluginActivator.java | 1 - .../CommandNotificationHandler.java | 6 +- .../notification/NotificationData.java | 115 +- .../notification/NotificationService.java | 23 +- .../notification/NotificationServiceImpl.java | 1432 ++++++----- .../PopupMessageNotificationHandler.java | 11 +- .../util/dns/ConfigurableDnssecResolver.java | 7 +- .../TestPopupMessageHandler.java | 8 - 14 files changed, 2199 insertions(+), 2120 deletions(-) diff --git a/lib/installer-exclude/libjitsi.jar b/lib/installer-exclude/libjitsi.jar index c1091d48e92f7812a7e8df1063ffc26866990880..6feb19ad8fd2e1a9e77f13238405171cba2cdc9d 100644 GIT binary patch delta 60705 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 diff --git a/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java b/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java index edf966b24..bf8b5b6b7 100644 --- a/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java +++ b/src/net/java/sip/communicator/impl/neomedia/NeomediaActivator.java @@ -6,31 +6,23 @@ */ package net.java.sip.communicator.impl.neomedia; -import java.awt.*; -import java.awt.event.*; import java.beans.*; import java.util.*; -import javax.swing.*; - import net.java.sip.communicator.impl.neomedia.codec.video.h264.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.notification.*; import net.java.sip.communicator.service.resources.*; -import net.java.sip.communicator.service.systray.*; import net.java.sip.communicator.service.systray.event.*; import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.swing.*; import org.jitsi.impl.neomedia.*; -import org.jitsi.impl.neomedia.codec.*; import org.jitsi.impl.neomedia.device.*; import org.jitsi.service.audionotifier.*; import org.jitsi.service.configuration.*; import org.jitsi.service.fileaccess.*; import org.jitsi.service.libjitsi.*; import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.codec.*; import org.jitsi.service.packetlogging.*; import org.jitsi.service.resources.*; import org.osgi.framework.*; @@ -146,11 +138,6 @@ public class NeomediaActivator private AudioDeviceConfigurationListener deviceConfigurationPropertyChangeListener; - /** - * Audio configuration dialog. - */ - private static SIPCommDialog audioConfigDialog = null; - /** * A {@link MediaConfigurationService} instance. */ @@ -553,7 +540,7 @@ public void managePopupMessageListenerRegistration(boolean enable) } /** - * Fonction called when an audio device is plugged or unplugged. + * Function called when an audio device is plugged or unplugged. * * @param The property change event which may concern the audio device. */ @@ -564,6 +551,7 @@ public void propertyChange(PropertyChangeEvent event) { NotificationService notificationService = getNotificationService(); + if(notificationService != null) { // Registers only once to the popup message notification @@ -573,9 +561,15 @@ public void propertyChange(PropertyChangeEvent event) isRegisteredToPopupMessageListener = true; managePopupMessageListenerRegistration(true); } + // Fires the popup notification. ResourceManagementService resources = NeomediaActivator.getResources(); + Map extras = new HashMap(); + + extras.put( + NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA, + this); notificationService.fireNotification( DEVICE_CONFIGURATION_HAS_CHANGED, resources.getI18NString( @@ -585,7 +579,7 @@ public void propertyChange(PropertyChangeEvent event) "impl.media.configform" + ".AUDIO_DEVICE_CONFIG_MANAGMENT_CLICK"), null, - this); + extras); } } } diff --git a/src/net/java/sip/communicator/impl/notification/PopupMessageNotificationHandlerImpl.java b/src/net/java/sip/communicator/impl/notification/PopupMessageNotificationHandlerImpl.java index 339503240..c15fb538c 100644 --- a/src/net/java/sip/communicator/impl/notification/PopupMessageNotificationHandlerImpl.java +++ b/src/net/java/sip/communicator/impl/notification/PopupMessageNotificationHandlerImpl.java @@ -57,11 +57,14 @@ public void popupMessage(PopupMessageNotificationAction action, return; if(!StringUtils.isNullOrEmpty(message)) + { systray.showPopupMessage( - new PopupMessage(title, message, icon, tag)); + new PopupMessage(title, message, icon, tag)); + } else - logger.error("Message is null or empty!", - new Throwable("Null or empty message")); + { + logger.error("Message is null or empty!"); + } } /** diff --git a/src/net/java/sip/communicator/impl/notification/SoundNotificationHandlerImpl.java b/src/net/java/sip/communicator/impl/notification/SoundNotificationHandlerImpl.java index fad7e2beb..c3e4ab28e 100644 --- a/src/net/java/sip/communicator/impl/notification/SoundNotificationHandlerImpl.java +++ b/src/net/java/sip/communicator/impl/notification/SoundNotificationHandlerImpl.java @@ -8,6 +8,7 @@ import java.awt.*; import java.util.*; +import java.util.concurrent.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.notification.*; @@ -23,13 +24,15 @@ public class SoundNotificationHandlerImpl implements SoundNotificationHandler { - WeakHashMap playedClips = - new WeakHashMap(); - /** - * If the sound is currently disabled. + * The indicator which determines whether this + * SoundNotificationHandler is currently muted i.e. the sounds are + * off. */ - private boolean isMute; + private boolean mute; + + private Map playedClips + = new WeakHashMap(); /** * {@inheritDoc} @@ -40,55 +43,13 @@ public String getActionType() } /** - * Plays the sound given by the containing soundFileDescriptor. The - * sound is played in loop if the loopInterval is defined. - * @param action The action to act upon. - * @param data Additional data for the event. + * Specifies if currently the sound is off. + * + * @return TRUE if currently the sound is off, FALSE otherwise */ - public void start(SoundNotificationAction action, NotificationData data) + public boolean isMute() { - if(isMute()) - return; - - boolean playOnlyOnPlayback = true; - - AudioNotifierService audioNotifService - = NotificationActivator.getAudioNotifier(); - if(audioNotifService != null) - playOnlyOnPlayback = - audioNotifService.audioOutAndNotificationsShareSameDevice(); - - if(playOnlyOnPlayback) - { - if(action.isSoundNotificationEnabled() - || action.isSoundPlaybackEnabled()) - { - play(action, data, true); - } - } - else - { - if(action.isSoundNotificationEnabled()) - { - play(action, data, false); - } - - if(action.isSoundPlaybackEnabled()) - { - play(action, data, true); - } - } - - if(action.isSoundPCSpeakerEnabled()) - { - PCSpeakerClip audio = new PCSpeakerClip(); - playedClips.put(audio, data); - - if(action.getLoopInterval() > -1) - audio.playInLoop(action.getLoopInterval()); - else - audio.play(); - } + return mute; } /** @@ -96,16 +57,18 @@ public void start(SoundNotificationAction action, NotificationData data) * sound is played in loop if the loopInterval is defined. * @param action The action to act upon. * @param data Additional data for the event. - * @param playback to use or not the playback or notification device. + * @param device */ - private void play(SoundNotificationAction action, NotificationData data, - boolean playback) + private void play( + SoundNotificationAction action, + NotificationData data, + SCAudioClipDevice device) { AudioNotifierService audioNotifService = NotificationActivator.getAudioNotifier(); - if(audioNotifService == null - || StringUtils.isNullOrEmpty(action.getDescriptor(), true)) + if((audioNotifService == null) + || StringUtils.isNullOrEmpty(action.getDescriptor(), true)) return; // this is hack, seen on some os (particularly seen on macosx with @@ -113,51 +76,42 @@ private void play(SoundNotificationAction action, NotificationData data, // when playing notification in the call, can break the call and // no further communicating can be done after the notification. // So we skip playing notification if we have a call running - if(playback) + if(SCAudioClipDevice.PLAYBACK.equals(device)) { UIService uiService = NotificationActivator.getUIService(); + if(uiService.getInProgressCalls().size() > 0) - { return; - } } - SCAudioClip audio = audioNotifService - .createAudio(action.getDescriptor(), playback); + SCAudioClip audio = null; + + switch (device) + { + case NOTIFICATION: + case PLAYBACK: + audio = audioNotifService.createAudio(action.getDescriptor(), SCAudioClipDevice.PLAYBACK.equals(device)); + break; + + case PC_SPEAKER: + audio = new PCSpeakerClip(); + break; + } // it is possible that audio cannot be created if(audio == null) return; playedClips.put(audio, data); - if(action.getLoopInterval() > -1) - audio.playInLoop(action.getLoopInterval()); - else - audio.play(); - } - /** - * Stops the sound. - * @param data Additional data for the event. - */ - public void stop(NotificationData data) - { - AudioNotifierService audioNotifService - = NotificationActivator.getAudioNotifier(); - - if(audioNotifService == null) - return; + @SuppressWarnings("unchecked") + Callable loopCondition + = (Callable) + data.getExtra( + NotificationData + .SOUND_NOTIFICATION_HANDLER_LOOP_CONDITION_EXTRA); - for (Map.Entry entry : playedClips - .entrySet()) - { - if(entry.getValue() == data) - { - SCAudioClip audio = entry.getKey(); - audio.stop(); - audioNotifService.destroyAudio(audio); - } - } + audio.play(action.getLoopInterval(), loopCondition); } /** @@ -167,7 +121,7 @@ public void stop(NotificationData data) */ public void setMute(boolean isMute) { - this.isMute = isMute; + this.mute = isMute; if(isMute) { @@ -189,182 +143,117 @@ public void setMute(boolean isMute) } /** - * Specifies if currently the sound is off. - * - * @return TRUE if currently the sound is off, FALSE otherwise - */ - public boolean isMute() - { - return isMute; - } - - /** - * Plays beep on the pc speaker. + * Plays the sound given by the containing soundFileDescriptor. The + * sound is played in loop if the loopInterval is defined. + * @param action The action to act upon. + * @param data Additional data for the event. */ - private class PCSpeakerClip - implements SCAudioClip + public void start(SoundNotificationAction action, NotificationData data) { - /** - * Synching start/stop. - */ - private final Object syncObject = new Object(); - - /** - * Is beep started. - */ - private boolean started = false; + if(isMute()) + return; - /** - * Is looping. - */ - private boolean isLooping; + boolean playOnlyOnPlayback = true; - /** - * The interval to loop. - */ - private int loopInterval; + AudioNotifierService audioNotifService + = NotificationActivator.getAudioNotifier(); - /** - * Plays this audio. - */ - public void play() + if(audioNotifService != null) { - started = true; - new Thread("Playing beep:" + this.getClass()) - { - @Override - public void run() - { - runInPlayThread(); - } - }.start(); + playOnlyOnPlayback + = audioNotifService.audioOutAndNotificationsShareSameDevice(); } - /** - * Plays this audio in loop. - * - * @param silenceInterval interval between loops - */ - public void playInLoop(int silenceInterval) + if(playOnlyOnPlayback) { - setLoopInterval(silenceInterval); - setIsLooping(true); - - play(); + if(action.isSoundNotificationEnabled() + || action.isSoundPlaybackEnabled()) + { + play(action, data, SCAudioClipDevice.PLAYBACK); + } } - - /** - * Stops this audio. - */ - public void stop() + else { - internalStop(); - setIsLooping(false); + if(action.isSoundNotificationEnabled()) + play(action, data, SCAudioClipDevice.NOTIFICATION); + if(action.isSoundPlaybackEnabled()) + play(action, data, SCAudioClipDevice.PLAYBACK); } - /** - * Stops this audio without setting the isLooping property in the case of - * a looping audio. - */ - public void internalStop() + if(action.isSoundPCSpeakerEnabled()) + play(action, data, SCAudioClipDevice.PC_SPEAKER); + } + + /** + * Stops the sound. + * @param data Additional data for the event. + */ + public void stop(NotificationData data) + { + AudioNotifierService audioNotifService + = NotificationActivator.getAudioNotifier(); + + if(audioNotifService == null) + return; + + for (Map.Entry entry + : playedClips.entrySet()) { - synchronized (syncObject) + if(entry.getValue() == data) { - if (started) - { - started = false; - syncObject.notifyAll(); - } + SCAudioClip audio = entry.getKey(); + + audio.stop(); + audioNotifService.destroyAudio(audio); } } + } + /** + * Beeps the PC speaker. + */ + private static class PCSpeakerClip + extends AbstractSCAudioClip + { /** - * Runs in a separate thread to perform the actual playback. + * Initializes a new PCSpeakerClip instance. */ - private void runInPlayThread() + public PCSpeakerClip() { - while (started) - { - if (!runOnceInPlayThread()) - break; - - if(isLooping()) - { - synchronized(syncObject) - { - if (started) - { - try - { - if(getLoopInterval() > 0) - syncObject.wait(getLoopInterval()); - } - catch (InterruptedException e) - { - } - } - } - } - else - break; - } + super(null, NotificationActivator.getAudioNotifier()); } /** - * Beeps. + * Beeps the PC speaker. * - * @return true if the playback was successful; - * otherwise, false + * @return true if the playback was successful; otherwise, + * false */ - private boolean runOnceInPlayThread() + protected boolean runOnceInPlayThread() { try { Toolkit.getDefaultToolkit().beep(); + return true; } catch (Throwable t) { - //logger.error("Failed to get audio stream " + url, ioex); - return false; + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + return false; } - - return true; - } - - /** - * Returns TRUE if this audio is currently playing in loop, - * FALSE otherwise. - * @return TRUE if this audio is currently playing in loop, - * FALSE otherwise. - */ - public boolean isLooping() - { - return isLooping; - } - - /** - * Returns the loop interval if this audio is looping. - * @return the loop interval if this audio is looping - */ - public int getLoopInterval() - { - return loopInterval; - } - - /** - * @param isLooping the isLooping to set - */ - public void setIsLooping(boolean isLooping) - { - this.isLooping = isLooping; } + } - /** - * @param loopInterval the loopInterval to set - */ - public void setLoopInterval(int loopInterval) - { - this.loopInterval = loopInterval; - } + /** + * Enumerates the types of devices on which SCAudioClips may be + * played back. + */ + private static enum SCAudioClipDevice + { + NOTIFICATION, + PC_SPEAKER, + PLAYBACK } } diff --git a/src/net/java/sip/communicator/plugin/loggingutils/LoggingConfigForm.java b/src/net/java/sip/communicator/plugin/loggingutils/LoggingConfigForm.java index e5db2e6bd..1c1d16201 100644 --- a/src/net/java/sip/communicator/plugin/loggingutils/LoggingConfigForm.java +++ b/src/net/java/sip/communicator/plugin/loggingutils/LoggingConfigForm.java @@ -474,22 +474,19 @@ private void collectLogs() if(notificationService != null) { - String bodyMsgKey = null; - - if(dest != null) - bodyMsgKey = "plugin.loggingutils.ARCHIVE_MESSAGE_OK"; - else - bodyMsgKey = "plugin.loggingutils.ARCHIVE_MESSAGE_NOTOK"; + String bodyMsgKey + = (dest == null) + ? "plugin.loggingutils.ARCHIVE_MESSAGE_NOTOK" + : "plugin.loggingutils.ARCHIVE_MESSAGE_OK"; notificationService.fireNotification( - LOGFILES_ARCHIVED, - resources.getI18NString( - "plugin.loggingutils.ARCHIVE_BUTTON"), - resources.getI18NString( - bodyMsgKey, - new String[]{dest.getAbsolutePath()}), - null, - null); + LOGFILES_ARCHIVED, + resources.getI18NString( + "plugin.loggingutils.ARCHIVE_BUTTON"), + resources.getI18NString( + bodyMsgKey, + new String[]{dest.getAbsolutePath()}), + null); } } @@ -721,20 +718,18 @@ static void uploadLogs( if(notificationService != null) { + ResourceManagementService resources + = LoggingUtilsActivator.getResourceService(); String bodyMsgKey = "plugin.loggingutils.ARCHIVE_MESSAGE_OK"; - ResourceManagementService resources = - LoggingUtilsActivator.getResourceService(); - notificationService.fireNotification( - LOGFILES_ARCHIVED, - resources.getI18NString( - "plugin.loggingutils.ARCHIVE_BUTTON"), - resources.getI18NString( - bodyMsgKey, - new String[]{uploadLocation}), - null, - null); + LOGFILES_ARCHIVED, + resources.getI18NString( + "plugin.loggingutils.ARCHIVE_BUTTON"), + resources.getI18NString( + bodyMsgKey, + new String[]{uploadLocation}), + null); } } } diff --git a/src/net/java/sip/communicator/plugin/notificationwiring/NotificationManager.java b/src/net/java/sip/communicator/plugin/notificationwiring/NotificationManager.java index 4aa9cb158..d2022e399 100644 --- a/src/net/java/sip/communicator/plugin/notificationwiring/NotificationManager.java +++ b/src/net/java/sip/communicator/plugin/notificationwiring/NotificationManager.java @@ -9,6 +9,7 @@ import java.awt.image.*; import java.net.*; import java.util.*; +import java.util.concurrent.*; import javax.imageio.*; @@ -22,72 +23,42 @@ import org.jitsi.service.neomedia.*; import org.jitsi.service.protocol.event.*; +import org.jitsi.service.resources.*; import org.osgi.framework.*; /** - * Listens for all kinds of events and triggers when needed a notification, - * a popup or sound one or other. + * Listens to various events which are related to the display and/or playback of + * notifications and shows/starts or hides/stops the notifications in question. + * * @author Damian Minkov + * @author Lyubomir Marinov */ public class NotificationManager - implements MessageListener, - ServiceListener, - FileTransferListener, - TypingNotificationsListener, - CallListener, + implements AdHocChatRoomMessageListener, CallChangeListener, + CallListener, + CallPeerConferenceListener, CallPeerListener, CallPeerSecurityListener, ChatRoomMessageListener, - LocalUserChatRoomPresenceListener, + FileTransferListener, LocalUserAdHocChatRoomPresenceListener, - AdHocChatRoomMessageListener, - CallPeerConferenceListener, - Recorder.Listener + LocalUserChatRoomPresenceListener, + MessageListener, + Recorder.Listener, + ServiceListener, + TypingNotificationsListener { /** - * Our logger. - */ - private static final Logger logger = - Logger.getLogger(NotificationManager.class); - - /** - * The image used, when a contact has no photo specified. - */ - public static final ImageID DEFAULT_USER_PHOTO - = new ImageID("service.gui.DEFAULT_USER_PHOTO"); - - /** - * Stores all already loaded images. - */ - private static final Map loadedImages = - new Hashtable(); - - /** - * Pseudo timer used to delay multiple typings notifications before - * receiving the message. - * - * Time to live : 1 minute - */ - private Map proactiveTimer = new HashMap(); - - /** - * Stores notification references to stop them if a notification has expired - * (e.g. to stop the dialing sound). + * Default event type for a busy call. */ - private Map callNotifications = - new WeakHashMap(); + public static final String BUSY_CALL = "BusyCall"; /** * Default event type for call been saved using a recorder. */ public static final String CALL_SAVED = "CallSaved"; - /** - * Default event type for incoming file transfers. - */ - public static final String INCOMING_FILE = "IncomingFile"; - /** * Default event type for security error on a call. */ @@ -99,15 +70,15 @@ public class NotificationManager public static final String CALL_SECURITY_ON = "CallSecurityOn"; /** - * Default event type when a secure message received. + * The image used, when a contact has no photo specified. */ - public static final String SECURITY_MESSAGE = "SecurityMessage"; + public static final ImageID DEFAULT_USER_PHOTO + = new ImageID("service.gui.DEFAULT_USER_PHOTO"); /** - * Default event type for - * proactive notifications (typing notifications when chatting). + * Default event type for dialing. */ - public static final String PROACTIVE_NOTIFICATION = "ProactiveNotification"; + public static final String DIALING = "Dialing"; /** * Default event type for hanging up calls. @@ -115,24 +86,22 @@ public class NotificationManager public static final String HANG_UP = "HangUp"; /** - * Default event type for dialing. - */ - public static final String DIALING = "Dialing"; - - /** - * Default event type for a busy call. + * The cache of BufferedImage instances which we have already + * loaded by ImageID and which we store so that we do not have to + * load them again. */ - public static final String BUSY_CALL = "BusyCall"; + private static final Map images + = new Hashtable(); /** - * Default event type for outgoing calls. + * Default event type for receiving calls (incoming calls). */ - public static final String OUTGOING_CALL = "OutgoingCall"; + public static final String INCOMING_CALL = "IncomingCall"; /** - * Default event type for receiving calls (incoming calls). + * Default event type for incoming file transfers. */ - public static final String INCOMING_CALL = "IncomingCall"; + public static final String INCOMING_FILE = "IncomingFile"; /** * Default event type for receiving messages. @@ -140,29 +109,42 @@ public class NotificationManager public static final String INCOMING_MESSAGE = "IncomingMessage"; /** - * Initialize, register default notifications and start listening for - * new protocols or removed one and find any that are already registered. + * The Logger used by the NotificationManager class and + * its instances for logging output. */ - void init() - { - registerDefaultNotifications(); + private static final Logger logger + = Logger.getLogger(NotificationManager.class); - // listens for new protocols - NotificationWiringActivator.bundleContext.addServiceListener(this); + /** + * Default event type for outgoing calls. + */ + public static final String OUTGOING_CALL = "OutgoingCall"; - // enumerate currently registered protocols - for(ProtocolProviderService pp : getProtocolProviders()) - { - handleProviderAdded(pp); - } + /** + * Default event type for + * proactive notifications (typing notifications when chatting). + */ + public static final String PROACTIVE_NOTIFICATION = "ProactiveNotification"; - NotificationWiringActivator.getMediaService().addRecorderListener(this); - } + /** + * Default event type when a secure message received. + */ + public static final String SECURITY_MESSAGE = "SecurityMessage"; /** - * Register all default notifications. + * Fires a chat message notification for the given event type through the + * NotificationService. + * + * @param chatContact the chat contact to which the chat message corresponds; + * the chat contact could be a Contact or a ChatRoom. + * @param eventType the event type for which we fire a notification + * @param messageTitle the title of the message + * @param message the content of the message */ - private void registerDefaultNotifications() + public static void fireChatNotification(Object chatContact, + String eventType, + String messageTitle, + String message) { NotificationService notificationService = NotificationWiringActivator.getNotificationService(); @@ -170,133 +152,249 @@ private void registerDefaultNotifications() if(notificationService == null) return; - // Register incoming message notifications. - notificationService.registerDefaultNotificationForEvent( - INCOMING_MESSAGE, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); - - notificationService.registerDefaultNotificationForEvent( - INCOMING_MESSAGE, - new SoundNotificationAction( - SoundProperties.INCOMING_MESSAGE, -1, true, false, false)); - - // Register incoming call notifications. - notificationService.registerDefaultNotificationForEvent( - INCOMING_CALL, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); - - SoundNotificationAction inCallSoundHandler - = new SoundNotificationAction( - SoundProperties.INCOMING_CALL, 2000, true, true, true); - - notificationService.registerDefaultNotificationForEvent( - INCOMING_CALL, - inCallSoundHandler); - - // Register outgoing call notifications. - SoundNotificationAction outCallSoundHandler - = new SoundNotificationAction( - SoundProperties.OUTGOING_CALL, 3000, false, true, false); - - notificationService.registerDefaultNotificationForEvent( - OUTGOING_CALL, - outCallSoundHandler); - - // Register busy call notifications. - SoundNotificationAction busyCallSoundHandler - = new SoundNotificationAction(SoundProperties.BUSY, 1, - false, true, false); + NotificationAction popupActionHandler = null; + UIService uiService = NotificationWiringActivator.getUIService(); - notificationService.registerDefaultNotificationForEvent( - BUSY_CALL, - busyCallSoundHandler); + Chat chatPanel = null; + byte[] contactIcon = null; + if (chatContact instanceof Contact) + { + Contact contact = (Contact) chatContact; - // Register dial notifications. - SoundNotificationAction dialSoundHandler - = new SoundNotificationAction( - SoundProperties.DIALING, -1, false, true, false); + if(uiService != null) + chatPanel = uiService.getChat(contact); - notificationService.registerDefaultNotificationForEvent( - DIALING, - dialSoundHandler); + contactIcon = contact.getImage(); + if(contactIcon == null) + { + contactIcon = + ImageUtils.toByteArray(getImage(DEFAULT_USER_PHOTO)); + } + } + else if (chatContact instanceof ChatRoom) + { + ChatRoom chatRoom = (ChatRoom) chatContact; - // Register the hangup sound notification. - SoundNotificationAction hangupSoundHandler - = new SoundNotificationAction( - SoundProperties.HANG_UP, -1, false, true, false); + // For system rooms we don't want to send notification events. + if (chatRoom.isSystem()) + return; - notificationService.registerDefaultNotificationForEvent( - HANG_UP, - hangupSoundHandler); + if(uiService != null) + chatPanel = uiService.getChat(chatRoom); + } - // Register proactive notifications. - notificationService.registerDefaultNotificationForEvent( - PROACTIVE_NOTIFICATION, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); + if (chatPanel != null) + { + if (eventType.equals(INCOMING_MESSAGE) + && chatPanel.isChatFocused()) + { + popupActionHandler = notificationService + .getEventNotificationAction(eventType, + NotificationAction.ACTION_POPUP_MESSAGE); - // Register warning message notifications. - notificationService.registerDefaultNotificationForEvent( - SECURITY_MESSAGE, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); + popupActionHandler.setEnabled(false); + } + } - // Register sound notification for security state on during a call. - notificationService.registerDefaultNotificationForEvent( - CALL_SECURITY_ON, - new SoundNotificationAction( - SoundProperties.CALL_SECURITY_ON, -1, - false, true, false)); + Map extras = new HashMap(); - // Register sound notification for security state off during a call. - notificationService.registerDefaultNotificationForEvent( - CALL_SECURITY_ERROR, - new SoundNotificationAction( - SoundProperties.CALL_SECURITY_ERROR, -1, - false, true, false)); + extras.put( + NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA, + chatContact); + notificationService.fireNotification( + eventType, + messageTitle, + message, + contactIcon, + extras); - // Register sound notification for incoming files. - notificationService.registerDefaultNotificationForEvent( - INCOMING_FILE, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); + if(popupActionHandler != null) + popupActionHandler.setEnabled(true); + } - notificationService.registerDefaultNotificationForEvent( - INCOMING_FILE, - new SoundNotificationAction( - SoundProperties.INCOMING_FILE, -1, - true, false, false)); + /** + * Fires a notification for the given event type through the + * NotificationService. The event type is one of the static + * constants defined in the NotificationManager class. + *

+ * Note: The uses of the method at the time of this writing do not + * take measures to stop looping sounds if the respective notifications use + * them i.e. there is implicit agreement that the notifications fired + * through the method do not loop sounds. Consequently, the method passes + * arguments to NotificationService so that sounds are played once + * only. + *

+ * + * @param eventType the event type for which we want to fire a notification + */ + private static void fireNotification(String eventType) + { + NotificationService notificationService + = NotificationWiringActivator.getNotificationService(); - // Register notification for saved calls. - notificationService.registerDefaultNotificationForEvent( - CALL_SAVED, - NotificationAction.ACTION_POPUP_MESSAGE, - null, - null); + if (notificationService != null) + notificationService.fireNotification(eventType); } /** - * Returns all ProtocolProviderFactorys obtained from the bundle - * context. + * Fires a notification for the given event type through the + * NotificationService. The event type is one of the static + * constants defined in the NotificationManager class. * - * @return all ProtocolProviderFactorys obtained from the bundle - * context + * @param eventType the event type for which we want to fire a notification + * @param loopCondition the method which will determine whether any sounds + * played as part of the specified notification will continue looping + * @return a reference to the fired notification to stop it. */ - public static Map - getProtocolProviderFactories() + private static NotificationData fireNotification( + String eventType, + Callable loopCondition) { - ServiceReference[] serRefs = null; - try - { - // get all registered provider factories - serRefs + return fireNotification(eventType, null, null, null, loopCondition); + } + + /** + * Fires a notification through the NotificationService with a + * specific event type, a specific message title and a specific message. + *

+ * Note: The uses of the method at the time of this writing do not + * take measures to stop looping sounds if the respective notifications use + * them i.e. there is implicit agreement that the notifications fired + * through the method do not loop sounds. Consequently, the method passes + * arguments to NotificationService so that sounds are played once + * only. + *

+ * + * @param eventType the event type of the notification to be fired + * @param messageTitle the title of the message to be displayed by the + * notification to be fired if such a display is supported + * @param message the message to be displayed by the notification to be + * fired if such a display is supported + */ + private static void fireNotification( + String eventType, + String messageTitle, + String message) + { + NotificationService notificationService + = NotificationWiringActivator.getNotificationService(); + + if (notificationService != null) + { + notificationService.fireNotification( + eventType, + messageTitle, + message, + null); + } + } + + /** + * Fires a message notification for the given event type through the + * NotificationService. + * + * @param eventType the event type for which we fire a notification + * @param messageTitle the title of the message + * @param message the content of the message + * @param cmdargs the value to be provided to + * {@link CommandNotificationHandler#execute(CommandNotificationAction, + * Map)} as the cmdargs argument + * @param loopCondition the method which will determine whether any sounds + * played as part of the specified notification will continue looping + * @return a reference to the fired notification to stop it. + */ + private static NotificationData fireNotification( + String eventType, + String messageTitle, + String message, + Map cmdargs, + Callable loopCondition) + { + NotificationService notificationService + = NotificationWiringActivator.getNotificationService(); + + if (notificationService == null) + return null; + else + { + Map extras = new HashMap(); + + if (cmdargs != null) + { + extras.put( + NotificationData + .COMMAND_NOTIFICATION_HANDLER_CMDARGS_EXTRA, + cmdargs); + } + if (loopCondition != null) + { + extras.put( + NotificationData + .SOUND_NOTIFICATION_HANDLER_LOOP_CONDITION_EXTRA, + loopCondition); + } + return + notificationService.fireNotification( + eventType, + messageTitle, + message, + null, + extras); + } + } + + /** + * Loads an image from a given image identifier. + * + * @param imageID The identifier of the image. + * @return The image for the given identifier. + */ + public static BufferedImage getImage(ImageID imageID) + { + /* + * If we were mapping ImageID to null, we would be using the method + * Map.containsKey. However, that does not seem to be the case. + */ + BufferedImage image = images.get(imageID); + + if (image == null) + { + URL path + = NotificationWiringActivator.getResources().getImageURL( + imageID.getId()); + + if (path != null) + { + try + { + image = ImageIO.read(path); + images.put(imageID, image); + } + catch (Exception ex) + { + logger.error("Failed to load image: " + path, ex); + } + } + } + + return image; + } + + /** + * Returns all ProtocolProviderFactorys obtained from the bundle + * context. + * + * @return all ProtocolProviderFactorys obtained from the bundle + * context + */ + public static Map + getProtocolProviderFactories() + { + ServiceReference[] serRefs = null; + try + { + // get all registered provider factories + serRefs = NotificationWiringActivator.bundleContext.getServiceReferences( ProtocolProviderFactory.class.getName(), null); @@ -364,111 +462,256 @@ private void registerDefaultNotifications() } /** - * Adds all listeners related to the given protocol provider. + * Determines whether a specific ChatRoom is private i.e. + * represents a one-to-one conversation which is not a channel. Since the + * interface {@link ChatRoom} does not expose the private property, an + * heuristic is used as a workaround: (1) a system ChatRoom is + * obviously not private and (2) a ChatRoom is private if it + * has only one ChatRoomMember who is not the local user. * - * @param protocolProvider the ProtocolProviderService + * @param chatRoom + * the ChatRoom to be determined as private or not + * @return true if the specified ChatRoom is private; + * otherwise, false */ - private void handleProviderAdded(ProtocolProviderService protocolProvider) + private static boolean isPrivate(ChatRoom chatRoom) { - if(!protocolProvider.getAccountID().isEnabled()) - return; + if (!chatRoom.isSystem() + && chatRoom.isJoined() + && (chatRoom.getMembersCount() == 1)) + { + String nickname = chatRoom.getUserNickname(); - Map supportedOperationSets - = protocolProvider.getSupportedOperationSets(); + if (nickname != null) + { + for (ChatRoomMember member : chatRoom.getMembers()) + if (nickname.equals(member.getName())) + return false; + return true; + } + } + return false; + } - // Obtain the basic instant messaging operation set. - String imOpSetClassName = OperationSetBasicInstantMessaging - .class.getName(); + /** + * Stops all sounds for the given event type. + * + * @param data the event type for which we should stop sounds. One of + * the static event types defined in this class. + */ + public static void stopSound(NotificationData data) + { + NotificationService notificationService + = NotificationWiringActivator.getNotificationService(); - if (supportedOperationSets.containsKey(imOpSetClassName)) - { - OperationSetBasicInstantMessaging im - = (OperationSetBasicInstantMessaging) - supportedOperationSets.get(imOpSetClassName); + if(notificationService != null) + notificationService.stopNotification(data); + } - //Add to all instant messaging operation sets the Message - //listener which handles all received messages. - im.addMessageListener(this); - } + /** + * Stores notification references to stop them if a notification has expired + * (e.g. to stop the dialing sound). + */ + private final Map callNotifications + = new WeakHashMap(); - // Obtain the typing notifications operation set. - String tnOpSetClassName = OperationSetTypingNotifications - .class.getName(); + /** + * The pseudo timer which is used to delay multiple typing notifications + * before receiving the message. + */ + private final Map proactiveTimer + = new HashMap(); - if (supportedOperationSets.containsKey(tnOpSetClassName)) + /** + * Implements CallListener.callEnded. Stops sounds that are playing at + * the moment if there're any. + * @param event the CallEvent + */ + public void callEnded(CallEvent event) + { + try { - OperationSetTypingNotifications tn - = (OperationSetTypingNotifications) - supportedOperationSets.get(tnOpSetClassName); + // Stop all telephony related sounds. +// stopAllTelephonySounds(); + stopSound(callNotifications.get(event.getSourceCall())); - //Add to all typing notification operation sets the Message - //listener implemented in the ContactListPanel, which handles - //all received messages. - tn.addTypingNotificationsListener(this); + // Play the hangup sound. + fireNotification(HANG_UP); } - - // Obtain file transfer operation set. - OperationSetFileTransfer fileTransferOpSet - = protocolProvider.getOperationSet(OperationSetFileTransfer.class); - - if (fileTransferOpSet != null) + catch(Throwable t) { - fileTransferOpSet.addFileTransferListener(this); + logger.error("Error notifying for call ended", t); } + } - OperationSetMultiUserChat multiChatOpSet - = protocolProvider.getOperationSet(OperationSetMultiUserChat.class); + /** + * Implements the CallChangeListener.callPeerAdded method. + * @param evt the CallPeerEvent that notifies us for the change + */ + public void callPeerAdded(CallPeerEvent evt) + { + CallPeer peer = evt.getSourceCallPeer(); - if (multiChatOpSet != null) - { - multiChatOpSet.addPresenceListener(this); - } + if(peer == null) + return; - OperationSetAdHocMultiUserChat multiAdHocChatOpSet - = protocolProvider.getOperationSet(OperationSetAdHocMultiUserChat.class); + peer.addCallPeerListener(this); + peer.addCallPeerSecurityListener(this); + peer.addCallPeerConferenceListener(this); + } - if (multiAdHocChatOpSet != null) - { - multiAdHocChatOpSet.addPresenceListener(this); - } + /** + * Implements the CallChangeListener.callPeerRemoved method. + * @param evt the CallPeerEvent that has been triggered + */ + public void callPeerRemoved(CallPeerEvent evt) + { + CallPeer peer = evt.getSourceCallPeer(); - OperationSetBasicTelephony basicTelephonyOpSet - = protocolProvider.getOperationSet(OperationSetBasicTelephony.class); + if(peer == null) + return; - if (basicTelephonyOpSet != null) - { - basicTelephonyOpSet.addCallListener(this); - } + peer.removeCallPeerListener(this); + peer.removeCallPeerSecurityListener(this); + peer.addCallPeerConferenceListener(this); } /** - * Removes all listeners related to the given protocol provider. + * {@inheritDoc} * - * @param protocolProvider the ProtocolProviderService + * Not used. */ - private void handleProviderRemoved(ProtocolProviderService protocolProvider) - { - Map supportedOperationSets - = protocolProvider.getSupportedOperationSets(); + public void callStateChanged(CallChangeEvent ev) {} - // Obtain the basic instant messaging operation set. - String imOpSetClassName = OperationSetBasicInstantMessaging - .class.getName(); + /** + * {@inheritDoc} + * + * Not used. + */ + public void conferenceFocusChanged(CallPeerConferenceEvent ev) {} - if (supportedOperationSets.containsKey(imOpSetClassName)) + /** + * Indicates that the given conference member has been added to the given + * peer. + * + * @param conferenceEvent the event + */ + public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent) + { + try { - OperationSetBasicInstantMessaging im - = (OperationSetBasicInstantMessaging) - supportedOperationSets.get(imOpSetClassName); + CallPeer peer + = conferenceEvent + .getConferenceMember() + .getConferenceFocusCallPeer(); - //Add to all instant messaging operation sets the Message - //listener which handles all received messages. - im.removeMessageListener(this); - } + if(peer.getConferenceMemberCount() > 0) + { + CallPeerSecurityStatusEvent securityEvent + = peer.getCurrentSecuritySettings(); - // Obtain the typing notifications operation set. - String tnOpSetClassName = OperationSetTypingNotifications - .class.getName(); + if (securityEvent instanceof CallPeerSecurityOnEvent) + fireNotification(CALL_SECURITY_ON); + } + } + catch(Throwable t) + { + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + logger.error("Error notifying for secured call member", t); + } + } + + /** + * {@inheritDoc} + * + * Not used. + */ + public void conferenceMemberRemoved(CallPeerConferenceEvent ev) {} + + /** + * {@inheritDoc} + * + * Not used. + */ + public void fileTransferCreated(FileTransferCreatedEvent ev) {} + + /** + * {@inheritDoc} + * + * Not used. + */ + public void fileTransferRequestCanceled(FileTransferRequestEvent ev) {} + + /** + * When a request has been received we show a notification. + * + * @param event FileTransferRequestEvent + * @see FileTransferListener#fileTransferRequestReceived(FileTransferRequestEvent) + */ + public void fileTransferRequestReceived(FileTransferRequestEvent event) + { + try + { + IncomingFileTransferRequest request = event.getRequest(); + Contact sourceContact = request.getSender(); + + //Fire notification + String title = NotificationWiringActivator.getResources().getI18NString( + "service.gui.FILE_RECEIVING_FROM", + new String[]{sourceContact.getDisplayName()}); + + fireChatNotification( + sourceContact, + INCOMING_FILE, + title, + request.getFileName()); + } + catch(Throwable t) + { + logger.error("Error notifying for file transfer req received", t); + } + } + + /** + * {@inheritDoc} + * + * Not used. + */ + public void fileTransferRequestRejected(FileTransferRequestEvent ev) {} + + /** + * Adds all listeners related to the given protocol provider. + * + * @param protocolProvider the ProtocolProviderService + */ + private void handleProviderAdded(ProtocolProviderService protocolProvider) + { + if(!protocolProvider.getAccountID().isEnabled()) + return; + + Map supportedOperationSets + = protocolProvider.getSupportedOperationSets(); + + // Obtain the basic instant messaging operation set. + String imOpSetClassName = OperationSetBasicInstantMessaging + .class.getName(); + + if (supportedOperationSets.containsKey(imOpSetClassName)) + { + OperationSetBasicInstantMessaging im + = (OperationSetBasicInstantMessaging) + supportedOperationSets.get(imOpSetClassName); + + //Add to all instant messaging operation sets the Message + //listener which handles all received messages. + im.addMessageListener(this); + } + + // Obtain the typing notifications operation set. + String tnOpSetClassName = OperationSetTypingNotifications + .class.getName(); if (supportedOperationSets.containsKey(tnOpSetClassName)) { @@ -479,7 +722,7 @@ private void handleProviderRemoved(ProtocolProviderService protocolProvider) //Add to all typing notification operation sets the Message //listener implemented in the ContactListPanel, which handles //all received messages. - tn.removeTypingNotificationsListener(this); + tn.addTypingNotificationsListener(this); } // Obtain file transfer operation set. @@ -488,7 +731,7 @@ private void handleProviderRemoved(ProtocolProviderService protocolProvider) if (fileTransferOpSet != null) { - fileTransferOpSet.removeFileTransferListener(this); + fileTransferOpSet.addFileTransferListener(this); } OperationSetMultiUserChat multiChatOpSet @@ -496,7 +739,7 @@ private void handleProviderRemoved(ProtocolProviderService protocolProvider) if (multiChatOpSet != null) { - multiChatOpSet.removePresenceListener(this); + multiChatOpSet.addPresenceListener(this); } OperationSetAdHocMultiUserChat multiAdHocChatOpSet @@ -504,7 +747,7 @@ private void handleProviderRemoved(ProtocolProviderService protocolProvider) if (multiAdHocChatOpSet != null) { - multiAdHocChatOpSet.removePresenceListener(this); + multiAdHocChatOpSet.addPresenceListener(this); } OperationSetBasicTelephony basicTelephonyOpSet @@ -512,272 +755,207 @@ private void handleProviderRemoved(ProtocolProviderService protocolProvider) if (basicTelephonyOpSet != null) { - basicTelephonyOpSet.removeCallListener(this); + basicTelephonyOpSet.addCallListener(this); } } /** - * Implements the ServiceListener method. Verifies whether the - * passed event concerns a ProtocolProviderService and adds the - * corresponding listeners. + * Removes all listeners related to the given protocol provider. * - * @param event The ServiceEvent object. + * @param protocolProvider the ProtocolProviderService */ - public void serviceChanged(ServiceEvent event) + private void handleProviderRemoved(ProtocolProviderService protocolProvider) { - ServiceReference serviceRef = event.getServiceReference(); + Map supportedOperationSets + = protocolProvider.getSupportedOperationSets(); - // if the event is caused by a bundle being stopped, we don't want to - // know - if (serviceRef.getBundle().getState() == Bundle.STOPPING) + // Obtain the basic instant messaging operation set. + String imOpSetClassName = OperationSetBasicInstantMessaging + .class.getName(); + + if (supportedOperationSets.containsKey(imOpSetClassName)) { - return; + OperationSetBasicInstantMessaging im + = (OperationSetBasicInstantMessaging) + supportedOperationSets.get(imOpSetClassName); + + //Add to all instant messaging operation sets the Message + //listener which handles all received messages. + im.removeMessageListener(this); + } + + // Obtain the typing notifications operation set. + String tnOpSetClassName = OperationSetTypingNotifications + .class.getName(); + + if (supportedOperationSets.containsKey(tnOpSetClassName)) + { + OperationSetTypingNotifications tn + = (OperationSetTypingNotifications) + supportedOperationSets.get(tnOpSetClassName); + + //Add to all typing notification operation sets the Message + //listener implemented in the ContactListPanel, which handles + //all received messages. + tn.removeTypingNotificationsListener(this); } - Object service = - NotificationWiringActivator.bundleContext.getService(serviceRef); + // Obtain file transfer operation set. + OperationSetFileTransfer fileTransferOpSet + = protocolProvider.getOperationSet(OperationSetFileTransfer.class); - // we don't care if the source service is not a protocol provider - if (!(service instanceof ProtocolProviderService)) + if (fileTransferOpSet != null) { - return; + fileTransferOpSet.removeFileTransferListener(this); } - switch (event.getType()) + OperationSetMultiUserChat multiChatOpSet + = protocolProvider.getOperationSet(OperationSetMultiUserChat.class); + + if (multiChatOpSet != null) { - case ServiceEvent.REGISTERED: - this.handleProviderAdded((ProtocolProviderService) service); - break; - case ServiceEvent.UNREGISTERING: - this.handleProviderRemoved((ProtocolProviderService) service); - break; + multiChatOpSet.removePresenceListener(this); + } + + OperationSetAdHocMultiUserChat multiAdHocChatOpSet + = protocolProvider.getOperationSet(OperationSetAdHocMultiUserChat.class); + + if (multiAdHocChatOpSet != null) + { + multiAdHocChatOpSet.removePresenceListener(this); + } + + OperationSetBasicTelephony basicTelephonyOpSet + = protocolProvider.getOperationSet(OperationSetBasicTelephony.class); + + if (basicTelephonyOpSet != null) + { + basicTelephonyOpSet.removeCallListener(this); } } /** - * Fires a message notification for the given event type through the - * NotificationService. + * Implements CallListener.incomingCallReceived. When a call is received + * plays the ring phone sound to the user and gathers caller information + * that may be used by a user-specified command (incomingCall event + * trigger). * - * @param eventType the event type for which we fire a notification - * @param messageTitle the title of the message - * @param message the content of the message - * @return A reference to the fired notification to stop it. + * @param ev the CallEvent */ - public static NotificationData fireNotification(String eventType, - String messageTitle, - String message) + public void incomingCallReceived(CallEvent ev) { - NotificationService notificationService - = NotificationWiringActivator.getNotificationService(); + try + { + final Call call = ev.getSourceCall(); + CallPeer peer = call.getCallPeers().next(); + Map peerInfo = new HashMap(); + String peerName = peer.getDisplayName(); + + peerInfo.put("caller.uri", peer.getURI()); + peerInfo.put("caller.address", peer.getAddress()); + peerInfo.put("caller.name", peerName); + peerInfo.put("caller.id", peer.getPeerID()); + + NotificationData notification + = fireNotification( + INCOMING_CALL, + "", + NotificationWiringActivator.getResources() + .getI18NString( + "service.gui.INCOMING_CALL", + new String[] { peerName }), + peerInfo, + new Callable() + { + public Boolean call() + { + /* + * INCOMING_CALL should be played for a Call + * only while there is a CallPeer in the + * INCOMING_CALL state. + */ + Iterator peerIter + = call.getCallPeers(); + boolean loop = false; + + while (peerIter.hasNext()) + { + CallPeer peer = peerIter.next(); + + if (CallPeerState.INCOMING_CALL.equals( + peer.getState())) + { + loop = true; + break; + } + } + return loop; + } + }); + + if (notification != null) + callNotifications.put(call, notification); - if(notificationService == null) - return null; + call.addCallChangeListener(this); - return notificationService.fireNotification( eventType, - messageTitle, - message, - null, - null); + peer.addCallPeerListener(this); + peer.addCallPeerSecurityListener(this); + peer.addCallPeerConferenceListener(this); + } + catch(Throwable t) + { + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + { + logger.error( + "An error occurred while trying to notify" + + " about an incoming call", + t); + } + } } /** - * Fires a message notification for the given event type through the - * NotificationService. - * - * @param eventType the event type for which we fire a notification - * @param messageTitle the title of the message - * @param message the content of the message - * @param extra additional event data for external processing - * @return A reference to the fired notification to stop it. + * Initialize, register default notifications and start listening for + * new protocols or removed one and find any that are already registered. */ - public static NotificationData fireNotification(String eventType, - String messageTitle, - String message, - Map extra) + void init() { - NotificationService notificationService - = NotificationWiringActivator.getNotificationService(); + registerDefaultNotifications(); - if(notificationService == null) - return null; + // listens for new protocols + NotificationWiringActivator.bundleContext.addServiceListener(this); + + // enumerate currently registered protocols + for(ProtocolProviderService pp : getProtocolProviders()) + { + handleProviderAdded(pp); + } - return notificationService.fireNotification(eventType, - messageTitle, - message, - extra, - null, - null); + NotificationWiringActivator.getMediaService().addRecorderListener(this); } /** - * Fires a chat message notification for the given event type through the - * NotificationService. + * Checks if the contained call is a conference call. * - * @param chatContact the chat contact to which the chat message corresponds; - * the chat contact could be a Contact or a ChatRoom. - * @param eventType the event type for which we fire a notification - * @param messageTitle the title of the message - * @param message the content of the message + * @param call the call to check + * @return true if the contained Call is a conference + * call, otherwise - returns false. */ - public static void fireChatNotification(Object chatContact, - String eventType, - String messageTitle, - String message) + public boolean isConference(Call call) { - NotificationService notificationService - = NotificationWiringActivator.getNotificationService(); + // If we're the focus of the conference. + if (call.isConferenceFocus()) + return true; - if(notificationService == null) - return; + // If one of our peers is a conference focus, we're in a + // conference call. + Iterator callPeers = call.getCallPeers(); - NotificationAction popupActionHandler = null; - UIService uiService = NotificationWiringActivator.getUIService(); - - Chat chatPanel = null; - byte[] contactIcon = null; - if (chatContact instanceof Contact) - { - Contact contact = (Contact) chatContact; - - if(uiService != null) - chatPanel = uiService.getChat(contact); - - contactIcon = contact.getImage(); - if(contactIcon == null) - { - contactIcon = - ImageUtils.toByteArray(getImage(DEFAULT_USER_PHOTO)); - } - } - else if (chatContact instanceof ChatRoom) - { - ChatRoom chatRoom = (ChatRoom) chatContact; - - // For system rooms we don't want to send notification events. - if (chatRoom.isSystem()) - return; - - if(uiService != null) - chatPanel = uiService.getChat(chatRoom); - } - - if (chatPanel != null) - { - if (eventType.equals(INCOMING_MESSAGE) - && chatPanel.isChatFocused()) - { - popupActionHandler = notificationService - .getEventNotificationAction(eventType, - NotificationAction.ACTION_POPUP_MESSAGE); - - popupActionHandler.setEnabled(false); - } - } - - notificationService.fireNotification( eventType, - messageTitle, - message, - null, - contactIcon, - chatContact); - - if(popupActionHandler != null) - popupActionHandler.setEnabled(true); - } - - /** - * Fires a notification for the given event type through the - * NotificationService. The event type is one of the static - * constants defined in this class. - * - * @param eventType the event type for which we want to fire a notification - * @return A reference to the fired notification to stop it. - */ - public static NotificationData fireNotification(String eventType) - { - NotificationService notificationService - = NotificationWiringActivator.getNotificationService(); - - if(notificationService == null) - return null; - - return notificationService.fireNotification(eventType); - } - - /** - * Stops all sounds for the given event type. - * - * @param data the event type for which we should stop sounds. One of - * the static event types defined in this class. - */ - public static void stopSound(NotificationData data) - { - NotificationService notificationService - = NotificationWiringActivator.getNotificationService(); - - if(notificationService == null) - return; - - notificationService.stopNotification(data); - } - - /** - * Loads an image from a given image identifier. - * - * @param imageID The identifier of the image. - * @return The image for the given identifier. - */ - public static BufferedImage getImage(ImageID imageID) - { - BufferedImage image = null; - - if (loadedImages.containsKey(imageID)) - { - image = loadedImages.get(imageID); - } - else - { - URL path = NotificationWiringActivator.getResources() - .getImageURL(imageID.getId()); - - if (path != null) - { - try - { - image = ImageIO.read(path); - - loadedImages.put(imageID, image); - } - catch (Exception ex) - { - logger.error("Failed to load image: " + path, ex); - } - } - } - - return image; - } - - /** - * Checks if the contained call is a conference call. - * - * @param call the call to check - * @return true if the contained Call is a conference - * call, otherwise - returns false. - */ - public boolean isConference(Call call) - { - // If we're the focus of the conference. - if (call.isConferenceFocus()) - return true; - - // If one of our peers is a conference focus, we're in a - // conference call. - Iterator callPeers = call.getCallPeers(); - - while (callPeers.hasNext()) - { - CallPeer callPeer = callPeers.next(); + while (callPeers.hasNext()) + { + CallPeer callPeer = callPeers.next(); if (callPeer.isConferenceFocus()) return true; @@ -793,268 +971,238 @@ public boolean isConference(Call call) } /** - * Determines whether a specific ChatRoom is private i.e. - * represents a one-to-one conversation which is not a channel. Since the - * interface {@link ChatRoom} does not expose the private property, an - * heuristic is used as a workaround: (1) a system ChatRoom is - * obviously not private and (2) a ChatRoom is private if it - * has only one ChatRoomMember who is not the local user. + * Implements the + * LocalUserAdHocChatRoomPresenceListener.localUserPresenceChanged + * method * - * @param chatRoom - * the ChatRoom to be determined as private or not - * @return true if the specified ChatRoom is private; - * otherwise, false + * @param evt the LocalUserAdHocChatRoomPresenceChangeEvent that + * notified us of a presence change */ - private static boolean isPrivate(ChatRoom chatRoom) + public void localUserAdHocPresenceChanged( + LocalUserAdHocChatRoomPresenceChangeEvent evt) { - if (!chatRoom.isSystem() - && chatRoom.isJoined() - && (chatRoom.getMembersCount() == 1)) - { - String nickname = chatRoom.getUserNickname(); + String eventType = evt.getEventType(); - if (nickname != null) - { - for (ChatRoomMember member : chatRoom.getMembers()) - if (nickname.equals(member.getName())) - return false; - return true; - } + if (LocalUserAdHocChatRoomPresenceChangeEvent + .LOCAL_USER_JOINED.equals(eventType)) + { + evt.getAdHocChatRoom().addMessageListener(this); + } + else if (LocalUserAdHocChatRoomPresenceChangeEvent + .LOCAL_USER_LEFT.equals(eventType) + || LocalUserAdHocChatRoomPresenceChangeEvent + .LOCAL_USER_DROPPED.equals(eventType)) + { + evt.getAdHocChatRoom().removeMessageListener(this); } - return false; } /** - * Fired on new messages. - * @param evt the MessageReceivedEvent containing - * details on the received message + * Implements the + * LocalUserChatRoomPresenceListener.localUserPresenceChanged + * method. + * @param evt the LocalUserChatRoomPresenceChangeEvent that + * notified us */ - public void messageReceived(MessageReceivedEvent evt) + public void localUserPresenceChanged( + LocalUserChatRoomPresenceChangeEvent evt) { - try - { - // Fire notification - String title = NotificationWiringActivator.getResources().getI18NString( - "service.gui.MSG_RECEIVED", - new String[]{evt.getSourceContact().getDisplayName()}); + ChatRoom sourceChatRoom = evt.getChatRoom(); + String eventType = evt.getEventType(); - fireChatNotification( - evt.getSourceContact(), - INCOMING_MESSAGE, - title, - evt.getSourceMessage().getContent()); + if (LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_JOINED.equals(eventType)) + { + sourceChatRoom.addMessageListener(this); } - catch(Throwable t) + else if (LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_LEFT.equals(eventType) + || LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_KICKED.equals(eventType) + || LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_DROPPED.equals(eventType)) { - logger.error("Error notifying for message received", t); + sourceChatRoom.removeMessageListener(this); } } /** - * Fired when message is delivered. - * @param evt the MessageDeliveredEvent containing - * details on the delivered message + * {@inheritDoc} + * + * Not used. */ - public void messageDelivered(MessageDeliveredEvent evt) - {} + public void messageDelivered(AdHocChatRoomMessageDeliveredEvent ev) {} /** - * Fired when message deliver fail. - * @param evt the MessageDeliveryFailedEvent containing - * details on the failed message + * {@inheritDoc} + * + * Not used. */ - public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) - {} + public void messageDelivered(ChatRoomMessageDeliveredEvent ev) {} /** - * When a request has been received we show a notification. + * {@inheritDoc} * - * @param event FileTransferRequestEvent - * @see FileTransferListener#fileTransferRequestReceived(FileTransferRequestEvent) + * Not used */ - public void fileTransferRequestReceived(FileTransferRequestEvent event) - { - try - { - IncomingFileTransferRequest request = event.getRequest(); - Contact sourceContact = request.getSender(); - - //Fire notification - String title = NotificationWiringActivator.getResources().getI18NString( - "service.gui.FILE_RECEIVING_FROM", - new String[]{sourceContact.getDisplayName()}); - - fireChatNotification( - sourceContact, - INCOMING_FILE, - title, - request.getFileName()); - } - catch(Throwable t) - { - logger.error("Error notifying for file transfer req received", t); - } - } + public void messageDelivered(MessageDeliveredEvent ev) {} /** - * Nothing to do here, because we already know when a file transfer is - * created. - * @param event the FileTransferCreatedEvent that notified us + * {@inheritDoc} + * + * Not used. */ - public void fileTransferCreated(FileTransferCreatedEvent event) - {} + public void messageDeliveryFailed( + AdHocChatRoomMessageDeliveryFailedEvent ev) {} /** - * Called when a new IncomingFileTransferRequest has been rejected. - * Nothing to do here, because we are the one who rejects the request. + * {@inheritDoc} * - * @param event the FileTransferRequestEvent containing the - * received request which was rejected. + * Not used. */ - public void fileTransferRequestRejected(FileTransferRequestEvent event) - {} + public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent ev) {} /** - * Called when an IncomingFileTransferRequest has been canceled - * from the contact who sent it. + * {@inheritDoc} * - * @param event the FileTransferRequestEvent containing the - * request which was canceled. + * Not used. */ - public void fileTransferRequestCanceled(FileTransferRequestEvent event) - {} + public void messageDeliveryFailed(MessageDeliveryFailedEvent ev) {} /** - * Informs the user what is the typing state of his chat contacts. - * - * @param event the event containing details on the typing notification + * Implements the AdHocChatRoomMessageListener.messageReceived + * method. + *
+ * @param evt the AdHocChatRoomMessageReceivedEvent that notified + * us */ - public void typingNotificationReceived(TypingNotificationEvent event) + public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) { try { - Contact contact = event.getSourceContact(); + AdHocChatRoom sourceChatRoom = evt.getSourceChatRoom(); + Contact sourceParticipant = evt.getSourceChatRoomParticipant(); - // we don't care for proactive notifications, different than typing - // sometimes after closing chat we can see someone is typing us - // its just server sanding that the chat is inactive (STATE_STOPPED) - if(event.getTypingState() - != OperationSetTypingNotifications.STATE_TYPING) - return; + // Fire notification + boolean fireChatNotification; - // check whether the current chat window shows the - // chat we received a typing info for and in such case don't show - // notifications - UIService uiService = NotificationWiringActivator.getUIService(); + String nickname = sourceChatRoom.getName(); + String messageContent = evt.getMessage().getContent(); - if(uiService != null) + fireChatNotification = + (nickname == null) + || messageContent.toLowerCase().contains( + nickname.toLowerCase()); + + if (fireChatNotification) { - Chat chat = uiService.getCurrentChat(); - if(chat != null) - { - MetaContact metaContact = uiService.getChatContact(chat); + String title + = NotificationWiringActivator.getResources().getI18NString( + "service.gui.MSG_RECEIVED", + new String[] { sourceParticipant.getDisplayName() }); - if(metaContact != null && metaContact.containsContact(contact) - && chat.isChatFocused()) - { - return; - } - } + fireChatNotification( + sourceChatRoom, + INCOMING_MESSAGE, + title, + messageContent); } + } + catch(Throwable t) + { + logger.error("Error notifying for adhoc message received", t); + } + } - long currentTime = System.currentTimeMillis(); + /** + * Implements the ChatRoomMessageListener.messageReceived method. + *
+ * Obtains the corresponding ChatPanel and process the message + * there. + * @param evt the ChatRoomMessageReceivedEvent that notified us + * that a message has been received + */ + public void messageReceived(ChatRoomMessageReceivedEvent evt) + { + try + { + ChatRoom sourceChatRoom = evt.getSourceChatRoom(); + ChatRoomMember sourceMember = evt.getSourceChatRoomMember(); + + // Fire notification + boolean fireChatNotification; + + String messageContent = evt.getMessage().getContent(); - if (this.proactiveTimer.size() > 0) + /* + * It is uncommon for IRC clients to display popup notifications for + * messages which are sent to public channels and which do not mention + * the nickname of the local user. + */ + if (sourceChatRoom.isSystem() + || isPrivate(sourceChatRoom) + || (messageContent == null)) + fireChatNotification = true; + else { - // first remove contacts that have been here longer than the - // timeout to avoid memory leaks - Iterator> entries - = this.proactiveTimer.entrySet().iterator(); - while (entries.hasNext()) - { - Map.Entry entry = entries.next(); - Long lastNotificationDate = entry.getValue(); - if (lastNotificationDate.longValue() + 30000 < currentTime) - { - // The entry is outdated - entries.remove(); - } - } + String nickname = sourceChatRoom.getUserNickname(); - // Now, check if the contact is still in the map - if (this.proactiveTimer.containsKey(contact)) - { - // We already notified the others about this - return; - } + int atIx = -1; + + if(nickname != null) + atIx = nickname.indexOf("@"); + + fireChatNotification = + (nickname == null) + || messageContent.toLowerCase().contains( + nickname.toLowerCase()) + || ((atIx == -1)? false : messageContent.toLowerCase() + .contains(nickname.substring(0, atIx).toLowerCase())); } - this.proactiveTimer.put(contact, currentTime); + if (fireChatNotification) + { + String title + = NotificationWiringActivator.getResources().getI18NString( + "service.gui.MSG_RECEIVED", + new String[] { sourceMember.getName() }); - fireChatNotification( - contact, - PROACTIVE_NOTIFICATION, - contact.getDisplayName(), - NotificationWiringActivator.getResources() - .getI18NString("service.gui.PROACTIVE_NOTIFICATION")); + fireChatNotification( + sourceChatRoom, + INCOMING_MESSAGE, + title, + messageContent); + } } catch(Throwable t) { - logger.error("Error notifying for typing evt received", t); + logger.error("Error notifying for chat room message received", t); } } /** - * Called to indicate that sending typing notification has failed. - * - * @param event a TypingNotificationEvent containing the sender - * of the notification and its type. - */ - public void typingNotificationDeliveryFailed(TypingNotificationEvent event) - {} - - /** - * Implements CallListener.incomingCallReceived. When a call is received - * plays the ring phone sound to the user and gathers caller information - * that may be used by a user-specified command (incomingCall event trigger). - * @param event the CallEvent + * Fired on new messages. + * @param evt the MessageReceivedEvent containing + * details on the received message */ - public void incomingCallReceived(CallEvent event) + public void messageReceived(MessageReceivedEvent evt) { try { - Call call = event.getSourceCall(); - CallPeer firstPeer = call.getCallPeers().next(); - String peerName = firstPeer.getDisplayName(); - - Map peerInfo = new HashMap(); - peerInfo.put("caller.uri", firstPeer.getURI()); - peerInfo.put("caller.address", firstPeer.getAddress()); - peerInfo.put("caller.name", firstPeer.getDisplayName()); - peerInfo.put("caller.id", firstPeer.getPeerID()); - - callNotifications.put(event.getSourceCall(), - fireNotification( - INCOMING_CALL, - "", - NotificationWiringActivator.getResources() - .getI18NString("service.gui.INCOMING_CALL", - new String[]{peerName}), - peerInfo)); - - call.addCallChangeListener(this); + // Fire notification + String title = NotificationWiringActivator.getResources().getI18NString( + "service.gui.MSG_RECEIVED", + new String[]{evt.getSourceContact().getDisplayName()}); - if(call.getCallPeers().hasNext()) - { - CallPeer peer = call.getCallPeers().next(); - peer.addCallPeerListener(this); - peer.addCallPeerSecurityListener(this); - peer.addCallPeerConferenceListener(this); - } + fireChatNotification( + evt.getSourceContact(), + INCOMING_MESSAGE, + title, + evt.getSourceMessage().getContent()); } catch(Throwable t) { - logger.error("Error notifying for incoming call received", t); + logger.error("Error notifying for message received", t); } } @@ -1077,87 +1225,64 @@ public void outgoingCallCreated(CallEvent event) } /** - * Implements CallListener.callEnded. Stops sounds that are playing at - * the moment if there're any. - * @param event the CallEvent - */ - public void callEnded(CallEvent event) - { - try - { - // Stop all telephony related sounds. -// stopAllTelephonySounds(); - stopSound(callNotifications.get(event.getSourceCall())); - - // Play the hangup sound. - fireNotification(HANG_UP); - } - catch(Throwable t) - { - logger.error("Error notifying for call ended", t); - } - } - - /** - * Implements the CallChangeListener.callPeerAdded method. - * @param evt the CallPeerEvent that notifies us for the change + * {@inheritDoc} + * + * Not used. */ - public void callPeerAdded(CallPeerEvent evt) - { - CallPeer peer = evt.getSourceCallPeer(); - - if(peer == null) - return; - - peer.addCallPeerListener(this); - peer.addCallPeerSecurityListener(this); - peer.addCallPeerConferenceListener(this); - } + public void peerAddressChanged(CallPeerChangeEvent ev) {} /** - * Implements the CallChangeListener.callPeerRemoved method. - * @param evt the CallPeerEvent that has been triggered + * {@inheritDoc} + * + * Not used. */ - public void callPeerRemoved(CallPeerEvent evt) - { - CallPeer peer = evt.getSourceCallPeer(); - - if(peer == null) - return; - - peer.removeCallPeerListener(this); - peer.removeCallPeerSecurityListener(this); - peer.addCallPeerConferenceListener(this); - } + public void peerDisplayNameChanged(CallPeerChangeEvent ev) {} /** - * Call state changed. - * @param evt the CallChangeEvent instance containing the source + * {@inheritDoc} + * + * Not used. */ - public void callStateChanged(CallChangeEvent evt) - { - } + public void peerImageChanged(CallPeerChangeEvent ev) {} /** * Fired when peer's state is changed * - * @param evt fired CallPeerEvent + * @param ev fired CallPeerEvent */ - public void peerStateChanged(CallPeerChangeEvent evt) + public void peerStateChanged(CallPeerChangeEvent ev) { try { - CallPeer sourcePeer = evt.getSourceCallPeer(); - Call call = sourcePeer.getCall(); - CallPeerState newState = (CallPeerState) evt.getNewValue(); - CallPeerState oldState = (CallPeerState) evt.getOldValue(); + final CallPeer peer = ev.getSourceCallPeer(); + Call call = peer.getCall(); + CallPeerState newState = (CallPeerState) ev.getNewValue(); + CallPeerState oldState = (CallPeerState) ev.getOldValue(); // Play the dialing audio when in connecting and initiating call state. // Stop the dialing audio when we enter any other state. - if (newState == CallPeerState.INITIATING_CALL - || newState == CallPeerState.CONNECTING) + if ((newState == CallPeerState.INITIATING_CALL) + || (newState == CallPeerState.CONNECTING)) { - callNotifications.put(call, fireNotification(DIALING)); + NotificationData notification + = fireNotification( + DIALING, + new Callable() + { + public Boolean call() + { + CallPeerState state = peer.getState(); + + return + CallPeerState.INITIATING_CALL.equals( + state) + || CallPeerState.CONNECTING.equals( + state); + } + }); + + if (notification != null) + callNotifications.put(call, notification); } else { @@ -1170,20 +1295,48 @@ public void peerStateChanged(CallPeerChangeEvent evt) //need to fire a notification here. && oldState != CallPeerState.CONNECTING_WITH_EARLY_MEDIA) { - callNotifications.put(call, fireNotification(OUTGOING_CALL)); + NotificationData notification + = fireNotification( + OUTGOING_CALL, + new Callable() + { + public Boolean call() + { + return + CallPeerState.ALERTING_REMOTE_SIDE + .equals(peer.getState()); + } + }); + + if (notification != null) + callNotifications.put(call, notification); } else if (newState == CallPeerState.BUSY) { // We start the busy sound only if we're in a simple call. if (!isConference(call)) { - callNotifications.put(call, fireNotification(BUSY_CALL)); + NotificationData notification + = fireNotification( + BUSY_CALL, + new Callable() + { + public Boolean call() + { + return + CallPeerState.BUSY.equals( + peer.getState()); + } + }); + + if (notification != null) + callNotifications.put(call, notification); } } - else if (newState == CallPeerState.DISCONNECTED - || newState == CallPeerState.FAILED) + else if ((newState == CallPeerState.DISCONNECTED) + || (newState == CallPeerState.FAILED)) { - callNotifications.put(call, fireNotification(HANG_UP)); + fireNotification(HANG_UP); } } catch(Throwable t) @@ -1193,417 +1346,413 @@ else if (newState == CallPeerState.DISCONNECTED } /** - * Fired when peer's display name is changed + * {@inheritDoc} * - * @param evt fired CallPeerEvent + * Not used. */ - public void peerDisplayNameChanged(CallPeerChangeEvent evt) - {} + public void peerTransportAddressChanged(CallPeerChangeEvent ev) {} /** - * Fired when peer's address is changed - * - * @param evt fired CallPeerEvent - */ - public void peerAddressChanged(CallPeerChangeEvent evt) - {} - - /** - * Fired when peer's transport is changed - * - * @param evt fired CallPeerEvent - */ - public void peerTransportAddressChanged(CallPeerChangeEvent evt) - {} - - /** - * Fired when peer's image is changed + * Notifies that a specific Recorder has + * stopped recording the media associated with it. * - * @param evt fired CallPeerEvent - */ - public void peerImageChanged(CallPeerChangeEvent evt) - {} - - /** - * When a securityOnEvent is received. - * @param evt the event we received + * @param recorder the Recorder which has stopped recording its + * associated media */ - public void securityOn(CallPeerSecurityOnEvent evt) + public void recorderStopped(Recorder recorder) { try { - CallPeer peer = (CallPeer) evt.getSource(); + ResourceManagementService resources + = NotificationWiringActivator.getResources(); - if((evt.getSecurityController().requiresSecureSignalingTransport() - && peer.getProtocolProvider().isSignalingTransportSecure()) - || !evt.getSecurityController().requiresSecureSignalingTransport()) - { - fireNotification(CALL_SECURITY_ON); - } + fireNotification( + CALL_SAVED, + resources.getI18NString( + "plugin.callrecordingconfig.CALL_SAVED"), + resources.getI18NString( + "plugin.callrecordingconfig.CALL_SAVED_TO", + new String[] { recorder.getFilename() })); } catch(Throwable t) { - logger.error("Error for notify for security event", t); + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + { + logger.error( + "An error occurred while trying to notify that" + + " the recording of a call has stopped.", + t); + } } } /** - * Indicates the new state through the security indicator components. - * @param securityOffEvent the event we received + * Register all default notifications. */ - public void securityOff(CallPeerSecurityOffEvent securityOffEvent) - {} + private void registerDefaultNotifications() + { + NotificationService notificationService + = NotificationWiringActivator.getNotificationService(); - /** - * The handler for the security event received. The security event - * represents a timeout trying to establish a secure connection. - * Most probably the other peer doesn't support it. - * - * @param securityTimeoutEvent - * the security timeout event received - */ - public void securityTimeout( - CallPeerSecurityTimeoutEvent securityTimeoutEvent) - {} + if(notificationService == null) + return; - /** - * 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) - {} + // Register incoming message notifications. + notificationService.registerDefaultNotificationForEvent( + INCOMING_MESSAGE, + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); - /** - * Processes the received security message. - * @param event the event we received - */ - public void securityMessageRecieved(CallPeerSecurityMessageEvent event) - { - try - { - int severity = event.getEventSeverity(); + notificationService.registerDefaultNotificationForEvent( + INCOMING_MESSAGE, + new SoundNotificationAction( + SoundProperties.INCOMING_MESSAGE, -1, true, false, false)); + + // Register incoming call notifications. + notificationService.registerDefaultNotificationForEvent( + INCOMING_CALL, + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); - String messageTitle = null; + SoundNotificationAction inCallSoundHandler + = new SoundNotificationAction( + SoundProperties.INCOMING_CALL, 2000, true, true, true); - switch (severity) - { - // Don't play alert sound for Info or warning. - case CallPeerSecurityMessageEvent.INFORMATION: - { - messageTitle = NotificationWiringActivator.getResources() - .getI18NString("service.gui.SECURITY_INFO"); - break; - } - case CallPeerSecurityMessageEvent.WARNING: - { - messageTitle = NotificationWiringActivator.getResources() - .getI18NString("service.gui.SECURITY_WARNING"); - break; - } - // Alert sound indicates: security cannot established - case CallPeerSecurityMessageEvent.SEVERE: - case CallPeerSecurityMessageEvent.ERROR: - { - messageTitle = NotificationWiringActivator.getResources() - .getI18NString("service.gui.SECURITY_ERROR"); - fireNotification(CALL_SECURITY_ERROR); - } - } + notificationService.registerDefaultNotificationForEvent( + INCOMING_CALL, + inCallSoundHandler); - fireNotification( + // Register outgoing call notifications. + SoundNotificationAction outCallSoundHandler + = new SoundNotificationAction( + SoundProperties.OUTGOING_CALL, 3000, false, true, false); + + notificationService.registerDefaultNotificationForEvent( + OUTGOING_CALL, + outCallSoundHandler); + + // Register busy call notifications. + notificationService.registerDefaultNotificationForEvent( + BUSY_CALL, + new SoundNotificationAction( + SoundProperties.BUSY, + 1, + false, true, false)); + + // Register dial notifications. + SoundNotificationAction dialSoundHandler + = new SoundNotificationAction( + SoundProperties.DIALING, -1, false, true, false); + + notificationService.registerDefaultNotificationForEvent( + DIALING, + dialSoundHandler); + + // Register the hangup sound notification. + notificationService.registerDefaultNotificationForEvent( + HANG_UP, + new SoundNotificationAction( + SoundProperties.HANG_UP, + -1, + false, true, false)); + + // Register proactive notifications. + notificationService.registerDefaultNotificationForEvent( + PROACTIVE_NOTIFICATION, + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); + + // Register warning message notifications. + notificationService.registerDefaultNotificationForEvent( SECURITY_MESSAGE, - messageTitle, - event.getI18nMessage()); - } - catch(Throwable t) - { - logger.error("Error notifying for security message received", t); - } - } + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); - /** - * Implements the ChatRoomMessageListener.messageReceived method. - *
- * Obtains the corresponding ChatPanel and process the message - * there. - * @param evt the ChatRoomMessageReceivedEvent that notified us - * that a message has been received - */ - public void messageReceived(ChatRoomMessageReceivedEvent evt) - { - try - { - ChatRoom sourceChatRoom = evt.getSourceChatRoom(); - ChatRoomMember sourceMember = evt.getSourceChatRoomMember(); + // Register sound notification for security state on during a call. + notificationService.registerDefaultNotificationForEvent( + CALL_SECURITY_ON, + new SoundNotificationAction( + SoundProperties.CALL_SECURITY_ON, -1, + false, true, false)); - // Fire notification - boolean fireChatNotification; + // Register sound notification for security state off during a call. + notificationService.registerDefaultNotificationForEvent( + CALL_SECURITY_ERROR, + new SoundNotificationAction( + SoundProperties.CALL_SECURITY_ERROR, -1, + false, true, false)); - String messageContent = evt.getMessage().getContent(); + // Register sound notification for incoming files. + notificationService.registerDefaultNotificationForEvent( + INCOMING_FILE, + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); - /* - * It is uncommon for IRC clients to display popup notifications for - * messages which are sent to public channels and which do not mention - * the nickname of the local user. - */ - if (sourceChatRoom.isSystem() - || isPrivate(sourceChatRoom) - || (messageContent == null)) - fireChatNotification = true; - else + notificationService.registerDefaultNotificationForEvent( + INCOMING_FILE, + new SoundNotificationAction( + SoundProperties.INCOMING_FILE, -1, + true, false, false)); + + // Register notification for saved calls. + notificationService.registerDefaultNotificationForEvent( + CALL_SAVED, + NotificationAction.ACTION_POPUP_MESSAGE, + null, + null); + } + + /** + * Processes the received security message. + * @param ev the event we received + */ + public void securityMessageRecieved(CallPeerSecurityMessageEvent ev) + { + try + { + String messageTitleKey; + + switch (ev.getEventSeverity()) { - String nickname = sourceChatRoom.getUserNickname(); + // Don't play alert sound for Info or warning. + case CallPeerSecurityMessageEvent.INFORMATION: + messageTitleKey = "service.gui.SECURITY_INFO"; + break; - int atIx = -1; + case CallPeerSecurityMessageEvent.WARNING: + messageTitleKey = "service.gui.SECURITY_WARNING"; + break; - if(nickname != null) - atIx = nickname.indexOf("@"); + // Security cannot be established! Play an alert sound. + case CallPeerSecurityMessageEvent.SEVERE: + case CallPeerSecurityMessageEvent.ERROR: + messageTitleKey = "service.gui.SECURITY_ERROR"; + fireNotification(CALL_SECURITY_ERROR); + break; - fireChatNotification = - (nickname == null) - || messageContent.toLowerCase().contains( - nickname.toLowerCase()) - || ((atIx == -1)? false : messageContent.toLowerCase() - .contains(nickname.substring(0, atIx).toLowerCase())); + default: + /* + * Whatever other severity there is or will be, we do not how to + * react to it yet. + */ + messageTitleKey = null; } - if (fireChatNotification) + if (messageTitleKey != null) { - String title - = NotificationWiringActivator.getResources().getI18NString( - "service.gui.MSG_RECEIVED", - new String[] { sourceMember.getName() }); - - fireChatNotification( - sourceChatRoom, - INCOMING_MESSAGE, - title, - messageContent); + fireNotification( + SECURITY_MESSAGE, + NotificationWiringActivator.getResources() + .getI18NString(messageTitleKey), + ev.getI18nMessage()); } } catch(Throwable t) { - logger.error("Error notifying for chat room message received", t); + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + { + logger.error( + "An error occurred while trying to notify" + + " about a security message", + t); + } } } /** - * Implements the ChatRoomMessageListener.messageDelivered method. - *
- * @param evt the ChatRoomMessageDeliveredEvent that notified us - * that the message was delivered to its destination + * {@inheritDoc} + * + * Not used. */ - public void messageDelivered(ChatRoomMessageDeliveredEvent evt) - {} + public void securityNegotiationStarted( + CallPeerSecurityNegotiationStartedEvent ev) {} /** - * Implements the ChatRoomMessageListener.messageDeliveryFailed - * method. - *
- * @param evt the ChatRoomMessageDeliveryFailedEvent that notified - * us of a delivery failure + * {@inheritDoc} + * + * Not used. */ - public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent evt) - {} + public void securityOff(CallPeerSecurityOffEvent ev) {} /** - * Implements the - * LocalUserChatRoomPresenceListener.localUserPresenceChanged - * method. - * @param evt the LocalUserChatRoomPresenceChangeEvent that - * notified us + * When a securityOnEvent is received. + * @param ev the event we received */ - public void localUserPresenceChanged( - LocalUserChatRoomPresenceChangeEvent evt) + public void securityOn(CallPeerSecurityOnEvent ev) { - ChatRoom sourceChatRoom = evt.getChatRoom(); - String eventType = evt.getEventType(); - - if (LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_JOINED.equals(eventType)) + try { - sourceChatRoom.addMessageListener(this); + SrtpControl securityController = ev.getSecurityController(); + CallPeer peer = (CallPeer) ev.getSource(); + + if(!securityController.requiresSecureSignalingTransport() + || peer.getProtocolProvider().isSignalingTransportSecure()) + { + fireNotification(CALL_SECURITY_ON); + } } - else if (LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_LEFT.equals(eventType) - || LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_KICKED.equals(eventType) - || LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_DROPPED.equals(eventType)) + catch(Throwable t) { - sourceChatRoom.removeMessageListener(this); + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + { + logger.error( + "An error occurred while trying to notify" + + " about a security-related event", + t); + } } } /** - * Implements the - * LocalUserAdHocChatRoomPresenceListener.localUserPresenceChanged - * method + * {@inheritDoc} * - * @param evt the LocalUserAdHocChatRoomPresenceChangeEvent that - * notified us of a presence change + * Not used. */ - public void localUserAdHocPresenceChanged( - LocalUserAdHocChatRoomPresenceChangeEvent evt) - { - String eventType = evt.getEventType(); - - if (LocalUserAdHocChatRoomPresenceChangeEvent - .LOCAL_USER_JOINED.equals(eventType)) - { - evt.getAdHocChatRoom().addMessageListener(this); - } - else if (LocalUserAdHocChatRoomPresenceChangeEvent - .LOCAL_USER_LEFT.equals(eventType) - || LocalUserAdHocChatRoomPresenceChangeEvent - .LOCAL_USER_DROPPED.equals(eventType)) - { - evt.getAdHocChatRoom().removeMessageListener(this); - } - } + public void securityTimeout(CallPeerSecurityTimeoutEvent ev) {} /** - * Implements the AdHocChatRoomMessageListener.messageReceived - * method. - *
- * @param evt the AdHocChatRoomMessageReceivedEvent that notified - * us + * Implements the ServiceListener method. Verifies whether the + * passed event concerns a ProtocolProviderService and adds the + * corresponding listeners. + * + * @param event The ServiceEvent object. */ - public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) + public void serviceChanged(ServiceEvent event) { - try - { - AdHocChatRoom sourceChatRoom = evt.getSourceChatRoom(); - Contact sourceParticipant = evt.getSourceChatRoomParticipant(); - - // Fire notification - boolean fireChatNotification; + ServiceReference serviceRef = event.getServiceReference(); - String nickname = sourceChatRoom.getName(); - String messageContent = evt.getMessage().getContent(); + // if the event is caused by a bundle being stopped, we don't want to + // know + if (serviceRef.getBundle().getState() == Bundle.STOPPING) + return; - fireChatNotification = - (nickname == null) - || messageContent.toLowerCase().contains( - nickname.toLowerCase()); + Object service + = NotificationWiringActivator.bundleContext.getService(serviceRef); - if (fireChatNotification) + // we don't care if the source service is not a protocol provider + if (service instanceof ProtocolProviderService) + { + switch (event.getType()) { - String title - = NotificationWiringActivator.getResources().getI18NString( - "service.gui.MSG_RECEIVED", - new String[] { sourceParticipant.getDisplayName() }); - - fireChatNotification( - sourceChatRoom, - INCOMING_MESSAGE, - title, - messageContent); + case ServiceEvent.REGISTERED: + handleProviderAdded((ProtocolProviderService) service); + break; + case ServiceEvent.UNREGISTERING: + handleProviderRemoved((ProtocolProviderService) service); + break; } } - catch(Throwable t) - { - logger.error("Error notifying for adhoc message received", t); - } } /** - * Implements the ChatRoomMessageListener.messageDelivered method. - *
- * @param evt the ChatRoomMessageDeliveredEvent that notified us - * that the message was delivered to its destination - */ - public void messageDelivered(AdHocChatRoomMessageDeliveredEvent evt) - {} - - /** - * Implements AdHocChatRoomMessageListener.messageDeliveryFailed - * method. - *
- * In the conversation area shows an error message, explaining the problem. - * @param evt the AdHocChatRoomMessageDeliveryFailedEvent that - * notified us - */ - public void messageDeliveryFailed(AdHocChatRoomMessageDeliveryFailedEvent evt) - {} - - /** - * Call peer has changed. - * @param conferenceEvent - * a CallPeerConferenceEvent with ID - * CallPeerConferenceEvent#CONFERENCE_FOCUS_CHANGED + * {@inheritDoc} + * + * Not used. */ - public void conferenceFocusChanged(CallPeerConferenceEvent conferenceEvent) - {} + public void typingNotificationDeliveryFailed(TypingNotificationEvent ev) {} /** - * Indicates that the given conference member has been added to the given - * peer. + * Informs the user what is the typing state of his chat contacts. * - * @param conferenceEvent the event + * @param ev the event containing details on the typing notification */ - public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent) + public void typingNotificationReceived(TypingNotificationEvent ev) { try { - CallPeer peer - = conferenceEvent - .getConferenceMember() - .getConferenceFocusCallPeer(); + Contact contact = ev.getSourceContact(); - if(peer.getConferenceMemberCount() > 0) + // we don't care for proactive notifications, different than typing + // sometimes after closing chat we can see someone is typing us + // its just server sanding that the chat is inactive (STATE_STOPPED) + if(ev.getTypingState() + != OperationSetTypingNotifications.STATE_TYPING) { - CallPeerSecurityStatusEvent securityEvent - = peer.getCurrentSecuritySettings(); + return; + } - if (securityEvent instanceof CallPeerSecurityOnEvent) - fireNotification(CALL_SECURITY_ON); + // check whether the current chat window shows the + // chat we received a typing info for and in such case don't show + // notifications + UIService uiService = NotificationWiringActivator.getUIService(); + + if(uiService != null) + { + Chat chat = uiService.getCurrentChat(); + + if(chat != null) + { + MetaContact metaContact = uiService.getChatContact(chat); + + if((metaContact != null) + && metaContact.containsContact(contact) + && chat.isChatFocused()) + { + return; + } + } } - } - catch(Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - else - logger.error("Error notifying for secured call member", t); - } - } - /** - * Indicates that the given conference member has been removed from the - * given peer. - * - * @param conferenceEvent the event - */ - public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent) - {} + long currentTime = System.currentTimeMillis(); - /** - * Notifies that a specific Recorder has - * stopped recording the media associated with it. - * - * @param recorder the Recorder which has stopped recording its - * associated media - */ - public void recorderStopped(Recorder recorder) - { - try - { - fireNotification( - CALL_SAVED, - NotificationWiringActivator.getResources().getI18NString( - "plugin.callrecordingconfig.CALL_SAVED"), + if (proactiveTimer.size() > 0) + { + // first remove contacts that have been here longer than the + // timeout to avoid memory leaks + Iterator> entries + = proactiveTimer.entrySet().iterator(); + + while (entries.hasNext()) + { + Map.Entry entry = entries.next(); + Long lastNotificationDate = entry.getValue(); + + if (lastNotificationDate.longValue() + 30000 < currentTime) + { + // The entry is outdated + entries.remove(); + } + } + + // Now, check if the contact is still in the map + if (proactiveTimer.containsKey(contact)) + { + // We already notified the others about this + return; + } + } + + proactiveTimer.put(contact, currentTime); + + fireChatNotification( + contact, + PROACTIVE_NOTIFICATION, + contact.getDisplayName(), NotificationWiringActivator.getResources().getI18NString( - "plugin.callrecordingconfig.CALL_SAVED_TO", - new String[] { recorder.getFilename() })); + "service.gui.PROACTIVE_NOTIFICATION")); } catch(Throwable t) { - logger.error("Error notifying for recorder stopped", t); + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + else + { + logger.error( + "An error occurred while handling" + + " a typing notification.", + t); + } } } } diff --git a/src/net/java/sip/communicator/plugin/reconnectplugin/ReconnectPluginActivator.java b/src/net/java/sip/communicator/plugin/reconnectplugin/ReconnectPluginActivator.java index b8983dd07..17958d29d 100644 --- a/src/net/java/sip/communicator/plugin/reconnectplugin/ReconnectPluginActivator.java +++ b/src/net/java/sip/communicator/plugin/reconnectplugin/ReconnectPluginActivator.java @@ -606,7 +606,6 @@ private void notify(String title, String i18nKey, String[] params) NETWORK_NOTIFICATIONS, title, getResources().getI18NString(i18nKey, params), - null, null); } diff --git a/src/net/java/sip/communicator/service/notification/CommandNotificationHandler.java b/src/net/java/sip/communicator/service/notification/CommandNotificationHandler.java index 854016614..e59e73a38 100644 --- a/src/net/java/sip/communicator/service/notification/CommandNotificationHandler.java +++ b/src/net/java/sip/communicator/service/notification/CommandNotificationHandler.java @@ -19,10 +19,12 @@ public interface CommandNotificationHandler { /** * Executes the program pointed by the descriptor. + * * @param action the action to act upon * @param cmdargs arguments that are passed to the command line specified * in the action */ - public void execute(CommandNotificationAction action, - Map cmdargs); + public void execute( + CommandNotificationAction action, + Map cmdargs); } diff --git a/src/net/java/sip/communicator/service/notification/NotificationData.java b/src/net/java/sip/communicator/service/notification/NotificationData.java index d88b6e6f5..fb88de082 100644 --- a/src/net/java/sip/communicator/service/notification/NotificationData.java +++ b/src/net/java/sip/communicator/service/notification/NotificationData.java @@ -16,34 +16,71 @@ */ public class NotificationData { + /** + * The name/key of the NotificationData extra which is provided to + * {@link CommandNotificationHandler#execute(CommandNotificationAction, + * Map)} i.e. a Map<String,String> which is known by the + * (argument) name cmdargs. + */ + public static final String COMMAND_NOTIFICATION_HANDLER_CMDARGS_EXTRA + = "CommandNotificationHandler.cmdargs"; + + /** + * The name/key of the NotificationData extra which is provided to + * {@link PopupMessageNotificationHandler#popupMessage( + * PopupMessageNotificationAction, String, String, byte[], Object)} i.e. an + * Object which is known by the (argument) name tag. + */ + public static final String POPUP_MESSAGE_HANDLER_TAG_EXTRA + = "PopupMessageNotificationHandler.tag"; + + /** + * The name/key of the NotificationData extra which is provided to + * {@link SoundNotificationHandler} i.e. a Callable<Boolean> + * which is known as the condition which determines whether looping sounds + * are to continue playing. + */ + public static final String SOUND_NOTIFICATION_HANDLER_LOOP_CONDITION_EXTRA + = "SoundNotificationHandler.loopCondition"; + private final String eventType; - private final String title; - private final String message; - private final Map extra; + + /** + * The {@link NotificationHandler}-specific extras provided to this + * instance. The keys are among the XXX_EXTRA constants defined by + * the NotificationData class. + */ + private final Map extras; + private final byte[] icon; - private final Object tag; + private final String message; + private final String title; /** * Creates a new instance of this class. * * @param eventType the type of the event that we'd like to fire a - * notification for. + * notification for. * @param title the title of the given message * @param message the message to use if and where appropriate (e.g. with - * systray or log notification.) - * @param extra additional data (such as caller information) + * systray or log notification.) * @param icon the icon to show in the notification if and where appropriate - * @param tag additional info to be used by the notification handler + * @param extras additional/extra {@link NotificationHandler}-specific data + * to be provided by the new instance to the various + * NotificationHandlers */ - NotificationData(String eventType, String title, String message, - Map extra, byte[] icon, Object tag) + NotificationData( + String eventType, + String title, + String message, + byte[] icon, + Map extras) { this.eventType = eventType; this.title = title; this.message = message; - this.extra = extra; this.icon = icon; - this.tag = tag; + this.extras = extras; } /** @@ -57,53 +94,61 @@ public String getEventType() } /** - * Gets the title of the given message. - * - * @return the title + * Gets the {@link NotificationHandler}-specific extras provided to this + * instance. + * + * @return the NotificationHandler-specific extras provided to this + * instance. The keys are among the XXX_EXTRA constants defined by + * the NotificationData class */ - String getTitle() + Map getExtras() { - return title; + return Collections.unmodifiableMap(extras); } /** - * Gets the message to use if and where appropriate (e.g. with systray or - * log notification). - * - * @return the message + * Gets the {@link NotificationHandler}-specific extra provided to this + * instance associated with a specific key. + * + * @param key the key whose associated NotificationHandler-specific + * extra is to be returned. Well known keys are defined by the + * NotificationData class as the XXX_EXTRA constants. + * @return the NotificationHandler-specific extra provided to this + * instance associated with the specified key */ - String getMessage() + public Object getExtra(String key) { - return message; + return (extras == null) ? null : extras.get(key); } /** - * Gets additional data (such as caller information). + * Gets the icon to show in the notification if and where appropriate. * - * @return the extra data + * @return the icon */ - public Map getExtra() + byte[] getIcon() { - return extra; + return icon; } /** - * Gets the icon to show in the notification if and where appropriate. + * Gets the message to use if and where appropriate (e.g. with systray or + * log notification). * - * @return the icon + * @return the message */ - byte[] getIcon() + String getMessage() { - return icon; + return message; } /** - * Gets additional info to be used by the notification handler. + * Gets the title of the given message. * - * @return the tag + * @return the title */ - Object getTag() + String getTitle() { - return tag; + return title; } } diff --git a/src/net/java/sip/communicator/service/notification/NotificationService.java b/src/net/java/sip/communicator/service/notification/NotificationService.java index c0b61ea49..5de9a3127 100644 --- a/src/net/java/sip/communicator/service/notification/NotificationService.java +++ b/src/net/java/sip/communicator/service/notification/NotificationService.java @@ -235,6 +235,7 @@ public void removeNotificationChangeListener( *

* This method does nothing if the given eventType is not contained * in the list of registered event types. + *

* * @param eventType the type of the event that we'd like to fire a * notification for. @@ -243,7 +244,6 @@ public void removeNotificationChangeListener( * @param message the message to use if and where appropriate (e.g. with * systray or log notification.) * @param icon the icon to show in the notification if and where appropriate - * @param tag additional info to be used by the notification handler * @return An object referencing the notification. It may be used to stop a * still running notification. Can be null if the eventType is * unknown or the notification is not active. @@ -251,8 +251,7 @@ public void removeNotificationChangeListener( public NotificationData fireNotification( String eventType, String messageTitle, String message, - byte[] icon, - Object tag); + byte[] icon); /** * Fires all notifications registered for the specified eventType @@ -268,19 +267,21 @@ public NotificationData fireNotification( String eventType, * (e.g. with systray) * @param message the message to use if and where appropriate (e.g. with * systray or log notification.) - * @param extra the extra data to pass (especially for Command execution) * @param icon the icon to show in the notification if and where appropriate - * @param tag additional info to be used by the notification handler + * @param extras additional/extra {@link NotificationHandler}-specific data + * to be provided to the firing of the specified notification(s). The + * well-known keys are defined by the NotificationData + * XXX_EXTRA constants. * @return An object referencing the notification. It may be used to stop a * still running notification. Can be null if the eventType is * unknown or the notification is not active. */ - public NotificationData fireNotification( String eventType, - String messageTitle, - String message, - Map extra, - byte[] icon, - Object tag); + public NotificationData fireNotification( + String eventType, + String messageTitle, + String message, + byte[] icon, + Map extras); /** * Fires all notifications registered for the specified eventType diff --git a/src/net/java/sip/communicator/service/notification/NotificationServiceImpl.java b/src/net/java/sip/communicator/service/notification/NotificationServiceImpl.java index 648b8bd3b..64a736987 100644 --- a/src/net/java/sip/communicator/service/notification/NotificationServiceImpl.java +++ b/src/net/java/sip/communicator/service/notification/NotificationServiceImpl.java @@ -33,20 +33,17 @@ class NotificationServiceImpl implements NotificationService { - private final Logger logger - = Logger.getLogger(NotificationServiceImpl.class); - - private final ConfigurationService configService = - NotificationServiceActivator.getConfigurationService(); - private static final String NOTIFICATIONS_PREFIX = "net.java.sip.communicator.impl.notifications"; /** - * A set of all registered event notifications. + * A list of all registered NotificationChangeListeners. */ - private final Map notifications - = new HashMap(); + private final List changeListeners + = new Vector(); + + private final ConfigurationService configService = + NotificationServiceActivator.getConfigurationService(); /** * A set of all registered event notifications. @@ -60,11 +57,8 @@ class NotificationServiceImpl private final Map handlers = new HashMap(); - /** - * A list of all registered NotificationChangeListeners. - */ - private final List changeListeners - = new Vector(); + private final Logger logger + = Logger.getLogger(NotificationServiceImpl.class); /** * Queue to cache fired notifications before all handlers are registered. @@ -72,6 +66,12 @@ class NotificationServiceImpl private Queue notificationCache = new LinkedList(); + /** + * A set of all registered event notifications. + */ + private final Map notifications + = new HashMap(); + /** * Creates an instance of NotificationServiceImpl by loading all * previously saved notifications. @@ -83,361 +83,213 @@ class NotificationServiceImpl } /** - * Creates a new EventNotification or obtains the corresponding - * existing one and registers a new action in it. + * Adds an object that executes the actual action of a notification action. + * If the same action type is added twice, the last added wins. * - * @param eventType the name of the event (as defined by the plugin that's - * registering it) that we are setting an action for. - * @param action the NotificationAction responsible for - * handling the given actionType + * @param handler The handler that executes the action. */ - public void registerNotificationForEvent( String eventType, - NotificationAction action) + public void addActionHandler(NotificationHandler handler) { - Notification notification = null; + if(handler == null) + throw new IllegalArgumentException("handler cannot be null"); - if(notifications.containsKey(eventType)) - notification = notifications.get(eventType); - else + synchronized(handlers) { - notification = new Notification(eventType); - notifications.put(eventType, notification); - - this.fireNotificationEventTypeEvent( - EVENT_TYPE_ADDED, eventType); - } - - Object existingAction = notification.addAction(action); + handlers.put(handler.getActionType(), handler); + if((handlers.size() == NUM_ACTIONS) && (notificationCache != null)) + { + for(NotificationData event : notificationCache) + fireNotification(event); - // We fire the appropriate event depending on whether this is an - // already existing actionType or a new one. - if (existingAction != null) - { - fireNotificationActionTypeEvent( - ACTION_CHANGED, - eventType, - action); - } - else - { - fireNotificationActionTypeEvent( - ACTION_ADDED, - eventType, - action); + notificationCache.clear(); + notificationCache = null; + } } - - // Save the notification through the ConfigurationService. - this.saveNotification(eventType, - action, - true, - false); } /** - * Creates a new EventNotification or obtains the corresponding - * existing one and registers a new action in it. + * Adds the given listener to the list of change listeners. * - * @param eventType the name of the event (as defined by the plugin that's - * registering it) that we are setting an action for. - * @param actionType the type of the action that is to be executed when the - * specified event occurs (could be one of the ACTION_XXX fields). - * @param actionDescriptor a String containing a description of the action - * (a URI to the sound file for audio notifications or a command line for - * exec action types) that should be executed when the action occurs. - * @param defaultMessage the default message to use if no specific message - * has been provided when firing the notification. + * @param listener the listener that we'd like to register to listen for + * changes in the event notifications stored by this service. */ - public void registerNotificationForEvent( String eventType, - String actionType, - String actionDescriptor, - String defaultMessage) + public void addNotificationChangeListener( + NotificationChangeListener listener) { - if (logger.isDebugEnabled()) - logger.debug("Registering event " + eventType + "/" + - actionType + "/" + actionDescriptor + "/" + defaultMessage); - - if (actionType.equals(ACTION_SOUND)) - { - Notification notification = defaultNotifications.get(eventType); - SoundNotificationAction action = - (SoundNotificationAction) notification.getAction(ACTION_SOUND); - registerNotificationForEvent ( - eventType, - new SoundNotificationAction( - actionDescriptor, - action.getLoopInterval())); - } - else if (actionType.equals(ACTION_LOG_MESSAGE)) - { - registerNotificationForEvent (eventType, - new LogMessageNotificationAction( - LogMessageNotificationAction.INFO_LOG_TYPE)); - } - else if (actionType.equals(ACTION_POPUP_MESSAGE)) - { - registerNotificationForEvent (eventType, - new PopupMessageNotificationAction(defaultMessage)); - } - else if (actionType.equals(ACTION_COMMAND)) + synchronized (changeListeners) { - registerNotificationForEvent (eventType, - new CommandNotificationAction(actionDescriptor)); + changeListeners.add(listener); } } /** - * Removes the EventNotification corresponding to the given - * eventType from the table of registered event notifications. - * - * @param eventType the name of the event (as defined by the plugin that's - * registering it) to be removed. + * Checking an action when it is edited (property .default=false). + * Checking for older versions of the property. If it is older one + * we migrate it to new configuration using the default values. + * + * @param eventType the event type. + * @param defaultAction the default action which values we will use. */ - public void removeEventNotification(String eventType) + private void checkDefaultAgainstLoadedNotification + (String eventType, NotificationAction defaultAction) { - notifications.remove(eventType); - - this.fireNotificationEventTypeEvent( - EVENT_TYPE_REMOVED, eventType); + // checking for new sound action properties + if(defaultAction instanceof SoundNotificationAction) + { + SoundNotificationAction soundDefaultAction + = (SoundNotificationAction)defaultAction; + SoundNotificationAction soundAction = (SoundNotificationAction) + getEventNotificationAction(eventType, ACTION_SOUND); + + boolean isSoundNotificationEnabledPropExist + = getNotificationActionProperty( + eventType, + defaultAction, + "isSoundNotificationEnabled") != null; + + if(!isSoundNotificationEnabledPropExist) + { + soundAction.setSoundNotificationEnabled( + soundDefaultAction.isSoundNotificationEnabled()); + } + + boolean isSoundPlaybackEnabledPropExist + = getNotificationActionProperty( + eventType, + defaultAction, + "isSoundPlaybackEnabled") != null; + + if(!isSoundPlaybackEnabledPropExist) + { + soundAction.setSoundPlaybackEnabled( + soundDefaultAction.isSoundPlaybackEnabled()); + } + + boolean isSoundPCSpeakerEnabledPropExist + = getNotificationActionProperty( + eventType, + defaultAction, + "isSoundPCSpeakerEnabled") != null; + + if(!isSoundPCSpeakerEnabledPropExist) + { + soundAction.setSoundPCSpeakerEnabled( + soundDefaultAction.isSoundPCSpeakerEnabled()); + } + + boolean fixDialingLoop = false; + + // hack to fix wrong value:just check whether loop for outgoing call + // (dialing) has gone into config as 0, should be -1 + if(eventType.equals("Dialing") + && soundAction.getLoopInterval() == 0) + { + soundAction.setLoopInterval( + soundDefaultAction.getLoopInterval()); + fixDialingLoop = true; + } + + if(!(isSoundNotificationEnabledPropExist + && isSoundPCSpeakerEnabledPropExist + && isSoundPlaybackEnabledPropExist) + || fixDialingLoop) + { + // this check is done only when the notification + // is edited and is not default + saveNotification( + eventType, + soundAction, + soundAction.isEnabled(), + false); + } + } } /** - * Removes the given actionType from the list of actions registered for the - * given eventType. - * - * @param eventType the name of the event (as defined by the plugin that's - * registering it) for which we'll remove the notification. - * @param actionType the type of the action that is to be executed when the - * specified event occurs (could be one of the ACTION_XXX fields). + * Executes a notification data object on the handlers. + * @param data The notification data to act upon. */ - public void removeEventNotificationAction( String eventType, - String actionType) + private void fireNotification(NotificationData data) { - Notification notification - = notifications.get(eventType); - - if(notification == null) - return; + Notification notification = notifications.get(data.getEventType()); - NotificationAction action = notification.getAction(actionType); - - if(action == null) + if((notification == null) || !notification.isActive()) return; - notification.removeAction(actionType); + for(NotificationAction action : notification.getActions().values()) + { + String actionType = action.getActionType(); - saveNotification( - eventType, - action, - false, - false); + if(!action.isEnabled()) + continue; - fireNotificationActionTypeEvent( - ACTION_REMOVED, - eventType, - action); - } - - /** - * Returns an iterator over a list of all events registered in this - * notification service. Each line in the returned list consists of - * a String, representing the name of the event (as defined by the plugin - * that registered it). - * - * @return an iterator over a list of all events registered in this - * notifications service - */ - public Iterable getRegisteredEvents() - { - return Collections.unmodifiableSet( - notifications.keySet()); - } - - /** - * Returns the notification action corresponding to the given - * eventType and actionType. - * - * @param eventType the type of the event that we'd like to retrieve. - * @param actionType the type of the action that we'd like to retrieve a - * descriptor for. - * @return the notification action of the action to be executed - * when an event of the specified type has occurred. - */ - public NotificationAction getEventNotificationAction( - String eventType, - String actionType) - { - Notification notification = notifications.get(eventType); - - if(notification == null) - return null; - - return notification.getAction(actionType); - } - - /** - * Adds the given listener to the list of change listeners. - * - * @param listener the listener that we'd like to register to listen for - * changes in the event notifications stored by this service. - */ - public void addNotificationChangeListener( - NotificationChangeListener listener) - { - synchronized (changeListeners) - { - changeListeners.add(listener); - } - } - - /** - * Removes the given listener from the list of change listeners. - * - * @param listener the listener that we'd like to remove - */ - public void removeNotificationChangeListener( - NotificationChangeListener listener) - { - synchronized (changeListeners) - { - changeListeners.remove(listener); - } - } - - /** - * Adds an object that executes the actual action of a notification action. - * If the same action type is added twice, the last added wins. - * - * @param handler The handler that executes the action. - */ - public void addActionHandler(NotificationHandler handler) - { - if(handler == null) - throw new IllegalArgumentException("handler cannot be null"); - - synchronized(handlers) - { - handlers.put(handler.getActionType(), handler); - if(handlers.size() == NUM_ACTIONS && notificationCache != null) - { - for(NotificationData event : notificationCache) - fireNotification(event); - - notificationCache.clear(); - notificationCache = null; - } - } - } - - /** - * Removes an object that executes the actual action of notification action. - * @param actionType The handler type to remove. - */ - public void removeActionHandler(String actionType) - { - if(actionType == null) - throw new IllegalArgumentException("actionType cannot be null"); - - synchronized(handlers) - { - handlers.remove(actionType); - } - } - - /** - * Gets a list of handler for the specified action type. - * - * @param actionType the type for which the list of handlers should be - * retrieved or null if all handlers shall be returned. - */ - public Iterable getActionHandlers(String actionType) - { - if (actionType != null) - { NotificationHandler handler = handlers.get(actionType); - Set ret; if (handler == null) - ret = Collections.emptySet(); - else - ret = Collections.singleton(handler); - return ret; - } - else - return handlers.values(); - } - - /** - * Executes a notification data object on the handlers. - * @param data The notification data to act upon. - */ - private void fireNotification(NotificationData data) - { - Notification notification = notifications.get(data.getEventType()); - if(notification == null || !notification.isActive()) - return; - - for(NotificationAction action : notification.getActions().values()) - { - String actionType = action.getActionType(); - if(!action.isEnabled() || !handlers.containsKey(actionType)) continue; - NotificationHandler handler = handlers.get(actionType); if (actionType.equals(ACTION_POPUP_MESSAGE)) { - ((PopupMessageNotificationHandler) handler) - .popupMessage((PopupMessageNotificationAction) action, - data.getTitle(), data.getMessage(), - data.getIcon(), data.getTag()); + ((PopupMessageNotificationHandler) handler).popupMessage( + (PopupMessageNotificationAction) action, + data.getTitle(), + data.getMessage(), + data.getIcon(), + data.getExtra( + NotificationData + .POPUP_MESSAGE_HANDLER_TAG_EXTRA)); } else if (actionType.equals(ACTION_LOG_MESSAGE)) { - ((LogMessageNotificationHandler) handler) - .logMessage((LogMessageNotificationAction) action, + ((LogMessageNotificationHandler) handler).logMessage( + (LogMessageNotificationAction) action, data.getMessage()); } else if (actionType.equals(ACTION_SOUND)) { SoundNotificationAction soundNotificationAction = (SoundNotificationAction) action; + if(soundNotificationAction.isSoundNotificationEnabled() - || soundNotificationAction.isSoundPlaybackEnabled() - || soundNotificationAction.isSoundPCSpeakerEnabled()) + || soundNotificationAction.isSoundPlaybackEnabled() + || soundNotificationAction.isSoundPCSpeakerEnabled()) { - ((SoundNotificationHandler) handler) - .start((SoundNotificationAction) action, data); + ((SoundNotificationHandler) handler).start( + (SoundNotificationAction) action, + data); } } else if (actionType.equals(ACTION_COMMAND)) { - ((CommandNotificationHandler) handler) - .execute( - (CommandNotificationAction)action, - data.getExtra()); + @SuppressWarnings("unchecked") + Map cmdargs + = (Map) + data.getExtra( + NotificationData + .COMMAND_NOTIFICATION_HANDLER_CMDARGS_EXTRA); + + ((CommandNotificationHandler) handler).execute( + (CommandNotificationAction) action, + cmdargs); } } } /** - * Stops a notification if notification is continuous, like playing sounds - * in loop. Do nothing if there are no such events currently processing. - * - * @param data the data that has been returned when firing the event.. + * If there is a registered event notification of the given + * eventType and the event notification is currently activated, we + * go through the list of registered actions and execute them. + * + * @param eventType the type of the event that we'd like to fire a + * notification for. + * + * @return An object referencing the notification. It may be used to stop a + * still running notification. Can be null if the eventType is + * unknown or the notification is not active. */ - public void stopNotification(NotificationData data) + public NotificationData fireNotification(String eventType) { - Iterable soundHandlers - = getActionHandlers(NotificationAction.ACTION_SOUND); - - // There could be no sound action handler for this event type - if (soundHandlers != null) - { - for (NotificationHandler handler : soundHandlers) - { - if (handler instanceof SoundNotificationHandler) - ((SoundNotificationHandler) handler).stop(data); - } - } + return fireNotification(eventType, null, null, null); } /** @@ -461,15 +313,9 @@ public NotificationData fireNotification( String eventType, String title, String message, - byte[] icon, - Object tag) + byte[] icon) { - return fireNotification(eventType, - title, - message, - null, - icon, - tag); + return fireNotification(eventType, title, message, icon, null); } /** @@ -482,28 +328,30 @@ public NotificationData fireNotification( * @param title the title of the given message * @param message the message to use if and where appropriate (e.g. with * systray or log notification.) - * @param extra the extra data to pass (especially for Command execution) * @param icon the icon to show in the notification if and where appropriate - * @param tag additional info to be used by the notification handler + * @param extras additiona/extra {@link NotificationHandler}-specific data + * to be provided to the firing of the specified notification(s). The + * well-known keys are defined by the NotificationData + * XXX_EXTRA constants. * * @return An object referencing the notification. It may be used to stop a * still running notification. Can be null if the eventType is * unknown or the notification is not active. */ public NotificationData fireNotification( - String eventType, - String title, - String message, - Map extra, - byte[] icon, - Object tag) + String eventType, + String title, + String message, + byte[] icon, + Map extras) { Notification notification = notifications.get(eventType); - if(notification == null || !notification.isActive()) + + if((notification == null) || !notification.isActive()) return null; - NotificationData data = new NotificationData(eventType, title, - message, extra, icon, tag); + NotificationData data + = new NotificationData(eventType, title, message, icon, extras); //cache the notification when the handlers are not yet ready if (notificationCache != null) @@ -515,67 +363,156 @@ public NotificationData fireNotification( } /** - * If there is a registered event notification of the given - * eventType and the event notification is currently activated, we - * go through the list of registered actions and execute them. - * - * @param eventType the type of the event that we'd like to fire a - * notification for. + * Notifies all registered NotificationChangeListeners that a + * NotificationActionTypeEvent has occurred. * - * @return An object referencing the notification. It may be used to stop a - * still running notification. Can be null if the eventType is - * unknown or the notification is not active. + * @param eventType the type of the event, which is one of ACTION_XXX + * constants declared in the NotificationActionTypeEvent class. + * @param sourceEventType the eventType, which is the parent of the + * action + * @param action the notification action */ - public NotificationData fireNotification(String eventType) + private void fireNotificationActionTypeEvent( + String eventType, + String sourceEventType, + NotificationAction action) { - return this.fireNotification(eventType, null, null, null, null, null); + NotificationActionTypeEvent event + = new NotificationActionTypeEvent( this, + eventType, + sourceEventType, + action); + + + for(NotificationChangeListener listener : changeListeners) + { + if (eventType.equals(ACTION_ADDED)) + { + listener.actionAdded(event); + } + else if (eventType.equals(ACTION_REMOVED)) + { + listener.actionRemoved(event); + } + else if (eventType.equals(ACTION_CHANGED)) + { + listener.actionChanged(event); + } + } } /** - * Saves the event notification given by these parameters through the - * ConfigurationService. + * Notifies all registered NotificationChangeListeners that a + * NotificationEventTypeEvent has occurred. * - * @param eventType the name of the event - * @param action the notification action to change - * @param isActive is the event active - * @param isDefault is it a default one + * @param eventType the type of the event, which is one of EVENT_TYPE_XXX + * constants declared in the NotificationEventTypeEvent class. + * @param sourceEventType the eventType, for which this event is + * about */ - private void saveNotification( String eventType, - NotificationAction action, - boolean isActive, - boolean isDefault) + private void fireNotificationEventTypeEvent(String eventType, + String sourceEventType) { - String eventTypeNodeName = null; - String actionTypeNodeName = null; - - List eventTypes = configService - .getPropertyNamesByPrefix(NOTIFICATIONS_PREFIX, true); + if (logger.isDebugEnabled()) + logger.debug("Dispatching NotificationEventType Change. Listeners=" + + changeListeners.size() + + " evt=" + eventType); - for (String eventTypeRootPropName : eventTypes) - { - String eType = configService.getString(eventTypeRootPropName); - if(eType.equals(eventType)) - eventTypeNodeName = eventTypeRootPropName; - } + NotificationEventTypeEvent event + = new NotificationEventTypeEvent(this, eventType, sourceEventType); - // If we didn't find the given event type in the configuration we save - // it here. - if(eventTypeNodeName == null) + for (NotificationChangeListener listener : changeListeners) { - eventTypeNodeName = NOTIFICATIONS_PREFIX - + ".eventType" - + Long.toString(System.currentTimeMillis()); - - configService.setProperty(eventTypeNodeName, eventType); + if (eventType.equals(EVENT_TYPE_ADDED)) + { + listener.eventTypeAdded(event); + } + else if (eventType.equals(EVENT_TYPE_REMOVED)) + { + listener.eventTypeRemoved(event); + } } + } - // if we set active/inactive for the whole event notification - if(action == null) + /** + * Gets a list of handler for the specified action type. + * + * @param actionType the type for which the list of handlers should be + * retrieved or null if all handlers shall be returned. + */ + public Iterable getActionHandlers(String actionType) + { + if (actionType != null) { - configService.setProperty( - eventTypeNodeName + ".active", - Boolean.toString(isActive)); - return; + NotificationHandler handler = handlers.get(actionType); + Set ret; + + if (handler == null) + ret = Collections.emptySet(); + else + ret = Collections.singleton(handler); + return ret; + } + else + return handlers.values(); + } + + /** + * Returns the notification action corresponding to the given + * eventType and actionType. + * + * @param eventType the type of the event that we'd like to retrieve. + * @param actionType the type of the action that we'd like to retrieve a + * descriptor for. + * @return the notification action of the action to be executed + * when an event of the specified type has occurred. + */ + public NotificationAction getEventNotificationAction( + String eventType, + String actionType) + { + Notification notification = notifications.get(eventType); + + return + (notification == null) ? null : notification.getAction(actionType); + } + + /** + * Getting a notification property directly from configuration service. + * Used to check do we have an updated version of already saved/edited + * notification configurations. Detects old configurations. + * + * @param eventType the event type + * @param action the action which property to check. + * @param property the property name without the action prefix. + * @return the property value or null if missing. + * @throws IllegalArgumentException when the event ot action is not + * found. + */ + private String getNotificationActionProperty( + String eventType, + NotificationAction action, + String property) + throws IllegalArgumentException + { + String eventTypeNodeName = null; + String actionTypeNodeName = null; + + List eventTypes = configService + .getPropertyNamesByPrefix(NOTIFICATIONS_PREFIX, true); + + for (String eventTypeRootPropName : eventTypes) + { + String eType = configService.getString(eventTypeRootPropName); + if(eType.equals(eventType)) + eventTypeNodeName = eventTypeRootPropName; + } + + // If we didn't find the given event type in the configuration + // there is not need to further check + if(eventTypeNodeName == null) + { + throw new IllegalArgumentException("Missing event type node"); } // Go through contained actions. @@ -591,81 +528,102 @@ private void saveNotification( String eventType, actionTypeNodeName = actionTypeRootPropName; } - Map configProperties = new HashMap(); - - // If we didn't find the given actionType in the configuration we save - // it here. + // If we didn't find the given actionType in the configuration + // there is no need to further check if(actionTypeNodeName == null) - { - actionTypeNodeName = actionPrefix - + ".actionType" - + Long.toString(System.currentTimeMillis()); + throw new IllegalArgumentException("Missing action type node"); - configProperties.put(actionTypeNodeName, action.getActionType()); - } + return + (String) + configService.getProperty(actionTypeNodeName + "." + property); + } - if(action instanceof SoundNotificationAction) - { - SoundNotificationAction soundAction - = (SoundNotificationAction) action; + /** + * Returns an iterator over a list of all events registered in this + * notification service. Each line in the returned list consists of + * a String, representing the name of the event (as defined by the plugin + * that registered it). + * + * @return an iterator over a list of all events registered in this + * notifications service + */ + public Iterable getRegisteredEvents() + { + return Collections.unmodifiableSet( + notifications.keySet()); + } - configProperties.put( - actionTypeNodeName + ".soundFileDescriptor", - soundAction.getDescriptor()); + /** + * Finds the EventNotification corresponding to the given + * eventType and returns its isActive status. + * + * @param eventType the name of the event (as defined by the plugin that's + * registered it) that we are checking. + * @return true if actions for the specified eventType + * are activated, false - otherwise. If the given + * eventType is not contained in the list of registered event + * types - returns false. + */ + public boolean isActive(String eventType) + { + Notification eventNotification + = notifications.get(eventType); - configProperties.put( - actionTypeNodeName + ".loopInterval", - soundAction.getLoopInterval()); + if(eventNotification == null) + return false; - configProperties.put( - actionTypeNodeName + ".isSoundNotificationEnabled", - soundAction.isSoundNotificationEnabled()); + return eventNotification.isActive(); + } - configProperties.put( - actionTypeNodeName + ".isSoundPlaybackEnabled", - soundAction.isSoundPlaybackEnabled()); + private boolean isDefault(String eventType, String actionType) + { + List eventTypes = configService + .getPropertyNamesByPrefix(NOTIFICATIONS_PREFIX, true); - configProperties.put( - actionTypeNodeName + ".isSoundPCSpeakerEnabled", - soundAction.isSoundPCSpeakerEnabled()); - } - else if(action instanceof PopupMessageNotificationAction) + for (String eventTypeRootPropName : eventTypes) { - PopupMessageNotificationAction messageAction - = (PopupMessageNotificationAction) action; + String eType + = configService.getString(eventTypeRootPropName); - configProperties.put( - actionTypeNodeName + ".defaultMessage", - messageAction.getDefaultMessage()); - } - else if(action instanceof LogMessageNotificationAction) - { - LogMessageNotificationAction logMessageAction - = (LogMessageNotificationAction) action; + if(!eType.equals(eventType)) + continue; - configProperties.put( - actionTypeNodeName + ".logType", - logMessageAction.getLogType()); - } - else if(action instanceof CommandNotificationAction) - { - CommandNotificationAction commandAction - = (CommandNotificationAction) action; + List actions = configService + .getPropertyNamesByPrefix( + eventTypeRootPropName + ".actions", true); - configProperties.put( - actionTypeNodeName + ".commandDescriptor", - commandAction.getDescriptor()); - } + for (String actionPropName : actions) + { + String aType + = configService.getString(actionPropName); - configProperties.put( - actionTypeNodeName + ".enabled", - Boolean.toString(isActive)); + if(!aType.equals(actionType)) + continue; - configProperties.put( - actionTypeNodeName + ".default", - Boolean.toString(isDefault)); + Object isDefaultdObj = + configService.getProperty(actionPropName + ".default"); - configService.setProperties(configProperties); + // if setting is missing we accept it is true + // this way we override old saved settings + if(isDefaultdObj == null) + return true; + else + return Boolean.parseBoolean((String)isDefaultdObj); + } + } + return true; + } + + private boolean isEnabled(String configProperty) + { + Object isEnabledObj = configService.getProperty(configProperty); + + // if setting is missing we accept it is true + // this way we not affect old saved settings + if(isEnabledObj == null) + return true; + else + return Boolean.parseBoolean((String)isEnabledObj); } /** @@ -766,175 +724,9 @@ else if(actionType.equals(ACTION_COMMAND)) } } - private boolean isEnabled(String configProperty) - { - Object isEnabledObj = configService.getProperty(configProperty); - - // if setting is missing we accept it is true - // this way we not affect old saved settings - if(isEnabledObj == null) - return true; - else - return Boolean.parseBoolean((String)isEnabledObj); - } - /** - * Finds the EventNotification corresponding to the given - * eventType and marks it as activated/deactivated. - * - * @param eventType the name of the event, which actions should be activated - * /deactivated. - * @param isActive indicates whether to activate or deactivate the actions - * related to the specified eventType. - */ - public void setActive(String eventType, boolean isActive) - { - Notification eventNotification - = notifications.get(eventType); - - if(eventNotification == null) - return; - - eventNotification.setActive(isActive); - saveNotification(eventType, null, isActive, false); - } - - /** - * Finds the EventNotification corresponding to the given - * eventType and returns its isActive status. - * - * @param eventType the name of the event (as defined by the plugin that's - * registered it) that we are checking. - * @return true if actions for the specified eventType - * are activated, false - otherwise. If the given - * eventType is not contained in the list of registered event - * types - returns false. - */ - public boolean isActive(String eventType) - { - Notification eventNotification - = notifications.get(eventType); - - if(eventNotification == null) - return false; - - return eventNotification.isActive(); - } - - /** - * Notifies all registered NotificationChangeListeners that a - * NotificationEventTypeEvent has occurred. - * - * @param eventType the type of the event, which is one of EVENT_TYPE_XXX - * constants declared in the NotificationEventTypeEvent class. - * @param sourceEventType the eventType, for which this event is - * about - */ - private void fireNotificationEventTypeEvent(String eventType, - String sourceEventType) - { - if (logger.isDebugEnabled()) - logger.debug("Dispatching NotificationEventType Change. Listeners=" - + changeListeners.size() - + " evt=" + eventType); - - NotificationEventTypeEvent event - = new NotificationEventTypeEvent(this, eventType, sourceEventType); - - for (NotificationChangeListener listener : changeListeners) - { - if (eventType.equals(EVENT_TYPE_ADDED)) - { - listener.eventTypeAdded(event); - } - else if (eventType.equals(EVENT_TYPE_REMOVED)) - { - listener.eventTypeRemoved(event); - } - } - } - - /** - * Notifies all registered NotificationChangeListeners that a - * NotificationActionTypeEvent has occurred. - * - * @param eventType the type of the event, which is one of ACTION_XXX - * constants declared in the NotificationActionTypeEvent class. - * @param sourceEventType the eventType, which is the parent of the - * action - * @param action the notification action - */ - private void fireNotificationActionTypeEvent( - String eventType, - String sourceEventType, - NotificationAction action) - { - NotificationActionTypeEvent event - = new NotificationActionTypeEvent( this, - eventType, - sourceEventType, - action); - - - for(NotificationChangeListener listener : changeListeners) - { - if (eventType.equals(ACTION_ADDED)) - { - listener.actionAdded(event); - } - else if (eventType.equals(ACTION_REMOVED)) - { - listener.actionRemoved(event); - } - else if (eventType.equals(ACTION_CHANGED)) - { - listener.actionChanged(event); - } - } - } - - private boolean isDefault(String eventType, String actionType) - { - List eventTypes = configService - .getPropertyNamesByPrefix(NOTIFICATIONS_PREFIX, true); - - for (String eventTypeRootPropName : eventTypes) - { - String eType - = configService.getString(eventTypeRootPropName); - - if(!eType.equals(eventType)) - continue; - - List actions = configService - .getPropertyNamesByPrefix( - eventTypeRootPropName + ".actions", true); - - for (String actionPropName : actions) - { - String aType - = configService.getString(actionPropName); - - if(!aType.equals(actionType)) - continue; - - Object isDefaultdObj = - configService.getProperty(actionPropName + ".default"); - - // if setting is missing we accept it is true - // this way we override old saved settings - if(isDefaultdObj == null) - return true; - else - return Boolean.parseBoolean((String)isDefaultdObj); - } - } - return true; - } - - /** - * Creates a new default EventNotification or obtains the - * corresponding existing one and registers a new action in it. + * Creates a new default EventNotification or obtains the + * corresponding existing one and registers a new action in it. * * @param eventType the name of the event (as defined by the plugin that's * registering it) that we are setting an action for. @@ -1114,105 +906,230 @@ else if (actionType.equals(ACTION_COMMAND)) } /** - * Checking an action when it is edited (property .default=false). - * Checking for older versions of the property. If it is older one - * we migrate it to new configuration using the default values. - * - * @param eventType the event type. - * @param defaultAction the default action which values we will use. + * Creates a new EventNotification or obtains the corresponding + * existing one and registers a new action in it. + * + * @param eventType the name of the event (as defined by the plugin that's + * registering it) that we are setting an action for. + * @param action the NotificationAction responsible for + * handling the given actionType */ - private void checkDefaultAgainstLoadedNotification - (String eventType, NotificationAction defaultAction) + public void registerNotificationForEvent( String eventType, + NotificationAction action) { - // checking for new sound action properties - if(defaultAction instanceof SoundNotificationAction) + Notification notification = null; + + if(notifications.containsKey(eventType)) + notification = notifications.get(eventType); + else { - SoundNotificationAction soundDefaultAction - = (SoundNotificationAction)defaultAction; - SoundNotificationAction soundAction = (SoundNotificationAction) - getEventNotificationAction(eventType, ACTION_SOUND); + notification = new Notification(eventType); + notifications.put(eventType, notification); - boolean isSoundNotificationEnabledPropExist - = getNotificationActionProperty( - eventType, - defaultAction, - "isSoundNotificationEnabled") != null; + this.fireNotificationEventTypeEvent( + EVENT_TYPE_ADDED, eventType); + } - if(!isSoundNotificationEnabledPropExist) - { - soundAction.setSoundNotificationEnabled( - soundDefaultAction.isSoundNotificationEnabled()); - } + Object existingAction = notification.addAction(action); - boolean isSoundPlaybackEnabledPropExist - = getNotificationActionProperty( - eventType, - defaultAction, - "isSoundPlaybackEnabled") != null; + // We fire the appropriate event depending on whether this is an + // already existing actionType or a new one. + if (existingAction != null) + { + fireNotificationActionTypeEvent( + ACTION_CHANGED, + eventType, + action); + } + else + { + fireNotificationActionTypeEvent( + ACTION_ADDED, + eventType, + action); + } - if(!isSoundPlaybackEnabledPropExist) - { - soundAction.setSoundPlaybackEnabled( - soundDefaultAction.isSoundPlaybackEnabled()); - } + // Save the notification through the ConfigurationService. + this.saveNotification(eventType, + action, + true, + false); + } - boolean isSoundPCSpeakerEnabledPropExist - = getNotificationActionProperty( - eventType, - defaultAction, - "isSoundPCSpeakerEnabled") != null; + /** + * Creates a new EventNotification or obtains the corresponding + * existing one and registers a new action in it. + * + * @param eventType the name of the event (as defined by the plugin that's + * registering it) that we are setting an action for. + * @param actionType the type of the action that is to be executed when the + * specified event occurs (could be one of the ACTION_XXX fields). + * @param actionDescriptor a String containing a description of the action + * (a URI to the sound file for audio notifications or a command line for + * exec action types) that should be executed when the action occurs. + * @param defaultMessage the default message to use if no specific message + * has been provided when firing the notification. + */ + public void registerNotificationForEvent( String eventType, + String actionType, + String actionDescriptor, + String defaultMessage) + { + if (logger.isDebugEnabled()) + logger.debug("Registering event " + eventType + "/" + + actionType + "/" + actionDescriptor + "/" + defaultMessage); - if(!isSoundPCSpeakerEnabledPropExist) - { - soundAction.setSoundPCSpeakerEnabled( - soundDefaultAction.isSoundPCSpeakerEnabled()); - } + if (actionType.equals(ACTION_SOUND)) + { + Notification notification = defaultNotifications.get(eventType); + SoundNotificationAction action = + (SoundNotificationAction) notification.getAction(ACTION_SOUND); + registerNotificationForEvent ( + eventType, + new SoundNotificationAction( + actionDescriptor, + action.getLoopInterval())); + } + else if (actionType.equals(ACTION_LOG_MESSAGE)) + { + registerNotificationForEvent (eventType, + new LogMessageNotificationAction( + LogMessageNotificationAction.INFO_LOG_TYPE)); + } + else if (actionType.equals(ACTION_POPUP_MESSAGE)) + { + registerNotificationForEvent (eventType, + new PopupMessageNotificationAction(defaultMessage)); + } + else if (actionType.equals(ACTION_COMMAND)) + { + registerNotificationForEvent (eventType, + new CommandNotificationAction(actionDescriptor)); + } + } - boolean fixDialingLoop = false; + /** + * Removes an object that executes the actual action of notification action. + * @param actionType The handler type to remove. + */ + public void removeActionHandler(String actionType) + { + if(actionType == null) + throw new IllegalArgumentException("actionType cannot be null"); - // hack to fix wrong value:just check whether loop for outgoing call - // (dialing) has gone into config as 0, should be -1 - if(eventType.equals("Dialing") - && soundAction.getLoopInterval() == 0) - { - soundAction.setLoopInterval( - soundDefaultAction.getLoopInterval()); - fixDialingLoop = true; - } + synchronized(handlers) + { + handlers.remove(actionType); + } + } - if(!(isSoundNotificationEnabledPropExist - && isSoundPCSpeakerEnabledPropExist - && isSoundPlaybackEnabledPropExist) - || fixDialingLoop) - { - // this check is done only when the notification - // is edited and is not default - saveNotification( - eventType, - soundAction, - soundAction.isEnabled(), - false); - } + /** + * Removes the EventNotification corresponding to the given + * eventType from the table of registered event notifications. + * + * @param eventType the name of the event (as defined by the plugin that's + * registering it) to be removed. + */ + public void removeEventNotification(String eventType) + { + notifications.remove(eventType); + + this.fireNotificationEventTypeEvent( + EVENT_TYPE_REMOVED, eventType); + } + + /** + * Removes the given actionType from the list of actions registered for the + * given eventType. + * + * @param eventType the name of the event (as defined by the plugin that's + * registering it) for which we'll remove the notification. + * @param actionType the type of the action that is to be executed when the + * specified event occurs (could be one of the ACTION_XXX fields). + */ + public void removeEventNotificationAction( String eventType, + String actionType) + { + Notification notification + = notifications.get(eventType); + + if(notification == null) + return; + + NotificationAction action = notification.getAction(actionType); + + if(action == null) + return; + + notification.removeAction(actionType); + + saveNotification( + eventType, + action, + false, + false); + + fireNotificationActionTypeEvent( + ACTION_REMOVED, + eventType, + action); + } + + /** + * Removes the given listener from the list of change listeners. + * + * @param listener the listener that we'd like to remove + */ + public void removeNotificationChangeListener( + NotificationChangeListener listener) + { + synchronized (changeListeners) + { + changeListeners.remove(listener); } } /** - * Getting a notification property directly from configuration service. - * Used to check do we have an updated version of already saved/edited - * notification configurations. Detects old configurations. - * - * @param eventType the event type - * @param action the action which property to check. - * @param property the property name without the action prefix. - * @return the property value or null if missing. - * @throws IllegalArgumentException when the event ot action is not - * found. + * Deletes all registered events and actions + * and registers and saves the default events as current. */ - private String getNotificationActionProperty( - String eventType, - NotificationAction action, - String property) - throws IllegalArgumentException + public void restoreDefaults() + { + for (String eventType : new Vector(notifications.keySet())) + { + Notification notification = notifications.get(eventType); + + for (String actionType + : new Vector(notification.getActions().keySet())) + removeEventNotificationAction(eventType, actionType); + + removeEventNotification(eventType); + } + + for (Map.Entry entry + : defaultNotifications.entrySet()) + { + String eventType = entry.getKey(); + Notification notification = entry.getValue(); + + for (NotificationAction action : notification.getActions().values()) + registerNotificationForEvent(eventType, action); + } + } + + /** + * Saves the event notification given by these parameters through the + * ConfigurationService. + * + * @param eventType the name of the event + * @param action the notification action to change + * @param isActive is the event active + * @param isDefault is it a default one + */ + private void saveNotification( String eventType, + NotificationAction action, + boolean isActive, + boolean isDefault) { String eventTypeNodeName = null; String actionTypeNodeName = null; @@ -1227,11 +1144,24 @@ private String getNotificationActionProperty( eventTypeNodeName = eventTypeRootPropName; } - // If we didn't find the given event type in the configuration - // there is not need to further check + // If we didn't find the given event type in the configuration we save + // it here. if(eventTypeNodeName == null) { - throw new IllegalArgumentException("Missing event type node"); + eventTypeNodeName = NOTIFICATIONS_PREFIX + + ".eventType" + + Long.toString(System.currentTimeMillis()); + + configService.setProperty(eventTypeNodeName, eventType); + } + + // if we set active/inactive for the whole event notification + if(action == null) + { + configService.setProperty( + eventTypeNodeName + ".active", + Boolean.toString(isActive)); + return; } // Go through contained actions. @@ -1249,43 +1179,121 @@ private String getNotificationActionProperty( Map configProperties = new HashMap(); - // If we didn't find the given actionType in the configuration - // there is no need to further check + // If we didn't find the given actionType in the configuration we save + // it here. if(actionTypeNodeName == null) { - throw new IllegalArgumentException("Missing action type node"); + actionTypeNodeName = actionPrefix + + ".actionType" + + Long.toString(System.currentTimeMillis()); + + configProperties.put(actionTypeNodeName, action.getActionType()); } - return - (String)configService - .getProperty(actionTypeNodeName + "." + property); + if(action instanceof SoundNotificationAction) + { + SoundNotificationAction soundAction + = (SoundNotificationAction) action; + + configProperties.put( + actionTypeNodeName + ".soundFileDescriptor", + soundAction.getDescriptor()); + + configProperties.put( + actionTypeNodeName + ".loopInterval", + soundAction.getLoopInterval()); + + configProperties.put( + actionTypeNodeName + ".isSoundNotificationEnabled", + soundAction.isSoundNotificationEnabled()); + + configProperties.put( + actionTypeNodeName + ".isSoundPlaybackEnabled", + soundAction.isSoundPlaybackEnabled()); + + configProperties.put( + actionTypeNodeName + ".isSoundPCSpeakerEnabled", + soundAction.isSoundPCSpeakerEnabled()); + } + else if(action instanceof PopupMessageNotificationAction) + { + PopupMessageNotificationAction messageAction + = (PopupMessageNotificationAction) action; + + configProperties.put( + actionTypeNodeName + ".defaultMessage", + messageAction.getDefaultMessage()); + } + else if(action instanceof LogMessageNotificationAction) + { + LogMessageNotificationAction logMessageAction + = (LogMessageNotificationAction) action; + + configProperties.put( + actionTypeNodeName + ".logType", + logMessageAction.getLogType()); + } + else if(action instanceof CommandNotificationAction) + { + CommandNotificationAction commandAction + = (CommandNotificationAction) action; + + configProperties.put( + actionTypeNodeName + ".commandDescriptor", + commandAction.getDescriptor()); + } + + configProperties.put( + actionTypeNodeName + ".enabled", + Boolean.toString(isActive)); + + configProperties.put( + actionTypeNodeName + ".default", + Boolean.toString(isDefault)); + + configService.setProperties(configProperties); } /** - * Deletes all registered events and actions - * and registers and saves the default events as current. + * Finds the EventNotification corresponding to the given + * eventType and marks it as activated/deactivated. + * + * @param eventType the name of the event, which actions should be activated + * /deactivated. + * @param isActive indicates whether to activate or deactivate the actions + * related to the specified eventType. */ - public void restoreDefaults() + public void setActive(String eventType, boolean isActive) { - for (String eventType : new Vector(notifications.keySet())) - { - Notification notification = notifications.get(eventType); + Notification eventNotification + = notifications.get(eventType); - for (String actionType - : new Vector(notification.getActions().keySet())) - removeEventNotificationAction(eventType, actionType); + if(eventNotification == null) + return; - removeEventNotification(eventType); - } + eventNotification.setActive(isActive); + saveNotification(eventType, null, isActive, false); + } - for (Map.Entry entry - : defaultNotifications.entrySet()) - { - String eventType = entry.getKey(); - Notification notification = entry.getValue(); + /** + * Stops a notification if notification is continuous, like playing sounds + * in loop. Do nothing if there are no such events currently processing. + * + * @param data the data that has been returned when firing the event.. + */ + public void stopNotification(NotificationData data) + { + Iterable soundHandlers + = getActionHandlers(NotificationAction.ACTION_SOUND); - for (NotificationAction action : notification.getActions().values()) - registerNotificationForEvent(eventType, action); + // There could be no sound action handler for this event type + if (soundHandlers != null) + { + for (NotificationHandler handler : soundHandlers) + { + if (handler instanceof SoundNotificationHandler) + ((SoundNotificationHandler) handler).stop(data); + } } } } diff --git a/src/net/java/sip/communicator/service/notification/PopupMessageNotificationHandler.java b/src/net/java/sip/communicator/service/notification/PopupMessageNotificationHandler.java index e8b57161f..d9cdfd018 100644 --- a/src/net/java/sip/communicator/service/notification/PopupMessageNotificationHandler.java +++ b/src/net/java/sip/communicator/service/notification/PopupMessageNotificationHandler.java @@ -29,11 +29,12 @@ public interface PopupMessageNotificationHandler * appropriate * @param tag additional info to be used by the notification handler */ - public void popupMessage(PopupMessageNotificationAction action, - String title, - String message, - byte[] icon, - Object tag); + public void popupMessage( + PopupMessageNotificationAction action, + String title, + String message, + byte[] icon, + Object tag); /** * Adds a listener for SystrayPopupMessageEvents posted when user diff --git a/src/net/java/sip/communicator/util/dns/ConfigurableDnssecResolver.java b/src/net/java/sip/communicator/util/dns/ConfigurableDnssecResolver.java index 0a5f4fb8a..cae92fcd4 100644 --- a/src/net/java/sip/communicator/util/dns/ConfigurableDnssecResolver.java +++ b/src/net/java/sip/communicator/util/dns/ConfigurableDnssecResolver.java @@ -142,9 +142,10 @@ protected void validateMessage(SecureMessage msg) || last.before(new Date(new Date().getTime() - 1000*60*5))) { DnsUtilActivator.getNotificationService().fireNotification( - EVENT_TYPE, - R.getI18NString("util.dns.INSECURE_ANSWER_TITLE"), - text, null, null); + EVENT_TYPE, + R.getI18NString("util.dns.INSECURE_ANSWER_TITLE"), + text, + null); lastNotifications.put(text, new Date()); } throw new DnssecRuntimeException(text); diff --git a/test/net/java/sip/communicator/slick/popupmessagehandler/TestPopupMessageHandler.java b/test/net/java/sip/communicator/slick/popupmessagehandler/TestPopupMessageHandler.java index babec92d8..01b6f3b00 100644 --- a/test/net/java/sip/communicator/slick/popupmessagehandler/TestPopupMessageHandler.java +++ b/test/net/java/sip/communicator/slick/popupmessagehandler/TestPopupMessageHandler.java @@ -10,11 +10,9 @@ import net.java.sip.communicator.service.notification.*; import net.java.sip.communicator.service.systray.*; import net.java.sip.communicator.service.systray.event.*; -import net.java.sip.communicator.util.*; import org.osgi.framework.*; - /** * Test suite for the popup message handler interface. * @author Symphorien Wanko @@ -22,10 +20,6 @@ public class TestPopupMessageHandler extends TestCase { - /** Logger for this class */ - private static final Logger logger - = Logger.getLogger(TestPopupMessageHandler.class); - /** * the SystrayService reference we will get from bundle * context to register ours handlers @@ -121,7 +115,6 @@ public void testNotificationHandling() { serviceReference = bc.getServiceReference( NotificationService.class.getName()); - notificationService = (NotificationService) bc.getService(serviceReference); @@ -129,7 +122,6 @@ public void testNotificationHandling() NotificationAction.ACTION_POPUP_MESSAGE, messageStart, messageStart, - null, null); }