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