123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /* 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, cc, tz, zip, city, rc, region, country, isp, org, as) {
- this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0);
- this.address = null;
- this.countrycode = cc;
- this.timezone = tz;
- this.zipcode = zip;
- this.postalcode = zip;
- this.city = city;
- this.regioncode = rc;
- this.region = region;
- this.country = country;
- this.isp = isp;
- this.org = org;
- this.as = as;
- 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);
- // XXX: Dead code?
- // this.notifyListener("locationUpdatePending");
- try {
- xhr.open("GET", url, true);
- xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS;
- } catch (e) {
- this.notifyListener("notifyError",
- [POSITION_UNAVAILABLE]);
- return;
- }
- 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.status == 'fail') {
- this.notifyListener("notifyError",
- [POSITION_UNAVAILABLE]);
- return;
- }
- let newLocation = new WifiGeoPositionObject(xhr.response.lat,
- xhr.response.lon,
- null, //accuracy not provided
- xhr.response.countryCode,
- xhr.response.timezone,
- xhr.response.zip,
- xhr.response.city,
- xhr.response.region,
- xhr.response.regionName,
- xhr.response.country,
- xhr.response.isp,
- xhr.response.org,
- xhr.response.as);
- 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 || [];
- LOG("Notify listener " + listenerFunc);
- try {
- this.listener[listenerFunc].apply(this.listener, args);
- } catch(e) {
- Cu.reportError(e);
- }
- }
- };
- this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);
|