File indexing completed on 2024-05-05 03:57:00
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 SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com> 0006 0007 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0008 */ 0009 0010 #include "notifybyaudio.h" 0011 #include "debug_p.h" 0012 0013 #include <QFile> 0014 #include <QFileInfo> 0015 #include <QGuiApplication> 0016 #include <QIcon> 0017 #include <QString> 0018 0019 #include "knotification.h" 0020 #include "knotifyconfig.h" 0021 0022 #include <canberra.h> 0023 0024 const QString DEFAULT_SOUND_THEME = QStringLiteral("ocean"); 0025 0026 NotifyByAudio::NotifyByAudio(QObject *parent) 0027 : KNotificationPlugin(parent) 0028 , m_soundTheme(DEFAULT_SOUND_THEME) 0029 , m_enabled(true) 0030 { 0031 qRegisterMetaType<uint32_t>("uint32_t"); 0032 0033 m_settingsWatcher = KConfigWatcher::create(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))); 0034 connect(m_settingsWatcher.get(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { 0035 if (group.name() != QLatin1String("Sounds")) { 0036 return; 0037 } 0038 if (names.contains(QByteArrayLiteral("Theme"))) { 0039 m_soundTheme = group.readEntry("Theme", DEFAULT_SOUND_THEME); 0040 } 0041 if (names.contains(QByteArrayLiteral("Enable"))) { 0042 m_enabled = group.readEntry("Enable", true); 0043 } 0044 }); 0045 0046 const KConfigGroup group = m_settingsWatcher->config()->group(QStringLiteral("Sounds")); 0047 m_soundTheme = group.readEntry("Theme", DEFAULT_SOUND_THEME); 0048 m_enabled = group.readEntry("Enable", true); 0049 } 0050 0051 NotifyByAudio::~NotifyByAudio() 0052 { 0053 if (m_context) { 0054 ca_context_destroy(m_context); 0055 } 0056 m_context = nullptr; 0057 } 0058 0059 ca_context *NotifyByAudio::context() 0060 { 0061 if (m_context) { 0062 return m_context; 0063 } 0064 0065 int ret = ca_context_create(&m_context); 0066 if (ret != CA_SUCCESS) { 0067 qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); 0068 m_context = nullptr; 0069 return nullptr; 0070 } 0071 0072 QString desktopFileName = QGuiApplication::desktopFileName(); 0073 // handle apps which set the desktopFileName property with filename suffix, 0074 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) 0075 if (desktopFileName.endsWith(QLatin1String(".desktop"))) { 0076 desktopFileName.chop(8); 0077 } 0078 ret = ca_context_change_props(m_context, 0079 CA_PROP_APPLICATION_NAME, 0080 qUtf8Printable(qApp->applicationDisplayName()), 0081 CA_PROP_APPLICATION_ID, 0082 qUtf8Printable(desktopFileName), 0083 CA_PROP_APPLICATION_ICON_NAME, 0084 qUtf8Printable(qApp->windowIcon().name()), 0085 nullptr); 0086 if (ret != CA_SUCCESS) { 0087 qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); 0088 } 0089 0090 return m_context; 0091 } 0092 0093 void NotifyByAudio::notify(KNotification *notification, const KNotifyConfig ¬ifyConfig) 0094 { 0095 if (!m_enabled) { 0096 qCDebug(LOG_KNOTIFICATIONS) << "Notification sounds are globally disabled"; 0097 return; 0098 } 0099 0100 const QString soundName = notifyConfig.readEntry(QStringLiteral("Sound")); 0101 if (soundName.isEmpty()) { 0102 qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound name provided in notifyrc file, aborting audio notification"; 0103 0104 finish(notification); 0105 return; 0106 } 0107 0108 // Legacy implementation. Fallback lookup for a full path within the `$XDG_DATA_LOCATION/sounds` dirs 0109 QUrl fallbackUrl; 0110 const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); 0111 for (const QString &dataLocation : dataLocations) { 0112 fallbackUrl = QUrl::fromUserInput(soundName, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile); 0113 if (fallbackUrl.isLocalFile() && QFileInfo::exists(fallbackUrl.toLocalFile())) { 0114 break; 0115 } else if (!fallbackUrl.isLocalFile() && fallbackUrl.isValid()) { 0116 break; 0117 } 0118 fallbackUrl.clear(); 0119 } 0120 0121 // Looping happens in the finishCallback 0122 if (!playSound(m_currentId, soundName, fallbackUrl)) { 0123 finish(notification); 0124 return; 0125 } 0126 0127 if (notification->flags() & KNotification::LoopSound) { 0128 m_loopSoundUrls.insert(m_currentId, {soundName, fallbackUrl}); 0129 } 0130 0131 Q_ASSERT(!m_notifications.value(m_currentId)); 0132 m_notifications.insert(m_currentId, notification); 0133 0134 ++m_currentId; 0135 } 0136 0137 bool NotifyByAudio::playSound(quint32 id, const QString &soundName, const QUrl &fallbackUrl) 0138 { 0139 if (!context()) { 0140 qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context"; 0141 return false; 0142 } 0143 0144 ca_proplist *props = nullptr; 0145 ca_proplist_create(&props); 0146 0147 ca_proplist_sets(props, CA_PROP_EVENT_ID, soundName.toLatin1().constData()); 0148 ca_proplist_sets(props, CA_PROP_CANBERRA_XDG_THEME_NAME, m_soundTheme.toLatin1().constData()); 0149 // Fallback to filename 0150 if (!fallbackUrl.isEmpty()) { 0151 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(fallbackUrl.toLocalFile()).constData()); 0152 } 0153 // We'll also want this cached for a time. volatile makes sure the cache is 0154 // dropped after some time or when the cache is under pressure. 0155 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile"); 0156 0157 int ret = ca_context_play_full(context(), id, props, &ca_finish_callback, this); 0158 0159 ca_proplist_destroy(props); 0160 0161 if (ret != CA_SUCCESS) { 0162 qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret); 0163 return false; 0164 } 0165 0166 return true; 0167 } 0168 0169 void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata) 0170 { 0171 Q_UNUSED(c); 0172 QMetaObject::invokeMethod(static_cast<NotifyByAudio *>(userdata), "finishCallback", Q_ARG(uint32_t, id), Q_ARG(int, error_code)); 0173 } 0174 0175 void NotifyByAudio::finishCallback(uint32_t id, int error_code) 0176 { 0177 KNotification *notification = m_notifications.value(id, nullptr); 0178 if (!notification) { 0179 // We may have gotten a late finish callback. 0180 return; 0181 } 0182 0183 if (error_code == CA_SUCCESS) { 0184 // Loop the sound now if we have one 0185 auto soundInfoIt = m_loopSoundUrls.constFind(id); 0186 if (soundInfoIt != m_loopSoundUrls.constEnd()) { 0187 if (!playSound(id, soundInfoIt->first, soundInfoIt->second)) { 0188 finishNotification(notification, id); 0189 } 0190 return; 0191 } 0192 } else if (error_code != CA_ERROR_CANCELED) { 0193 qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code); 0194 } 0195 0196 finishNotification(notification, id); 0197 } 0198 0199 void NotifyByAudio::close(KNotification *notification) 0200 { 0201 if (!m_notifications.values().contains(notification)) { 0202 return; 0203 } 0204 0205 const auto id = m_notifications.key(notification); 0206 if (m_context) { 0207 int ret = ca_context_cancel(m_context, id); 0208 if (ret != CA_SUCCESS) { 0209 qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret); 0210 return; 0211 } 0212 } 0213 0214 // Consider the notification finished. ca_context_cancel schedules a cancel 0215 // but we need to stop using the noficiation immediately or we could access 0216 // a notification past its lifetime (close() may, or indeed must, 0217 // schedule deletion of the notification). 0218 // https://bugs.kde.org/show_bug.cgi?id=398695 0219 finishNotification(notification, id); 0220 } 0221 0222 void NotifyByAudio::finishNotification(KNotification *notification, quint32 id) 0223 { 0224 m_notifications.remove(id); 0225 m_loopSoundUrls.remove(id); 0226 finish(notification); 0227 } 0228 0229 #include "moc_notifybyaudio.cpp"