dom/system/NetworkGeolocationProvider.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rwxr-xr-x

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 6 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 7
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cc = Components.classes;
michael@0 10
michael@0 11 const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE;
michael@0 12 const SETTING_DEBUG_ENABLED = "geolocation.debugging.enabled";
michael@0 13 const SETTING_CHANGED_TOPIC = "mozsettings-changed";
michael@0 14
michael@0 15 let gLoggingEnabled = false;
michael@0 16
michael@0 17 // if we don't see any wifi responses in 5 seconds, send the request.
michael@0 18 let gTimeToWaitBeforeSending = 5000; //ms
michael@0 19
michael@0 20 let gWifiScanningEnabled = true;
michael@0 21 let gWifiResults;
michael@0 22
michael@0 23 let gCellScanningEnabled = false;
michael@0 24 let gCellResults;
michael@0 25
michael@0 26 function LOG(aMsg) {
michael@0 27 if (gLoggingEnabled) {
michael@0 28 aMsg = "*** WIFI GEO: " + aMsg + "\n";
michael@0 29 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg);
michael@0 30 dump(aMsg);
michael@0 31 }
michael@0 32 }
michael@0 33
michael@0 34 function CachedRequest(loc, cellInfo, wifiList) {
michael@0 35 this.location = loc;
michael@0 36
michael@0 37 let wifis = new Set();
michael@0 38 if (wifiList) {
michael@0 39 for (let i = 0; i < wifiList.length; i++) {
michael@0 40 wifis.add(wifiList[i].macAddress);
michael@0 41 }
michael@0 42 }
michael@0 43
michael@0 44 // Use only these values for equality
michael@0 45 // (the JSON will contain additional values in future)
michael@0 46 function makeCellKey(cell) {
michael@0 47 return "" + cell.radio + ":" + cell.mobileCountryCode + ":" +
michael@0 48 cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" +
michael@0 49 cell.cellId;
michael@0 50 }
michael@0 51
michael@0 52 let cells = new Set();
michael@0 53 if (cellInfo) {
michael@0 54 for (let i = 0; i < cellInfo.length; i++) {
michael@0 55 cells.add(makeCellKey(cellInfo[i]));
michael@0 56 }
michael@0 57 }
michael@0 58
michael@0 59 this.hasCells = () => cells.size > 0;
michael@0 60
michael@0 61 this.hasWifis = () => wifis.size > 0;
michael@0 62
michael@0 63 // if fields match
michael@0 64 this.isCellEqual = function(cellInfo) {
michael@0 65 if (!this.hasCells()) {
michael@0 66 return false;
michael@0 67 }
michael@0 68
michael@0 69 let len1 = cells.size;
michael@0 70 let len2 = cellInfo.length;
michael@0 71
michael@0 72 if (len1 != len2) {
michael@0 73 LOG("cells not equal len");
michael@0 74 return false;
michael@0 75 }
michael@0 76
michael@0 77 for (let i = 0; i < len2; i++) {
michael@0 78 if (!cells.has(makeCellKey(cellInfo[i]))) {
michael@0 79 return false;
michael@0 80 }
michael@0 81 }
michael@0 82 return true;
michael@0 83 };
michael@0 84
michael@0 85 // if 50% of the SSIDS match
michael@0 86 this.isWifiApproxEqual = function(wifiList) {
michael@0 87 if (!this.hasWifis()) {
michael@0 88 return false;
michael@0 89 }
michael@0 90
michael@0 91 // if either list is a 50% subset of the other, they are equal
michael@0 92 let common = 0;
michael@0 93 for (let i = 0; i < wifiList.length; i++) {
michael@0 94 if (wifis.has(wifiList[i].macAddress)) {
michael@0 95 common++;
michael@0 96 }
michael@0 97 }
michael@0 98 let kPercentMatch = 0.5;
michael@0 99 return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch);
michael@0 100 };
michael@0 101
michael@0 102 this.isGeoip = function() {
michael@0 103 return !this.hasCells() && !this.hasWifis();
michael@0 104 };
michael@0 105
michael@0 106 this.isCellAndWifi = function() {
michael@0 107 return this.hasCells() && this.hasWifis();
michael@0 108 };
michael@0 109
michael@0 110 this.isCellOnly = function() {
michael@0 111 return this.hasCells() && !this.hasWifis();
michael@0 112 };
michael@0 113
michael@0 114 this.isWifiOnly = function() {
michael@0 115 return this.hasWifis() && !this.hasCells();
michael@0 116 };
michael@0 117 }
michael@0 118
michael@0 119 let gCachedRequest = null;
michael@0 120 let gDebugCacheReasoning = ""; // for logging the caching logic
michael@0 121
michael@0 122 // This function serves two purposes:
michael@0 123 // 1) do we have a cached request
michael@0 124 // 2) is the cached request better than what newCell and newWifiList will obtain
michael@0 125 // If the cached request exists, and we know it to have greater accuracy
michael@0 126 // by the nature of its origin (wifi/cell/geoip), use its cached location.
michael@0 127 //
michael@0 128 // If there is more source info than the cached request had, return false
michael@0 129 // In other cases, MLS is known to produce better/worse accuracy based on the
michael@0 130 // inputs, so base the decision on that.
michael@0 131 function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList)
michael@0 132 {
michael@0 133 gDebugCacheReasoning = "";
michael@0 134 let isNetworkRequestCacheEnabled = true;
michael@0 135 try {
michael@0 136 // Mochitest needs this pref to simulate request failure
michael@0 137 isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled");
michael@0 138 if (!isNetworkRequestCacheEnabled) {
michael@0 139 gCachedRequest = null;
michael@0 140 }
michael@0 141 } catch (e) {}
michael@0 142
michael@0 143 if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
michael@0 144 gDebugCacheReasoning = "No cached data";
michael@0 145 return false;
michael@0 146 }
michael@0 147
michael@0 148 if (!newCell && !newWifiList) {
michael@0 149 gDebugCacheReasoning = "New req. is GeoIP.";
michael@0 150 return true;
michael@0 151 }
michael@0 152
michael@0 153 if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) {
michael@0 154 gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi.";
michael@0 155 return false;
michael@0 156 }
michael@0 157
michael@0 158 if (newCell && gCachedRequest.isWifiOnly()) {
michael@0 159 // In order to know if a cell-only request should trump a wifi-only request
michael@0 160 // need to know if wifi is low accuracy. >5km would be VERY low accuracy,
michael@0 161 // it is worth trying the cell
michael@0 162 var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000;
michael@0 163 gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi;
michael@0 164 return isHighAccuracyWifi;
michael@0 165 }
michael@0 166
michael@0 167 let hasEqualCells = false;
michael@0 168 if (newCell) {
michael@0 169 hasEqualCells = gCachedRequest.isCellEqual(newCell);
michael@0 170 }
michael@0 171
michael@0 172 let hasEqualWifis = false;
michael@0 173 if (newWifiList) {
michael@0 174 hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
michael@0 175 }
michael@0 176
michael@0 177 gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis;
michael@0 178
michael@0 179 if (gCachedRequest.isCellOnly()) {
michael@0 180 gDebugCacheReasoning += ", Cell only.";
michael@0 181 if (hasEqualCells) {
michael@0 182 return true;
michael@0 183 }
michael@0 184 } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) {
michael@0 185 gDebugCacheReasoning +=", Wifi only."
michael@0 186 return true;
michael@0 187 } else if (gCachedRequest.isCellAndWifi()) {
michael@0 188 gDebugCacheReasoning += ", Cache has Cell+Wifi.";
michael@0 189 if ((hasEqualCells && hasEqualWifis) ||
michael@0 190 (!newWifiList && hasEqualCells) ||
michael@0 191 (!newCell && hasEqualWifis))
michael@0 192 {
michael@0 193 return true;
michael@0 194 }
michael@0 195 }
michael@0 196
michael@0 197 return false;
michael@0 198 }
michael@0 199
michael@0 200 function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) {
michael@0 201 this.latitude = lat;
michael@0 202 this.longitude = lon;
michael@0 203 this.accuracy = acc;
michael@0 204 this.altitude = alt;
michael@0 205 this.altitudeAccuracy = altacc;
michael@0 206 }
michael@0 207
michael@0 208 WifiGeoCoordsObject.prototype = {
michael@0 209 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords])
michael@0 210 };
michael@0 211
michael@0 212 function WifiGeoPositionObject(lat, lng, acc) {
michael@0 213 this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0);
michael@0 214 this.address = null;
michael@0 215 this.timestamp = Date.now();
michael@0 216 }
michael@0 217
michael@0 218 WifiGeoPositionObject.prototype = {
michael@0 219 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition])
michael@0 220 };
michael@0 221
michael@0 222 function WifiGeoPositionProvider() {
michael@0 223 try {
michael@0 224 gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled");
michael@0 225 } catch (e) {}
michael@0 226
michael@0 227 try {
michael@0 228 gTimeToWaitBeforeSending = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending");
michael@0 229 } catch (e) {}
michael@0 230
michael@0 231 try {
michael@0 232 gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan");
michael@0 233 } catch (e) {}
michael@0 234
michael@0 235 try {
michael@0 236 gCellScanningEnabled = Services.prefs.getBoolPref("geo.cell.scan");
michael@0 237 } catch (e) {}
michael@0 238
michael@0 239 this.wifiService = null;
michael@0 240 this.timeoutTimer = null;
michael@0 241 this.started = false;
michael@0 242 }
michael@0 243
michael@0 244 WifiGeoPositionProvider.prototype = {
michael@0 245 classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
michael@0 246 QueryInterface: XPCOMUtils.generateQI([Ci.nsIGeolocationProvider,
michael@0 247 Ci.nsIWifiListener,
michael@0 248 Ci.nsITimerCallback,
michael@0 249 Ci.nsIObserver]),
michael@0 250 listener: null,
michael@0 251
michael@0 252 observe: function(aSubject, aTopic, aData) {
michael@0 253 if (aTopic != SETTING_CHANGED_TOPIC) {
michael@0 254 return;
michael@0 255 }
michael@0 256
michael@0 257 try {
michael@0 258 let setting = JSON.parse(aData);
michael@0 259 if (setting.key != SETTING_DEBUG_ENABLED) {
michael@0 260 return;
michael@0 261 }
michael@0 262 gLoggingEnabled = setting.value;
michael@0 263 } catch (e) {
michael@0 264 }
michael@0 265 },
michael@0 266
michael@0 267 startup: function() {
michael@0 268 if (this.started)
michael@0 269 return;
michael@0 270
michael@0 271 this.started = true;
michael@0 272 let settingsCallback = {
michael@0 273 handle: function(name, result) {
michael@0 274 gLoggingEnabled = result && result.value === true ? true : false;
michael@0 275 },
michael@0 276
michael@0 277 handleError: function(message) {
michael@0 278 gLoggingEnabled = false;
michael@0 279 LOG("settings callback threw an exception, dropping");
michael@0 280 }
michael@0 281 };
michael@0 282
michael@0 283 try {
michael@0 284 Services.obs.addObserver(this, SETTING_CHANGED_TOPIC, false);
michael@0 285 let settings = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
michael@0 286 settings.createLock().get(SETTING_DEBUG_ENABLED, settingsCallback);
michael@0 287 } catch(ex) {
michael@0 288 // This platform doesn't have the settings interface, and that is just peachy
michael@0 289 }
michael@0 290
michael@0 291 if (gWifiScanningEnabled) {
michael@0 292 this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Components.interfaces.nsIWifiMonitor);
michael@0 293 this.wifiService.startWatching(this);
michael@0 294 }
michael@0 295 this.timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 296 this.timeoutTimer.initWithCallback(this,
michael@0 297 gTimeToWaitBeforeSending,
michael@0 298 this.timeoutTimer.TYPE_REPEATING_SLACK);
michael@0 299 LOG("startup called.");
michael@0 300 },
michael@0 301
michael@0 302 watch: function(c) {
michael@0 303 this.listener = c;
michael@0 304 },
michael@0 305
michael@0 306 shutdown: function() {
michael@0 307 LOG("shutdown called");
michael@0 308 if (this.started == false) {
michael@0 309 return;
michael@0 310 }
michael@0 311
michael@0 312 // Without clearing this, we could end up using the cache almost indefinitely
michael@0 313 // TODO: add logic for cache lifespan, for now just be safe and clear it
michael@0 314 gCachedRequest = null;
michael@0 315
michael@0 316 if (this.timeoutTimer) {
michael@0 317 this.timeoutTimer.cancel();
michael@0 318 this.timeoutTimer = null;
michael@0 319 }
michael@0 320
michael@0 321 if(this.wifiService) {
michael@0 322 this.wifiService.stopWatching(this);
michael@0 323 this.wifiService = null;
michael@0 324 }
michael@0 325
michael@0 326 Services.obs.removeObserver(this, SETTING_CHANGED_TOPIC);
michael@0 327
michael@0 328 this.listener = null;
michael@0 329 this.started = false;
michael@0 330 },
michael@0 331
michael@0 332 setHighAccuracy: function(enable) {
michael@0 333 },
michael@0 334
michael@0 335 onChange: function(accessPoints) {
michael@0 336
michael@0 337 function isPublic(ap) {
michael@0 338 let mask = "_nomap"
michael@0 339 let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length);
michael@0 340 if (result != -1) {
michael@0 341 LOG("Filtering out " + ap.ssid + " " + result);
michael@0 342 }
michael@0 343 return result;
michael@0 344 };
michael@0 345
michael@0 346 function sort(a, b) {
michael@0 347 return b.signal - a.signal;
michael@0 348 };
michael@0 349
michael@0 350 function encode(ap) {
michael@0 351 return { 'macAddress': ap.mac, 'signalStrength': ap.signal };
michael@0 352 };
michael@0 353
michael@0 354 if (accessPoints) {
michael@0 355 gWifiResults = accessPoints.filter(isPublic).sort(sort).map(encode);
michael@0 356 } else {
michael@0 357 gWifiResults = null;
michael@0 358 }
michael@0 359 },
michael@0 360
michael@0 361 onError: function (code) {
michael@0 362 LOG("wifi error: " + code);
michael@0 363 },
michael@0 364
michael@0 365 updateMobileInfo: function() {
michael@0 366 LOG("updateMobileInfo called");
michael@0 367 try {
michael@0 368 let radio = Cc["@mozilla.org/ril;1"]
michael@0 369 .getService(Ci.nsIRadioInterfaceLayer)
michael@0 370 .getRadioInterface(0);
michael@0 371
michael@0 372 let iccInfo = radio.rilContext.iccInfo;
michael@0 373 let cell = radio.rilContext.voice.cell;
michael@0 374
michael@0 375 LOG("mcc: " + iccInfo.mcc);
michael@0 376 LOG("mnc: " + iccInfo.mnc);
michael@0 377 LOG("cid: " + cell.gsmCellId);
michael@0 378 LOG("lac: " + cell.gsmLocationAreaCode);
michael@0 379
michael@0 380 gCellResults = [{
michael@0 381 "radio": "gsm",
michael@0 382 "mobileCountryCode": iccInfo.mcc,
michael@0 383 "mobileNetworkCode": iccInfo.mnc,
michael@0 384 "locationAreaCode": cell.gsmLocationAreaCode,
michael@0 385 "cellId": cell.gsmCellId,
michael@0 386 }];
michael@0 387 } catch (e) {
michael@0 388 gCellResults = null;
michael@0 389 }
michael@0 390 },
michael@0 391
michael@0 392 notify: function (timeoutTimer) {
michael@0 393 let data = {};
michael@0 394 if (gWifiResults) {
michael@0 395 data.wifiAccessPoints = gWifiResults;
michael@0 396 }
michael@0 397
michael@0 398 if (gCellScanningEnabled) {
michael@0 399 this.updateMobileInfo();
michael@0 400 }
michael@0 401
michael@0 402 if (gCellResults && gCellResults.length > 0) {
michael@0 403 data.cellTowers = gCellResults;
michael@0 404 }
michael@0 405
michael@0 406 let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers,
michael@0 407 data.wifiAccessPoints);
michael@0 408
michael@0 409 LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
michael@0 410
michael@0 411 if (useCached) {
michael@0 412 gCachedRequest.location.timestamp = Date.now();
michael@0 413 this.listener.update(gCachedRequest.location);
michael@0 414 return;
michael@0 415 }
michael@0 416
michael@0 417 // From here on, do a network geolocation request //
michael@0 418 let url = Services.urlFormatter.formatURLPref("geo.wifi.uri");
michael@0 419 let listener = this.listener;
michael@0 420 LOG("Sending request: " + url + "\n");
michael@0 421
michael@0 422 let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 423 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 424
michael@0 425 listener.locationUpdatePending();
michael@0 426
michael@0 427 try {
michael@0 428 xhr.open("POST", url, true);
michael@0 429 } catch (e) {
michael@0 430 listener.notifyError(POSITION_UNAVAILABLE);
michael@0 431 return;
michael@0 432 }
michael@0 433 xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
michael@0 434 xhr.responseType = "json";
michael@0 435 xhr.mozBackgroundRequest = true;
michael@0 436 xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS;
michael@0 437 xhr.onerror = function() {
michael@0 438 listener.notifyError(POSITION_UNAVAILABLE);
michael@0 439 };
michael@0 440 xhr.onload = function() {
michael@0 441 LOG("gls returned status: " + xhr.status + " --> " + JSON.stringify(xhr.response));
michael@0 442 if ((xhr.channel instanceof Ci.nsIHttpChannel && xhr.status != 200) ||
michael@0 443 !xhr.response || !xhr.response.location) {
michael@0 444 listener.notifyError(POSITION_UNAVAILABLE);
michael@0 445 return;
michael@0 446 }
michael@0 447
michael@0 448 let newLocation = new WifiGeoPositionObject(xhr.response.location.lat,
michael@0 449 xhr.response.location.lng,
michael@0 450 xhr.response.accuracy);
michael@0 451
michael@0 452 listener.update(newLocation);
michael@0 453 gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints);
michael@0 454 };
michael@0 455
michael@0 456 var requestData = JSON.stringify(data);
michael@0 457 gWifiResults = gCellResults = null;
michael@0 458 LOG("sending " + requestData);
michael@0 459 xhr.send(requestData);
michael@0 460 },
michael@0 461 };
michael@0 462
michael@0 463 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);

mercurial