chromium/chrome/browser/ui/webui/ash/settings/search/search_tag_registry.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ui/webui/ash/settings/search/search_tag_registry.h"

#include <algorithm>
#include <sstream>

#include "base/feature_list.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/webui/ash/settings/search/search_concept.h"
#include "chromeos/ash/components/local_search_service/public/cpp/local_search_service_proxy.h"
#include "ui/base/l10n/l10n_util.h"

namespace ash::settings {

namespace {

std::vector<int> GetMessageIds(const SearchConcept* search_concept) {
  // Start with only the canonical ID.
  std::vector<int> alt_tag_message_ids{search_concept->canonical_message_id};

  // Add alternate IDs, if they exist.
  for (size_t i = 0; i < SearchConcept::kMaxAltTagsPerConcept; ++i) {
    int curr_alt_tag_message_id = search_concept->alt_tag_ids[i];
    if (curr_alt_tag_message_id == SearchConcept::kAltTagEnd) {
      break;
    }
    alt_tag_message_ids.push_back(curr_alt_tag_message_id);
  }

  return alt_tag_message_ids;
}

}  // namespace

SearchTagRegistry::ScopedTagUpdater::ScopedTagUpdater(
    SearchTagRegistry* registry)
    : registry_(registry) {}

SearchTagRegistry::ScopedTagUpdater::ScopedTagUpdater(ScopedTagUpdater&&) =
    default;

SearchTagRegistry::ScopedTagUpdater::~ScopedTagUpdater() {
  std::vector<const SearchConcept*> pending_adds;
  std::vector<const SearchConcept*> pending_removals;

  for (const auto& map_entry : pending_updates_) {
    const std::string& result_id = map_entry.first;
    const SearchConcept* search_concept = map_entry.second.first;
    bool is_pending_add = map_entry.second.second;

    // If tag metadata is present for this tag, it has already been added and is
    // present in LocalSearchService.
    bool is_concept_already_added =
        registry_->GetTagMetadata(result_id) != nullptr;

    // Only add concepts which are intended to be added and have not yet been
    // added; only remove concepts which are intended to be removed and have
    // already been added.
    if (is_pending_add && !is_concept_already_added) {
      pending_adds.push_back(search_concept);
    }
    if (!is_pending_add && is_concept_already_added) {
      pending_removals.push_back(search_concept);
    }
  }

  if (!pending_adds.empty()) {
    registry_->AddSearchTags(pending_adds);
  }
  if (!pending_removals.empty()) {
    registry_->RemoveSearchTags(pending_removals);
  }
}

void SearchTagRegistry::ScopedTagUpdater::AddSearchTags(
    const std::vector<SearchConcept>& search_tags) {
  ProcessPendingSearchTags(search_tags, /*is_pending_add=*/true);
}

void SearchTagRegistry::ScopedTagUpdater::RemoveSearchTags(
    const std::vector<SearchConcept>& search_tags) {
  ProcessPendingSearchTags(search_tags, /*is_pending_add=*/false);
}

void SearchTagRegistry::ScopedTagUpdater::ProcessPendingSearchTags(
    const std::vector<SearchConcept>& search_tags,
    bool is_pending_add) {
  for (const auto& search_concept : search_tags) {
    std::string result_id = ToResultId(search_concept);
    auto it = pending_updates_.find(result_id);
    if (it == pending_updates_.end()) {
      pending_updates_.emplace(
          std::piecewise_construct, std::forward_as_tuple(result_id),
          std::forward_as_tuple(&search_concept, is_pending_add));
    } else {
      it->second.second = is_pending_add;
    }
  }
}

SearchTagRegistry::SearchTagRegistry(
    local_search_service::LocalSearchServiceProxy* local_search_service_proxy) {
  local_search_service_proxy->GetIndex(
      local_search_service::IndexId::kCrosSettings,
      local_search_service::Backend::kLinearMap,
      index_remote_.BindNewPipeAndPassReceiver());
  DCHECK(index_remote_.is_bound());
}

SearchTagRegistry::~SearchTagRegistry() = default;

void SearchTagRegistry::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void SearchTagRegistry::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

SearchTagRegistry::ScopedTagUpdater SearchTagRegistry::StartUpdate() {
  return ScopedTagUpdater(this);
}

void SearchTagRegistry::AddSearchTags(
    const std::vector<const SearchConcept*>& search_tags) {
  // Add each concept to the map. Note that it is safe to take the address of
  // each concept because all concepts are allocated via static
  // base::NoDestructor objects in the Get*SearchConcepts() helper functions.
  for (const auto* search_concept : search_tags) {
    result_id_to_metadata_list_map_[ToResultId(*search_concept)] =
        search_concept;
  }

  index_remote_->AddOrUpdate(
      ConceptVectorToDataVector(search_tags),
      base::BindOnce(&SearchTagRegistry::NotifyRegistryAdded,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SearchTagRegistry::RemoveSearchTags(
    const std::vector<const SearchConcept*>& search_tags) {
  std::vector<std::string> data_ids;
  for (const auto* search_concept : search_tags) {
    std::string result_id = ToResultId(*search_concept);
    result_id_to_metadata_list_map_.erase(result_id);
    data_ids.push_back(std::move(result_id));
  }

  index_remote_->Delete(
      data_ids, base::BindOnce(&SearchTagRegistry::NotifyRegistryDeleted,
                               weak_ptr_factory_.GetWeakPtr()));
}

const SearchConcept* SearchTagRegistry::GetTagMetadata(
    const std::string& result_id) const {
  const auto it = result_id_to_metadata_list_map_.find(result_id);
  if (it == result_id_to_metadata_list_map_.end()) {
    return nullptr;
  }
  return it->second;
}

// static
std::string SearchTagRegistry::ToResultId(const SearchConcept& search_concept) {
  std::stringstream ss;
  switch (search_concept.type) {
    case mojom::SearchResultType::kSection:
      ss << search_concept.id.section;
      break;
    case mojom::SearchResultType::kSubpage:
      ss << search_concept.id.subpage;
      break;
    case mojom::SearchResultType::kSetting:
      ss << search_concept.id.setting;
      break;
  }
  ss << "," << search_concept.canonical_message_id;
  return ss.str();
}

std::vector<local_search_service::Data>
SearchTagRegistry::ConceptVectorToDataVector(
    const std::vector<const SearchConcept*>& search_tags) {
  std::vector<local_search_service::Data> data_list;

  for (const auto* search_concept : search_tags) {
    // Create a list of Content objects, which use the stringified version of
    // message IDs as identifiers.
    std::vector<local_search_service::Content> content_list;
    for (int message_id : GetMessageIds(search_concept)) {
      content_list.emplace_back(
          /*id=*/base::NumberToString(message_id),
          /*content=*/l10n_util::GetStringUTF16(message_id));
    }

    // Compute an identifier for this result; the same ID format it used in
    // GetTagMetadata().
    data_list.emplace_back(ToResultId(*search_concept),
                           std::move(content_list));
  }

  return data_list;
}

void SearchTagRegistry::NotifyRegistryUpdated() {
  for (auto& observer : observer_list_) {
    observer.OnRegistryUpdated();
  }
}

void SearchTagRegistry::NotifyRegistryAdded() {
  NotifyRegistryUpdated();
}

void SearchTagRegistry::NotifyRegistryDeleted(uint32_t num_deleted) {
  NotifyRegistryUpdated();
}

}  // namespace ash::settings