blob: df0d0e3d9316956ece88d4dfe72a89b2d8074261 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/message_center/notification_list.h"
#include <utility>
#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/notification_blocker.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
namespace message_center {
namespace {
bool ShouldShowNotificationAsPopup(const Notification& notification,
const NotificationBlockers& blockers,
const NotificationBlocker* except) {
for (message_center::NotificationBlocker* blocker : blockers) {
if (blocker != except &&
!blocker->ShouldShowNotificationAsPopup(notification)) {
return false;
}
}
return true;
}
} // namespace
bool ComparePriorityTimestampSerial::operator()(Notification* n1,
Notification* n2) const {
if (n1->priority() > n2->priority()) { // Higher pri go first.
return true;
}
if (n1->priority() < n2->priority()) {
return false;
}
return CompareTimestampSerial()(n1, n2);
}
bool CompareTimestampSerial::operator()(Notification* n1,
Notification* n2) const {
if (n1->timestamp() > n2->timestamp()) { // Newer come first.
return true;
}
if (n1->timestamp() < n2->timestamp()) {
return false;
}
if (n1->serial_number() > n2->serial_number()) { // Newer come first.
return true;
}
if (n1->serial_number() < n2->serial_number()) {
return false;
}
return false;
}
bool NotificationList::NotificationState::operator!=(
const NotificationState& other) const {
return shown_as_popup != other.shown_as_popup || is_read != other.is_read;
}
NotificationList::NotificationList(MessageCenter* message_center)
: message_center_(message_center), quiet_mode_(false) {}
NotificationList::~NotificationList() = default;
#if BUILDFLAG(IS_CHROMEOS_ASH)
std::string NotificationList::GetOldestNonGroupedNotificationId() {
auto oldest_lowest_priority_notification_iter = --notifications_.end();
// Do not return a parent notification with grouped children because this kind
// of notification is a container of child notifications, and do not return a
// pinned notification.
while (oldest_lowest_priority_notification_iter->first->pinned() ||
oldest_lowest_priority_notification_iter->first->group_parent()) {
// If all of the notifications are pinned or grouped, return nothing.
if (oldest_lowest_priority_notification_iter == notifications_.begin()) {
return std::string();
}
--oldest_lowest_priority_notification_iter;
}
return oldest_lowest_priority_notification_iter->first->id();
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
void NotificationList::SetNotificationsShown(
const NotificationBlockers& blockers,
std::set<std::string>* updated_ids) {
Notifications notifications = GetVisibleNotifications(blockers);
for (Notification* notification : notifications) {
NotificationState* state = &GetNotification(notification->id())->second;
const NotificationState original_state = *state;
state->shown_as_popup = true;
state->is_read = true;
if (updated_ids && (original_state != *state)) {
updated_ids->insert(notification->id());
}
}
}
void NotificationList::AddNotification(
std::unique_ptr<Notification> notification) {
PushNotification(std::move(notification));
}
void NotificationList::UpdateNotificationMessage(
const std::string& old_id,
std::unique_ptr<Notification> new_notification) {
auto iter = GetNotification(old_id);
if (iter == notifications_.end()) {
return;
}
NotificationState state = iter->second;
if ((new_notification->renotify() ||
!message_center_->HasMessageCenterView()) &&
!quiet_mode_) {
state = NotificationState();
}
// Do not use EraseNotification and PushNotification, since we don't want to
// change unread counts nor to update is_read/shown_as_popup states.
notifications_.erase(iter);
// We really don't want duplicate IDs.
DCHECK(GetNotification(new_notification->id()) == notifications_.end());
notifications_.emplace(std::move(new_notification), state);
}
void NotificationList::RemoveNotification(const std::string& id) {
EraseNotification(GetNotification(id));
}
NotificationList::Notifications NotificationList::GetNotifications() const {
Notifications notifications;
for (const auto& tuple : notifications_) {
notifications.insert(tuple.first.get());
}
return notifications;
}
NotificationList::Notifications NotificationList::GetNotificationsByNotifierId(
const NotifierId& notifier_id) const {
Notifications notifications;
for (const auto& tuple : notifications_) {
Notification* notification = tuple.first.get();
if (notification->notifier_id() == notifier_id) {
notifications.insert(notification);
}
}
return notifications;
}
NotificationList::Notifications NotificationList::GetNotificationsByAppId(
const std::string& app_id) const {
Notifications notifications;
for (const auto& tuple : notifications_) {
Notification* notification = tuple.first.get();
if (notification->notifier_id().id == app_id) {
notifications.insert(notification);
}
}
return notifications;
}
NotificationList::Notifications NotificationList::GetNotificationsByOriginUrl(
const GURL& source_url) const {
Notifications notifications;
for (const auto& tuple : notifications_) {
Notification* notification = tuple.first.get();
if (notification->origin_url() == source_url) {
notifications.insert(notification);
}
}
return notifications;
}
bool NotificationList::SetNotificationIcon(const std::string& notification_id,
const ui::ImageModel& image) {
auto iter = GetNotification(notification_id);
if (iter == notifications_.end()) {
return false;
}
iter->first->set_icon(image);
return true;
}
bool NotificationList::SetNotificationImage(const std::string& notification_id,
const gfx::Image& image) {
auto iter = GetNotification(notification_id);
if (iter == notifications_.end()) {
return false;
}
iter->first->set_image(image);
return true;
}
bool NotificationList::HasNotificationOfType(
const std::string& id,
const NotificationType type) const {
auto iter = GetNotification(id);
if (iter == notifications_.end()) {
return false;
}
return iter->first->type() == type;
}
bool NotificationList::HasPopupNotifications(
const NotificationBlockers& blockers) const {
for (const auto& tuple : notifications_) {
if (tuple.first->priority() < DEFAULT_PRIORITY) {
break;
}
if (!tuple.second.shown_as_popup &&
ShouldShowNotificationAsPopup(*tuple.first, blockers,
/*except=*/nullptr)) {
return true;
}
}
return false;
}
NotificationList::PopupNotifications NotificationList::GetPopupNotifications(
const NotificationBlockers& blockers,
std::list<std::string>* blocked) {
PopupNotifications result;
size_t default_priority_popup_count = 0;
// Collect notifications that should be shown as popups. Start from oldest.
for (auto& [notification, state] : base::Reversed(notifications_)) {
if (state.shown_as_popup) {
continue;
}
// No popups for LOW/MIN priority.
if (notification->priority() < DEFAULT_PRIORITY) {
continue;
}
// Group child notifications are shown in their parent's popup.
if (notification->group_child()) {
continue;
}
if (!ShouldShowNotificationAsPopup(*notification, blockers,
/*except=*/nullptr)) {
if (state.is_read) {
state.shown_as_popup = true;
}
if (blocked) {
blocked->push_back(notification->id());
}
continue;
}
// Checking limits. No limits for HIGH/MAX priority. DEFAULT priority
// will return at most kMaxVisiblePopupNotifications entries. If the
// popup entries are more, older entries are used. see crbug.com/165768
if (notification->priority() == DEFAULT_PRIORITY &&
default_priority_popup_count++ >= kMaxVisiblePopupNotifications) {
continue;
}
result.insert(notification.get());
}
return result;
}
NotificationList::PopupNotifications
NotificationList::GetPopupNotificationsWithoutBlocker(
const NotificationBlockers& blockers,
const NotificationBlocker& blocker) const {
PopupNotifications result;
// Collect notifications that should be shown as popups, starting with the
// newest.
// TODO(1276903): see if we can merge this logic with `GetPopupNotifications`.
// In particular, we could pass an optional blocker argument that would be
// bypassed if specified.
for (const auto& iter : notifications_) {
const NotificationState* state = &iter.second;
Notification* notification = iter.first.get();
if (state->shown_as_popup) {
continue;
}
// No popups for LOW/MIN priority.
if (notification->priority() < DEFAULT_PRIORITY) {
continue;
}
// Group child notifications are shown in their parent's popup.
if (notification->group_child()) {
continue;
}
if (!ShouldShowNotificationAsPopup(*notification, blockers, &blocker)) {
continue;
}
result.insert(notification);
}
return result;
}
void NotificationList::MarkSinglePopupAsShown(const std::string& id,
bool mark_notification_as_read) {
auto iter = GetNotification(id);
DCHECK(iter != notifications_.end());
NotificationState* state = &iter->second;
if (iter->second.shown_as_popup) {
return;
}
state->shown_as_popup = true;
// The popup notification is already marked as read when it's displayed.
// Set the is_read back to false if necessary.
if (!mark_notification_as_read) {
state->is_read = false;
}
}
void NotificationList::MarkSinglePopupAsDisplayed(const std::string& id) {
auto iter = GetNotification(id);
if (iter == notifications_.end()) {
return;
}
NotificationState* state = &iter->second;
if (state->shown_as_popup) {
return;
}
state->is_read = true;
}
void NotificationList::ResetSinglePopup(const std::string& id) {
auto iter = GetNotification(id);
DCHECK(iter != notifications_.end());
NotificationState* state = &iter->second;
// `shown_as_popup` should be true if quiet mode is enabled.
state->shown_as_popup = quiet_mode_;
state->is_read = false;
state->expand_state = ExpandState::DEFAULT;
}
ExpandState NotificationList::GetNotificationExpandState(
const std::string& id) {
auto iter = GetNotification(id);
if (iter == notifications_.end()) {
return ExpandState::DEFAULT;
}
return iter->second.expand_state;
}
void NotificationList::SetNotificationExpandState(
const std::string& id,
const ExpandState expand_state) {
auto iter = GetNotification(id);
if (iter == notifications_.end()) {
return;
}
iter->second.expand_state = expand_state;
}
NotificationDelegate* NotificationList::GetNotificationDelegate(
const std::string& id) {
auto iter = GetNotification(id);
if (iter == notifications_.end()) {
return nullptr;
}
return iter->first->delegate();
}
void NotificationList::SetQuietMode(bool quiet_mode) {
quiet_mode_ = quiet_mode;
if (quiet_mode_) {
// To prevent popups showing in quiet mode, mark all notifications'
// `shown_as_popup` to true.
for (auto& tuple : notifications_) {
tuple.second.shown_as_popup = true;
}
}
}
Notification* NotificationList::GetNotificationById(const std::string& id) {
auto iter = GetNotification(id);
if (iter != notifications_.end()) {
return iter->first.get();
}
return nullptr;
}
NotificationList::Notifications NotificationList::GetVisibleNotifications(
const NotificationBlockers& blockers) const {
return GetVisibleNotificationsWithoutBlocker(blockers, nullptr);
}
NotificationList::Notifications
NotificationList::GetVisibleNotificationsWithoutBlocker(
const NotificationBlockers& blockers,
const NotificationBlocker* ignored_blocker) const {
Notifications result;
for (const auto& tuple : notifications_) {
auto it = (base::ranges::find_if(
blockers, [&ignored_blocker,
&tuple](message_center::NotificationBlocker* blocker) {
return blocker != ignored_blocker &&
!blocker->ShouldShowNotification(*tuple.first);
}));
if (it == blockers.end()) {
result.insert(tuple.first.get());
}
}
return result;
}
size_t NotificationList::NotificationCount(
const NotificationBlockers& blockers) const {
return GetVisibleNotifications(blockers).size();
}
NotificationList::OwnedNotifications::iterator
NotificationList::GetNotification(const std::string& id) {
for (auto iter = notifications_.begin(); iter != notifications_.end();
++iter) {
if (iter->first->id() == id) {
return iter;
}
}
return notifications_.end();
}
NotificationList::OwnedNotifications::const_iterator
NotificationList::GetNotification(const std::string& id) const {
for (auto iter = notifications_.begin(); iter != notifications_.end();
++iter) {
if (iter->first->id() == id) {
return iter;
}
}
return notifications_.end();
}
void NotificationList::EraseNotification(OwnedNotifications::iterator iter) {
notifications_.erase(iter);
}
void NotificationList::PushNotification(
std::unique_ptr<Notification> notification) {
// Ensure that notification.id is unique by erasing any existing
// notification with the same id (shouldn't normally happen).
auto iter = GetNotification(notification->id());
NotificationState state;
if (iter != notifications_.end()) {
state = iter->second;
EraseNotification(iter);
} else {
// For critical ChromeOS system notifications, we ignore the standard quiet
// mode behaviour and show the notification anyways.
bool effective_quiet_mode = quiet_mode_;
#if BUILDFLAG(IS_CHROMEOS_ASH)
effective_quiet_mode &= notification->system_notification_warning_level() !=
SystemNotificationWarningLevel::CRITICAL_WARNING;
#endif
// TODO(mukai): needs to distinguish if a notification is dismissed by
// the quiet mode or user operation.
state.shown_as_popup =
message_center_->IsMessageCenterVisible() || effective_quiet_mode;
}
if (notification->priority() == MIN_PRIORITY) {
state.is_read = true;
}
notifications_.emplace(std::move(notification), state);
}
} // namespace message_center