Warning: That file was not part of the compilation database. It may have many parsing errors.

1/* This file is part of the KDE libraries
2 Copyright (C) 2014-2015 by Martin Klapetek <mklapetek@kde.org>
3 Copyright (C) 2018 Kai Uwe Broulik <kde@privat.broulik.de>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) version 3, or any
9 later version accepted by the membership of KDE e.V. (or its
10 successor approved by the membership of KDE e.V.), which shall
11 act as a proxy defined in Section 6 of version 3 of the license.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library. If not, see <http://www.gnu.org/licenses/>.
20
21*/
22
23#include "notifybyaudio_canberra.h"
24#include "debug_p.h"
25
26#include <QGuiApplication>
27#include <QFile>
28#include <QFileInfo>
29#include <QIcon>
30#include <QString>
31
32#include "knotifyconfig.h"
33#include "knotification.h"
34
35#include <canberra.h>
36
37NotifyByAudio::NotifyByAudio(QObject *parent)
38 : KNotificationPlugin(parent)
39{
40 qRegisterMetaType<uint32_t>("uint32_t");
41
42 int ret = ca_context_create(&m_context);
43 if (ret != CA_SUCCESS) {
44 qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret);
45 m_context = nullptr;
46 return;
47 }
48
49 QString desktopFileName = QGuiApplication::desktopFileName();
50 // handle apps which set the desktopFileName property with filename suffix,
51 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
52 if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
53 desktopFileName.chop(8);
54 }
55 ret = ca_context_change_props(m_context,
56 CA_PROP_APPLICATION_NAME, qUtf8Printable(qApp->applicationDisplayName()),
57 CA_PROP_APPLICATION_ID, qUtf8Printable(desktopFileName),
58 CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(qApp->windowIcon().name()),
59 nullptr);
60 if (ret != CA_SUCCESS) {
61 qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret);
62 }
63}
64
65NotifyByAudio::~NotifyByAudio()
66{
67 if (m_context) {
68 ca_context_destroy(m_context);
69 }
70 m_context = nullptr;
71}
72
73void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config)
74{
75 const QString soundFilename = config->readEntry(QStringLiteral("Sound"));
76 if (soundFilename.isEmpty()) {
77 qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification";
78
79 finish(notification);
80 return;
81 }
82
83 QUrl soundURL;
84 const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
85 for (const QString &dataLocation : dataLocations) {
86 soundURL = QUrl::fromUserInput(soundFilename,
87 dataLocation + QStringLiteral("/sounds"),
88 QUrl::AssumeLocalFile);
89 if (soundURL.isLocalFile() && QFileInfo::exists(soundURL.toLocalFile())) {
90 break;
91 } else if (!soundURL.isLocalFile() && soundURL.isValid()) {
92 break;
93 }
94 soundURL.clear();
95 }
96 if (soundURL.isEmpty()) {
97 qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification";
98 finish(notification);
99 return;
100 }
101
102 // Looping happens in the finishCallback
103 if (!playSound(m_currentId, soundURL)) {
104 finish(notification);
105 return;
106 }
107
108 if (notification->flags() & KNotification::LoopSound) {
109 m_loopSoundUrls.insert(m_currentId, soundURL);
110 }
111
112 Q_ASSERT(!m_notifications.value(m_currentId));
113 m_notifications.insert(m_currentId, notification);
114
115 ++m_currentId;
116}
117
118bool NotifyByAudio::playSound(quint32 id, const QUrl &url)
119{
120 if (!m_context) {
121 qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context";
122 return false;
123 }
124
125 ca_proplist *props = nullptr;
126 ca_proplist_create(&props);
127
128 // We'll also want this cached for a time. volatile makes sure the cache is
129 // dropped after some time or when the cache is under pressure.
130 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(url.toLocalFile()).constData());
131 ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile");
132
133 int ret = ca_context_play_full(m_context, id, props, &ca_finish_callback, this);
134
135 ca_proplist_destroy(props);
136
137 if (ret != CA_SUCCESS) {
138 qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret);
139 return false;
140 }
141
142 return true;
143}
144
145void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata)
146{
147 Q_UNUSED(c);
148 QMetaObject::invokeMethod(static_cast<NotifyByAudio*>(userdata),
149 "finishCallback",
150 Q_ARG(uint32_t, id),
151 Q_ARG(int, error_code));
152}
153
154void NotifyByAudio::finishCallback(uint32_t id, int error_code)
155{
156 KNotification *notification = m_notifications.value(id, nullptr);
157 if (!notification) {
158 // We may have gotten a late finish callback.
159 return;
160 }
161
162 if (error_code == CA_SUCCESS) {
163 // Loop the sound now if we have one
164 const QUrl soundUrl = m_loopSoundUrls.value(id);
165 if (soundUrl.isValid()) {
166 if (!playSound(id, soundUrl)) {
167 finishNotification(notification, id);
168 }
169 return;
170 }
171 } else if (error_code != CA_ERROR_CANCELED) {
172 qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code);
173 }
174
175 finishNotification(notification, id);
176}
177
178void NotifyByAudio::close(KNotification *notification)
179{
180 if (!m_notifications.values().contains(notification)) {
181 return;
182 }
183
184 const auto id = m_notifications.key(notification);
185 if (m_context) {
186 int ret = ca_context_cancel(m_context, id);
187 if (ret != CA_SUCCESS) {
188 qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret);
189 return;
190 }
191 }
192
193 // Consider the notification finished. ca_context_cancel schedules a cancel
194 // but we need to stop using the noficiation immediately or we could access
195 // a notification past its lifetime (close() may, or indeed must,
196 // schedule deletion of the notification).
197 // https://bugs.kde.org/show_bug.cgi?id=398695
198 finishNotification(notification, id);
199}
200
201void NotifyByAudio::finishNotification(KNotification *notification, quint32 id)
202{
203 m_notifications.remove(id);
204 m_loopSoundUrls.remove(id);
205 finish(notification);
206}
207

Warning: That file was not part of the compilation database. It may have many parsing errors.