[ads] Unregister resource components on shutdown and opt-out (#36769)

Resource components were registered with the component updater but never
explicitly unregistered, causing it to keep downloading updates for
country and language models even after the ads service shut down or the
user opted out of notification ads. Components are now unregistered in
OnPrefChanged when the service becomes ineligible, and the language
component is additionally unregistered immediately when the user opts
out of notification ads while the service is still running.
This commit is contained in:
Terry Mancey
2026-05-27 15:45:55 -05:00
committed by GitHub
parent fba382bb84
commit 898059b5a2
13 changed files with 305 additions and 33 deletions
+5
View File
@@ -113,6 +113,7 @@ source_set("test_support") {
"//brave/components/services/bat_ads/public/interfaces",
"//components/prefs",
"//mojo/public/cpp/bindings",
"//testing/gmock",
"//ui/base/idle",
"//url",
]
@@ -137,6 +138,10 @@ source_set("unit_tests") {
"ads_service_impl_unittest.cc",
"application_state/application_state_monitor_unittest.cc",
"component_updater/resource_component_registrar_unittest.cc",
"test/fake_component_updater_delegate.cc",
"test/fake_component_updater_delegate.h",
"test/mock_resource_component.cc",
"test/mock_resource_component.h",
]
deps = [
@@ -192,8 +192,8 @@ AdsServiceImpl::AdsServiceImpl(
// upgrades regardless of whether the service is eligible to start.
Migrate();
// Must be active regardless of whether the service starts so that
// pref-driven eligibility changes are always observed.
// Always registered to observe eligibility pref changes even when the
// service is not running.
InitializeLocalStatePrefChangeRegistrar();
InitializePrefChangeRegistrar();
@@ -209,15 +209,7 @@ AdsServiceImpl::~AdsServiceImpl() = default;
///////////////////////////////////////////////////////////////////////////////
void AdsServiceImpl::RegisterResourceComponents() {
RegisterCountryResourceComponent();
if (UserHasOptedInToNotificationAds()) {
// Only utilized for text classification, which requires the user to have
// joined Brave Rewards and opted into notification ads.
RegisterLanguageResourceComponent();
}
}
void AdsServiceImpl::Migrate() {
// Added 10/2025.
@@ -232,6 +224,16 @@ void AdsServiceImpl::Migrate() {
}
}
void AdsServiceImpl::RegisterResourceComponents() {
RegisterCountryResourceComponent();
if (UserHasOptedInToNotificationAds()) {
// Only utilized for text classification, which requires the user to have
// joined Brave Rewards and opted into notification ads.
RegisterLanguageResourceComponent();
}
}
void AdsServiceImpl::RegisterCountryResourceComponent() {
if (resource_component_) {
resource_component_->RegisterCountryComponent(
@@ -239,13 +241,25 @@ void AdsServiceImpl::RegisterCountryResourceComponent() {
}
}
void AdsServiceImpl::UnregisterCountryResourceComponent() {
resource_component_->UnregisterCountryComponent();
}
void AdsServiceImpl::RegisterLanguageResourceComponent() {
if (resource_component_) {
resource_component_->RegisterLanguageComponent(CurrentLanguageCode());
}
}
void AdsServiceImpl::UnregisterLanguageResourceComponent() {
resource_component_->UnregisterLanguageComponent();
}
bool AdsServiceImpl::UserHasJoinedBraveRewards() const {
if (prefs_->IsManagedPreference(brave_rewards::prefs::kDisabledByPolicy) &&
prefs_->GetBoolean(brave_rewards::prefs::kDisabledByPolicy)) {
return false;
}
return prefs_->GetBoolean(brave_rewards::prefs::kEnabled);
}
@@ -690,6 +704,11 @@ void AdsServiceImpl::InitializePrefChangeRegistrar() {
}
void AdsServiceImpl::InitializeBraveRewardsPrefChangeRegistrar() {
pref_change_registrar_.Add(
brave_rewards::prefs::kDisabledByPolicy,
base::BindRepeating(&AdsServiceImpl::OnAdsPrefChanged,
base::Unretained(this)));
pref_change_registrar_.Add(
brave_rewards::prefs::kEnabled,
base::BindRepeating(&AdsServiceImpl::NotifyPrefChanged,
@@ -749,18 +768,24 @@ void AdsServiceImpl::InitializeSearchResultAdsPrefChangeRegistrar() {
void AdsServiceImpl::OnAdsPrefChanged(const std::string& path) {
if (!CanStartBatAdsService()) {
// The pref change made the service ineligible to run, so tear it down.
// The pref change made the service ineligible to run, so tear it down and
// release resource components that are no longer needed.
UnregisterCountryResourceComponent();
UnregisterLanguageResourceComponent();
return ShutdownAdsService();
}
if (bat_ads_service_remote_.is_bound() && UserHasOptedInToNotificationAds() &&
if (bat_ads_service_remote_.is_bound() &&
path == prefs::kOptedInToNotificationAds) {
// Register language resource components if the user has joined Brave
// Rewards, opted into notification ads, and the Bat Ads Service has
// already started.
RegisterLanguageResourceComponent();
if (UserHasOptedInToNotificationAds()) {
// Register now that the user has opted in.
RegisterLanguageResourceComponent();
delegate_->MaybeInitNotificationHelper();
delegate_->MaybeInitNotificationHelper();
} else {
// Unregister now that the user has opted out.
UnregisterLanguageResourceComponent();
}
}
MaybeStartBatAdsService();
@@ -97,11 +97,11 @@ class AdsServiceImpl : public AdsService,
#endif
public content_settings::Observer {
public:
// `http_client`, `resource_component`, `history_service`, and
// `host_content_settings` can be `nullptr` in tests. `rewards_service`
// can be `nullptr` when Rewards is unsupported or disabled by policy.
// `policy_initialization_waiter` defers the initial ads-eligibility gate
// until the policy bundle has been merged into the managed pref store.
// `http_client`, `history_service`, and `host_content_settings` can be
// `nullptr` in tests. `rewards_service` can be `nullptr` when Rewards is
// unsupported or disabled by policy. `policy_initialization_waiter` defers
// the initial ads-eligibility gate until the policy bundle has been merged
// into the managed pref store.
explicit AdsServiceImpl(
std::unique_ptr<Delegate> delegate,
PrefService& prefs,
@@ -138,11 +138,13 @@ class AdsServiceImpl : public AdsService,
private:
friend class BraveAdsAdsServiceImplTest;
void Migrate();
void RegisterResourceComponents();
void RegisterCountryResourceComponent();
void UnregisterCountryResourceComponent();
void RegisterLanguageResourceComponent();
void Migrate();
void UnregisterLanguageResourceComponent();
bool UserHasJoinedBraveRewards() const;
bool UserHasOptedInToNewTabPageAds() const;
@@ -22,6 +22,7 @@
#include "brave/components/brave_ads/browser/test/fake_bat_ads_service_factory.h"
#include "brave/components/brave_ads/browser/test/fake_device_id.h"
#include "brave/components/brave_ads/browser/test/fake_virtual_pref_provider_delegate.h"
#include "brave/components/brave_ads/browser/test/mock_resource_component.h"
#include "brave/components/brave_ads/core/public/prefs/pref_names.h"
#include "brave/components/brave_ads/core/public/prefs/pref_registry.h"
#include "brave/components/brave_policy/policy_initialization_waiter.h"
@@ -92,7 +93,7 @@ class BraveAdsAdsServiceImplTest : public testing::Test {
std::make_unique<test::FakeVirtualPrefProviderDelegate>(),
/*channel_name=*/"foo", profile_dir_.GetPath(),
std::make_unique<test::FakeAdsTooltipsDelegate>(), std::move(device_id),
std::move(bat_ads_service_factory), /*resource_component=*/nullptr,
std::move(bat_ads_service_factory), &mock_resource_component_,
/*history_service=*/nullptr,
#if BUILDFLAG(ENABLE_BRAVE_REWARDS)
&rewards_service_,
@@ -139,6 +140,8 @@ class BraveAdsAdsServiceImplTest : public testing::Test {
test::FakeRewardsService rewards_service_;
#endif // BUILDFLAG(ENABLE_BRAVE_REWARDS)
testing::NiceMock<test::MockResourceComponent> mock_resource_component_;
std::unique_ptr<AdsServiceImpl> ads_service_;
};
@@ -636,4 +639,59 @@ TEST_F(BraveAdsAdsServiceImplTest,
EXPECT_THAT(prefs_.GetList(prefs::kNotificationAds), testing::IsEmpty());
}
#if BUILDFLAG(ENABLE_BRAVE_REWARDS)
TEST_F(BraveAdsAdsServiceImplTest,
RegistersLanguageResourceComponentWhenUserOptsInToNotificationAds) {
// Arrange: start the service via search result ads and wait for
// initialization so `bat_ads_service_remote_` is bound.
prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true);
prefs_.SetBoolean(brave_rewards::prefs::kEnabled, true);
prefs_.SetBoolean(prefs::kOptedInToNotificationAds, false);
Startup();
ASSERT_TRUE(base::test::RunUntil(
[&] { return bat_ads_service_factory_->initialize_count() == 1U; }));
EXPECT_CALL(mock_resource_component_, RegisterLanguageComponent);
// Act
prefs_.SetBoolean(prefs::kOptedInToNotificationAds, true);
}
TEST_F(BraveAdsAdsServiceImplTest,
UnregistersLanguageResourceComponentWhenUserOptsOutOfNotificationAds) {
// Arrange: start with notification ads opted in so the language component
// is already registered; service must be running before opting out.
prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true);
prefs_.SetBoolean(brave_rewards::prefs::kEnabled, true);
prefs_.SetBoolean(prefs::kOptedInToNotificationAds, true);
Startup();
ASSERT_TRUE(base::test::RunUntil(
[&] { return bat_ads_service_factory_->initialize_count() == 1U; }));
EXPECT_CALL(mock_resource_component_, UnregisterLanguageComponent());
// Act
prefs_.SetBoolean(prefs::kOptedInToNotificationAds, false);
}
#endif // BUILDFLAG(ENABLE_BRAVE_REWARDS)
#if !BUILDFLAG(IS_ANDROID)
TEST_F(BraveAdsAdsServiceImplTest,
UnregistersResourceComponentsWhenServiceBecomesIneligible) {
// Arrange: start the service so resource components are registered; then
// disable ads via policy to trigger unregistration.
prefs_.SetBoolean(prefs::kOptedInToSearchResultAds, true);
Startup();
ASSERT_TRUE(base::test::RunUntil(
[&] { return bat_ads_service_factory_->initialize_count() == 1U; }));
EXPECT_CALL(mock_resource_component_, UnregisterCountryComponent());
EXPECT_CALL(mock_resource_component_, UnregisterLanguageComponent());
// Act
prefs_.SetManagedPref(brave_rewards::prefs::kDisabledByPolicy,
base::Value(true));
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace brave_ads
@@ -76,12 +76,20 @@ void ResourceComponent::RegisterCountryComponent(
country_resource_component_registrar_.RegisterResourceComponent(country_code);
}
void ResourceComponent::UnregisterCountryComponent() {
country_resource_component_registrar_.UnregisterResourceComponent();
}
void ResourceComponent::RegisterLanguageComponent(
const std::string& language_code) {
language_resource_component_registrar_.RegisterResourceComponent(
language_code);
}
void ResourceComponent::UnregisterLanguageComponent() {
language_resource_component_registrar_.UnregisterResourceComponent();
}
std::optional<base::FilePath> ResourceComponent::MaybeGetPath(
const std::string& id,
int version) {
@@ -127,8 +135,6 @@ void ResourceComponent::OnResourceComponentUnregistered(
void ResourceComponent::LoadManifestCallback(const std::string& component_id,
const base::FilePath& install_dir,
const std::string& json) {
VLOG(8) << "Manifest JSON: " << json;
std::optional<base::DictValue> dict =
base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC);
if (!dict) {
@@ -155,8 +161,6 @@ void ResourceComponent::LoadResourceCallback(
const std::string& component_id,
const base::FilePath& install_dir,
const std::string& json) {
VLOG(8) << "Resource JSON: " << json;
std::optional<base::DictValue> root =
base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC);
if (!root) {
@@ -28,7 +28,7 @@ static_assert(BUILDFLAG(ENABLE_BRAVE_ADS));
namespace brave_ads {
class ResourceComponent final : public ResourceComponentRegistrarDelegate {
class ResourceComponent : public ResourceComponentRegistrarDelegate {
public:
explicit ResourceComponent(
brave_component_updater::BraveComponent::Delegate* delegate);
@@ -41,8 +41,10 @@ class ResourceComponent final : public ResourceComponentRegistrarDelegate {
void AddObserver(ResourceComponentObserver* observer);
void RemoveObserver(ResourceComponentObserver* observer);
void RegisterCountryComponent(const std::string& country_code);
void RegisterLanguageComponent(const std::string& language_code);
virtual void RegisterCountryComponent(const std::string& country_code);
virtual void UnregisterCountryComponent();
virtual void RegisterLanguageComponent(const std::string& language_code);
virtual void UnregisterLanguageComponent();
std::optional<base::FilePath> MaybeGetPath(const std::string& id,
int version);
@@ -68,6 +68,17 @@ void ResourceComponentRegistrar::RegisterResourceComponent(
}
}
void ResourceComponentRegistrar::UnregisterResourceComponent() {
if (!resource_component_id_) {
return;
}
Unregister();
OnComponentUnregistered(*resource_component_id_);
last_install_dir_.reset();
resource_component_id_.reset();
}
///////////////////////////////////////////////////////////////////////////////
void ResourceComponentRegistrar::OnComponentReady(
@@ -32,6 +32,7 @@ class ResourceComponentRegistrar final
~ResourceComponentRegistrar() override;
void RegisterResourceComponent(const std::string& resource_id);
void UnregisterResourceComponent();
private:
// brave_component_updater::BraveComponent:
@@ -0,0 +1,44 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/components/brave_ads/browser/test/fake_component_updater_delegate.h"
#include "base/strings/string_util.h"
namespace brave_ads::test {
FakeComponentUpdaterDelegate::FakeComponentUpdaterDelegate() = default;
FakeComponentUpdaterDelegate::~FakeComponentUpdaterDelegate() = default;
void FakeComponentUpdaterDelegate::Register(
const std::string& /*component_name*/,
const std::string& /*component_base64_public_key*/,
base::OnceClosure registered_callback,
brave_component_updater::BraveComponent::ReadyCallback /*ready_callback*/) {
if (registered_callback) {
std::move(registered_callback).Run();
}
}
bool FakeComponentUpdaterDelegate::Unregister(
const std::string& /*component_id*/) {
return true;
}
scoped_refptr<base::SequencedTaskRunner>
FakeComponentUpdaterDelegate::GetTaskRunner() {
return nullptr;
}
const std::string& FakeComponentUpdaterDelegate::locale() const {
return base::EmptyString();
}
PrefService* FakeComponentUpdaterDelegate::local_state() {
return nullptr;
}
} // namespace brave_ads::test
@@ -0,0 +1,59 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_
#define BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_
#include <string>
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "brave/components/brave_component_updater/browser/brave_component.h"
class PrefService;
namespace brave_ads::test {
// Minimal no-op `BraveComponent::Delegate` for use in unit tests that need a
// `ResourceComponent` without a real component updater.
class FakeComponentUpdaterDelegate final
: public brave_component_updater::BraveComponent::Delegate {
public:
FakeComponentUpdaterDelegate();
FakeComponentUpdaterDelegate(const FakeComponentUpdaterDelegate&) = delete;
FakeComponentUpdaterDelegate& operator=(const FakeComponentUpdaterDelegate&) =
delete;
~FakeComponentUpdaterDelegate() override;
void Register(const std::string& /*component_name*/,
const std::string& /*component_base64_public_key*/,
base::OnceClosure registered_callback,
brave_component_updater::BraveComponent::ReadyCallback
/*ready_callback*/) override;
bool Unregister(const std::string& /*component_id*/) override;
void EnsureInstalled(const std::string& /*component_id*/) override {}
void AddObserver(brave_component_updater::BraveComponent::ComponentObserver*
/*observer*/) override {}
void RemoveObserver(
brave_component_updater::BraveComponent::ComponentObserver*
/*observer*/) override {}
scoped_refptr<base::SequencedTaskRunner> GetTaskRunner() override;
const std::string& locale() const override;
PrefService* local_state() override;
};
} // namespace brave_ads::test
#endif // BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_FAKE_COMPONENT_UPDATER_DELEGATE_H_
@@ -288,7 +288,10 @@ void FakeRewardsService::GetEventLogs(
void FakeRewardsService::GetRewardsWallet(
brave_rewards::GetRewardsWalletCallback callback) {
std::move(callback).Run(nullptr);
auto wallet = brave_rewards::mojom::RewardsWallet::New();
wallet->payment_id = "foo";
wallet->recovery_seed = std::vector<uint8_t>(32, 0);
std::move(callback).Run(std::move(wallet));
}
void FakeRewardsService::GetEnvironment(
@@ -0,0 +1,15 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/components/brave_ads/browser/test/mock_resource_component.h"
namespace brave_ads::test {
MockResourceComponent::MockResourceComponent()
: ResourceComponent(&fake_component_updater_delegate_) {}
MockResourceComponent::~MockResourceComponent() = default;
} // namespace brave_ads::test
@@ -0,0 +1,43 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_
#define BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_
#include <string>
#include "brave/components/brave_ads/browser/component_updater/resource_component.h"
#include "brave/components/brave_ads/browser/test/fake_component_updater_delegate.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace brave_ads::test {
class MockResourceComponent : public ResourceComponent {
public:
MockResourceComponent();
MockResourceComponent(const MockResourceComponent&) = delete;
MockResourceComponent& operator=(const MockResourceComponent&) = delete;
~MockResourceComponent() override;
MOCK_METHOD(void,
RegisterCountryComponent,
(const std::string& country_code),
(override));
MOCK_METHOD(void, UnregisterCountryComponent, (), (override));
MOCK_METHOD(void,
RegisterLanguageComponent,
(const std::string& language_code),
(override));
MOCK_METHOD(void, UnregisterLanguageComponent, (), (override));
private:
FakeComponentUpdaterDelegate fake_component_updater_delegate_;
};
} // namespace brave_ads::test
#endif // BRAVE_COMPONENTS_BRAVE_ADS_BROWSER_TEST_MOCK_RESOURCE_COMPONENT_H_