/** * 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(); }, /** * Setup the tone * * @access private * @memberOf SliderHandler.prototype * @param {Blob} blob tone blob */ _setupTone: function vm_setupTone(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; }, /** * 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._setupTone(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, // and start playing tone. if (value !== this._previous) { navigator.mozSettings.createLock().set(settingObject); this._player.play(); 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(); }; });