blob: c1fca28cd63c5d380cabdde750822c30064bb17f [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/skills/internal/skills_service_impl.h"
#include "base/check_is_test.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/task/single_thread_task_runner.h"
#include "base/uuid.h"
#include "components/optimization_guide/core/hints/optimization_guide_decider.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/skills/features.h"
#include "components/skills/internal/skills_downloader.h"
#include "components/skills/internal/skills_sync_bridge.h"
#include "components/skills/public/skill.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/report_unrecoverable_error.h"
#include "components/sync/model/client_tag_based_data_type_processor.h"
#include "components/sync/model/data_type_controller_delegate.h"
#include "components/sync/protocol/skill_specifics.pb.h"
namespace skills {
SkillsServiceImpl::SkillsServiceImpl(
optimization_guide::OptimizationGuideDecider* optimization_guide,
version_info::Channel channel,
syncer::OnceDataTypeStoreFactory create_store_callback,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
sync_bridge_ = std::make_unique<SkillsSyncBridge>(
std::make_unique<syncer::ClientTagBasedDataTypeProcessor>(
syncer::SKILL,
base::BindRepeating(&syncer::ReportUnrecoverableError, channel)),
std::move(create_store_callback), *this);
if (base::FeatureList::IsEnabled(features::kSkillsEnabled)) {
// If the Skills feature is enabled, register the optimization type to
// signal to Optimization Guide that it should fetch and cache the URL-keyed
// Skills on each page load.
if (optimization_guide) {
optimization_guide->RegisterOptimizationTypes(
{optimization_guide::proto::SKILLS});
}
}
skills_downloader_ =
std::make_unique<SkillsDownloader>(std::move(url_loader_factory));
}
SkillsServiceImpl::~SkillsServiceImpl() = default;
void SkillsServiceImpl::Shutdown() {
for (Observer& observer : observers_) {
observer.OnSkillsServiceShuttingDown();
}
}
void SkillsServiceImpl::NotifySkillChanged(std::string_view skill_id,
UpdateSource update_source) {
for (Observer& observer : observers_) {
observer.OnSkillUpdated(skill_id, update_source);
}
}
const Skill* SkillsServiceImpl::AddSkill(const std::string& source_skill_id,
const std::string& name,
const std::string& icon,
const std::string& prompt) {
if (GetServiceStatus() != ServiceStatus::kReady) {
return nullptr;
}
auto skill = std::make_unique<Skill>(
base::Uuid::GenerateRandomV4().AsLowercaseString(), name, icon, prompt);
skill->source_skill_id = source_skill_id;
// If the skill has a source skill id, it is a derived skill.
if (!source_skill_id.empty()) {
skill->source = sync_pb::SkillSource::SKILL_SOURCE_DERIVED_FROM_FIRST_PARTY;
}
return AddSkillImpl(std::move(skill), UpdateSource::kLocal);
}
const Skill* SkillsServiceImpl::AddOrUpdateSkillFromSync(
std::string_view skill_id,
std::string_view source_skill_id,
std::string_view name,
std::string_view icon,
std::string_view prompt,
std::string_view description,
base::Time creation_time,
base::Time last_update_time,
sync_pb::SkillSource source) {
CHECK_EQ(GetServiceStatus(), ServiceStatus::kReady);
if (Skill* skill = GetMutableSkillById(skill_id)) {
// Skill already exists, update its fields.
UpdateSkillImpl(skill, name, icon, prompt, description, last_update_time,
UpdateSource::kSync);
return skill;
}
auto skill = std::make_unique<Skill>(std::string(skill_id), std::string(name),
std::string(icon), std::string(prompt),
std::string(description));
skill->source_skill_id = source_skill_id;
// Use the creation and last update time from sync to keep them in sync with
// other clients.
skill->creation_time = creation_time;
skill->last_update_time = last_update_time;
skill->source = source;
return AddSkillImpl(std::move(skill), UpdateSource::kSync);
}
const Skill* SkillsServiceImpl::UpdateSkill(std::string_view skill_id,
std::string_view name,
std::string_view icon,
std::string_view prompt) {
if (GetServiceStatus() != ServiceStatus::kReady) {
return nullptr;
}
Skill* skill = GetMutableSkillById(skill_id);
if (!skill) {
// Skill does not exist, nothing to update.
return nullptr;
}
UpdateSkillImpl(skill, name, icon, prompt, /*description=*/"",
/*update_time=*/base::Time::Now(), UpdateSource::kLocal);
return skill;
}
void SkillsServiceImpl::DeleteSkill(std::string_view skill_id,
UpdateSource update_source) {
const std::string id_copy(skill_id);
const size_t num_erased =
std::erase_if(skills_, [&id_copy](const std::unique_ptr<Skill>& skill) {
return skill->id == id_copy;
});
if (num_erased > 0) {
NotifySkillChanged(id_copy, update_source);
}
}
const Skill* SkillsServiceImpl::GetSkillById(std::string_view skill_id) const {
for (const std::unique_ptr<Skill>& skill : skills_) {
if (skill->id == skill_id) {
return skill.get();
}
}
return nullptr;
}
const std::vector<std::unique_ptr<Skill>>& SkillsServiceImpl::GetSkills()
const {
return skills_;
}
const SkillsService::SkillsMap& SkillsServiceImpl::Get1PSkills() const {
return first_party_skills_map_;
}
void SkillsServiceImpl::LoadInitialSkills(
std::vector<std::unique_ptr<Skill>> initial_skills) {
CHECK(!is_initialized_);
skills_ = std::move(initial_skills);
SortSkills();
is_initialized_ = true;
for (Observer& observer : observers_) {
observer.OnStatusChanged();
}
}
SkillsService::ServiceStatus SkillsServiceImpl::GetServiceStatus() const {
if (service_status_for_testing_.has_value()) {
CHECK_IS_TEST();
return *service_status_for_testing_;
}
if (!is_initialized_) {
return ServiceStatus::kNotInitialized;
}
if (!sync_bridge_->change_processor()->IsTrackingMetadata()) {
return ServiceStatus::kInitializedWaitingForSyncReady;
}
return ServiceStatus::kReady;
}
void SkillsServiceImpl::SortSkills() {
std::sort(skills_.begin(), skills_.end(),
[](const std::unique_ptr<Skill>& a,
const std::unique_ptr<Skill>& b) { return a->name < b->name; });
}
void SkillsServiceImpl::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
observer->OnStatusChanged();
}
void SkillsServiceImpl::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
base::WeakPtr<syncer::DataTypeControllerDelegate>
SkillsServiceImpl::GetControllerDelegate() {
if (sync_bridge_) {
return sync_bridge_->change_processor()->GetControllerDelegate();
}
return nullptr;
}
void SkillsServiceImpl::SyncStatusChanged() {
for (Observer& observer : observers_) {
observer.OnStatusChanged();
}
}
void SkillsServiceImpl::SetServiceStatusForTesting(ServiceStatus status) {
service_status_for_testing_ = status;
for (Observer& observer : observers_) {
observer.OnStatusChanged();
}
}
const Skill* SkillsServiceImpl::AddSkillImpl(std::unique_ptr<Skill> skill,
UpdateSource update_source) {
// Added skill must not exist in the service.
CHECK(!GetSkillById(skill->id));
const Skill* skill_ptr = skill.get();
skills_.push_back(std::move(skill));
NotifySkillChanged(skill_ptr->id, update_source);
return skill_ptr;
}
void SkillsServiceImpl::FetchDiscoverySkills() {
if (!base::FeatureList::IsEnabled(features::kSkillsEnabled)) {
return;
}
skills_downloader_->FetchDiscoverySkills(base::BindOnce(
&SkillsServiceImpl::Handle1pSkillsMap, weak_ptr_factory_.GetWeakPtr()));
}
void SkillsServiceImpl::Handle1pSkillsMap(
std::unique_ptr<SkillsMap> skills_map) {
SkillsMap* notification_ptr = nullptr;
// If skills_map is null, this means we don't have an updated value so we
// shouldn't modify the stored 1p map.
if (skills_map) {
first_party_skills_map_.swap(*skills_map);
notification_ptr = &first_party_skills_map_;
}
for (Observer& observer : observers_) {
observer.OnDiscoverySkillsUpdated(notification_ptr);
}
}
Skill* SkillsServiceImpl::GetMutableSkillById(std::string_view skill_id) {
return const_cast<Skill*>(GetSkillById(skill_id));
}
void SkillsServiceImpl::UpdateSkillImpl(Skill* skill,
std::string_view name,
std::string_view icon,
std::string_view prompt,
std::string_view description,
base::Time update_time,
UpdateSource update_source) {
CHECK(skill);
// Update the existing skill.
bool is_changed = false;
if (skill->name != name) {
skill->name = name;
is_changed = true;
}
if (skill->icon != icon) {
skill->icon = icon;
is_changed = true;
}
if (skill->prompt != prompt) {
skill->prompt = prompt;
is_changed = true;
}
if (skill->description != description) {
skill->description = description;
is_changed = true;
}
if (update_source == UpdateSource::kSync &&
skill->last_update_time < update_time) {
// Mark the skill as changed to update its last update time and notify
// observers. This is relevant for sync updates only to keep the
// `last_update_time` in sync with other clients.
is_changed = true;
}
if (is_changed) {
skill->last_update_time = update_time;
NotifySkillChanged(skill->id, update_source);
}
}
} // namespace skills