michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cc = Components.classes; michael@0: michael@0: const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE; michael@0: const SETTING_DEBUG_ENABLED = "geolocation.debugging.enabled"; michael@0: const SETTING_CHANGED_TOPIC = "mozsettings-changed"; michael@0: michael@0: let gLoggingEnabled = false; michael@0: michael@0: // if we don't see any wifi responses in 5 seconds, send the request. michael@0: let gTimeToWaitBeforeSending = 5000; //ms michael@0: michael@0: let gWifiScanningEnabled = true; michael@0: let gWifiResults; michael@0: michael@0: let gCellScanningEnabled = false; michael@0: let gCellResults; michael@0: michael@0: function LOG(aMsg) { michael@0: if (gLoggingEnabled) { michael@0: aMsg = "*** WIFI GEO: " + aMsg + "\n"; michael@0: Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg); michael@0: dump(aMsg); michael@0: } michael@0: } michael@0: michael@0: function CachedRequest(loc, cellInfo, wifiList) { michael@0: this.location = loc; michael@0: michael@0: let wifis = new Set(); michael@0: if (wifiList) { michael@0: for (let i = 0; i < wifiList.length; i++) { michael@0: wifis.add(wifiList[i].macAddress); michael@0: } michael@0: } michael@0: michael@0: // Use only these values for equality michael@0: // (the JSON will contain additional values in future) michael@0: function makeCellKey(cell) { michael@0: return "" + cell.radio + ":" + cell.mobileCountryCode + ":" + michael@0: cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" + michael@0: cell.cellId; michael@0: } michael@0: michael@0: let cells = new Set(); michael@0: if (cellInfo) { michael@0: for (let i = 0; i < cellInfo.length; i++) { michael@0: cells.add(makeCellKey(cellInfo[i])); michael@0: } michael@0: } michael@0: michael@0: this.hasCells = () => cells.size > 0; michael@0: michael@0: this.hasWifis = () => wifis.size > 0; michael@0: michael@0: // if fields match michael@0: this.isCellEqual = function(cellInfo) { michael@0: if (!this.hasCells()) { michael@0: return false; michael@0: } michael@0: michael@0: let len1 = cells.size; michael@0: let len2 = cellInfo.length; michael@0: michael@0: if (len1 != len2) { michael@0: LOG("cells not equal len"); michael@0: return false; michael@0: } michael@0: michael@0: for (let i = 0; i < len2; i++) { michael@0: if (!cells.has(makeCellKey(cellInfo[i]))) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: }; michael@0: michael@0: // if 50% of the SSIDS match michael@0: this.isWifiApproxEqual = function(wifiList) { michael@0: if (!this.hasWifis()) { michael@0: return false; michael@0: } michael@0: michael@0: // if either list is a 50% subset of the other, they are equal michael@0: let common = 0; michael@0: for (let i = 0; i < wifiList.length; i++) { michael@0: if (wifis.has(wifiList[i].macAddress)) { michael@0: common++; michael@0: } michael@0: } michael@0: let kPercentMatch = 0.5; michael@0: return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch); michael@0: }; michael@0: michael@0: this.isGeoip = function() { michael@0: return !this.hasCells() && !this.hasWifis(); michael@0: }; michael@0: michael@0: this.isCellAndWifi = function() { michael@0: return this.hasCells() && this.hasWifis(); michael@0: }; michael@0: michael@0: this.isCellOnly = function() { michael@0: return this.hasCells() && !this.hasWifis(); michael@0: }; michael@0: michael@0: this.isWifiOnly = function() { michael@0: return this.hasWifis() && !this.hasCells(); michael@0: }; michael@0: } michael@0: michael@0: let gCachedRequest = null; michael@0: let gDebugCacheReasoning = ""; // for logging the caching logic michael@0: michael@0: // This function serves two purposes: michael@0: // 1) do we have a cached request michael@0: // 2) is the cached request better than what newCell and newWifiList will obtain michael@0: // If the cached request exists, and we know it to have greater accuracy michael@0: // by the nature of its origin (wifi/cell/geoip), use its cached location. michael@0: // michael@0: // If there is more source info than the cached request had, return false michael@0: // In other cases, MLS is known to produce better/worse accuracy based on the michael@0: // inputs, so base the decision on that. michael@0: function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) michael@0: { michael@0: gDebugCacheReasoning = ""; michael@0: let isNetworkRequestCacheEnabled = true; michael@0: try { michael@0: // Mochitest needs this pref to simulate request failure michael@0: isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled"); michael@0: if (!isNetworkRequestCacheEnabled) { michael@0: gCachedRequest = null; michael@0: } michael@0: } catch (e) {} michael@0: michael@0: if (!gCachedRequest || !isNetworkRequestCacheEnabled) { michael@0: gDebugCacheReasoning = "No cached data"; michael@0: return false; michael@0: } michael@0: michael@0: if (!newCell && !newWifiList) { michael@0: gDebugCacheReasoning = "New req. is GeoIP."; michael@0: return true; michael@0: } michael@0: michael@0: if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) { michael@0: gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; michael@0: return false; michael@0: } michael@0: michael@0: if (newCell && gCachedRequest.isWifiOnly()) { michael@0: // In order to know if a cell-only request should trump a wifi-only request michael@0: // need to know if wifi is low accuracy. >5km would be VERY low accuracy, michael@0: // it is worth trying the cell michael@0: var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; michael@0: gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; michael@0: return isHighAccuracyWifi; michael@0: } michael@0: michael@0: let hasEqualCells = false; michael@0: if (newCell) { michael@0: hasEqualCells = gCachedRequest.isCellEqual(newCell); michael@0: } michael@0: michael@0: let hasEqualWifis = false; michael@0: if (newWifiList) { michael@0: hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); michael@0: } michael@0: michael@0: gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; michael@0: michael@0: if (gCachedRequest.isCellOnly()) { michael@0: gDebugCacheReasoning += ", Cell only."; michael@0: if (hasEqualCells) { michael@0: return true; michael@0: } michael@0: } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { michael@0: gDebugCacheReasoning +=", Wifi only." michael@0: return true; michael@0: } else if (gCachedRequest.isCellAndWifi()) { michael@0: gDebugCacheReasoning += ", Cache has Cell+Wifi."; michael@0: if ((hasEqualCells && hasEqualWifis) || michael@0: (!newWifiList && hasEqualCells) || michael@0: (!newCell && hasEqualWifis)) michael@0: { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) { michael@0: this.latitude = lat; michael@0: this.longitude = lon; michael@0: this.accuracy = acc; michael@0: this.altitude = alt; michael@0: this.altitudeAccuracy = altacc; michael@0: } michael@0: michael@0: WifiGeoCoordsObject.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords]) michael@0: }; michael@0: michael@0: function WifiGeoPositionObject(lat, lng, acc) { michael@0: this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0); michael@0: this.address = null; michael@0: this.timestamp = Date.now(); michael@0: } michael@0: michael@0: WifiGeoPositionObject.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition]) michael@0: }; michael@0: michael@0: function WifiGeoPositionProvider() { michael@0: try { michael@0: gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled"); michael@0: } catch (e) {} michael@0: michael@0: try { michael@0: gTimeToWaitBeforeSending = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending"); michael@0: } catch (e) {} michael@0: michael@0: try { michael@0: gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan"); michael@0: } catch (e) {} michael@0: michael@0: try { michael@0: gCellScanningEnabled = Services.prefs.getBoolPref("geo.cell.scan"); michael@0: } catch (e) {} michael@0: michael@0: this.wifiService = null; michael@0: this.timeoutTimer = null; michael@0: this.started = false; michael@0: } michael@0: michael@0: WifiGeoPositionProvider.prototype = { michael@0: classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIGeolocationProvider, michael@0: Ci.nsIWifiListener, michael@0: Ci.nsITimerCallback, michael@0: Ci.nsIObserver]), michael@0: listener: null, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic != SETTING_CHANGED_TOPIC) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: let setting = JSON.parse(aData); michael@0: if (setting.key != SETTING_DEBUG_ENABLED) { michael@0: return; michael@0: } michael@0: gLoggingEnabled = setting.value; michael@0: } catch (e) { michael@0: } michael@0: }, michael@0: michael@0: startup: function() { michael@0: if (this.started) michael@0: return; michael@0: michael@0: this.started = true; michael@0: let settingsCallback = { michael@0: handle: function(name, result) { michael@0: gLoggingEnabled = result && result.value === true ? true : false; michael@0: }, michael@0: michael@0: handleError: function(message) { michael@0: gLoggingEnabled = false; michael@0: LOG("settings callback threw an exception, dropping"); michael@0: } michael@0: }; michael@0: michael@0: try { michael@0: Services.obs.addObserver(this, SETTING_CHANGED_TOPIC, false); michael@0: let settings = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService); michael@0: settings.createLock().get(SETTING_DEBUG_ENABLED, settingsCallback); michael@0: } catch(ex) { michael@0: // This platform doesn't have the settings interface, and that is just peachy michael@0: } michael@0: michael@0: if (gWifiScanningEnabled) { michael@0: this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Components.interfaces.nsIWifiMonitor); michael@0: this.wifiService.startWatching(this); michael@0: } michael@0: this.timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this.timeoutTimer.initWithCallback(this, michael@0: gTimeToWaitBeforeSending, michael@0: this.timeoutTimer.TYPE_REPEATING_SLACK); michael@0: LOG("startup called."); michael@0: }, michael@0: michael@0: watch: function(c) { michael@0: this.listener = c; michael@0: }, michael@0: michael@0: shutdown: function() { michael@0: LOG("shutdown called"); michael@0: if (this.started == false) { michael@0: return; michael@0: } michael@0: michael@0: // Without clearing this, we could end up using the cache almost indefinitely michael@0: // TODO: add logic for cache lifespan, for now just be safe and clear it michael@0: gCachedRequest = null; michael@0: michael@0: if (this.timeoutTimer) { michael@0: this.timeoutTimer.cancel(); michael@0: this.timeoutTimer = null; michael@0: } michael@0: michael@0: if(this.wifiService) { michael@0: this.wifiService.stopWatching(this); michael@0: this.wifiService = null; michael@0: } michael@0: michael@0: Services.obs.removeObserver(this, SETTING_CHANGED_TOPIC); michael@0: michael@0: this.listener = null; michael@0: this.started = false; michael@0: }, michael@0: michael@0: setHighAccuracy: function(enable) { michael@0: }, michael@0: michael@0: onChange: function(accessPoints) { michael@0: michael@0: function isPublic(ap) { michael@0: let mask = "_nomap" michael@0: let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length); michael@0: if (result != -1) { michael@0: LOG("Filtering out " + ap.ssid + " " + result); michael@0: } michael@0: return result; michael@0: }; michael@0: michael@0: function sort(a, b) { michael@0: return b.signal - a.signal; michael@0: }; michael@0: michael@0: function encode(ap) { michael@0: return { 'macAddress': ap.mac, 'signalStrength': ap.signal }; michael@0: }; michael@0: michael@0: if (accessPoints) { michael@0: gWifiResults = accessPoints.filter(isPublic).sort(sort).map(encode); michael@0: } else { michael@0: gWifiResults = null; michael@0: } michael@0: }, michael@0: michael@0: onError: function (code) { michael@0: LOG("wifi error: " + code); michael@0: }, michael@0: michael@0: updateMobileInfo: function() { michael@0: LOG("updateMobileInfo called"); michael@0: try { michael@0: let radio = Cc["@mozilla.org/ril;1"] michael@0: .getService(Ci.nsIRadioInterfaceLayer) michael@0: .getRadioInterface(0); michael@0: michael@0: let iccInfo = radio.rilContext.iccInfo; michael@0: let cell = radio.rilContext.voice.cell; michael@0: michael@0: LOG("mcc: " + iccInfo.mcc); michael@0: LOG("mnc: " + iccInfo.mnc); michael@0: LOG("cid: " + cell.gsmCellId); michael@0: LOG("lac: " + cell.gsmLocationAreaCode); michael@0: michael@0: gCellResults = [{ michael@0: "radio": "gsm", michael@0: "mobileCountryCode": iccInfo.mcc, michael@0: "mobileNetworkCode": iccInfo.mnc, michael@0: "locationAreaCode": cell.gsmLocationAreaCode, michael@0: "cellId": cell.gsmCellId, michael@0: }]; michael@0: } catch (e) { michael@0: gCellResults = null; michael@0: } michael@0: }, michael@0: michael@0: notify: function (timeoutTimer) { michael@0: let data = {}; michael@0: if (gWifiResults) { michael@0: data.wifiAccessPoints = gWifiResults; michael@0: } michael@0: michael@0: if (gCellScanningEnabled) { michael@0: this.updateMobileInfo(); michael@0: } michael@0: michael@0: if (gCellResults && gCellResults.length > 0) { michael@0: data.cellTowers = gCellResults; michael@0: } michael@0: michael@0: let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers, michael@0: data.wifiAccessPoints); michael@0: michael@0: LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); michael@0: michael@0: if (useCached) { michael@0: gCachedRequest.location.timestamp = Date.now(); michael@0: this.listener.update(gCachedRequest.location); michael@0: return; michael@0: } michael@0: michael@0: // From here on, do a network geolocation request // michael@0: let url = Services.urlFormatter.formatURLPref("geo.wifi.uri"); michael@0: let listener = this.listener; michael@0: LOG("Sending request: " + url + "\n"); michael@0: michael@0: let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: michael@0: listener.locationUpdatePending(); michael@0: michael@0: try { michael@0: xhr.open("POST", url, true); michael@0: } catch (e) { michael@0: listener.notifyError(POSITION_UNAVAILABLE); michael@0: return; michael@0: } michael@0: xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); michael@0: xhr.responseType = "json"; michael@0: xhr.mozBackgroundRequest = true; michael@0: xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS; michael@0: xhr.onerror = function() { michael@0: listener.notifyError(POSITION_UNAVAILABLE); michael@0: }; michael@0: xhr.onload = function() { michael@0: LOG("gls returned status: " + xhr.status + " --> " + JSON.stringify(xhr.response)); michael@0: if ((xhr.channel instanceof Ci.nsIHttpChannel && xhr.status != 200) || michael@0: !xhr.response || !xhr.response.location) { michael@0: listener.notifyError(POSITION_UNAVAILABLE); michael@0: return; michael@0: } michael@0: michael@0: let newLocation = new WifiGeoPositionObject(xhr.response.location.lat, michael@0: xhr.response.location.lng, michael@0: xhr.response.accuracy); michael@0: michael@0: listener.update(newLocation); michael@0: gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints); michael@0: }; michael@0: michael@0: var requestData = JSON.stringify(data); michael@0: gWifiResults = gCellResults = null; michael@0: LOG("sending " + requestData); michael@0: xhr.send(requestData); michael@0: }, michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);