/* 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', 'systemwindowaudiochannelsregistered' ]; AudioChannelService.SUB_MODULES = [ 'AudioChannelPolicy' ]; BaseModule.create(AudioChannelService, { name: 'AudioChannelService', DEBUG: false, // If true, all audio channels of System app are muted. _isSystemMuted: false, // A map contains playing audio channels. _activeAudioChannels: null, // A map contains audio channels' weights. _audioChannelWeights: 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._audioChannelWeights = new Map([ ['none', 0], ['system', 1], ['normal', 2], ['content', 3], ['alarm', 4], ['ringer', 5], ['telephony', 6], ['notification', 7], ['publicnotification', 8] ]); this._interruptedAudioChannels = []; this._muteSystemAudioChannels(); 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. */ _handle_hierarchytopmostwindowchanged: function() { this._topMostWindow = Service.query('getTopMostWindow'); if (this._topMostWindow) { this.debug(this._topMostWindow.name + ' is opened'); this._resumeAudioChannels(this._topMostWindow); } }, /** * Mute the System app's audio channels after they are registered. */ _handle_systemwindowaudiochannelsregistered: function() { this._muteSystemAudioChannels(); }, /** * 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 ); this._activeAudioChannels.forEach((audioChannel) => { this._handleAudioChannel(audioChannel); }); this._handleAudioChannel(audioChannel); if (!this._isAudioChannelInBackground(audioChannel)) { this.publish('visibleaudiochannelchanged', { channel: audioChannel.name }); } var channel = this._getTopPriorityAudioChannel(); this.publish('audiochannelchanged', { channel: channel }); } 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); if (audioChannel.name === 'notification' || audioChannel.name === 'publicnotification') { 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) { // FIXME: The `app` param should always have `audioChannels`, // then we don't need to check it. // Resume the app's audio channels. if (app) { app.audioChannels && app.audioChannels.forEach((audioChannel) => { audioChannel.isActive() && this._manageAudioChannels(audioChannel); audioChannel.isPlaying() && this._deleteAudioChannelFromInterruptedAudioChannels(audioChannel); }); } // Resume the latest interrupted audio channel. var audioChannel; 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(); } // Send audiochannelchanged evnet to SoundManager. var channel = this._getTopPriorityAudioChannel(); this.publish('audiochannelchanged', { channel: channel }); }, /** * Get the top priority audio channel from activeAudioChannels. * * @return {String} */ _getTopPriorityAudioChannel: function() { var channel = 'none'; this._activeAudioChannels.forEach((audioChannel) => { if (this._audioChannelWeights.get(audioChannel.name) > this._audioChannelWeights.get(channel)) { channel = audioChannel.name; } }); return channel; }, /** * 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. * @return {Boolean} */ _isAudioChannelInBackground: function(audioChannel) { var isAudioChannelInBackground = true; if ((this._topMostWindow && this._topMostWindow.instanceID === audioChannel.app.instanceID) || (audioChannel.isActive() && audioChannel.app.isInputMethod)) { isAudioChannelInBackground = false; } return isAudioChannelInBackground; }, _muteSystemAudioChannels: function() { var audioChannels = this.service.query('SystemWindow.getAudioChannels'); if (!this._isSystemMuted && audioChannels && audioChannels.size) { audioChannels.forEach((audioChannel) => { audioChannel .setPolicy({ isAllowedToPlay: false }) .proceedPolicy(); }); this._isSystemMuted = true; } } }); }());