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"