blob: b49cce3ac25ef6e8eb9f6e8651b85caa7fc4ef68 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/message_center/notification_grouping_controller.h"
#include "ash/system/message_center/ash_message_popup_collection.h"
#include "ash/system/message_center/unified_message_center_bubble.h"
#include "ash/system/message_center/unified_message_center_view.h"
#include "ash/system/message_center/unified_message_list_view.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/notification_view_controller.h"
#include "ui/message_center/public/cpp/message_center_constants.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/views/message_view.h"
using message_center::MessageCenter;
using message_center::MessageView;
using message_center::Notification;
namespace ash {
namespace {
class GroupedNotificationList {
public:
GroupedNotificationList() = default;
GroupedNotificationList(const GroupedNotificationList& other) = delete;
GroupedNotificationList& operator=(const GroupedNotificationList& other) =
delete;
~GroupedNotificationList() = default;
void AddGroupedNotification(const std::string& notification_id,
const std::string& parent_id) {
if (notifications_in_parent_map_.find(parent_id) ==
notifications_in_parent_map_.end()) {
notifications_in_parent_map_[parent_id] = {};
}
child_parent_map_[notification_id] = parent_id;
if (notification_id != parent_id)
notifications_in_parent_map_[parent_id].insert(notification_id);
}
// Remove a single child notification from a grouped notification.
void RemoveGroupedChildNotification(const std::string& notification_id) {
std::string& parent_id = child_parent_map_[notification_id];
notifications_in_parent_map_[parent_id].erase(notification_id);
child_parent_map_.erase(notification_id);
}
// Clear the entire grouped notification with `parent_id`
void ClearGroupedNotification(const std::string& parent_id) {
notifications_in_parent_map_.erase(parent_id);
std::vector<std::string> to_be_deleted;
for (const auto& it : child_parent_map_) {
if (it.second == parent_id)
to_be_deleted.push_back(it.first);
}
for (const auto& child : to_be_deleted)
child_parent_map_.erase(child);
}
const std::string& GetParentForChild(const std::string& child_id) {
return child_parent_map_[child_id];
}
std::set<std::string>& GetGroupedNotificationsForParent(
const std::string& parent_id) {
return notifications_in_parent_map_[parent_id];
}
bool GroupedChildNotificationExists(const std::string& child_id) {
return child_parent_map_.find(child_id) != child_parent_map_.end();
}
bool ParentNotificationExists(const std::string& parent_id) {
return notifications_in_parent_map_.find(parent_id) !=
notifications_in_parent_map_.end();
}
// Replaces all instances of `old_parent_id` with `new_parent_id` in
// the `child_parent_map_`.
void ReplaceParentId(const std::string& new_parent_id,
const std::string& old_parent_id) {
// Remove entry with `new_parent_id` as a child id and replace with
// `old_parent_id` as a child id.
child_parent_map_.erase(new_parent_id);
child_parent_map_[old_parent_id] = new_parent_id;
// Replace all occurrences of `old_parent_id` with `new_parent_id`.
std::vector<std::string> to_be_updated;
for (const auto& child : child_parent_map_) {
if (child.second == old_parent_id)
to_be_updated.push_back(child.first);
}
for (const auto& id : to_be_updated) {
child_parent_map_.erase(child_parent_map_.find(id));
child_parent_map_[id] = new_parent_id;
}
}
private:
// Map for looking up the parent `notification_id` for any given notification
// id.
std::map<std::string, std::string> child_parent_map_;
// Map containing a list of child notification ids per each group parent id.
// Used to keep track of grouped notifications which already have a parent
// notification view.
std::map<std::string, std::set<std::string>> notifications_in_parent_map_;
};
} // namespace
NotificationGroupingController::NotificationGroupingController(
UnifiedSystemTray* tray)
: tray_(tray),
grouped_notification_list_(std::make_unique<GroupedNotificationList>()) {
observer_.Observe(MessageCenter::Get());
}
NotificationGroupingController::~NotificationGroupingController() = default;
void NotificationGroupingController::PopulateGroupParent(
const std::string& notification_id) {
DCHECK(MessageCenter::Get()
->FindNotificationById(notification_id)
->group_parent());
MessageView* parent_view =
GetActiveNotificationViewController()->GetMessageViewForNotificationId(
notification_id);
// TODO(crbug/1277765) Need this check to fix crbug/1275765. However, this
// should not be necessary if the message center bubble is initialized
// properly. Need to monitor for empty group notifications if this check is
// hit and fix the root cause.
if (!parent_view) {
LOG(WARNING) << "MessageView does not exist for notification: "
<< notification_id;
return;
}
if (!parent_view->IsManuallyExpandedOrCollapsed())
parent_view->SetExpanded(false);
std::vector<const Notification*> notifications;
for (const auto* notification : MessageCenter::Get()->GetNotifications()) {
if (notification->notifier_id() == parent_view->notifier_id() &&
notification->id() != parent_view->notification_id()) {
grouped_notification_list_->AddGroupedNotification(notification->id(),
notification_id);
notifications.push_back(notification);
}
}
parent_view->PopulateGroupNotifications(notifications);
}
const std::string& NotificationGroupingController::GetParentIdForChildForTest(
const std::string& notification_id) const {
return grouped_notification_list_->GetParentForChild(notification_id);
}
void NotificationGroupingController::SetupParentNotification(
std::string* parent_id) {
Notification* parent_notification =
MessageCenter::Get()->FindNotificationById(*parent_id);
std::unique_ptr<Notification> notification_copy =
CreateCopyForParentNotification(*parent_notification);
std::string new_parent_id = notification_copy->id();
std::string old_parent_id = *parent_id;
*parent_id = new_parent_id;
grouped_notification_list_->AddGroupedNotification(old_parent_id,
new_parent_id);
Notification* new_parent_notification = notification_copy.get();
{
// Prevent double processing `parent_notification`'s copy, used as a group
// parent.
base::AutoReset<bool> reset(&adding_parent_grouped_notification_, true);
MessageCenter::Get()->AddNotification(std::move(notification_copy));
}
GetActiveNotificationViewController()
->ConvertNotificationViewToGroupedNotificationView(
/*ungrouped_notification_id=*/old_parent_id,
/*new_grouped_notification_id=*/new_parent_id);
grouped_notification_list_->ReplaceParentId(
/*new_parent_id=*/new_parent_id,
/*old_parent_id=*/old_parent_id);
// Add the old parent notification as a group child to the
// newly created parent notification which will act as a
// container for this group as long as it exists.
new_parent_notification->SetGroupParent();
parent_notification->SetGroupChild();
auto* parent_view =
GetActiveNotificationViewController()->GetMessageViewForNotificationId(
new_parent_id);
if (parent_view) {
parent_view->UpdateWithNotification(*new_parent_notification);
// Grouped notifications should start off in the collapsed state.
parent_view->SetExpanded(false);
parent_view->AddGroupNotification(*parent_notification,
/*newest_first=*/false);
}
}
void NotificationGroupingController::
SetupSingleNotificationFromGroupedNotification(
const std::string& group_parent_id,
const std::string& new_single_notification_id) {
auto* message_center = MessageCenter::Get();
MessageView* parent_view =
GetActiveNotificationViewController()->GetMessageViewForNotificationId(
group_parent_id);
auto* new_single_notification =
message_center->FindNotificationById(new_single_notification_id);
// These could already have been removed in case of a clear all action.
// Therefore, do not do anything if either of them has already been removed.
if (!parent_view || !new_single_notification)
return;
parent_view->RemoveGroupNotification(new_single_notification_id);
parent_view->UpdateWithNotification(*new_single_notification);
GetActiveNotificationViewController()
->ConvertGroupedNotificationViewToNotificationView(
/*grouped_notification_id=*/group_parent_id,
/*new_single_notification_id=*/new_single_notification_id);
message_center->FindNotificationById(group_parent_id)->ClearGroupParent();
new_single_notification->ClearGroupChild();
grouped_notification_list_->ClearGroupedNotification(group_parent_id);
message_center->RemoveNotification(group_parent_id, /*by_user=*/false);
}
std::unique_ptr<Notification>
NotificationGroupingController::CreateCopyForParentNotification(
const Notification& parent_notification) {
// Create a copy with a timestamp that is older than the copied notification.
// We need to set an older timestamp so that this notification will become
// the parent notification for it's notifier_id.
auto child_copy = std::make_unique<Notification>(
message_center::NotificationType::NOTIFICATION_TYPE_SIMPLE,
parent_notification.id() +
message_center::kIdSuffixForGroupContainerNotification,
parent_notification.title(), parent_notification.message(), gfx::Image(),
std::u16string(), parent_notification.origin_url(),
parent_notification.notifier_id(), message_center::RichNotificationData(),
/*delegate=*/nullptr);
child_copy->set_timestamp(parent_notification.timestamp() -
base::Milliseconds(1));
child_copy->SetGroupChild();
return child_copy;
}
void NotificationGroupingController::RemoveGroupedChild(
const std::string& notification_id) {
if (!grouped_notification_list_->GroupedChildNotificationExists(
notification_id)) {
return;
}
const std::string parent_id =
grouped_notification_list_->GetParentForChild(notification_id);
MessageView* parent_view =
GetActiveNotificationViewController()->GetMessageViewForNotificationId(
parent_id);
if (parent_view)
parent_view->RemoveGroupNotification(notification_id);
grouped_notification_list_->RemoveGroupedChildNotification(notification_id);
// Convert back to a single notification if there is only one
// group child left in the group notification.
auto grouped_notifications =
grouped_notification_list_->GetGroupedNotificationsForParent(parent_id);
if (GetActiveNotificationViewController()->GetMessageViewForNotificationId(
parent_id) &&
grouped_notifications.size() == 1) {
SetupSingleNotificationFromGroupedNotification(
/*group_parent_id=*/parent_id,
/*new_single_notification_id=*/*grouped_notifications.begin());
}
}
message_center::NotificationViewController*
NotificationGroupingController::GetActiveNotificationViewController() {
if (tray_->IsMessageCenterBubbleShown()) {
return tray_->message_center_bubble()
->message_center_view()
->message_list_view();
} else {
return tray_->GetMessagePopupCollection();
}
}
void NotificationGroupingController::OnNotificationAdded(
const std::string& notification_id) {
// Do not double process a notification that was re-added as a grouped parent.
if (adding_parent_grouped_notification_)
return;
auto* message_center = MessageCenter::Get();
Notification* notification =
message_center->FindNotificationById(notification_id);
// We only need to process notifications that are children of an
// existing group. So do nothing otherwise.
if (!notification)
return;
if (!notification->group_child())
return;
Notification* parent_notification =
message_center->FindParentNotificationForOriginUrl(
notification->origin_url());
std::string parent_id = parent_notification->id();
// If we are creating a new notification group for this `notifier_id`,
// we must create a copy of the designated parent notification and
// use it to set up a container notification which will hold all
// notifications for this group.
if (!grouped_notification_list_->ParentNotificationExists(parent_id))
SetupParentNotification(&parent_id);
grouped_notification_list_->AddGroupedNotification(notification_id,
parent_id);
MessageView* parent_view =
GetActiveNotificationViewController()->GetMessageViewForNotificationId(
parent_id);
if (parent_view)
parent_view->AddGroupNotification(*notification, /*newest_first=*/false);
else
message_center->ResetSinglePopup(parent_id);
}
void NotificationGroupingController::OnNotificationDisplayed(
const std::string& notification_id,
const message_center::DisplaySource source) {
if (grouped_notification_list_->ParentNotificationExists(notification_id))
PopulateGroupParent(notification_id);
}
void NotificationGroupingController::OnNotificationRemoved(
const std::string& notification_id,
bool by_user) {
if (grouped_notification_list_->GroupedChildNotificationExists(
notification_id)) {
RemoveGroupedChild(notification_id);
}
if (grouped_notification_list_->ParentNotificationExists(notification_id)) {
std::vector<std::string> to_be_deleted;
auto grouped_notifications =
grouped_notification_list_->GetGroupedNotificationsForParent(
notification_id);
std::copy(grouped_notifications.begin(), grouped_notifications.end(),
std::back_inserter(to_be_deleted));
grouped_notification_list_->ClearGroupedNotification(notification_id);
for (const auto& id : to_be_deleted)
MessageCenter::Get()->RemoveNotification(id, by_user);
}
}
} // namespace ash