[3PCD] Check profile eligible in Eligiblity Service constructor

When the eligibility service is created, if the client eligibility is
unknown yet, consult the privacy sandbox settings for the profile
eligibility and broadcast to the experiment manager.

Bug: b/300490678
Change-Id: Ia46550051feabf1d3441200d6adc222b47f67dea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4885227
Reviewed-by: Anton Maliev <[email protected]>
Commit-Queue: Nan Lin <[email protected]>
Reviewed-by: Ryan Tarpine <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1202224}
diff --git a/chrome/browser/tpcd/experiment/BUILD.gn b/chrome/browser/tpcd/experiment/BUILD.gn
index f9ada77a..0e49c21 100644
--- a/chrome/browser/tpcd/experiment/BUILD.gn
+++ b/chrome/browser/tpcd/experiment/BUILD.gn
@@ -4,12 +4,17 @@
 
 source_set("unit_tests") {
   testonly = true
-  sources = [ "experiment_manager_unittest.cc" ]
+  sources = [
+    "eligibility_service_unittest.cc",
+    "experiment_manager_impl_unittest.cc",
+  ]
 
   deps = [
     "//base",
     "//chrome/browser",
     "//chrome/test:test_support",
+    "//components/privacy_sandbox:privacy_sandbox_settings_headers",
+    "//components/privacy_sandbox:test_support",
     "//content/test:test_support",
     "//testing/gtest",
   ]
diff --git a/chrome/browser/tpcd/experiment/eligibility_service.cc b/chrome/browser/tpcd/experiment/eligibility_service.cc
index 7906f92..10f1b10 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service.cc
+++ b/chrome/browser/tpcd/experiment/eligibility_service.cc
@@ -6,10 +6,12 @@
 
 #include "base/check.h"
 #include "base/functional/bind.h"
+#include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h"
 #include "chrome/browser/privacy_sandbox/tracking_protection_onboarding_factory.h"
 #include "chrome/browser/tpcd/experiment/eligibility_service_factory.h"
 #include "chrome/browser/tpcd/experiment/experiment_manager.h"
 #include "chrome/browser/tpcd/experiment/tpcd_experiment_features.h"
+#include "components/privacy_sandbox/privacy_sandbox_settings.h"
 #include "content/public/browser/cookie_deprecation_label_manager.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/common/content_features.h"
@@ -21,20 +23,14 @@
 EligibilityService::EligibilityService(Profile* profile,
                                        ExperimentManager* experiment_manager)
     : profile_(profile),
-      pref_service_(profile->GetPrefs()),
       onboarding_service_(
           TrackingProtectionOnboardingFactory::GetForProfile(profile_)),
       experiment_manager_(experiment_manager) {
   CHECK(base::FeatureList::IsEnabled(
       features::kCookieDeprecationFacilitatedTesting));
-  CHECK(pref_service_);
   CHECK(experiment_manager_);
 
-  absl::optional<bool> is_client_eligible =
-      experiment_manager_->IsClientEligible();
-  if (is_client_eligible.has_value()) {
-    MarkProfileEligibility(is_client_eligible.value());
-  }
+  BroadcastProfileEligibility();
 }
 
 EligibilityService::~EligibilityService() = default;
@@ -48,6 +44,20 @@
   onboarding_service_ = nullptr;
 }
 
+void EligibilityService::BroadcastProfileEligibility() {
+  absl::optional<bool> is_client_eligible =
+      experiment_manager_->IsClientEligible();
+  if (is_client_eligible.has_value()) {
+    MarkProfileEligibility(is_client_eligible.value());
+    return;
+  }
+
+  experiment_manager_->SetClientEligibility(
+      IsProfileEligible(),
+      base::BindOnce(&EligibilityService::OnClientEligibilityChanged,
+                     weak_factory_.GetWeakPtr()));
+}
+
 void EligibilityService::OnClientEligibilityChanged(bool is_eligible) {
   // For each storage partition, update the cookie deprecation label to the
   // updated value from the CookieDeprecationLabelManager.
@@ -75,4 +85,13 @@
   }
 }
 
+bool EligibilityService::IsProfileEligible() {
+  auto* privacy_sandbox_settings =
+      PrivacySandboxSettingsFactory::GetForProfile(profile_);
+  CHECK(privacy_sandbox_settings);
+
+  return privacy_sandbox_settings
+      ->IsCookieDeprecationExperimentCurrentlyEligible();
+}
+
 }  // namespace tpcd::experiment
diff --git a/chrome/browser/tpcd/experiment/eligibility_service.h b/chrome/browser/tpcd/experiment/eligibility_service.h
index 1c0f40f..fa049da 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service.h
+++ b/chrome/browser/tpcd/experiment/eligibility_service.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_TPCD_EXPERIMENT_ELIGIBILITY_SERVICE_H_
 #define CHROME_BROWSER_TPCD_EXPERIMENT_ELIGIBILITY_SERVICE_H_
 
+#include "base/memory/weak_ptr.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/tpcd/experiment/eligibility_service_factory.h"
 #include "components/keyed_service/core/keyed_service.h"
@@ -12,7 +13,6 @@
 namespace privacy_sandbox {
 class TrackingProtectionOnboarding;
 }
-class PrefService;
 
 namespace tpcd::experiment {
 
@@ -20,8 +20,7 @@
 
 class EligibilityService : public KeyedService {
  public:
-  explicit EligibilityService(Profile* profile,
-                              ExperimentManager* experiment_manager);
+  EligibilityService(Profile* profile, ExperimentManager* experiment_manager);
   EligibilityService(const EligibilityService&) = delete;
   EligibilityService& operator=(const EligibilityService&) = delete;
   ~EligibilityService() override;
@@ -42,12 +41,16 @@
   // MarkProfileEligibility should be called for all profiles to set their
   // eligibility, whether currently loaded or created later.
   void MarkProfileEligibility(bool is_eligible);
+  void BroadcastProfileEligibility();
+  bool IsProfileEligible();
 
   raw_ptr<Profile> profile_;
-  raw_ptr<PrefService> pref_service_;
   // onboarding_service_ may be null for OTR and system profiles.
   raw_ptr<privacy_sandbox::TrackingProtectionOnboarding> onboarding_service_;
+  // `ExperimentManager` is a singleton and lives forever.
   raw_ptr<ExperimentManager> experiment_manager_;
+
+  base::WeakPtrFactory<EligibilityService> weak_factory_{this};
 };
 
 }  // namespace tpcd::experiment
diff --git a/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc b/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
index 13fffb0..9f5fdd6 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
+++ b/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
@@ -7,18 +7,27 @@
 #include <utility>
 
 #include "base/containers/contains.h"
+#include "base/files/file_path.h"
 #include "base/strings/strcat.h"
 #include "base/test/scoped_feature_list.h"
+#include "build/buildflag.h"
+#include "build/chromeos_buildflags.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h"
 #include "chrome/browser/privacy_sandbox/tracking_protection_onboarding_factory.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/profiles/profile_test_util.h"
 #include "chrome/browser/tpcd/experiment/eligibility_service.h"
 #include "chrome/browser/tpcd/experiment/eligibility_service_factory.h"
 #include "chrome/browser/tpcd/experiment/tpcd_experiment_features.h"
+#include "chrome/browser/tpcd/experiment/tpcd_pref_names.h"
+#include "chrome/browser/tpcd/experiment/tpcd_utils.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/prefs/pref_service.h"
 #include "components/privacy_sandbox/privacy_sandbox_settings.h"
 #include "components/privacy_sandbox/privacy_sandbox_test_util.h"
 #include "components/privacy_sandbox/tracking_protection_onboarding.h"
@@ -58,25 +67,32 @@
  protected:
   std::string GetDisable3PCookies() { return GetParam() ? "true" : "false"; }
 
-  void AddImageToDocument(const GURL& src_url) {
+  Profile* GenerateNewProfile() {
+    ProfileManager* profile_manager = g_browser_process->profile_manager();
+    base::FilePath current_profile_path = browser()->profile()->GetPath();
+
+    // Create an additional profile.
+    base::FilePath new_path =
+        profile_manager->GenerateNextProfileDirectoryPath();
+
+    return &profiles::testing::CreateProfileSync(profile_manager, new_path);
+  }
+
+  void AddImageToDocument(Browser* browser, const GURL& src_url) {
     ASSERT_EQ(true,
-              EvalJs(GetActiveWebContents(),
+              EvalJs(GetActiveWebContents(browser),
                      base::StrCat({"((() => { const img = "
                                    "document.createElement('img'); img.src = '",
                                    src_url.spec(), "'; return true; })())"})));
   }
 
-  void FlushNetworkInterface() {
-    browser()
-        ->profile()
-        ->GetDefaultStoragePartition()
-        ->FlushNetworkInterfaceForTesting();
+  void FlushNetworkInterface(Profile* profile) {
+    profile->GetDefaultStoragePartition()->FlushNetworkInterfaceForTesting();
   }
 
-  void FireOnClientEligibilityChanged(bool is_eligible) {
+  void FireOnClientEligibilityChanged(Profile* profile, bool is_eligible) {
     auto* eligibility_service =
-        tpcd::experiment::EligibilityServiceFactory::GetForProfile(
-            browser()->profile());
+        tpcd::experiment::EligibilityServiceFactory::GetForProfile(profile);
     eligibility_service->OnClientEligibilityChanged(is_eligible);
   }
 
@@ -84,13 +100,16 @@
       net::test_server::EmbeddedTestServer::TYPE_HTTPS};
 
  private:
-  content::WebContents* GetActiveWebContents() {
-    return browser()->tab_strip_model()->GetActiveWebContents();
+  content::WebContents* GetActiveWebContents(Browser* browser) {
+    return browser->tab_strip_model()->GetActiveWebContents();
   }
 
   base::test::ScopedFeatureList feature_list_;
 };
 
+// This test creates a new profile to avoid being flaky, CrOS multi-profiles
+// implementation is too different for this test.
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
 IN_PROC_BROWSER_TEST_P(EligibilityServiceBrowserTest,
                        EligibilityChanged_NetworkContextUpdated) {
   auto response_b_a =
@@ -104,8 +123,16 @@
           &https_server_, "/b_c");
   ASSERT_TRUE(https_server_.Start());
 
+  // Sets up local state pref and creates a new profile to avoid flaky tests.
+  g_browser_process->local_state()->SetInteger(
+      prefs::kTPCDExperimentClientState,
+      static_cast<int>(utils::ExperimentState::kIneligible));
+
+  Profile* profile = GenerateNewProfile();
+  Browser* current_browser = CreateBrowser(profile);
+
   auto* privacy_sandbox_settings =
-      PrivacySandboxSettingsFactory::GetForProfile(browser()->profile());
+      PrivacySandboxSettingsFactory::GetForProfile(profile);
   auto privacy_sandbox_delegate = std::make_unique<
       privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>();
   EXPECT_CALL(*privacy_sandbox_delegate, IsCookieDeprecationExperimentEligible)
@@ -119,15 +146,15 @@
 
   ASSERT_FALSE(privacy_sandbox_settings->IsCookieDeprecationLabelAllowed());
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/true);
+  FireOnClientEligibilityChanged(profile, /*is_eligible=*/false);
 
   // Ensures the cookie deprecation label is updated in the network context.
-  FlushNetworkInterface();
+  FlushNetworkInterface(profile);
 
   ASSERT_TRUE(ui_test_utils::NavigateToURL(
-      browser(), https_server_.GetURL("a.test", "/title1.html")));
+      current_browser, https_server_.GetURL("a.test", "/title1.html")));
 
-  AddImageToDocument(https_server_.GetURL("b.test", "/b_a"));
+  AddImageToDocument(current_browser, https_server_.GetURL("b.test", "/b_a"));
 
   // [b.test/a] - Non opted-in request should not receive a label header.
   response_b_a->WaitForRequest();
@@ -154,12 +181,12 @@
 
   ASSERT_TRUE(privacy_sandbox_settings->IsCookieDeprecationLabelAllowed());
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/false);
+  FireOnClientEligibilityChanged(profile, /*is_eligible=*/true);
 
   // Ensures the cookie deprecation label is updated in the network context.
-  FlushNetworkInterface();
+  FlushNetworkInterface(profile);
 
-  AddImageToDocument(https_server_.GetURL("b.test", "/b_c"));
+  AddImageToDocument(current_browser, https_server_.GetURL("b.test", "/b_c"));
 
   // [b.test/c] - Opted-in request should receive a label header if allowed.
   response_b_c->WaitForRequest();
@@ -167,14 +194,18 @@
                              "Sec-Cookie-Deprecation"));
   EXPECT_EQ(response_b_c->http_request()->headers.at("Sec-Cookie-Deprecation"),
             "label_test");
+
+  g_browser_process->local_state()->ClearPref(
+      prefs::kTPCDExperimentClientState);
 }
+#endif  // !BUILDFLAG(IS_CHROMEOS_ASH)
 
 IN_PROC_BROWSER_TEST_P(EligibilityServiceBrowserTest,
                        EligibilityChanged_OnboardingServiceNotified) {
   privacy_sandbox::TrackingProtectionOnboarding* onboarding_service =
       TrackingProtectionOnboardingFactory::GetForProfile(browser()->profile());
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/true);
+  FireOnClientEligibilityChanged(browser()->profile(), /*is_eligible=*/true);
 
   // Onboarding should be marked eligible for the cohort where 3PCD is enabled,
   // but not when 3PCD is disabled.
@@ -184,7 +215,7 @@
                        : privacy_sandbox::TrackingProtectionOnboarding::
                              OnboardingStatus::kIneligible);
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/false);
+  FireOnClientEligibilityChanged(browser()->profile(), /*is_eligible=*/false);
 
   EXPECT_EQ(onboarding_service->GetOnboardingStatus(),
             privacy_sandbox::TrackingProtectionOnboarding::OnboardingStatus::
diff --git a/chrome/browser/tpcd/experiment/eligibility_service_factory.cc b/chrome/browser/tpcd/experiment/eligibility_service_factory.cc
index 4a129198..47df0d2 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service_factory.cc
+++ b/chrome/browser/tpcd/experiment/eligibility_service_factory.cc
@@ -9,7 +9,7 @@
 #include "chrome/browser/privacy_sandbox/tracking_protection_onboarding_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/tpcd/experiment/eligibility_service.h"
-#include "chrome/browser/tpcd/experiment/experiment_manager.h"
+#include "chrome/browser/tpcd/experiment/experiment_manager_impl.h"
 #include "content/public/common/content_features.h"
 
 namespace tpcd::experiment {
@@ -47,7 +47,8 @@
     return nullptr;
   }
   return std::make_unique<EligibilityService>(
-      Profile::FromBrowserContext(context), ExperimentManager::GetInstance());
+      Profile::FromBrowserContext(context),
+      ExperimentManagerImpl::GetInstance());
 }
 
 }  // namespace tpcd::experiment
diff --git a/chrome/browser/tpcd/experiment/eligibility_service_unittest.cc b/chrome/browser/tpcd/experiment/eligibility_service_unittest.cc
new file mode 100644
index 0000000..9c4cec97
--- /dev/null
+++ b/chrome/browser/tpcd/experiment/eligibility_service_unittest.cc
@@ -0,0 +1,109 @@
+// 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 "chrome/browser/tpcd/experiment/eligibility_service.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/memory/raw_ptr.h"
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h"
+#include "chrome/browser/tpcd/experiment/experiment_manager.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/privacy_sandbox/privacy_sandbox_settings.h"
+#include "components/privacy_sandbox/privacy_sandbox_test_util.h"
+#include "content/public/common/content_features.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace tpcd::experiment {
+
+namespace {
+
+using ::testing::_;
+using ::testing::Return;
+
+class MockExperimentManager : public ExperimentManager {
+ public:
+  MockExperimentManager() = default;
+  ~MockExperimentManager() override = default;
+
+  MOCK_METHOD(void,
+              SetClientEligibility,
+              (bool, EligibilityDecisionCallback),
+              (override));
+  MOCK_METHOD(absl::optional<bool>, IsClientEligible, (), (const, override));
+};
+
+}  // namespace
+
+class EligibilityServiceTest : public testing::Test {
+ public:
+  EligibilityServiceTest() {
+    feature_list_.InitAndEnableFeature(
+        features::kCookieDeprecationFacilitatedTesting);
+  }
+
+  void SetUp() override {
+    experiment_manager_ = std::make_unique<MockExperimentManager>();
+
+    auto* privacy_sandbox_settings =
+        PrivacySandboxSettingsFactory::GetForProfile(&profile_);
+    auto privacy_sandbox_delegate = std::make_unique<
+        privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>();
+    privacy_sandbox_delegate_ = privacy_sandbox_delegate.get();
+    privacy_sandbox_settings->SetDelegateForTesting(
+        std::move(privacy_sandbox_delegate));
+  }
+
+ protected:
+  content::BrowserTaskEnvironment browser_task_environment_;
+  TestingProfile profile_;
+  std::unique_ptr<MockExperimentManager> experiment_manager_;
+  raw_ptr<privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>
+      privacy_sandbox_delegate_;
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(EligibilityServiceTest, ClientEligibilityKnown_ClientEligibilityNotSet) {
+  EXPECT_CALL(*experiment_manager_, IsClientEligible).WillOnce(Return(false));
+  EXPECT_CALL(*experiment_manager_, SetClientEligibility).Times(0);
+
+  EligibilityService eligibility_service(&profile_, experiment_manager_.get());
+}
+
+TEST_F(EligibilityServiceTest,
+       ClientEligibilityUnknownProfileIneligible_ClientEligibilitySet) {
+  EXPECT_CALL(*experiment_manager_, IsClientEligible)
+      .WillOnce(Return(absl::nullopt));
+
+  EXPECT_CALL(*privacy_sandbox_delegate_,
+              IsCookieDeprecationExperimentCurrentlyEligible)
+      .WillOnce(Return(false));
+
+  EXPECT_CALL(*experiment_manager_, SetClientEligibility(false, _));
+
+  EligibilityService eligibility_service(&profile_, experiment_manager_.get());
+}
+
+TEST_F(EligibilityServiceTest,
+       ClientEligibilityUnknownProfileEligible_ClientEligibilitySet) {
+  EXPECT_CALL(*experiment_manager_, IsClientEligible)
+      .WillOnce(Return(absl::nullopt));
+
+  EXPECT_CALL(*privacy_sandbox_delegate_,
+              IsCookieDeprecationExperimentCurrentlyEligible)
+      .WillOnce(Return(true));
+
+  EXPECT_CALL(*experiment_manager_, SetClientEligibility(true, _));
+
+  EligibilityService eligibility_service(&profile_, experiment_manager_.get());
+}
+
+}  // namespace tpcd::experiment
diff --git a/chrome/browser/tpcd/experiment/experiment_manager.h b/chrome/browser/tpcd/experiment/experiment_manager.h
index 16d88bf2..9d82a123 100644
--- a/chrome/browser/tpcd/experiment/experiment_manager.h
+++ b/chrome/browser/tpcd/experiment/experiment_manager.h
@@ -5,49 +5,29 @@
 #ifndef CHROME_BROWSER_TPCD_EXPERIMENT_EXPERIMENT_MANAGER_H_
 #define CHROME_BROWSER_TPCD_EXPERIMENT_EXPERIMENT_MANAGER_H_
 
-#include <vector>
-
 #include "base/functional/callback_forward.h"
-#include "base/no_destructor.h"
-#include "base/sequence_checker.h"
-#include "base/thread_annotations.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace tpcd::experiment {
 
-// Can only be used on the main thread.
 class ExperimentManager {
  public:
   using EligibilityDecisionCallback = base::OnceCallback<void(bool)>;
 
-  static ExperimentManager* GetInstance();
+  ExperimentManager() = default;
+  virtual ~ExperimentManager() = default;
 
   // Called by `EligibilityService` to tell the manager whether a profile is
   // eligible, with a callback to complete the profile-level work required once
-  // the final decision is made. The final decision is recorded in a local state
-  // pref. If this is called after the final decision is made, the local state
-  // pref value takes precedence and the callbacks are still performed.
-  void SetClientEligibility(
+  // the final decision is made.
+  virtual void SetClientEligibility(
       bool is_eligible,
-      EligibilityDecisionCallback on_eligibility_decision_callback);
+      EligibilityDecisionCallback on_eligibility_decision_callback) = 0;
 
   // Returns the final decision for client eligibility, if completed.
   // `absl::nullopt` will be returned if the final decision has not been made
   // yet.
-  absl::optional<bool> IsClientEligible() const;
-
- protected:
-  ExperimentManager();
-  ~ExperimentManager();
-
- private:
-  friend base::NoDestructor<ExperimentManager>;
-  bool client_is_eligible_ GUARDED_BY_CONTEXT(sequence_checker_) = true;
-  std::vector<EligibilityDecisionCallback> callbacks_
-      GUARDED_BY_CONTEXT(sequence_checker_);
-  SEQUENCE_CHECKER(sequence_checker_);
-
-  void CaptureEligibilityInLocalStatePref();
+  virtual absl::optional<bool> IsClientEligible() const = 0;
 };
 
 }  // namespace tpcd::experiment
diff --git a/chrome/browser/tpcd/experiment/experiment_manager.cc b/chrome/browser/tpcd/experiment/experiment_manager_impl.cc
similarity index 86%
rename from chrome/browser/tpcd/experiment/experiment_manager.cc
rename to chrome/browser/tpcd/experiment/experiment_manager_impl.cc
index 003fc06..cb03f79 100644
--- a/chrome/browser/tpcd/experiment/experiment_manager.cc
+++ b/chrome/browser/tpcd/experiment/experiment_manager_impl.cc
@@ -2,9 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include <utility>
+#include "chrome/browser/tpcd/experiment/experiment_manager_impl.h"
 
-#include "chrome/browser/tpcd/experiment/experiment_manager.h"
+#include <utility>
 
 #include "base/check.h"
 #include "base/feature_list.h"
@@ -27,17 +27,17 @@
 namespace tpcd::experiment {
 
 // static
-ExperimentManager* ExperimentManager::GetInstance() {
+ExperimentManagerImpl* ExperimentManagerImpl::GetInstance() {
   if (!base::FeatureList::IsEnabled(
           features::kCookieDeprecationFacilitatedTesting)) {
     return nullptr;
   }
 
-  static base::NoDestructor<ExperimentManager> instance;
+  static base::NoDestructor<ExperimentManagerImpl> instance;
   return instance.get();
 }
 
-ExperimentManager::ExperimentManager() {
+ExperimentManagerImpl::ExperimentManagerImpl() {
   CHECK(base::FeatureList::IsEnabled(
       features::kCookieDeprecationFacilitatedTesting));
 
@@ -62,22 +62,21 @@
   // to use `base::Unretained()`.
   base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
       FROM_HERE,
-      base::BindOnce(&ExperimentManager::CaptureEligibilityInLocalStatePref,
+      base::BindOnce(&ExperimentManagerImpl::CaptureEligibilityInLocalStatePref,
                      base::Unretained(this)),
       kDecisionDelayTime.Get());
 }
 
-ExperimentManager::~ExperimentManager() {
+ExperimentManagerImpl::~ExperimentManagerImpl() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 }
 
-void ExperimentManager::SetClientEligibility(
+void ExperimentManagerImpl::SetClientEligibility(
     bool is_eligible,
     EligibilityDecisionCallback on_eligibility_decision_callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
-  if (absl::optional<bool> client_is_eligible = IsClientEligible();
-      client_is_eligible.has_value()) {
+  if (absl::optional<bool> client_is_eligible = IsClientEligible()) {
     // If client eligibility is already known, just run callback.
     client_is_eligible_ = *client_is_eligible;
     std::move(on_eligibility_decision_callback).Run(client_is_eligible_);
@@ -90,7 +89,7 @@
   callbacks_.push_back(std::move(on_eligibility_decision_callback));
 }
 
-void ExperimentManager::CaptureEligibilityInLocalStatePref() {
+void ExperimentManagerImpl::CaptureEligibilityInLocalStatePref() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   g_browser_process->local_state()->SetInteger(
       prefs::kTPCDExperimentClientState,
@@ -103,7 +102,7 @@
   callbacks_.clear();
 }
 
-absl::optional<bool> ExperimentManager::IsClientEligible() const {
+absl::optional<bool> ExperimentManagerImpl::IsClientEligible() const {
   switch (g_browser_process->local_state()->GetInteger(
       prefs::kTPCDExperimentClientState)) {
     case static_cast<int>(utils::ExperimentState::kEligible):
diff --git a/chrome/browser/tpcd/experiment/experiment_manager_impl.h b/chrome/browser/tpcd/experiment/experiment_manager_impl.h
new file mode 100644
index 0000000..25136fd
--- /dev/null
+++ b/chrome/browser/tpcd/experiment/experiment_manager_impl.h
@@ -0,0 +1,49 @@
+// 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.
+
+#ifndef CHROME_BROWSER_TPCD_EXPERIMENT_EXPERIMENT_MANAGER_IMPL_H_
+#define CHROME_BROWSER_TPCD_EXPERIMENT_EXPERIMENT_MANAGER_IMPL_H_
+
+#include <vector>
+
+#include "base/functional/callback_forward.h"
+#include "base/no_destructor.h"
+#include "base/sequence_checker.h"
+#include "base/thread_annotations.h"
+#include "chrome/browser/tpcd/experiment/experiment_manager.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace tpcd::experiment {
+
+// Can only be used on the main thread.
+class ExperimentManagerImpl : public ExperimentManager {
+ public:
+  static ExperimentManagerImpl* GetInstance();
+
+  // The final decision is recorded in a local state pref. If this is called
+  // after the final decision is made, the local state pref value takes
+  // precedence and the callbacks are still performed.
+  void SetClientEligibility(
+      bool is_eligible,
+      EligibilityDecisionCallback on_eligibility_decision_callback) override;
+
+  absl::optional<bool> IsClientEligible() const override;
+
+ protected:
+  ExperimentManagerImpl();
+  ~ExperimentManagerImpl() override;
+
+ private:
+  friend base::NoDestructor<ExperimentManagerImpl>;
+  bool client_is_eligible_ GUARDED_BY_CONTEXT(sequence_checker_) = true;
+  std::vector<EligibilityDecisionCallback> callbacks_
+      GUARDED_BY_CONTEXT(sequence_checker_);
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  void CaptureEligibilityInLocalStatePref();
+};
+
+}  // namespace tpcd::experiment
+
+#endif  // CHROME_BROWSER_TPCD_EXPERIMENT_EXPERIMENT_MANAGER_IMPL_H_
diff --git a/chrome/browser/tpcd/experiment/experiment_manager_unittest.cc b/chrome/browser/tpcd/experiment/experiment_manager_impl_unittest.cc
similarity index 83%
rename from chrome/browser/tpcd/experiment/experiment_manager_unittest.cc
rename to chrome/browser/tpcd/experiment/experiment_manager_impl_unittest.cc
index 640fdeb..c7b1ccfe 100644
--- a/chrome/browser/tpcd/experiment/experiment_manager_unittest.cc
+++ b/chrome/browser/tpcd/experiment/experiment_manager_impl_unittest.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/tpcd/experiment/experiment_manager.h"
+#include "chrome/browser/tpcd/experiment/experiment_manager_impl.h"
 
 #include "base/test/bind.h"
 #include "base/test/mock_callback.h"
@@ -22,7 +22,7 @@
 namespace tpcd::experiment {
 namespace {
 
-class TestingExperimentManager : public ExperimentManager {};
+class TestingExperimentManagerImpl : public ExperimentManagerImpl {};
 
 using ::testing::InSequence;
 using ::testing::Optional;
@@ -31,9 +31,9 @@
 
 }  // namespace
 
-class ExperimentManagerTestBase : public testing::Test {
+class ExperimentManagerImplTestBase : public testing::Test {
  public:
-  ExperimentManagerTestBase()
+  ExperimentManagerImplTestBase()
       : local_state_(TestingBrowserProcess::GetGlobal()) {}
 
   PrefService& prefs() { return *local_state_.Get(); }
@@ -54,7 +54,7 @@
   base::TimeDelta delay_time_;
 };
 
-TEST_F(ExperimentManagerTestBase, Version) {
+TEST_F(ExperimentManagerImplTestBase, Version) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndEnableFeatureWithParameters(
       features::kCookieDeprecationFacilitatedTesting, {{"version", "2"}});
@@ -110,7 +110,7 @@
       prefs().ClearPref(prefs::kTPCDExperimentClientState);
     }
 
-    TestingExperimentManager experiment_manager;
+    TestingExperimentManagerImpl experiment_manager;
 
     EXPECT_EQ(prefs().GetInteger(prefs::kTPCDExperimentClientStateVersion),
               test_case.expected_version);
@@ -119,9 +119,9 @@
   }
 }
 
-class ExperimentManagerTest : public ExperimentManagerTestBase {
+class ExperimentManagerImplTest : public ExperimentManagerImplTestBase {
  public:
-  ExperimentManagerTest() {
+  ExperimentManagerImplTest() {
     feature_list_.InitAndEnableFeature(
         features::kCookieDeprecationFacilitatedTesting);
   }
@@ -130,9 +130,9 @@
   base::test::ScopedFeatureList feature_list_;
 };
 
-TEST_F(ExperimentManagerTest,
+TEST_F(ExperimentManagerImplTest,
        ExperimentManager_OneEligibleProfileCallSetsPrefEligible) {
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/true, mock_callback_.Get());
   EXPECT_CALL(mock_callback_, Run(true)).Times(1);
   task_environment_.FastForwardBy(delay_time_);
@@ -142,9 +142,9 @@
 }
 
 TEST_F(
-    ExperimentManagerTest,
+    ExperimentManagerImplTest,
     ExperimentManager_OneIneligibleProfileCallSetsPrefIneligibleAndReturnsEarly) {
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/false,
                                     mock_callback_.Get());
   EXPECT_CALL(mock_callback_, Run(false)).Times(1);
@@ -155,9 +155,9 @@
 }
 
 TEST_F(
-    ExperimentManagerTest,
+    ExperimentManagerImplTest,
     ExperimentManager_OneEligibleOneIneligibleProfileCallSetsPrefIneligible) {
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/true, mock_callback_.Get());
   test_manager.SetClientEligibility(/*is_eligible=*/false,
                                     mock_callback_.Get());
@@ -169,9 +169,9 @@
 }
 
 TEST_F(
-    ExperimentManagerTest,
+    ExperimentManagerImplTest,
     ExperimentManager_OneIneligibleOneEligibleProfileCallSetsPrefIneligible) {
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/false,
                                     mock_callback_.Get());
   test_manager.SetClientEligibility(/*is_eligible=*/true, mock_callback_.Get());
@@ -182,7 +182,7 @@
             static_cast<int>(utils::ExperimentState::kIneligible));
 }
 
-TEST_F(ExperimentManagerTest,
+TEST_F(ExperimentManagerImplTest,
        ExperimentManager_SetIneligibleAfterDecisionCallDoesNothing) {
   Checkpoint checkpoint;
   {
@@ -194,7 +194,7 @@
     EXPECT_CALL(mock_callback_, Run(true)).Times(1);
   }
 
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/true, mock_callback_.Get());
 
   checkpoint.Call(1);
@@ -210,7 +210,7 @@
             static_cast<int>(utils::ExperimentState::kEligible));
 }
 
-TEST_F(ExperimentManagerTest,
+TEST_F(ExperimentManagerImplTest,
        ExperimentManager_SetEligibleAfterDecisionCallDoesNothing) {
   Checkpoint checkpoint;
   {
@@ -222,7 +222,7 @@
     EXPECT_CALL(mock_callback_, Run(false)).Times(1);
   }
 
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/false,
                                     mock_callback_.Get());
 
@@ -238,9 +238,9 @@
             static_cast<int>(utils::ExperimentState::kIneligible));
 }
 
-TEST_F(ExperimentManagerTest,
+TEST_F(ExperimentManagerImplTest,
        ExperimentManager_PrefUnsetBeforeFinalDecisionIsMade) {
-  TestingExperimentManager test_manager;
+  TestingExperimentManagerImpl test_manager;
   test_manager.SetClientEligibility(/*is_eligible=*/false,
                                     mock_callback_.Get());
   // No callbacks run before the delay_time_ time completes.
@@ -254,37 +254,40 @@
             static_cast<int>(utils::ExperimentState::kUnknownEligibility));
 }
 
-TEST_F(ExperimentManagerTest, PrefIneligibleReturnsEarly) {
+TEST_F(ExperimentManagerImplTest, PrefIneligibleReturnsEarly) {
   prefs().SetInteger(prefs::kTPCDExperimentClientState,
                      static_cast<int>(utils::ExperimentState::kIneligible));
   EXPECT_CALL(mock_callback_, Run(false)).Times(1);
-  TestingExperimentManager().SetClientEligibility(/*is_eligible=*/true,
-                                                  mock_callback_.Get());
+  TestingExperimentManagerImpl().SetClientEligibility(/*is_eligible=*/true,
+                                                      mock_callback_.Get());
 
   EXPECT_EQ(prefs().GetInteger(prefs::kTPCDExperimentClientState),
             static_cast<int>(utils::ExperimentState::kIneligible));
 }
 
-TEST_F(ExperimentManagerTest, IsClientEligible_PrefIsEligibleReturnsTrue) {
+TEST_F(ExperimentManagerImplTest, IsClientEligible_PrefIsEligibleReturnsTrue) {
   prefs().SetInteger(prefs::kTPCDExperimentClientState,
                      static_cast<int>(utils::ExperimentState::kEligible));
 
-  EXPECT_THAT(TestingExperimentManager().IsClientEligible(), Optional(true));
+  EXPECT_THAT(TestingExperimentManagerImpl().IsClientEligible(),
+              Optional(true));
 }
 
-TEST_F(ExperimentManagerTest, IsClientEligible_PrefIsIneligibleReturnsFalse) {
+TEST_F(ExperimentManagerImplTest,
+       IsClientEligible_PrefIsIneligibleReturnsFalse) {
   prefs().SetInteger(prefs::kTPCDExperimentClientState,
                      static_cast<int>(utils::ExperimentState::kIneligible));
 
-  EXPECT_THAT(TestingExperimentManager().IsClientEligible(), Optional(false));
+  EXPECT_THAT(TestingExperimentManagerImpl().IsClientEligible(),
+              Optional(false));
 }
 
-TEST_F(ExperimentManagerTest, IsClientEligible_PrefIsUnknownReturnsEmpty) {
+TEST_F(ExperimentManagerImplTest, IsClientEligible_PrefIsUnknownReturnsEmpty) {
   prefs().SetInteger(
       prefs::kTPCDExperimentClientState,
       static_cast<int>(utils::ExperimentState::kUnknownEligibility));
 
-  EXPECT_EQ(TestingExperimentManager().IsClientEligible(), absl::nullopt);
+  EXPECT_EQ(TestingExperimentManagerImpl().IsClientEligible(), absl::nullopt);
 }
 
 }  // namespace tpcd::experiment