diff --git a/panels/notification/CMakeLists.txt b/panels/notification/CMakeLists.txt index b7c6414eb..f7a60ff03 100644 --- a/panels/notification/CMakeLists.txt +++ b/panels/notification/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(ds-notification-shared SHARED ${CMAKE_SOURCE_DIR}/panels/notification/common/dbaccessor.cpp ${CMAKE_SOURCE_DIR}/panels/notification/common/notifysetting.h ${CMAKE_SOURCE_DIR}/panels/notification/common/notifysetting.cpp + ${CMAKE_SOURCE_DIR}/panels/notification/common/expiretimer.h + ${CMAKE_SOURCE_DIR}/panels/notification/common/expiretimer.cpp ) set_target_properties(ds-notification-shared PROPERTIES diff --git a/panels/notification/bubble/bubbleitem.cpp b/panels/notification/bubble/bubbleitem.cpp index 60d48a6d7..695a8d23b 100644 --- a/panels/notification/bubble/bubbleitem.cpp +++ b/panels/notification/bubble/bubbleitem.cpp @@ -51,6 +51,11 @@ qint64 BubbleItem::id() const return m_entity.id(); } +const NotifyEntity &BubbleItem::entity() const +{ + return m_entity; +} + uint BubbleItem::bubbleId() const { return m_entity.bubbleId(); diff --git a/panels/notification/bubble/bubbleitem.h b/panels/notification/bubble/bubbleitem.h index 1b28f0aa8..417bd5ba2 100644 --- a/panels/notification/bubble/bubbleitem.h +++ b/panels/notification/bubble/bubbleitem.h @@ -21,6 +21,7 @@ class BubbleItem : public QObject public: void setEntity(const NotifyEntity &entity); + const NotifyEntity &entity() const; public: qint64 id() const; diff --git a/panels/notification/bubble/bubblemodel.cpp b/panels/notification/bubble/bubblemodel.cpp index c9bbee6fb..78e7acc5e 100644 --- a/panels/notification/bubble/bubblemodel.cpp +++ b/panels/notification/bubble/bubblemodel.cpp @@ -7,6 +7,7 @@ #include #include "bubbleitem.h" +#include "expiretimer.h" #include #include @@ -41,6 +42,12 @@ BubbleModel::BubbleModel(QObject *parent) m_processPendingTimer->start(); } }); + connect(ExpireTimer::instance(), &ExpireTimer::expired, this, [this](qint64 id, uint bubbleId) { + // A bubble that has been pushed off the display (overflow) is no longer + // in the model, but its countdown still finishes and the notification + // should still be closed. + Q_EMIT bubbleExpired(id, bubbleId); + }); connect(NotifySetting::instance(), &NotifySetting::contentRowCountChanged, this, &BubbleModel::updateContentRowCount); connect(NotifySetting::instance(), &NotifySetting::bubbleCountChanged, this, &BubbleModel::updateBubbleCount); @@ -82,6 +89,10 @@ void BubbleModel::insertBubble(BubbleItem *bubble) beginInsertRows(QModelIndex(), 0, 0); m_bubbles.prepend(bubble); endInsertRows(); + + // A non-positive interval (Critical urgency or expireTimeout 0) means the + // bubble never expires on its own. + ExpireTimer::instance()->push(bubble->entity()); } bool BubbleModel::isReplaceBubble(const BubbleItem *bubble) const @@ -98,25 +109,12 @@ BubbleItem *BubbleModel::replaceBubble(BubbleItem *bubble) m_bubbles.replace(replaceIndex, bubble); Q_EMIT dataChanged(index(replaceIndex), index(replaceIndex)); - return oldBubble; -} - -void BubbleModel::clear() -{ - if (m_processPendingTimer) { - m_processPendingTimer->stop(); - } - qDeleteAll(m_pendingBubbles); - m_pendingBubbles.clear(); + // The replacement shares the bubble slot of the old bubble; ExpireTimer + // cancels the old countdown (transferring a hover block) and starts a new + // one, so just push it like insertBubble() does. + ExpireTimer::instance()->push(bubble->entity()); - if (m_bubbles.count() <= 0) - return; - beginResetModel(); - qDeleteAll(m_bubbles); - m_bubbles.clear(); - endResetModel(); - - m_updateTimeTipTimer->stop(); + return oldBubble; } QList BubbleModel::items() const @@ -131,9 +129,9 @@ void BubbleModel::remove(int index) beginRemoveRows(QModelIndex(), index, index); auto bubble = m_bubbles.takeAt(index); + ExpireTimer::instance()->remove(bubble->entity()); bubble->deleteLater(); endRemoveRows(); - } void BubbleModel::remove(const BubbleItem *bubble) @@ -298,4 +296,5 @@ void BubbleModel::updateContentRowCount(int rowCount) Q_EMIT dataChanged(index(0), index(m_bubbles.size() - 1), {BubbleModel::ContentRowCount}); } } -} + +} // notification diff --git a/panels/notification/bubble/bubblemodel.h b/panels/notification/bubble/bubblemodel.h index b9b8f6203..78cc5b086 100644 --- a/panels/notification/bubble/bubblemodel.h +++ b/panels/notification/bubble/bubblemodel.h @@ -8,6 +8,7 @@ #include "notifyentity.h" #include +#include #include class QTimer; @@ -38,6 +39,10 @@ class BubbleModel : public QAbstractListModel explicit BubbleModel(QObject *parent = nullptr); ~BubbleModel() override; +Q_SIGNALS: + // Emitted when a bubble reaches its expire timeout and should be closed. + void bubbleExpired(qint64 id, uint bubbleId); + public: void push(BubbleItem *bubble); @@ -49,7 +54,6 @@ class BubbleModel : public QAbstractListModel Q_INVOKABLE void remove(int index); void remove(const BubbleItem *bubble); BubbleItem *removeById(qint64 id); - void clear(); BubbleItem *bubbleItem(int bubbleIndex) const; @@ -68,7 +72,6 @@ class BubbleModel : public QAbstractListModel void updateBubbleTimeTip(); void updateContentRowCount(int rowCount); -private: QTimer *m_updateTimeTipTimer = nullptr; QTimer *m_processPendingTimer = nullptr; QList m_bubbles; diff --git a/panels/notification/bubble/bubblepanel.cpp b/panels/notification/bubble/bubblepanel.cpp index 469fe67f7..68b2768bf 100644 --- a/panels/notification/bubble/bubblepanel.cpp +++ b/panels/notification/bubble/bubblepanel.cpp @@ -6,6 +6,7 @@ #include "bubbleitem.h" #include "bubblemodel.h" #include "dataaccessorproxy.h" +#include "expiretimer.h" #include "pluginfactory.h" #include @@ -53,6 +54,14 @@ bool BubblePanel::init() connect(m_bubbles, &BubbleModel::rowsInserted, this, &BubblePanel::onBubbleCountChanged); connect(m_bubbles, &BubbleModel::rowsRemoved, this, &BubblePanel::onBubbleCountChanged); + // ExpireTimer tracks the countdown of every shown bubble. When a bubble + // times out, tell the server so it moves the notification from the + // in-memory store to the center database; the server then emits + // NotificationStateChanged which closes the bubble through the normal flow. + connect(m_bubbles, &BubbleModel::bubbleExpired, this, [this](qint64 id, uint bubbleId) { + QMetaObject::invokeMethod(m_notificationServer, "notificationClosed", Qt::AutoConnection, + Q_ARG(qint64, id), Q_ARG(uint, bubbleId), Q_ARG(uint, NotifyEntity::Expired)); + }); return true; } @@ -217,7 +226,7 @@ void BubblePanel::setEnabled(bool newEnabled) void BubblePanel::setHoveredId(qint64 id) { - QMetaObject::invokeMethod(m_notificationServer, "setBlockClosedId", Qt::DirectConnection, Q_ARG(qint64, id)); + ExpireTimer::instance()->setBlockId(id); } } diff --git a/panels/notification/center/notifystagingmodel.cpp b/panels/notification/center/notifystagingmodel.cpp index 303bfc75c..98f56d5c0 100644 --- a/panels/notification/center/notifystagingmodel.cpp +++ b/panels/notification/center/notifystagingmodel.cpp @@ -8,6 +8,7 @@ #include #include "dataaccessorproxy.h" +#include "expiretimer.h" #include "notifyaccessor.h" #include "notifyentity.h" #include "notifyitem.h" @@ -25,6 +26,11 @@ NotifyStagingModel::NotifyStagingModel(QObject *parent) connect(NotifyAccessor::instance(), &NotifyAccessor::stagingEntityReceived, this, &NotifyStagingModel::doEntityReceived); connect(NotifyAccessor::instance(), &NotifyAccessor::stagingEntityClosed, this, &NotifyStagingModel::onEntityClosed); connect(NotifySetting::instance(), &NotifySetting::contentRowCountChanged, this, &NotifyStagingModel::updateContentRowCount); + + connect(ExpireTimer::instance(), &ExpireTimer::expired, this, [this](qint64 id, uint bubbleId) { + Q_UNUSED(bubbleId) + closeNotify(id, NotifyEntity::Expired); + }); } void NotifyStagingModel::close() @@ -63,6 +69,10 @@ void NotifyStagingModel::push(const NotifyEntity &entity) updateOverlapCount(count); } + // A non-positive interval (Critical urgency or expireTimeout 0) means the + // notification never expires on its own. + ExpireTimer::instance()->push(entity); + if (m_refreshTimer < 0) { m_refreshTimer = startTimer(std::chrono::milliseconds(1000)); } @@ -90,6 +100,10 @@ void NotifyStagingModel::remove(qint64 id) { qDebug(notifyLog) << "Remove notify by id" << id; + const auto entity = notifyById(id); + if (entity.isValid()) + ExpireTimer::instance()->remove(entity); + int row = -1; for (int i = 0; i < m_appNotifies.size(); i++) { auto item = m_appNotifies[i]; @@ -146,6 +160,7 @@ void NotifyStagingModel::remove(qint64 id) auto notify = new AppNotifyItem(newEntity); m_appNotifies.insert(insertedIndex, notify); endInsertRows(); + ExpireTimer::instance()->push(newEntity); } } updateOverlapCount(entities.size()); @@ -172,6 +187,9 @@ void NotifyStagingModel::open() auto notify = new AppNotifyItem(entities.at(i)); m_appNotifies << notify; } + for (const auto &entity : entities) { + ExpireTimer::instance()->push(entity); + } updateOverlapCount(entities.size()); endResetModel(); @@ -250,8 +268,11 @@ void NotifyStagingModel::replace(const NotifyEntity &entity) { for (int i = 0; i < m_appNotifies.size(); i++) { auto item = m_appNotifies[i]; - if (item->id() == entity.bubbleId()) { + if (item->id() == entity.id()) { + // push() handles the replacement internally: it cancels the old + // countdown of the same bubble slot and starts the new one. item->setEntity(entity); + ExpireTimer::instance()->push(entity); const auto index = this->index(i, 0, {}); dataChanged(index, index); break; diff --git a/panels/notification/common/expiretimer.cpp b/panels/notification/common/expiretimer.cpp new file mode 100644 index 000000000..543cfa818 --- /dev/null +++ b/panels/notification/common/expiretimer.cpp @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "expiretimer.h" + +#include +#include + +#include +#include + +namespace notification { + +static const int DefaultTimeoutMSecs = 5000; + +// Hover grace period: when the hover moves away from a bubble, its countdown +// resumes with at least this much time left so the bubble lingers briefly. +static const int BlockItemTimeout = 1000; + +// Upper bound on how many suspended deadlines remove() may remember. Ids that are +// removed and never pushed again (genuinely removed notifications) are +// evicted oldest-first once the cap is reached, so they cannot leak unboundedly. +static const int MaxRememberedIds = 64; + +// Effective expire timeout in milliseconds for a notification. +// Returns 0 for "never expire" (Critical urgency or expireTimeout == 0) and +// falls back to the server default of 5000 ms for expireTimeout == -1. +static int effectiveTimeout(const NotifyEntity &entity) +{ + if (entity.urgency() == NotifyEntity::Critical || entity.timeout() == 0) + return 0; + + return entity.timeout() == -1 ? DefaultTimeoutMSecs : entity.timeout(); +} + +ExpireTimer::ExpireTimer(QObject *parent) + : QObject(parent) + , m_timer(new QTimer(this)) +{ + m_timer->setSingleShot(true); + connect(m_timer, &QTimer::timeout, this, &ExpireTimer::onTimeout); +} + +ExpireTimer *ExpireTimer::instance() +{ + static ExpireTimer expireTimer; + return &expireTimer; +} + +void ExpireTimer::push(const NotifyEntity &entity) +{ + const auto id = entity.id(); + const int interval = effectiveTimeout(entity); + + // A replacement notification occupies the same bubble slot as the replaced + // one, so forget the old countdown (active or suspended) before starting + // the new one. If the replaced bubble is hovered, keep the block on the new + // id so hovering still keeps the replacement on screen. + if (entity.isReplace()) + cancelReplacement(entity); + + if (interval <= 0) { + // Never expire: cancel any pending, paused or suspended countdown. + if (m_pausedId == id) { + m_pausedId = NotifyEntity::InvalidId; + m_pausedRemaining = 0; + m_pausedEntity = {}; + } + m_deadlines.remove(id); + m_retired.remove(id); + schedule(); + return; + } + + // Keep the paused countdown while the id is hovered. + if (m_pausedId == id) + return; + + // A notification displayed in both the bubble and the staging area shares + // one deadline: starting an already tracked id keeps the original countdown + // instead of restarting it. + if (m_deadlines.contains(id)) + return; + + const auto now = QDateTime::currentMSecsSinceEpoch(); + + // Resume a countdown suspended by remove() instead of restarting it, so a + // notification that moved between the bubble and the staging area keeps + // its original expire deadline (or expires immediately if it already + // passed while suspended). + if (const auto it = m_retired.constFind(id); it != m_retired.cend()) { + m_deadlines.insert(id, {it->entity, qMax(now, it->point)}); + m_retired.erase(it); + schedule(); + return; + } + + m_deadlines.insert(id, {entity, now + interval}); + schedule(); +} + +void ExpireTimer::cancelReplacement(const NotifyEntity &entity) +{ + const auto id = entity.id(); + const auto bubbleId = entity.bubbleId(); + + const auto forgetMatching = [this, id, bubbleId, &entity](QHash &table) { + const QList matching = [&table, id, bubbleId] { + QList ids; + for (auto it = table.cbegin(); it != table.cend(); ++it) { + if (it.key() != id && it.value().entity.bubbleId() == bubbleId) + ids.append(it.key()); + } + return ids; + }(); + for (const auto &oldId : matching) { + table.remove(oldId); + // The replaced bubble is gone but the hover remains on the same + // slot, so freeze the replacement instead of the old id. + if (m_pausedId == oldId) { + m_pausedId = id; + m_pausedEntity = entity; + } + } + }; + + forgetMatching(m_deadlines); + forgetMatching(m_retired); + + // A hovered (paused) replaced bubble is in none of the tables; transfer the + // block to the replacement so hovering still keeps it on screen. + if (m_pausedId != NotifyEntity::InvalidId && m_pausedId != id && m_pausedEntity.bubbleId() == bubbleId) { + m_pausedId = id; + m_pausedEntity = entity; + } + schedule(); +} + +void ExpireTimer::remove(const NotifyEntity &entity) +{ + const auto id = entity.id(); + + // A hovered countdown that is removed keeps its remaining time too. + if (m_pausedId == id) { + m_retired.insert(id, {m_pausedEntity, QDateTime::currentMSecsSinceEpoch() + m_pausedRemaining}); + m_pausedId = NotifyEntity::InvalidId; + m_pausedRemaining = 0; + m_pausedEntity = {}; + } + + if (const auto it = m_deadlines.constFind(id); it != m_deadlines.cend()) { + // Suspend rather than forget so a later push() resumes the same + // countdown instead of restarting it. + m_retired.insert(id, it.value()); + m_deadlines.erase(it); + + // Bound the memory of ids that are removed and never pushed again. + if (m_retired.size() > MaxRememberedIds) { + const auto minIt = std::min_element(m_retired.constBegin(), m_retired.constEnd(), + [](const Deadline &lhs, const Deadline &rhs) { return lhs.point < rhs.point; }); + m_retired.erase(minIt); + } + schedule(); + } +} + +void ExpireTimer::setBlockId(qint64 id) +{ + if (id == m_pausedId) + return; + + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + + // The hover moved away from the previously blocked id: resume its + // countdown with at least BlockItemTimeout ms left so the bubble lingers + // briefly after the hover ends. + if (m_pausedId != NotifyEntity::InvalidId) { + m_deadlines.insert(m_pausedId, {m_pausedEntity, now + qMax(m_pausedRemaining, BlockItemTimeout)}); + schedule(); + } + + m_pausedId = id; + + // Block the newly hovered id by freezing its remaining time. If it has no + // running countdown (never-expire or not yet started) there is nothing to + // pause; clear the block so unhover does not later resume a ghost deadline. + const auto it = m_deadlines.constFind(id); + if (it == m_deadlines.cend()) { + m_pausedId = NotifyEntity::InvalidId; + m_pausedRemaining = 0; + m_pausedEntity = {}; + return; + } + + const qint64 remaining = qMax(0, it.value().point - now); + m_pausedRemaining = static_cast(qMin(remaining, std::numeric_limits::max())); + m_pausedEntity = it->entity; + m_deadlines.erase(it); + schedule(); +} + +void ExpireTimer::schedule() +{ + if (m_deadlines.isEmpty()) { + m_timer->stop(); + return; + } + + auto it = std::min_element(m_deadlines.cbegin(), m_deadlines.cend(), + [](const Deadline &lhs, const Deadline &rhs) { return lhs.point < rhs.point; }); + const qint64 remaining = qMax(0, it.value().point - QDateTime::currentMSecsSinceEpoch()); + m_timer->start(static_cast(remaining)); +} + +void ExpireTimer::onTimeout() +{ + const auto now = QDateTime::currentMSecsSinceEpoch(); + const QList expiredIds = [this, now] { + QList ids; + for (auto it = m_deadlines.cbegin(); it != m_deadlines.cend(); ++it) { + if (it.value().point <= now) + ids.append(it.key()); + } + return ids; + }(); + + for (const auto &id : expiredIds) { + const auto it = m_deadlines.find(id); + if (it == m_deadlines.cend()) + continue; + + // The id expired: its countdown is over for good, so forget it in every + // table (active, suspended and hovered). + const auto bubbleId = it->entity.bubbleId(); + m_deadlines.erase(it); + m_retired.remove(id); + if (m_pausedId == id) { + m_pausedId = NotifyEntity::InvalidId; + m_pausedRemaining = 0; + m_pausedEntity = {}; + } + Q_EMIT expired(id, bubbleId); + } + + schedule(); +} + +} diff --git a/panels/notification/common/expiretimer.h b/panels/notification/common/expiretimer.h new file mode 100644 index 000000000..5194a1e79 --- /dev/null +++ b/panels/notification/common/expiretimer.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "notifyentity.h" + +class QTimer; + +namespace notification { + +/** + * @brief Process-wide singleton that tracks the expire deadline of every + * displayed notification (bubble or staging area) with one shared + * single-shot QTimer. + * + * Each notification id keeps its own deadline and the nearest one drives the + * shared QTimer; when a deadline passes, expired() is emitted once with the + * notification id and its bubble id, and the id is forgotten. Pushing an + * already tracked id keeps the original deadline, and remove() suspends a + * countdown by remembering its absolute deadline, so a notification shown in + * both the bubble and the staging area (or moving between them) never restarts + * its countdown. The effective timeout is derived internally from the entity, + * so callers only describe what to do (push, remove, setBlockId) and never + * have to compute or query timeouts. No QTimer is allocated per id and nothing + * leaks when an id expires or is removed. Only one id can be blocked at a time + * (the hovered bubble); the hover handling is encapsulated in setBlockId so + * callers do not deal with pause/resume details. + */ +class ExpireTimer : public QObject +{ + Q_OBJECT +public: + static ExpireTimer *instance(); + + // Pushes the entity into the expire tracking so its id gets a countdown + // based on its urgency and expire timeout. A non-positive timeout (Critical + // urgency or expireTimeout 0) cancels any pending or paused countdown, i.e. + // the notification never expires. If the id was previously removed, the + // original countdown is resumed instead of being restarted. A replacement + // notification (isReplace()) occupies the same bubble slot as the replaced + // one, so its countdown is cancelled here before the new one is started; + // a hovered replaced bubble keeps its block on the new id. + void push(const NotifyEntity &entity); + // Removes the entity's id from tracking. The remaining countdown is + // suspended (not forgotten), so a later push() with the same id resumes it. + void remove(const NotifyEntity &entity); + // Blocks the hovered id from expiring, keeping its remaining time. Only one + // id is blocked at a time: switching to another id resumes the previous one + // with at least a short grace period left (keeping the bubble visible for a + // brief moment after the hover moves away). Passing InvalidId clears the + // block. + void setBlockId(qint64 id); + +Q_SIGNALS: + // Emitted once when the deadline of id passes. + void expired(qint64 id, uint bubbleId); + +private: + // A tracked countdown: the notification being timed and its absolute + // expire deadline. The bubble id used on expiry is read from the entity. + struct Deadline + { + NotifyEntity entity; + // Absolute expiry deadline in ms since the epoch. + qint64 point = 0; + }; + + explicit ExpireTimer(QObject *parent = nullptr); + + void schedule(); + void onTimeout(); + void cancelReplacement(const NotifyEntity &entity); + + QTimer *m_timer = nullptr; + // Active countdowns keyed by notification id. + QHash m_deadlines; + // Countdowns suspended by remove(); restored by push() so a context switch + // (bubble <-> staging) resumes the countdown. + QHash m_retired; + // The single hovered id whose countdown is frozen, and the state needed to + // restore it when the hover moves away. + qint64 m_pausedId = NotifyEntity::InvalidId; + int m_pausedRemaining = 0; + NotifyEntity m_pausedEntity; +}; + +} diff --git a/panels/notification/common/notifyentity.cpp b/panels/notification/common/notifyentity.cpp index 7c55cdc82..a7c832096 100644 --- a/panels/notification/common/notifyentity.cpp +++ b/panels/notification/common/notifyentity.cpp @@ -236,6 +236,16 @@ bool NotifyEntity::isReplace() const return d->replacesId != NoReplaceId; } +int NotifyEntity::timeout() const +{ + return d->expireTimeout; +} + +int NotifyEntity::urgency() const +{ + return d->hints.value("urgency").toInt(); +} + qint64 NotifyEntity::cTime() const { return d->cTime; diff --git a/panels/notification/common/notifyentity.h b/panels/notification/common/notifyentity.h index 967280ddb..f3f99200f 100644 --- a/panels/notification/common/notifyentity.h +++ b/panels/notification/common/notifyentity.h @@ -81,6 +81,10 @@ class NotifyEntity void setReplacesId(uint replacesId); bool isReplace() const; + // Expire timeout in milliseconds passed in by the client (-1 means server default). + int timeout() const; + int urgency() const; + qint64 cTime() const; void setCTime(qint64 cTime); diff --git a/panels/notification/server/notificationmanager.cpp b/panels/notification/server/notificationmanager.cpp index 74c564197..e8fc0d09a 100644 --- a/panels/notification/server/notificationmanager.cpp +++ b/panels/notification/server/notificationmanager.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -37,8 +36,6 @@ Q_DECLARE_LOGGING_CATEGORY(notifyLog) namespace notification { static const uint NoReplacesId = 0; -static const int DefaultTimeOutMSecs = 5000; -static const int BlockItemTimeout = 1000; static const QString NotificationsDBusService = "org.freedesktop.Notifications"; static const QString NotificationsDBusPath = "/org/freedesktop/Notifications"; static const QString DDENotifyDBusServer = "org.deepin.dde.Notification1"; @@ -50,11 +47,7 @@ NotificationManager::NotificationManager(QObject *parent) : QObject(parent) , m_persistence(DataAccessorProxy::instance()) , m_setting(new NotificationSetting(this)) - , m_pendingTimeout(new QTimer(this)) { - m_pendingTimeout->setSingleShot(true); - connect(m_pendingTimeout, &QTimer::timeout, this, &NotificationManager::onHandingPendingEntities); - DataAccessorProxy::instance()->setSource(DBAccessor::instance()); DAppletBridge bridge("org.deepin.ds.dde-apps"); @@ -164,6 +157,16 @@ void NotificationManager::actionInvoked(qint64 id, uint bubbleId, const QString void NotificationManager::notificationClosed(qint64 id, uint bubbleId, uint reason) { qDebug(notifyLog) << "Close notification id" << id << ", reason" << reason; + + const auto entity = m_persistence->fetchEntity(id); + // A notification can be tracked by more than one expire timer (the bubble + // frontend and the notification center staging model both schedule a timeout + // for the same id), so it may already be closed or removed by the time this + // is reached. Report the close only once to avoid emitting NotificationClosed + // twice for a single notification. + if (!entity.isValid()) + return; + updateEntityProcessed(id, reason); Q_EMIT NotificationClosed(bubbleId, reason); @@ -296,22 +299,9 @@ uint NotificationManager::Notify(const QString &appName, uint replacesId, const return 0; } - if (entity.isReplace() && m_persistence->fetchLastEntity(entity.bubbleId()).isValid()) { - removePendingEntity(entity); - } - emitRecordCountChanged(); Q_EMIT NotificationStateChanged(entity.id(), entity.processedType()); - - bool critical = false; - if (auto iter = hints.find("urgency"); iter != hints.end()) { - critical = iter.value().toUInt() == NotifyEntity::Critical; - } - // 0: never expire. -1: DefaultTimeOutMSecs - if (expireTimeout != 0 && !critical) { - pushPendingEntity(entity, expireTimeout); - } } tryPlayNotificationSound(entity, appId, dndMode); @@ -372,29 +362,6 @@ QVariant NotificationManager::GetSystemInfo(uint configItem) return m_setting->systemValue(static_cast(configItem)); } -void NotificationManager::setBlockClosedId(qint64 id) -{ - if (id == m_blockClosedId) { - return; - } - - if(m_blockClosedId != NotifyEntity::InvalidId) { - auto findIter = std::find_if(m_pendingTimeoutEntities.begin(), m_pendingTimeoutEntities.end(), [this](const NotifyEntity &entity) { - return entity.id() == m_blockClosedId; - }); - - const auto current = QDateTime::currentMSecsSinceEpoch(); - if (findIter != m_pendingTimeoutEntities.end()) { - if (current > findIter.key() - BlockItemTimeout) { - qDebug(notifyLog) << "Delay close bubble id:" << m_blockClosedId << "for the new block bubble id:" << id; - m_pendingTimeoutEntities.insert(current + BlockItemTimeout, findIter.value()); - m_pendingTimeoutEntities.erase(findIter); - } - } - } - m_blockClosedId = id; - onHandingPendingEntities(); -} bool NotificationManager::isDoNotDisturb() const { @@ -498,21 +465,6 @@ void NotificationManager::emitRecordCountChanged() emit RecordCountChanged(count); } -void NotificationManager::pushPendingEntity(const NotifyEntity &entity, int expireTimeout) -{ - const int interval = expireTimeout == -1 ? DefaultTimeOutMSecs : expireTimeout; - - qint64 point = QDateTime::currentMSecsSinceEpoch() + interval; - m_pendingTimeoutEntities.insert(point, entity); - - if (m_lastTimeoutPoint > point) { - m_lastTimeoutPoint = point; - auto newInterval = m_lastTimeoutPoint - QDateTime::currentMSecsSinceEpoch(); - m_pendingTimeout->setInterval(newInterval); - m_pendingTimeout->start(); - } -} - void NotificationManager::updateEntityProcessed(qint64 id, uint reason) { auto entity = m_persistence->fetchEntity(id); @@ -546,7 +498,6 @@ void NotificationManager::updateEntityProcessed(const NotifyEntity &entity) Q_EMIT NotificationStateChanged(entity.id(), entity.processedType()); - removePendingEntity(entity); emitRecordCountChanged(); } @@ -707,69 +658,6 @@ void NotificationManager::initScreenLockedState() "Visible", this, SLOT(onScreenLockedChanged(bool))); } -void NotificationManager::onHandingPendingEntities() -{ - QList timeoutEntities; - - const auto current = QDateTime::currentMSecsSinceEpoch(); - for (auto iter = m_pendingTimeoutEntities.begin(); iter != m_pendingTimeoutEntities.end();) { - const auto point = iter.key(); - if (point > current) { - iter++; - continue; - } - - const auto entity = iter.value();; - timeoutEntities << entity; - iter = m_pendingTimeoutEntities.erase(iter); - } - - // update pendingTimeout to deal with m_pendingTimeoutEntities - if (!m_pendingTimeoutEntities.isEmpty()) { - auto points = m_pendingTimeoutEntities.keys(); - std::sort(points.begin(), points.end()); - // find last point to restart pendingTimeout - m_lastTimeoutPoint = points.first(); - auto newInterval = m_lastTimeoutPoint - current; - // let timer start in main thread - QMetaObject::invokeMethod(m_pendingTimeout, "start", Qt::QueuedConnection, Q_ARG(int, newInterval)); - } else { - // reset m_lastTimeoutPoint - m_lastTimeoutPoint = std::numeric_limits::max(); - } - - for (const auto &item : timeoutEntities) { - // Validate entity before processing timeout to prevent race conditions - if (!item.isValid()) { - qWarning(notifyLog) << "Skipping timeout processing for invalid entity id:" << item.id() << "appName:" << item.appName() - << "cTime:" << item.cTime(); - continue; - } - - if (item.id() == m_blockClosedId) { - qDebug(notifyLog) << "bubble id:" << item.bubbleId() << "entity id:" << item.id(); - m_pendingTimeoutEntities.insert(current, item); - continue; - } - - qDebug(notifyLog) << "Expired for the notification " << item.id() << item.appName(); - notificationClosed(item.id(), item.bubbleId(), NotifyEntity::Expired); - } -} - -void NotificationManager::removePendingEntity(const NotifyEntity &entity) -{ - for (auto iter = m_pendingTimeoutEntities.begin(); iter != m_pendingTimeoutEntities.end();) { - const auto item = iter.value(); - if (item == entity || (entity.isReplace() && item.bubbleId() == entity.bubbleId())) { - m_pendingTimeoutEntities.erase(iter); - onHandingPendingEntities(); - break; - } - ++iter; - } -} - void NotificationManager::onScreenLockedChanged(bool screenLocked) { m_screenLocked = screenLocked; diff --git a/panels/notification/server/notificationmanager.h b/panels/notification/server/notificationmanager.h index f2756669d..1d95b4530 100644 --- a/panels/notification/server/notificationmanager.h +++ b/panels/notification/server/notificationmanager.h @@ -7,7 +7,6 @@ #include #include -class QTimer; namespace notification { class NotifyEntity; @@ -68,14 +67,12 @@ public Q_SLOTS: void SetSystemInfo(uint configItem, const QVariant &value); QVariant GetSystemInfo(uint configItem); - void setBlockClosedId(qint64 id); private: bool isDoNotDisturb() const; bool recordNotification(NotifyEntity &entity); void tryPlayNotificationSound(const NotifyEntity &entity, const QString &appId, bool dndMode) const; void emitRecordCountChanged(); - void pushPendingEntity(const NotifyEntity &entity, int expireTimeout); void updateEntityProcessed(qint64 id, uint reason); void updateEntityProcessed(const NotifyEntity &entity); @@ -86,8 +83,6 @@ public Q_SLOTS: void initScreenLockedState(); private slots: - void onHandingPendingEntities(); - void removePendingEntity(const NotifyEntity &entity); void onScreenLockedChanged(bool); private: @@ -96,13 +91,9 @@ private slots: DataAccessor *m_persistence = nullptr; NotificationSetting *m_setting = nullptr; - QTimer *m_pendingTimeout = nullptr; - qint64 m_lastTimeoutPoint = std::numeric_limits::max(); - QMultiHash m_pendingTimeoutEntities; QStringList m_systemApps; QMap m_appNamesMap; int m_cleanupDays = 7; - qint64 m_blockClosedId = 0; }; } // notification diff --git a/panels/notification/server/notifyserverapplet.cpp b/panels/notification/server/notifyserverapplet.cpp index b4de43dfc..54bdca64f 100644 --- a/panels/notification/server/notifyserverapplet.cpp +++ b/panels/notification/server/notifyserverapplet.cpp @@ -76,7 +76,8 @@ void NotifyServerApplet::actionInvoked(qint64 id, const QString &actionKey) void NotifyServerApplet::notificationClosed(qint64 id, uint bubbleId, uint reason) { - QMetaObject::invokeMethod(m_manager, "notificationClosed", Qt::DirectConnection, Q_ARG(qint64, id), Q_ARG(uint, bubbleId), Q_ARG(uint, reason)); + // The manager lives on the worker thread, so deliver the close to it there. + QMetaObject::invokeMethod(m_manager, "notificationClosed", Qt::QueuedConnection, Q_ARG(qint64, id), Q_ARG(uint, bubbleId), Q_ARG(uint, reason)); } QVariant NotifyServerApplet::appValue(const QString &appId, int configItem) @@ -104,11 +105,6 @@ void NotifyServerApplet::removeExpiredNotifications() m_manager->removeExpiredNotifications(); } -void NotifyServerApplet::setBlockClosedId(qint64 id) -{ - m_manager->setBlockClosedId(id); -} - D_APPLET_CLASS(NotifyServerApplet) } diff --git a/panels/notification/server/notifyserverapplet.h b/panels/notification/server/notifyserverapplet.h index 20975e91d..ff8ea57e9 100644 --- a/panels/notification/server/notifyserverapplet.h +++ b/panels/notification/server/notifyserverapplet.h @@ -31,7 +31,6 @@ public Q_SLOTS: void removeNotifications(const QString &appName); void removeNotifications(); void removeExpiredNotifications(); - void setBlockClosedId(qint64 id); private: NotificationManager *m_manager = nullptr; diff --git a/tests/panels/notification/server/notifyserverapplet_test.cpp b/tests/panels/notification/server/notifyserverapplet_test.cpp index 9a0463167..64ba2b46d 100644 --- a/tests/panels/notification/server/notifyserverapplet_test.cpp +++ b/tests/panels/notification/server/notifyserverapplet_test.cpp @@ -34,7 +34,6 @@ class MockNotificationManager : public NotificationManager { MOCK_METHOD(void, removeNotifications, (const QString &appName)); MOCK_METHOD(void, removeNotifications, ()); MOCK_METHOD(void, removeExpiredNotifications, ()); - MOCK_METHOD(void, setBlockClosedId, (qint64 id)); }; // Test fixture for NotifyServerApplet @@ -244,17 +243,6 @@ TEST_F(NotifyServerAppletTest, RemoveExpiredNotificationsTest) { EXPECT_NO_THROW(applet->removeExpiredNotifications()); } -// Test setBlockClosedId -TEST_F(NotifyServerAppletTest, SetBlockClosedIdTest) { - // Initialize applet first - applet->init(); - - qint64 testId = 12345; - - // Test that setBlockClosedId doesn't crash - EXPECT_NO_THROW(applet->setBlockClosedId(testId)); -} - // Test notificationStateChanged signal TEST_F(NotifyServerAppletTest, NotificationStateChangedSignalTest) { // Initialize applet first @@ -315,16 +303,6 @@ TEST_F(NotifyServerAppletTest, NotificationClosedEdgeCasesTest) { EXPECT_NO_THROW(applet->notificationClosed(999999999, 999999, 3)); } -// Test edge cases for setBlockClosedId -TEST_F(NotifyServerAppletTest, SetBlockClosedIdEdgeCasesTest) { - applet->init(); - - // Test with various ID values - EXPECT_NO_THROW(applet->setBlockClosedId(0)); - EXPECT_NO_THROW(applet->setBlockClosedId(-1)); - EXPECT_NO_THROW(applet->setBlockClosedId(9223372036854775807LL)); // max qint64 -} - // Test that applet properly inherits from DApplet TEST_F(NotifyServerAppletTest, InheritanceTest) { EXPECT_TRUE(applet->inherits("ds::DApplet"));