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