/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.plugin.notificationwiring;

import java.lang.ref.*;
import java.util.*;
import java.util.concurrent.*;

import net.java.sip.communicator.service.contactlist.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.notification.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.resources.*;
import net.java.sip.communicator.util.*;

import org.apache.commons.lang3.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.event.*;
import org.jitsi.service.neomedia.recording.*;
import org.jitsi.service.resources.*;
import org.osgi.framework.*;

/**
 * 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 AdHocChatRoomMessageListener,
               CallChangeListener,
               CallListener,
               CallPeerConferenceListener,
               CallPeerListener,
               CallPeerSecurityListener,
               ChatRoomMessageListener,
               FileTransferListener,
               LocalUserAdHocChatRoomPresenceListener,
               LocalUserChatRoomPresenceListener,
               MessageListener,
               Recorder.Listener,
               ServiceListener,
               TypingNotificationsListener
{
    /**
     * Default event type for a busy call.
     */
    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 security error on a call.
     */
    public static final String CALL_SECURITY_ERROR = "CallSecurityError";

    /**
     * Default event type for activated security on a call.
     */
    public static final String CALL_SECURITY_ON = "CallSecurityOn";

    /**
     * The image used, when a contact has no photo specified.
     */
    public static final ImageID DEFAULT_USER_PHOTO
        = new ImageID("service.gui.DEFAULT_USER_PHOTO");

    /**
     * Default event type for dialing.
     */
    public static final String DIALING = "Dialing";

    /**
     * Default event type for hanging up calls.
     */
    public static final String HANG_UP = "HangUp";

    /**
     * Default event type for receiving calls (incoming calls).
     */
    public static final String INCOMING_CALL = "IncomingCall";

    /**
     * Default event type for incoming file transfers.
     */
    public static final String INCOMING_FILE = "IncomingFile";

    /**
     * Default event type for receiving messages.
     */
    public static final String INCOMING_MESSAGE = "IncomingMessage";

    /**
     * HTML content type.
     */
    private static final String HTML_CONTENT_TYPE = "text/html";

    /**
     * The <tt>Logger</tt> used by the <tt>NotificationManager</tt> class and
     * its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(NotificationManager.class);

    /**
     * Default event type for outgoing calls.
     */
    public static final String OUTGOING_CALL = "OutgoingCall";

    /**
     * Default event type for
     * proactive notifications (typing notifications when chatting).
     */
    public static final String PROACTIVE_NOTIFICATION = "ProactiveNotification";

    /**
     * Default event type when a secure message received.
     */
    public static final String SECURITY_MESSAGE = "SecurityMessage";

    /**
     * Fires a chat message notification for the given event type through the
     * <tt>NotificationService</tt>.
     *
     * @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 messageUID the message UID
     */
    public static void fireChatNotification(Object chatContact,
                                            String eventType,
                                            String messageTitle,
                                            String message,
                                            String messageUID)
    {
        NotificationService notificationService
            = NotificationWiringActivator.getNotificationService();

        if(notificationService == null)
            return;

        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, messageUID);

            contactIcon = contact.getImage();
            if(contactIcon == null)
            {
                contactIcon =
                    NotificationWiringActivator.getImageLoaderService()
                        .getImageBytes(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);
            }
        }

        Map<String,Object> extras = new HashMap<String,Object>();

        extras.put(
                NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA,
                chatContact);
        notificationService.fireNotification(
                eventType,
                messageTitle,
                message,
                contactIcon,
                extras);

        if(popupActionHandler != null)
            popupActionHandler.setEnabled(true);
    }

    /**
     * Fires a notification for the given event type through the
     * <tt>NotificationService</tt>. The event type is one of the static
     * constants defined in the <tt>NotificationManager</tt> class.
     * <p>
     * <b>Note</b>: 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 <tt>NotificationService</tt> so that sounds are played once
     * only.
     * </p>
     *
     * @param eventType the event type for which we want to fire a notification
     */
    private static void fireNotification(String eventType)
    {
        NotificationService notificationService
            = NotificationWiringActivator.getNotificationService();

        if (notificationService != null)
            notificationService.fireNotification(eventType);
    }

    /**
     * Fires a notification for the given event type through the
     * <tt>NotificationService</tt>. The event type is one of the static
     * constants defined in the <tt>NotificationManager</tt> class.
     *
     * @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.
     */
    private static NotificationData fireNotification(
            String eventType,
            Callable<Boolean> loopCondition)
    {
        return fireNotification(eventType, null, null, null, loopCondition);
    }

    /**
     * Fires a notification through the <tt>NotificationService</tt> with a
     * specific event type, a specific message title and a specific message.
     * <p>
     * <b>Note</b>: 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 <tt>NotificationService</tt> so that sounds are played once
     * only.
     * </p>
     *
     * @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
     * <tt>NotificationService</tt>.
     *
     * @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 <tt>cmdargs</tt> 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<String,String> cmdargs,
            Callable<Boolean> loopCondition)
    {
        NotificationService notificationService
            = NotificationWiringActivator.getNotificationService();

        if (notificationService == null)
            return null;
        else
        {
            Map<String,Object> extras = new HashMap<String,Object>();

            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);
        }
    }

    /**
     * Returns all <tt>ProtocolProviderFactory</tt>s obtained from the bundle
     * context.
     *
     * @return all <tt>ProtocolProviderFactory</tt>s obtained from the bundle
     *         context
     */
    public static Map<Object, ProtocolProviderFactory>
        getProtocolProviderFactories()
    {
        ServiceReference[] serRefs = null;
        try
        {
            // get all registered provider factories
            serRefs
                = NotificationWiringActivator.bundleContext.getServiceReferences(
                        ProtocolProviderFactory.class.getName(),
                        null);
        }
        catch (InvalidSyntaxException e)
        {
            logger.error("NotificationManager : " + e);
        }

        Map<Object, ProtocolProviderFactory>
            providerFactoriesMap = new Hashtable<Object, ProtocolProviderFactory>();

        if (serRefs != null)
        {
            for (ServiceReference serRef : serRefs)
            {
                ProtocolProviderFactory providerFactory
                    = (ProtocolProviderFactory)
                        NotificationWiringActivator.bundleContext.getService(serRef);

                providerFactoriesMap.put(
                        serRef.getProperty(ProtocolProviderFactory.PROTOCOL),
                        providerFactory);
            }
        }
        return providerFactoriesMap;
    }

    /**
     * Returns all protocol providers currently registered.
     * @return all protocol providers currently registered.
     */
    public static List<ProtocolProviderService>
        getProtocolProviders()
    {
        ServiceReference[] serRefs = null;
        try
        {
            // get all registered provider factories
            serRefs
                = NotificationWiringActivator.bundleContext.getServiceReferences(
                        ProtocolProviderService.class.getName(),
                        null);
        }
        catch (InvalidSyntaxException e)
        {
            logger.error("NotificationManager : " + e);
        }

        List<ProtocolProviderService>
            providersList = new ArrayList<ProtocolProviderService>();

        if (serRefs != null)
        {
            for (ServiceReference serRef : serRefs)
            {
                ProtocolProviderService pp
                    = (ProtocolProviderService)
                        NotificationWiringActivator.bundleContext.getService(serRef);

                providersList.add(pp);
            }
        }
        return providersList;
    }

    /**
     * Determines whether a specific <code>ChatRoom</code> 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 <code>ChatRoom</code> is
     * obviously not private and (2) a <code>ChatRoom</code> is private if it
     * has only one <code>ChatRoomMember</code> who is not the local user.
     *
     * @param chatRoom
     *            the <code>ChatRoom</code> to be determined as private or not
     * @return <tt>true</tt> if the specified <code>ChatRoom</code> is private;
     *         otherwise, <tt>false</tt>
     */
    private static boolean isPrivate(ChatRoom chatRoom)
    {
        if (!chatRoom.isSystem()
            && chatRoom.isJoined()
            && (chatRoom.getMembersCount() == 1))
        {
            String nickname = chatRoom.getUserNickname();

            if (nickname != null)
            {
                for (ChatRoomMember member : chatRoom.getMembers())
                    if (nickname.equals(member.getName()))
                        return false;
                return true;
            }
        }
        return false;
    }

    /**
     * Determines whether the <tt>DIALING</tt> sound notification should be
     * played for a specific <tt>CallPeer</tt>.
     *
     * @param weakPeer the <tt>CallPeer</tt> for which it is to be determined
     * whether the <tt>DIALING</tt> sound notification is to be played
     * @return <tt>true</tt> if the <tt>DIALING</tt> sound notification should
     * be played for the specified <tt>callPeer</tt>; otherwise, <tt>false</tt>
     */
    private static boolean shouldPlayDialingSound(
            WeakReference<CallPeer> weakPeer)
    {
        CallPeer peer = weakPeer.get();

        if (peer == null)
            return false;

        Call call = peer.getCall();

        if (call == null)
            return false;

        CallConference conference = call.getConference();

        if (conference == null)
            return false;

        boolean play = false;

        for (Call aCall : conference.getCalls())
        {
            Iterator<? extends CallPeer> peerIter = aCall.getCallPeers();

            while (peerIter.hasNext())
            {
                CallPeer aPeer = peerIter.next();

                /*
                 * The peer is still in a call/telephony conference so the
                 * DIALING sound may need to be played.
                 */
                if (peer == aPeer)
                    play = true;

                CallPeerState state = peer.getState();

                if (CallPeerState.INITIATING_CALL.equals(state)
                        || CallPeerState.CONNECTING.equals(state))
                {
                    /*
                     * The DIALING sound should be played for the first CallPeer
                     * only.
                     */
                    if (peer != aPeer)
                        return false;
                }
                else
                {
                    /*
                     * The DIALING sound should not be played if there is a
                     * CallPeer which does not require the DIALING sound to be
                     * played.
                     */
                    return false;
                }
            }
        }

        return play;
    }

    /**
     * Stores notification references to stop them if a notification has expired
     * (e.g. to stop the dialing sound).
     */
    private final Map<Call, NotificationData> callNotifications
        = new WeakHashMap<Call, NotificationData>();

    /**
     * The pseudo timer which is used to delay multiple typing notifications
     * before receiving the message.
     */
    private final Map<Contact, Long> proactiveTimer
        = new HashMap<Contact, Long>();

    /**
     * Implements CallListener.callEnded. Stops sounds that are playing at
     * the moment if there're any.
     *
     * @param ev the <tt>CallEvent</tt>
     */
    public void callEnded(CallEvent ev)
    {
        try
        {
            // Stop all telephony related sounds.
//            stopAllTelephonySounds();
            NotificationData notification
                = callNotifications.get(ev.getSourceCall());

            if (notification != null)
                stopSound(notification);

            // Play the hangup sound.
            fireNotification(HANG_UP);
        }
        catch(Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
            {
                logger.error(
                        "An error occurred while trying to notify"
                            + " about the end of a call.",
                        t);
            }
        }
    }

    /**
     * Implements the <tt>CallChangeListener.callPeerAdded</tt> method.
     * @param evt the <tt>CallPeerEvent</tt> that notifies us for the change
     */
    public void callPeerAdded(CallPeerEvent evt)
    {
        CallPeer peer = evt.getSourceCallPeer();

        if(peer == null)
            return;

        peer.addCallPeerListener(this);
        peer.addCallPeerSecurityListener(this);
        peer.addCallPeerConferenceListener(this);
    }

    /**
     * Implements the <tt>CallChangeListener.callPeerRemoved</tt> method.
     * @param evt the <tt>CallPeerEvent</tt> that has been triggered
     */
    public void callPeerRemoved(CallPeerEvent evt)
    {
        CallPeer peer = evt.getSourceCallPeer();

        if(peer == null)
            return;

        peer.removeCallPeerListener(this);
        peer.removeCallPeerSecurityListener(this);
        peer.addCallPeerConferenceListener(this);
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void callStateChanged(CallChangeEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void conferenceFocusChanged(CallPeerConferenceEvent ev) {}

    /**
     * Indicates that the given conference member has been added to the given
     * peer.
     *
     * @param conferenceEvent the event
     */
    public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent)
    {
        try
        {
            CallPeer peer
                = conferenceEvent
                    .getConferenceMember()
                        .getConferenceFocusCallPeer();

            if(peer.getConferenceMemberCount() > 0)
            {
                CallPeerSecurityStatusEvent securityEvent
                    = peer.getCurrentSecuritySettings();

                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 conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {}

    /**
     * {@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 <tt>FileTransferRequestEvent</tt>
     * @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(),
                    request.getID());
        }
        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 <tt>ProtocolProviderService</tt>
     */
    private void handleProviderAdded(ProtocolProviderService protocolProvider)
    {
        if(!protocolProvider.getAccountID().isEnabled())
            return;

        Map<String, OperationSet> 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))
        {
            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.addTypingNotificationsListener(this);
        }

        // Obtain file transfer operation set.
        OperationSetFileTransfer fileTransferOpSet
            = protocolProvider.getOperationSet(OperationSetFileTransfer.class);

        if (fileTransferOpSet != null)
        {
            fileTransferOpSet.addFileTransferListener(this);
        }

        OperationSetMultiUserChat multiChatOpSet
            = protocolProvider.getOperationSet(OperationSetMultiUserChat.class);

        if (multiChatOpSet != null)
        {
            multiChatOpSet.addPresenceListener(this);
        }

        OperationSetAdHocMultiUserChat multiAdHocChatOpSet
            = protocolProvider.getOperationSet(OperationSetAdHocMultiUserChat.class);

        if (multiAdHocChatOpSet != null)
        {
            multiAdHocChatOpSet.addPresenceListener(this);
        }

        OperationSetBasicTelephony<?> basicTelephonyOpSet
            = protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

        if (basicTelephonyOpSet != null)
        {
            basicTelephonyOpSet.addCallListener(this);
        }
    }

    /**
     * Removes all listeners related to the given protocol provider.
     *
     * @param protocolProvider the <tt>ProtocolProviderService</tt>
     */
    private void handleProviderRemoved(ProtocolProviderService protocolProvider)
    {
        Map<String, OperationSet> 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.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);
        }

        // Obtain file transfer operation set.
        OperationSetFileTransfer fileTransferOpSet
            = protocolProvider.getOperationSet(OperationSetFileTransfer.class);

        if (fileTransferOpSet != null)
        {
            fileTransferOpSet.removeFileTransferListener(this);
        }

        OperationSetMultiUserChat multiChatOpSet
            = protocolProvider.getOperationSet(OperationSetMultiUserChat.class);

        if (multiChatOpSet != null)
        {
            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);
        }
    }

    /**
     * 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 ev the <tt>CallEvent</tt>
     */
    public void incomingCallReceived(CallEvent ev)
    {
        try
        {
            Call call = ev.getSourceCall();
            CallPeer peer = call.getCallPeers().next();
            Map<String,String> peerInfo = new HashMap<String, String>();
            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());

            /*
             * The loopCondition will stay with the notification sound until the
             * latter is stopped. If by any chance the sound fails to stop by
             * the time the call is no longer referenced, do try to stop it
             * then. That's why the loopCondition will weakly reference the
             * call.
             */
            final WeakReference<Call> weakCall = new WeakReference<Call>(call);
            NotificationData notification
                = fireNotification(
                        INCOMING_CALL,
                        "",
                        ev.isDesktopStreaming() ?
                            NotificationWiringActivator.getResources()
                                    .getI18NString(
                                            "service.gui.INCOMING_SCREEN_SHARE",
                                            new String[] { peerName })
                            : NotificationWiringActivator.getResources()
                                    .getI18NString(
                                            "service.gui.INCOMING_CALL",
                                            new String[] { peerName }),
                        peerInfo,
                        new Callable<Boolean>()
                        {
                            public Boolean call()
                            {
                                Call call = weakCall.get();

                                if (call == null)
                                    return false;

                                /*
                                 * INCOMING_CALL should be played for a Call
                                 * only while there is a CallPeer in the
                                 * INCOMING_CALL state.
                                 */
                                Iterator<? extends CallPeer> 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);

            call.addCallChangeListener(this);

            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);
            }
        }
    }

    /**
     * Initialize, register default notifications and start listening for
     * new protocols or removed one and find any that are already registered.
     */
    void init()
    {
        registerDefaultNotifications();

        // listens for new protocols
        NotificationWiringActivator.bundleContext.addServiceListener(this);

        // enumerate currently registered protocols
        for(ProtocolProviderService pp : getProtocolProviders())
        {
            handleProviderAdded(pp);
        }

        NotificationWiringActivator.getMediaService().addRecorderListener(this);
    }

    /**
     * Checks if the contained call is a conference call.
     *
     * @param call the call to check
     * @return <code>true</code> if the contained <tt>Call</tt> is a conference
     * call, otherwise - returns <code>false</code>.
     */
    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<? extends CallPeer> callPeers = call.getCallPeers();

        while (callPeers.hasNext())
        {
            CallPeer callPeer = callPeers.next();

            if (callPeer.isConferenceFocus())
                return true;
        }

        // the call can have two peers at the same time and there is no one
        // is conference focus. This is situation when some one has made an
        // attended transfer and has transfered us. We have one call with two
        // peers the one we are talking to and the one we have been transfered
        // to. And the first one is been hanguped and so the call passes through
        // conference call fo a moment and than go again to one to one call.
        return call.getCallPeerCount() > 1;
    }

    /**
     * Implements the
     * <tt>LocalUserAdHocChatRoomPresenceListener.localUserPresenceChanged</tt>
     * method
     *
     * @param evt the <tt>LocalUserAdHocChatRoomPresenceChangeEvent</tt> that
     * notified us of a presence change
     */
    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);
        }
    }

    /**
     * Implements the
     * <tt>LocalUserChatRoomPresenceListener.localUserPresenceChanged</tt>
     * method.
     * @param evt the <tt>LocalUserChatRoomPresenceChangeEvent</tt> that
     * notified us
     */
    public void localUserPresenceChanged(
                    LocalUserChatRoomPresenceChangeEvent evt)
    {
        ChatRoom sourceChatRoom = evt.getChatRoom();
        String eventType = evt.getEventType();

        if (LocalUserChatRoomPresenceChangeEvent
                .LOCAL_USER_JOINED.equals(eventType))
        {
            sourceChatRoom.addMessageListener(this);
        }
        else if (LocalUserChatRoomPresenceChangeEvent
                        .LOCAL_USER_LEFT.equals(eventType)
                    || LocalUserChatRoomPresenceChangeEvent
                            .LOCAL_USER_KICKED.equals(eventType)
                    || LocalUserChatRoomPresenceChangeEvent
                            .LOCAL_USER_DROPPED.equals(eventType))
        {
            sourceChatRoom.removeMessageListener(this);
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void messageDelivered(AdHocChatRoomMessageDeliveredEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void messageDelivered(ChatRoomMessageDeliveredEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used
     */
    public void messageDelivered(MessageDeliveredEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void messageDeliveryFailed(
            AdHocChatRoomMessageDeliveryFailedEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void messageDeliveryFailed(MessageDeliveryFailedEvent ev) {}

    /**
     * Implements the <tt>AdHocChatRoomMessageListener.messageReceived</tt>
     * method.
     * <br>
     * @param evt the <tt>AdHocChatRoomMessageReceivedEvent</tt> that notified
     * us
     */
    public void messageReceived(AdHocChatRoomMessageReceivedEvent evt)
    {
        try
        {
            AdHocChatRoom sourceChatRoom = evt.getSourceChatRoom();
            Contact sourceParticipant = evt.getSourceChatRoomParticipant();

            // Fire notification
            boolean fireChatNotification;

            String nickname = sourceChatRoom.getName();
            String messageContent = evt.getMessage().getContent();

            fireChatNotification =
                (nickname == null)
                    || messageContent.toLowerCase().contains(
                            nickname.toLowerCase());

            if (fireChatNotification)
            {
                String title
                    = NotificationWiringActivator.getResources().getI18NString(
                            "service.gui.MSG_RECEIVED",
                            new String[] { sourceParticipant.getDisplayName() });
                final String htmlContent;
                if (HTML_CONTENT_TYPE.equals(evt.getMessage().getContentType()))
                {
                    htmlContent = messageContent;
                }
                else
                {
                    htmlContent = StringEscapeUtils.escapeHtml4(messageContent);
                }
                fireChatNotification(
                    sourceChatRoom,
                    INCOMING_MESSAGE,
                    title,
                    htmlContent,
                    evt.getMessage().getMessageUID());
            }
        }
        catch(Throwable t)
        {
            logger.error("Error notifying for adhoc message received", t);
        }
    }

    /**
     * Implements the <tt>ChatRoomMessageListener.messageReceived</tt> method.
     * <br>
     * Obtains the corresponding <tt>ChatPanel</tt> and process the message
     * there.
     * @param evt the <tt>ChatRoomMessageReceivedEvent</tt> 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;

            final Message sourceMsg = evt.getMessage();
            String messageContent = sourceMsg.getContent();

            /*
             * 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
            {
                String nickname = sourceChatRoom.getUserNickname();

                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()));
            }

            if (fireChatNotification)
            {
                String title
                    = NotificationWiringActivator.getResources().getI18NString(
                        "service.gui.MSG_RECEIVED",
                        new String[] { sourceMember.getName() });
                final String htmlContent;
                if (HTML_CONTENT_TYPE.equals(sourceMsg.getContentType()))
                {
                    htmlContent = messageContent;
                }
                else
                {
                    htmlContent = StringEscapeUtils.escapeHtml4(messageContent);
                }
                fireChatNotification(
                        sourceChatRoom,
                        INCOMING_MESSAGE,
                        title,
                        htmlContent,
                        sourceMsg.getMessageUID());
            }
        }
        catch(Throwable t)
        {
            logger.error("Error notifying for chat room message received", t);
        }
    }

    /**
     *  Fired on new messages.
     * @param evt the <tt>MessageReceivedEvent</tt> containing
     * details on the received message
     */
    public void messageReceived(MessageReceivedEvent evt)
    {
        try
        {
            // Fire notification
            String title = NotificationWiringActivator.getResources().getI18NString(
                "service.gui.MSG_RECEIVED",
                new String[]{evt.getSourceContact().getDisplayName()});

            final Message sourceMsg = evt.getSourceMessage();
            final String htmlContent;
            if (HTML_CONTENT_TYPE.equals(sourceMsg.getContentType()))
            {
                htmlContent = sourceMsg.getContent();
            }
            else
            {
                htmlContent =
                    StringEscapeUtils.escapeHtml4(sourceMsg.getContent());
            }
            fireChatNotification(
                    evt.getSourceContact(),
                    INCOMING_MESSAGE,
                    title,
                    htmlContent,
                    sourceMsg.getMessageUID());
        }
        catch(Throwable t)
        {
            logger.error("Error notifying for message received", t);
        }
    }

    /**
     * Do nothing. Implements CallListener.outGoingCallCreated.
     * @param event the <tt>CallEvent</tt>
     */
    public void outgoingCallCreated(CallEvent event)
    {
        Call call = event.getSourceCall();
        call.addCallChangeListener(this);

        if(call.getCallPeers().hasNext())
        {
            CallPeer peer = call.getCallPeers().next();
            peer.addCallPeerListener(this);
            peer.addCallPeerSecurityListener(this);
            peer.addCallPeerConferenceListener(this);
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void peerAddressChanged(CallPeerChangeEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void peerDisplayNameChanged(CallPeerChangeEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void peerImageChanged(CallPeerChangeEvent ev) {}

    /**
     * Fired when peer's state is changed
     *
     * @param ev fired CallPeerEvent
     */
    public void peerStateChanged(CallPeerChangeEvent ev)
    {
        try
        {
            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))
            {
                /*
                 * The loopCondition will stay with the notification sound until
                 * the latter is stopped. If by any chance the sound fails to
                 * stop by the time the peer is no longer referenced, do try to
                 * stop it then. That's why the loopCondition will weakly
                 * reference the peer.
                 */
                final WeakReference<CallPeer> weakPeer
                    = new WeakReference<CallPeer>(peer);

                /* We want to play the dialing once for multiple CallPeers. */
                if (shouldPlayDialingSound(weakPeer))
                {
                    NotificationData notification
                        = fireNotification(
                                DIALING,
                                new Callable<Boolean>()
                                {
                                    public Boolean call()
                                    {
                                        return shouldPlayDialingSound(weakPeer);
                                    }
                                });

                    if (notification != null)
                        callNotifications.put(call, notification);
                }
            }
            else
            {
                NotificationData notification = callNotifications.get(call);

                if (notification != null)
                    stopSound(notification);
            }

            if (newState == CallPeerState.ALERTING_REMOTE_SIDE
                //if we were already in state CONNECTING_WITH_EARLY_MEDIA the server
                //is already taking care of playing the notifications so we don't
                //need to fire a notification here.
                && oldState != CallPeerState.CONNECTING_WITH_EARLY_MEDIA)
            {
                final WeakReference<CallPeer> weakPeer
                    = new WeakReference<CallPeer>(peer);
                NotificationData notification
                    = fireNotification(
                            OUTGOING_CALL,
                            new Callable<Boolean>()
                            {
                                public Boolean call()
                                {
                                    CallPeer peer = weakPeer.get();

                                    return
                                        (peer != null)
                                            && 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))
                {
                    final WeakReference<CallPeer> weakPeer
                        = new WeakReference<CallPeer>(peer);
                    NotificationData notification
                        = fireNotification(
                                BUSY_CALL,
                                new Callable<Boolean>()
                                {
                                    public Boolean call()
                                    {
                                        CallPeer peer = weakPeer.get();

                                        return
                                            (peer != null)
                                                && CallPeerState.BUSY.equals(
                                                        peer.getState());
                                    }
                                });

                    if (notification != null)
                        callNotifications.put(call, notification);
                }
            }
            else if ((newState == CallPeerState.DISCONNECTED)
                    || (newState == CallPeerState.FAILED))
            {
                fireNotification(HANG_UP);
            }
        }
        catch(Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
            {
                logger.error(
                        "An error occurred while trying to notify"
                            + " about a change in the state of a call peer.",
                        t);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void peerTransportAddressChanged(CallPeerChangeEvent ev) {}

    /**
     * Notifies that a specific <tt>Recorder</tt> has
     * stopped recording the media associated with it.
     *
     * @param recorder the <tt>Recorder</tt> which has stopped recording its
     * associated media
     */
    public void recorderStopped(Recorder recorder)
    {
        try
        {
            ResourceManagementService resources
                = NotificationWiringActivator.getResources();

            fireNotification(
                    CALL_SAVED,
                    resources.getI18NString(
                            "plugin.callrecordingconfig.CALL_SAVED"),
                    resources.getI18NString(
                            "plugin.callrecordingconfig.CALL_SAVED_TO",
                            new String[] { recorder.getFilename() }));
        }
        catch(Throwable 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);
            }
        }
    }

    /**
     * Register all default notifications.
     */
    private void registerDefaultNotifications()
    {
        NotificationService notificationService
            = NotificationWiringActivator.getNotificationService();

        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.
        notificationService.registerDefaultNotificationForEvent(
                OUTGOING_CALL,
                new SoundNotificationAction(
                        SoundProperties.OUTGOING_CALL,
                        3000,
                        false, true, false));

        // Register busy call notifications.
        notificationService.registerDefaultNotificationForEvent(
                BUSY_CALL,
                new SoundNotificationAction(
                        SoundProperties.BUSY,
                        1,
                        false, true, false));

        // Register dial notifications.
        notificationService.registerDefaultNotificationForEvent(
                DIALING,
                new SoundNotificationAction(
                        SoundProperties.DIALING,
                        -1,
                        false, true, false));

        // 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,
                NotificationAction.ACTION_POPUP_MESSAGE,
                null,
                null);

        // 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));

        // 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));

        // Register sound notification for incoming files.
        notificationService.registerDefaultNotificationForEvent(
                INCOMING_FILE,
                NotificationAction.ACTION_POPUP_MESSAGE,
                null,
                null);

        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())
            {
            // Don't play alert sound for Info or warning.
            case SrtpListener.INFORMATION:
                messageTitleKey = "service.gui.SECURITY_INFO";
                break;

            case SrtpListener.WARNING:
                messageTitleKey = "service.gui.SECURITY_WARNING";
                break;

            // Security cannot be established! Play an alert sound.
            case SrtpListener.SEVERE:
            case SrtpListener.ERROR:
                messageTitleKey = "service.gui.SECURITY_ERROR";
                fireNotification(CALL_SECURITY_ERROR);
                break;

            default:
                /*
                 * Whatever other severity there is or will be, we do not how to
                 * react to it yet.
                 */
                messageTitleKey = null;
            }

            if (messageTitleKey != null)
            {
                fireNotification(
                        SECURITY_MESSAGE,
                        NotificationWiringActivator.getResources()
                                .getI18NString(messageTitleKey),
                        ev.getI18nMessage());
            }
        }
        catch(Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
            {
                logger.error(
                        "An error occurred while trying to notify"
                            + " about a security message",
                        t);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void securityNegotiationStarted(
            CallPeerSecurityNegotiationStartedEvent ev) {}

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void securityOff(CallPeerSecurityOffEvent ev) {}

    /**
     * When a <tt>securityOnEvent</tt> is received.
     * @param ev the event we received
     */
    public void securityOn(CallPeerSecurityOnEvent ev)
    {
        try
        {
            SrtpControl securityController = ev.getSecurityController();
            CallPeer peer = (CallPeer) ev.getSource();

            if(!securityController.requiresSecureSignalingTransport()
                    || peer.getProtocolProvider().isSignalingTransportSecure())
            {
                fireNotification(CALL_SECURITY_ON);
            }
        }
        catch(Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
            {
                logger.error(
                        "An error occurred while trying to notify"
                            + " about a security-related event",
                        t);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void securityTimeout(CallPeerSecurityTimeoutEvent ev) {}

    /**
     * Implements the <tt>ServiceListener</tt> method. Verifies whether the
     * passed event concerns a <tt>ProtocolProviderService</tt> and adds the
     * corresponding listeners.
     *
     * @param event The <tt>ServiceEvent</tt> object.
     */
    public void serviceChanged(ServiceEvent event)
    {
        ServiceReference serviceRef = event.getServiceReference();

        // if the event is caused by a bundle being stopped, we don't want to
        // know
        if (serviceRef.getBundle().getState() == Bundle.STOPPING)
            return;

        Object service
            = NotificationWiringActivator.bundleContext.getService(serviceRef);

        // we don't care if the source service is not a protocol provider
        if (service instanceof ProtocolProviderService)
        {
            switch (event.getType())
            {
                case ServiceEvent.REGISTERED:
                    handleProviderAdded((ProtocolProviderService) service);
                    break;
                case ServiceEvent.UNREGISTERING:
                    handleProviderRemoved((ProtocolProviderService) service);
                    break;
            }
        }
    }

    /**
     * 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.
     */
    private void stopSound(NotificationData data)
    {
        if (data == null)
            return;

        try
        {
            NotificationService notificationService
                = NotificationWiringActivator.getNotificationService();

            if(notificationService != null)
                notificationService.stopNotification(data);
        }
        finally
        {
            /*
             * The field callNotifications associates a Call with a
             * NotificationData for the purposes of the stopSound method so the
             * stopSound method should dissociate them upon stopping a specific
             * NotificationData.
             */
            Iterator<Map.Entry<Call, NotificationData>> i
                = callNotifications.entrySet().iterator();

            while (i.hasNext())
            {
                Map.Entry<Call, NotificationData> e = i.next();

                if (data.equals(e.getValue()))
                    i.remove();
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * Not used.
     */
    public void typingNotificationDeliveryFailed(TypingNotificationEvent ev) {}

    /**
     * Informs the user what is the typing state of his chat contacts.
     *
     * @param ev the event containing details on the typing notification
     */
    public void typingNotificationReceived(TypingNotificationEvent ev)
    {
        try
        {
            Contact contact = ev.getSourceContact();

            // 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)
            {
                return;
            }

            // 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;
                    }
                }
            }

            long currentTime = System.currentTimeMillis();

            if (proactiveTimer.size() > 0)
            {
                // first remove contacts that have been here longer than the
                // timeout to avoid memory leaks
                Iterator<Map.Entry<Contact, Long>> entries
                    = proactiveTimer.entrySet().iterator();

                while (entries.hasNext())
                {
                    Map.Entry<Contact, Long> 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(
                            "service.gui.PROACTIVE_NOTIFICATION"),
                    null);
        }
        catch(Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
            {
                logger.error(
                        "An error occurred while handling"
                            + " a typing notification.",
                        t);
            }
        }
    }
}
