Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cmake/modules/NSIS.template.in
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,6 @@ Section "${APPLICATION_NAME}" SEC_APPLICATION
File "${QT_DLL_PATH}\Qt5Xml.dll"

;QtWebKit dependencies
File "${QT_DLL_PATH}\Qt5Multimedia.dll"
File "${QT_DLL_PATH}\Qt5MultimediaWidgets.dll"
File "${QT_DLL_PATH}\Qt5Sensors.dll"

;Qt deps
Expand Down
30 changes: 29 additions & 1 deletion src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ set(client_SRCS
caseclashfilenamedialog.cpp
callstatechecker.h
callstatechecker.cpp
notificationsoundplayer.h
notificationsoundplayer.cpp
notificationsoundplayer_p.h
conflictdialog.h
conflictdialog.cpp
conflictsolver.h
Expand Down Expand Up @@ -289,6 +292,7 @@ endif()
IF( APPLE )
list(APPEND client_SRCS cocoainitializer_mac.mm)
list(APPEND client_SRCS systray_mac_common.mm)
list(APPEND client_SRCS notificationsoundplayer_mac.mm)
list(APPEND client_SRCS macOS/trayaccountpopup_mac.mm)
list(APPEND client_SRCS macOS/singleinstancemanager_mac.mm)

Expand Down Expand Up @@ -351,12 +355,13 @@ IF( APPLE )
ENDIF()

IF( NOT WIN32 AND NOT APPLE )
set(client_SRCS ${client_SRCS} folderwatcher_linux.cpp)
set(client_SRCS ${client_SRCS} folderwatcher_linux.cpp notificationsoundplayer_linux.cpp)
ENDIF()
IF( WIN32 )
set(client_SRCS
${client_SRCS}
folderwatcher_win.cpp
notificationsoundplayer_win.cpp
navigationpanehelper.h
navigationpanehelper.cpp
shellextensionsserver.cpp
Expand Down Expand Up @@ -677,6 +682,22 @@ if( UNIX AND NOT APPLE )
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS DBus)
target_link_libraries(nextcloudCore PUBLIC Qt::DBus)
target_compile_definitions(nextcloudCore PUBLIC "USE_FDO_NOTIFICATIONS")

# libcanberra is the freedesktop event-sound API used to play the
# incoming-call ringtone. If the dev package is missing, we fall back to
# a no-op backend so the build still succeeds; distro packaging should
# treat libcanberra as a soft runtime dependency.
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(CANBERRA IMPORTED_TARGET libcanberra)
endif()
if(CANBERRA_FOUND)
target_link_libraries(nextcloudCore PRIVATE PkgConfig::CANBERRA)
target_compile_definitions(nextcloudCore PRIVATE HAVE_LIBCANBERRA)
else()
message(STATUS "libcanberra not found; notification sounds will be silent on this build")
endif()
target_compile_definitions(nextcloudCore PRIVATE NEXTCLOUD_HAS_NATIVE_SOUND_BACKEND)
endif()

if (APPLE)
Expand All @@ -685,6 +706,13 @@ if (APPLE)
else()
target_link_libraries(nextcloudCore PUBLIC "-framework UserNotifications")
endif()
target_link_libraries(nextcloudCore PRIVATE "-framework AVFoundation" "-framework Foundation")
target_compile_definitions(nextcloudCore PRIVATE NEXTCLOUD_HAS_NATIVE_SOUND_BACKEND)
endif()

if (WIN32)
target_link_libraries(nextcloudCore PRIVATE xaudio2)
target_compile_definitions(nextcloudCore PRIVATE NEXTCLOUD_HAS_NATIVE_SOUND_BACKEND)
endif()

install(TARGETS nextcloud
Expand Down
293 changes: 293 additions & 0 deletions src/gui/notificationsoundplayer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/

Check warning on line 4 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Edit this comment to use the C++ format, i.e. "//".

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL70&open=AZ6OC6N0jA-zK_J2XL70&pullRequest=10098

#include "notificationsoundplayer.h"
#include "notificationsoundplayer_p.h"

#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QLoggingCategory>
#include <QMutex>
#include <QMutexLocker>
#include <QSaveFile>
#include <QStandardPaths>
#include <QString>
#include <QTemporaryFile>
#include <QUrl>

namespace OCC {

Q_LOGGING_CATEGORY(lcNotificationSoundPlayer, "nextcloud.gui.notificationsoundplayer", QtInfoMsg)

Check warning on line 26 in src/gui/notificationsoundplayer.cpp

View workflow job for this annotation

GitHub Actions / build

src/gui/notificationsoundplayer.cpp:26:1 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'Q_LOGGING_CATEGORY' is non-const and globally accessible, consider making it const

namespace {

QString extractQrcToCache(const QString &qrcResourcePath)
{
static QMutex cacheMutex;
static QHash<QString, QString> extractedPaths;

QMutexLocker locker(&cacheMutex);

Check warning on line 35 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "locker" of type "class QMutexLocker<class QMutex>" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL72&open=AZ6OC6N0jA-zK_J2XL72&pullRequest=10098
if (const auto cached = extractedPaths.value(qrcResourcePath); !cached.isEmpty()) {
if (QFile::exists(cached)) {
return cached;
}
extractedPaths.remove(qrcResourcePath);
}

QFile source(qrcResourcePath);
if (!source.open(QIODevice::ReadOnly)) {
qCWarning(lcNotificationSoundPlayer) << "Cannot open embedded resource" << qrcResourcePath
<< ":" << source.errorString();
return {};
}
const auto bytes = source.readAll();
source.close();

QByteArray fingerprint = QByteArrayLiteral("notificationsoundplayer:");

Check warning on line 52 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL71&open=AZ6OC6N0jA-zK_J2XL71&pullRequest=10098
fingerprint += qrcResourcePath.toUtf8();
fingerprint += ':';
fingerprint += QByteArray::number(bytes.size());
const auto hash = QCryptographicHash::hash(fingerprint, QCryptographicHash::Sha1).toHex();

const auto cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/sounds");
const auto suffix = QFileInfo(qrcResourcePath).suffix();
const auto dottedSuffix = suffix.isEmpty() ? QString() : QStringLiteral(".") + suffix;
const auto destinationPath = cacheDir + QStringLiteral("/") + QString::fromLatin1(hash) + dottedSuffix;

if (QFile::exists(destinationPath)) {
extractedPaths.insert(qrcResourcePath, destinationPath);
return destinationPath;
}

if (QDir().mkpath(cacheDir)) {
QSaveFile out(destinationPath);
if (out.open(QIODevice::WriteOnly)) {
if (out.write(bytes) == bytes.size() && out.commit()) {
extractedPaths.insert(qrcResourcePath, destinationPath);
return destinationPath;
}
qCWarning(lcNotificationSoundPlayer) << "Failed to write extracted sound to" << destinationPath
<< ":" << out.errorString();
} else {
qCWarning(lcNotificationSoundPlayer) << "Failed to open" << destinationPath << "for writing:"
<< out.errorString();
}
} else {
qCWarning(lcNotificationSoundPlayer) << "Cannot create cache directory" << cacheDir
<< "for extracted sounds; falling back to QTemporaryFile";
}

QTemporaryFile fallback(QDir::tempPath() + QStringLiteral("/nc-sound-XXXXXX") + dottedSuffix);
fallback.setAutoRemove(false);
if (fallback.open() && fallback.write(bytes) == bytes.size()) {
fallback.close();
const auto fallbackPath = fallback.fileName();
extractedPaths.insert(qrcResourcePath, fallbackPath);
return fallbackPath;
}

qCWarning(lcNotificationSoundPlayer) << "Could not materialise embedded sound" << qrcResourcePath
<< "to a filesystem path";
return {};
}

}

#if !defined(NEXTCLOUD_HAS_NATIVE_SOUND_BACKEND)
namespace {

class NoOpBackend : public NotificationSoundPlayer::Backend
{
public:
void setSource(const QString &) override {}

void play(int) override
{
if (!_warned) {
qCWarning(lcNotificationSoundPlayer) << "No native audio backend compiled in; notification sound is silent";
_warned = true;
}
if (onFinished) {
// Invoke asynchronously so loop bookkeeping does not recurse
// inside the dispatcher's play() call.
auto cb = onFinished;
QMetaObject::invokeMethod(qApp, [cb]() { cb(); }, Qt::QueuedConnection);
}
}

void stop() override {}

[[nodiscard]] bool handlesLoopsNatively() const override { return true; }

private:
bool _warned = false;
};

}

std::unique_ptr<NotificationSoundPlayer::Backend> createNotificationSoundPlayerBackend()
{
return std::make_unique<NoOpBackend>();
}
#endif

NotificationSoundPlayer::NotificationSoundPlayer(QObject *parent)

Check warning on line 140 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "parent" of type "class QObject *" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL73&open=AZ6OC6N0jA-zK_J2XL73&pullRequest=10098
: QObject(parent)
, _backend(createNotificationSoundPlayerBackend())
{
bindBackend();
}

NotificationSoundPlayer::NotificationSoundPlayer(std::unique_ptr<Backend> backend, QObject *parent)

Check warning on line 147 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "parent" of type "class QObject *" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL74&open=AZ6OC6N0jA-zK_J2XL74&pullRequest=10098
: QObject(parent)
, _backend(std::move(backend))
{
bindBackend();
}

NotificationSoundPlayer::~NotificationSoundPlayer()
{
if (_backend) {
_backend->stop();
}
}

void NotificationSoundPlayer::bindBackend()
{
_backend->onFinished = [this]() {
// Backend may invoke this from a worker thread. Marshal to the
// dispatcher's thread via the lambda-on-QObject overload of
// invokeMethod, which silently drops the event if `this` is
// destroyed before delivery.
QMetaObject::invokeMethod(this, [this]() { onBackendPlaybackFinished(); }, Qt::QueuedConnection);
};
}

QString NotificationSoundPlayer::source() const
{
return _source;
}

int NotificationSoundPlayer::loops() const
{
return _loops;
}

bool NotificationSoundPlayer::isPlaying() const
{
return _playing;
}

void NotificationSoundPlayer::setSource(const QString &source)
{
if (_source == source) {
return;
}
_source = source;
_resolvedFilePath.clear();
Q_EMIT sourceChanged();
}

void NotificationSoundPlayer::setLoops(int loops)

Check warning on line 197 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "loops" of type "int" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL75&open=AZ6OC6N0jA-zK_J2XL75&pullRequest=10098
{
if (_loops == loops) {
return;
}
_loops = loops;
Q_EMIT loopsChanged();
}

void NotificationSoundPlayer::play()
{
if (_source.isEmpty() || _loops <= 0) {
return;
}

if (_resolvedFilePath.isEmpty()) {
_resolvedFilePath = resolveToFilesystemPath(_source);
if (_resolvedFilePath.isEmpty()) {
return;
}
_backend->setSource(_resolvedFilePath);
}

if (_playing) {
_backend->stop();
}

_remainingLoops = _loops;
setPlaying(true);
_backend->play(_loops);
}

void NotificationSoundPlayer::stop()
{
if (!_playing) {
return;
}
_remainingLoops = 0;
_backend->stop();
setPlaying(false);
}

void NotificationSoundPlayer::onBackendPlaybackFinished()
{
if (!_playing) {
return;
}

if (!_backend->handlesLoopsNatively() && --_remainingLoops > 0) {

Check failure on line 245 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove any side effects from right hand operands of logical && operator.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL76&open=AZ6OC6N0jA-zK_J2XL76&pullRequest=10098
_backend->play(_remainingLoops);
return;
}

_remainingLoops = 0;
setPlaying(false);
}

void NotificationSoundPlayer::setPlaying(bool playing)

Check warning on line 254 in src/gui/notificationsoundplayer.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unmodified variable "playing" of type "_Bool" should be const-qualified.

See more on https://sonarcloud.io/project/issues?id=nextcloud_desktop&issues=AZ6OC6N0jA-zK_J2XL77&open=AZ6OC6N0jA-zK_J2XL77&pullRequest=10098
{
if (_playing == playing) {
return;
}
_playing = playing;
Q_EMIT playingChanged();
}

QString NotificationSoundPlayer::resolveToFilesystemPath(const QString &source)
{
if (source.isEmpty()) {
return {};
}

QString candidate = source;
if (source.startsWith(QStringLiteral("qrc://"))) {
candidate = QStringLiteral(":") + source.mid(6);
} else if (source.startsWith(QStringLiteral("qrc:"))) {
candidate = QStringLiteral(":") + source.mid(4);
} else if (source.startsWith(QStringLiteral("file://"))) {
candidate = QUrl(source).toLocalFile();
}

if (candidate.startsWith(QStringLiteral(":"))) {
const auto extracted = extractQrcToCache(candidate);
if (extracted.isEmpty()) {
return {};
}
return extracted;
}

if (!QFile::exists(candidate)) {
qCWarning(lcNotificationSoundPlayer) << "Sound source does not exist:" << source;
return {};
}
return candidate;
}

}
Loading
Loading