define(['mmirf/resources', 'mmirf/mediaManager', 'mmirf/logger', 'module'],
/**
*
* @name NotificationManager
* @memberOf mmir
* @static
* @class
* @hideconstructor
*
* @requires mmir.MediaManager
*/
function(
res, mediaManager, Logger, module
){
//the next comment enables JSDoc2 to map all functions etc. to the correct class description
/** @scope mmir.NotificationManager.prototype *///for jsdoc2
//private members
/**
*
* @private
* @memberOf NotificationManager#
*/
var isCordovaEnv = res.isCordovaEnv();
/**
* @private
* @memberOf NotificationManager#
*/
var instance = null;
/**
* @private
* @memberOf NotificationManager#
*/
var logger = Logger.create(module);
//private methods
/**
* Constructor-Method of Singleton mmir.NotificationManager.<br>
*
* @constructs NotificationManager
* @memberOf NotificationManager#
* @ignore
*/
function constructor(){
/**
* @private
* @memberOf NotificationManager.prototype
*/
var INIT = 'init';
/**
* VIBRATE initialization status
* @private
* @memberOf NotificationManager.prototype
*/
var isHapticEnabled = true;
/**
* Implementation for vibrate-function:
* platform-dependent (if platform/device does not support it: as stub-function)
*
* @private
* @type {Function}
* @memberOf NotificationManager.prototype
*/
var doVibrate = null;
/**
* Implementation for confirm-function:
* shows "native" platform-specific confirm-dialog.
*
* <code>function(message, confirmCallback, title, buttonLabels)</code>
*
* @private
* @type {Function}
* @memberOf NotificationManager.prototype
*/
var doConfirm = null;
/**
* Implementation for confirm-function:
* shows "native" platform-specific alert-dialog.
*
* <code>(message, alertCallback, title, buttonName)</code>
*
* @private
* @type {Function}
* @memberOf NotificationManager.prototype
*/
var doAlert = null;
/**
* Initialize the NotificationManager.
*
* At the moment this set the internal vibrate-function,
* if available in the current execution environment
* (or with a dummy function, if not).
*
* In addition, the alert-, and confirm-functions are set to their
* platform-specific implementation.
*
* @memberOf NotificationManager.prototype
* @private
* @function
*/
var _init = function(){
var isNavigator = typeof navigator !== 'undefined';
if(isCordovaEnv){
if(navigator.vibrate){
// logger.debug('Vibrate: navigator (API)');
/** @ignore */
doVibrate = function vibrate(n){ navigator.vibrate(n); };
}
else if(navigator.notification && navigator.notification.vibrate){
// logger.debug('Vibrate: navigator.notification');
/** @ignore */
doVibrate = function vibrate(n){ navigator.notification.vibrate(n); };
}
else {
logger.warn('INIT: could not detect navigator.notification.vibrate, using NOOP dummy instead.');
/** @ignore */
doVibrate = function dummyVibrate(n){ logger.warn('NotificationManager.vibrate('+n+') triggered in CORDOVA environment, but no VIBRATE functionality available.'); };
}
}
else if (isNavigator && navigator.vibrate){
// logger.debug('Vibrate API');
/** @ignore */
doVibrate = function vibrate(n){ navigator.vibrate(n); };
}
else if (isNavigator && navigator.webkitVibrate){
// logger.debug('Vibrate: webkit');
/** @ignore */
doVibrate = function vibrate(n){ navigator.webkitVibrate(n); };
}
//set confirm-implementation
if(isNavigator && navigator.notification && navigator.notification.confirm){
// logger.debug('Confirm: navigator.notification');
/** @ignore */
doConfirm = function confirm(message, confirmCallback, title, buttonLabels){
var cbWrapper = confirmCallback;
if(confirmCallback){
var self = this;
cbWrapper = function(result){
//need to convert NUMBER result to BOOLEAN:
// result = [1,2,..]
// -> default is: OK = 1, CANCEL = 2, close-the-dialog = 0
var res = result === 1 ? true : false;
confirmCallback.call(self, res);
};
}
navigator.notification.confirm(message, cbWrapper, title, buttonLabels);
};
}
else if(typeof window !== 'undefined' && window && window.confirm) {
/** @ignore */
doConfirm = function confirmWindow(message, confirmCallback, _title, _buttonLabels){
//TODO use setTimeout here to "simulate" async execution?
var result = window.confirm(message);
if(confirmCallback){
confirmCallback.call(this, result);
}
};
}
//set alert-implementation
if(isNavigator && navigator.notification && navigator.notification.alert){
// logger.debug('Alert: navigator.notification');
/** @ignore */
doAlert = function confirm(message, alertCallback, title, buttonLabels){
navigator.notification.alert(message, alertCallback, title, buttonLabels);
};
}
else if(typeof window !== 'undefined' && window && window.alert){
/** @ignore */
doAlert = function confirmWindow(message, alertCallback, _title, _buttonLabels){
//TODO use setTimeout here to "simulate" async execution?
window.alert(message);
if(alertCallback){
alertCallback.call(this);
}
};
}
};
//SOUND / BEEP initialization:
/**
* @private
* @type Number
*
* @memberOf NotificationManager.prototype
*/
var beepVolume = 1.0;
/**
* The Audio object for the <em>beep</em> sound.
*
* @private
* @type AudioObject
*
* @memberOf NotificationManager.prototype
*/
var beepAudio = null;
/**
* Map for managing the currently loaded sounds
*
* @private
* @type Map
*
* @memberOf NotificationManager.prototype
*/
//TODO add option for limiting size of soundMap (-> e.g. how many resources are max. cached/occupied for Android)
var soundMap = new Map();
/**
* Factory function for creating "sounds objects",
* i.e. extend the basic Audio objects with needed functions/properties
*
* @private
* @function
*
* @param {mmir.env.media.IAudio} audioObj
* @param {String} name
*
* @returns {mmir.env.media.INotificationSound} the extended audio object, i.e. a NotificationSound
*
* @memberOf NotificationManager.prototype
*/
function initNotificationSound(audioObj, name){
audioObj.name = name;
audioObj.setVolume(beepVolume);
audioObj.isNotificationPlaying = false;
audioObj.repeatNotification = 0;
audioObj.playNotification = function(repeatNTimes){
// logger.debug('isPlaying: '+this.isNotificationPlaying+', repeat: '+this.repeatNotification+', args: '+repeatNTimes+'');
//"blocking mode": only re-start, if not already playing
if(!this.isNotificationPlaying){
this.repeatNotification = repeatNTimes ? repeatNTimes : 0;
}
if( this.repeatNotification < 1){
//end recurusion
this.isNotificationPlaying = false;
this.repeatNotification = 0;
}
else {
this.isNotificationPlaying = true;
--this.repeatNotification;
// this.stop();
this.play();
}
};
audioObj.setCallbacks = function(onFinished, onError){
this.onFinishedListener = onFinished;
this.onErrorListener = onError;
};
audioObj.clearCallbacks = function(){
this.onFinishedListener = null;
this.onErrorListener = null;
};
audioObj.fireFinished = function(){
var tempOnFinishedListener = this.onFinishedListener;
//clear callbacks
// NOTE: do this before triggering callback, in case the callback re-plays the notification with new callbacks!
// (if we would clear callbacks after invocation, we would delete the new callbacks!)
this.clearCallbacks();
if(tempOnFinishedListener){
tempOnFinishedListener();
}
};
audioObj.fireError = function(error){
var tempOnErrorListener = this.onErrorListener;
//clear callbacks
// NOTE: do this before triggering callback, in case the callback re-plays the notification with new callbacks!
// (if we would clear callbacks after invocation, we would delete the new callbacks!)
this.clearCallbacks();
//create error message with details
var id;
if(this.name){
var entry = soundMap.get(this.name);
id = '"' + this.name + '" -> ' + (entry? '"'+entry.url+'"' : 'UNDEF');
}
else {
id = '"BEEP" -> "'+res.getBeepUrl()+'"';
}
var msg = 'Notification: Error playing the sound for notification '+id;
//create Error object if necessary
if(!error){
error = new Error(msg);
msg = null;
}
if(tempOnErrorListener){
tempOnErrorListener(error, msg);
}
else {
//if no callback: print debug output in error stream:
logger.error( (msg? msg + ': ' : '') + error, error);
}
};
return audioObj;
};
/**
* Helper for creating an Audio object
*
* @private
* @function
*
* @param {String} url
* @param {Function} success
* @param {Function} fail
* @param {Function} init
*
* @returns {AudioObject} audio object
*
* @memberOf NotificationManager.prototype
*/
function createAudio(url, success, fail, init){
return mediaManager.getURLAsAudio(url, success, fail, init);
}
/**
* Helper for "registering" a NotificationSound.
*
* Stores the sound object in {@link #soundMap}
* with the ID <code>name</code>.
*
* The sound object will be initialized on first
* retrieval, ie. {@link #doGetSoundFromMap}
*
* @private
* @function
*
* @param {String} name
* @param {String} theUrl
* @param {Boolean} isKeepOnPause
*
* @memberOf NotificationManager.prototype
*/
function initAudioSoundEntry(name, theUrl, isKeepOnPause){
var config = {url: theUrl, audio: null};
if(isKeepOnPause){
config.isKeepOnPause = true;
}
soundMap.set(name, config);
}
/**
* Helper for retrieving an existing sound from
* the {@link #soundMap}.
*
* Initializes the sound if necessary.
*
* @private
* @function
*
* @param {String} name
* @param {Function} onErrorCallback
*
* @memberOf NotificationManager.prototype
*/
function doGetSoundFromMap(name, onErrorCallback){
var audioObj = null;
var audioUrl = null;
var keepOnPause = false;
//if no name: use default beep
if(!name){
audioObj = beepAudio;
audioUrl = res.getBeepUrl();
}
else {
//retrieve information for sound
var soundEntry = soundMap.get(name);
if(soundEntry === INIT){
//TODO set repeat-times?
//sound is still initializing -> return
return null; ////////////////////// EARLY EXIT //////////////////////
}
if(!soundEntry){
var errMsg = 'NotificationManager: no sound "'+name+'" initialized!';
if(onErrorCallback){
onErrorCallback(errMsg);
}
else {
logger.error(errMsg);
}
// throw new Error(errMsg);
return null; ////////////////////// EARLY EXIT //////////////////////
}
audioObj = soundEntry.audio;//<- may be null
audioUrl = soundEntry.url;//<- must NOT be null
keepOnPause = soundEntry.isKeepOnPause? true : false;
}
return {
sound: audioObj,
url: audioUrl,
isKeepOnPause: keepOnPause
};
}
/**
* Helper for playing a registered notification sound.
*
* Initializes the sound if necessary.
*
* @private
* @function
*
* @param {String} name
* ID of the sound
* @param {Number} times
* @param {Function} onFinishedCallback
* @param {Function} onErrorCallback
*
* @memberOf NotificationManager.prototype
*/
function playAudioSound(name, times, onFinishedCallback, onErrorCallback){
var soundEntry = doGetSoundFromMap(name, onErrorCallback);
if(soundEntry === null){
//could not retrieve sound-object
// (error callback will already have been invoked, so just return)
return;/////////////////////// EARYL EXIT ///////////
}
var audioObj = soundEntry.sound;
var audioUrl = soundEntry.url;
var isKeepOnPause = soundEntry.isKeepOnPause;
//create audio-object, if not existing yet
if(audioObj === null){
if(name){
soundMap.set(name, INIT);
}
audioObj = createAudio(
audioUrl,
function onFinished(){
this.playNotification();
audioObj.fireFinished();
},
function onError(e){
if(name) {
soundMap.delete(name);
};
if(audioObj && audioObj.fireError){
audioObj.fireError(e);
}
else {
if(onErrorCallback){
onErrorCallback(e);
}
else {
logger.error('Error playing the sound from "'+audioUrl+'": '+ (e && typeof e.code !== 'undefined'? 'code '+e.code : e), e);
}
}
},
function onInit(){
//FIX for Android/Cordova: return-value of createAudio will not set audioObj "fast enough"
// (i.e. may not use async-initialization, depending on where the audio-file is located)
// ... in order to be able to use keep variable audioObj useful -> do assignment now/here
audioObj = this;
initNotificationSound(audioObj, name);
audioObj.setCallbacks(onFinishedCallback, onErrorCallback);
//if no name: assume default beep
if(!name){
beepAudio = audioObj;
}
else {
var theEntry = {url: audioUrl, audio: audioObj};
if(isKeepOnPause){
theEntry.isKeepOnPause = true;
}
soundMap.set(name, theEntry);
}
audioObj.playNotification(times);
}
);
// //FIXME this is a QUICK-FIX:
// // Android needs invocation of a media-method, before it triggers the on-init callback.
// // We need to do this here, not within the
// if(isCordovaEnv){
// audioObj.init();
// }
}
else {
audioObj.setCallbacks(onFinishedCallback, onErrorCallback);
audioObj.playNotification(times);
}
}
/**
* Helper for stop playing a registered notification sound.
*
* Initializes the sound if necessary.
*
* @private
* @function
*
* @param {String} name
* ID of the sound
* @param {Function} onFinishedCallback
* @param {Function} onErrorCallback
*
* @memberOf NotificationManager.prototype
*/
function stopAudioSound(name, onFinishedCallback, onErrorCallback){
var soundEntry = doGetSoundFromMap(name, onErrorCallback);
// logger.error('Notification: invoked stop on notification-sound '+name+' -> '+JSON.stringify(soundEntry));//FIXM debug
if(soundEntry === null){
//could not retrieve sound-object
// (error callback will already have been invoked, so just return)
return;/////////////////////// EARYL EXIT ///////////
}
var audioObj = soundEntry.sound;
//NOTE audioObj may be null, e.g. if sound is still initializing.
if(audioObj != null){
if(audioObj.repeatNotification > 0)
audioObj.repeatNotification = 0;
if(audioObj.isNotificationPlaying === true)
audioObj.isNotificationPlaying = false;
if(audioObj.stop){
// logger.verbose('Notification: stopping notification-sound -> '+name);
audioObj.stop();
}
}
if(onFinishedCallback){
onFinishedCallback.call(audioObj);
}
};
//on Android: release resources on pause/exit, since they are limited
if(isCordovaEnv){
document.addEventListener("resume", function(_event){
//initialize beep sound:
playAudioSound(null, 0);
});
document.addEventListener(
"pause",
function(_event){
//use temporal variable for minimizing concurrency problems
var temp;
if(beepAudio !== null){
temp = beepAudio;
beepAudio = null;
temp.release();
logger.debug('released media resources for beep.');
}
soundMap.forEach(function(entry){
if(entry !== null && entry != INIT && ! entry.isKeepOnPause){
temp = entry.audio;
//audio may not be initialized yet:
if(temp != null){
entry.audio = null;
temp.release();
}
if(logger.isd()) logger.debug('released media resources for '+entry.url);
}
});
},
false
);
}
/** @lends mmir.NotificationManager.prototype */
return { //public members and methods
/** @scope mmir.NotificationManager.prototype */
/**
* Trigger a haptic vibration feedback.
*
* <p>Note: The device / execution environment may not support haptic vibration feedback
*
* @function
* @param {Number} milliseconds
* duration for vibration in milliseconds. Must be <code>> 0</code>
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
vibrate: function(milliseconds){
if (isHapticEnabled && doVibrate){
doVibrate(milliseconds);
}
},
/**
* Check if {@link #vibrate} is functional and enabled.
*
* <p>
* If <code>false</code> is returned, calling the <code>vibrate()</code>
* function will have no effect.
*
* @function
* @returns {Boolean} <code>true</code> if {@link #vibrate} is functional
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
isVibrateEnabled: function(){
if (isHapticEnabled && doVibrate){
return true;
}
else {
return false;
}
},
/**
* Check if the execution environment supports {@link #vibrate}.
*
* <p>
* If <code>false</code> is returned, calling the <code>vibrate()</code>
* function will have no effect.
*
* @function
* @returns {Boolean} <code>true</code> if {@link #vibrate} is functional
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
isVibrateAvailable: function(){
if (doVibrate){
return true;
}
else {
return false;
}
},
/**
* Enable or disable {@link #vibrate}.
* <p>
* NOTE: If {@ #isVibrateAvailable} returns <code>false</code>, enabling will have no effect.
*
* @function
* @public
*
* @param {Boolean} enabled
* set vibrate function to <code>enable</code>
*
* @memberOf mmir.NotificationManager.prototype
*/
setVibrateEnabled: function(enabled){
isHapticEnabled = enabled;
},
/**
* Opens a (native) alert-notification dialog.
*
* @param {String} message
* the alert message
* @param {Function} [alertCallback]
* callback that is triggered, after dialog was closed
* @param {String} [title] OPTIONAL
* the title for the alert dialog
* (may not be provided / settable in all execution environments)
* @param {String} [buttonName] OPTIONAL
* the label for the close button in the alert dialog
* (may not be provided / settable in all execution environments)
* @function
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
alert: function(message, alertCallback, title, buttonName){
if(doAlert){
doAlert.call(this, message, alertCallback, title, buttonName);
}
else if(logger.isw()) {
logger.warn('No alert dialog implementation available: (' + message + ', ' + alertCallback + ', ' + title + ', ' + buttonName + ')');
}
},
/**
* Opens a (native) confirm-notification dialog.
*
* @param {String} message
* the confirm message
* @param {Function} [alertCallback]
* callback that is triggered, after dialog was closed.
* The callback will be invoked with 1 argument:<br>
* <code>callback(wasConfirmed : Boolean)</code><br>
* if the OK/CONFIRM button was pressed, <code>wasConfirmed</code>
* will be <code>true</code>, otherwise <code>false</code>.
* @param {String} [title] OPTIONAL
* the title for the confirm dialog
* (may not be provided / settable in all execution environments)
* @param {Array<String>} [buttonLabels] OPTIONAL
* the labels for the buttons of the confirm dialog
* (may not be provided / settable in all execution environments)
*
* @function
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
confirm: function(message, confirmCallback, title, buttonLabels){
if(doConfirm){
doConfirm.call(this, message, confirmCallback, title, buttonLabels);
}
else if(logger.isw()) {
logger.warn('NotificationManager.confirm: No confirm dialog implementation available (' + message + ', ' + confirmCallback + ', ' + title + ', ' + (buttonLabels? JSON.stringify(buttonLabels) : buttonLabels) + ')');
}
},
/**
* Trigger a beep notification sound.
*
* @function
* @param {Number} times
* how many times should to beep repeated
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
beep: function(times){
if (times>0){
playAudioSound(null, times);
}
},
/**
* @memberOf mmir.NotificationManager.prototype
*/
getVolume: function(){
return beepVolume;
},
/**
* Set the volume for sound notifications.
*
* @param {Number} vol
* the new volume: a number between [0, 1]
*
* @see mmir.env.media.IAudio#setVolume
*
* @memberOf mmir.NotificationManager.prototype
*/
setVolume: function(vol){
if(typeof vol !== 'number'){
throw new TypeError('argument vol (value: '+vol+') must be a number, but is instead: '+(typeof vol));
}
if(vol !== beepVolume){
//set volume for beep notification
beepVolume = vol;
if(beepAudio){
beepAudio.setVolume(beepVolume);
}
//set volume for notification sounds
soundMap.forEach(function(entry){
if(entry.audio !== null){
entry.audio.setVolume(vol);
}
});
}
}
/**
* Trigger a sound notification by NAME (needs to be created first).
*
* @function
* @param {String} name
* the name / identifier for the sound (if <code>null</code>, beep notification is used)
* @param {Number} times
* how many times should to beep repeated
* @public
*
* @see #createSound
*
* @memberOf mmir.NotificationManager.prototype
*/
,playSound: function(name, times, onFinished, onError){
if (times>0){
playAudioSound(name, times, onFinished, onError);
}
},
/**
* Create a sound notification.
*
* <p>
* After creation, the sound "theSoundId" can be played via
* <code>playSound("theSoundId", 1)</code>
*
* @function
* @param {String} name
* the name / identifier for the sound
* @param {String} url
* the URL for the audio of the sound
* @param {Boolean} [isKeepOnPause] OPTIONAL
* flag indicating, if the audio resources should be keept
* when the device goes into <em>pause mode</em>
* (may not apply to all execution environments;
* e.g. relevant for Android environment)
* <br>
* DEFAULT: <code>false</code>
* @public
*
* @memberOf mmir.NotificationManager.prototype
*/
createSound: function(name, url, isKeepOnPause){ // TODO add callbacks? this would make the impl. more complex ..., successCallback, errorCallback){
initAudioSoundEntry(name, url, isKeepOnPause);
//DISABLED this triggers an error if MediaManager / LanguageManager etc. are not initialized yet!
// logger.error('created sound "'+name+'" for url "'+url+'", calling from: ' + new Error().stack);
// //immediately initialize the sound (but do not play it yet);
// playAudioSound(name, 0);
}
/**
* Stop a sound notification, if it is playing.
*
* Has no effect, if the notification is not playing.
*
* @function
* @param {String} name
* the name / identifier for the sound
*
* @memberOf mmir.NotificationManager.prototype
*/
,stopSound: function(name){
stopAudioSound(name);
}
/**
* <em>used by framework to initialize the default beep-sound</em>
* @private
* @memberOf mmir.NotificationManager.prototype
*/
, initBeep: function(){
//initialize beep sound:
playAudioSound(null, 0);
}
/**
* Initialize a sound notification.
*
* <p>
* NOTE a sound does not need to be explicitly initialized, <code>playSound</code> will
* automatically initialize the sound if necessary.
*
* <p>
* Initializing a sound prepares all resources, so that the sound can be immediately played.
*
* For instance, a sound that needs to loaded from a remote server first, may take some time
* before it can be played.
*
* <p>
* NOTE the sound must be {@link #createSound|created} first, before initializing it.
*
* @function
* @param {String} name
* the name / identifier for the sound
* @public
*
* @see #createSound
*
* @memberOf mmir.NotificationManager.prototype
*/
, initSound: function(name){
//initialize sound (identified by its name):
playAudioSound(name, 0);
}
/**
* <em>used by framework to initialize the NotificationManager</em>
*
* @memberOf mmir.NotificationManager.prototype
*/
, init: function(){//<- used by framework to initialize the NotificationManager
_init();
this.init = function(){ return this; };
return this;
}
};
}
instance = new constructor();
return instance;
});//END: define(..., function(){...