1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "GpsdLocationProvider.h"
8 #include <errno.h>
9 #include <gps.h>
10 #include "MLSFallback.h"
11 #include "mozilla/Atomics.h"
12 #include "mozilla/FloatingPoint.h"
13 #include "mozilla/LazyIdleThread.h"
14 #include "mozilla/dom/GeolocationPositionErrorBinding.h"
15 #include "GeolocationPosition.h"
16 #include "nsProxyRelease.h"
17 #include "nsThreadUtils.h"
18 #include "prtime.h"
19
20 namespace mozilla {
21 namespace dom {
22
23 //
24 // MLSGeolocationUpdate
25 //
26
27 /**
28 * |MLSGeolocationUpdate| provides a fallback if gpsd is not supported.
29 */
30 class GpsdLocationProvider::MLSGeolocationUpdate final
31 : public nsIGeolocationUpdate {
32 public:
33 NS_DECL_ISUPPORTS
34 NS_DECL_NSIGEOLOCATIONUPDATE
35
36 explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback);
37
38 protected:
39 ~MLSGeolocationUpdate() = default;
40
41 private:
42 nsCOMPtr<nsIGeolocationUpdate> mCallback;
43 };
44
MLSGeolocationUpdate(nsIGeolocationUpdate * aCallback)45 GpsdLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate(
46 nsIGeolocationUpdate* aCallback)
47 : mCallback(aCallback) {
48 MOZ_ASSERT(mCallback);
49 }
50
51 // nsISupports
52 //
53
54 NS_IMPL_ISUPPORTS(GpsdLocationProvider::MLSGeolocationUpdate,
55 nsIGeolocationUpdate);
56
57 // nsIGeolocationUpdate
58 //
59
60 NS_IMETHODIMP
Update(nsIDOMGeoPosition * aPosition)61 GpsdLocationProvider::MLSGeolocationUpdate::Update(
62 nsIDOMGeoPosition* aPosition) {
63 nsCOMPtr<nsIDOMGeoPositionCoords> coords;
64 aPosition->GetCoords(getter_AddRefs(coords));
65 if (!coords) {
66 return NS_ERROR_FAILURE;
67 }
68
69 return mCallback->Update(aPosition);
70 }
71
72 NS_IMETHODIMP
NotifyError(uint16_t aError)73 GpsdLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) {
74 return mCallback->NotifyError(aError);
75 }
76
77 //
78 // UpdateRunnable
79 //
80
81 class GpsdLocationProvider::UpdateRunnable final : public Runnable {
82 public:
UpdateRunnable(const nsMainThreadPtrHandle<GpsdLocationProvider> & aLocationProvider,nsIDOMGeoPosition * aPosition)83 UpdateRunnable(
84 const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider,
85 nsIDOMGeoPosition* aPosition)
86 : Runnable("GpsdU"),
87 mLocationProvider(aLocationProvider),
88 mPosition(aPosition) {
89 MOZ_ASSERT(mLocationProvider);
90 MOZ_ASSERT(mPosition);
91 }
92
93 // nsIRunnable
94 //
95
Run()96 NS_IMETHOD Run() override {
97 mLocationProvider->Update(mPosition);
98 return NS_OK;
99 }
100
101 private:
102 nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
103 RefPtr<nsIDOMGeoPosition> mPosition;
104 };
105
106 //
107 // NotifyErrorRunnable
108 //
109
110 class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable {
111 public:
NotifyErrorRunnable(const nsMainThreadPtrHandle<GpsdLocationProvider> & aLocationProvider,int aError)112 NotifyErrorRunnable(
113 const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider,
114 int aError)
115 : Runnable("GpsdNE"),
116 mLocationProvider(aLocationProvider),
117 mError(aError) {
118 MOZ_ASSERT(mLocationProvider);
119 }
120
121 // nsIRunnable
122 //
123
Run()124 NS_IMETHOD Run() override {
125 mLocationProvider->NotifyError(mError);
126 return NS_OK;
127 }
128
129 private:
130 nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
131 int mError;
132 };
133
134 //
135 // PollRunnable
136 //
137
138 /**
139 * |PollRunnable| does the main work of processing GPS data received
140 * from gpsd. libgps blocks while polling, so this runnable has to be
141 * executed on it's own thread. To cancel the poll runnable, invoke
142 * |StopRunning| and |PollRunnable| will stop within a reasonable time
143 * frame.
144 */
145 class GpsdLocationProvider::PollRunnable final : public Runnable {
146 public:
PollRunnable(const nsMainThreadPtrHandle<GpsdLocationProvider> & aLocationProvider)147 PollRunnable(
148 const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider)
149 : Runnable("GpsdP"),
150 mLocationProvider(aLocationProvider),
151 mRunning(true) {
152 MOZ_ASSERT(mLocationProvider);
153 }
154
IsSupported()155 static bool IsSupported() {
156 return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 10;
157 }
158
IsRunning() const159 bool IsRunning() const { return mRunning; }
160
StopRunning()161 void StopRunning() { mRunning = false; }
162
163 // nsIRunnable
164 //
165
Run()166 NS_IMETHOD Run() override {
167 int err;
168
169 switch (GPSD_API_MAJOR_VERSION) {
170 case 5 ... 10:
171 err = PollLoop5();
172 break;
173 default:
174 err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
175 break;
176 }
177
178 if (err) {
179 NS_DispatchToMainThread(
180 MakeAndAddRef<NotifyErrorRunnable>(mLocationProvider, err));
181 }
182
183 mLocationProvider = nullptr;
184
185 return NS_OK;
186 }
187
188 protected:
PollLoop5()189 int PollLoop5() {
190 #if GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 10
191 static const int GPSD_WAIT_TIMEOUT_US =
192 1000000; /* us to wait for GPS data */
193
194 struct gps_data_t gpsData;
195
196 auto res = gps_open(nullptr, nullptr, &gpsData);
197
198 if (res < 0) {
199 return ErrnoToError(errno);
200 }
201
202 gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL);
203
204 int err = 0;
205
206 // nsGeoPositionCoords will convert NaNs to null for optional properties of
207 // the JavaScript Coordinates object.
208 double lat = 0;
209 double lon = 0;
210 double alt = UnspecifiedNaN<double>();
211 double hError = 0;
212 double vError = UnspecifiedNaN<double>();
213 double heading = UnspecifiedNaN<double>();
214 double speed = UnspecifiedNaN<double>();
215
216 while (IsRunning()) {
217 errno = 0;
218 auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US);
219 int status;
220
221 if (errno) {
222 err = ErrnoToError(errno);
223 break;
224 }
225 if (!hasGpsData) {
226 continue; /* woke up from timeout */
227 }
228
229 # if GPSD_API_MAJOR_VERSION >= 7
230 res = gps_read(&gpsData, nullptr, 0);
231 # else
232
233 res = gps_read(&gpsData);
234 # endif
235
236 if (res < 0) {
237 err = ErrnoToError(errno);
238 break;
239 } else if (!res) {
240 continue; /* no data available */
241 }
242
243 # if GPSD_API_MAJOR_VERSION >= 10
244 status = gpsData.fix.status;
245 # else
246 status = gpsData.status;
247 # endif
248
249 if (status == STATUS_NO_FIX) {
250 continue;
251 }
252
253 switch (gpsData.fix.mode) {
254 case MODE_3D:
255 double galt;
256
257 # if GPSD_API_MAJOR_VERSION >= 9
258 galt = gpsData.fix.altMSL;
259 # else
260 galt = gpsData.fix.altitude;
261 # endif
262 if (!IsNaN(galt)) {
263 alt = galt;
264 }
265 [[fallthrough]];
266 case MODE_2D:
267 if (!IsNaN(gpsData.fix.latitude)) {
268 lat = gpsData.fix.latitude;
269 }
270 if (!IsNaN(gpsData.fix.longitude)) {
271 lon = gpsData.fix.longitude;
272 }
273 if (!IsNaN(gpsData.fix.epx) && !IsNaN(gpsData.fix.epy)) {
274 hError = std::max(gpsData.fix.epx, gpsData.fix.epy);
275 } else if (!IsNaN(gpsData.fix.epx)) {
276 hError = gpsData.fix.epx;
277 } else if (!IsNaN(gpsData.fix.epy)) {
278 hError = gpsData.fix.epy;
279 }
280 if (!IsNaN(gpsData.fix.epv)) {
281 vError = gpsData.fix.epv;
282 }
283 if (!IsNaN(gpsData.fix.track)) {
284 heading = gpsData.fix.track;
285 }
286 if (!IsNaN(gpsData.fix.speed)) {
287 speed = gpsData.fix.speed;
288 }
289 break;
290 default:
291 continue; // There's no useful data in this fix; continue.
292 }
293
294 NS_DispatchToMainThread(MakeAndAddRef<UpdateRunnable>(
295 mLocationProvider,
296 new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed,
297 PR_Now() / PR_USEC_PER_MSEC)));
298 }
299
300 gps_stream(&gpsData, WATCH_DISABLE, NULL);
301 gps_close(&gpsData);
302
303 return err;
304 #else
305 return GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
306 #endif // GPSD_MAJOR_API_VERSION
307 }
308
ErrnoToError(int aErrno)309 static int ErrnoToError(int aErrno) {
310 switch (aErrno) {
311 case EACCES:
312 [[fallthrough]];
313 case EPERM:
314 [[fallthrough]];
315 case EROFS:
316 return GeolocationPositionError_Binding::PERMISSION_DENIED;
317 case ETIME:
318 [[fallthrough]];
319 case ETIMEDOUT:
320 return GeolocationPositionError_Binding::TIMEOUT;
321 default:
322 return GeolocationPositionError_Binding::POSITION_UNAVAILABLE;
323 }
324 }
325
326 private:
327 nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider;
328 Atomic<bool> mRunning;
329 };
330
331 //
332 // GpsdLocationProvider
333 //
334
335 const uint32_t GpsdLocationProvider::GPSD_POLL_THREAD_TIMEOUT_MS = 5000;
336
GpsdLocationProvider()337 GpsdLocationProvider::GpsdLocationProvider() {}
338
~GpsdLocationProvider()339 GpsdLocationProvider::~GpsdLocationProvider() {}
340
Update(nsIDOMGeoPosition * aPosition)341 void GpsdLocationProvider::Update(nsIDOMGeoPosition* aPosition) {
342 if (!mCallback || !mPollRunnable) {
343 return; // not initialized or already shut down
344 }
345
346 if (mMLSProvider) {
347 /* We got a location from gpsd, so let's cancel our MLS fallback. */
348 mMLSProvider->Shutdown();
349 mMLSProvider = nullptr;
350 }
351
352 mCallback->Update(aPosition);
353 }
354
NotifyError(int aError)355 void GpsdLocationProvider::NotifyError(int aError) {
356 if (!mCallback) {
357 return; // not initialized or already shut down
358 }
359
360 if (!mMLSProvider) {
361 /* With gpsd failed, we restart MLS. It will be canceled once we
362 * get another location from gpsd.
363 */
364 mMLSProvider = MakeAndAddRef<MLSFallback>();
365 mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback));
366 }
367
368 mCallback->NotifyError(aError);
369 }
370
371 // nsISupports
372 //
373
NS_IMPL_ISUPPORTS(GpsdLocationProvider,nsIGeolocationProvider)374 NS_IMPL_ISUPPORTS(GpsdLocationProvider, nsIGeolocationProvider)
375
376 // nsIGeolocationProvider
377 //
378
379 NS_IMETHODIMP
380 GpsdLocationProvider::Startup() {
381 if (!PollRunnable::IsSupported()) {
382 return NS_OK; // We'll fall back to MLS.
383 }
384
385 if (mPollRunnable) {
386 return NS_OK; // already running
387 }
388
389 RefPtr<PollRunnable> pollRunnable =
390 MakeAndAddRef<PollRunnable>(nsMainThreadPtrHandle<GpsdLocationProvider>(
391 new nsMainThreadPtrHolder<GpsdLocationProvider>("GpsdLP", this)));
392
393 // Use existing poll thread...
394 RefPtr<LazyIdleThread> pollThread = mPollThread;
395
396 // ... or create a new one.
397 if (!pollThread) {
398 pollThread = MakeAndAddRef<LazyIdleThread>(GPSD_POLL_THREAD_TIMEOUT_MS,
399 "Gpsd poll thread"_ns,
400 LazyIdleThread::ManualShutdown);
401 }
402
403 auto rv = pollThread->Dispatch(pollRunnable, NS_DISPATCH_NORMAL);
404
405 if (NS_FAILED(rv)) {
406 return rv;
407 }
408
409 mPollRunnable = pollRunnable.forget();
410 mPollThread = pollThread.forget();
411
412 return NS_OK;
413 }
414
415 NS_IMETHODIMP
Watch(nsIGeolocationUpdate * aCallback)416 GpsdLocationProvider::Watch(nsIGeolocationUpdate* aCallback) {
417 mCallback = aCallback;
418
419 /* The MLS fallback will kick in after a few seconds if gpsd
420 * doesn't provide location information within time. Once we
421 * see the first message from gpsd, the fallback will be
422 * disabled in |Update|.
423 */
424 mMLSProvider = MakeAndAddRef<MLSFallback>();
425 mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback));
426
427 return NS_OK;
428 }
429
430 NS_IMETHODIMP
Shutdown()431 GpsdLocationProvider::Shutdown() {
432 if (mMLSProvider) {
433 mMLSProvider->Shutdown();
434 mMLSProvider = nullptr;
435 }
436
437 if (!mPollRunnable) {
438 return NS_OK; // not running
439 }
440
441 mPollRunnable->StopRunning();
442 mPollRunnable = nullptr;
443
444 return NS_OK;
445 }
446
447 NS_IMETHODIMP
SetHighAccuracy(bool aHigh)448 GpsdLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; }
449
450 } // namespace dom
451 } // namespace mozilla
452