/**
* Handle each slider's functionality.
* Get correspondent tone, make sure the tone is playable,
* set volume based on slider position.
*
* @module SliderHandler
*/
define(function(require) {
'use strict';
var SettingsListener = require('shared/settings_listener');
var SettingsCache = require('modules/settings_cache');
var INTERVAL = 500;
var DELAY = 800;
var BASESHAREURL = '/shared/resources/media/';
var TONEURLS = {
'content': BASESHAREURL + 'notifications/notifier_firefox.opus',
'notification': BASESHAREURL + 'ringtones/ringer_firefox.opus',
'alarm': BASESHAREURL + 'alarms/ac_awake.opus'
};
var TONEKEYS = {
'content': 'media.ringtone',
'notification': 'dialer.ringtone',
'alarm': 'alarm.ringtone'
};
var SliderHandler = function() {
this._element = null;
this._channelType = '';
this._channelKey = '';
this._toneURL = '';
this._toneKey = '';
this._previous = null;
this._isTouching = false;
this._isFirstInput = false;
this._intervalID = null;
this._timeoutID = null;
this._player = new Audio();
};
SliderHandler.prototype = {
/**
* initialization
*
* The sliders listen to input, touchstart and touchend events to fit
* the ux requirements, and when the user tap or drag the sliders, the
* sequence of the events is:
* touchstart -> input -> input(more if dragging) -> touchend -> input
*
* @access public
* @memberOf SliderHandler.prototype
* @param {Object} element html elements
* @param {String} channelType type of sound channel
*/
init: function sh_init(element, channelType) {
this._element = element;
this._channelType = channelType;
this._channelKey = 'audio.volume.' + channelType;
this._toneURL = TONEURLS[channelType];
this._toneKey = TONEKEYS[channelType];
this._boundSetSliderValue = function(value) {
this._setSliderValue(value);
}.bind(this);
// Get the volume value for the slider, also observe the value change.
SettingsListener.observe(this._channelKey, '', this._boundSetSliderValue);
this._element.addEventListener('touchstart',
this._touchStartHandler.bind(this));
this._element.addEventListener('input',
this._inputHandler.bind(this));
this._element.addEventListener('touchend',
this._touchEndHandler.bind(this));
},
/**
* Stop the tone
*
* @access private
* @memberOf SliderHandler.prototype
*/
_stopTone: function vm_stopTone() {
this._player.pause();
this._player.removeAttribute('src');
this._player.load();
},
/**
* Play the tone
*
* @access private
* @memberOf SliderHandler.prototype
* @param {Blob} blob tone blob
*/
_playTone: function vm_playTone(blob) {
// Don't set the audio channel type to content or it will interrupt the
// background music and won't resume after the user previewed the tone.
if (this._channelType !== 'content') {
this._player.mozAudioChannelType = this._channelType;
}
this._player.src = URL.createObjectURL(blob);
this._player.load();
this._player.loop = true;
this._player.play();
},
/**
* Change slider's value
*
* @access private
* @memberOf SliderHandler.prototype
* @param {Number} value slider value
*/
_setSliderValue: function vm_setSliderValue(value) {
this._element.value = value;
// The slider is transparent if the value is not set yet, display it
// once the value is set.
if (this._element.style.opacity !== 1) {
this._element.style.opacity = 1;
}
// If it is the first time we set the slider value, we must update the
// previous value of this channel type
if (this._previous === null) {
this._previous = value;
}
},
/**
* get default tone
*
* @access private
* @memberOf SliderHandler.prototype
* @param {Function} callback callback function
*/
_getDefaultTone: function vm_getDefaultTone(callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', this._toneURL);
xhr.overrideMimeType('audio/ogg');
xhr.responseType = 'blob';
xhr.send();
xhr.onload = function() {
callback(xhr.response);
};
},
/**
* get tone's blob object
*
* @access private
* @memberOf SliderHandler.prototype
* @param {Function} callback callback function
*/
_getToneBlob: function vm_getToneBlob(callback) {
SettingsCache.getSettings(function(results) {
if (results[this._toneKey]) {
callback(results[this._toneKey]);
} else {
// Fall back to the predefined tone if the value does not exist
// in the mozSettings.
this._getDefaultTone(function(blob) {
// Save the default tone to mozSettings so that next time we
// don't have to fall back to it from the system files.
var settingObject = {};
settingObject[this._toneKey] = blob;
navigator.mozSettings.createLock().set(settingObject);
callback(blob);
});
}
}.bind(this));
},
/**
* Handle touchstart event
*
* It stop the tone previewing from the last touchstart if the delayed
* stopTone() is not called yet.
*
* It stop observing when the user is adjusting the slider, this is to
* get better ux that the slider won't be updated by both the observer
* and the ui.
*
* @access private
* @memberOf SliderHandler.prototype
*/
_touchStartHandler: function sh_touchStartHandler(event) {
this._isTouching = true;
this._isFirstInput = true;
this._stopTone();
SettingsListener.unobserve(this._channelKey, this._boundSetSliderValue);
this._getToneBlob(function(blob) {
this._playTone(blob);
}.bind(this));
},
/**
* Change volume
*
* @access private
* @memberOf SliderHandler.prototype
*/
_setVolume: function sh_setVolume() {
var value = parseInt(this._element.value);
var settingObject = {};
settingObject[this._channelKey] = value;
// Only set the new value if it does not equal to the previous one.
if (value !== this._previous) {
navigator.mozSettings.createLock().set(settingObject);
this._previous = value;
}
},
/**
* Handle input event
*
* The mozSettings api is not designed to call rapidly, but ux want the
* new volume to be applied immediately while previewing the tone, so
* here we use setInterval() as a timer to ease the number of calling,
* or we will see the queued callbacks try to update the slider's value
* which we are unable to avoid and make bad ux for the users.
*
* It uses setTimeout to re-observe the value change after the user finished
* tapping/dragging on the slider and the preview is ended.
*
* If the user tap the slider very quickly, like the click event, then
* we try to stop the player after a constant duration so that the user
* is able to hear the tone's preview with the adjusted volume.
*
* @access private
* @memberOf SliderHandler.prototype
*/
_inputHandler: function sh_inputHandler(event) {
if (this._isFirstInput) {
this._isFirstInput = false;
this._setVolume();
this._intervalID = setInterval(this._setVolume.bind(this), INTERVAL);
}
clearTimeout(this._timeoutID);
this._timeoutID = setTimeout(function() {
if (!this._isTouching) {
SettingsListener.observe(this._channelKey, '',
this._boundSetSliderValue);
this._stopTone();
}
}.bind(this), DELAY);
},
/**
* Handle touchend event
*
* It Clear the interval setVolume() and set it directly when the
* user's finger leaves the panel.
*
* @access private
* @memberOf SliderHandler.prototype
*/
_touchEndHandler: function sh_touchEndHandler(event) {
this._isTouching = false;
clearInterval(this._intervalID);
this._setVolume();
}
};
return function ctor_sliderHandler() {
return new SliderHandler();
};
});