// 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.
#include "chrome/browser/ui/webui/ash/settings/search/hierarchy.h"
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/webui/ash/settings/constants/constants_util.h"
#include "chrome/browser/ui/webui/ash/settings/pages/os_settings_section.h"
#include "chrome/browser/ui/webui/ash/settings/pages/os_settings_sections.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash::settings {
namespace mojom {
using ::chromeos::settings::mojom::Section;
using ::chromeos::settings::mojom::Setting;
using ::chromeos::settings::mojom::Subpage;
} // namespace mojom
namespace {
// Used to generate localized names.
constexpr double kDummyRelevanceScore = 0;
} // namespace
class Hierarchy::PerSectionHierarchyGenerator
: public OsSettingsSection::HierarchyGenerator {
public:
PerSectionHierarchyGenerator(mojom::Section section, Hierarchy* hierarchy)
: section_(section), hierarchy_(hierarchy) {}
void RegisterTopLevelSubpage(
int name_message_id,
mojom::Subpage subpage,
mojom::SearchResultIcon icon,
mojom::SearchResultDefaultRank default_rank,
const std::string& url_path_with_parameters) override {
Hierarchy::SubpageMetadata& metadata = GetSubpageMetadata(
name_message_id, subpage, icon, default_rank, url_path_with_parameters);
CHECK_EQ(section_, metadata.section)
<< "Subpage registered in multiple sections: " << subpage;
}
void RegisterNestedSubpage(
int name_message_id,
mojom::Subpage subpage,
mojom::Subpage parent_subpage,
mojom::SearchResultIcon icon,
mojom::SearchResultDefaultRank default_rank,
const std::string& url_path_with_parameters) override {
Hierarchy::SubpageMetadata& metadata = GetSubpageMetadata(
name_message_id, subpage, icon, default_rank, url_path_with_parameters);
CHECK_EQ(section_, metadata.section)
<< "Subpage registered in multiple sections: " << subpage;
CHECK(!metadata.parent_subpage)
<< "Subpage has multiple registered parent subpages: " << subpage;
metadata.parent_subpage = parent_subpage;
}
void RegisterTopLevelSetting(mojom::Setting setting) override {
Hierarchy::SettingMetadata& metadata = GetSettingMetadata(setting);
CHECK_EQ(section_, metadata.primary.section)
<< "Setting registered in multiple primary sections: " << setting;
CHECK(!metadata.primary.subpage)
<< "Setting registered in multiple primary locations: " << setting;
}
void RegisterNestedSetting(mojom::Setting setting,
mojom::Subpage subpage) override {
Hierarchy::SettingMetadata& metadata = GetSettingMetadata(setting);
CHECK_EQ(section_, metadata.primary.section)
<< "Setting registered in multiple primary sections: " << setting;
CHECK(!metadata.primary.subpage)
<< "Setting registered in multiple primary locations: " << setting;
metadata.primary.subpage = subpage;
}
void RegisterTopLevelAltSetting(mojom::Setting setting) override {
Hierarchy::SettingMetadata& metadata = GetSettingMetadata(setting);
CHECK(metadata.primary.section != section_ || metadata.primary.subpage)
<< "Setting's primary and alternate locations are identical: "
<< setting;
for (const auto& alternate : metadata.alternates) {
CHECK(alternate.section != section_ || alternate.subpage)
<< "Setting has multiple identical alternate locations: " << setting;
}
metadata.alternates.emplace_back(section_, /*subpage=*/std::nullopt);
}
void RegisterNestedAltSetting(mojom::Setting setting,
mojom::Subpage subpage) override {
Hierarchy::SettingMetadata& metadata = GetSettingMetadata(setting);
CHECK(metadata.primary.section != section_ ||
metadata.primary.subpage != subpage)
<< "Setting's primary and alternate locations are identical: "
<< setting;
for (const auto& alternate : metadata.alternates) {
CHECK(alternate.section != section_ || alternate.subpage != subpage)
<< "Setting has multiple identical alternate locations: " << setting;
}
metadata.alternates.emplace_back(section_, subpage);
}
private:
Hierarchy::SubpageMetadata& GetSubpageMetadata(
int name_message_id,
mojom::Subpage subpage,
mojom::SearchResultIcon icon,
mojom::SearchResultDefaultRank default_rank,
const std::string& url_path_with_parameters) {
auto& subpage_map = hierarchy_->subpage_map_;
auto it = subpage_map.find(subpage);
// Metadata already exists; return it.
if (it != subpage_map.end())
return it->second;
// Metadata does not exist yet; insert then return it.
auto pair = subpage_map.emplace(
std::piecewise_construct, std::forward_as_tuple(subpage),
std::forward_as_tuple(name_message_id, section_, subpage, icon,
default_rank, url_path_with_parameters,
hierarchy_));
CHECK(pair.second);
return pair.first->second;
}
Hierarchy::SettingMetadata& GetSettingMetadata(mojom::Setting setting) {
auto& settings_map = hierarchy_->setting_map_;
auto it = settings_map.find(setting);
// Metadata already exists; return it.
if (it != settings_map.end())
return it->second;
// Metadata does not exist yet; insert then return it.
auto pair = settings_map.emplace(setting, section_);
CHECK(pair.second);
return pair.first->second;
}
mojom::Section section_;
raw_ptr<Hierarchy> hierarchy_;
};
Hierarchy::SectionMetadata::SectionMetadata(mojom::Section section,
const Hierarchy* hierarchy)
: section_(section), hierarchy_(hierarchy) {}
Hierarchy::SectionMetadata::~SectionMetadata() = default;
mojom::SearchResultPtr Hierarchy::SectionMetadata::ToSearchResult(
double relevance_score) const {
return hierarchy_->sections_->GetSection(section_)
->GenerateSectionSearchResult(relevance_score);
}
Hierarchy::SubpageMetadata::SubpageMetadata(
int name_message_id,
mojom::Section section,
mojom::Subpage subpage,
mojom::SearchResultIcon icon,
mojom::SearchResultDefaultRank default_rank,
const std::string& url_path_with_parameters,
const Hierarchy* hierarchy)
: section(section),
subpage_(subpage),
name_message_id_(name_message_id),
icon_(icon),
default_rank_(default_rank),
unmodified_url_path_with_parameters_(url_path_with_parameters),
hierarchy_(hierarchy) {}
Hierarchy::SubpageMetadata::~SubpageMetadata() = default;
mojom::SearchResultPtr Hierarchy::SubpageMetadata::ToSearchResult(
double relevance_score) const {
return mojom::SearchResult::New(
/*text=*/l10n_util::GetStringUTF16(name_message_id_),
/*canonical_text=*/l10n_util::GetStringUTF16(name_message_id_),
hierarchy_->ModifySearchResultUrl(
section, mojom::SearchResultType::kSubpage, {.subpage = subpage_},
unmodified_url_path_with_parameters_),
icon_, relevance_score,
hierarchy_->GenerateAncestorHierarchyStrings(subpage_), default_rank_,
/*was_generated_from_text_match=*/false,
mojom::SearchResultType::kSubpage,
mojom::SearchResultIdentifier::NewSubpage(subpage_));
}
Hierarchy::SettingMetadata::SettingMetadata(mojom::Section primary_section)
: primary(primary_section, /*subpage=*/std::nullopt) {}
Hierarchy::SettingMetadata::~SettingMetadata() = default;
Hierarchy::Hierarchy(const OsSettingsSections* sections) : sections_(sections) {
for (const auto& section : AllSections()) {
auto pair = section_map_.insert({section, SectionMetadata(section, this)});
CHECK(pair.second);
PerSectionHierarchyGenerator generator(section, this);
sections->GetSection(section)->RegisterHierarchy(&generator);
}
}
Hierarchy::~Hierarchy() = default;
const Hierarchy::SectionMetadata& Hierarchy::GetSectionMetadata(
mojom::Section section) const {
const auto it = section_map_.find(section);
CHECK(it != section_map_.end())
<< "Section missing from settings hierarchy: " << section;
return it->second;
}
const Hierarchy::SubpageMetadata& Hierarchy::GetSubpageMetadata(
mojom::Subpage subpage) const {
const auto it = subpage_map_.find(subpage);
CHECK(it != subpage_map_.end())
<< "Subpage missing from settings hierarchy: " << subpage;
return it->second;
}
const Hierarchy::SettingMetadata& Hierarchy::GetSettingMetadata(
mojom::Setting setting) const {
const auto it = setting_map_.find(setting);
CHECK(it != setting_map_.end())
<< "Setting missing from settings hierarchy: " << setting;
return it->second;
}
std::string Hierarchy::ModifySearchResultUrl(
mojom::Section section,
mojom::SearchResultType type,
OsSettingsIdentifier id,
const std::string& url_to_modify) const {
return sections_->GetSection(section)->ModifySearchResultUrl(type, id,
url_to_modify);
}
std::vector<std::u16string> Hierarchy::GenerateAncestorHierarchyStrings(
mojom::Subpage subpage) const {
const SubpageMetadata& subpage_metadata = GetSubpageMetadata(subpage);
// Top-level subpage; simply return section hierarchy.
if (!subpage_metadata.parent_subpage)
return GenerateHierarchyStrings(subpage_metadata.section);
// Nested subpage; use recursive call, then append parent subpage name itself.
std::vector<std::u16string> hierarchy_strings =
GenerateAncestorHierarchyStrings(*subpage_metadata.parent_subpage);
hierarchy_strings.push_back(
GetSubpageMetadata(*subpage_metadata.parent_subpage)
.ToSearchResult(kDummyRelevanceScore)
->text);
return hierarchy_strings;
}
std::vector<std::u16string> Hierarchy::GenerateAncestorHierarchyStrings(
mojom::Setting setting) const {
const SettingMetadata& setting_metadata = GetSettingMetadata(setting);
// Top-level setting; simply return section hierarchy.
if (!setting_metadata.primary.subpage)
return GenerateHierarchyStrings(setting_metadata.primary.section);
// Nested setting; use subpage ancestors, then append subpage name itself.
std::vector<std::u16string> hierarchy_strings =
GenerateAncestorHierarchyStrings(*setting_metadata.primary.subpage);
hierarchy_strings.push_back(
GetSubpageMetadata(*setting_metadata.primary.subpage)
.ToSearchResult(kDummyRelevanceScore)
->text);
return hierarchy_strings;
}
std::vector<std::u16string> Hierarchy::GenerateHierarchyStrings(
mojom::Section section) const {
std::vector<std::u16string> hierarchy_strings;
hierarchy_strings.push_back(
l10n_util::GetStringUTF16(IDS_INTERNAL_APP_SETTINGS));
hierarchy_strings.push_back(
GetSectionMetadata(section).ToSearchResult(kDummyRelevanceScore)->text);
return hierarchy_strings;
}
#ifdef DCHECK
namespace {
// Number of spaces for each indent level.
constexpr int kIndent = 2;
void PrintSettings(std::ostream& os,
int indent,
std::vector<mojom::Setting>& collection) {
for (auto& it : collection) {
os << std::string(indent, ' ') << "(s)" << it << std::endl;
}
}
void PrintSubpages(
std::ostream& os,
int indent,
std::vector<mojom::Subpage>& collection,
std::map<mojom::Subpage, std::vector<mojom::Subpage>>& subpage_subpage,
std::map<mojom::Subpage, std::vector<mojom::Setting>>& subpage_setting) {
for (auto& it : collection) {
os << std::string(indent, ' ') << "(p)" << it << std::endl;
PrintSettings(os, indent + kIndent, subpage_setting[it]);
PrintSubpages(os, indent + kIndent, subpage_subpage[it], subpage_subpage,
subpage_setting);
}
}
} // namespace
std::ostream& operator<<(std::ostream& os, const Hierarchy& h) {
// This method logs all sections -> subpages -> settings
// First restructure the `Hierarchy` data into hierarchies we can work with.
std::map<mojom::Section, std::vector<mojom::Subpage>> section_subpage;
std::map<mojom::Section, std::vector<mojom::Setting>> section_setting;
std::map<mojom::Subpage, std::vector<mojom::Subpage>> subpage_subpage;
std::map<mojom::Subpage, std::vector<mojom::Setting>> subpage_setting;
std::vector<mojom::Subpage> none_exist_subpage;
std::vector<mojom::Setting> none_exist_setting;
for (auto& section_id : AllSections()) {
section_subpage.insert({section_id, {}});
section_setting.insert({section_id, {}});
}
for (auto& subpage_id : AllSubpages()) {
subpage_subpage.insert({subpage_id, {}});
subpage_setting.insert({subpage_id, {}});
if (!base::Contains(h.subpage_map_, subpage_id)) {
none_exist_subpage.push_back(subpage_id);
continue;
}
auto& subpage = h.GetSubpageMetadata(subpage_id);
if (subpage.parent_subpage) {
// if this is a nested subpage, only record the immediate parent subpage.
subpage_subpage[subpage.parent_subpage.value()].push_back(subpage_id);
} else {
section_subpage[subpage.section].push_back(subpage_id);
}
}
for (auto& setting_id : AllSettings()) {
if (!base::Contains(h.setting_map_, setting_id)) {
none_exist_setting.push_back(setting_id);
continue;
}
auto& setting = h.GetSettingMetadata(setting_id);
if (setting.primary.subpage) {
subpage_setting[setting.primary.subpage.value()].push_back(setting_id);
} else {
section_setting[setting.primary.section].push_back(setting_id);
}
for (auto& alt : setting.alternates) {
if (alt.subpage) {
subpage_setting[alt.subpage.value()].push_back(setting_id);
} else {
section_setting[alt.section].push_back(setting_id);
}
}
}
// Print out all the information.
os << "Settings Hierarchy:" << std::endl;
for (auto& section_id : AllSections()) {
auto& subpages = section_subpage[section_id];
auto& settings = section_setting[section_id];
os << "[" << section_id << "]" << std::endl;
PrintSubpages(os, kIndent, subpages, subpage_subpage, subpage_setting);
PrintSettings(os, kIndent, settings);
}
os << "Unused Subpages: " << std::endl;
PrintSubpages(os, kIndent, none_exist_subpage, subpage_subpage,
subpage_setting);
os << "Unused Settings: " << std::endl;
PrintSettings(os, kIndent, none_exist_setting);
return os;
}
#endif
} // namespace ash::settings