Bumping manifests a=b2g-bump
[gecko.git] / dom / system / NetworkGeolocationProvider.js
blobe48bb9341d8b1afd8d68531cc210640f64adf092
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;
35 function LOG(aMsg) {
36   if (gLoggingEnabled) {
37     aMsg = "*** WIFI GEO: " + aMsg + "\n";
38     Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg);
39     dump(aMsg);
40   }
43 function CachedRequest(loc, cellInfo, wifiList) {
44   this.location = loc;
46   let wifis = new Set();
47   if (wifiList) {
48     for (let i = 0; i < wifiList.length; i++) {
49       wifis.add(wifiList[i].macAddress);
50     }
51   }
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 + ":" +
58     cell.cellId;
59   }
61   let cells = new Set();
62   if (cellInfo) {
63     for (let i = 0; i < cellInfo.length; i++) {
64       cells.add(makeCellKey(cellInfo[i]));
65     }
66   }
68   this.hasCells = () => cells.size > 0;
70   this.hasWifis = () => wifis.size > 0;
72   // if fields match
73   this.isCellEqual = function(cellInfo) {
74     if (!this.hasCells()) {
75       return false;
76     }
78     let len1 = cells.size;
79     let len2 = cellInfo.length;
81     if (len1 != len2) {
82       LOG("cells not equal len");
83       return false;
84     }
86     for (let i = 0; i < len2; i++) {
87       if (!cells.has(makeCellKey(cellInfo[i]))) {
88         return false;
89       }
90     }
91     return true;
92   };
94   // if 50% of the SSIDS match
95   this.isWifiApproxEqual = function(wifiList) {
96     if (!this.hasWifis()) {
97       return false;
98     }
100     // if either list is a 50% subset of the other, they are equal
101     let common = 0;
102     for (let i = 0; i < wifiList.length; i++) {
103       if (wifis.has(wifiList[i].macAddress)) {
104         common++;
105       }
106     }
107     let kPercentMatch = 0.5;
108     return common >= (Math.max(wifis.size, wifiList.length) * kPercentMatch);
109   };
111   this.isGeoip = function() {
112     return !this.hasCells() && !this.hasWifis();
113   };
115   this.isCellAndWifi = function() {
116     return this.hasCells() && this.hasWifis();
117   };
119   this.isCellOnly = function() {
120     return this.hasCells() && !this.hasWifis();
121   };
123   this.isWifiOnly = function() {
124     return this.hasWifis() && !this.hasCells();
125   };
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;
144   try {
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;
149     }
150   } catch (e) {}
152   if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
153     gDebugCacheReasoning = "No cached data";
154     return false;
155   }
157   if (!newCell && !newWifiList) {
158     gDebugCacheReasoning = "New req. is GeoIP.";
159     return true;
160   }
162   if (newCell && newWifiList && (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())) {
163     gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi.";
164     return false;
165   }
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;
174   }
176   let hasEqualCells = false;
177   if (newCell) {
178     hasEqualCells = gCachedRequest.isCellEqual(newCell);
179   }
181   let hasEqualWifis = false;
182   if (newWifiList) {
183     hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
184   }
186   gDebugCacheReasoning = "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis;
188   if (gCachedRequest.isCellOnly()) {
189     gDebugCacheReasoning += ", Cell only.";
190     if (hasEqualCells) {
191       return true;
192     }
193   } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) {
194     gDebugCacheReasoning +=", Wifi only."
195     return true;
196   } else if (gCachedRequest.isCellAndWifi()) {
197      gDebugCacheReasoning += ", Cache has Cell+Wifi.";
198     if ((hasEqualCells && hasEqualWifis) ||
199         (!newWifiList && hasEqualCells) ||
200         (!newCell && hasEqualWifis))
201     {
202      return true;
203     }
204   }
206   return false;
209 function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) {
210   this.latitude = lat;
211   this.longitude = lon;
212   this.accuracy = acc;
213   this.altitude = alt;
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);
223   this.address = null;
224   this.timestamp = Date.now();
227 WifiGeoPositionObject.prototype = {
228   QueryInterface:   XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition])
231 function WifiGeoPositionProvider() {
232   try {
233     gLoggingEnabled = Services.prefs.getBoolPref("geo.wifi.logging.enabled");
234   } catch (e) {}
236   try {
237     gLocationRequestTimeout = Services.prefs.getIntPref("geo.wifi.timeToWaitBeforeSending");
238   } catch (e) {}
240   try {
241     gWifiScanningEnabled = Services.prefs.getBoolPref("geo.wifi.scan");
242   } catch (e) {}
244   try {
245     gCellScanningEnabled = Services.prefs.getBoolPref("geo.cell.scan");
246   } catch (e) {}
248   this.wifiService = null;
249   this.timer = null;
250   this.started = false;
253 WifiGeoPositionProvider.prototype = {
254   classID:          Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
255   QueryInterface:   XPCOMUtils.generateQI([Ci.nsIGeolocationProvider,
256                                            Ci.nsIWifiListener,
257                                            Ci.nsITimerCallback,
258                                            Ci.nsIObserver]),
259   listener: null,
261   observe: function(aSubject, aTopic, aData) {
262     if (aTopic != SETTINGS_CHANGED_TOPIC) {
263       return;
264     }
266     try {
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;
272       }
273     } catch (e) {
274     }
275   },
277   resetTimer: function() {
278     if (this.timer) {
279       this.timer.cancel();
280       this.timer = null;
281     }
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);
287   },
289   startup:  function() {
290     if (this.started)
291       return;
293     this.started = true;
294     let self = this;
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);
305           }
306           if (gWifiScanningEnabled) {
307             self.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
308             self.wifiService.startWatching(self);
309           }
310         }
311       },
313       handleError: function(message) {
314         gLoggingEnabled = false;
315         LOG("settings callback threw an exception, dropping");
316       }
317     };
319     try {
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);
324     } catch(ex) {
325       // This platform doesn't have the settings interface, and that is just peachy
326     }
328     if (gWifiScanningEnabled && Cc["@mozilla.org/wifi/monitor;1"]) {
329       if (this.wifiService) {
330         this.wifiService.stopWatching(this);
331       }
332       this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(Ci.nsIWifiMonitor);
333       this.wifiService.startWatching(this);
334     }
336     this.resetTimer();
337     LOG("startup called.");
338   },
340   watch: function(c) {
341     this.listener = c;
342   },
344   shutdown: function() {
345     LOG("shutdown called");
346     if (this.started == false) {
347       return;
348     }
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;
354     if (this.timer) {
355       this.timer.cancel();
356       this.timer = null;
357     }
359     if(this.wifiService) {
360       this.wifiService.stopWatching(this);
361       this.wifiService = null;
362     }
364     Services.obs.removeObserver(this, SETTINGS_CHANGED_TOPIC);
366     this.listener = null;
367     this.started = false;
368   },
370   setHighAccuracy: function(enable) {
371   },
373   onChange: function(accessPoints) {
375     // we got some wifi data, rearm the timer.
376     this.resetTimer();
378     function isPublic(ap) {
379       let mask = "_nomap"
380       let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length);
381       if (result != -1) {
382         LOG("Filtering out " + ap.ssid + " " + result);
383       }
384       return result;
385     };
387     function sort(a, b) {
388       return b.signal - a.signal;
389     };
391     function encode(ap) {
392       return { 'macAddress': ap.mac, 'signalStrength': ap.signal };
393     };
395     let wifiData = null;
396     if (accessPoints) {
397       wifiData = accessPoints.filter(isPublic).sort(sort).map(encode);
398     }
399     this.sendLocationRequest(wifiData);
400   },
402   onError: function (code) {
403     LOG("wifi error: " + code);
404     this.sendLocationRequest(null);
405   },
407   getMobileInfo: function() {
408     LOG("getMobileInfo called");
409     try {
410       let radioService = Cc["@mozilla.org/ril;1"]
411                     .getService(Ci.nsIRadioInterfaceLayer);
412       let numInterfaces = radioService.numRadioInterfaces;
413       let result = [];
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") {
423             type = "gsm";
424           } else {
425             type = "wcdma";
426           }
427           result.push({ radio: type,
428                       mobileCountryCode: iccInfo.mcc,
429                       mobileNetworkCode: iccInfo.mnc,
430                       locationAreaCode: cell.gsmLocationAreaCode,
431                       cellId: cell.gsmCellId });
432         }
433       }
434       return result;
435     } catch (e) {
436       return null;
437     }
438   },
440   notify: function (timer) {
441     this.sendLocationRequest(null);
442   },
444   sendLocationRequest: function (wifiData) {
445     let data = {};
446     if (wifiData) {
447       data.wifiAccessPoints = wifiData;
448     }
450     if (gCellScanningEnabled) {
451       let cellData = this.getMobileInfo();
452       if (cellData && cellData.length > 0) {
453         data.cellTowers = cellData;
454       }
455     }
457     let useCached = isCachedRequestMoreAccurateThanServerRequest(data.cellTowers,
458                                                                  data.wifiAccessPoints);
460     LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
462     if (useCached) {
463       gCachedRequest.location.timestamp = Date.now();
464       this.notifyListener("update", [gCachedRequest.location]);
465       return;
466     }
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");
477     try {
478       xhr.open("POST", url, true);
479     } catch (e) {
480       this.notifyListener("notifyError",
481                           [POSITION_UNAVAILABLE]);
482       return;
483     }
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]);
491     }).bind(this);
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]);
498         return;
499       }
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);
507     }).bind(this);
509     var requestData = JSON.stringify(data);
510     LOG("sending " + requestData);
511     xhr.send(requestData);
512   },
514   notifyListener: function(listenerFunc, args) {
515     args = args || [];
516     try {
517       this.listener[listenerFunc].apply(this.listener, args);
518     } catch(e) {
519       Cu.reportError(e);
520     }
521   }
524 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);