// 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