File indexing completed on 2024-04-28 16:54:33

0001 /*
0002     SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "abstractnotificationsmodel.h"
0008 #include "abstractnotificationsmodel_p.h"
0009 #include "debug.h"
0010 
0011 #include "utils_p.h"
0012 
0013 #include "notification_p.h"
0014 
0015 #include <QDebug>
0016 #include <QProcess>
0017 #include <QTextDocumentFragment>
0018 
0019 #include <KLocalizedString>
0020 #include <KShell>
0021 
0022 #include <algorithm>
0023 #include <chrono>
0024 #include <functional>
0025 
0026 using namespace std::chrono_literals;
0027 
0028 static const int s_notificationsLimit = 1000;
0029 
0030 using namespace NotificationManager;
0031 
0032 AbstractNotificationsModel::Private::Private(AbstractNotificationsModel *q)
0033     : q(q)
0034     , lastRead(QDateTime::currentDateTimeUtc())
0035 {
0036     pendingRemovalTimer.setSingleShot(true);
0037     pendingRemovalTimer.setInterval(50ms);
0038     connect(&pendingRemovalTimer, &QTimer::timeout, q, [this, q] {
0039         QVector<int> rowsToBeRemoved;
0040         rowsToBeRemoved.reserve(pendingRemovals.count());
0041         for (uint id : qAsConst(pendingRemovals)) {
0042             int row = q->rowOfNotification(id); // oh the complexity...
0043             if (row == -1) {
0044                 continue;
0045             }
0046             rowsToBeRemoved.append(row);
0047         }
0048 
0049         removeRows(rowsToBeRemoved);
0050     });
0051 }
0052 
0053 AbstractNotificationsModel::Private::~Private()
0054 {
0055     qDeleteAll(notificationTimeouts);
0056     notificationTimeouts.clear();
0057 }
0058 
0059 void AbstractNotificationsModel::Private::onNotificationAdded(const Notification &notification)
0060 {
0061     // Once we reach a certain insane number of notifications discard some old ones
0062     // as we keep pixmaps around etc
0063     if (notifications.count() >= s_notificationsLimit) {
0064         const int cleanupCount = s_notificationsLimit / 2;
0065         qCDebug(NOTIFICATIONMANAGER) << "Reached the notification limit of" << s_notificationsLimit << ", discarding the oldest" << cleanupCount
0066                                      << "notifications";
0067         q->beginRemoveRows(QModelIndex(), 0, cleanupCount - 1);
0068         for (int i = 0; i < cleanupCount; ++i) {
0069             notifications.removeAt(0);
0070             // TODO close gracefully?
0071         }
0072         q->endRemoveRows();
0073     }
0074 
0075     setupNotificationTimeout(notification);
0076 
0077     q->beginInsertRows(QModelIndex(), notifications.count(), notifications.count());
0078     notifications.append(std::move(notification));
0079     q->endInsertRows();
0080 }
0081 
0082 void AbstractNotificationsModel::Private::onNotificationReplaced(uint replacedId, const Notification &notification)
0083 {
0084     const int row = q->rowOfNotification(replacedId);
0085 
0086     if (row == -1) {
0087         qCWarning(NOTIFICATIONMANAGER) << "Trying to replace notification with id" << replacedId
0088                                        << "which doesn't exist, creating a new one. This is an application bug!";
0089         onNotificationAdded(notification);
0090         return;
0091     }
0092 
0093     setupNotificationTimeout(notification);
0094 
0095     Notification newNotification(notification);
0096 
0097     const Notification &oldNotification = notifications.at(row);
0098     // As per spec a notification must be replaced atomically with no visual cues.
0099     // Transfer over properties that might cause this, such as unread showing the bell again,
0100     // or created() which should indicate the original date, whereas updated() is when it was last updated
0101     newNotification.setCreated(oldNotification.created());
0102     newNotification.setExpired(oldNotification.expired());
0103     newNotification.setDismissed(oldNotification.dismissed());
0104     newNotification.setRead(oldNotification.read());
0105 
0106     notifications[row] = newNotification;
0107     const QModelIndex idx = q->index(row, 0);
0108     Q_EMIT q->dataChanged(idx, idx);
0109 }
0110 
0111 void AbstractNotificationsModel::Private::onNotificationRemoved(uint removedId, Server::CloseReason reason)
0112 {
0113     const int row = q->rowOfNotification(removedId);
0114     if (row == -1) {
0115         return;
0116     }
0117 
0118     q->stopTimeout(removedId);
0119 
0120     // When a notification expired, keep it around in the history and mark it as such
0121     if (reason == Server::CloseReason::Expired) {
0122         const QModelIndex idx = q->index(row, 0);
0123 
0124         Notification &notification = notifications[row];
0125         notification.setExpired(true);
0126 
0127         // Since the notification is "closed" it cannot have any actions
0128         // unless it is "resident" which we don't support
0129         notification.setActions(QStringList());
0130 
0131         // clang-format off
0132         Q_EMIT q->dataChanged(idx, idx, {
0133             Notifications::ExpiredRole,
0134             // TODO only Q_EMIT those if actually changed?
0135             Notifications::ActionNamesRole,
0136             Notifications::ActionLabelsRole,
0137             Notifications::HasDefaultActionRole,
0138             Notifications::DefaultActionLabelRole,
0139             Notifications::ConfigurableRole
0140         });
0141         // clang-format on
0142 
0143         return;
0144     }
0145 
0146     // Otherwise if explicitly closed by either user or app, mark it for removal
0147     // some apps are notorious for closing a bunch of notifications at once
0148     // causing newer notifications to move up and have a dialogs created for them
0149     // just to then be discarded causing excess CPU usage
0150     if (!pendingRemovals.contains(removedId)) {
0151         pendingRemovals.append(removedId);
0152     }
0153 
0154     if (!pendingRemovalTimer.isActive()) {
0155         pendingRemovalTimer.start();
0156     }
0157 }
0158 
0159 void AbstractNotificationsModel::Private::setupNotificationTimeout(const Notification &notification)
0160 {
0161     if (notification.timeout() == 0) {
0162         // In case it got replaced by a persistent notification
0163         q->stopTimeout(notification.id());
0164         return;
0165     }
0166 
0167     QTimer *timer = notificationTimeouts.value(notification.id());
0168     if (!timer) {
0169         timer = new QTimer();
0170         timer->setSingleShot(true);
0171 
0172         connect(timer, &QTimer::timeout, q, [this, timer] {
0173             const uint id = timer->property("notificationId").toUInt();
0174             q->expire(id);
0175         });
0176         notificationTimeouts.insert(notification.id(), timer);
0177     }
0178 
0179     timer->stop();
0180     timer->setProperty("notificationId", notification.id());
0181     timer->setInterval(60000 /*1min*/ + (notification.timeout() == -1 ? 120000 /*2min, max configurable default timeout*/ : notification.timeout()));
0182     timer->start();
0183 }
0184 
0185 void AbstractNotificationsModel::Private::removeRows(const QVector<int> &rows)
0186 {
0187     if (rows.isEmpty()) {
0188         return;
0189     }
0190 
0191     QVector<int> rowsToBeRemoved(rows);
0192     std::sort(rowsToBeRemoved.begin(), rowsToBeRemoved.end());
0193 
0194     QVector<QPair<int, int>> clearQueue;
0195 
0196     QPair<int, int> clearRange{rowsToBeRemoved.first(), rowsToBeRemoved.first()};
0197 
0198     for (int row : rowsToBeRemoved) {
0199         if (row > clearRange.second + 1) {
0200             clearQueue.append(clearRange);
0201             clearRange.first = row;
0202         }
0203 
0204         clearRange.second = row;
0205     }
0206 
0207     if (clearQueue.isEmpty() || clearQueue.last() != clearRange) {
0208         clearQueue.append(clearRange);
0209     }
0210 
0211     int rowsRemoved = 0;
0212 
0213     for (int i = clearQueue.count() - 1; i >= 0; --i) {
0214         const auto &range = clearQueue.at(i);
0215 
0216         q->beginRemoveRows(QModelIndex(), range.first, range.second);
0217         for (int j = range.second; j >= range.first; --j) {
0218             notifications.removeAt(j);
0219             ++rowsRemoved;
0220         }
0221         q->endRemoveRows();
0222     }
0223 
0224     Q_ASSERT(rowsRemoved == rowsToBeRemoved.count());
0225 
0226     pendingRemovals.clear();
0227 }
0228 
0229 int AbstractNotificationsModel::rowOfNotification(uint id) const
0230 {
0231     auto it = std::find_if(d->notifications.constBegin(), d->notifications.constEnd(), [id](const Notification &item) {
0232         return item.id() == id;
0233     });
0234 
0235     if (it == d->notifications.constEnd()) {
0236         return -1;
0237     }
0238 
0239     return std::distance(d->notifications.constBegin(), it);
0240 }
0241 
0242 AbstractNotificationsModel::AbstractNotificationsModel()
0243     : QAbstractListModel(nullptr)
0244     , d(new Private(this))
0245 {
0246 }
0247 
0248 AbstractNotificationsModel::~AbstractNotificationsModel() = default;
0249 
0250 QDateTime AbstractNotificationsModel::lastRead() const
0251 {
0252     return d->lastRead;
0253 }
0254 
0255 void AbstractNotificationsModel::setLastRead(const QDateTime &lastRead)
0256 {
0257     if (d->lastRead != lastRead) {
0258         d->lastRead = lastRead;
0259         Q_EMIT lastReadChanged();
0260     }
0261 }
0262 
0263 QWindow *AbstractNotificationsModel::window() const
0264 {
0265     return d->window;
0266 }
0267 
0268 void AbstractNotificationsModel::setWindow(QWindow *window)
0269 {
0270     if (d->window == window) {
0271         return;
0272     }
0273     if (d->window) {
0274         disconnect(d->window, &QObject::destroyed, this, nullptr);
0275     }
0276     d->window = window;
0277     if (d->window) {
0278         connect(d->window, &QObject::destroyed, this, [this] {
0279             setWindow(nullptr);
0280         });
0281     }
0282     Q_EMIT windowChanged(window);
0283 }
0284 
0285 QVariant AbstractNotificationsModel::data(const QModelIndex &index, int role) const
0286 {
0287     if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
0288         return QVariant();
0289     }
0290 
0291     const Notification &notification = d->notifications.at(index.row());
0292 
0293     switch (role) {
0294     case Notifications::IdRole:
0295         return notification.id();
0296     case Notifications::TypeRole:
0297         return Notifications::NotificationType;
0298 
0299     case Notifications::CreatedRole:
0300         if (notification.created().isValid()) {
0301             return notification.created();
0302         }
0303         break;
0304     case Notifications::UpdatedRole:
0305         if (notification.updated().isValid()) {
0306             return notification.updated();
0307         }
0308         break;
0309     case Notifications::SummaryRole:
0310         return notification.summary();
0311     case Notifications::BodyRole:
0312         return notification.body();
0313     case Qt::AccessibleDescriptionRole:
0314         return i18nc("@info %1 notification body %2 application name",
0315                      "%1 from %2",
0316                      QTextDocumentFragment::fromHtml(notification.body()).toPlainText(),
0317                      notification.applicationName());
0318     case Notifications::IconNameRole:
0319         if (notification.image().isNull()) {
0320             return notification.icon();
0321         }
0322         break;
0323     case Notifications::ImageRole:
0324         if (!notification.image().isNull()) {
0325             return notification.image();
0326         }
0327         break;
0328     case Notifications::DesktopEntryRole:
0329         return notification.desktopEntry();
0330     case Notifications::NotifyRcNameRole:
0331         return notification.notifyRcName();
0332 
0333     case Notifications::ApplicationNameRole:
0334         return notification.applicationName();
0335     case Notifications::ApplicationIconNameRole:
0336         return notification.applicationIconName();
0337     case Notifications::OriginNameRole:
0338         return notification.originName();
0339 
0340     case Notifications::ActionNamesRole:
0341         return notification.actionNames();
0342     case Notifications::ActionLabelsRole:
0343         return notification.actionLabels();
0344     case Notifications::HasDefaultActionRole:
0345         return notification.hasDefaultAction();
0346     case Notifications::DefaultActionLabelRole:
0347         return notification.defaultActionLabel();
0348 
0349     case Notifications::UrlsRole:
0350         return QVariant::fromValue(notification.urls());
0351 
0352     case Notifications::UrgencyRole:
0353         return static_cast<int>(notification.urgency());
0354     case Notifications::UserActionFeedbackRole:
0355         return notification.userActionFeedback();
0356 
0357     case Notifications::TimeoutRole:
0358         return notification.timeout();
0359 
0360     case Notifications::ClosableRole:
0361         return true;
0362     case Notifications::ConfigurableRole:
0363         return notification.configurable();
0364     case Notifications::ConfigureActionLabelRole:
0365         return notification.configureActionLabel();
0366 
0367     case Notifications::CategoryRole:
0368         return notification.category();
0369 
0370     case Notifications::ExpiredRole:
0371         return notification.expired();
0372     case Notifications::ReadRole:
0373         return notification.read();
0374     case Notifications::ResidentRole:
0375         return notification.resident();
0376     case Notifications::TransientRole:
0377         return notification.transient();
0378 
0379     case Notifications::HasReplyActionRole:
0380         return notification.hasReplyAction();
0381     case Notifications::ReplyActionLabelRole:
0382         return notification.replyActionLabel();
0383     case Notifications::ReplyPlaceholderTextRole:
0384         return notification.replyPlaceholderText();
0385     case Notifications::ReplySubmitButtonTextRole:
0386         return notification.replySubmitButtonText();
0387     case Notifications::ReplySubmitButtonIconNameRole:
0388         return notification.replySubmitButtonIconName();
0389     }
0390 
0391     return QVariant();
0392 }
0393 
0394 bool AbstractNotificationsModel::setData(const QModelIndex &index, const QVariant &value, int role)
0395 {
0396     if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
0397         return false;
0398     }
0399 
0400     Notification &notification = d->notifications[index.row()];
0401     bool dirty = false;
0402 
0403     switch (role) {
0404     case Notifications::ReadRole:
0405         if (value.toBool() != notification.read()) {
0406             notification.setRead(value.toBool());
0407             dirty = true;
0408         }
0409         break;
0410     // Allows to mark a notification as expired without actually sending that out through expire() for persistency
0411     case Notifications::ExpiredRole:
0412         if (value.toBool() != notification.expired()) {
0413             notification.setExpired(value.toBool());
0414             dirty = true;
0415         }
0416         break;
0417     }
0418 
0419     if (dirty) {
0420         Q_EMIT dataChanged(index, index, {role});
0421     }
0422 
0423     return dirty;
0424 }
0425 
0426 int AbstractNotificationsModel::rowCount(const QModelIndex &parent) const
0427 {
0428     if (parent.isValid()) {
0429         return 0;
0430     }
0431 
0432     return d->notifications.count();
0433 }
0434 
0435 QHash<int, QByteArray> AbstractNotificationsModel::roleNames() const
0436 {
0437     return Utils::roleNames();
0438 }
0439 
0440 void AbstractNotificationsModel::startTimeout(uint notificationId)
0441 {
0442     const int row = rowOfNotification(notificationId);
0443     if (row == -1) {
0444         return;
0445     }
0446 
0447     const Notification &notification = d->notifications.at(row);
0448 
0449     if (!notification.timeout() || notification.expired()) {
0450         return;
0451     }
0452 
0453     d->setupNotificationTimeout(notification);
0454 }
0455 
0456 void AbstractNotificationsModel::stopTimeout(uint notificationId)
0457 {
0458     delete d->notificationTimeouts.take(notificationId);
0459 }
0460 
0461 void AbstractNotificationsModel::clear(Notifications::ClearFlags flags)
0462 {
0463     if (d->notifications.isEmpty()) {
0464         return;
0465     }
0466 
0467     QVector<int> rowsToRemove;
0468 
0469     for (int i = 0; i < d->notifications.count(); ++i) {
0470         const Notification &notification = d->notifications.at(i);
0471 
0472         if (flags.testFlag(Notifications::ClearExpired) && notification.expired()) {
0473             close(notification.id());
0474         }
0475     }
0476 }
0477 
0478 void AbstractNotificationsModel::onNotificationAdded(const Notification &notification)
0479 {
0480     d->onNotificationAdded(notification);
0481 }
0482 
0483 void AbstractNotificationsModel::onNotificationReplaced(uint replacedId, const Notification &notification)
0484 {
0485     d->onNotificationReplaced(replacedId, notification);
0486 }
0487 
0488 void AbstractNotificationsModel::onNotificationRemoved(uint notificationId, Server::CloseReason reason)
0489 {
0490     d->onNotificationRemoved(notificationId, reason);
0491 }
0492 
0493 void AbstractNotificationsModel::setupNotificationTimeout(const Notification &notification)
0494 {
0495     d->setupNotificationTimeout(notification);
0496 }
0497 
0498 const QVector<Notification> &AbstractNotificationsModel::notifications()
0499 {
0500     return d->notifications;
0501 }