chromium/chrome/services/mac_notifications/notification_category_manager.mm

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/services/mac_notifications/notification_category_manager.h"

#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/uuid.h"
#include "chrome/grit/generated_resources.h"
#import "chrome/services/mac_notifications/mac_notification_service_utils.h"
#include "ui/base/l10n/l10n_util_mac.h"

namespace mac_notifications {

namespace {

UNNotificationAction* CreateAction(
    const NotificationCategoryManager::Button& button,
    NSString* identifier) {
  if (button.second) {
    return [UNTextInputNotificationAction
        actionWithIdentifier:identifier
                       title:base::SysUTF16ToNSString(button.first)
                     options:UNNotificationActionOptionNone
        textInputButtonTitle:base::SysUTF16ToNSString(button.first)
        textInputPlaceholder:base::SysUTF16ToNSString(*button.second)];
  }

  return [UNNotificationAction
      actionWithIdentifier:identifier
                     title:base::SysUTF16ToNSString(button.first)
                   options:UNNotificationActionOptionNone];
}

NotificationCategoryManager::Button GetButtonFromAction(
    UNNotificationAction* action) {
  std::u16string title = base::SysNSStringToUTF16([action title]);
  std::optional<std::u16string> placeholder;

  if ([action isKindOfClass:[UNTextInputNotificationAction class]]) {
    auto* text_action = static_cast<UNTextInputNotificationAction*>(action);
    placeholder = base::SysNSStringToUTF16([text_action textInputPlaceholder]);
  }

  return {title, placeholder};
}

}  // namespace

NotificationCategoryManager::NotificationCategoryManager(
    UNUserNotificationCenter* notification_center)
    : notification_center_(notification_center) {}

NotificationCategoryManager::~NotificationCategoryManager() = default;

void NotificationCategoryManager::InitializeExistingCategories(
    NSArray<UNNotification*>* notifications,
    NSSet<UNNotificationCategory*>* categories) {
  base::flat_map<std::string, UNNotificationCategory*> category_map;

  // Setup map from category ID to category for faster lookup.
  for (UNNotificationCategory* category in categories) {
    std::string category_id = base::SysNSStringToUTF8(category.identifier);
    category_map.emplace(category_id, category);
  }

  // Setup links from notifications to categories and count how many times each
  // category is used.
  for (UNNotification* notification in notifications) {
    std::string notification_id =
        base::SysNSStringToUTF8([[notification request] identifier]);
    std::string category_id = base::SysNSStringToUTF8(
        notification.request.content.categoryIdentifier);

    if (notification_id_buttons_map_.count(notification_id))
      continue;

    auto entry = category_map.find(category_id);
    if (entry == category_map.end())
      continue;

    // Link |notification_id| to |category_key|.
    auto category_key = GetCategoryKey(entry->second);
    notification_id_buttons_map_.emplace(notification_id, category_key);

    // Increment refcount for |category_key|.
    auto existing = buttons_category_map_.find(category_key);
    if (existing != buttons_category_map_.end()) {
      ++existing->second.second;
    } else {
      CategoryEntry category_entry(entry->second, /*refcount=*/1);
      buttons_category_map_.emplace(category_key, std::move(category_entry));
    }
  }
}

NSString* NotificationCategoryManager::GetOrCreateCategory(
    const std::string& notification_id,
    const Buttons& buttons,
    bool settings_button) {
  // Update category associations for the given |notification_id|.
  ReleaseCategories({notification_id});
  auto category_key = std::make_pair(buttons, settings_button);
  notification_id_buttons_map_.emplace(notification_id, category_key);

  // Try to find an existing category with the given buttons.
  auto existing = buttons_category_map_.find(category_key);
  if (existing != buttons_category_map_.end()) {
    UNNotificationCategory* category = existing->second.first;
    int& refcount = existing->second.second;
    // Increment refcount so we keep the category alive.
    ++refcount;
    return [category identifier];
  }

  // Create a new category with the given buttons.
  UNNotificationCategory* category = CreateCategory(category_key);
  CategoryEntry category_entry(category, /*refcount=*/1);
  buttons_category_map_.emplace(category_key, std::move(category_entry));

  UpdateNotificationCenterCategories();

  return category.identifier;
}

void NotificationCategoryManager::ReleaseCategories(
    const std::vector<std::string>& notification_ids) {
  bool needs_update = false;

  for (const auto& notification_id : notification_ids) {
    auto existing_key = notification_id_buttons_map_.find(notification_id);
    if (existing_key == notification_id_buttons_map_.end())
      continue;

    CategoryKey category_key = std::move(existing_key->second);
    notification_id_buttons_map_.erase(existing_key);

    auto existing_entry = buttons_category_map_.find(category_key);
    if (existing_entry == buttons_category_map_.end())
      continue;

    // Decrement refcount and cleanup the category if unused.
    int& refcount = existing_entry->second.second;
    --refcount;
    if (refcount > 0)
      continue;

    buttons_category_map_.erase(existing_entry);
    needs_update = true;
  }

  if (needs_update)
    UpdateNotificationCenterCategories();
}

void NotificationCategoryManager::ReleaseAllCategories() {
  if (buttons_category_map_.empty() && notification_id_buttons_map_.empty())
    return;

  buttons_category_map_.clear();
  notification_id_buttons_map_.clear();
  UpdateNotificationCenterCategories();
}

void NotificationCategoryManager::UpdateNotificationCenterCategories() {
  NSMutableSet* categories = [[NSMutableSet alloc] init];
  for (const auto& entry : buttons_category_map_)
    [categories addObject:entry.second.first];

  [notification_center_ setNotificationCategories:categories];
}

UNNotificationCategory* NotificationCategoryManager::CreateCategory(
    const CategoryKey& key) {
  const NotificationCategoryManager::Buttons& buttons = key.first;
  bool settings_button = key.second;
  NSMutableArray* buttons_array = [NSMutableArray arrayWithCapacity:4];

  // We only support up to two user action buttons.
  DCHECK_LE(buttons.size(), 2u);
  if (buttons.size() >= 1u)
    [buttons_array addObject:CreateAction(buttons[0], kNotificationButtonOne)];
  if (buttons.size() >= 2u)
    [buttons_array addObject:CreateAction(buttons[1], kNotificationButtonTwo)];

  if (settings_button) {
    UNNotificationAction* button = [UNNotificationAction
        actionWithIdentifier:kNotificationSettingsButtonTag
                       title:l10n_util::GetNSString(
                                 IDS_NOTIFICATION_BUTTON_SETTINGS)
                     options:UNNotificationActionOptionNone];
    [buttons_array addObject:button];
  }

  NSString* category_id = base::SysUTF8ToNSString(
      base::Uuid::GenerateRandomV4().AsLowercaseString());

  UNNotificationCategory* category = [UNNotificationCategory
      categoryWithIdentifier:category_id
                     actions:buttons_array
           intentIdentifiers:@[]
                     options:UNNotificationCategoryOptionCustomDismissAction];

  // This uses a private API to change the text of the actions menu title so
  // that it is consistent with the rest of the notification buttons
  if ([category respondsToSelector:@selector(actionsMenuTitle)]) {
    [category setValue:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_MORE)
                forKey:@"_actionsMenuTitle"];
  }

  return category;
}

NotificationCategoryManager::CategoryKey
NotificationCategoryManager::GetCategoryKey(UNNotificationCategory* category) {
  Buttons buttons;
  bool settings_button = false;

  for (UNNotificationAction* action in category.actions) {
    NSString* identifier = action.identifier;
    if ([kNotificationSettingsButtonTag isEqualToString:identifier]) {
      settings_button = true;
    } else if ([kNotificationButtonOne isEqualToString:identifier] ||
               [kNotificationButtonTwo isEqualToString:identifier]) {
      buttons.push_back(GetButtonFromAction(action));
    }
  }

  return std::make_pair(std::move(buttons), settings_button);
}

}  // namespace mac_notifications