diff options
Diffstat (limited to 'dom/system/linux')
-rw-r--r-- | dom/system/linux/GeoclueLocationProvider.cpp | 1060 | ||||
-rw-r--r-- | dom/system/linux/GeoclueLocationProvider.h | 32 | ||||
-rw-r--r-- | dom/system/linux/GpsdLocationProvider.cpp | 446 | ||||
-rw-r--r-- | dom/system/linux/GpsdLocationProvider.h | 51 | ||||
-rw-r--r-- | dom/system/linux/PortalLocationProvider.cpp | 351 | ||||
-rw-r--r-- | dom/system/linux/PortalLocationProvider.h | 54 | ||||
-rw-r--r-- | dom/system/linux/moz.build | 23 |
7 files changed, 2017 insertions, 0 deletions
diff --git a/dom/system/linux/GeoclueLocationProvider.cpp b/dom/system/linux/GeoclueLocationProvider.cpp new file mode 100644 index 0000000000..c9f5ef5dac --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.cpp @@ -0,0 +1,1060 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Author: Maciej S. Szmigiero <mail@maciej.szmigiero.name> + */ + +#include "GeoclueLocationProvider.h" + +#include <gio/gio.h> +#include <glib.h> +#include "mozilla/FloatingPoint.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_geo.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/XREAppData.h" +#include "mozilla/dom/GeolocationPosition.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "MLSFallback.h" +#include "nsAppRunner.h" +#include "nsCOMPtr.h" +#include "nsIDOMGeoPosition.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsStringFwd.h" +#include "prtime.h" + +namespace mozilla::dom { + +static LazyLogModule gGCLocationLog("GeoclueLocation"); + +#define GCL_LOG(level, ...) \ + MOZ_LOG(gGCLocationLog, mozilla::LogLevel::level, (__VA_ARGS__)) + +static const char* const kGeoclueBusName = "org.freedesktop.GeoClue2"; +static const char* const kGCManagerPath = "/org/freedesktop/GeoClue2/Manager"; +static const char* const kGCManagerInterface = + "org.freedesktop.GeoClue2.Manager"; +static const char* const kGCClientInterface = "org.freedesktop.GeoClue2.Client"; +static const char* const kGCLocationInterface = + "org.freedesktop.GeoClue2.Location"; +static const char* const kDBPropertySetMethod = + "org.freedesktop.DBus.Properties.Set"; + +/* + * Minimum altitude reported as valid (in meters), + * https://en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level + * says that lowest land in the world is at -430 m, so let's use -500 m here. + */ +static const double kGCMinAlt = -500; + +/* + * Matches "enum GClueAccuracyLevel" values, see: + * https://www.freedesktop.org/software/geoclue/docs/geoclue-gclue-enums.html#GClueAccuracyLevel + */ +enum class GCAccuracyLevel { + None = 0, + Country = 1, + City = 4, + Neighborhood = 5, + Street = 6, + Exact = 8, +}; + +/* + * Whether to reuse D-Bus proxies between uses of this provider. + * Usually a good thing, can be disabled for debug purposes. + */ +static const bool kGCReuseDBusProxy = true; + +class GCLocProviderPriv final : public nsIGeolocationProvider, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GCLocProviderPriv(); + + void UpdateLastPosition(); + + private: + class LocationTimerCallback final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + explicit LocationTimerCallback(GCLocProviderPriv* aParent) + : mParent(aParent) {} + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("GCLocProvider::LocationTimerCallback"); + return NS_OK; + } + + private: + ~LocationTimerCallback() = default; + WeakPtr<GCLocProviderPriv> mParent; + }; + + enum class Accuracy { Unset, Low, High }; + // States: + // Uninit: The default / initial state, with no client proxy yet. + // Initing: Takes care of establishing the client connection (GetClient / + // ConnectClient / SetDesktopID). + // SettingAccuracy: Does SetAccuracy operation, knows it should just go idle + // after finishing it. + // SettingAccuracyForStart: Does SetAccuracy operation, knows it then needs + // to do a Start operation after finishing it. + // Idle: Fully initialized, but not running state (quiescent). + // Starting: Starts the client by calling the Start D-Bus method. + // Started: Normal running state. + // Stopping: Stops the client by calling the Stop D-Bus method, knows it + // should just go idle after finishing it. + // StoppingForRestart: Stops the client by calling the Stop D-Bus method as + // a part of a Stop -> Start sequence (with possibly + // an accuracy update between these method calls). + // + // Valid state transitions are: + // (any state) -> Uninit: Transition when a D-Bus call failed or + // provided invalid data. + // + // Watch() startup path: + // Uninit -> Initing: Transition after getting the very first Watch() + // request + // or any such request while not having the client proxy. + // Initing -> SettingAccuracyForStart: Transition after getting a successful + // SetDesktopID response. + // SettingAccuracyForStart -> Starting: Transition after getting a + // successful + // SetAccuracy response. + // Idle -> Starting: Transition after getting a Watch() request while in + // fully + // initialized, but not running state. + // SettingAccuracy -> SettingAccuracyForStart: Transition after getting a + // Watch() + // request in the middle of + // setting accuracy during idle + // status. + // Stopping -> StoppingForRestart: Transition after getting a Watch() + // request + // in the middle of doing a Stop D-Bus call + // for idle status. + // StoppingForRestart -> Starting: Transition after getting a successful + // Stop response as a part of a Stop -> + // Start sequence while the previously set + // accuracy is still correct. + // StoppingForRestart -> SettingAccuracyForStart: Transition after getting + // a successful Stop response + // as a part of a Stop -> + // Start sequence but the set + // accuracy needs updating. + // Starting -> Started: Transition after getting a successful Start + // response. + // + // Shutdown() path: + // (any state) -> Uninit: Transition when not reusing the client proxy for + // any reason. + // Started -> Stopping: Transition from normal running state when reusing + // the client proxy. + // SettingAccuracyForStart -> SettingAccuracy: Transition when doing + // a shutdown in the middle of + // setting accuracy for a start + // when reusing the client + // proxy. + // SettingAccuracy -> Idle: Transition after getting a successful + // SetAccuracy + // response. + // StoppingForRestart -> Stopping: Transition when doing shutdown + // in the middle of a Stop -> Start sequence + // when reusing the client proxy. + // Stopping -> Idle: Transition after getting a successful Stop response. + // + // SetHighAccuracy() path: + // Started -> StoppingForRestart: Transition when accuracy needs updating + // on a running client. + // (the rest of the flow in StoppingForRestart state is the same as when + // being in this state in the Watch() startup path) + enum class ClientState { + Uninit, + Initing, + SettingAccuracy, + SettingAccuracyForStart, + Idle, + Starting, + Started, + Stopping, + StoppingForRestart + }; + + ~GCLocProviderPriv(); + + static bool AlwaysHighAccuracy(); + + void SetState(ClientState aNewState, const char* aNewStateStr); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT void NotifyError(int aError); + MOZ_CAN_RUN_SCRIPT void DBusProxyError(const GError* aGError, + bool aResetManager = false); + + MOZ_CAN_RUN_SCRIPT static void GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void ConnectClient(const gchar* aClientPath); + MOZ_CAN_RUN_SCRIPT static void ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData); + void SetDesktopID(); + MOZ_CAN_RUN_SCRIPT static void SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void SetAccuracy(); + MOZ_CAN_RUN_SCRIPT static void SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StartClient(); + MOZ_CAN_RUN_SCRIPT static void StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClient(bool aForRestart); + MOZ_CAN_RUN_SCRIPT static void StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData); + void StopClientNoWait(); + void MaybeRestartForAccuracy(); + + MOZ_CAN_RUN_SCRIPT static void GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData); + + static void GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, GVariant* aParameters, + gpointer aUserData); + void ConnectLocation(const gchar* aLocationPath); + static bool GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut); + static void ConnectLocationResponse(GObject* aObject, GAsyncResult* aResult, + gpointer aUserData); + + void SetLocationTimer(); + void StopLocationTimer(); + + bool InDBusCall(); + bool InDBusStoppingCall(); + bool InDBusStoppedCall(); + + void DeleteManager(); + void DoShutdown(bool aDeleteClient, bool aDeleteManager); + void DoShutdownClearCallback(bool aDestroying); + + nsresult FallbackToMLS(); + void StopMLSFallback(); + + void WatchStart(); + + Accuracy mAccuracyWanted = Accuracy::Unset; + Accuracy mAccuracySet = Accuracy::Unset; + RefPtr<GDBusProxy> mProxyManager; + RefPtr<GDBusProxy> mProxyClient; + RefPtr<GCancellable> mCancellable; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + ClientState mClientState = ClientState::Uninit; + RefPtr<nsIDOMGeoPosition> mLastPosition; + RefPtr<nsITimer> mLocationTimer; + RefPtr<MLSFallback> mMLSFallback; +}; + +// +// GCLocProviderPriv +// + +#define GCLP_SETSTATE(this, state) this->SetState(ClientState::state, #state) + +GCLocProviderPriv::GCLocProviderPriv() { + if (AlwaysHighAccuracy()) { + mAccuracyWanted = Accuracy::High; + } else { + mAccuracyWanted = Accuracy::Low; + } +} + +GCLocProviderPriv::~GCLocProviderPriv() { DoShutdownClearCallback(true); } + +bool GCLocProviderPriv::AlwaysHighAccuracy() { + return StaticPrefs::geo_provider_geoclue_always_high_accuracy(); +} + +void GCLocProviderPriv::SetState(ClientState aNewState, + const char* aNewStateStr) { + if (mClientState == aNewState) { + return; + } + + GCL_LOG(Debug, "changing state to %s", aNewStateStr); + mClientState = aNewState; +} + +void GCLocProviderPriv::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; + } + + mCallback->Update(aPosition); +} + +void GCLocProviderPriv::UpdateLastPosition() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "No last position to update"); + StopLocationTimer(); + Update(mLastPosition); +} + +nsresult GCLocProviderPriv::FallbackToMLS() { + GCL_LOG(Debug, "trying to fall back to MLS"); + StopMLSFallback(); + + RefPtr fallback = new MLSFallback(0); + MOZ_TRY(fallback->Startup(mCallback)); + + GCL_LOG(Debug, "Started up MLS fallback"); + mMLSFallback = std::move(fallback); + return NS_OK; +} + +void GCLocProviderPriv::StopMLSFallback() { + if (!mMLSFallback) { + return; + } + GCL_LOG(Debug, "Clearing MLS fallback"); + if (mMLSFallback) { + mMLSFallback->Shutdown(); + mMLSFallback = nullptr; + } +} + +void GCLocProviderPriv::NotifyError(int aError) { + if (!mCallback) { + return; + } + + // We errored out, try to fall back to MLS. + if (NS_SUCCEEDED(FallbackToMLS())) { + return; + } + + nsCOMPtr callback = mCallback; + callback->NotifyError(aError); +} + +void GCLocProviderPriv::DBusProxyError(const GError* aGError, + bool aResetManager) { + // that G_DBUS_ERROR below is actually a function call, not a constant + GQuark gdbusDomain = G_DBUS_ERROR; + int error = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + if (aGError) { + if (g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMEOUT) || + g_error_matches(aGError, gdbusDomain, G_DBUS_ERROR_TIMED_OUT)) { + error = GeolocationPositionError_Binding::TIMEOUT; + } else if (g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_LIMITS_EXCEEDED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_ACCESS_DENIED) || + g_error_matches(aGError, gdbusDomain, + G_DBUS_ERROR_AUTH_FAILED)) { + error = GeolocationPositionError_Binding::PERMISSION_DENIED; + } + } + + DoShutdown(true, aResetManager); + NotifyError(error); +} + +void GCLocProviderPriv::GetClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to get client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get(), true); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + auto signalError = MakeScopeExit([&]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->DBusProxyError(nullptr, true); + }); + + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected get client call return type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + if (g_variant_n_children(variant) < 1) { + GCL_LOG(Error, + "Not enough params in get client call return: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(variant)); + return; + } + + variant = dont_AddRef(g_variant_get_child_value(variant, 0)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, "Unexpected get client call return type inside tuple: %s\n", + g_variant_get_type_string(variant)); + return; + } + + const gchar* clientPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Debug, "Client path: %s\n", clientPath); + + signalError.release(); + self->ConnectClient(clientPath); +} + +void GCLocProviderPriv::ConnectClient(const gchar* aClientPath) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_ASSERT(mCancellable, "Watch() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aClientPath, kGCClientInterface, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(ConnectClientResponse), this); +} + +void GCLocProviderPriv::ConnectClientResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GDBusProxy> proxyClient = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyClient) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to connect to client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->mProxyClient = std::move(proxyClient); + + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCL_LOG(Info, "Client interface connected\n"); + + g_signal_connect(self->mProxyClient, "g-signal", G_CALLBACK(GCClientSignal), + self); + self->SetDesktopID(); +} + +void GCLocProviderPriv::SetDesktopID() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Initing, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_dbus_proxy_call(mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "DesktopId", + g_variant_new_string(appName.get())), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(SetDesktopIDResponse), + this); +} + +void GCLocProviderPriv::SetDesktopIDResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set DesktopId: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Initing, + "Client in a wrong state"); + + GCLP_SETSTATE(self, Idle); + self->SetAccuracy(); +} + +void GCLocProviderPriv::SetAccuracy() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + MOZ_ASSERT(mAccuracyWanted != Accuracy::Unset, "Invalid accuracy"); + + guint32 accuracy; + if (mAccuracyWanted == Accuracy::High) { + accuracy = (guint32)GCAccuracyLevel::Exact; + } else { + accuracy = (guint32)GCAccuracyLevel::City; + } + + mAccuracySet = mAccuracyWanted; + GCLP_SETSTATE(this, SettingAccuracyForStart); + g_dbus_proxy_call( + mProxyClient, kDBPropertySetMethod, + g_variant_new("(ssv)", kGCClientInterface, "RequestedAccuracyLevel", + g_variant_new_uint32(accuracy)), + G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(SetAccuracyResponse), this); +} + +void GCLocProviderPriv::SetAccuracyResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to set requested accuracy level: %s\n", + error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT( + self->mClientState == ClientState::SettingAccuracyForStart || + self->mClientState == ClientState::SettingAccuracy, + "Client in a wrong state"); + bool wantStart = self->mClientState == ClientState::SettingAccuracyForStart; + GCLP_SETSTATE(self, Idle); + + if (wantStart) { + self->StartClient(); + } +} + +void GCLocProviderPriv::StartClient() { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Idle, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + GCLP_SETSTATE(this, Starting); + g_dbus_proxy_call( + mProxyClient, "Start", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(StartClientResponse), this); +} + +void GCLocProviderPriv::StartClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to start client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + /* + * A workaround for + * https://gitlab.freedesktop.org/geoclue/geoclue/-/issues/143 We need to + * get a new client instance once the agent finally connects to the + * Geoclue service, otherwise every Start request on the old client + * interface will be denied. We need to reconnect to the Manager interface + * to achieve this since otherwise GetClient call will simply return the + * old client instance. + */ + bool resetManager = g_error_matches(error.get(), G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED); + self->DBusProxyError(error.get(), resetManager); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->mClientState == ClientState::Starting, + "Client in a wrong state"); + GCLP_SETSTATE(self, Started); + self->MaybeRestartForAccuracy(); +} + +void GCLocProviderPriv::StopClient(bool aForRestart) { + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Started, + "Client in a wrong state"); + MOZ_DIAGNOSTIC_ASSERT(mProxyClient && mCancellable, + "Watch() wasn't successfully called"); + + if (aForRestart) { + GCLP_SETSTATE(this, StoppingForRestart); + } else { + GCLP_SETSTATE(this, Stopping); + } + + g_dbus_proxy_call( + mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(StopClientResponse), this); +} + +void GCLocProviderPriv::StopClientResponse(GDBusProxy* aProxy, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GVariant> variant = dont_AddRef( + g_dbus_proxy_call_finish(aProxy, aResult, getter_Transfers(error))); + if (!variant) { + // if cancelled |self| might no longer be there + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Error, "Failed to stop client: %s\n", error->message); + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + self->DBusProxyError(error.get()); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + MOZ_DIAGNOSTIC_ASSERT(self->InDBusStoppingCall(), "Client in a wrong state"); + bool wantRestart = self->mClientState == ClientState::StoppingForRestart; + GCLP_SETSTATE(self, Idle); + + if (!wantRestart) { + return; + } + + if (self->mAccuracyWanted != self->mAccuracySet) { + self->SetAccuracy(); + } else { + self->StartClient(); + } +} + +void GCLocProviderPriv::StopClientNoWait() { + MOZ_DIAGNOSTIC_ASSERT(mProxyClient, "Watch() wasn't successfully called"); + g_dbus_proxy_call(mProxyClient, "Stop", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, + nullptr, nullptr, nullptr); +} + +void GCLocProviderPriv::MaybeRestartForAccuracy() { + if (mAccuracyWanted == mAccuracySet) { + return; + } + + if (mClientState != ClientState::Started) { + return; + } + + // Setting a new accuracy requires restarting the client + StopClient(true); +} + +void GCLocProviderPriv::GCManagerOwnerNotify(GObject* aObject, + GParamSpec* aPSpec, + gpointer aUserData) { + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + GUniquePtr<gchar> managerOwner( + g_dbus_proxy_get_name_owner(self->mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has lost its owner\n"); + self->DBusProxyError(nullptr, true); + } +} + +void GCLocProviderPriv::GCClientSignal(GDBusProxy* aProxy, gchar* aSenderName, + gchar* aSignalName, + GVariant* aParameters, + gpointer aUserData) { + if (g_strcmp0(aSignalName, "LocationUpdated")) { + return; + } + + if (!g_variant_is_of_type(aParameters, G_VARIANT_TYPE_TUPLE)) { + GCL_LOG(Error, "Unexpected location updated signal params type: %s\n", + g_variant_get_type_string(aParameters)); + return; + } + + if (g_variant_n_children(aParameters) < 2) { + GCL_LOG(Error, + "Not enough params in location updated signal: %" G_GSIZE_FORMAT + "\n", + g_variant_n_children(aParameters)); + return; + } + + RefPtr<GVariant> variant = + dont_AddRef(g_variant_get_child_value(aParameters, 1)); + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_OBJECT_PATH)) { + GCL_LOG(Error, + "Unexpected location updated signal new location path type: %s\n", + g_variant_get_type_string(variant)); + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + const gchar* locationPath = g_variant_get_string(variant, nullptr); + GCL_LOG(Verbose, "New location path: %s\n", locationPath); + self->ConnectLocation(locationPath); +} + +void GCLocProviderPriv::ConnectLocation(const gchar* aLocationPath) { + MOZ_ASSERT(mCancellable, "Startup() wasn't successfully called"); + g_dbus_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + aLocationPath, kGCLocationInterface, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(ConnectLocationResponse), this); +} + +bool GCLocProviderPriv::GetLocationProperty(GDBusProxy* aProxyLocation, + const gchar* aName, double* aOut) { + RefPtr<GVariant> property = + dont_AddRef(g_dbus_proxy_get_cached_property(aProxyLocation, aName)); + if (!g_variant_is_of_type(property, G_VARIANT_TYPE_DOUBLE)) { + GCL_LOG(Error, "Unexpected location property %s type: %s\n", aName, + g_variant_get_type_string(property)); + return false; + } + + *aOut = g_variant_get_double(property); + return true; +} + +void GCLocProviderPriv::ConnectLocationResponse(GObject* aObject, + GAsyncResult* aResult, + gpointer aUserData) { + GUniquePtr<GError> error; + RefPtr<GDBusProxy> proxyLocation = + dont_AddRef(g_dbus_proxy_new_finish(aResult, getter_Transfers(error))); + if (!proxyLocation) { + if (!g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + GCL_LOG(Warning, "Failed to connect to location: %s\n", error->message); + } + return; + } + + RefPtr self = static_cast<GCLocProviderPriv*>(aUserData); + /* + * nsGeoPositionCoords will convert NaNs to null for optional properties of + * the JavaScript Coordinates object. + */ + double lat = UnspecifiedNaN<double>(); + double lon = UnspecifiedNaN<double>(); + double alt = UnspecifiedNaN<double>(); + double hError = UnspecifiedNaN<double>(); + const double vError = UnspecifiedNaN<double>(); + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + struct { + const gchar* name; + double* out; + } props[] = { + {"Latitude", &lat}, {"Longitude", &lon}, {"Altitude", &alt}, + {"Accuracy", &hError}, {"Heading", &heading}, {"Speed", &speed}, + }; + + for (auto& prop : props) { + if (!GetLocationProperty(proxyLocation, prop.name, prop.out)) { + return; + } + } + + if (alt < kGCMinAlt) { + alt = UnspecifiedNaN<double>(); + } + if (speed < 0) { + speed = UnspecifiedNaN<double>(); + } + if (heading < 0 || std::isnan(speed) || speed == 0) { + heading = UnspecifiedNaN<double>(); + } + + GCL_LOG(Info, "New location: %f %f +-%fm @ %gm; hdg %f spd %fm/s\n", lat, lon, + hError, alt, heading, speed); + + self->mLastPosition = + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC); + self->UpdateLastPosition(); +} + +void GCLocProviderPriv::SetLocationTimer() { + MOZ_DIAGNOSTIC_ASSERT(mLastPosition, "no last position to report"); + + StopLocationTimer(); + + RefPtr<LocationTimerCallback> timerCallback = new LocationTimerCallback(this); + NS_NewTimerWithCallback(getter_AddRefs(mLocationTimer), timerCallback, 1000, + nsITimer::TYPE_ONE_SHOT); +} + +void GCLocProviderPriv::StopLocationTimer() { + if (!mLocationTimer) { + return; + } + + mLocationTimer->Cancel(); + mLocationTimer = nullptr; +} + +// Did we made some D-Bus call and are still waiting for its response? +bool GCLocProviderPriv::InDBusCall() { + return mClientState == ClientState::Initing || + mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart || + mClientState == ClientState::Starting || + mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +bool GCLocProviderPriv::InDBusStoppingCall() { + return mClientState == ClientState::Stopping || + mClientState == ClientState::StoppingForRestart; +} + +/* + * Did we made some D-Bus call while stopped and + * are still waiting for its response? + */ +bool GCLocProviderPriv::InDBusStoppedCall() { + return mClientState == ClientState::SettingAccuracy || + mClientState == ClientState::SettingAccuracyForStart; +} + +void GCLocProviderPriv::DeleteManager() { + if (!mProxyManager) { + return; + } + + g_signal_handlers_disconnect_matched(mProxyManager, G_SIGNAL_MATCH_DATA, 0, 0, + nullptr, nullptr, this); + mProxyManager = nullptr; +} + +void GCLocProviderPriv::DoShutdown(bool aDeleteClient, bool aDeleteManager) { + MOZ_DIAGNOSTIC_ASSERT( + !aDeleteManager || aDeleteClient, + "deleting manager proxy requires deleting client one, too"); + + // Invalidate the cached last position + StopLocationTimer(); + mLastPosition = nullptr; + + /* + * Do we need to delete the D-Bus proxy (or proxies)? + * Either because that's what our caller wanted, or because we are set to + * never reuse them, or because we are in a middle of some D-Bus call while + * having the service running (and so not being able to issue an immediate + * Stop call). + */ + if (aDeleteClient || !kGCReuseDBusProxy || + (InDBusCall() && !InDBusStoppingCall() && !InDBusStoppedCall())) { + if (mClientState == ClientState::Started) { + StopClientNoWait(); + GCLP_SETSTATE(this, Idle); + } + if (mProxyClient) { + g_signal_handlers_disconnect_matched(mProxyClient, G_SIGNAL_MATCH_DATA, 0, + 0, nullptr, nullptr, this); + } + if (mCancellable) { + g_cancellable_cancel(mCancellable); + mCancellable = nullptr; + } + mProxyClient = nullptr; + + if (aDeleteManager || !kGCReuseDBusProxy) { + DeleteManager(); + } + + GCLP_SETSTATE(this, Uninit); + } else if (mClientState == ClientState::Started) { + StopClient(false); + } else if (mClientState == ClientState::SettingAccuracyForStart) { + GCLP_SETSTATE(this, SettingAccuracy); + } else if (mClientState == ClientState::StoppingForRestart) { + GCLP_SETSTATE(this, Stopping); + } +} + +void GCLocProviderPriv::DoShutdownClearCallback(bool aDestroying) { + mCallback = nullptr; + StopMLSFallback(); + DoShutdown(aDestroying, aDestroying); +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +/* + * The Startup() method should only succeed if Geoclue is available on D-Bus + * so it can be used for determining whether to continue with this geolocation + * provider in Geolocation.cpp + */ +NS_IMETHODIMP +GCLocProviderPriv::Startup() { + if (mProxyManager) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(mClientState == ClientState::Uninit, + "Client in a initialized state but no manager"); + + GUniquePtr<GError> error; + mProxyManager = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, kGeoclueBusName, + kGCManagerPath, kGCManagerInterface, nullptr, getter_Transfers(error))); + if (!mProxyManager) { + GCL_LOG(Info, "Cannot connect to the Manager interface: %s\n", + error->message); + return NS_ERROR_FAILURE; + } + + g_signal_connect(mProxyManager, "notify::g-name-owner", + G_CALLBACK(GCManagerOwnerNotify), this); + + GUniquePtr<gchar> managerOwner(g_dbus_proxy_get_name_owner(mProxyManager)); + if (!managerOwner) { + GCL_LOG(Info, "The Manager interface has no owner\n"); + DeleteManager(); + return NS_ERROR_FAILURE; + } + + GCL_LOG(Info, "Manager interface connected successfully\n"); + + return NS_OK; +} + +void GCLocProviderPriv::WatchStart() { + if (mClientState == ClientState::Idle) { + StartClient(); + } else if (mClientState == ClientState::Started) { + if (mLastPosition && !mLocationTimer) { + GCL_LOG(Verbose, + "Will report the existing location if new one doesn't come up\n"); + SetLocationTimer(); + } + } else if (mClientState == ClientState::SettingAccuracy) { + GCLP_SETSTATE(this, SettingAccuracyForStart); + } else if (mClientState == ClientState::Stopping) { + GCLP_SETSTATE(this, StoppingForRestart); + } +} + +NS_IMETHODIMP +GCLocProviderPriv::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (!mCancellable) { + mCancellable = dont_AddRef(g_cancellable_new()); + } + + if (mClientState != ClientState::Uninit) { + WatchStart(); + return NS_OK; + } + + if (!mProxyManager) { + GCL_LOG(Debug, "watch request falling back to MLS"); + return FallbackToMLS(); + } + + StopMLSFallback(); + + GCLP_SETSTATE(this, Initing); + g_dbus_proxy_call(mProxyManager, "GetClient", nullptr, G_DBUS_CALL_FLAGS_NONE, + -1, mCancellable, + reinterpret_cast<GAsyncReadyCallback>(GetClientResponse), + this); + + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::Shutdown() { + DoShutdownClearCallback(false); + return NS_OK; +} + +NS_IMETHODIMP +GCLocProviderPriv::SetHighAccuracy(bool aHigh) { + GCL_LOG(Verbose, "Want %s accuracy\n", aHigh ? "high" : "low"); + if (!aHigh && AlwaysHighAccuracy()) { + GCL_LOG(Verbose, "Forcing high accuracy due to pref\n"); + aHigh = true; + } + + mAccuracyWanted = aHigh ? Accuracy::High : Accuracy::Low; + MaybeRestartForAccuracy(); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(GCLocProviderPriv::LocationTimerCallback, nsITimerCallback, + nsINamed) + +NS_IMETHODIMP +GCLocProviderPriv::LocationTimerCallback::Notify(nsITimer* aTimer) { + if (mParent) { + RefPtr<GCLocProviderPriv> parent(mParent); + parent->UpdateLastPosition(); + } + + return NS_OK; +} + +GeoclueLocationProvider::GeoclueLocationProvider() { + mPriv = new GCLocProviderPriv; +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GeoclueLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GeoclueLocationProvider::Startup() { return mPriv->Startup(); } + +NS_IMETHODIMP +GeoclueLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + return mPriv->Watch(aCallback); +} + +NS_IMETHODIMP +GeoclueLocationProvider::Shutdown() { return mPriv->Shutdown(); } + +NS_IMETHODIMP +GeoclueLocationProvider::SetHighAccuracy(bool aHigh) { + return mPriv->SetHighAccuracy(aHigh); +} + +} // namespace mozilla::dom diff --git a/dom/system/linux/GeoclueLocationProvider.h b/dom/system/linux/GeoclueLocationProvider.h new file mode 100644 index 0000000000..908cd25e37 --- /dev/null +++ b/dom/system/linux/GeoclueLocationProvider.h @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GeoclueLocationProvider_h +#define GeoclueLocationProvider_h + +#include "mozilla/RefPtr.h" +#include "nsIGeolocationProvider.h" + +namespace mozilla::dom { + +class GCLocProviderPriv; + +class GeoclueLocationProvider final : public nsIGeolocationProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GeoclueLocationProvider(); + + private: + ~GeoclueLocationProvider() = default; + + RefPtr<GCLocProviderPriv> mPriv; +}; + +} // namespace mozilla::dom + +#endif /* GeoclueLocationProvider_h */ diff --git a/dom/system/linux/GpsdLocationProvider.cpp b/dom/system/linux/GpsdLocationProvider.cpp new file mode 100644 index 0000000000..34cd23c453 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GpsdLocationProvider.h" +#include <errno.h> +#include <gps.h> +#include "MLSFallback.h" +#include "mozilla/Atomics.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "prtime.h" + +namespace mozilla { +namespace dom { + +// +// MLSGeolocationUpdate +// + +/** + * |MLSGeolocationUpdate| provides a fallback if gpsd is not supported. + */ +class GpsdLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + nsCOMPtr<nsIGeolocationUpdate> mCallback; +}; + +GpsdLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +GpsdLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + return mCallback->NotifyError(aError); +} + +// +// UpdateRunnable +// + +class GpsdLocationProvider::UpdateRunnable final : public Runnable { + public: + UpdateRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + nsIDOMGeoPosition* aPosition) + : Runnable("GpsdU"), + mLocationProvider(aLocationProvider), + mPosition(aPosition) { + MOZ_ASSERT(mLocationProvider); + MOZ_ASSERT(mPosition); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->Update(mPosition); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + RefPtr<nsIDOMGeoPosition> mPosition; +}; + +// +// NotifyErrorRunnable +// + +class GpsdLocationProvider::NotifyErrorRunnable final : public Runnable { + public: + NotifyErrorRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider, + int aError) + : Runnable("GpsdNE"), + mLocationProvider(aLocationProvider), + mError(aError) { + MOZ_ASSERT(mLocationProvider); + } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + mLocationProvider->NotifyError(mError); + return NS_OK; + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + int mError; +}; + +// +// PollRunnable +// + +/** + * |PollRunnable| does the main work of processing GPS data received + * from gpsd. libgps blocks while polling, so this runnable has to be + * executed on it's own thread. To cancel the poll runnable, invoke + * |StopRunning| and |PollRunnable| will stop within a reasonable time + * frame. + */ +class GpsdLocationProvider::PollRunnable final : public Runnable { + public: + PollRunnable( + const nsMainThreadPtrHandle<GpsdLocationProvider>& aLocationProvider) + : Runnable("GpsdP"), + mLocationProvider(aLocationProvider), + mRunning(true) { + MOZ_ASSERT(mLocationProvider); + } + + static bool IsSupported() { + return GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12; + } + + bool IsRunning() const { return mRunning; } + + void StopRunning() { mRunning = false; } + + // nsIRunnable + // + + NS_IMETHOD Run() override { + int err; + + switch (GPSD_API_MAJOR_VERSION) { + case 5 ... 12: + err = PollLoop5(); + break; + default: + err = GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + break; + } + + if (err) { + NS_DispatchToMainThread( + MakeAndAddRef<NotifyErrorRunnable>(mLocationProvider, err)); + } + + mLocationProvider = nullptr; + + return NS_OK; + } + + protected: + int PollLoop5() { +#if GPSD_API_MAJOR_VERSION >= 5 && GPSD_API_MAJOR_VERSION <= 12 + static const int GPSD_WAIT_TIMEOUT_US = + 1000000; /* us to wait for GPS data */ + + struct gps_data_t gpsData; + + auto res = gps_open(nullptr, nullptr, &gpsData); + + if (res < 0) { + return ErrnoToError(errno); + } + + gps_stream(&gpsData, WATCH_ENABLE | WATCH_JSON, NULL); + + int err = 0; + + // nsGeoPositionCoords will convert NaNs to null for optional properties of + // the JavaScript Coordinates object. + double lat = 0; + double lon = 0; + double alt = UnspecifiedNaN<double>(); + double hError = 0; + double vError = UnspecifiedNaN<double>(); + double heading = UnspecifiedNaN<double>(); + double speed = UnspecifiedNaN<double>(); + + while (IsRunning()) { + errno = 0; + auto hasGpsData = gps_waiting(&gpsData, GPSD_WAIT_TIMEOUT_US); + + if (errno) { + err = ErrnoToError(errno); + break; + } + if (!hasGpsData) { + continue; /* woke up from timeout */ + } + +# if GPSD_API_MAJOR_VERSION >= 7 + res = gps_read(&gpsData, nullptr, 0); +# else + + res = gps_read(&gpsData); +# endif + + if (res < 0) { + err = ErrnoToError(errno); + break; + } else if (!res) { + continue; /* no data available */ + } + +# if GPSD_API_MAJOR_VERSION < 10 + if (gpsData.status == STATUS_NO_FIX) { + continue; + } +# endif + + switch (gpsData.fix.mode) { + case MODE_3D: + double galt; + +# if GPSD_API_MAJOR_VERSION >= 9 + galt = gpsData.fix.altMSL; +# else + galt = gpsData.fix.altitude; +# endif + if (!std::isnan(galt)) { + alt = galt; + } + [[fallthrough]]; + case MODE_2D: + if (!std::isnan(gpsData.fix.latitude)) { + lat = gpsData.fix.latitude; + } + if (!std::isnan(gpsData.fix.longitude)) { + lon = gpsData.fix.longitude; + } + if (!std::isnan(gpsData.fix.epx) && !std::isnan(gpsData.fix.epy)) { + hError = std::max(gpsData.fix.epx, gpsData.fix.epy); + } else if (!std::isnan(gpsData.fix.epx)) { + hError = gpsData.fix.epx; + } else if (!std::isnan(gpsData.fix.epy)) { + hError = gpsData.fix.epy; + } + if (!std::isnan(gpsData.fix.epv)) { + vError = gpsData.fix.epv; + } + if (!std::isnan(gpsData.fix.track)) { + heading = gpsData.fix.track; + } + if (!std::isnan(gpsData.fix.speed)) { + speed = gpsData.fix.speed; + } + break; + default: + continue; // There's no useful data in this fix; continue. + } + + NS_DispatchToMainThread(MakeAndAddRef<UpdateRunnable>( + mLocationProvider, + new nsGeoPosition(lat, lon, alt, hError, vError, heading, speed, + PR_Now() / PR_USEC_PER_MSEC))); + } + + gps_stream(&gpsData, WATCH_DISABLE, NULL); + gps_close(&gpsData); + + return err; +#else + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; +#endif // GPSD_MAJOR_API_VERSION + } + + static int ErrnoToError(int aErrno) { + switch (aErrno) { + case EACCES: + [[fallthrough]]; + case EPERM: + [[fallthrough]]; + case EROFS: + return GeolocationPositionError_Binding::PERMISSION_DENIED; + case ETIME: + [[fallthrough]]; + case ETIMEDOUT: + return GeolocationPositionError_Binding::TIMEOUT; + default: + return GeolocationPositionError_Binding::POSITION_UNAVAILABLE; + } + } + + private: + nsMainThreadPtrHandle<GpsdLocationProvider> mLocationProvider; + Atomic<bool> mRunning; +}; + +// +// GpsdLocationProvider +// + +const uint32_t GpsdLocationProvider::GPSD_POLL_THREAD_TIMEOUT_MS = 5000; + +GpsdLocationProvider::GpsdLocationProvider() {} + +GpsdLocationProvider::~GpsdLocationProvider() {} + +void GpsdLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback || !mPollRunnable) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + /* We got a location from gpsd, so let's cancel our MLS fallback. */ + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + mCallback->Update(aPosition); +} + +void GpsdLocationProvider::NotifyError(int aError) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With gpsd failed, we restart MLS. It will be canceled once we + * get another location from gpsd. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + mCallback->NotifyError(aError); +} + +// nsISupports +// + +NS_IMPL_ISUPPORTS(GpsdLocationProvider, nsIGeolocationProvider) + +// nsIGeolocationProvider +// + +NS_IMETHODIMP +GpsdLocationProvider::Startup() { + if (!PollRunnable::IsSupported()) { + return NS_OK; // We'll fall back to MLS. + } + + if (mPollRunnable) { + return NS_OK; // already running + } + + RefPtr<PollRunnable> pollRunnable = + MakeAndAddRef<PollRunnable>(nsMainThreadPtrHandle<GpsdLocationProvider>( + new nsMainThreadPtrHolder<GpsdLocationProvider>("GpsdLP", this))); + + // Use existing poll thread... + RefPtr<LazyIdleThread> pollThread = mPollThread; + + // ... or create a new one. + if (!pollThread) { + pollThread = MakeAndAddRef<LazyIdleThread>(GPSD_POLL_THREAD_TIMEOUT_MS, + "Gpsd poll thread", + LazyIdleThread::ManualShutdown); + } + + auto rv = pollThread->Dispatch(pollRunnable, NS_DISPATCH_NORMAL); + + if (NS_FAILED(rv)) { + return rv; + } + + mPollRunnable = pollRunnable.forget(); + mPollThread = pollThread.forget(); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + /* The MLS fallback will kick in after a few seconds if gpsd + * doesn't provide location information within time. Once we + * see the first message from gpsd, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::Shutdown() { + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + if (!mPollRunnable) { + return NS_OK; // not running + } + + mPollRunnable->StopRunning(); + mPollRunnable = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +GpsdLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace dom +} // namespace mozilla diff --git a/dom/system/linux/GpsdLocationProvider.h b/dom/system/linux/GpsdLocationProvider.h new file mode 100644 index 0000000000..544f0e4c69 --- /dev/null +++ b/dom/system/linux/GpsdLocationProvider.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GpsdLocationProvider_h +#define GpsdLocationProvider_h + +#include "nsCOMPtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" + +class MLSFallback; + +namespace mozilla { + +class LazyIdleThread; + +namespace dom { + +class GpsdLocationProvider final : public nsIGeolocationProvider { + class MLSGeolocationUpdate; + class NotifyErrorRunnable; + class PollRunnable; + class UpdateRunnable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + + GpsdLocationProvider(); + + private: + ~GpsdLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + void NotifyError(int aError); + + static const uint32_t GPSD_POLL_THREAD_TIMEOUT_MS; + + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<LazyIdleThread> mPollThread; + RefPtr<PollRunnable> mPollRunnable; + RefPtr<MLSFallback> mMLSProvider; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/PortalLocationProvider.cpp b/dom/system/linux/PortalLocationProvider.cpp new file mode 100644 index 0000000000..6ebb1854dc --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.cpp @@ -0,0 +1,351 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PortalLocationProvider.h" +#include "MLSFallback.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "prtime.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/XREAppData.h" + +#include <gio/gio.h> +#include <glib-object.h> + +extern const mozilla::XREAppData* gAppData; + +namespace mozilla::dom { + +#ifdef MOZ_LOGGING +static LazyLogModule sPortalLog("Portal"); +# define LOG_PORTAL(...) MOZ_LOG(sPortalLog, LogLevel::Debug, (__VA_ARGS__)) +#else +# define LOG_PORTAL(...) +#endif /* MOZ_LOGGING */ + +const char kDesktopBusName[] = "org.freedesktop.portal.Desktop"; +const char kSessionInterfaceName[] = "org.freedesktop.portal.Session"; + +/** + * |MLSGeolocationUpdate| provides a fallback if Portal is not supported. + */ +class PortalLocationProvider::MLSGeolocationUpdate final + : public nsIGeolocationUpdate { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + explicit MLSGeolocationUpdate(nsIGeolocationUpdate* aCallback); + + protected: + ~MLSGeolocationUpdate() = default; + + private: + const nsCOMPtr<nsIGeolocationUpdate> mCallback; +}; + +PortalLocationProvider::MLSGeolocationUpdate::MLSGeolocationUpdate( + nsIGeolocationUpdate* aCallback) + : mCallback(aCallback) { + MOZ_ASSERT(mCallback); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider::MLSGeolocationUpdate, + nsIGeolocationUpdate); + +// nsIGeolocationUpdate +// + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::Update( + nsIDOMGeoPosition* aPosition) { + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + aPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + LOG_PORTAL("MLS is updating position\n"); + return mCallback->Update(aPosition); +} + +NS_IMETHODIMP +PortalLocationProvider::MLSGeolocationUpdate::NotifyError(uint16_t aError) { + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + return callback->NotifyError(aError); +} + +// +// PortalLocationProvider +// + +PortalLocationProvider::PortalLocationProvider() = default; + +PortalLocationProvider::~PortalLocationProvider() { + if (mDBUSLocationProxy || mRefreshTimer || mMLSProvider) { + NS_WARNING( + "PortalLocationProvider: Shutdown() had not been called before " + "destructor."); + Shutdown(); + } +} + +void PortalLocationProvider::Update(nsIDOMGeoPosition* aPosition) { + if (!mCallback) { + return; // not initialized or already shut down + } + + if (mMLSProvider) { + LOG_PORTAL( + "Update from location portal received: Cancelling fallback MLS " + "provider\n"); + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + + LOG_PORTAL("Send updated location to the callback %p", mCallback.get()); + mCallback->Update(aPosition); + + aPosition->GetCoords(getter_AddRefs(mLastGeoPositionCoords)); + // Schedule sending repetitive updates because we don't get more until + // position is changed from portal. That would lead to timeout on the + // Firefox side. + SetRefreshTimer(5000); +} + +void PortalLocationProvider::NotifyError(int aError) { + LOG_PORTAL("*****NotifyError %d\n", aError); + if (!mCallback) { + return; // not initialized or already shut down + } + + if (!mMLSProvider) { + /* With Portal failed, we restart MLS. It will be canceled once we + * get another location from Portal. Start it immediately. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(0); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + nsCOMPtr<nsIGeolocationUpdate> callback(mCallback); + callback->NotifyError(aError); +} + +NS_IMPL_ISUPPORTS(PortalLocationProvider, nsIGeolocationProvider) + +static void location_updated_signal_cb(GDBusProxy* proxy, gchar* sender_name, + gchar* signal_name, GVariant* parameters, + gpointer user_data) { + LOG_PORTAL("Signal: %s received from: %s\n", sender_name, signal_name); + + if (g_strcmp0(signal_name, "LocationUpdated")) { + LOG_PORTAL("Unexpected signal %s received", signal_name); + return; + } + + auto* locationProvider = static_cast<PortalLocationProvider*>(user_data); + RefPtr<GVariant> response_data; + gchar* session_handle; + g_variant_get(parameters, "(o@a{sv})", &session_handle, + response_data.StartAssignment()); + if (!response_data) { + LOG_PORTAL("Missing response data from portal\n"); + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + LOG_PORTAL("Session handle: %s Response data: %s\n", session_handle, + GUniquePtr<gchar>(g_variant_print(response_data, TRUE)).get()); + g_free(session_handle); + + double lat = 0; + double lon = 0; + if (!g_variant_lookup(response_data, "Latitude", "d", &lat) || + !g_variant_lookup(response_data, "Longitude", "d", &lon)) { + locationProvider->NotifyError( + GeolocationPositionError_Binding::POSITION_UNAVAILABLE); + return; + } + + double alt = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Altitude", "d", &alt); + double vError = 0; + double hError = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Accuracy", "d", &hError); + double heading = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Heading", "d", &heading); + double speed = UnspecifiedNaN<double>(); + g_variant_lookup(response_data, "Speed", "d", &speed); + + locationProvider->Update(new nsGeoPosition(lat, lon, alt, hError, vError, + heading, speed, + PR_Now() / PR_USEC_PER_MSEC)); +} + +NS_IMETHODIMP +PortalLocationProvider::Startup() { + LOG_PORTAL("Starting location portal"); + if (mDBUSLocationProxy) { + LOG_PORTAL("Proxy already started.\n"); + return NS_OK; + } + + // Create dbus proxy for the Location portal + GUniquePtr<GError> error; + mDBUSLocationProxy = dont_AddRef(g_dbus_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, + nullptr, /* GDBusInterfaceInfo */ + kDesktopBusName, "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Location", nullptr, /* GCancellable */ + getter_Transfers(error))); + if (!mDBUSLocationProxy) { + g_printerr("Error creating location dbus proxy: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Listen to signals which will be send to us with the location data + mDBUSSignalHandler = + g_signal_connect(mDBUSLocationProxy, "g-signal", + G_CALLBACK(location_updated_signal_cb), this); + + // Call CreateSession of the location portal + GVariantBuilder builder; + + nsAutoCString appName; + gAppData->GetDBusAppName(appName); + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", + g_variant_new_string(appName.get())); + + RefPtr<GVariant> result = dont_AddRef(g_dbus_proxy_call_sync( + mDBUSLocationProxy, "CreateSession", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error))); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling CreateSession method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + + // Start to listen to the location changes + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + // TODO Use wayland:handle as described in + // https://flatpak.github.io/xdg-desktop-portal/#parent_window + const gchar* parent_window = ""; + gchar* portalSession; + g_variant_get_child(result, 0, "o", &portalSession); + mPortalSession.reset(portalSession); + + result = g_dbus_proxy_call_sync( + mDBUSLocationProxy, "Start", + g_variant_new("(osa{sv})", mPortalSession.get(), parent_window, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, getter_Transfers(error)); + + g_variant_builder_clear(&builder); + + if (!result) { + g_printerr("Error calling Start method: %s\n", error->message); + return NS_OK; // fallback to MLS + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Watch(nsIGeolocationUpdate* aCallback) { + mCallback = aCallback; + + if (mLastGeoPositionCoords) { + // We cannot immediately call the Update there becase the window is not + // yet ready for that. + LOG_PORTAL( + "Update location in 1ms because we have the valid coords cached."); + SetRefreshTimer(1); + return NS_OK; + } + + /* The MLS fallback will kick in after 12 seconds if portal + * doesn't provide location information within time. Once we + * see the first message from portal, the fallback will be + * disabled in |Update|. + */ + mMLSProvider = MakeAndAddRef<MLSFallback>(12000); + mMLSProvider->Startup(new MLSGeolocationUpdate(aCallback)); + + return NS_OK; +} + +NS_IMETHODIMP PortalLocationProvider::GetName(nsACString& aName) { + aName.AssignLiteral("PortalLocationProvider"); + return NS_OK; +} + +void PortalLocationProvider::SetRefreshTimer(int aDelay) { + LOG_PORTAL("SetRefreshTimer for %p to %d ms\n", this, aDelay); + if (!mRefreshTimer) { + NS_NewTimerWithCallback(getter_AddRefs(mRefreshTimer), this, aDelay, + nsITimer::TYPE_ONE_SHOT); + } else { + mRefreshTimer->Cancel(); + mRefreshTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT); + } +} + +NS_IMETHODIMP +PortalLocationProvider::Notify(nsITimer* timer) { + // We need to reschedule the timer because we won't get any update + // from portal until the location is changed. That would cause + // watchPosition to fail with TIMEOUT error. + SetRefreshTimer(5000); + if (mLastGeoPositionCoords) { + LOG_PORTAL("Update location callback with latest coords."); + mCallback->Update( + new nsGeoPosition(mLastGeoPositionCoords, PR_Now() / PR_USEC_PER_MSEC)); + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::Shutdown() { + LOG_PORTAL("Shutdown location provider"); + if (mRefreshTimer) { + mRefreshTimer->Cancel(); + mRefreshTimer = nullptr; + } + mLastGeoPositionCoords = nullptr; + if (mDBUSLocationProxy) { + g_signal_handler_disconnect(mDBUSLocationProxy, mDBUSSignalHandler); + LOG_PORTAL("calling Close method to the session interface...\n"); + RefPtr<GDBusMessage> message = dont_AddRef(g_dbus_message_new_method_call( + kDesktopBusName, mPortalSession.get(), kSessionInterfaceName, "Close")); + mPortalSession = nullptr; + if (message) { + GUniquePtr<GError> error; + GDBusConnection* connection = + g_dbus_proxy_get_connection(mDBUSLocationProxy); + g_dbus_connection_send_message( + connection, message, G_DBUS_SEND_MESSAGE_FLAGS_NONE, + /*out_serial=*/nullptr, getter_Transfers(error)); + if (error) { + g_printerr("Failed to close the session: %s\n", error->message); + } + } + mDBUSLocationProxy = nullptr; + } + if (mMLSProvider) { + mMLSProvider->Shutdown(); + mMLSProvider = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +PortalLocationProvider::SetHighAccuracy(bool aHigh) { return NS_OK; } + +} // namespace mozilla::dom diff --git a/dom/system/linux/PortalLocationProvider.h b/dom/system/linux/PortalLocationProvider.h new file mode 100644 index 0000000000..e7ead0ab5c --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PortalLocationProvider_h +#define PortalLocationProvider_h + +#include "nsCOMPtr.h" +#include "mozilla/GRefPtr.h" +#include "mozilla/GUniquePtr.h" +#include "Geolocation.h" +#include "nsIGeolocationProvider.h" +#include <gio/gio.h> + +class MLSFallback; + +namespace mozilla::dom { + +class PortalLocationProvider final : public nsIGeolocationProvider, + public nsITimerCallback, + public nsINamed { + class MLSGeolocationUpdate; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + PortalLocationProvider(); + + void Update(nsIDOMGeoPosition* aPosition); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void NotifyError(int aError); + + private: + ~PortalLocationProvider(); + void SetRefreshTimer(int aDelay); + + RefPtr<GDBusProxy> mDBUSLocationProxy; + gulong mDBUSSignalHandler = 0; + + GUniquePtr<gchar> mPortalSession; + nsCOMPtr<nsIGeolocationUpdate> mCallback; + RefPtr<MLSFallback> mMLSProvider; + nsCOMPtr<nsIDOMGeoPositionCoords> mLastGeoPositionCoords; + nsCOMPtr<nsITimer> mRefreshTimer; +}; + +} // namespace mozilla::dom + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build new file mode 100644 index 0000000000..9e7d6ef31d --- /dev/null +++ b/dom/system/linux/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["MOZ_GPSD"]: + SOURCES += ["GpsdLocationProvider.cpp"] + + CXXFLAGS += CONFIG["MOZ_GPSD_CFLAGS"] + + OS_LIBS += CONFIG["MOZ_GPSD_LIBS"] + + LOCAL_INCLUDES += ["/dom/geolocation"] + + +if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += ["GeoclueLocationProvider.cpp"] + SOURCES += ["PortalLocationProvider.cpp"] + LOCAL_INCLUDES += ["/dom/geolocation"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +FINAL_LIBRARY = "xul" |