/* -*- 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 #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->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 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