diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp index 7858c69827b3..efcd459ecdef 100644 --- a/dom/geolocation/Geolocation.cpp +++ b/dom/geolocation/Geolocation.cpp @@ -20,6 +20,7 @@ #include "mozilla/Unused.h" #include "mozilla/WeakPtr.h" #include "mozilla/EventStateManager.h" +#include "mozilla/WidgetUtilsGtk.h" #include "nsComponentManagerUtils.h" #include "nsContentPermissionHelper.h" #include "nsContentUtils.h" @@ -43,6 +44,10 @@ class nsIPrincipal; # include "GpsdLocationProvider.h" #endif +#ifdef MOZ_ENABLE_DBUS +# include "PortalLocationProvider.h" +#endif + #ifdef MOZ_WIDGET_COCOA # include "CoreLocationLocationProvider.h" #endif @@ -494,6 +499,11 @@ nsresult nsGeolocationService::Init() { mProvider = new GpsdLocationProvider(); } # endif +# ifdef MOZ_ENABLE_DBUS + if (!mProvider && widget::ShouldUsePortal(widget::PortalKind::Location)) { + mProvider = new PortalLocationProvider(); + } +# endif #endif #ifdef MOZ_WIDGET_COCOA diff --git a/dom/geolocation/moz.build b/dom/geolocation/moz.build index 5c7f7471818c..8482c8aa1fc1 100644 --- a/dom/geolocation/moz.build +++ b/dom/geolocation/moz.build @@ -55,3 +55,7 @@ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": "/dom/system/linux", ] DEFINES["MOZ_GPSD"] = True + if CONFIG["MOZ_ENABLE_DBUS"]: + LOCAL_INCLUDES += ["/dom/system/linux"] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] diff --git a/dom/system/linux/PortalLocationProvider.cpp b/dom/system/linux/PortalLocationProvider.cpp new file mode 100644 index 000000000000..bdae871a52f4 --- /dev/null +++ b/dom/system/linux/PortalLocationProvider.cpp @@ -0,0 +1,356 @@ +/* -*- 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 +#include "MLSFallback.h" +#include "mozilla/Atomics.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/GeolocationPositionErrorBinding.h" +#include "GeolocationPosition.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "prtime.h" +#include "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/XREAppData.h" + +#include +#include + +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 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 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 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(0); + mMLSProvider->Startup(new MLSGeolocationUpdate(mCallback)); + } + + nsCOMPtr 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(user_data); + RefPtr 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(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(); + g_variant_lookup(response_data, "Altitude", "d", &alt); + double vError = 0; + double hError = UnspecifiedNaN(); + g_variant_lookup(response_data, "Accuracy", "d", &hError); + double heading = UnspecifiedNaN(); + g_variant_lookup(response_data, "Heading", "d", &heading); + double speed = UnspecifiedNaN(); + 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 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->remotingName); + appName.ReplaceChar("+/=-", '_'); + 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 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(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 message = dont_AddRef(g_dbus_message_new_method_call( + kDesktopBusName, mPortalSession.get(), kSessionInterfaceName, "Close")); + mPortalSession = nullptr; + if (message) { + GUniquePtr 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 000000000000..e7ead0ab5c79 --- /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 + +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 mDBUSLocationProxy; + gulong mDBUSSignalHandler = 0; + + GUniquePtr mPortalSession; + nsCOMPtr mCallback; + RefPtr mMLSProvider; + nsCOMPtr mLastGeoPositionCoords; + nsCOMPtr mRefreshTimer; +}; + +} // namespace mozilla::dom + +#endif /* GpsLocationProvider_h */ diff --git a/dom/system/linux/moz.build b/dom/system/linux/moz.build index ab9d076deb16..840066a51cd4 100644 --- a/dom/system/linux/moz.build +++ b/dom/system/linux/moz.build @@ -13,4 +13,11 @@ if CONFIG["MOZ_GPSD"]: LOCAL_INCLUDES += ["/dom/geolocation"] + +if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += ["PortalLocationProvider.cpp"] + LOCAL_INCLUDES += ["/dom/geolocation"] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + FINAL_LIBRARY = "xul" diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 2264cae0db10..bc4701e9e02e 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -13200,6 +13200,16 @@ type: int32_t value: 2 mirror: always + +# Whether to use XDG portal for geolocation. +# https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Location +# - 0: never +# - 1: always +# - 2: auto +- name: widget.use-xdg-desktop-portal.location + type: int32_t + value: 2 + mirror: always #endif #ifdef XP_WIN diff --git a/widget/gtk/GRefPtr.h b/widget/gtk/GRefPtr.h index 1ad6b7005401..6c118d68597e 100644 --- a/widget/gtk/GRefPtr.h +++ b/widget/gtk/GRefPtr.h @@ -39,6 +39,7 @@ GOBJECT_TRAITS(GDBusProxy) GOBJECT_TRAITS(GAppInfo) GOBJECT_TRAITS(GAppLaunchContext) GOBJECT_TRAITS(GdkDragContext) +GOBJECT_TRAITS(GDBusMessage) GOBJECT_TRAITS(GdkPixbuf) #ifdef MOZ_ENABLE_DBUS diff --git a/widget/gtk/WidgetUtilsGtk.cpp b/widget/gtk/WidgetUtilsGtk.cpp index 33faa61f1b78..9b75a341b958 100644 --- a/widget/gtk/WidgetUtilsGtk.cpp +++ b/widget/gtk/WidgetUtilsGtk.cpp @@ -136,6 +136,8 @@ bool ShouldUsePortal(PortalKind aPortalKind) { case PortalKind::Settings: autoBehavior = true; return StaticPrefs::widget_use_xdg_desktop_portal_settings(); + case PortalKind::Location: + return StaticPrefs::widget_use_xdg_desktop_portal_location(); } return 2; }(); diff --git a/widget/gtk/WidgetUtilsGtk.h b/widget/gtk/WidgetUtilsGtk.h index cb163d33594c..960e206f23c5 100644 --- a/widget/gtk/WidgetUtilsGtk.h +++ b/widget/gtk/WidgetUtilsGtk.h @@ -44,6 +44,7 @@ enum class PortalKind { FilePicker, MimeHandler, Settings, + Location, }; bool ShouldUsePortal(PortalKind);