1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
6 Components.utils.import("resource://gre/modules/Services.jsm");
8 const Ci = Components.interfaces;
9 const Cc = Components.classes;
10 const Cu = Components.utils;
12 const POSITION_UNAVAILABLE = Ci.nsIDOMGeoPositionError.POSITION_UNAVAILABLE;
13 const SETTINGS_DEBUG_ENABLED = "geolocation.debugging.enabled";
14 const SETTINGS_CHANGED_TOPIC = "mozsettings-changed";
15 const SETTINGS_WIFI_ENABLED = "wifi.enabled";
17 let gLoggingEnabled = false;
20 The gLocationRequestTimeout controls how long we wait on receiving an update
21 from the Wifi subsystem. If this timer fires, we believe the Wifi scan has
22 had a problem and we no longer can use Wifi to position the user this time
23 around (we will continue to be hopeful that Wifi will recover).
25 This timeout value is also used when Wifi scanning is disabled (see
26 gWifiScanningEnabled). In this case, we use this timer to collect cell/ip
27 data and xhr it to the location server.
30 let gLocationRequestTimeout = 5000;
32 let gWifiScanningEnabled = true;
33 let gCellScanningEnabled = false;
36 if (gLoggingEnabled) {
37 aMsg = "*** WIFI GEO: " + aMsg + "\n";
38 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg);
43 function CachedRequest(loc, cellInfo, wifiList) {
46 let wifis = new Set();
48 for (let i = 0; i < wifiList.length; i++) {
49 wifis.add(wifiList[i].macAddress);
53 // Use only these values for equality
54 // (the JSON will contain additional values in future)
55 function makeCellKey(cell) {
56 return "" + cell.radio + ":" + cell.mobileCountryCode + ":" +
57 cell.mobileNetworkCode + ":" + cell.locationAreaCode + ":" +
61 let cells = new Set();
63 for (let i = 0; i < cellInfo.length; i++) {
64 cells.add(makeCellKey(cellInfo[i]));
68 this.hasCells = () => cells.size > 0;
70 this.hasWifis = () => wifis.size > 0;
73 this.isCellEqual = function(cellInfo) {
74 if (!this.hasCells()) {
78 let len1 = cells.size;
79 let len2 = cellInfo.length;
82 LOG("cells not equal len");
86 for (let i = 0; i < len2; i++) {
87 if (!cells.has(makeCellKey(cellInfo[i]))) {
94 // if 50% of the SSIDS match
95 this.isWifiApproxEqual = function(wifiList) {
96 if (!this.hasWifis()) {
100 // if either list is a 50% subset of the other, they are equal
102 for (let i = 0; i < wifiList.length; i++) {
103 if (wifis.has(wifiList[i].macAddress)) {
107 let kPercentMatch = 0.5;
108 return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch);
111 this.isGeoip = function() {
112 return !this.hasCells() && !this.hasWifis();
115 this.isCellAndWifi = function() {
116 return this.hasCells() && this.hasWifis();
119 this.isCellOnly = function() {
120 return this.hasCells() && !this.hasWifis();
123 this.isWifiOnly = function() {
124 return this.hasWifis() && !this.hasCells();
128 let gCachedRequest = null;
129 let gDebugCacheReasoning = ""; // for logging the caching logic
131 // This function serves two purposes:
132 // 1) do we have a cached request
133 // 2) is the cached request better than what newCell and newWifiList will obtain
134 // If the cached request exists, and we know it to have greater accuracy
135 // by the nature of its origin (wifi/cell/geoip), use its cached location.
137 // If there is more source info than the cached request had, return false
138 // In other cases, MLS is known to produce better/worse accuracy based on the
139 // inputs, so base the decision on that.
140 function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList)
142 gDebugCacheReasoning = "";
143 let isNetworkRequestCacheEnabled = true;
145 // Mochitest needs this pref to simulate request failure
146 isNetworkRequestCacheEnabled = Services.prefs.getBoolPref("geo.wifi.debug.requestCache.enabled");
147 if (!isNetworkRequestCacheEnabled) {
148 gCachedRequest = null;
152 if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
153 gDebugCacheReasoning = "No cached data";
157 if (!newCell && !newWifiList) {
158 gDebugCacheReasoning = "New req. is GeoIP.";
162 if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) {
163 gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi.";
167 if (newCell && gCachedRequest.isWifiOnly()) {
168 // In order to know if a cell-only request should trump a wifi-only request
169 // need to know if wifi is low accuracy. >5km would be VERY low accuracy,
170 // it is worth trying the cell
171 var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000;
172 gDebugCacheReasoning = "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi;
173 return isHighAccuracyWifi;
176 let hasEqualCells = false;
178 hasEqualCells = gCachedRequest.isCellEqual(newCell);
181 let hasEqualWifis = false;
183 hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
186 gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis;
188 if (gCachedRequest.isCellOnly()) {
189 gDebugCacheReasoning += ", Cell only.";
193 } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) {
194 gDebugCacheReasoning +=", Wifi only."
196 } else if (gCachedRequest.isCellAndWifi()) {
197 gDebugCacheReasoning += ", Cache has Cell+Wifi.";
198 if ((hasEqualCells && hasEqualWifis) ||
199 (!newWifiList && hasEqualCells) ||
200 (!newCell && hasEqualWifis))
209 function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) {
211 this.longitude = lon;
214 this.altitudeAccuracy = altacc;
217 WifiGeoCoordsObject.prototype = {
218 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords])
221 function WifiGeoPositionObject(lat, lng, acc) {
222 this.coords = new WifiGeoCoordsObject(lat, lng, acc, 0, 0);
224 this.timestamp = Date.now();
227 WifiGeoPositionObject.prototype = {
228 QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition])
231 function WifiGeoPositionProvider() {
233 gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled");
237 gLocationRequestTimeout = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending");
241 gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan");
245 gCellScanningEnabled = Services.prefs.getBoolPref("geo.cell.scan");
248 this.wifiService = null;
250 this.started = false;
253 WifiGeoPositionProvider.prototype = {
254 classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
255 QueryInterface: XPCOMUtils.generateQI([Ci.nsIGeolocationProvider,
261 observe: function(aSubject, aTopic, aData) {
262 if (aTopic != SETTINGS_CHANGED_TOPIC) {
267 let setting = JSON.parse(aData);
268 if (setting.key == SETTINGS_DEBUG_ENABLED) {
269 gLoggingEnabled = setting.value;
270 } else if (setting.key == SETTINGS_WIFI_ENABLED) {
271 gWifiScanningEnabled = setting.value;
277 resetTimer: function() {
282 // wifi thread triggers WifiGeoPositionProvider to proceed, with no wifi, do manual timeout
283 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
284 this.timer.initWithCallback(this,
285 gLocationRequestTimeout,
286 this.timer.TYPE_REPEATING_SLACK);
289 startup: function() {
295 let settingsCallback = {
296 handle: function(name, result) {
297 // Stop the B2G UI setting from overriding the js prefs setting, and turning off logging
298 // If gLoggingEnabled is already on during startup, that means it was set in js prefs.
299 if (name == SETTINGS_DEBUG_ENABLED && !gLoggingEnabled) {
300 gLoggingEnabled = result;
301 } else if (name == SETTINGS_WIFI_ENABLED) {
302 gWifiScanningEnabled = result;
303 if (self.wifiService) {
304 self.wifiService.stopWatching(self);
306 if (gWifiScanningEnabled) {
307 self.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
308 self.wifiService.startWatching(self);
313 handleError: function(message) {
314 gLoggingEnabled = false;
315 LOG("settings callback threw an exception, dropping");
320 Services.obs.addObserver(this, SETTINGS_CHANGED_TOPIC, false);
321 let settings = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
322 settings.createLock().get(SETTINGS_WIFI_ENABLED, settingsCallback);
323 settings.createLock().get(SETTINGS_DEBUG_ENABLED, settingsCallback);
325 // This platform doesn't have the settings interface, and that is just peachy
328 if (gWifiScanningEnabled && Cc["@mozilla.org/wifi/monitor;1"]) {
329 if (this.wifiService) {
330 this.wifiService.stopWatching(this);
332 this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
333 this.wifiService.startWatching(this);
337 LOG("startup called.");
344 shutdown: function() {
345 LOG("shutdown called");
346 if (this.started == false) {
350 // Without clearing this, we could end up using the cache almost indefinitely
351 // TODO: add logic for cache lifespan, for now just be safe and clear it
352 gCachedRequest = null;
359 if(this.wifiService) {
360 this.wifiService.stopWatching(this);
361 this.wifiService = null;
364 Services.obs.removeObserver(this, SETTINGS_CHANGED_TOPIC);
366 this.listener = null;
367 this.started = false;
370 setHighAccuracy: function(enable) {
373 onChange: function(accessPoints) {
375 // we got some wifi data, rearm the timer.
378 function isPublic(ap) {
380 let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length);
382 LOG("Filtering out " + ap.ssid + " " + result);
387 function sort(a, b) {
388 return b.signal - a.signal;
391 function encode(ap) {
392 return { 'macAddress': ap.mac, 'signalStrength': ap.signal };
397 wifiData = accessPoints.filter(isPublic).sort(sort).map(encode);
399 this.sendLocationRequest(wifiData);
402 onError: function (code) {
403 LOG("wifi error: " + code);
404 this.sendLocationRequest(null);
407 getMobileInfo: function() {
408 LOG("getMobileInfo called");
410 let radioService = Cc["@mozilla.org/ril;1"]
411 .getService(Ci.nsIRadioInterfaceLayer);
412 let numInterfaces = radioService.numRadioInterfaces;
414 for (let i = 0; i < numInterfaces; i++) {
415 LOG("Looking for SIM in slot:" + i + " of " + numInterfaces);
416 let radio = radioService.getRadioInterface(i);
417 let iccInfo = radio.rilContext.iccInfo;
418 let cell = radio.rilContext.voice.cell;
419 let type = radio.rilContext.voice.type;
421 if (iccInfo && cell && type) {
422 if (type === "gsm" || type === "gprs" || type === "edge") {
427 result.push({ radio: type,
428 mobileCountryCode: iccInfo.mcc,
429 mobileNetworkCode: iccInfo.mnc,
430 locationAreaCode: cell.gsmLocationAreaCode,
431 cellId: cell.gsmCellId });
440 notify: function (timer) {
441 this.sendLocationRequest(null);
444 sendLocationRequest: function (wifiData) {
447 data.wifiAccessPoints = wifiData;
450 if (gCellScanningEnabled) {
451 let cellData = this.getMobileInfo();
452 if (cellData && cellData.length > 0) {
453 data.cellTowers = cellData;
457 let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers,
458 data.wifiAccessPoints);
460 LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
463 gCachedRequest.location.timestamp = Date.now();
464 this.notifyListener("update", [gCachedRequest.location]);
468 // From here on, do a network geolocation request //
469 let url = Services.urlFormatter.formatURLPref("geo.wifi.uri");
470 LOG("Sending request");
472 let xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
473 .createInstance(Ci.nsIXMLHttpRequest);
475 this.notifyListener("locationUpdatePending");
478 xhr.open("POST", url, true);
480 this.notifyListener("notifyError",
481 [POSITION_UNAVAILABLE]);
484 xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
485 xhr.responseType = "json";
486 xhr.mozBackgroundRequest = true;
487 xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS;
488 xhr.onerror = (function() {
489 this.notifyListener("notifyError",
490 [POSITION_UNAVAILABLE]);
492 xhr.onload = (function() {
493 LOG("gls returned status: " + xhr.status + " --> " + JSON.stringify(xhr.response));
494 if ((xhr.channel instanceof Ci.nsIHttpChannel && xhr.status != 200) ||
495 !xhr.response || !xhr.response.location) {
496 this.notifyListener("notifyError",
497 [POSITION_UNAVAILABLE]);
501 let newLocation = new WifiGeoPositionObject(xhr.response.location.lat,
502 xhr.response.location.lng,
503 xhr.response.accuracy);
505 this.notifyListener("update", [newLocation]);
506 gCachedRequest = new CachedRequest(newLocation, data.cellTowers, data.wifiAccessPoints);
509 var requestData = JSON.stringify(data);
510 LOG("sending " + requestData);
511 xhr.send(requestData);
514 notifyListener: function(listenerFunc, args) {
517 this.listener[listenerFunc].apply(this.listener, args);
524 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);