|
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]); |