| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/webui/help_app_ui/search/search_concept.h" |
| |
| #include <cstddef> |
| #include <iostream> |
| #include <memory> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| |
| namespace ash::help_app { |
| |
| namespace { |
| |
| using Concept = SearchConceptProto::Concept; |
| |
| constexpr char kReadHistogram[] = |
| "Discover.SearchConcept.PersistenceReadStatus"; |
| constexpr char kWriteHistogram[] = |
| "Discover.SearchConcept.PersistenceWriteStatus"; |
| |
| // The result of reading a backing file from disk. These values persist to logs. |
| // Entries should not be renumbered and numeric values should never be reused. |
| enum class ReadStatus { |
| kOk = 0, |
| kMissing = 1, |
| kReadError = 2, |
| kParseError = 3, |
| kMaxValue = kParseError, |
| }; |
| |
| // The result of writing a backing file to disk. These values persist to logs. |
| // Entries should not be renumbered and numeric values should never be reused. |
| enum class WriteStatus { |
| kOk = 0, |
| kWriteError = 1, |
| kSerializationError = 2, |
| kReplaceError = 3, |
| kMaxValue = kReplaceError, |
| }; |
| |
| // This should be incremented whenever a change to the search concept is made |
| // that is incompatible with on-disk state. On reading, any state is wiped if |
| // its version doesn't match. |
| constexpr int32_t kVersion = 1; |
| |
| // Read proto from the disk. |
| std::unique_ptr<SearchConceptProto> ProtoRead(const base::FilePath& file_path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| if (!base::PathExists(file_path)) { |
| base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kMissing); |
| return nullptr; |
| } |
| |
| std::string proto_str; |
| if (!base::ReadFileToString(file_path, &proto_str)) { |
| base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kReadError); |
| return nullptr; |
| } |
| |
| auto proto = std::make_unique<SearchConceptProto>(); |
| if (!proto->ParseFromString(proto_str)) { |
| base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kParseError); |
| return nullptr; |
| } |
| base::UmaHistogramEnumeration(kReadHistogram, ReadStatus::kOk); |
| |
| // Discard the proto if the version does not match. |
| if (!proto->has_version() || proto->version() != kVersion) { |
| base::DeleteFile(file_path); |
| return nullptr; |
| } |
| |
| return proto; |
| } |
| |
| // Write proto to the disk. |
| void ProtoWrite(std::unique_ptr<SearchConceptProto> proto, |
| const base::FilePath& file_path, |
| const base::FilePath& temp_file_path) { |
| std::string proto_str; |
| if (!proto->SerializeToString(&proto_str)) { |
| base::UmaHistogramEnumeration(kWriteHistogram, |
| WriteStatus::kSerializationError); |
| return; |
| } |
| |
| const auto directory = temp_file_path.DirName(); |
| if (!base::DirectoryExists(directory)) { |
| base::CreateDirectory(directory); |
| } |
| |
| // Write temporary proto to `temp_file_path_`. |
| bool write_succeed; |
| { |
| base::ScopedBlockingCall scoped_blocking_call( |
| FROM_HERE, base::BlockingType::MAY_BLOCK); |
| write_succeed = base::ImportantFileWriter::WriteFileAtomically( |
| temp_file_path, proto_str, "HelpAppPersistentProto"); |
| } |
| |
| if (!write_succeed) { |
| base::UmaHistogramEnumeration(kWriteHistogram, WriteStatus::kWriteError); |
| return; |
| } |
| |
| // Replace the proto in `file_path_` by the temporary proto if the write is |
| // succeed. |
| const bool replace_succeed = |
| base::ReplaceFile(temp_file_path, file_path, nullptr); |
| base::UmaHistogramEnumeration( |
| kWriteHistogram, |
| replace_succeed ? WriteStatus::kOk : WriteStatus::kReplaceError); |
| } |
| |
| } // namespace |
| |
| SearchConcept::SearchConcept(const base::FilePath& file_path) |
| : file_path_(file_path), |
| temp_file_path_(file_path.DirName().AppendASCII("tmp.pb")), |
| task_runner_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::TaskPriority::BEST_EFFORT, base::MayBlock(), |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {} |
| |
| SearchConcept::~SearchConcept() = default; |
| |
| void SearchConcept::GetSearchConcepts(ReadCallback on_read) { |
| task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&ProtoRead, file_path_), |
| base::BindOnce(&SearchConcept::OnProtoRead, weak_factory_.GetWeakPtr(), |
| base::BindPostTaskToCurrentDefault(std::move(on_read)))); |
| } |
| |
| void SearchConcept::UpdateSearchConcepts( |
| const std::vector<mojom::SearchConceptPtr>& search_concepts) { |
| // Ignore the request if the SearchConcepts is empty. |
| if (search_concepts.empty()) { |
| return; |
| } |
| |
| std::unique_ptr<SearchConceptProto> proto = |
| std::make_unique<SearchConceptProto>(); |
| proto->set_version(kVersion); |
| auto& proto_concepts = *proto->mutable_concepts(); |
| |
| for (const auto& search_concept : search_concepts) { |
| Concept& proto_concept = *proto_concepts.Add(); |
| proto_concept.set_id(search_concept->id); |
| proto_concept.set_title(base::UTF16ToUTF8(search_concept->title)); |
| proto_concept.set_main_category( |
| base::UTF16ToUTF8(search_concept->main_category)); |
| |
| auto& proto_tags = *proto_concept.mutable_tags(); |
| for (const auto& search_tag : search_concept->tags) { |
| proto_tags.Add(base::UTF16ToUTF8(search_tag)); |
| } |
| |
| proto_concept.set_tag_locale(search_concept->tag_locale); |
| proto_concept.set_url_path_with_parameters( |
| search_concept->url_path_with_parameters); |
| proto_concept.set_locale(search_concept->locale); |
| } |
| |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&ProtoWrite, std::move(proto), file_path_, |
| temp_file_path_)); |
| } |
| |
| void SearchConcept::OnProtoRead(ReadCallback on_read, |
| std::unique_ptr<SearchConceptProto> proto) { |
| std::vector<mojom::SearchConceptPtr> search_concepts; |
| if (!proto) { |
| std::move(on_read).Run(std::move(search_concepts)); |
| return; |
| } |
| |
| const auto proto_concepts = proto->concepts(); |
| |
| if (proto_concepts.empty()) { |
| std::move(on_read).Run(std::move(search_concepts)); |
| return; |
| } |
| |
| // convert the concepts of proto into |
| // `std::vector<mojom::SearchConceptPtr>`. |
| for (const auto& proto_concept : proto_concepts) { |
| mojom::SearchConceptPtr search_concept = mojom::SearchConcept::New(); |
| search_concept->id = proto_concept.id(); |
| search_concept->title = base::UTF8ToUTF16(proto_concept.title()); |
| search_concept->main_category = |
| base::UTF8ToUTF16(proto_concept.main_category()); |
| |
| std::vector<::std::u16string> search_tags; |
| for (const auto& tag : proto_concept.tags()) { |
| search_tags.push_back(base::UTF8ToUTF16(tag)); |
| } |
| search_concept->tags = search_tags; |
| |
| search_concept->tag_locale = proto_concept.tag_locale(); |
| search_concept->url_path_with_parameters = |
| proto_concept.url_path_with_parameters(); |
| search_concept->locale = proto_concept.locale(); |
| |
| search_concepts.push_back(std::move(search_concept)); |
| } |
| |
| std::move(on_read).Run(std::move(search_concepts)); |
| } |
| |
| } // namespace ash::help_app |