From aa5a30c8d94305afc728d11ff0e74ce7f3b456ce Mon Sep 17 00:00:00 2001 From: Ivy233 Date: Mon, 10 Aug 2026 11:14:39 +0800 Subject: [PATCH] fix: move notification expire timer to the bubble frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notification expire timer previously ran in the notification server (worker thread) and was started when the notification was received, so a notification waiting in a long display queue could expire before its bubble was shown. The timeout is now owned by the bubble frontend and only starts once the bubble is actually displayed. 1. Add an ExpireTimer helper that manages each tracked key's deadline, paused remaining time and caller data (such as the bubble id) with a single shared single-shot QTimer. It exposes operation-based APIs (start/pause/resume/stop/clear) instead of lower-level get/set ones, so no QTimer is allocated per notification and nothing leaks on expiry. 2. BubbleModel starts a per-bubble expire timer when a bubble is shown (insert/replace) using the client expireTimeout (0: never expire, -1: 5000ms default, Critical urgency: never expire). On expiry it emits bubbleExpired(id, bubbleId). 3. BubblePanel closes the expired bubble and calls notificationClosed(id, bubbleId, Expired) on the server. 4. Move the hover block to the frontend: BubbleModel::setBlockedId pauses/resumes the expire timer so hovering keeps the bubble on screen, keeping the 1s grace after unhover. 5. Give the notification center staging model its own expire timer: notifications shown in the staging area also time out, because the bubble panel is disabled while the center window is open so the bubble-side timers are not running. 6. Make NotificationManager::notificationClosed idempotent. A notification is tracked by both the bubble and the staging expire timers, so the close is reported only while the entity still exists, preventing a notification from being closed twice. 7. Critical notifications never expire on their own, enforced both when computing the frontend timeout and on the server. 8. Remove the server-side timeout bookkeeping: notificationDisplayed, setBlockClosedId, pushPendingEntity, onHandingPendingEntities, removePendingEntity and the pending-timeout QTimer. 9. Expose BubbleItem::timeout() and NotifyEntity::timeout()/urgency() for the frontend timeout computation. 10. Remove the obsolete NotificationDisplayed/SetBlockClosedId tests. Log: Fixed notifications expiring before their bubble was displayed. Influence: 1. Send several notifications at once and verify each bubble stays for the full expire timeout. 2. Verify critical notifications and expireTimeout 0 never close. 3. Hover a bubble and verify it does not expire, then closes 1s after unhover. 4. Verify an expired notification is closed only once and moved to the notification center, and the DBus NotificationClosed(Expired) signal is emitted once. 5. Open the notification center and verify staged notifications expire after their timeout and leave the staging area. 6. Run the notification server unit tests. fix: 将通知过期计时迁移到气泡前端 通知过期计时此前在通知服务器(工作线程)中运行,收到通知时即启动,导致 长显示队列中的通知可能在显示前就已过期。现将超时逻辑交由气泡前端持有, 气泡真正显示后才开始计时。 1. 新增 ExpireTimer 工具类:用一个共享的单次 QTimer 管理每个键的过期 时间、暂停剩余时间及调用方数据(如气泡 ID)。提供 start/pause/resume/ stop/clear 操作化接口,而非 contains/remaining 这类底层读写操作,不再 为每个通知分配 QTimer,到期后也不会泄漏对象。 2. BubbleModel 在气泡显示(插入/替换)时根据客户端 expireTimeout 启动 过期计时(0:永不过期,-1:默认 5000ms,Critical 优先级:永不过期), 到期时发出 bubbleExpired(id, bubbleId)。 3. 到期后 BubblePanel 关闭气泡并调用服务器 notificationClosed(id, bubbleId, Expired)。 4. 将悬停阻塞移到前端:BubbleModel::setBlockedId 暂停/恢复过期计时,使 悬停时气泡不关闭,并保留取消悬停后 1s 缓冲。 5. 为通知中心暂存模型增加过期计时:暂存区展示的通知同样会超时,因为 中心窗口打开时气泡面板被禁用,气泡侧的计时器不会运行。 6. 使 NotificationManager::notificationClosed 幂等:同一通知会被气泡与 暂存模型的过期计时同时跟踪,因此在实体仍存在时才上报关闭,避免通知 被关闭两次。 7. Critical 通知永不自动过期,前端超时计算与服务端均强制执行。 8. 移除服务端超时簿记:notificationDisplayed、setBlockClosedId、 pushPendingEntity、onHandingPendingEntities、removePendingEntity 以及 pending-timeout 定时器。 9. 为前端超时计算暴露 BubbleItem::timeout() 与 NotifyEntity::timeout()/ urgency()。 10. 移除过时的 NotificationDisplayed/SetBlockClosedId 测试。 Log: 修复通知在气泡显示前就过期的问题。 Influence: 1. 一次性发送多条通知,验证每个气泡都能保持完整的过期时间。 2. 验证 Critical 通知与 expireTimeout 为 0 的通知永不过期。 3. 悬停气泡验证其不关闭,取消悬停 1s 后关闭。 4. 验证过期通知只被关闭一次并进入通知中心,DBus NotificationClosed (Expired) 信号只发送一次。 5. 打开通知中心,验证暂存区的通知到期后超时并移出暂存区。 6. 运行通知服务器单元测试。 PMS: BUG-372279 --- panels/notification/CMakeLists.txt | 2 + panels/notification/bubble/bubbleitem.cpp | 5 + panels/notification/bubble/bubbleitem.h | 1 + panels/notification/bubble/bubblemodel.cpp | 40 +++++- panels/notification/bubble/bubblemodel.h | 13 +- panels/notification/bubble/bubblepanel.cpp | 10 +- .../center/notifystagingmodel.cpp | 22 ++- .../notification/center/notifystagingmodel.h | 7 +- panels/notification/common/expiretimer.cpp | 130 +++++++++++++++++ panels/notification/common/expiretimer.h | 64 +++++++++ panels/notification/common/notifyentity.cpp | 10 ++ panels/notification/common/notifyentity.h | 4 + .../server/notificationmanager.cpp | 136 ++---------------- .../notification/server/notificationmanager.h | 9 -- .../server/notifyserverapplet.cpp | 5 - .../notification/server/notifyserverapplet.h | 1 - .../server/notifyserverapplet_test.cpp | 22 --- 17 files changed, 316 insertions(+), 165 deletions(-) create mode 100644 panels/notification/common/expiretimer.cpp create mode 100644 panels/notification/common/expiretimer.h 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..51d6f3e96 100644 --- a/panels/notification/bubble/bubbleitem.cpp +++ b/panels/notification/bubble/bubbleitem.cpp @@ -91,6 +91,11 @@ int BubbleItem::urgency() const return m_urgency; } +int BubbleItem::timeout() const +{ + return m_entity.timeout(); +} + QString BubbleItem::bodyImagePath() const { return m_entity.bodyIcon(); diff --git a/panels/notification/bubble/bubbleitem.h b/panels/notification/bubble/bubbleitem.h index 1b28f0aa8..463fedda5 100644 --- a/panels/notification/bubble/bubbleitem.h +++ b/panels/notification/bubble/bubbleitem.h @@ -32,6 +32,7 @@ class BubbleItem : public QObject uint replacesId() const; bool isReplace() const; int urgency() const; + int timeout() const; QString bodyImagePath() const; qint64 ctime() const; diff --git a/panels/notification/bubble/bubblemodel.cpp b/panels/notification/bubble/bubblemodel.cpp index c9bbee6fb..fb338b7eb 100644 --- a/panels/notification/bubble/bubblemodel.cpp +++ b/panels/notification/bubble/bubblemodel.cpp @@ -20,6 +20,8 @@ Q_DECLARE_LOGGING_CATEGORY(notifyLog) namespace notification { +static const int BlockItemTimeout = 1000; + BubbleModel::BubbleModel(QObject *parent) : QAbstractListModel(parent) , m_updateTimeTipTimer(new QTimer(this)) @@ -41,6 +43,9 @@ BubbleModel::BubbleModel(QObject *parent) m_processPendingTimer->start(); } }); + connect(&m_expireTimer, &ExpireTimer::expired, this, [this](qint64 id, const QVariant &data) { + Q_EMIT bubbleExpired(id, data.toUInt()); + }); connect(NotifySetting::instance(), &NotifySetting::contentRowCountChanged, this, &BubbleModel::updateContentRowCount); connect(NotifySetting::instance(), &NotifySetting::bubbleCountChanged, this, &BubbleModel::updateBubbleCount); @@ -82,6 +87,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. + m_expireTimer.start(bubble->id(), effectiveTimeout(bubble->urgency(), bubble->timeout()), bubble->bubbleId()); } bool BubbleModel::isReplaceBubble(const BubbleItem *bubble) const @@ -95,9 +104,17 @@ BubbleItem *BubbleModel::replaceBubble(BubbleItem *bubble) const auto replaceIndex = replaceBubbleIndex(bubble); const auto oldBubble = m_bubbles[replaceIndex]; + m_expireTimer.stop(oldBubble->id()); + m_bubbles.replace(replaceIndex, bubble); Q_EMIT dataChanged(index(replaceIndex), index(replaceIndex)); + m_expireTimer.start(bubble->id(), effectiveTimeout(bubble->urgency(), bubble->timeout()), bubble->bubbleId()); + + // If the replaced bubble was the hovered one, keep blocking the new one. + if (m_blockedId == oldBubble->id()) + m_expireTimer.pause(bubble->id()); + return oldBubble; } @@ -109,6 +126,9 @@ void BubbleModel::clear() qDeleteAll(m_pendingBubbles); m_pendingBubbles.clear(); + m_blockedId = NotifyEntity::InvalidId; + m_expireTimer.clear(); + if (m_bubbles.count() <= 0) return; beginResetModel(); @@ -129,11 +149,12 @@ void BubbleModel::remove(int index) if (index < 0 || index >= m_bubbles.size()) return; - beginRemoveRows(QModelIndex(), index, index); auto bubble = m_bubbles.takeAt(index); + m_expireTimer.stop(bubble->id()); + + beginRemoveRows(QModelIndex(), index, index); bubble->deleteLater(); endRemoveRows(); - } void BubbleModel::remove(const BubbleItem *bubble) @@ -298,4 +319,19 @@ void BubbleModel::updateContentRowCount(int rowCount) Q_EMIT dataChanged(index(0), index(m_bubbles.size() - 1), {BubbleModel::ContentRowCount}); } } + +void BubbleModel::setBlockedId(qint64 id) +{ + if (id == m_blockedId) + return; + + if (m_blockedId != NotifyEntity::InvalidId) + m_expireTimer.resume(m_blockedId, BlockItemTimeout); + + m_blockedId = id; + + if (id != NotifyEntity::InvalidId) + m_expireTimer.pause(id); } + +} // notification diff --git a/panels/notification/bubble/bubblemodel.h b/panels/notification/bubble/bubblemodel.h index b9b8f6203..dc18fdaad 100644 --- a/panels/notification/bubble/bubblemodel.h +++ b/panels/notification/bubble/bubblemodel.h @@ -6,8 +6,10 @@ #include "dsglobal.h" #include "notifyentity.h" +#include "expiretimer.h" #include +#include #include class QTimer; @@ -38,6 +40,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); @@ -51,6 +57,10 @@ class BubbleModel : public QAbstractListModel BubbleItem *removeById(qint64 id); void clear(); + // Pause/resume the expire timer of the hovered bubble so hovering + // keeps the bubble on screen (0 clears the blocked bubble). + void setBlockedId(qint64 id); + BubbleItem *bubbleItem(int bubbleIndex) const; int rowCount(const QModelIndex &parent) const override; @@ -68,11 +78,12 @@ class BubbleModel : public QAbstractListModel void updateBubbleTimeTip(); void updateContentRowCount(int rowCount); -private: QTimer *m_updateTimeTipTimer = nullptr; QTimer *m_processPendingTimer = nullptr; + ExpireTimer m_expireTimer; QList m_bubbles; QQueue m_pendingBubbles; + qint64 m_blockedId = NotifyEntity::InvalidId; int m_maxKeep{5}; int m_contentRowCount{6}; }; diff --git a/panels/notification/bubble/bubblepanel.cpp b/panels/notification/bubble/bubblepanel.cpp index 469fe67f7..61073138e 100644 --- a/panels/notification/bubble/bubblepanel.cpp +++ b/panels/notification/bubble/bubblepanel.cpp @@ -53,6 +53,14 @@ bool BubblePanel::init() connect(m_bubbles, &BubbleModel::rowsInserted, this, &BubblePanel::onBubbleCountChanged); connect(m_bubbles, &BubbleModel::rowsRemoved, this, &BubblePanel::onBubbleCountChanged); + // The bubble model runs one expire timer per shown bubble. When a bubble + // times out, close it and notify the server so it moves the notification + // from the in-memory store to the center database and emits the signals. + connect(m_bubbles, &BubbleModel::bubbleExpired, this, [this](qint64 id, uint bubbleId) { + closeBubble(id); + QMetaObject::invokeMethod(m_notificationServer, "notificationClosed", Qt::DirectConnection, + Q_ARG(qint64, id), Q_ARG(uint, bubbleId), Q_ARG(uint, NotifyEntity::Expired)); + }); return true; } @@ -217,7 +225,7 @@ void BubblePanel::setEnabled(bool newEnabled) void BubblePanel::setHoveredId(qint64 id) { - QMetaObject::invokeMethod(m_notificationServer, "setBlockClosedId", Qt::DirectConnection, Q_ARG(qint64, id)); + m_bubbles->setBlockedId(id); } } diff --git a/panels/notification/center/notifystagingmodel.cpp b/panels/notification/center/notifystagingmodel.cpp index 303bfc75c..f93fd7c4f 100644 --- a/panels/notification/center/notifystagingmodel.cpp +++ b/panels/notification/center/notifystagingmodel.cpp @@ -25,12 +25,18 @@ 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(&m_expireTimer, &ExpireTimer::expired, this, [this](qint64 id, const QVariant &) { + closeNotify(id, NotifyEntity::Expired); + }); } void NotifyStagingModel::close() { qDebug(notifyLog) << "close"; + m_expireTimer.clear(); + beginResetModel(); qDeleteAll(m_appNotifies); m_appNotifies.clear(); @@ -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. + m_expireTimer.start(entity.id(), effectiveTimeout(entity.urgency(), entity.timeout())); + if (m_refreshTimer < 0) { m_refreshTimer = startTimer(std::chrono::milliseconds(1000)); } @@ -90,6 +100,8 @@ void NotifyStagingModel::remove(qint64 id) { qDebug(notifyLog) << "Remove notify by id" << id; + m_expireTimer.stop(id); + int row = -1; for (int i = 0; i < m_appNotifies.size(); i++) { auto item = m_appNotifies[i]; @@ -146,6 +158,7 @@ void NotifyStagingModel::remove(qint64 id) auto notify = new AppNotifyItem(newEntity); m_appNotifies.insert(insertedIndex, notify); endInsertRows(); + m_expireTimer.start(newEntity.id(), effectiveTimeout(newEntity.urgency(), newEntity.timeout())); } } updateOverlapCount(entities.size()); @@ -155,6 +168,8 @@ void NotifyStagingModel::open() { qDebug(notifyLog) << "Open staging model"; + m_expireTimer.clear(); + auto entities = m_accessor->fetchEntities(DataAccessor::AllApp(), NotifyEntity::NotProcessed, BubbleMaxCount + OverlayMaxCount); qDebug(notifyLog) << "Fetched staging size" << entities.size(); @@ -172,6 +187,9 @@ void NotifyStagingModel::open() auto notify = new AppNotifyItem(entities.at(i)); m_appNotifies << notify; } + for (const auto &entity : entities) { + m_expireTimer.start(entity.id(), effectiveTimeout(entity.urgency(), entity.timeout())); + } updateOverlapCount(entities.size()); endResetModel(); @@ -251,7 +269,9 @@ 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()) { + m_expireTimer.stop(entity.bubbleId()); item->setEntity(entity); + m_expireTimer.start(entity.id(), effectiveTimeout(entity.urgency(), entity.timeout())); const auto index = this->index(i, 0, {}); dataChanged(index, index); break; @@ -333,9 +353,9 @@ void NotifyStagingModel::updateContentRowCount(int rowCount) return; m_contentRowCount = rowCount; - if (!m_appNotifies.isEmpty()) { dataChanged(index(0), index(m_appNotifies.size() - 1), {NotifyRole::NotifyContentRowCount}); } } + } diff --git a/panels/notification/center/notifystagingmodel.h b/panels/notification/center/notifystagingmodel.h index 7a71112fb..c5281c801 100644 --- a/panels/notification/center/notifystagingmodel.h +++ b/panels/notification/center/notifystagingmodel.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -10,6 +10,7 @@ #include "notifyitem.h" #include "dataaccessor.h" +#include "expiretimer.h" namespace notifycenter { /** @@ -73,5 +74,9 @@ private slots: DataAccessor *m_accessor = nullptr; int m_overlapCount = 0; int m_contentRowCount = 6; + // Notifications shown in the staging area (notification center) need their + // own expire timeout because the bubble panel is disabled while the center + // window is open, so the bubble-side timers are not running. + ExpireTimer m_expireTimer; }; } diff --git a/panels/notification/common/expiretimer.cpp b/panels/notification/common/expiretimer.cpp new file mode 100644 index 000000000..d9f0bc917 --- /dev/null +++ b/panels/notification/common/expiretimer.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "expiretimer.h" + +#include +#include + +#include + +#include "notifyentity.h" + +namespace notification { + +static const int DefaultTimeoutMSecs = 5000; + +ExpireTimer::ExpireTimer(QObject *parent) + : QObject(parent) + , m_timer(new QTimer(this)) +{ + m_timer->setSingleShot(true); + connect(m_timer, &QTimer::timeout, this, &ExpireTimer::onTimeout); +} + +void ExpireTimer::start(qint64 key, int interval, const QVariant &data) +{ + if (interval <= 0) { + // Never expire: cancel any pending or paused countdown. + m_deadlines.remove(key); + m_paused.remove(key); + m_data.remove(key); + schedule(); + return; + } + + m_paused.remove(key); + m_data.insert(key, data); + m_deadlines.insert(key, QDateTime::currentMSecsSinceEpoch() + interval); + schedule(); +} + +void ExpireTimer::pause(qint64 key) +{ + const auto it = m_deadlines.constFind(key); + if (it == m_deadlines.cend()) + return; + + m_paused.insert(key, static_cast(qMax(0, it.value() - QDateTime::currentMSecsSinceEpoch()))); + m_deadlines.erase(it); + schedule(); +} + +void ExpireTimer::resume(qint64 key, int minRemaining) +{ + const auto it = m_paused.find(key); + if (it == m_paused.end()) + return; + + const int remaining = qMax(it.value(), minRemaining); + m_paused.erase(it); + m_deadlines.insert(key, QDateTime::currentMSecsSinceEpoch() + remaining); + schedule(); +} + +void ExpireTimer::stop(qint64 key) +{ + const bool removed = m_deadlines.remove(key) > 0; + m_paused.remove(key); + m_data.remove(key); + if (removed) + schedule(); +} + +void ExpireTimer::clear() +{ + m_deadlines.clear(); + m_paused.clear(); + m_data.clear(); + m_timer->stop(); +} + +void ExpireTimer::schedule() +{ + if (m_deadlines.isEmpty()) { + m_timer->stop(); + return; + } + + auto it = std::min_element(m_deadlines.cbegin(), m_deadlines.cend(), + [](const qint64 &lhs, const qint64 &rhs) { return lhs < rhs; }); + const qint64 remaining = qMax(0, it.value() - QDateTime::currentMSecsSinceEpoch()); + m_timer->start(static_cast(remaining)); +} + +void ExpireTimer::onTimeout() +{ + const auto now = QDateTime::currentMSecsSinceEpoch(); + const QList expiredKeys = [this, now] { + QList keys; + for (auto it = m_deadlines.cbegin(); it != m_deadlines.cend(); ++it) { + if (it.value() <= now) + keys.append(it.key()); + } + return keys; + }(); + + for (const auto &key : expiredKeys) { + if (m_deadlines.remove(key) > 0) { + const auto data = m_data.take(key); + Q_EMIT expired(key, data); + } + } + + schedule(); +} + +int effectiveTimeout(int urgency, int expireTimeout) +{ + // Critical notifications never expire. + if (urgency == NotifyEntity::Critical) + return 0; + + if (expireTimeout == 0) + return 0; + + return expireTimeout == -1 ? DefaultTimeoutMSecs : expireTimeout; +} + +} diff --git a/panels/notification/common/expiretimer.h b/panels/notification/common/expiretimer.h new file mode 100644 index 000000000..f40b635d5 --- /dev/null +++ b/panels/notification/common/expiretimer.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +class QTimer; + +namespace notification { + +/** + * @brief Tracks expire deadlines for a set of keys using one shared QTimer. + * + * Each key keeps its own deadline, the caller data associated with it and, while + * blocked, its remaining time. The nearest deadline drives a single-shot QTimer; + * when a deadline passes, expired() is emitted once for that key and the key is + * forgotten. All bookkeeping lives inside this class, so callers only describe + * what to do (start, pause, resume, stop, clear) and never have to compose + * lower-level operations. No QTimer is allocated per key and nothing leaks when + * a key expires or is stopped. + */ +class ExpireTimer : public QObject +{ + Q_OBJECT +public: + explicit ExpireTimer(QObject *parent = nullptr); + + // Starts a countdown for key. A non-positive interval cancels any pending + // countdown, i.e. the key never expires. data is stored with the key and + // handed back by expired() when the countdown finishes. + void start(qint64 key, int interval, const QVariant &data = QVariant()); + // Blocks key from expiring, keeping its remaining time for resume(). + void pause(qint64 key); + // Resumes a paused key with at least minRemaining milliseconds left. + void resume(qint64 key, int minRemaining = 0); + // Forgets key so it never expires. + void stop(qint64 key); + // Forgets every key and stops the timer. + void clear(); + +Q_SIGNALS: + // Emitted once when the deadline of key passes, with the data passed to start(). + void expired(qint64 key, const QVariant &data); + +private: + void schedule(); + void onTimeout(); + + QTimer *m_timer = nullptr; + QHash m_deadlines; + QHash m_paused; + QHash m_data; +}; + +// 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. +int effectiveTimeout(int urgency, int expireTimeout); + +} 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..7edca3aa2 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,20 @@ 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; + + // Critical notifications must not disappear on their own. + if (reason == NotifyEntity::Expired && entity.urgency() == NotifyEntity::Critical) + return; + updateEntityProcessed(id, reason); Q_EMIT NotificationClosed(bubbleId, reason); @@ -296,22 +303,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 +366,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 +469,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 +502,6 @@ void NotificationManager::updateEntityProcessed(const NotifyEntity &entity) Q_EMIT NotificationStateChanged(entity.id(), entity.processedType()); - removePendingEntity(entity); emitRecordCountChanged(); } @@ -707,69 +662,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..cd426cbf9 100644 --- a/panels/notification/server/notifyserverapplet.cpp +++ b/panels/notification/server/notifyserverapplet.cpp @@ -104,11 +104,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"));