1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/system/NetworkGeolocationProvider.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,463 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.9 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.10 + 1.11 +const Ci = Components.interfaces; 1.12 +const Cc = Components.classes; 1.13 + 1.14 +const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE; 1.15 +const SETTING_DEBUG_ENABLED = "geolocation.debugging.enabled"; 1.16 +const SETTING_CHANGED_TOPIC = "mozsettings-changed"; 1.17 + 1.18 +let gLoggingEnabled = false; 1.19 + 1.20 +// if we don't see any wifi responses in 5 seconds, send the request. 1.21 +let gTimeToWaitBeforeSending = 5000; //ms 1.22 + 1.23 +let gWifiScanningEnabled = true; 1.24 +let gWifiResults; 1.25 + 1.26 +let gCellScanningEnabled = false; 1.27 +let gCellResults; 1.28 + 1.29 +function LOG(aMsg) { 1.30 + if (gLoggingEnabled) { 1.31 + aMsg = "*** WIFI GEO: " + aMsg + "\n"; 1.32 + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg); 1.33 + dump(aMsg); 1.34 + } 1.35 +} 1.36 + 1.37 +function CachedRequest(loc, cellInfo, wifiList) { 1.38 + this.location = loc; 1.39 + 1.40 + let wifis = new Set(); 1.41 + if (wifiList) { 1.42 + for (let i = 0; i < wifiList.length; i++) { 1.43 + wifis.add(wifiList[i].macAddress); 1.44 + } 1.45 + } 1.46 + 1.47 + // Use only these values for equality 1.48 + // (the JSON will contain additional values in future) 1.49 + function makeCellKey(cell) { 1.50 + return "" + cell.radio + ":" + cell.mobileCountryCode + ":" + 1.51 + cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" + 1.52 + cell.cellId; 1.53 + } 1.54 + 1.55 + let cells = new Set(); 1.56 + if (cellInfo) { 1.57 + for (let i = 0; i < cellInfo.length; i++) { 1.58 + cells.add(makeCellKey(cellInfo[i])); 1.59 + } 1.60 + } 1.61 + 1.62 + this.hasCells = () => cells.size > 0; 1.63 + 1.64 + this.hasWifis = () => wifis.size > 0; 1.65 + 1.66 + // if fields match 1.67 + this.isCellEqual = function(cellInfo) { 1.68 + if (!this.hasCells()) { 1.69 + return false; 1.70 + } 1.71 + 1.72 + let len1 = cells.size; 1.73 + let len2 = cellInfo.length; 1.74 + 1.75 + if (len1 != len2) { 1.76 + LOG("cells not equal len"); 1.77 + return false; 1.78 + } 1.79 + 1.80 + for (let i = 0; i < len2; i++) { 1.81 + if (!cells.has(makeCellKey(cellInfo[i]))) { 1.82 + return false; 1.83 + } 1.84 + } 1.85 + return true; 1.86 + }; 1.87 + 1.88 + // if 50% of the SSIDS match 1.89 + this.isWifiApproxEqual = function(wifiList) { 1.90 + if (!this.hasWifis()) { 1.91 + return false; 1.92 + } 1.93 + 1.94 + // if either list is a 50% subset of the other, they are equal 1.95 + let common = 0; 1.96 + for (let i = 0; i < wifiList.length; i++) { 1.97 + if (wifis.has(wifiList[i].macAddress)) { 1.98 + common++; 1.99 + } 1.100 + } 1.101 + let kPercentMatch = 0.5; 1.102 + return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch); 1.103 + }; 1.104 + 1.105 + this.isGeoip = function() { 1.106 + return !this.hasCells() && !this.hasWifis(); 1.107 + }; 1.108 + 1.109 + this.isCellAndWifi = function() { 1.110 + return this.hasCells() && this.hasWifis(); 1.111 + }; 1.112 + 1.113 + this.isCellOnly = function() { 1.114 + return this.hasCells() && !this.hasWifis(); 1.115 + }; 1.116 + 1.117 + this.isWifiOnly = function() { 1.118 + return this.hasWifis() && !this.hasCells(); 1.119 + }; 1.120 + } 1.121 + 1.122 +let gCachedRequest = null; 1.123 +let gDebugCacheReasoning = ""; // for logging the caching logic 1.124 + 1.125 +// This function serves two purposes: 1.126 +// 1) do we have a cached request 1.127 +// 2) is the cached request better than what newCell and newWifiList will obtain 1.128 +// If the cached request exists, and we know it to have greater accuracy 1.129 +// by the nature of its origin (wifi/cell/geoip), use its cached location. 1.130 +// 1.131 +// If there is more source info than the cached request had, return false 1.132 +// In other cases, MLS is known to produce better/worse accuracy based on the 1.133 +// inputs, so base the decision on that. 1.134 +function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) 1.135 +{ 1.136 + gDebugCacheReasoning = ""; 1.137 + let isNetworkRequestCacheEnabled = true; 1.138 + try { 1.139 + // Mochitest needs this pref to simulate request failure 1.140 + isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled"); 1.141 + if (!isNetworkRequestCacheEnabled) { 1.142 + gCachedRequest = null; 1.143 + } 1.144 + } catch (e) {} 1.145 + 1.146 + if (!gCachedRequest || !isNetworkRequestCacheEnabled) { 1.147 + gDebugCacheReasoning = "No cached data"; 1.148 + return false; 1.149 + } 1.150 + 1.151 + if (!newCell && !newWifiList) { 1.152 + gDebugCacheReasoning = "New req. is GeoIP."; 1.153 + return true; 1.154 + } 1.155 + 1.156 + if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) { 1.157 + gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi."; 1.158 + return false; 1.159 + } 1.160 + 1.161 + if (newCell && gCachedRequest.isWifiOnly()) { 1.162 + // In order to know if a cell-only request should trump a wifi-only request 1.163 + // need to know if wifi is low accuracy. >5km would be VERY low accuracy, 1.164 + // it is worth trying the cell 1.165 + var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000; 1.166 + gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi; 1.167 + return isHighAccuracyWifi; 1.168 + } 1.169 + 1.170 + let hasEqualCells = false; 1.171 + if (newCell) { 1.172 + hasEqualCells = gCachedRequest.isCellEqual(newCell); 1.173 + } 1.174 + 1.175 + let hasEqualWifis = false; 1.176 + if (newWifiList) { 1.177 + hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList); 1.178 + } 1.179 + 1.180 + gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis; 1.181 + 1.182 + if (gCachedRequest.isCellOnly()) { 1.183 + gDebugCacheReasoning += ", Cell only."; 1.184 + if (hasEqualCells) { 1.185 + return true; 1.186 + } 1.187 + } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) { 1.188 + gDebugCacheReasoning +=", Wifi only." 1.189 + return true; 1.190 + } else if (gCachedRequest.isCellAndWifi()) { 1.191 + gDebugCacheReasoning += ", Cache has Cell+Wifi."; 1.192 + if ((hasEqualCells && hasEqualWifis) || 1.193 + (!newWifiList && hasEqualCells) || 1.194 + (!newCell && hasEqualWifis)) 1.195 + { 1.196 + return true; 1.197 + } 1.198 + } 1.199 + 1.200 + return false; 1.201 +} 1.202 + 1.203 +function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) { 1.204 + this.latitude = lat; 1.205 + this.longitude = lon; 1.206 + this.accuracy = acc; 1.207 + this.altitude = alt; 1.208 + this.altitudeAccuracy = altacc; 1.209 +} 1.210 + 1.211 +WifiGeoCoordsObject.prototype = { 1.212 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords]) 1.213 +}; 1.214 + 1.215 +function WifiGeoPositionObject(lat, lng, acc) { 1.216 + this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0); 1.217 + this.address = null; 1.218 + this.timestamp = Date.now(); 1.219 +} 1.220 + 1.221 +WifiGeoPositionObject.prototype = { 1.222 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition]) 1.223 +}; 1.224 + 1.225 +function WifiGeoPositionProvider() { 1.226 + try { 1.227 + gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled"); 1.228 + } catch (e) {} 1.229 + 1.230 + try { 1.231 + gTimeToWaitBeforeSending = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending"); 1.232 + } catch (e) {} 1.233 + 1.234 + try { 1.235 + gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan"); 1.236 + } catch (e) {} 1.237 + 1.238 + try { 1.239 + gCellScanningEnabled = Services.prefs.getBoolPref("geo.cell.scan"); 1.240 + } catch (e) {} 1.241 + 1.242 + this.wifiService = null; 1.243 + this.timeoutTimer = null; 1.244 + this.started = false; 1.245 +} 1.246 + 1.247 +WifiGeoPositionProvider.prototype = { 1.248 + classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"), 1.249 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIGeolocationProvider, 1.250 + Ci.nsIWifiListener, 1.251 + Ci.nsITimerCallback, 1.252 + Ci.nsIObserver]), 1.253 + listener: null, 1.254 + 1.255 + observe: function(aSubject, aTopic, aData) { 1.256 + if (aTopic != SETTING_CHANGED_TOPIC) { 1.257 + return; 1.258 + } 1.259 + 1.260 + try { 1.261 + let setting = JSON.parse(aData); 1.262 + if (setting.key != SETTING_DEBUG_ENABLED) { 1.263 + return; 1.264 + } 1.265 + gLoggingEnabled = setting.value; 1.266 + } catch (e) { 1.267 + } 1.268 + }, 1.269 + 1.270 + startup: function() { 1.271 + if (this.started) 1.272 + return; 1.273 + 1.274 + this.started = true; 1.275 + let settingsCallback = { 1.276 + handle: function(name, result) { 1.277 + gLoggingEnabled = result && result.value === true ? true : false; 1.278 + }, 1.279 + 1.280 + handleError: function(message) { 1.281 + gLoggingEnabled = false; 1.282 + LOG("settings callback threw an exception, dropping"); 1.283 + } 1.284 + }; 1.285 + 1.286 + try { 1.287 + Services.obs.addObserver(this, SETTING_CHANGED_TOPIC, false); 1.288 + let settings = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService); 1.289 + settings.createLock().get(SETTING_DEBUG_ENABLED, settingsCallback); 1.290 + } catch(ex) { 1.291 + // This platform doesn't have the settings interface, and that is just peachy 1.292 + } 1.293 + 1.294 + if (gWifiScanningEnabled) { 1.295 + this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Components.interfaces.nsIWifiMonitor); 1.296 + this.wifiService.startWatching(this); 1.297 + } 1.298 + this.timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.299 + this.timeoutTimer.initWithCallback(this, 1.300 + gTimeToWaitBeforeSending, 1.301 + this.timeoutTimer.TYPE_REPEATING_SLACK); 1.302 + LOG("startup called."); 1.303 + }, 1.304 + 1.305 + watch: function(c) { 1.306 + this.listener = c; 1.307 + }, 1.308 + 1.309 + shutdown: function() { 1.310 + LOG("shutdown called"); 1.311 + if (this.started == false) { 1.312 + return; 1.313 + } 1.314 + 1.315 + // Without clearing this, we could end up using the cache almost indefinitely 1.316 + // TODO: add logic for cache lifespan, for now just be safe and clear it 1.317 + gCachedRequest = null; 1.318 + 1.319 + if (this.timeoutTimer) { 1.320 + this.timeoutTimer.cancel(); 1.321 + this.timeoutTimer = null; 1.322 + } 1.323 + 1.324 + if(this.wifiService) { 1.325 + this.wifiService.stopWatching(this); 1.326 + this.wifiService = null; 1.327 + } 1.328 + 1.329 + Services.obs.removeObserver(this, SETTING_CHANGED_TOPIC); 1.330 + 1.331 + this.listener = null; 1.332 + this.started = false; 1.333 + }, 1.334 + 1.335 + setHighAccuracy: function(enable) { 1.336 + }, 1.337 + 1.338 + onChange: function(accessPoints) { 1.339 + 1.340 + function isPublic(ap) { 1.341 + let mask = "_nomap" 1.342 + let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length); 1.343 + if (result != -1) { 1.344 + LOG("Filtering out " + ap.ssid + " " + result); 1.345 + } 1.346 + return result; 1.347 + }; 1.348 + 1.349 + function sort(a, b) { 1.350 + return b.signal - a.signal; 1.351 + }; 1.352 + 1.353 + function encode(ap) { 1.354 + return { 'macAddress': ap.mac, 'signalStrength': ap.signal }; 1.355 + }; 1.356 + 1.357 + if (accessPoints) { 1.358 + gWifiResults = accessPoints.filter(isPublic).sort(sort).map(encode); 1.359 + } else { 1.360 + gWifiResults = null; 1.361 + } 1.362 + }, 1.363 + 1.364 + onError: function (code) { 1.365 + LOG("wifi error: " + code); 1.366 + }, 1.367 + 1.368 + updateMobileInfo: function() { 1.369 + LOG("updateMobileInfo called"); 1.370 + try { 1.371 + let radio = Cc["@mozilla.org/ril;1"] 1.372 + .getService(Ci.nsIRadioInterfaceLayer) 1.373 + .getRadioInterface(0); 1.374 + 1.375 + let iccInfo = radio.rilContext.iccInfo; 1.376 + let cell = radio.rilContext.voice.cell; 1.377 + 1.378 + LOG("mcc: " + iccInfo.mcc); 1.379 + LOG("mnc: " + iccInfo.mnc); 1.380 + LOG("cid: " + cell.gsmCellId); 1.381 + LOG("lac: " + cell.gsmLocationAreaCode); 1.382 + 1.383 + gCellResults = [{ 1.384 + "radio": "gsm", 1.385 + "mobileCountryCode": iccInfo.mcc, 1.386 + "mobileNetworkCode": iccInfo.mnc, 1.387 + "locationAreaCode": cell.gsmLocationAreaCode, 1.388 + "cellId": cell.gsmCellId, 1.389 + }]; 1.390 + } catch (e) { 1.391 + gCellResults = null; 1.392 + } 1.393 + }, 1.394 + 1.395 + notify: function (timeoutTimer) { 1.396 + let data = {}; 1.397 + if (gWifiResults) { 1.398 + data.wifiAccessPoints = gWifiResults; 1.399 + } 1.400 + 1.401 + if (gCellScanningEnabled) { 1.402 + this.updateMobileInfo(); 1.403 + } 1.404 + 1.405 + if (gCellResults && gCellResults.length > 0) { 1.406 + data.cellTowers = gCellResults; 1.407 + } 1.408 + 1.409 + let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers, 1.410 + data.wifiAccessPoints); 1.411 + 1.412 + LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning); 1.413 + 1.414 + if (useCached) { 1.415 + gCachedRequest.location.timestamp = Date.now(); 1.416 + this.listener.update(gCachedRequest.location); 1.417 + return; 1.418 + } 1.419 + 1.420 + // From here on, do a network geolocation request // 1.421 + let url = Services.urlFormatter.formatURLPref("geo.wifi.uri"); 1.422 + let listener = this.listener; 1.423 + LOG("Sending request: " + url + "\n"); 1.424 + 1.425 + let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.426 + .createInstance(Ci.nsIXMLHttpRequest); 1.427 + 1.428 + listener.locationUpdatePending(); 1.429 + 1.430 + try { 1.431 + xhr.open("POST", url, true); 1.432 + } catch (e) { 1.433 + listener.notifyError(POSITION_UNAVAILABLE); 1.434 + return; 1.435 + } 1.436 + xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 1.437 + xhr.responseType = "json"; 1.438 + xhr.mozBackgroundRequest = true; 1.439 + xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS; 1.440 + xhr.onerror = function() { 1.441 + listener.notifyError(POSITION_UNAVAILABLE); 1.442 + }; 1.443 + xhr.onload = function() { 1.444 + LOG("gls returned status: " + xhr.status + " --> " + JSON.stringify(xhr.response)); 1.445 + if ((xhr.channel instanceof Ci.nsIHttpChannel && xhr.status != 200) || 1.446 + !xhr.response || !xhr.response.location) { 1.447 + listener.notifyError(POSITION_UNAVAILABLE); 1.448 + return; 1.449 + } 1.450 + 1.451 + let newLocation = new WifiGeoPositionObject(xhr.response.location.lat, 1.452 + xhr.response.location.lng, 1.453 + xhr.response.accuracy); 1.454 + 1.455 + listener.update(newLocation); 1.456 + gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints); 1.457 + }; 1.458 + 1.459 + var requestData = JSON.stringify(data); 1.460 + gWifiResults = gCellResults = null; 1.461 + LOG("sending " + requestData); 1.462 + xhr.send(requestData); 1.463 + }, 1.464 +}; 1.465 + 1.466 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);