File indexing completed on 2024-04-28 15:29:12

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2014-2015 Martin Klapetek <mklapetek@kde.org>
0004     SPDX-FileCopyrightText: 2018 Kai Uwe Broulik <kde@privat.broulik.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007 */
0008 
0009 #include "notifybyaudio_canberra.h"
0010 #include "debug_p.h"
0011 
0012 #include <QFile>
0013 #include <QFileInfo>
0014 #include <QGuiApplication>
0015 #include <QIcon>
0016 #include <QString>
0017 
0018 #include "knotification.h"
0019 #include "knotifyconfig.h"
0020 
0021 #include <canberra.h>
0022 
0023 NotifyByAudio::NotifyByAudio(QObject *parent)
0024     : KNotificationPlugin(parent)
0025 {
0026     qRegisterMetaType<uint32_t>("uint32_t");
0027 
0028     int ret = ca_context_create(&m_context);
0029     if (ret != CA_SUCCESS) {
0030         qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
0031         m_context = nullptr;
0032         return;
0033     }
0034 
0035     QString desktopFileName = QGuiApplication::desktopFileName();
0036     // handle apps which set the desktopFileName property with filename suffix,
0037     // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
0038     if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
0039         desktopFileName.chop(8);
0040     }
0041     ret = ca_context_change_props(m_context,
0042                                   CA_PROP_APPLICATION_NAME,
0043                                   qUtf8Printable(qApp->applicationDisplayName()),
0044                                   CA_PROP_APPLICATION_ID,
0045                                   qUtf8Printable(desktopFileName),
0046                                   CA_PROP_APPLICATION_ICON_NAME,
0047                                   qUtf8Printable(qApp->windowIcon().name()),
0048                                   nullptr);
0049     if (ret != CA_SUCCESS) {
0050         qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
0051     }
0052 }
0053 
0054 NotifyByAudio::~NotifyByAudio()
0055 {
0056     if (m_context) {
0057         ca_context_destroy(m_context);
0058     }
0059     m_context = nullptr;
0060 }
0061 
0062 void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config)
0063 {
0064     const QString soundFilename = config->readEntry(QStringLiteral("Sound"));
0065     if (soundFilename.isEmpty()) {
0066         qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification";
0067 
0068         finish(notification);
0069         return;
0070     }
0071 
0072     QUrl soundURL;
0073     const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0074     for (const QString &dataLocation : dataLocations) {
0075         soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile);
0076         if (soundURL.isLocalFile() && QFileInfo::exists(soundURL.toLocalFile())) {
0077             break;
0078         } else if (!soundURL.isLocalFile() && soundURL.isValid()) {
0079             break;
0080         }
0081         soundURL.clear();
0082     }
0083     if (soundURL.isEmpty()) {
0084         qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification";
0085         finish(notification);
0086         return;
0087     }
0088 
0089     // Looping happens in the finishCallback
0090     if (!playSound(m_currentId, soundURL)) {
0091         finish(notification);
0092         return;
0093     }
0094 
0095     if (notification->flags() & KNotification::LoopSound) {
0096         m_loopSoundUrls.insert(m_currentId, soundURL);
0097     }
0098 
0099     Q_ASSERT(!m_notifications.value(m_currentId));
0100     m_notifications.insert(m_currentId, notification);
0101 
0102     ++m_currentId;
0103 }
0104 
0105 bool NotifyByAudio::playSound(quint32 id, const QUrl &url)
0106 {
0107     if (!m_context) {
0108         qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context";
0109         return false;
0110     }
0111 
0112     ca_proplist *props = nullptr;
0113     ca_proplist_create(&props);
0114 
0115     // We'll also want this cached for a time. volatile makes sure the cache is
0116     // dropped after some time or when the cache is under pressure.
0117     ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(url.toLocalFile()).constData());
0118     ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
0119 
0120     int ret = ca_context_play_full(m_context, id, props, &ca_finish_callback, this);
0121 
0122     ca_proplist_destroy(props);
0123 
0124     if (ret != CA_SUCCESS) {
0125         qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret);
0126         return false;
0127     }
0128 
0129     return true;
0130 }
0131 
0132 void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata)
0133 {
0134     Q_UNUSED(c);
0135     QMetaObject::invokeMethod(static_cast<NotifyByAudio *>(userdata), "finishCallback", Q_ARG(uint32_t, id), Q_ARG(int, error_code));
0136 }
0137 
0138 void NotifyByAudio::finishCallback(uint32_t id, int error_code)
0139 {
0140     KNotification *notification = m_notifications.value(id, nullptr);
0141     if (!notification) {
0142         // We may have gotten a late finish callback.
0143         return;
0144     }
0145 
0146     if (error_code == CA_SUCCESS) {
0147         // Loop the sound now if we have one
0148         const QUrl soundUrl = m_loopSoundUrls.value(id);
0149         if (soundUrl.isValid()) {
0150             if (!playSound(id, soundUrl)) {
0151                 finishNotification(notification, id);
0152             }
0153             return;
0154         }
0155     } else if (error_code != CA_ERROR_CANCELED) {
0156         qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code);
0157     }
0158 
0159     finishNotification(notification, id);
0160 }
0161 
0162 void NotifyByAudio::close(KNotification *notification)
0163 {
0164     if (!m_notifications.values().contains(notification)) {
0165         return;
0166     }
0167 
0168     const auto id = m_notifications.key(notification);
0169     if (m_context) {
0170         int ret = ca_context_cancel(m_context, id);
0171         if (ret != CA_SUCCESS) {
0172             qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret);
0173             return;
0174         }
0175     }
0176 
0177     // Consider the notification finished. ca_context_cancel schedules a cancel
0178     // but we need to stop using the noficiation immediately or we could access
0179     // a notification past its lifetime (close() may, or indeed must,
0180     // schedule deletion of the notification).
0181     // https://bugs.kde.org/show_bug.cgi?id=398695
0182     finishNotification(notification, id);
0183 }
0184 
0185 void NotifyByAudio::finishNotification(KNotification *notification, quint32 id)
0186 {
0187     m_notifications.remove(id);
0188     m_loopSoundUrls.remove(id);
0189     finish(notification);
0190 }
0191 
0192 #include "moc_notifybyaudio_canberra.cpp"