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 ¬ification)
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 ¬ification)
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 ¬ification = 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 ¬ification)
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 ¬ification = 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 ¬ification = 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 ¬ification = 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 ¬ification = 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 ¬ification)
449 {
450 d->onNotificationAdded(notification);
451 }
452
onNotificationReplaced(uint replacedId,const Notification & notification)453 void AbstractNotificationsModel::onNotificationReplaced(uint replacedId, const Notification ¬ification)
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 ¬ification)
464 {
465 d->setupNotificationTimeout(notification);
466 }
467
notifications()468 const QVector<Notification> &AbstractNotificationsModel::notifications()
469 {
470 return d->notifications;
471 }
472