dom/system/NetworkGeolocationProvider.js

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

mercurial