Source: js/sound_manager.js

/* global AsyncSemaphore, SettingsListener, Service,
          HeadphoneIcon, PlayingIcon, MuteIcon,
          LazyLoader */

(function(exports) {
  'use strict';
  /**
   * SoundManager handles hardware volume key events, bluetooth volume changes,
   * and volume/channel change events.
   * @class SoundManager
   * @requires AsyncSemaphore
   * @requires Service
   */
  function SoundManager() {
  }

  /**
   * settings key for vibration
   * @memberOf SoundManager
   */
  SoundManager.VIBRATION_SETTINGS_KEY = 'vibration.enabled';
  /**
   * user preference key for vibration which is used at async storage.
   * @memberOf SoundManager
   */
  SoundManager.VIBRATION_USER_PREF_KEY = 'preference.vibration.enabled';
  /**
   * reset time span for volume warning dialog.
   * @memberOf SoundManager
   */
  SoundManager.CE_RESET_TIME = 72000000;
  /**
   * constant for CE counter interval.
   * @memberOf SoundManager
   */
  SoundManager.TIME_ONE_MINUTE = 60000;
  /**
   * elapsed time from last volume warning dialog which is used at async
   * storage.
   * @memberOf SoundManager
   */
  SoundManager.CACHE_CETIMES = 'CE_ACCTIME';
  // volume cache
  // Platform doesn't provide the maximum value of each channel
  // therefore, hard code here.
  SoundManager.MAX_VOLUME = {
    'alarm': 15,
    'notification': 15,
    'telephony': 5,
    'content': 15,
    'bt_sco': 15
  };

  SoundManager.prototype.name = 'SoundManager';

  SoundManager.prototype.publish = function(evtName, detail) {
    window.dispatchEvent(new CustomEvent(evtName), {
      detail: detail || this
    });
  };

  SoundManager.prototype.setHeadsetState = function(enabled) {
    if (this.isHeadsetConnected === enabled) {
      return;
    }
    this.isHeadsetConnected = enabled;
    if (this.headphoneIcon) {
      this.headphoneIcon.update();
    }
    this.publish('headphones-status-changed', this.isHeadsetConnected);
  };

  SoundManager.prototype.setAudioChannel = function(channel) {
    if (this.currentChannel === channel) {
      return;
    }
    this.currentChannel = channel;
    if (this.playingIcon) {
      this.playingIcon.update();
    }
    this.publish('audio-channel-changed', this.currentChannel);
  };

  /**
   * Store the current active channel;
   * change with 'audio-channel-changed' mozChromeEvent
   * All candidates and definitions can be found at AudioChannels link.
   *
   * @see {@link https://wiki.mozilla.org/WebAPI/AudioChannels|AudioChannels}
   * @memberOf SoundManager.prototype
   * @type {String}
   */
  SoundManager.prototype.currentChannel = 'none';

  /**
   * Tell if vibration is enabled currently.
   *
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.vibrationEnabled = true;

  /**
   * Default volume control channel
   * Possible values:
   *   normal
   *   content
   *   notification
   *   alarm
   *   telephony
   *   ringer
   *   publicnotification
   *   unknown
   * @memberOf SoundManager.prototype
   * @type {String}
   */
  SoundManager.prototype.defaultVolumeControlChannel = 'unknown';

  /**
   * is headset connected.
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.isHeadsetConnected = false;

  /**
   * We have three virtual states here:
   * OFF -> VIBRATION -> MUTE
   * @memberOf SoundManager.prototype
   * @type {String}
   */
  SoundManager.prototype.muteState = 'OFF';

  /**
   * User preference to tell if vibration is enabled. The value is read from
   * 'preference.vibration.enabled' key.
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.vibrationUserPrefEnabled = true;

  /**
   * Cache the volume when entering silent mode.
   * Currently only two channel would be used for mute.
   * @memberOf SoundManager.prototype
   * @type {Object}
   */
  SoundManager.prototype.cachedVolume = {
    'content': -1,
    'notification': -1
  };

  /**
   * The keys of cachedVolume.
   * @memberOf SoundManager.prototype
   * @type {Array}
   * @see cachedVolume
   */
  SoundManager.prototype.cachedChannels = ['content', 'notification'];

  /**
   * The interval ID of CE accumulator.
   * @memberOf SoundManager.prototype
   * @type {Number}
   */
  SoundManager.prototype.CEAccumulatorID = null;
  /**
   * The minimum warning volume level.
   * @memberOf SoundManager.prototype
   * @type {Number}
   * @default 11
   */
  SoundManager.prototype.CEWarningVol = 11;
  /**
   * The accumulated time where the volume is above {@link CEWarningVol} and the
   * channel is "content".
   * @memberOf SoundManager.prototype
   * @type {Number}
   * @see CEWarningVol
   */
  SoundManager.prototype.CEAccumulatorTime = 0;
  /**
   * The start time of accumulator running.
   * @memberOf SoundManager.prototype
   * @type {Number}
   */
  SoundManager.prototype.CETimestamp = 0;

  /**
   * The current volume for all channels: alarm, notification, telephony,
   * content, and bt_sco.
   * @memberOf SoundManager.prototype
   * @type {Object}
   * @see {@link https://wiki.mozilla.org/WebAPI/AudioChannels#Volume_control}
   */
  SoundManager.prototype.currentVolume = {
    'alarm': 15,
    'notification': 15,
    'telephony': 5,
    'content': 15,
    'bt_sco': 15
  };

  /**
   * A semaphore used inside of SoundManager.
   * @memberOf SoundManager.prototype
   * @type {AsyncSemaphore}
   */
  SoundManager.prototype.pendingRequest = null;

  /**
   * To tell if the homescreen is visible.
   *
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.homescreenVisible = true;

  /**
   * A counter for checking if the vibration settings is made by SoundManager.
   *
   * @memberOf SoundManager.prototype
   * @type {Number}
   */
  SoundManager.prototype.setVibrationEnabledCount = 0;
  /**
   * A flag to tell if the volume is fetched from settings.
   *
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.volumeFetched = false;
  /**
   * A timer ID for auto hiding volume UI.
   *
   * @memberOf SoundManager.prototype
   * @type {Boolean}
   */
  SoundManager.prototype.activeTimerID = 0;

  /**
   * It adds listeners to window events, observes the change of mozSettings, and
   * loads settings from mozSettings.
   *
   * @memberOf SoundManager.prototype
   * @returns {SoundManager}
   */
  SoundManager.prototype.start = function sm_start() {
    LazyLoader.load(['shared/js/async_semaphore.js']).then(() => {
      this.pendingRequest = new AsyncSemaphore();
    }).catch((err) => {
      console.error(err);
    });
    this.element = document.getElementById('volume');
    this.screen = document.getElementById('screen');
    this.overlay = document.getElementById('system-overlay');
    window.addEventListener('volumeup', this);
    window.addEventListener('volumedown', this);
    window.addEventListener('mute', this);
    window.addEventListener('unmute', this);
    window.addEventListener('mozChromeEvent', this);
    window.addEventListener('unload', this);
    window.addEventListener('appopen', this);
    window.addEventListener('ftudone', this);
    window.addEventListener('holdhome', this);
    window.addEventListener('homescreenopening', this);
    window.addEventListener('homescreenopened', this);

    LazyLoader.load(['js/headphone_icon.js',
                     'js/mute_icon.js',
                     'js/playing_icon.js']).then(function() {
      this.playingIcon = new PlayingIcon(this);
      this.playingIcon.start();
      this.headphoneIcon = new HeadphoneIcon(this);
      this.headphoneIcon.start();
      this.muteIcon = new MuteIcon(this);
      this.muteIcon.start();
    }.bind(this)).catch(function(err) {
      console.error(err);
    });

    // mozChromeEvent fired from Gecko is earlier been loaded,
    // so we use mozAudioChannelManager to
    // check the headphone plugged or not when booting up
    var acm = navigator.mozAudioChannelManager;
    if (acm) {
      this.setHeadsetState(acm.headphones);
    }

    this.initVibrationUserPref();
    this.bindVolumeSettingsHandlers();

    var self = this;
    SettingsListener.observe('audio.volume.cemaxvol', 11, function(volume) {
      self.CEWarningVol = volume;
    });

    window.asyncStorage.getItem(SoundManager.CACHE_CETIMES,
                                function getCETime(value) {
      if (!value) {
        return;
      } else {
        self.CEAccumulatorTime = value;
      }
    });

    Service.registerState('isHeadsetConnected', this);
    Service.registerState('currentChannel', this);
  };

  /**
   * It removes listeners from window events
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.stop = function sm_stop() {
    window.removeEventListener('volumeup', this);
    window.removeEventListener('volumedown', this);
    window.removeEventListener('mute', this);
    window.removeEventListener('unmute', this);
    window.removeEventListener('mozChromeEvent', this);
    window.removeEventListener('unload', this);
    window.removeEventListener('appopen', this);
    window.removeEventListener('ftudone', this);
    window.removeEventListener('holdhome', this);
    window.removeEventListener('homescreenopening', this);
    window.removeEventListener('homescreenopened', this);

    Service.unregisterState('isHeadsetConnected', this);
    Service.unregisterState('currentChannel', this);
  };

  /**
   * It handles the events from window object, including hardware key events,
   * mozChromeEvent, and custom event made by System app.
   *
   * @memberOf SoundManager.prototype
   * @param {DOMEvent} e
   */
  SoundManager.prototype.handleEvent = function sm_handleEvent(e) {
    switch (e.type) {
      case 'volumeup':
        this.handleVolumeKey(1);
        break;
      case 'volumedown':
        this.handleVolumeKey(-1);
        break;
      case 'mute':
        this.setMute(true);
        break;
      case 'unmute':
        this.setMute(false);
        break;
      case 'mozChromeEvent':
        switch (e.detail.type) {
          case 'bluetooth-volumeset':
            this.changeVolume(e.detail.value - this.currentVolume.bt_sco,
                              'bt_sco');
            break;
          case 'audio-channel-changed':
            this.setAudioChannel(e.detail.channel);
            this.ceAccumulator();
            break;
          case 'headphones-status-changed':
            this.setHeadsetState(e.detail.state !== 'off');
            this.ceAccumulator();
            break;
          case 'default-volume-channel-changed':
            this.defaultVolumeControlChannel = e.detail.channel;
            // Do not accumulate CE time here because this event
            // doesn't mean the content is playing now.
            break;
        }
        break;
      case 'unload':
        this.stopAccumulator();
        break;
      case 'appopen':
        this.homescreenVisible = false;
        break;
      case 'ftudone':
        this.homescreenVisible = true;
        break;
      case 'holdhome':
        Service.request('hideCustomDialog');
        break;
      case 'homescreenopening':
      case 'homescreenopened':
        this.homescreenVisible = true;
        Service.request('hideCustomDialog');
        break;
    }
  };

  /**
   * This function handles the volume key up/down behavior. When it is under a
   * call and user sues a bluetooth, we need to change the volume of BT SCO.
   * When the headset is connected, we need to activate CE counter. Otherwise,
   * we calls normal changeVolume API.
   *
   * @memberOf SoundManager.prototype
   * @param {Number} offset the offset which will be added to volume value.
   */
  SoundManager.prototype.handleVolumeKey = function sm_handleVolumeKey(offset) {
    if (!Service.query('screenEnabled') && this.currentChannel === 'none') {
      return;
    }

    if (Service.query('Bluetooth.isSCOProfileConnected') &&
        this.isOnCall()) {
      this.changeVolume(offset, 'bt_sco');
    } else if (this.isHeadsetConnected && offset > 0) {
      this.headsetVolumeup();
    } else {
      this.changeVolume(offset);
    }
  };

  /**
   * The mute/unmute event is dispatched from sleep menu.
   * But if we have a mute/unmute hardware button or virtual button,
   * we could make the button handler to fire this event, too.
   *
   * @memberOf SoundManager.prototype
   * @param {Boolean} mute the state of mute.
   */
  SoundManager.prototype.setMute = function sm_setMute(mute) {
    /**
     */
    if (mute) {
      // Turn off vibration for really silence.
      this.setVibrationEnabled(false);
      this.enterSilentMode('notification');
    } else {
      // Turn on vibration.
      this.setVibrationEnabled(true);
      this.leaveSilentMode('notification');
      this.leaveSilentMode('content');
    }
  };

  /*
   * When hardware volume key is pressed, we need to decide which channel we
   * should toggle.
   * This method returns the string for setting key 'audio.volume.*' represents
   * that.
   * Note: this string does not always equal to currentChannel since some
   * different channels are grouped together to listen to the same setting.
   * @memberOf SoundManager.prototype
   * @returns {String} the volume channel
   */
  SoundManager.prototype.getChannel = function sm_getChannel() {
    if (this.isOnCall()) {
      return 'telephony';
    }

    switch (this.currentChannel) {
      case 'normal':
      case 'content':
        return 'content';
      case 'telephony':
        return 'telephony';
      case 'alarm':
        return 'alarm';
      case 'notification':
      case 'ringer':
          return 'notification';
      default:
        if (this.defaultVolumeControlChannel !== 'unknown') {
          return this.defaultVolumeControlChannel;
        } else {
          return this.homescreenVisible || (Service.query('locked')) ||
            Service.query('isFtuRunning') ? 'notification' : 'content';
        }
    }
  };

  /**
   * It reads vibration user preference from asyncStorage and listen the change
   * from mozSettings.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.initVibrationUserPref = function sm_initVUserPref() {
    var self = this;
    // check asyncStorage
    window.asyncStorage.getItem(SoundManager.VIBRATION_USER_PREF_KEY,
                                function onok(value) {
      if (value === null) {
        // not found, read from settings.
        var r = SettingsListener.getSettingsLock().get(
                                           SoundManager.VIBRATION_SETTINGS_KEY);
        r.onsuccess = function get_onsuccess() {
          // write back to asyncStorage.
          self.writeVibrationUserPref(
                                 r.result[SoundManager.VIBRATION_SETTINGS_KEY]);
        };
        r.onerror = function get_onerror() {
          // initial value to true
          self.writeVibrationUserPref(true);
        };
      } else {
        self.vibrationUserPrefEnabled = value;
      }
    });
    // observe settings
    SettingsListener.observe(SoundManager.VIBRATION_SETTINGS_KEY,
                             true, function(vibration) {
      var setBySelf = false;
      var toggleVibrationEnabled = function toggle_vibration_enabled() {
        // XXX: If the value does not set by sound manager,
        //      we assume it comes from
        //      the settings app and consider it as user preference.
        if (!setBySelf) {
          self.writeVibrationUserPref(vibration);
        }
        self.vibrationEnabled = vibration;
        self.muteIcon && self.muteIcon.update();
      };

      if (self.setVibrationEnabledCount > 0) {
        self.setVibrationEnabledCount--;
        setBySelf = true;
      }
      self.pendingRequest.wait(toggleVibrationEnabled, self);
    });
  };

  /**
   * It saves the user perference of vibration to asyncStorage.
   *
   * @memberOf SoundManager.prototype
   * @param {Boolean} value vibration preference
   */
  SoundManager.prototype.writeVibrationUserPref = function sm_writeUP(value) {
    if (this.vibrationUserPrefEnabled === value) {
      return;
    }
    // if value is null or undefined, we view it as initial value which is true.
    if (value === null || typeof(value) === 'undefined') {
      value = true;
    }

    var self = this;
    window.asyncStorage.setItem(SoundManager.VIBRATION_USER_PREF_KEY, value,
      function set_onsuccess() {
        self.vibrationUserPrefEnabled = value;
    });
  };

  /**
   * It checks the state to start/stop the CE accumulator.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.ceAccumulator = function sm_ceAccumulator() {
    if (this.isHeadsetConnected && this.getChannel() === 'content' &&
      this.currentVolume[this.currentChannel] >= this.CEWarningVol) {
      if (this.CEAccumulatorTime === 0) {
        this.showCEWarningDialog();
      } else {
        this.startAccumulator();
      }
    } else {
      this.stopAccumulator();
    }
  };

  /**
   * It handles the case of pressing volumeup hardware key when headset is
   * connected. This function mainly checks the state to start the CE
   * accumulator and change the volume.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.headsetVolumeup = function sm_headsetVolumeup() {
    if ((this.currentVolume[this.getChannel()] + 1) >= this.CEWarningVol &&
        this.getChannel() === 'content') {
      if (this.CEAccumulatorTime === 0) {
        var self = this;
        var okfn = function() {
          self.changeVolume(1);
          self.startAccumulator();
        };
        this.showCEWarningDialog(okfn);
      } else {
        this.startAccumulator();
        this.changeVolume(1);
      }
    } else {
      this.changeVolume(1);
    }
  };

  /**
   * It shows a warning dialog to tell user that the volume may hurt his/her
   * ear.
   *
   * @memberOf SoundManager.prototype
   * @param {Function} okfun the callback function when user press ok.
   */
  SoundManager.prototype.showCEWarningDialog = function sm_showCEDialog(okfn) {
    // Show dialog.

    var ceTitle = {
      'icon': '/style/sound_manager/images/icon_Volumewarning.png',
      'id': 'ceWarningtitle'
    };
    var ceMsg = 'ceWarningcontent';

    var cancel = {
      'title': 'ok'
    };

    var self = this;

    if (okfn instanceof Function) {
      cancel.callback = function onCancel() {
        okfn();
        Service.request('hideCustomDialog');
      };
    } else {
      cancel.callback = function onCancel() {
        self.startAccumulator();
        Service.request('hideCustomDialog');
      };
    }

    Service.request('showCustomDialog',
      ceTitle, ceMsg, cancel, null);
  };

  /**
   * It starts the CE accumulator.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.startAccumulator = function sm_startAccumulator() {
    if (this.CEAccumulatorID === null) {
      if (this.CEAccumulatorTime === 0) {
        this.CEAccumulatorTime = 1;
        this.CETimestamp = parseInt(new Date().getTime(), 10);
      }
      var self = this;
      this.CEAccumulatorID = window.setInterval(function ceCounter() {
        self.CEAccumulatorTime += SoundManager.TIME_ONE_MINUTE;
        self.CETimestamp = parseInt(new Date().getTime(), 10);
        if (self.CEAccumulatorTime > SoundManager.CE_RESET_TIME) {
          self.CEAccumulatorTime = 0; // reset time
          self.CETimestamp = 0; // reset timestamp
          self.stopAccumulator();
          self.showCEWarningDialog();
        }
      }, SoundManager.TIME_ONE_MINUTE);
    }
  };

  /**
   * It stops the CE accumulator.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.stopAccumulator = function sm_stopAccumulator() {
    if (this.CEAccumulatorID !== null) {
      window.clearInterval(this.CEAccumulatorID);
      this.CEAccumulatorID = null;
      if (this.CETimestamp !== 0) {
         this.CEAccumulatorTime = this.CEAccumulatorTime +
                       (parseInt(new Date().getTime(), 10) - this.CETimestamp);
      }
      window.asyncStorage.setItem(SoundManager.CACHE_CETIMES,
                                  this.CEAccumulatorTime);
    }
  };

  /**
   * It tells if it is currently on a call.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.isOnCall = function sm_isOnCall() {
    if (this.currentChannel == 'telephony') {
      return true;
    }

    // XXX: This work should be removed
    // once we could get telephony channel change event
    // https://bugzilla.mozilla.org/show_bug.cgi?id=819858
    var telephony = window.navigator.mozTelephony;
    if (!telephony) {
      return false;
    }

    return telephony.calls.some(function callIterator(call) {
        return (call.state == 'connected');
    });
  };

  /**
   * Bind setting handlers for each channel's volume change.
   * @memberOf SoundManager.prototype
   * @param {Function} callback Callback being called after each setting handler
   *                            has been invoked once.
   */
  SoundManager.prototype.bindVolumeSettingsHandlers = function sm_bindHdlers() {
    var callsMade = 0;
    var callbacksReceived = 0;
    var self = this;

    function observeSettingsVolumeChange(channel) {
      var setting = 'audio.volume.' + channel;
      SettingsListener.observe(setting, 5, function onChange(volume) {
        var settingsChange = function settings_change() {
          var max = SoundManager.MAX_VOLUME[channel];
          self.currentVolume[channel] =
            parseInt(Math.max(0, Math.min(max, volume)), 10);

          if (channel === 'content' && self.volumeFetched && volume > 0) {
            self.leaveSilentMode('content',
                            /* skip volume restore */ true);
          } else if (channel === 'notification' && volume > 0) {
            self.leaveSilentMode('notification',
                            /* skip volume restore */ true);
            self.muteIcon && self.muteIcon.update();
          } else if (channel === 'notification' && volume === 0) {
            // Enter silent mode when notification volume is 0
            // no matter who sets this value.
            self.enterSilentMode('notification');
            self.muteIcon && self.muteIcon.update();
          }

          if (!self.volumeFetched && ++callbacksReceived === callsMade) {
            self.fetchCachedVolume();
          }
        };

        // Initial loaded setting should always pass through
        // (one per channel)
        self.pendingRequest.wait(settingsChange, self);
      });
    }

    for (var channel in this.currentVolume) {
      callsMade++;
      observeSettingsVolumeChange(channel);
    }
  };

  /**
   * Fetch stored volume if it exists.
   * We should make sure this happens after settingsDB callback
   * after booting.
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.fetchCachedVolume = function sm_fetchCachedVolume() {
    if (this.volumeFetched) {
      return;
    }

    this.volumeFetched = true;
    this.pendingRequest.v(this.cachedChannels.length);
    var self = this;
    this.cachedChannels.forEach(
      function iterator(channel) {
        window.asyncStorage.getItem('content.volume',
          function onGettingCachedVolume(value) {
            if (!value) {
              self.pendingRequest.p();
              return;
            }

            self.cachedVolume[channel] = value;
            self.pendingRequest.p();
          });
      }
    );
  };

  /**
   * It handles the vibration case when changing the volume.
   * @memberOf SoundManager.prototype
   * @param {Number} curVolume the base volume
   * @param {Number} delta the offset of the change
   * @param {String} channel the target channel
   * @returns {Number} the volume value. (-1) is for slient mode and (0) is for
   *                   vibration mode
   */
  SoundManager.prototype.calculateVolume = function sm_calVol(
                                                    curVolume, delta, channel) {
    var volume = curVolume;
    if (channel === 'notification') {
      if (volume === 0 && !this.vibrationEnabled) {
        // This is for voluming up from Silent to Vibrate.
        // Let's take -1 as the silent state and
        // 0 as the vibrate state for easier calculation here.
        volume = -1;
      }
      volume += delta;
    } else {
      volume += delta;
    }
    return volume;
  };

  /**
   * It enables the vibration and returns the mute state.
   * @memberOf SoundManager.prototype
   * @param {Number} delta the offset of the change
   * @param {String} channel the target channel
   * @returns {String} the mute state
   */
  SoundManager.prototype.getVibrationAndMuteState = function sm_getState(
                                                    delta, channel) {
    var curVolume = this.currentVolume[channel];
    if (channel === 'notification') {
      var state;
      var volume = curVolume;
      if (volume === 0 && !this.vibrationEnabled) {
        // This is for voluming up from Silent to Vibrate.
        // Let's take -1 as the silent state and
        // 0 as the vibrate state for easier calculation here.
        volume = -1;
      }
      volume += delta;

      if (volume < 0) {
        state = 'MUTE';
        this.vibrationEnabled = false;
      } else if (volume === 0) {
        state = 'MUTE';
        this.vibrationEnabled = true;
      } else {
        // Restore the vibration setting only when leaving silent mode.
        if (curVolume <= 0) {
          this.vibrationEnabled = this.vibrationUserPrefEnabled;
        }
        state = 'OFF';
      }
      // Notify the user vibration is enabled when volume is 0.
      if (delta !== 0 && volume === 0 && this.vibrationEnabled) {
        this.notifyByVibrating();
      }

      return state;
    } else {
      if (curVolume + delta <= 0) {
        return 'MUTE';
      } else {
        return 'OFF';
      }
    }
  };

  /**
   * Notify the user vibration is on.
   *
   * @memberOf SoundManager.prototype
   */
  SoundManager.prototype.notifyByVibrating = function sm_notifyByVibrating() {
    window.navigator.vibrate(200);
  };

  /**
   * Entering silent mode.
   * @memberOf SoundManager.prototype
   * @param  {String} [channel="content"] Specify the channel name
   *                          which is going to enter silent mode.
   */
  SoundManager.prototype.enterSilentMode = function sm_enterSlient(channel) {
    if (!channel) {
      channel = 'content';
    }

    // Don't need to enter silent mode more than once.
    if (this.currentVolume[channel] === 0) {
      return;
    }

    var isCachedAlready =
      (this.cachedVolume[channel] === this.currentVolume[channel]);
    this.cachedVolume[channel] = this.currentVolume[channel];
    this.pendingRequest.v();

    var settingObject = {};
    settingObject['audio.volume.' + channel] = 0;
    var req = SettingsListener.getSettingsLock().set(settingObject);

    var self = this;
    req.onsuccess = function onSuccess() {
      self.pendingRequest.p();
      // Write to async storage only happens when
      // we haven't stored it before.
      // If the user presses the volume rockers repeatedly down and up,
      // between silent-mode/vibration mode/normal mode,
      // we won't repeatedly write the same value to storage.
      if (!isCachedAlready) {
        window.asyncStorage.setItem(channel + '.volume',
                                    self.cachedVolume[channel]);
      }
    };

    req.onerror = function onError() {
      self.pendingRequest.p();
    };
  };

  /**
   * Leaving silent mode.
   * @memberOf SoundManager.prototype
   * @param  {String} channel Specify the channel name
   *                          which is going to leave silent mode.
   * @param  {Boolean} skip_restore Specify to skip the volume restore or not.
   */
  SoundManager.prototype.leaveSilentMode = function sm_leaveSlient(channel,
                                                                 skip_restore) {
    if (!channel) {
      channel = 'content';
    }

    // We're leaving silent mode.
    if (!skip_restore &&
        (this.cachedVolume[channel] > 0 ||
         this.currentVolume[channel] === 0)) {
      var req;
      var settingObject = {};
      var self = this;

      // At least rollback to 1,
      // otherwise we don't really leave silent mode.
      settingObject['audio.volume.' + channel] =
        (this.cachedVolume[channel] > 0) ? this.cachedVolume[channel] : 1;

      this.pendingRequest.v();
      req = SettingsListener.getSettingsLock().set(settingObject);

      req.onsuccess = function onSuccess() {
        self.pendingRequest.p();
      };

      req.onerror = function onError() {
        self.pendingRequest.p();
      };
    }

    this.cachedVolume[channel] = -1;
  };

  /**
   * It updates the volume, turns on/off slient mode, and updates UI.
   * @memberOf SoundManager.prototype
   * @param  {Number} delta Specify the channel name
   * @param  {String} [channel] Specify the channel name. It uses getChannel()
   *                            when this argument is absent.
   */
  SoundManager.prototype.changeVolume = function sm_changeVolume(delta,
                                                                 channel) {
    channel = channel ? channel : this.getChannel();

    var vibrationEnabledOld = this.vibrationEnabled;
    var volume = this.calculateVolume(this.currentVolume[channel], delta,
                                       channel);
    this.muteState =
      this.getVibrationAndMuteState(delta, channel);

    // Silent mode entry point
    if (volume <= 0 && delta < 0 && channel == 'notification') {
      this.enterSilentMode('content');
    } else if (volume == 1 && delta > 0 && channel == 'notification' &&
               this.cachedVolume.content >= 0) {
      // Now since the active channel is notification channel,
      // we're leaving content silent mode and the same time restoring it.
      this.leaveSilentMode('content');

      // In the notification silent mode, volume rocker priority is higher
      // than stored notification volume value so we skip the restore.
      this.leaveSilentMode('notification', /*skip volume restore*/ true);
    }

    this.currentVolume[channel] = volume =
      Math.max(0, Math.min(SoundManager.MAX_VOLUME[channel], volume));

    var overlay = this.overlay;
    var notification = this.element;
    var overlayClasses = overlay.classList;
    var classes = notification.classList;

    switch (this.muteState) {
      case 'OFF':
        classes.remove('mute');
        break;
      case 'MUTE':
        classes.add('mute');
        break;
    }

    if (this.vibrationEnabled) {
      classes.add('vibration');
    } else {
      classes.remove('vibration');
    }

    if (vibrationEnabledOld != this.vibrationEnabled) {
      this.setVibrationEnabled(this.vibrationEnabled);
    }

    var steps =
      Array.prototype.slice.call(notification.querySelectorAll('div'), 0);

    var maxVolumeStep = (channel == 'telephony' || channel == 'bt_sco') ?
      volume + 1 : volume;

    for (var i = 0; i < steps.length; i++) {
      var step = steps[i];
      if (i < maxVolumeStep) {
        step.classList.add('active');
      } else {
        step.classList.remove('active');
      }
    }

    overlayClasses.add('volume');
    classes.add('visible');
    window.clearTimeout(this.activeTimerID);
    this.activeTimerID = window.setTimeout(function hideSound() {
      overlayClasses.remove('volume');
      classes.remove('visible');
    }, 1500);

    if (!window.navigator.mozSettings) {
      return;
    }

    this.pendingRequest.v();

    notification.dataset.channel = channel;

    var settingObject = {};
    settingObject['audio.volume.' + channel] = volume;

    var req = SettingsListener.getSettingsLock().set(settingObject);
    var self = this;

    req.onsuccess = function onSuccess() {
      self.pendingRequest.p();
    };

    req.onerror = function onError() {
      self.pendingRequest.p();
    };
  };

  /**
   * It enables the vibration.
   * @memberOf SoundManager.prototype
   * @param  {Boolean} enabled the enable state of vibration.
   */
  SoundManager.prototype.setVibrationEnabled = function sm_enableVib(enabled) {
    this.setVibrationEnabledCount++;
    SettingsListener.getSettingsLock().set({
      'vibration.enabled': enabled
    });
    this.muteIcon && this.muteIcon.update();
  };
  exports.SoundManager = SoundManager;
})(window);