/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict" let DEBUG = 0; if (DEBUG) debug = function(s) { dump("-*- DOMFMRadioParent component: " + s + "\n"); }; else debug = function(s) {}; const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; const MOZ_SETTINGS_CHANGED_OBSERVER_TOPIC = "mozsettings-changed"; const PROFILE_BEFORE_CHANGE_OBSERVER_TOPIC = "profile-before-change"; const BAND_87500_108000_kHz = 1; const BAND_76000_108000_kHz = 2; const BAND_76000_90000_kHz = 3; const FM_BANDS = { }; FM_BANDS[BAND_76000_90000_kHz] = { lower: 76000, upper: 90000 }; FM_BANDS[BAND_87500_108000_kHz] = { lower: 87500, upper: 108000 }; FM_BANDS[BAND_76000_108000_kHz] = { lower: 76000, upper: 108000 }; const BAND_SETTING_KEY = "fmRadio.band"; const CHANNEL_WIDTH_SETTING_KEY = "fmRadio.channelWidth"; // Hal types const CHANNEL_WIDTH_200KHZ = 200; const CHANNEL_WIDTH_100KHZ = 100; const CHANNEL_WIDTH_50KHZ = 50; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageListenerManager"); XPCOMUtils.defineLazyGetter(this, "FMRadio", function() { return Cc["@mozilla.org/fmradio;1"].getService(Ci.nsIFMRadio); }); XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", "@mozilla.org/settingsService;1", "nsISettingsService"); this.EXPORTED_SYMBOLS = ["DOMFMRadioParent"]; this.DOMFMRadioParent = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISettingsServiceCallback]), _initialized: false, /* Indicates if the FM radio is currently enabled */ _isEnabled: false, /* Indicates if the FM radio is currently being enabled */ _enabling: false, /* Current frequency in KHz */ _currentFrequency: 0, /* Current band setting */ _currentBand: BAND_87500_108000_kHz, /* Current channel width */ _currentWidth: CHANNEL_WIDTH_100KHZ, /* Indicates if the antenna is currently available */ _antennaAvailable: true, _seeking: false, _seekingCallback: null, init: function() { if (this._initialized === true) { return; } this._initialized = true; this._messages = ["DOMFMRadio:enable", "DOMFMRadio:disable", "DOMFMRadio:setFrequency", "DOMFMRadio:getCurrentBand", "DOMFMRadio:getPowerState", "DOMFMRadio:getFrequency", "DOMFMRadio:getAntennaState", "DOMFMRadio:seekUp", "DOMFMRadio:seekDown", "DOMFMRadio:cancelSeek" ]; this._messages.forEach(function(msgName) { ppmm.addMessageListener(msgName, this); }.bind(this)); Services.obs.addObserver(this, PROFILE_BEFORE_CHANGE_OBSERVER_TOPIC, false); Services.obs.addObserver(this, MOZ_SETTINGS_CHANGED_OBSERVER_TOPIC, false); this._updatePowerState(); // Get the band setting and channel width setting let lock = gSettingsService.createLock(); lock.get(BAND_SETTING_KEY, this); lock.get(CHANNEL_WIDTH_SETTING_KEY, this); this._updateAntennaState(); let self = this; FMRadio.onantennastatechange = function onantennachange() { self._updateAntennaState(); }; debug("Initialized"); }, // nsISettingsServiceCallback handle: function(aName, aResult) { if (aName == BAND_SETTING_KEY) { this._updateBand(aResult); } else if (aName == CHANNEL_WIDTH_SETTING_KEY) { this._updateChannelWidth(aResult); } }, handleError: function(aErrorMessage) { this._updateBand(BAND_87500_108000_kHz); this._updateChannelWidth(CHANNEL_WIDTH_100KHZ); }, _updateAntennaState: function() { let antennaState = FMRadio.isAntennaAvailable; if (antennaState != this._antennaAvailable) { this._antennaAvailable = antennaState; ppmm.broadcastAsyncMessage("DOMFMRadio:antennaChange", { }); } }, _updateBand: function(band) { switch (parseInt(band)) { case BAND_87500_108000_kHz: case BAND_76000_108000_kHz: case BAND_76000_90000_kHz: this._currentBand = band; break; } }, _updateChannelWidth: function(channelWidth) { switch (parseInt(channelWidth)) { case CHANNEL_WIDTH_50KHZ: case CHANNEL_WIDTH_100KHZ: case CHANNEL_WIDTH_200KHZ: this._currentWidth = channelWidth; break; } }, /** * Update and cache the current frequency. * Send frequency change message if the frequency is changed. * The returned boolean value indicates if the frequency is changed. */ _updateFrequency: function() { let frequency = FMRadio.frequency; if (frequency != this._currentFrequency) { this._currentFrequency = frequency; ppmm.broadcastAsyncMessage("DOMFMRadio:frequencyChange", { }); return true; } return false; }, /** * Update and cache the power state of the FM radio. * Send message if the power state is changed. */ _updatePowerState: function() { let enabled = FMRadio.enabled; if (this._isEnabled != enabled) { this._isEnabled = enabled; ppmm.broadcastAsyncMessage("DOMFMRadio:powerStateChange", { }); // If the FM radio is enabled, update the current frequency immediately, if (enabled) { this._updateFrequency(); } } }, _onSeekComplete: function(success) { if (this._seeking) { this._seeking = false; if (this._seekingCallback) { this._seekingCallback(success); this._seekingCallback = null; } } }, /** * Seek the next channel with given direction. * Only one seek action is allowed at once. */ _seekStation: function(direction, aMessage) { let msg = aMessage.json || { }; let messageName = aMessage.name + ":Return"; // If the FM radio is disabled, do not execute the seek action. if(!this._isEnabled) { this._sendMessage(messageName, false, null, msg); return; } let self = this; function callback(success) { debug("Seek completed."); if (!success) { self._sendMessage(messageName, false, null, msg); } else { // Make sure the FM app will get the right frequency. self._updateFrequency(); self._sendMessage(messageName, true, null, msg); } } if (this._seeking) { // Pass a boolean value to the callback which indicates that // the seek action failed. callback(false); return; } this._seekingCallback = callback; this._seeking = true; let self = this; FMRadio.seek(direction); FMRadio.addEventListener("seekcomplete", function FM_onSeekComplete() { FMRadio.removeEventListener("seekcomplete", FM_onSeekComplete); self._onSeekComplete(true); }); }, /** * Round the frequency to match the range of frequency and the channel width. * If the given frequency is out of range, return null. * For example: * - lower: 87.5MHz, upper: 108MHz, channel width: 0.2MHz * 87600 is rounded to 87700 * 87580 is rounded to 87500 * 109000 is not rounded, null will be returned */ _roundFrequency: function(frequencyInKHz) { if (frequencyInKHz < FM_BANDS[this._currentBand].lower || frequencyInKHz > FM_BANDS[this._currentBand].upper) { return null; } let partToBeRounded = frequencyInKHz - FM_BANDS[this._currentBand].lower; let roundedPart = Math.round(partToBeRounded / this._currentWidth) * this._currentWidth; return FM_BANDS[this._currentBand].lower + roundedPart; }, observe: function(aSubject, aTopic, aData) { switch (aTopic) { case PROFILE_BEFORE_CHANGE_OBSERVER_TOPIC: this._messages.forEach(function(msgName) { ppmm.removeMessageListener(msgName, this); }.bind(this)); Services.obs.removeObserver(this, PROFILE_BEFORE_CHANGE_OBSERVER_TOPIC); Services.obs.removeObserver(this, MOZ_SETTINGS_CHANGED_OBSERVER_TOPIC); ppmm = null; this._messages = null; break; case MOZ_SETTINGS_CHANGED_OBSERVER_TOPIC: let setting = JSON.parse(aData); this.handleMozSettingsChanged(setting); break; } }, _sendMessage: function(message, success, data, msg) { msg.manager.sendAsyncMessage(message + (success ? ":OK" : ":NO"), { data: data, rid: msg.rid, mid: msg.mid }); }, handleMozSettingsChanged: function(settings) { switch (settings.key) { case BAND_SETTING_KEY: this._updateBand(settings.value); break; case CHANNEL_WIDTH_SETTING_KEY: this._updateChannelWidth(settings.value); break; } }, _enableFMRadio: function(msg) { let frequencyInKHz = this._roundFrequency(msg.data * 1000); // If the FM radio is already enabled or it is currently being enabled // or the given frequency is out of range, return false. if (this._isEnabled || this._enabling || !frequencyInKHz) { this._sendMessage("DOMFMRadio:enable:Return", false, null, msg); return; } this._enabling = true; let self = this; FMRadio.addEventListener("enabled", function on_enabled() { debug("FM Radio is enabled!"); self._enabling = false; FMRadio.removeEventListener("enabled", on_enabled); // To make sure the FM app will get right frequency after the FM // radio is enabled, we have to set the frequency first. FMRadio.setFrequency(frequencyInKHz); // Update the current frequency without sending 'frequencyChange' // msg, to make sure the FM app will get the right frequency when the // 'enabled' event is fired. self._currentFrequency = FMRadio.frequency; self._updatePowerState(); self._sendMessage("DOMFMRadio:enable:Return", true, null, msg); // The frequency is changed from 'null' to some number, so we should // send the 'frequencyChange' message manually. ppmm.broadcastAsyncMessage("DOMFMRadio:frequencyChange", { }); }); FMRadio.enable({ lowerLimit: FM_BANDS[self._currentBand].lower, upperLimit: FM_BANDS[self._currentBand].upper, channelWidth: self._currentWidth // 100KHz by default }); }, _disableFMRadio: function(msg) { // If the FM radio is already disabled, return false. if (!this._isEnabled) { this._sendMessage("DOMFMRadio:disable:Return", false, null, msg); return; } let self = this; FMRadio.addEventListener("disabled", function on_disabled() { debug("FM Radio is disabled!"); FMRadio.removeEventListener("disabled", on_disabled); self._updatePowerState(); self._sendMessage("DOMFMRadio:disable:Return", true, null, msg); // If the FM Radio is currently seeking, no fail-to-seek or similar // event will be fired, execute the seek callback manually. self._onSeekComplete(false); }); FMRadio.disable(); }, receiveMessage: function(aMessage) { let msg = aMessage.json || {}; msg.manager = aMessage.target; let ret = 0; let self = this; switch (aMessage.name) { case "DOMFMRadio:enable": self._enableFMRadio(msg); break; case "DOMFMRadio:disable": self._disableFMRadio(msg); break; case "DOMFMRadio:setFrequency": let frequencyInKHz = self._roundFrequency(msg.data * 1000); // If the FM radio is disabled or the given frequency is out of range, // skip to set frequency and send back the False message immediately. if (!self._isEnabled || !frequencyInKHz) { self._sendMessage("DOMFMRadio:setFrequency:Return", false, null, msg); } else { FMRadio.setFrequency(frequencyInKHz); self._sendMessage("DOMFMRadio:setFrequency:Return", true, null, msg); this._updateFrequency(); } break; case "DOMFMRadio:getCurrentBand": // this message is sync return { lower: FM_BANDS[self._currentBand].lower / 1000, // in MHz upper: FM_BANDS[self._currentBand].upper / 1000, // in MHz channelWidth: self._currentWidth / 1000 // in MHz }; case "DOMFMRadio:getPowerState": // this message is sync return self._isEnabled; case "DOMFMRadio:getFrequency": // this message is sync return self._isEnabled ? this._currentFrequency / 1000 : null; // in MHz case "DOMFMRadio:getAntennaState": // this message is sync return self._antennaAvailable; case "DOMFMRadio:seekUp": self._seekStation(Ci.nsIFMRadio.SEEK_DIRECTION_UP, aMessage); break; case "DOMFMRadio:seekDown": self._seekStation(Ci.nsIFMRadio.SEEK_DIRECTION_DOWN, aMessage); break; case "DOMFMRadio:cancelSeek": // If the FM radio is disabled, or the FM radio is not currently // seeking, do not execute the cancel seek action. if (!self._isEnabled || !self._seeking) { self._sendMessage("DOMFMRadio:cancelSeek:Return", false, null, msg); } else { FMRadio.cancelSeek(); // No fail-to-seek or similar event will be fired from the hal part, // so execute the seek callback here manually. this._onSeekComplete(false); // The FM radio will stop at one frequency without any event, so we need to // update the current frequency, make sure the FM app will get the right frequency. this._updateFrequency(); self._sendMessage("DOMFMRadio:cancelSeek:Return", true, null, msg); } break; } } }; DOMFMRadioParent.init();