diff options
Diffstat (limited to 'dom/system/NetworkGeolocationProvider.js')
-rw-r--r-- | dom/system/NetworkGeolocationProvider.js | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/dom/system/NetworkGeolocationProvider.js b/dom/system/NetworkGeolocationProvider.js new file mode 100644 index 000000000..ea2abe55f --- /dev/null +++ b/dom/system/NetworkGeolocationProvider.js @@ -0,0 +1,489 @@ +/* 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"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE; +const SETTINGS_DEBUG_ENABLED = "geolocation.debugging.enabled"; +const SETTINGS_CHANGED_TOPIC = "mozsettings-changed"; +const SETTINGS_WIFI_ENABLED = "wifi.enabled"; + +var gLoggingEnabled = false; + +/* + The gLocationRequestTimeout controls how long we wait on receiving an update + from the Wifi subsystem. If this timer fires, we believe the Wifi scan has + had a problem and we no longer can use Wifi to position the user this time + around (we will continue to be hopeful that Wifi will recover). + + This timeout value is also used when Wifi scanning is disabled (see + gWifiScanningEnabled). In this case, we use this timer to collect cell/ip + data and xhr it to the location server. +*/ + +var gLocationRequestTimeout = 5000; + +var gWifiScanningEnabled = true; + +function LOG(aMsg) { + if (gLoggingEnabled) { + aMsg = "*** WIFI GEO: " + aMsg + "\n"; + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg); + dump(aMsg); + } +} + +function CachedRequest(loc, cellInfo, wifiList) { + this.location = loc; + + let wifis = new Set(); + if (wifiList) { + for (let i = 0; i < wifiList.length; i++) { + wifis.add(wifiList[i].macAddress); + } + } + + // Use only these values for equality + // (the JSON will contain additional values in future) + function makeCellKey(cell) { + return "" + cell.radio + ":" + cell.mobileCountryCode + ":" + + cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" + + cell.cellId; + } + + let cells = new Set(); + if (cellInfo) { + for (let i = 0; i < cellInfo.length; i++) { + cells.add(makeCellKey(cellInfo[i])); + } + } + + this.hasCells = () => cells.size > 0; + + this.hasWifis = () => wifis.size > 0; + + // if fields match + this.isCellEqual = function(cellInfo) { + if (!this.hasCells()) { + return false; + } + + let len1 = cells.size; + let len2 = cellInfo.length; + + if (len1 != len2) { + LOG("cells not equal len"); + return false; + } + + for (let i = 0; i < len2; i++) { + if (!cells.has(makeCellKey(cellInfo[i]))) { + return false; + } + } + return true; + }; + + // if 50% of the SSIDS match + this.isWifiApproxEqual = function(wifiList) { + if (!this.hasWifis()) { + return false; + } + + // if either list is a 50% subset of the other, they are equal + let common = 0; + for (let i = 0; i < wifiList.length; i++) { + if (wifis.has(wifiList[i].macAddress)) { + common++; + } + } + let kPercentMatch = 0.5; + return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch); + }; + + this.isGeoip = function() { + return !this.hasCells() && !this.hasWifis(); + }; + + this.isCellAndWifi = function() { + return this.hasCells() && this.hasWifis(); + }; + + this.isCellOnly = function() { + return this.hasCells() && !this.hasWifis(); + }; + + this.isWifiOnly = function() { + return this.hasWifis() && !this.hasCells(); + }; + } + +var gCachedRequest = null; +var gDebugCacheReasoning = ""; // for logging the caching logic + +// This function serves two purposes: +// 1) do we have a cached request +// 2) is the cached request better than what newCell and newWifiList will obtain +// If the cached request exists, and we know it to have greater accuracy +// by the nature of its origin (wifi/cell/geoip), use its cached location. +// +// If there is more source info than the cached request had, return false +// In other cases, MLS is known to produce better/worse accuracy based on the +// inputs, so base the decision on that. +function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) +{ + gDebugCacheReasoning = ""; + let isNetworkRequestCacheEnabled = true; + try { + // Mochitest needs this pref to simulate request failure + isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled"); + if (!isNetworkRequestCacheEnabled) { + gCachedRequest = null; + } + } catch (e) {} + + if (!gCachedRequest || !isNetworkRequestCacheEnabled) { + gDebugCacheReasoning = "No cached data"; + return false; + } + + if (!newCell && !newWifiList) { + gDebugCacheReasoning = "New req. is GeoIP."; + return true; + } + + if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) { + gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; + return false; + } + + if (newCell && gCachedRequest.isWifiOnly()) { + // In order to know if a cell-only request should trump a wifi-only request + // need to know if wifi is low accuracy. >5km would be VERY low accuracy, + // it is worth trying the cell + var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; + gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; + return isHighAccuracyWifi; + } + + let hasEqualCells = false; + if (newCell) { + hasEqualCells = gCachedRequest.isCellEqual(newCell); + } + + let hasEqualWifis = false; + if (newWifiList) { + hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); + } + + gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; + + if (gCachedRequest.isCellOnly()) { + gDebugCacheReasoning += ", Cell only."; + if (hasEqualCells) { + return true; + } + } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { + gDebugCacheReasoning +=", Wifi only." + return true; + } else if (gCachedRequest.isCellAndWifi()) { + gDebugCacheReasoning += ", Cache has Cell+Wifi."; + if ((hasEqualCells && hasEqualWifis) || + (!newWifiList && hasEqualCells) || + (!newCell && hasEqualWifis)) + { + return true; + } + } + + return false; +} + +function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) { + this.latitude = lat; + this.longitude = lon; + this.accuracy = acc; + this.altitude = alt; + this.altitudeAccuracy = altacc; +} + +WifiGeoCoordsObject.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords]) +}; + +function WifiGeoPositionObject(lat, lng, acc) { + this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0); + this.address = null; + this.timestamp = Date.now(); +} + +WifiGeoPositionObject.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition]) +}; + +function WifiGeoPositionProvider() { + try { + gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled"); + } catch (e) {} + + try { + gLocationRequestTimeout = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending"); + } catch (e) {} + + try { + gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan"); + } catch (e) {} + + this.wifiService = null; + this.timer = null; + this.started = false; +} + +WifiGeoPositionProvider.prototype = { + classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIGeolocationProvider, + Ci.nsIWifiListener, + Ci.nsITimerCallback, + Ci.nsIObserver]), + listener: null, + + observe: function(aSubject, aTopic, aData) { + if (aTopic != SETTINGS_CHANGED_TOPIC) { + return; + } + + try { + if ("wrappedJSObject" in aSubject) { + aSubject = aSubject.wrappedJSObject; + } + if (aSubject.key == SETTINGS_DEBUG_ENABLED) { + gLoggingEnabled = aSubject.value; + } else if (aSubject.key == SETTINGS_WIFI_ENABLED) { + gWifiScanningEnabled = aSubject.value; + } + } catch (e) { + } + }, + + resetTimer: function() { + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + // wifi thread triggers WifiGeoPositionProvider to proceed, with no wifi, do manual timeout + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback(this, + gLocationRequestTimeout, + this.timer.TYPE_REPEATING_SLACK); + }, + + startup: function() { + if (this.started) + return; + + this.started = true; + let self = this; + let settingsCallback = { + handle: function(name, result) { + // Stop the B2G UI setting from overriding the js prefs setting, and turning off logging + // If gLoggingEnabled is already on during startup, that means it was set in js prefs. + if (name == SETTINGS_DEBUG_ENABLED && !gLoggingEnabled) { + gLoggingEnabled = result; + } else if (name == SETTINGS_WIFI_ENABLED) { + gWifiScanningEnabled = result; + if (self.wifiService) { + self.wifiService.stopWatching(self); + } + if (gWifiScanningEnabled) { + self.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + self.wifiService.startWatching(self); + } + } + }, + + handleError: function(message) { + gLoggingEnabled = false; + LOG("settings callback threw an exception, dropping"); + } + }; + + Services.obs.addObserver(this, SETTINGS_CHANGED_TOPIC, false); + let settingsService = Cc["@mozilla.org/settingsService;1"]; + if (settingsService) { + let settings = settingsService.getService(Ci.nsISettingsService); + settings.createLock().get(SETTINGS_WIFI_ENABLED, settingsCallback); + settings.createLock().get(SETTINGS_DEBUG_ENABLED, settingsCallback); + } + + if (gWifiScanningEnabled && Cc["@mozilla.org/wifi/monitor;1"]) { + if (this.wifiService) { + this.wifiService.stopWatching(this); + } + this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor); + this.wifiService.startWatching(this); + } + + this.resetTimer(); + LOG("startup called."); + }, + + watch: function(c) { + this.listener = c; + }, + + shutdown: function() { + LOG("shutdown called"); + if (this.started == false) { + return; + } + + // Without clearing this, we could end up using the cache almost indefinitely + // TODO: add logic for cache lifespan, for now just be safe and clear it + gCachedRequest = null; + + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + + if(this.wifiService) { + this.wifiService.stopWatching(this); + this.wifiService = null; + } + + Services.obs.removeObserver(this, SETTINGS_CHANGED_TOPIC); + + this.listener = null; + this.started = false; + }, + + setHighAccuracy: function(enable) { + }, + + onChange: function(accessPoints) { + + // we got some wifi data, rearm the timer. + this.resetTimer(); + + function isPublic(ap) { + let mask = "_nomap" + let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length); + if (result != -1) { + LOG("Filtering out " + ap.ssid + " " + result); + return false; + } + return true; + }; + + function sort(a, b) { + return b.signal - a.signal; + }; + + function encode(ap) { + return { 'macAddress': ap.mac, 'signalStrength': ap.signal }; + }; + + let wifiData = null; + if (accessPoints) { + wifiData = accessPoints.filter(isPublic).sort(sort).map(encode); + } + this.sendLocationRequest(wifiData); + }, + + onError: function (code) { + LOG("wifi error: " + code); + this.sendLocationRequest(null); + }, + + notify: function (timer) { + this.sendLocationRequest(null); + }, + + sendLocationRequest: function (wifiData) { + let data = { cellTowers: undefined, wifiAccessPoints: undefined }; + if (wifiData && wifiData.length >= 2) { + data.wifiAccessPoints = wifiData; + } + + let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers, + data.wifiAccessPoints); + + LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); + + if (useCached) { + gCachedRequest.location.timestamp = Date.now(); + this.notifyListener("update", [gCachedRequest.location]); + return; + } + + // From here on, do a network geolocation request // + let url = Services.urlFormatter.formatURLPref("geo.wifi.uri"); + LOG("Sending request"); + + let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + + this.notifyListener("locationUpdatePending"); + + try { + xhr.open("POST", url, true); + xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS; + } catch (e) { + this.notifyListener("notifyError", + [POSITION_UNAVAILABLE]); + return; + } + xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); + xhr.responseType = "json"; + xhr.mozBackgroundRequest = true; + xhr.timeout = Services.prefs.getIntPref("geo.wifi.xhr.timeout"); + xhr.ontimeout = (function() { + LOG("Location request XHR timed out.") + this.notifyListener("notifyError", + [POSITION_UNAVAILABLE]); + }).bind(this); + xhr.onerror = (function() { + this.notifyListener("notifyError", + [POSITION_UNAVAILABLE]); + }).bind(this); + xhr.onload = (function() { + LOG("server returned status: " + xhr.status + " --> " + JSON.stringify(xhr.response)); + if ((xhr.channel instanceof Ci.nsIHttpChannel && xhr.status != 200) || + !xhr.response || !xhr.response.location) { + this.notifyListener("notifyError", + [POSITION_UNAVAILABLE]); + return; + } + + let newLocation = new WifiGeoPositionObject(xhr.response.location.lat, + xhr.response.location.lng, + xhr.response.accuracy); + + this.notifyListener("update", [newLocation]); + gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints); + }).bind(this); + + var requestData = JSON.stringify(data); + LOG("sending " + requestData); + xhr.send(requestData); + }, + + notifyListener: function(listenerFunc, args) { + args = args || []; + try { + this.listener[listenerFunc].apply(this.listener, args); + } catch(e) { + Cu.reportError(e); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]); |