Source: js/audio_channel_service.js

/* global BaseModule */
/* global Service */
'use strict';

(function() {
  /**
   * AudioChannelService manages audio channels in apps.
   * It could allow or deny a audio channel to play in some specific cases.
   * For example, Music app would like to play when FM app is already playing.
   * It will allow Music app to play, and pause FM app.
   *
   * @class AudioChannelService
   */
  var AudioChannelService = function() {};

  AudioChannelService.EVENTS = [
    'audiochannelstatechanged',
    'audiochanneldestroyed',
    'hierarchytopmostwindowchanged'
  ];

  AudioChannelService.SUB_MODULES = [
    'AudioChannelPolicy'
  ];

  BaseModule.create(AudioChannelService, {
    name: 'AudioChannelService',
    DEBUG: false,
    // A map contains playing audio channels.
    _activeAudioChannels: null,
    // An array contains audio channels could be resumed
    // when any other audio channel ends.
    _interruptedAudioChannels: null,
    // The most top app window.
    _topMostWindow: null,

    /**
     * Initial the module.
     */
    _start: function() {
      this._activeAudioChannels = new Map();
      this._interruptedAudioChannels = [];
      this.debug('Start Audio Channel Manager');
    },

    /**
     * Handle the audio chanel when it is active or in inactive.
     *
     * @param {Event} evt The event to handle.
     */
    _handle_audiochannelstatechanged: function(evt) {
      var audioChannel = evt.detail;
      this.debug('Audio channel state is ' + audioChannel.isActive());
      this._manageAudioChannels(audioChannel);
    },

    /**
     * Remove the window's audio channels from
     * `_activeAudioChannels` and `_interruptedAudioChannels`
     * when the window is terminated.
     *
     * @param {Event} evt The event to handle.
     */
    _handle_audiochanneldestroyed: function(evt) {
      var audioChannel = evt.detail;
      this._activeAudioChannels.delete(audioChannel.instanceID);
      this._deleteAudioChannelFromInterruptedAudioChannels(audioChannel);
      this._resumeAudioChannels();
    },

    /**
     * Handle the audio chanel when the app is in foreground or background.
     */
    _handle_hierarchytopmostwindowchanged: function() {
      if (this._topMostWindow && this._topMostWindow.audioChannels) {
        // Normal channel could not play in background.
        this.debug(this._topMostWindow.name + ' is closed');
        var audioChannel = this._topMostWindow.audioChannels.get('normal');
        if (audioChannel && audioChannel.isPlaying()) {
          audioChannel.setPolicy({ isAllowedToPlay: false });
          this._handleAudioChannel(audioChannel);
        }
      }
      this._topMostWindow = Service.query('getTopMostWindow');
      if (this._topMostWindow) {
        this.debug(this._topMostWindow.name + ' is opened');
        this._resumeAudioChannels(this._topMostWindow);
      }
    },

    /**
     * Play or pause the new audio channel and the active audio channels.
     *
     * @param {AudioChannelController} audioChannel The new audio channel.
     */
    _manageAudioChannels: function(audioChannel) {
      if (audioChannel.isActive()) {
        this.audioChannelPolicy.applyPolicy(
          audioChannel,
          this._activeAudioChannels,
          {
            isNewAudioChannelInBackground:
              this._isAudioChannelInBackground(audioChannel)
          }
        );
        this._activeAudioChannels.forEach((audioChannel) => {
          this._handleAudioChannel(audioChannel);
        });
        this._handleAudioChannel(audioChannel);
      } else {
        this._resetAudioChannel(audioChannel);
        this._resumeAudioChannels();
      }
    },

    /**
     * Set the audio channel as default state as muted,
     * and fade in the faded out audio channels.
     *
     * @param {AudioChannelController} audioChannel The audio channel.
     */
    _resetAudioChannel: function(audioChannel) {
      audioChannel.setPolicy({ isAllowedToPlay: false });
      this._handleAudioChannel(audioChannel);
      audioChannel.name === 'notification' &&
        this._fadeInFadedOutAudioChannels();
    },

    /**
     * Handle the audio channel
     * and update `_activeAudioChannels` and `_interruptedAudioChannels`.
     *
     * @param {AudioChannelController} audioChannel The audio channel.
     */
    _handleAudioChannel: function(audioChannel) {
      var policy = audioChannel.proceedPolicy().getPolicy();
      if (policy.isAllowedToPlay) {
        this._activeAudioChannels.set(
          audioChannel.instanceID,
          audioChannel
        );
        this.debug('Playing ' + audioChannel.instanceID);
      } else {
        this._activeAudioChannels.delete(audioChannel.instanceID);
        if (policy.isNeededToResumeWhenOtherEnds) {
          this._interruptedAudioChannels.push(audioChannel);
          this.debug('Interrupted ' + audioChannel.instanceID);
        }
      }
    },

    /**
     * Fade in all faded out audio channels.
     */
    _fadeInFadedOutAudioChannels: function() {
      this._activeAudioChannels.forEach((audioChannel) => {
        audioChannel.isFadingOut() && audioChannel
          .setPolicy({ isNeededToFadeOut: false })
          .proceedPolicy();
      });
    },

    /**
     * Resume interrupted audio channels.
     *
     * @param {AppWindow} [app] The app window in foreground.
     */
    _resumeAudioChannels: function(app) {
      var audioChannel;
      // Resume the app's audio channels.
      app && app.audioChannels.forEach((audioChannel) => {
        audioChannel.isActive() && this._manageAudioChannels(audioChannel);
        if (audioChannel.isPlaying()) {
          this._deleteAudioChannelFromInterruptedAudioChannels(audioChannel);
        }
      });
      // Resume the latest interrupted audio channel.
      var length = this._interruptedAudioChannels.length;
      if (this._activeAudioChannels.size === 0 && length) {
        audioChannel = this._interruptedAudioChannels[length - 1];
        audioChannel.setPolicy({ isAllowedToPlay: true });
        this._handleAudioChannel(audioChannel);
        audioChannel.isPlaying() && this._interruptedAudioChannels.pop();
      }
    },

    /**
     * Delete the audio channel from `_interruptedAudioChannels` array.
     *
     * @param {AudioChannelController} audioChannel
     * The audio channel want to delete.
     */
    _deleteAudioChannelFromInterruptedAudioChannels: function(audioChannel) {
      var index = this._interruptedAudioChannels
        .findIndex(function(interruptedAudioChannel) {
          return interruptedAudioChannel.instanceID ===
            audioChannel.instanceID;
      });
      index !== -1 && this._interruptedAudioChannels.splice(index, 1);
    },

    /**
     * Check the audio channel is in background or not.
     *
     * @param {AudioChannelController} audioChannel The audio channel.
     * @retrun {Boolean}
     */
    _isAudioChannelInBackground: function(audioChannel) {
      var isAudioChannelInBackground = true;
      if (this._topMostWindow &&
          this._topMostWindow.instanceID === audioChannel.app.instanceID) {
        isAudioChannelInBackground = false;
      }
      return isAudioChannelInBackground;
    }
  });
}());