1 /*
2     SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
3 
4     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 #include "abstractnotificationsmodel.h"
8 #include "abstractnotificationsmodel_p.h"
9 #include "debug.h"
10 
11 #include "utils_p.h"
12 
13 #include "notification_p.h"
14 
15 #include <QDebug>
16 #include <QProcess>
17 
18 #include <KShell>
19 
20 #include <algorithm>
21 #include <functional>
22 
23 static const int s_notificationsLimit = 1000;
24 
25 using namespace NotificationManager;
26 
Private(AbstractNotificationsModel * q)27 AbstractNotificationsModel::Private::Private(AbstractNotificationsModel *q)
28     : q(q)
29     , lastRead(QDateTime::currentDateTimeUtc())
30 {
31     pendingRemovalTimer.setSingleShot(true);
32     pendingRemovalTimer.setInterval(50);
33     connect(&pendingRemovalTimer, &QTimer::timeout, q, [this, q] {
34         QVector<int> rowsToBeRemoved;
35         rowsToBeRemoved.reserve(pendingRemovals.count());
36         for (uint id : qAsConst(pendingRemovals)) {
37             int row = q->rowOfNotification(id); // oh the complexity...
38             if (row == -1) {
39                 continue;
40             }
41             rowsToBeRemoved.append(row);
42         }
43 
44         removeRows(rowsToBeRemoved);
45     });
46 }
47 
~Private()48 AbstractNotificationsModel::Private::~Private()
49 {
50     qDeleteAll(notificationTimeouts);
51     notificationTimeouts.clear();
52 }
53 
onNotificationAdded(const Notification & notification)54 void AbstractNotificationsModel::Private::onNotificationAdded(const Notification &notification)
55 {
56     // Once we reach a certain insane number of notifications discard some old ones
57     // as we keep pixmaps around etc
58     if (notifications.count() >= s_notificationsLimit) {
59         const int cleanupCount = s_notificationsLimit / 2;
60         qCDebug(NOTIFICATIONMANAGER) << "Reached the notification limit of" << s_notificationsLimit << ", discarding the oldest" << cleanupCount
61                                      << "notifications";
62         q->beginRemoveRows(QModelIndex(), 0, cleanupCount - 1);
63         for (int i = 0; i < cleanupCount; ++i) {
64             notifications.removeAt(0);
65             // TODO close gracefully?
66         }
67         q->endRemoveRows();
68     }
69 
70     setupNotificationTimeout(notification);
71 
72     q->beginInsertRows(QModelIndex(), notifications.count(), notifications.count());
73     notifications.append(std::move(notification));
74     q->endInsertRows();
75 }
76 
onNotificationReplaced(uint replacedId,const Notification & notification)77 void AbstractNotificationsModel::Private::onNotificationReplaced(uint replacedId, const Notification &notification)
78 {
79     const int row = q->rowOfNotification(replacedId);
80 
81     if (row == -1) {
82         qCWarning(NOTIFICATIONMANAGER) << "Trying to replace notification with id" << replacedId
83                                        << "which doesn't exist, creating a new one. This is an application bug!";
84         onNotificationAdded(notification);
85         return;
86     }
87 
88     setupNotificationTimeout(notification);
89 
90     Notification newNotification(notification);
91 
92     const Notification &oldNotification = notifications.at(row);
93     // As per spec a notification must be replaced atomically with no visual cues.
94     // Transfer over properties that might cause this, such as unread showing the bell again,
95     // or created() which should indicate the original date, whereas updated() is when it was last updated
96     newNotification.setCreated(oldNotification.created());
97     newNotification.setExpired(oldNotification.expired());
98     newNotification.setDismissed(oldNotification.dismissed());
99     newNotification.setRead(oldNotification.read());
100 
101     notifications[row] = newNotification;
102     const QModelIndex idx = q->index(row, 0);
103     emit q->dataChanged(idx, idx);
104 }
105 
onNotificationRemoved(uint removedId,Server::CloseReason reason)106 void AbstractNotificationsModel::Private::onNotificationRemoved(uint removedId, Server::CloseReason reason)
107 {
108     const int row = q->rowOfNotification(removedId);
109     if (row == -1) {
110         return;
111     }
112 
113     q->stopTimeout(removedId);
114 
115     // When a notification expired, keep it around in the history and mark it as such
116     if (reason == Server::CloseReason::Expired) {
117         const QModelIndex idx = q->index(row, 0);
118 
119         Notification &notification = notifications[row];
120         notification.setExpired(true);
121 
122         // Since the notification is "closed" it cannot have any actions
123         // unless it is "resident" which we don't support
124         notification.setActions(QStringList());
125 
126         // clang-format off
127         emit q->dataChanged(idx, idx, {
128             Notifications::ExpiredRole,
129             // TODO only emit those if actually changed?
130             Notifications::ActionNamesRole,
131             Notifications::ActionLabelsRole,
132             Notifications::HasDefaultActionRole,
133             Notifications::DefaultActionLabelRole,
134             Notifications::ConfigurableRole
135         });
136         // clang-format on
137 
138         return;
139     }
140 
141     // Otherwise if explicitly closed by either user or app, mark it for removal
142     // some apps are notorious for closing a bunch of notifications at once
143     // causing newer notifications to move up and have a dialogs created for them
144     // just to then be discarded causing excess CPU usage
145     if (!pendingRemovals.contains(removedId)) {
146         pendingRemovals.append(removedId);
147     }
148 
149     if (!pendingRemovalTimer.isActive()) {
150         pendingRemovalTimer.start();
151     }
152 }
153 
setupNotificationTimeout(const Notification & notification)154 void AbstractNotificationsModel::Private::setupNotificationTimeout(const Notification &notification)
155 {
156     if (notification.timeout() == 0) {
157         // In case it got replaced by a persistent notification
158         q->stopTimeout(notification.id());
159         return;
160     }
161 
162     QTimer *timer = notificationTimeouts.value(notification.id());
163     if (!timer) {
164         timer = new QTimer();
165         timer->setSingleShot(true);
166 
167         connect(timer, &QTimer::timeout, q, [this, timer] {
168             const uint id = timer->property("notificationId").toUInt();
169             q->expire(id);
170         });
171         notificationTimeouts.insert(notification.id(), timer);
172     }
173 
174     timer->stop();
175     timer->setProperty("notificationId", notification.id());
176     timer->setInterval(60000 /*1min*/ + (notification.timeout() == -1 ? 120000 /*2min, max configurable default timeout*/ : notification.timeout()));
177     timer->start();
178 }
179 
removeRows(const QVector<int> & rows)180 void AbstractNotificationsModel::Private::removeRows(const QVector<int> &rows)
181 {
182     if (rows.isEmpty()) {
183         return;
184     }
185 
186     QVector<int> rowsToBeRemoved(rows);
187     std::sort(rowsToBeRemoved.begin(), rowsToBeRemoved.end());
188 
189     QVector<QPair<int, int>> clearQueue;
190 
191     QPair<int, int> clearRange{rowsToBeRemoved.first(), rowsToBeRemoved.first()};
192 
193     for (int row : rowsToBeRemoved) {
194         if (row > clearRange.second + 1) {
195             clearQueue.append(clearRange);
196             clearRange.first = row;
197         }
198 
199         clearRange.second = row;
200     }
201 
202     if (clearQueue.isEmpty() || clearQueue.last() != clearRange) {
203         clearQueue.append(clearRange);
204     }
205 
206     int rowsRemoved = 0;
207 
208     for (int i = clearQueue.count() - 1; i >= 0; --i) {
209         const auto &range = clearQueue.at(i);
210 
211         q->beginRemoveRows(QModelIndex(), range.first, range.second);
212         for (int j = range.second; j >= range.first; --j) {
213             notifications.removeAt(j);
214             ++rowsRemoved;
215         }
216         q->endRemoveRows();
217     }
218 
219     Q_ASSERT(rowsRemoved == rowsToBeRemoved.count());
220 
221     pendingRemovals.clear();
222 }
223 
rowOfNotification(uint id) const224 int AbstractNotificationsModel::rowOfNotification(uint id) const
225 {
226     auto it = std::find_if(d->notifications.constBegin(), d->notifications.constEnd(), [id](const Notification &item) {
227         return item.id() == id;
228     });
229 
230     if (it == d->notifications.constEnd()) {
231         return -1;
232     }
233 
234     return std::distance(d->notifications.constBegin(), it);
235 }
236 
AbstractNotificationsModel()237 AbstractNotificationsModel::AbstractNotificationsModel()
238     : QAbstractListModel(nullptr)
239     , d(new Private(this))
240 {
241 }
242 
243 AbstractNotificationsModel::~AbstractNotificationsModel() = default;
244 
lastRead() const245 QDateTime AbstractNotificationsModel::lastRead() const
246 {
247     return d->lastRead;
248 }
249 
setLastRead(const QDateTime & lastRead)250 void AbstractNotificationsModel::setLastRead(const QDateTime &lastRead)
251 {
252     if (d->lastRead != lastRead) {
253         d->lastRead = lastRead;
254         emit lastReadChanged();
255     }
256 }
257 
data(const QModelIndex & index,int role) const258 QVariant AbstractNotificationsModel::data(const QModelIndex &index, int role) const
259 {
260     if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
261         return QVariant();
262     }
263 
264     const Notification &notification = d->notifications.at(index.row());
265 
266     switch (role) {
267     case Notifications::IdRole:
268         return notification.id();
269     case Notifications::TypeRole:
270         return Notifications::NotificationType;
271 
272     case Notifications::CreatedRole:
273         if (notification.created().isValid()) {
274             return notification.created();
275         }
276         break;
277     case Notifications::UpdatedRole:
278         if (notification.updated().isValid()) {
279             return notification.updated();
280         }
281         break;
282     case Notifications::SummaryRole:
283         return notification.summary();
284     case Notifications::BodyRole:
285         return notification.body();
286     case Notifications::IconNameRole:
287         if (notification.image().isNull()) {
288             return notification.icon();
289         }
290         break;
291     case Notifications::ImageRole:
292         if (!notification.image().isNull()) {
293             return notification.image();
294         }
295         break;
296     case Notifications::DesktopEntryRole:
297         return notification.desktopEntry();
298     case Notifications::NotifyRcNameRole:
299         return notification.notifyRcName();
300 
301     case Notifications::ApplicationNameRole:
302         return notification.applicationName();
303     case Notifications::ApplicationIconNameRole:
304         return notification.applicationIconName();
305     case Notifications::OriginNameRole:
306         return notification.originName();
307 
308     case Notifications::ActionNamesRole:
309         return notification.actionNames();
310     case Notifications::ActionLabelsRole:
311         return notification.actionLabels();
312     case Notifications::HasDefaultActionRole:
313         return notification.hasDefaultAction();
314     case Notifications::DefaultActionLabelRole:
315         return notification.defaultActionLabel();
316 
317     case Notifications::UrlsRole:
318         return QVariant::fromValue(notification.urls());
319 
320     case Notifications::UrgencyRole:
321         return static_cast<int>(notification.urgency());
322     case Notifications::UserActionFeedbackRole:
323         return notification.userActionFeedback();
324 
325     case Notifications::TimeoutRole:
326         return notification.timeout();
327 
328     case Notifications::ClosableRole:
329         return true;
330     case Notifications::ConfigurableRole:
331         return notification.configurable();
332     case Notifications::ConfigureActionLabelRole:
333         return notification.configureActionLabel();
334 
335     case Notifications::CategoryRole:
336         return notification.category();
337 
338     case Notifications::ExpiredRole:
339         return notification.expired();
340     case Notifications::ReadRole:
341         return notification.read();
342     case Notifications::ResidentRole:
343         return notification.resident();
344     case Notifications::TransientRole:
345         return notification.transient();
346 
347     case Notifications::HasReplyActionRole:
348         return notification.hasReplyAction();
349     case Notifications::ReplyActionLabelRole:
350         return notification.replyActionLabel();
351     case Notifications::ReplyPlaceholderTextRole:
352         return notification.replyPlaceholderText();
353     case Notifications::ReplySubmitButtonTextRole:
354         return notification.replySubmitButtonText();
355     case Notifications::ReplySubmitButtonIconNameRole:
356         return notification.replySubmitButtonIconName();
357     }
358 
359     return QVariant();
360 }
361 
setData(const QModelIndex & index,const QVariant & value,int role)362 bool AbstractNotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role)
363 {
364     if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
365         return false;
366     }
367 
368     Notification &notification = d->notifications[index.row()];
369     bool dirty = false;
370 
371     switch (role) {
372     case Notifications::ReadRole:
373         if (value.toBool() != notification.read()) {
374             notification.setRead(value.toBool());
375             dirty = true;
376         }
377         break;
378     // Allows to mark a notification as expired without actually sending that out through expire() for persistency
379     case Notifications::ExpiredRole:
380         if (value.toBool() != notification.expired()) {
381             notification.setExpired(value.toBool());
382             dirty = true;
383         }
384         break;
385     }
386 
387     if (dirty) {
388         Q_EMIT dataChanged(index, index, {role});
389     }
390 
391     return dirty;
392 }
393 
rowCount(const QModelIndex & parent) const394 int AbstractNotificationsModel::rowCount(const QModelIndex &parent) const
395 {
396     if (parent.isValid()) {
397         return 0;
398     }
399 
400     return d->notifications.count();
401 }
402 
roleNames() const403 QHash<int, QByteArray> AbstractNotificationsModel::roleNames() const
404 {
405     return Utils::roleNames();
406 }
407 
startTimeout(uint notificationId)408 void AbstractNotificationsModel::startTimeout(uint notificationId)
409 {
410     const int row = rowOfNotification(notificationId);
411     if (row == -1) {
412         return;
413     }
414 
415     const Notification &notification = d->notifications.at(row);
416 
417     if (!notification.timeout() || notification.expired()) {
418         return;
419     }
420 
421     d->setupNotificationTimeout(notification);
422 }
423 
stopTimeout(uint notificationId)424 void AbstractNotificationsModel::stopTimeout(uint notificationId)
425 {
426     delete d->notificationTimeouts.take(notificationId);
427 }
428 
clear(Notifications::ClearFlags flags)429 void AbstractNotificationsModel::clear(Notifications::ClearFlags flags)
430 {
431     if (d->notifications.isEmpty()) {
432         return;
433     }
434 
435     QVector<int> rowsToRemove;
436 
437     for (int i = 0; i < d->notifications.count(); ++i) {
438         const Notification &notification = d->notifications.at(i);
439 
440         if (flags.testFlag(Notifications::ClearExpired) && notification.expired()) {
441             rowsToRemove.append(i);
442         }
443     }
444 
445     d->removeRows(rowsToRemove);
446 }
447 
onNotificationAdded(const Notification & notification)448 void AbstractNotificationsModel::onNotificationAdded(const Notification &notification)
449 {
450     d->onNotificationAdded(notification);
451 }
452 
onNotificationReplaced(uint replacedId,const Notification & notification)453 void AbstractNotificationsModel::onNotificationReplaced(uint replacedId, const Notification &notification)
454 {
455     d->onNotificationReplaced(replacedId, notification);
456 }
457 
onNotificationRemoved(uint notificationId,Server::CloseReason reason)458 void AbstractNotificationsModel::onNotificationRemoved(uint notificationId, Server::CloseReason reason)
459 {
460     d->onNotificationRemoved(notificationId, reason);
461 }
462 
setupNotificationTimeout(const Notification & notification)463 void AbstractNotificationsModel::setupNotificationTimeout(const Notification &notification)
464 {
465     d->setupNotificationTimeout(notification);
466 }
467 
notifications()468 const QVector<Notification> &AbstractNotificationsModel::notifications()
469 {
470     return d->notifications;
471 }
472