/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.notification; import java.lang.reflect.*; import java.util.*; import java.util.concurrent.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.notification.*; import org.jitsi.service.audionotifier.*; import org.jitsi.service.configuration.*; import org.jitsi.util.*; /** * An implementation of the <tt>SoundNotificationHandler</tt> interface. * * @author Yana Stamcheva */ public class SoundNotificationHandlerImpl implements SoundNotificationHandler { /** * The logger that will be used to log messages. */ private static Logger logger = Logger.getLogger(SoundNotificationHandlerImpl.class); /** * The indicator which determines whether this * <tt>SoundNotificationHandler</tt> is currently muted i.e. the sounds are * off. */ private boolean mute; private Map<SCAudioClip, NotificationData> playedClips = new WeakHashMap<SCAudioClip, NotificationData>(); /** * Property to disable sound notification during an on-going call. */ private static final String PROP_DISABLE_NOTIFICATION_DURING_CALL = "net.java.sip.communicator.impl.notification.disableNotificationDuringCall"; /** * {@inheritDoc} */ public String getActionType() { return NotificationAction.ACTION_SOUND; } /** * Specifies if currently the sound is off. * * @return TRUE if currently the sound is off, FALSE otherwise */ public boolean isMute() { return mute; } /** * Plays the sound given by the containing <tt>soundFileDescriptor</tt>. 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. * @param device */ private void play( SoundNotificationAction action, NotificationData data, SCAudioClipDevice device) { AudioNotifierService audioNotifService = NotificationActivator.getAudioNotifier(); if((audioNotifService == null) || StringUtils.isNullOrEmpty(action.getDescriptor(), true)) return; // this is hack, seen on some os (particularly seen on macosx with // external devices). // 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 ConfigurationService cfg = NotificationActivator.getConfigurationService(); if(cfg != null && cfg.getBoolean( PROP_DISABLE_NOTIFICATION_DURING_CALL, false) && SCAudioClipDevice.PLAYBACK.equals(device)) { UIService uiService = NotificationActivator.getUIService(); if(!uiService.getInProgressCalls().isEmpty()) return; } SCAudioClip audio = null; switch (device) { case NOTIFICATION: case PLAYBACK: audio = audioNotifService.createAudio( action.getDescriptor(), SCAudioClipDevice.PLAYBACK.equals(device)); break; case PC_SPEAKER: if(!OSUtils.IS_ANDROID) audio = new PCSpeakerClip(); break; } // it is possible that audio cannot be created if(audio == null) return; synchronized(playedClips) { playedClips.put(audio, data); } boolean played = false; try { @SuppressWarnings("unchecked") Callable<Boolean> loopCondition = (Callable<Boolean>) data.getExtra( NotificationData .SOUND_NOTIFICATION_HANDLER_LOOP_CONDITION_EXTRA); audio.play(action.getLoopInterval(), loopCondition); played = true; } finally { synchronized(playedClips) { if (!played) playedClips.remove(audio); } } } /** * Stops/Restores all currently playing sounds. * * @param mute mute or not currently playing sounds */ public void setMute(boolean mute) { this.mute = mute; if (mute) { AudioNotifierService ans = NotificationActivator.getAudioNotifier(); if ((ans != null) && (ans.isMute() != this.mute)) ans.setMute(this.mute); } } /** * Plays the sound given by the containing <tt>soundFileDescriptor</tt>. 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. */ public void start(SoundNotificationAction action, NotificationData data) { 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, SCAudioClipDevice.PLAYBACK); } } else { if(action.isSoundNotificationEnabled()) play(action, data, SCAudioClipDevice.NOTIFICATION); if(action.isSoundPlaybackEnabled()) play(action, data, SCAudioClipDevice.PLAYBACK); } 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) { List<SCAudioClip> clipsToStop = new ArrayList<SCAudioClip>(); synchronized(playedClips) { Iterator<Map.Entry<SCAudioClip, NotificationData>> i = playedClips.entrySet().iterator(); while (i.hasNext()) { Map.Entry<SCAudioClip, NotificationData> e = i.next(); if (e.getValue() == data) { clipsToStop.add(e.getKey()); i.remove(); } } } for(SCAudioClip clip : clipsToStop) { try { clip.stop(); } catch(Throwable t) { logger.error("Error stopping audio clip", t); } } } } /** * Tells if the given notification sound is currently played. * * @param data Additional data for the event. */ public boolean isPlaying(NotificationData data) { AudioNotifierService audioNotifService = NotificationActivator.getAudioNotifier(); if (audioNotifService != null) { synchronized(playedClips) { Iterator<Map.Entry<SCAudioClip, NotificationData>> i = playedClips.entrySet().iterator(); while (i.hasNext()) { Map.Entry<SCAudioClip, NotificationData> e = i.next(); if (e.getValue() == data) { return e.getKey().isStarted(); } } } } return false; } /** * Beeps the PC speaker. */ private static class PCSpeakerClip extends AbstractSCAudioClip { /** * The beep method. */ private Method beepMethod = null; /** * The toolkit. */ private Object toolkit = null; /** * Initializes a new <tt>PCSpeakerClip</tt> instance. */ public PCSpeakerClip() { super(null, NotificationActivator.getAudioNotifier()); // load the method java.awt.Toolkit.getDefaultToolkit().beep(); // use reflection to be sure it will not throw exception in Android try { Method getDefaultToolkitMethod = Class.forName("java.awt.Toolkit") .getMethod("getDefaultToolkit"); toolkit = getDefaultToolkitMethod.invoke(null); beepMethod = toolkit.getClass().getMethod("beep"); } catch(Throwable t) { logger.error("Cannot load awt.Toolkit", t); } } /** * Beeps the PC speaker. * * @return <tt>true</tt> if the playback was successful; otherwise, * <tt>false</tt> */ @Override protected boolean runOnceInPlayThread() { try { if(beepMethod != null) beepMethod.invoke(toolkit); return true; } catch (Throwable t) { if (t instanceof ThreadDeath) throw (ThreadDeath) t; else return false; } } } /** * Enumerates the types of devices on which <tt>SCAudioClip</tt>s may be * played back. */ private static enum SCAudioClipDevice { NOTIFICATION, PC_SPEAKER, PLAYBACK } }