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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 LocationHelper: "resource://gre/modules/LocationHelper.sys.mjs",
11 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
12 setTimeout: "resource://gre/modules/Timer.sys.mjs",
15 // GeolocationPositionError has no interface object, so we can't use that here.
16 const POSITION_UNAVAILABLE = 2;
18 XPCOMUtils.defineLazyPreferenceGetter(
21 "geo.provider.network.logging.enabled",
26 if (lazy.gLoggingEnabled) {
27 dump("*** WIFI GEO: " + aMsg + "\n");
31 function CachedRequest(loc, cellInfo, wifiList) {
34 let wifis = new Set();
36 for (let i = 0; i < wifiList.length; i++) {
37 wifis.add(wifiList[i].macAddress);
41 // Use only these values for equality
42 // (the JSON will contain additional values in future)
43 function makeCellKey(cell) {
48 cell.mobileCountryCode +
50 cell.mobileNetworkCode +
52 cell.locationAreaCode +
58 let cells = new Set();
60 for (let i = 0; i < cellInfo.length; i++) {
61 cells.add(makeCellKey(cellInfo[i]));
65 this.hasCells = () => cells.size > 0;
67 this.hasWifis = () => wifis.size > 0;
70 this.isCellEqual = function (cellInfo) {
71 if (!this.hasCells()) {
75 let len1 = cells.size;
76 let len2 = cellInfo.length;
79 LOG("cells not equal len");
83 for (let i = 0; i < len2; i++) {
84 if (!cells.has(makeCellKey(cellInfo[i]))) {
91 // if 50% of the SSIDS match
92 this.isWifiApproxEqual = function (wifiList) {
93 if (!this.hasWifis()) {
97 // if either list is a 50% subset of the other, they are equal
99 for (let i = 0; i < wifiList.length; i++) {
100 if (wifis.has(wifiList[i].macAddress)) {
104 let kPercentMatch = 0.5;
105 return common >= Math.max(wifis.size, wifiList.length) * kPercentMatch;
108 this.isGeoip = function () {
109 return !this.hasCells() && !this.hasWifis();
112 this.isCellAndWifi = function () {
113 return this.hasCells() && this.hasWifis();
116 this.isCellOnly = function () {
117 return this.hasCells() && !this.hasWifis();
120 this.isWifiOnly = function () {
121 return this.hasWifis() && !this.hasCells();
125 var gCachedRequest = null;
126 var gDebugCacheReasoning = ""; // for logging the caching logic
128 // This function serves two purposes:
129 // 1) do we have a cached request
130 // 2) is the cached request better than what newCell and newWifiList will obtain
131 // If the cached request exists, and we know it to have greater accuracy
132 // by the nature of its origin (wifi/cell/geoip), use its cached location.
134 // If there is more source info than the cached request had, return false
135 // In other cases, MLS is known to produce better/worse accuracy based on the
136 // inputs, so base the decision on that.
137 function isCachedRequestMoreAccurateThanServerRequest(newCell, newWifiList) {
138 gDebugCacheReasoning = "";
139 let isNetworkRequestCacheEnabled = true;
141 // Mochitest needs this pref to simulate request failure
142 isNetworkRequestCacheEnabled = Services.prefs.getBoolPref(
143 "geo.provider.network.debug.requestCache.enabled"
145 if (!isNetworkRequestCacheEnabled) {
146 gCachedRequest = null;
150 if (!gCachedRequest || !isNetworkRequestCacheEnabled) {
151 gDebugCacheReasoning = "No cached data";
155 if (!newCell && !newWifiList) {
156 gDebugCacheReasoning = "New req. is GeoIP.";
163 (gCachedRequest.isCellOnly() || gCachedRequest.isWifiOnly())
165 gDebugCacheReasoning = "New req. is cell+wifi, cache only cell or wifi.";
169 if (newCell && gCachedRequest.isWifiOnly()) {
170 // In order to know if a cell-only request should trump a wifi-only request
171 // need to know if wifi is low accuracy. >5km would be VERY low accuracy,
172 // it is worth trying the cell
173 var isHighAccuracyWifi = gCachedRequest.location.coords.accuracy < 5000;
174 gDebugCacheReasoning =
175 "Req. is cell, cache is wifi, isHigh:" + isHighAccuracyWifi;
176 return isHighAccuracyWifi;
179 let hasEqualCells = false;
181 hasEqualCells = gCachedRequest.isCellEqual(newCell);
184 let hasEqualWifis = false;
186 hasEqualWifis = gCachedRequest.isWifiApproxEqual(newWifiList);
189 gDebugCacheReasoning =
190 "EqualCells:" + hasEqualCells + " EqualWifis:" + hasEqualWifis;
192 if (gCachedRequest.isCellOnly()) {
193 gDebugCacheReasoning += ", Cell only.";
197 } else if (gCachedRequest.isWifiOnly() && hasEqualWifis) {
198 gDebugCacheReasoning += ", Wifi only.";
200 } else if (gCachedRequest.isCellAndWifi()) {
201 gDebugCacheReasoning += ", Cache has Cell+Wifi.";
203 (hasEqualCells && hasEqualWifis) ||
204 (!newWifiList && hasEqualCells) ||
205 (!newCell && hasEqualWifis)
214 function NetworkGeoCoordsObject(lat, lon, acc) {
216 this.longitude = lon;
219 // Neither GLS nor MLS return the following properties, so set them to NaN
220 // here. nsGeoPositionCoords will convert NaNs to null for optional properties
221 // of the JavaScript Coordinates object.
223 this.altitudeAccuracy = NaN;
228 NetworkGeoCoordsObject.prototype = {
229 QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPositionCoords"]),
232 function NetworkGeoPositionObject(lat, lng, acc) {
233 this.coords = new NetworkGeoCoordsObject(lat, lng, acc);
235 this.timestamp = Date.now();
238 NetworkGeoPositionObject.prototype = {
239 QueryInterface: ChromeUtils.generateQI(["nsIDOMGeoPosition"]),
242 export function NetworkGeolocationProvider() {
244 The _wifiMonitorTimeout controls how long we wait on receiving an update
245 from the Wifi subsystem. If this timer fires, we believe the Wifi scan has
246 had a problem and we no longer can use Wifi to position the user this time
247 around (we will continue to be hopeful that Wifi will recover).
249 This timeout value is also used when Wifi scanning is disabled (see
250 isWifiScanningEnabled). In this case, we use this timer to collect cell/ip
251 data and xhr it to the location server.
253 XPCOMUtils.defineLazyPreferenceGetter(
255 "_wifiMonitorTimeout",
256 "geo.provider.network.timeToWaitBeforeSending",
260 XPCOMUtils.defineLazyPreferenceGetter(
262 "_wifiScanningEnabled",
263 "geo.provider.network.scan",
267 this.wifiService = null;
269 this.started = false;
272 NetworkGeolocationProvider.prototype = {
273 classID: Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
274 name: "NetworkGeolocationProvider",
275 QueryInterface: ChromeUtils.generateQI([
276 "nsIGeolocationProvider",
284 get isWifiScanningEnabled() {
285 return Cc["@mozilla.org/wifi/monitor;1"] && this._wifiScanningEnabled;
293 // Wifi thread triggers NetworkGeolocationProvider to proceed. With no wifi,
294 // do manual timeout.
295 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
296 this.timer.initWithCallback(
298 this._wifiMonitorTimeout,
299 this.timer.TYPE_REPEATING_SLACK
310 if (this.isWifiScanningEnabled) {
311 if (this.wifiService) {
312 this.wifiService.stopWatching(this);
314 this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(
317 this.wifiService.startWatching(this, false);
321 LOG("startup called.");
329 LOG("shutdown called");
334 // Without clearing this, we could end up using the cache almost indefinitely
335 // TODO: add logic for cache lifespan, for now just be safe and clear it
336 gCachedRequest = null;
343 if (this.wifiService) {
344 this.wifiService.stopWatching(this);
345 this.wifiService = null;
348 this.listener = null;
349 this.started = false;
352 setHighAccuracy(enable) {
353 // Mochitest wants to check this value
354 if (Services.prefs.getBoolPref("geo.provider.testing")) {
355 Services.obs.notifyObservers(
357 "testing-geolocation-high-accuracy",
363 onChange(accessPoints) {
364 // we got some wifi data, rearm the timer.
369 wifiData = lazy.LocationHelper.formatWifiAccessPoints(accessPoints);
371 this.sendLocationRequest(wifiData);
375 LOG("wifi error: " + code);
376 this.sendLocationRequest(null);
379 onStatus(err, statusMessage) {
380 if (!this.listener) {
383 LOG("onStatus called." + statusMessage);
385 if (statusMessage && this.listener.notifyStatus) {
386 this.listener.notifyStatus(statusMessage);
389 if (err && this.listener.notifyError) {
390 this.listener.notifyError(POSITION_UNAVAILABLE, statusMessage);
395 this.onStatus(false, "wifi-timeout");
396 this.sendLocationRequest(null);
400 * After wifi (and possible cell tower) data has been gathered, this method is
401 * invoked to perform the request to network geolocation provider.
402 * The result of each request is sent to all registered listener (@see watch)
403 * by invoking its respective `update`, `notifyError` or `notifyStatus`
405 * `update` is called upon a successful request with its response data; this will be a `NetworkGeoPositionObject` instance.
406 * `notifyError` is called whenever the request gets an error from the local
407 * network subsystem, the server or simply times out.
408 * `notifyStatus` is called for each status change of the request that may be
409 * of interest to the consumer of this class. Currently the following status
410 * changes are reported: 'xhr-start', 'xhr-timeout', 'xhr-error' and
413 * @param {Array} wifiData Optional set of publicly available wifi networks
414 * in the following structure:
417 * { macAddress: <mac1>, signalStrength: <signal1> },
418 * { macAddress: <mac2>, signalStrength: <signal2> }
422 async sendLocationRequest(wifiData) {
423 let data = { cellTowers: undefined, wifiAccessPoints: undefined };
424 if (wifiData && wifiData.length >= 2) {
425 data.wifiAccessPoints = wifiData;
428 let useCached = isCachedRequestMoreAccurateThanServerRequest(
430 data.wifiAccessPoints
433 LOG("Use request cache:" + useCached + " reason:" + gDebugCacheReasoning);
436 gCachedRequest.location.timestamp = Date.now();
438 this.listener.update(gCachedRequest.location);
443 // From here on, do a network geolocation request //
444 let url = Services.urlFormatter.formatURLPref("geo.provider.network.url");
445 LOG("Sending request");
449 result = await this.makeRequest(url, wifiData);
451 `geo provider reported: ${result.location.lng}:${result.location.lat}`
453 let newLocation = new NetworkGeoPositionObject(
460 this.listener.update(newLocation);
463 gCachedRequest = new CachedRequest(
466 data.wifiAccessPoints
469 LOG("Location request hit error: " + err.name);
471 if (err.name == "AbortError") {
472 this.onStatus(true, "xhr-timeout");
474 this.onStatus(true, "xhr-error");
479 async makeRequest(url, wifiData) {
480 this.onStatus(false, "xhr-start");
482 let fetchController = new AbortController();
485 headers: { "Content-Type": "application/json; charset=UTF-8" },
487 signal: fetchController.signal,
491 fetchOpts.body = JSON.stringify({ wifiAccessPoints: wifiData });
494 let timeoutId = lazy.setTimeout(
495 () => fetchController.abort(),
496 Services.prefs.getIntPref("geo.provider.network.timeout")
499 let req = await fetch(url, fetchOpts);
500 lazy.clearTimeout(timeoutId);
501 let result = req.json();