diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c19ede88cee61..6336ce1c3d312 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,6 +41,14 @@ find_package(Qt${QT_VERSION_MAJOR}Core5Compat ${REQUIRED_QT_VERSION} CONFIG QUIE TYPE REQUIRED ) +if(UNIX AND NOT APPLE) + find_package(Qt${QT_VERSION_MAJOR}DBus ${REQUIRED_QT_VERSION} CONFIG QUIET) + set_package_properties(Qt${QT_VERSION_MAJOR}DBus PROPERTIES + DESCRIPTION "Qt${QT_VERSION_MAJOR} DBus component." + TYPE REQUIRED + ) +endif() + get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) message(STATUS "Using Qt ${Qt${QT_MAJOR_VERSION}Core_VERSION} (${QT_QMAKE_EXECUTABLE})") diff --git a/src/common/common.cmake b/src/common/common.cmake index 564b6c69b5776..645640362405d 100644 --- a/src/common/common.cmake +++ b/src/common/common.cmake @@ -41,6 +41,8 @@ elseif(APPLE) elseif(UNIX AND NOT APPLE) list(APPEND common_SOURCES ${CMAKE_CURRENT_LIST_DIR}/utility_unix.cpp + ${CMAKE_CURRENT_LIST_DIR}/xdg_portal.h + ${CMAKE_CURRENT_LIST_DIR}/xdg_portal.cpp ) endif() diff --git a/src/common/utility.h b/src/common/utility.h index e8eaa10db6da4..b7ca0b2230bf2 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -153,6 +153,10 @@ namespace Utility { OCSYNC_EXPORT uint convertSizeToUint(size_t &convertVar); OCSYNC_EXPORT int convertSizeToInt(size_t &convertVar); +#ifdef Q_OS_UNIX + OCSYNC_EXPORT QString getAppExecutablePath(); +#endif + #ifdef Q_OS_WIN OCSYNC_EXPORT DWORD convertSizeToDWORD(size_t &convertVar); #endif diff --git a/src/common/utility_unix.cpp b/src/common/utility_unix.cpp index 555a37531cc98..120fb65bb962e 100644 --- a/src/common/utility_unix.cpp +++ b/src/common/utility_unix.cpp @@ -6,6 +6,7 @@ #include "utility.h" #include "config.h" +#include "xdg_portal.h" #include #include @@ -72,6 +73,10 @@ bool Utility::hasSystemLaunchOnStartup(const QString &appName) bool Utility::hasLaunchOnStartup(const QString &appName) { + //! if we are using portals this check will not work, that's why its usage in generalsettings.cpp is currently disabled. + //! as far as i can tell the portal does not let us do this check. how badly do we want it? the only two times its used is + //! that time in generalsettings.cpp and in tests, for settings as far as i can tell that check isn't really *needed* and + //! we might want to adapt tests to portals anyway... const QString desktopFileLocation = getUserAutostartDir() + appName + QLatin1String(".desktop"); return QFile::exists(desktopFileLocation); } @@ -96,43 +101,58 @@ QString Utility::syncFolderDisplayName(const QString &folder, const QString &dis void Utility::setLaunchOnStartup(const QString &appName, const QString &guiName, bool enable) { - const auto userAutoStartPath = getUserAutostartDir(); - const QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop"); - if (enable) { - if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) { - qCWarning(lcUtility) << "Could not create autostart folder" << userAutoStartPath; - return; - } - QFile iniFile(desktopFileLocation); - if (!iniFile.open(QIODevice::WriteOnly)) { - qCWarning(lcUtility) << "Could not write auto start entry" << desktopFileLocation; - return; - } - // When running inside an AppImage, we need to set the path to the - // AppImage instead of the path to the executable - const QString appImagePath = qEnvironmentVariable("APPIMAGE"); - const bool runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath); - const QString executablePath = runningInsideAppImage ? appImagePath : QCoreApplication::applicationFilePath(); - - QTextStream ts(&iniFile); - ts << QLatin1String("[Desktop Entry]\n") - << QLatin1String("Name=") << guiName << QLatin1Char('\n') - << QLatin1String("GenericName=") << QLatin1String("File Synchronizer\n") - << QLatin1String("Exec=\"") << executablePath << "\" --background\n" - << QLatin1String("Terminal=") << "false\n" - << QLatin1String("Icon=") << APPLICATION_ICON_NAME << QLatin1Char('\n') - << QLatin1String("Categories=") << QLatin1String("Network\n") - << QLatin1String("Type=") << QLatin1String("Application\n") - << QLatin1String("StartupNotify=") << "false\n" - << QLatin1String("X-GNOME-Autostart-enabled=") << "true\n" - << QLatin1String("X-GNOME-Autostart-Delay=10") << Qt::endl; + const bool usePortals = true; // TODO: how should this be configured? probably a build flag? or should we just default always use portals and if the call fails *then* fallback? XdgPortal::background does return a bool for if it was successful so that wouldn't be a difficulty. + if (usePortals) { + XdgPortal portal; + portal.background(appName, enable); } else { - if (!QFile::remove(desktopFileLocation)) { - qCWarning(lcUtility) << "Could not remove autostart desktop file"; + const auto userAutoStartPath = getUserAutostartDir(); + const QString desktopFileLocation = userAutoStartPath + appName + QLatin1String(".desktop"); + if (enable) { + if (!QDir().exists(userAutoStartPath) && !QDir().mkpath(userAutoStartPath)) { + qCWarning(lcUtility) << "Could not create autostart folder" << userAutoStartPath; + return; + } + QFile iniFile(desktopFileLocation); + if (!iniFile.open(QIODevice::WriteOnly)) { + qCWarning(lcUtility) << "Could not write auto start entry" << desktopFileLocation; + return; + } + + const auto executablePath = Utility::getAppExecutablePath(); + + QTextStream ts(&iniFile); + ts << QLatin1String("[Desktop Entry]\n") + << QLatin1String("Name=") << guiName << QLatin1Char('\n') + << QLatin1String("GenericName=") << QLatin1String("File Synchronizer\n") + << QLatin1String("Exec=\"") << executablePath << "\" --background\n" + << QLatin1String("Terminal=") << "false\n" + << QLatin1String("Icon=") << APPLICATION_ICON_NAME << QLatin1Char('\n') + << QLatin1String("Categories=") << QLatin1String("Network\n") + << QLatin1String("Type=") << QLatin1String("Application\n") + << QLatin1String("StartupNotify=") << "false\n" + << QLatin1String("X-GNOME-Autostart-enabled=") << "true\n" + << QLatin1String("X-GNOME-Autostart-Delay=10") << Qt::endl; + } else { + if (!QFile::remove(desktopFileLocation)) { + qCWarning(lcUtility) << "Could not remove autostart desktop file"; + } } } } +QString Utility::getAppExecutablePath() +{ + // When running inside an AppImage, we need to set the path to the + // AppImage instead of the path to the executable + const QString appImagePath = qEnvironmentVariable("APPIMAGE"); + const QString flatpakId = qEnvironmentVariable("FLATPAK_ID"); + const bool runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath); + const bool runningInsideFlatpak = !flatpakId.isNull(); + const QString executablePath = runningInsideAppImage ? QLatin1String("\"%1\"").arg(appImagePath) : runningInsideFlatpak ? QLatin1String("flatpak run %1").arg(flatpakId) : QLatin1String("\"%1\"").arg(QCoreApplication::applicationFilePath()); + return executablePath; +} + bool Utility::hasDarkSystray() { return true; diff --git a/src/common/xdg_portal.cpp b/src/common/xdg_portal.cpp new file mode 100644 index 0000000000000..1b3face40cdb4 --- /dev/null +++ b/src/common/xdg_portal.cpp @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "xdg_portal.h" +#include "utility.h" +#include +#include +#include +#include +#include +#include +#include + +namespace OCC { + +constexpr auto portalDesktopService = "org.freedesktop.portal.Desktop"; +constexpr auto portalDesktopPath = "/org/freedesktop/portal/desktop"; + +XdgPortal::XdgPortal(QObject *parent) + : QObject(parent) + , m_available(false) +{ + initPortalInterface(); +} + +void XdgPortal::initPortalInterface() +{ + auto *busInterface = QDBusConnection::sessionBus().interface(); + if (!busInterface) { + qWarning() << "XDG Desktop Portal not available"; + return; + } + + const auto registered = busInterface->isServiceRegistered(QLatin1String(portalDesktopService)); + m_available = registered.isValid() && registered.value(); + if (!m_available) { + qWarning() << "XDG Desktop Portal not available"; + } +} + +bool XdgPortal::background(const QString &handle_token, const bool &autostart) +{ + if (!m_available) { + return false; + } + + QDBusInterface backgroundInterface( + QLatin1String(portalDesktopService), + QLatin1String(portalDesktopPath), + QLatin1String("org.freedesktop.portal.Background"), + QDBusConnection::sessionBus()); + if (!backgroundInterface.isValid()) { // just in case + qWarning() << "org.freedesktop.portal.Background not available"; + return false; + } + + QVariantMap options; + options[QLatin1String("autostart")] = autostart; + const QString flatpakId = qEnvironmentVariable("FLATPAK_ID"); + if (flatpakId.isNull()) { + QStringList list = {Utility::getAppExecutablePath(), QLatin1String("--background")}; + options[QLatin1String("commandline")] = list; + } else { // when an app is running as a flatpak its "commandline" parameter is handled differently + QStringList list = {QFileInfo(QCoreApplication::applicationFilePath()).fileName(), QLatin1String("--background")}; + options[QLatin1String("commandline")] = list; + } + + QDBusMessage reply = backgroundInterface.call( + QLatin1String("RequestBackground"), + handle_token, + options + ); + + if (reply.type() == QDBusMessage::ErrorMessage) { + qWarning() << "Background request failed: " << reply.errorMessage(); + return false; + } + + return true; +} + +} // namespace OCC \ No newline at end of file diff --git a/src/common/xdg_portal.h b/src/common/xdg_portal.h new file mode 100644 index 0000000000000..9a73c41848946 --- /dev/null +++ b/src/common/xdg_portal.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef XDG_PORTAL_H +#define XDG_PORTAL_H + +#include +#include +#include + +namespace OCC { + +/** + * @brief XDG Desktop Portal interface for Qt D-Bus + * + * Adstracts D-Bus calls behind simpler methods. + */ +class XdgPortal : public QObject +{ + Q_OBJECT + +public: + explicit XdgPortal(QObject *parent = nullptr); + + /** + * @brief Requests that the application is allowed to run in the background; see https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Background.html + * @param handle_token A string that will be used as the last element of the handle. Must be a valid object path element. See the [Request](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request) documentation for more information about the handle. + * @param autostart true if the app also wants to be started automatically at login. + * @return True if successful + */ + bool background(const QString &handle_token, const bool &autostart); + +private: + bool m_available; + + void initPortalInterface(); +}; + +} // namespace OCC + +#endif // XDG_PORTAL_H \ No newline at end of file diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index f79a80988468a..498e96d5da940 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -651,9 +651,10 @@ void GeneralSettings::saveMiscSettings() void GeneralSettings::slotToggleLaunchOnStartup(bool enable) { const auto theme = Theme::instance(); - if (enable == Utility::hasLaunchOnStartup(theme->appName())) { - return; - } + //! please see Utility::hasLaunchOnStartup for why this is disabled + // if (enable == Utility::hasLaunchOnStartup(theme->appName())) { + // return; + // } ConfigFile configFile; configFile.setLaunchOnSystemStartup(enable);