UNPKG

mmir-lib

Version:

MMIR (Mobile Multimodal Interaction and Relay) library

940 lines (840 loc) 26.6 kB
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(){...