1 /*
2  * Notifier.cpp
3  * Copyright (C) 2017  Belledonne Communications, Grenoble, France
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  *
19  *  Created on: February 2, 2017
20  *      Author: Ronan Abhamon
21  */
22 
23 #include <QQmlComponent>
24 #include <QScreen>
25 #include <QTimer>
26 
27 #include "../../app/App.hpp"
28 #include "../../utils/Utils.hpp"
29 #include "../core/CoreManager.hpp"
30 
31 #include "Notifier.hpp"
32 
33 #define NOTIFICATIONS_PATH "qrc:/ui/modules/Linphone/Notifications/"
34 
35 // -----------------------------------------------------------------------------
36 // Notifications QML properties/methods.
37 // -----------------------------------------------------------------------------
38 
39 #define NOTIFICATION_SHOW_METHOD_NAME "open"
40 
41 #define NOTIFICATION_PROPERTY_DATA "notificationData"
42 
43 #define NOTIFICATION_PROPERTY_X "popupX"
44 #define NOTIFICATION_PROPERTY_Y "popupY"
45 
46 #define NOTIFICATION_PROPERTY_WINDOW "__internalWindow"
47 
48 #define NOTIFICATION_PROPERTY_TIMER "__timer"
49 
50 // -----------------------------------------------------------------------------
51 // Arbitrary hardcoded values.
52 // -----------------------------------------------------------------------------
53 
54 #define NOTIFICATION_SPACING 10
55 #define N_MAX_NOTIFICATIONS 5
56 #define MAX_TIMEOUT 30000
57 
58 using namespace std;
59 
60 // =============================================================================
61 
62 template<class T>
setProperty(QObject & object,const char * property,const T & value)63 void setProperty (QObject &object, const char *property, const T &value) {
64   if (!object.setProperty(property, QVariant(value))) {
65     qWarning() << QStringLiteral("Unable to set property: `%1`.").arg(property);
66     abort();
67   }
68 }
69 
70 // =============================================================================
71 // Available notifications.
72 // =============================================================================
73 
74 const QHash<int, Notifier::Notification> Notifier::mNotifications = {
75   { Notifier::ReceivedMessage, { "NotificationReceivedMessage.qml", 10 } },
76   { Notifier::ReceivedFileMessage, { "NotificationReceivedFileMessage.qml", 10 } },
77   { Notifier::ReceivedCall, { "NotificationReceivedCall.qml", 30 } },
78   { Notifier::NewVersionAvailable, { "NotificationNewVersionAvailable.qml", 30 } },
79   { Notifier::SnapshotWasTaken, { "NotificationSnapshotWasTaken.qml", 10 } },
80   { Notifier::RecordingCompleted, { "NotificationRecordingCompleted.qml", 10 } }
81 };
82 
83 // -----------------------------------------------------------------------------
84 
Notifier(QObject * parent)85 Notifier::Notifier (QObject *parent) : QObject(parent) {
86   const int nComponents = mNotifications.size();
87   mComponents = new QQmlComponent *[nComponents];
88 
89   QQmlEngine *engine = App::getInstance()->getEngine();
90   for (const auto &key : mNotifications.keys()) {
91     QQmlComponent *component = new QQmlComponent(engine, QUrl(NOTIFICATIONS_PATH + Notifier::mNotifications[key].filename));
92     if (Q_UNLIKELY(component->isError())) {
93       qWarning() << QStringLiteral("Errors found in `Notification` component %1:").arg(key) << component->errors();
94       abort();
95     }
96     mComponents[key] = component;
97   }
98 
99   mMutex = new QMutex();
100 }
101 
~Notifier()102 Notifier::~Notifier () {
103   delete mMutex;
104 
105   const int nComponents = mNotifications.size();
106   for (int i = 0; i < nComponents; ++i)
107     delete mComponents[i];
108   delete[] mComponents;
109 }
110 
111 // -----------------------------------------------------------------------------
112 
createNotification(Notifier::NotificationType type)113 QObject *Notifier::createNotification (Notifier::NotificationType type) {
114   mMutex->lock();
115 
116   Q_ASSERT(mInstancesNumber <= N_MAX_NOTIFICATIONS);
117 
118   // Check existing instances.
119   if (mInstancesNumber == N_MAX_NOTIFICATIONS) {
120     qWarning() << QStringLiteral("Unable to create another notification.");
121     mMutex->unlock();
122     return nullptr;
123   }
124 
125   // Create instance and set attributes.
126   QObject *instance = mComponents[type]->create();
127   qInfo() << QStringLiteral("Create notification:") << instance;
128 
129   mInstancesNumber++;
130 
131   {
132     QQuickWindow *window = instance->findChild<QQuickWindow *>(NOTIFICATION_PROPERTY_WINDOW);
133     Q_CHECK_PTR(window);
134 
135     QScreen *screen = window->screen();
136     Q_CHECK_PTR(screen);
137 
138     QRect geometry = screen->availableGeometry();
139 
140     // Set X/Y. (Not Pokémon games.)
141     int windowHeight = window->height();
142     int offset = geometry.y() + geometry.height() - windowHeight;
143 
144     ::setProperty(*instance, NOTIFICATION_PROPERTY_X, geometry.x() + geometry.width() - window->width());
145     ::setProperty(*instance, NOTIFICATION_PROPERTY_Y, offset - (mOffset % offset));
146 
147     // Update offset.
148     mOffset = (windowHeight + mOffset) + NOTIFICATION_SPACING;
149     if (mOffset - offset + geometry.y() >= 0)
150       mOffset = 0;
151   }
152 
153   mMutex->unlock();
154 
155   return instance;
156 }
157 
158 // -----------------------------------------------------------------------------
159 
showNotification(QObject * notification,int timeout)160 void Notifier::showNotification (QObject *notification, int timeout) {
161   // Display notification.
162   QMetaObject::invokeMethod(notification, NOTIFICATION_SHOW_METHOD_NAME, Qt::DirectConnection);
163 
164   QTimer *timer = new QTimer(notification);
165   timer->setInterval(timeout > MAX_TIMEOUT ? MAX_TIMEOUT : timeout);
166   timer->setSingleShot(true);
167   notification->setProperty(NOTIFICATION_PROPERTY_TIMER, QVariant::fromValue(timer));
168 
169   // Destroy it after timeout.
170   QObject::connect(timer, &QTimer::timeout, this, [this, notification]() {
171       deleteNotification(QVariant::fromValue(notification));
172     });
173 
174   // Called explicitly (by a click on notification for example)
175   QObject::connect(notification, SIGNAL(deleteNotification(QVariant)), this, SLOT(deleteNotification(QVariant)));
176 
177   timer->start();
178 }
179 
180 // -----------------------------------------------------------------------------
181 
deleteNotification(QVariant notification)182 void Notifier::deleteNotification (QVariant notification) {
183   mMutex->lock();
184 
185   QObject *instance = notification.value<QObject *>();
186 
187   // Notification marked destroyed.
188   if (instance->property("__valid").isValid()) {
189     mMutex->unlock();
190     return;
191   }
192 
193   qInfo() << QStringLiteral("Delete notification:") << instance;
194 
195   instance->setProperty("__valid", true);
196   instance->property(NOTIFICATION_PROPERTY_TIMER).value<QTimer *>()->stop();
197 
198   mInstancesNumber--;
199   Q_ASSERT(mInstancesNumber >= 0);
200 
201   if (mInstancesNumber == 0)
202     mOffset = 0;
203 
204   mMutex->unlock();
205 
206   instance->deleteLater();
207 }
208 
209 // =============================================================================
210 
211 #define CREATE_NOTIFICATION(TYPE) \
212   QObject * notification = createNotification(TYPE); \
213   if (!notification) \
214     return; \
215   const int timeout = mNotifications[TYPE].timeout * 1000;
216 
217 #define SHOW_NOTIFICATION(DATA) \
218   ::setProperty(*notification, NOTIFICATION_PROPERTY_DATA, DATA); \
219   showNotification(notification, timeout);
220 
221 // -----------------------------------------------------------------------------
222 // Notification functions.
223 // -----------------------------------------------------------------------------
224 
notifyReceivedMessage(const shared_ptr<linphone::ChatMessage> & message)225 void Notifier::notifyReceivedMessage (const shared_ptr<linphone::ChatMessage> &message) {
226   CREATE_NOTIFICATION(Notifier::ReceivedMessage);
227 
228   QVariantMap map;
229   map["message"] = message->getFileTransferInformation()
230     ? tr("newFileMessage")
231     : ::Utils::coreStringToAppString(message->getText());
232 
233   map["sipAddress"] = ::Utils::coreStringToAppString(message->getFromAddress()->asStringUriOnly());
234   map["window"].setValue(App::getInstance()->getMainWindow());
235 
236   SHOW_NOTIFICATION(map);
237 }
238 
notifyReceivedFileMessage(const shared_ptr<linphone::ChatMessage> & message)239 void Notifier::notifyReceivedFileMessage (const shared_ptr<linphone::ChatMessage> &message) {
240   CREATE_NOTIFICATION(Notifier::ReceivedFileMessage);
241 
242   QVariantMap map;
243   map["fileUri"] = ::Utils::coreStringToAppString(message->getFileTransferFilepath());
244   map["fileSize"] = static_cast<quint64>(message->getFileTransferInformation()->getSize());
245 
246   SHOW_NOTIFICATION(map);
247 }
248 
notifyReceivedCall(const shared_ptr<linphone::Call> & call)249 void Notifier::notifyReceivedCall (const shared_ptr<linphone::Call> &call) {
250   CREATE_NOTIFICATION(Notifier::ReceivedCall);
251 
252   CallModel *callModel = &call->getData<CallModel>("call-model");
253 
254   QObject::connect(callModel, &CallModel::statusChanged, notification, [this, notification](CallModel::CallStatus status) {
255       if (status == CallModel::CallStatusEnded || status == CallModel::CallStatusConnected)
256         deleteNotification(QVariant::fromValue(notification));
257     });
258 
259   QVariantMap map;
260   map["call"].setValue(callModel);
261 
262   SHOW_NOTIFICATION(map);
263 }
264 
notifyNewVersionAvailable(const QString & version,const QString & url)265 void Notifier::notifyNewVersionAvailable (const QString &version, const QString &url) {
266   CREATE_NOTIFICATION(Notifier::NewVersionAvailable);
267 
268   QVariantMap map;
269   map["message"] = tr("newVersionAvailable").arg(version);
270   map["url"] = url;
271 
272   SHOW_NOTIFICATION(map);
273 }
274 
notifySnapshotWasTaken(const QString & filePath)275 void Notifier::notifySnapshotWasTaken (const QString &filePath) {
276   CREATE_NOTIFICATION(Notifier::SnapshotWasTaken);
277 
278   QVariantMap map;
279   map["filePath"] = filePath;
280 
281   SHOW_NOTIFICATION(map);
282 }
283 
notifyRecordingCompleted(const QString & filePath)284 void Notifier::notifyRecordingCompleted (const QString &filePath) {
285   CREATE_NOTIFICATION(Notifier::RecordingCompleted);
286 
287   QVariantMap map;
288   map["filePath"] = filePath;
289 
290   SHOW_NOTIFICATION(map);
291 }
292 
293 #undef SHOW_NOTIFICATION
294 #undef CREATE_NOTIFICATION
295