Add used containers tracking. (#34705)

Add used containers tracking and make them available via `GetRuntimeContainerById`.

The PR does not include storage cleanup and pref cleanup. This is intentional - the cleanup logic will be landed separately.
This commit is contained in:
Aleksei Khoroshilov
2026-03-31 17:31:01 +07:00
committed by GitHub
parent c5d27921bf
commit 7371c54760
22 changed files with 599 additions and 73 deletions
+5
View File
@@ -9,14 +9,19 @@ assert(enable_containers)
source_set("containers") {
sources = [
"container_tab_tracker.cc",
"container_tab_tracker.h",
"containers_service_factory.cc",
"containers_service_factory.h",
]
deps = [
"//brave/components/containers/content/browser",
"//brave/components/containers/core/browser",
"//brave/components/containers/core/common",
"//chrome/browser/profiles:profile",
"//chrome/browser/ui/tabs",
"//content/public/browser",
]
}
@@ -0,0 +1,70 @@
// 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/browser/containers/container_tab_tracker.h"
#include "base/memory/ptr_util.h"
#include "brave/browser/containers/containers_service_factory.h"
#include "brave/components/containers/content/browser/storage_partition_utils.h"
#include "brave/components/containers/core/browser/containers_service.h"
#include "chrome/browser/profiles/profile.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
namespace containers {
// static
std::unique_ptr<ContainerTabTracker> ContainerTabTracker::MaybeCreate(
tabs::TabInterface& tab) {
content::WebContents* const contents = tab.GetContents();
CHECK(contents);
if (!ContainersServiceFactory::GetForProfile(
Profile::FromBrowserContext(contents->GetBrowserContext()))) {
return nullptr;
}
return base::WrapUnique(new ContainerTabTracker(tab));
}
ContainerTabTracker::ContainerTabTracker(tabs::TabInterface& tab)
: tabs::ContentsObservingTabFeature(tab) {}
ContainerTabTracker::~ContainerTabTracker() = default;
void ContainerTabTracker::OnDiscardContents(
tabs::TabInterface* tab,
content::WebContents* old_contents,
content::WebContents* new_contents) {
tracked_ = false;
tabs::ContentsObservingTabFeature::OnDiscardContents(tab, old_contents,
new_contents);
}
void ContainerTabTracker::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (tracked_) {
return;
}
if (!navigation_handle->IsInPrimaryMainFrame()) {
return;
}
tracked_ = true;
const std::string container_id =
GetContainerIdForWebContents(navigation_handle->GetWebContents());
if (container_id.empty()) {
return;
}
auto* service =
ContainersServiceFactory::GetForProfile(Profile::FromBrowserContext(
navigation_handle->GetWebContents()->GetBrowserContext()));
CHECK(service);
service->MarkContainerUsed(container_id);
}
} // namespace containers
@@ -0,0 +1,56 @@
// 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_BROWSER_CONTAINERS_CONTAINER_TAB_TRACKER_H_
#define BRAVE_BROWSER_CONTAINERS_CONTAINER_TAB_TRACKER_H_
#include <memory>
#include "chrome/browser/ui/tabs/contents_observing_tab_feature.h"
namespace content {
class NavigationHandle;
class WebContents;
} // namespace content
namespace tabs {
class TabInterface;
} // namespace tabs
namespace containers {
// Observes the tab's active WebContents for the first primary main-frame
// navigation and, if the tab uses a Containers storage partition, marks the
// container as used.
class ContainerTabTracker : public tabs::ContentsObservingTabFeature {
public:
~ContainerTabTracker() override;
ContainerTabTracker(const ContainerTabTracker&) = delete;
ContainerTabTracker& operator=(const ContainerTabTracker&) = delete;
// Returns nullptr if |ContainersService| is unavailable for this tab's
// profile.
static std::unique_ptr<ContainerTabTracker> MaybeCreate(
tabs::TabInterface& tab);
private:
explicit ContainerTabTracker(tabs::TabInterface& tab);
// tabs::ContentsObservingTabFeature:
void OnDiscardContents(tabs::TabInterface* tab,
content::WebContents* old_contents,
content::WebContents* new_contents) override;
// content::WebContentsObserver:
void DidStartNavigation(
content::NavigationHandle* navigation_handle) override;
bool tracked_ = false;
};
} // namespace containers
#endif // BRAVE_BROWSER_CONTAINERS_CONTAINER_TAB_TRACKER_H_
@@ -4,9 +4,12 @@
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "base/test/run_until.h"
#include "brave/browser/containers/containers_service_factory.h"
#include "brave/browser/ui/browser_commands.h"
#include "brave/browser/ui/views/tabs/brave_tab.h"
#include "brave/components/containers/content/browser/storage_partition_utils.h"
#include "brave/components/containers/core/browser/containers_service.h"
#include "brave/components/containers/core/browser/prefs.h"
#include "brave/components/containers/core/common/features.h"
#include "brave/components/containers/core/mojom/containers.mojom.h"
#include "chrome/browser/profiles/profile.h"
@@ -220,6 +223,10 @@ class ContainersBrowserTest : public InProcessBrowserTest {
"});";
}
ContainersService* GetContainersService() {
return ContainersServiceFactory::GetForProfile(browser()->profile());
}
protected:
base::test::ScopedFeatureList feature_list_;
net::EmbeddedTestServer https_server_;
@@ -415,6 +422,85 @@ IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
content::EvalJs(web_contents_reloaded, GetIndexedDBJS("persistent_key")));
}
// With the container still in the synced list, the first navigation in a
// container tab records a used snapshot via ContainerTabTracker. After the
// synced entry is removed, session restore still exposes the partition on the
// navigation entry and GetRuntimeContainerById resolves metadata from used
// prefs.
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
PRE_RuntimeContainerAvailableAfterSyncedRemovalRestart) {
std::vector<containers::mojom::ContainerPtr> synced;
synced.push_back(containers::mojom::Container::New(
kTestContainerId, "ReadableName", containers::mojom::Icon::kWork,
SK_ColorRED));
SetContainersToPrefs(synced, *browser()->profile()->GetPrefs());
const GURL url("https://a.test/simple.html");
NavigateParams params(browser(), url, ui::PAGE_TRANSITION_LINK);
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
params.storage_partition_config = content::StoragePartitionConfig::Create(
browser()->profile(), kContainersStoragePartitionDomain, kTestContainerId,
browser()->profile()->IsOffTheRecord());
ui_test_utils::NavigateToURL(&params);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
ASSERT_TRUE(content::WaitForLoadStop(web_contents));
EXPECT_EQ(url, web_contents->GetLastCommittedURL());
const ContainersService* service = GetContainersService();
ASSERT_TRUE(service);
EXPECT_TRUE(service->GetRuntimeContainerById(kTestContainerId));
PrefService* prefs = browser()->profile()->GetPrefs();
EXPECT_TRUE(GetContainerFromPrefs(*prefs, kTestContainerId));
mojom::ContainerPtr used_after_nav =
GetLocallyUsedContainerFromPrefs(*prefs, kTestContainerId);
ASSERT_TRUE(used_after_nav);
EXPECT_EQ("ReadableName", used_after_nav->name);
// Remove the container from the synced list.
SetContainersToPrefs({}, *prefs);
EXPECT_FALSE(GetContainerFromPrefs(*prefs, kTestContainerId));
mojom::ContainerPtr used_after_removal =
GetLocallyUsedContainerFromPrefs(*prefs, kTestContainerId);
ASSERT_TRUE(used_after_removal);
EXPECT_EQ("ReadableName", used_after_removal->name);
}
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
RuntimeContainerAvailableAfterSyncedRemovalRestart) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
ASSERT_TRUE(content::WaitForLoadStop(web_contents));
content::NavigationEntry* entry =
web_contents->GetController().GetLastCommittedEntry();
ASSERT_TRUE(entry);
auto storage_key = entry->GetStoragePartitionKeyToRestore();
ASSERT_TRUE(storage_key.has_value())
<< "Restored navigation should expose container storage partition key";
EXPECT_EQ(kContainersStoragePartitionDomain, storage_key->first);
EXPECT_EQ(kTestContainerId, storage_key->second);
content::StoragePartitionConfig frame_config =
web_contents->GetPrimaryMainFrame()->GetStoragePartition()->GetConfig();
EXPECT_EQ(kContainersStoragePartitionDomain, frame_config.partition_domain());
EXPECT_EQ(kTestContainerId, frame_config.partition_name());
const ContainersService* service = GetContainersService();
ASSERT_TRUE(service);
mojom::ContainerPtr runtime =
service->GetRuntimeContainerById(storage_key->second);
ASSERT_TRUE(runtime);
EXPECT_EQ(kTestContainerId, runtime->id);
EXPECT_EQ("ReadableName", runtime->name);
EXPECT_EQ(mojom::Icon::kWork, runtime->icon);
EXPECT_EQ(SK_ColorRED, runtime->background_color);
}
IN_PROC_BROWSER_TEST_F(ContainersBrowserTest,
LinkNavigationInheritsContainerStoragePartition) {
const GURL url("https://a.test/simple.html");
+2 -12
View File
@@ -5,7 +5,6 @@
#include "brave/browser/ui/containers/container_model.h"
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
@@ -13,7 +12,7 @@
#include "base/functional/bind.h"
#include "brave/browser/ui/containers/containers_icon_generator.h"
#include "brave/components/containers/core/browser/containers_service.h"
#include "third_party/skia/include/core/SkColor.h"
#include "brave/components/containers/core/browser/unknown_container.h"
#include "ui/gfx/geometry/size.h"
namespace containers {
@@ -34,15 +33,6 @@ ui::ImageModel GetImageModelForContainer(const mojom::ContainerPtr& container,
} // namespace
// static
ContainerModel ContainerModel::CreateForUnknown(const std::string& id,
float scale_factor) {
return ContainerModel(
mojom::Container::New(id, id, containers::mojom::Icon::kDefault,
SkColorSetRGB(0xb7, 0x4d, 0x49)),
scale_factor);
}
ContainerModel::ContainerModel(mojom::ContainerPtr container,
float scale_factor)
: container_(std::move(container)),
@@ -73,7 +63,7 @@ ContainerModel GetRuntimeContainerModel(const ContainersService& service,
return ContainerModel(std::move(container), scale_factor);
}
return ContainerModel::CreateForUnknown(std::string(id), scale_factor);
return ContainerModel(CreateUnknownContainer(id), scale_factor);
}
} // namespace containers
-2
View File
@@ -56,8 +56,6 @@ std::vector<ContainerModel> GetContainerModels(const ContainersService& service,
// Resolves a runtime container model using synced pref. Falls back to an
// unknown model when the container is not found.
// TODO(https://github.com/brave/brave-browser/issues/53604): Will fallback to
// local used-containers cache.
ContainerModel GetRuntimeContainerModel(const ContainersService& service,
std::string_view id,
float scale_factor);
+1
View File
@@ -49,6 +49,7 @@ if (!is_android) {
if (enable_containers) {
deps += [
"//brave/browser/containers",
"//brave/components/containers/core/browser",
"//brave/components/containers/core/common:features",
]
+13 -10
View File
@@ -19,6 +19,7 @@
#include "components/tabs/public/tab_interface.h"
#if BUILDFLAG(ENABLE_CONTAINERS)
#include "brave/browser/containers/container_tab_tracker.h"
#include "brave/browser/ui/views/page_action/partitioned_storage_page_action_controller.h"
#include "brave/components/containers/core/common/features.h"
#endif
@@ -75,16 +76,18 @@ void BraveTabFeatures::Init(TabInterface& tab, Profile* profile) {
#endif
#if BUILDFLAG(ENABLE_CONTAINERS)
if (page_action_controller() &&
base::FeatureList::IsEnabled(containers::features::kContainers)) {
// In case of features::kPageActionsMigration is disabled, this controller
// can be null. The feature is enabled by default. So note that we don't
// show the partitioned storage page action when the features is disabled
// by users.
partitioned_storage_page_action_controller_ =
std::make_unique<page_actions::PartitionedStoragePageActionController>(
tab, *page_action_controller());
partitioned_storage_page_action_controller_->Init();
if (base::FeatureList::IsEnabled(containers::features::kContainers)) {
container_tab_tracker_ = containers::ContainerTabTracker::MaybeCreate(tab);
if (page_action_controller()) {
// In case of features::kPageActionsMigration is disabled, this controller
// can be null. The feature is enabled by default. So note that we don't
// show the partitioned storage page action when the features is disabled
// by users.
partitioned_storage_page_action_controller_ = std::make_unique<
page_actions::PartitionedStoragePageActionController>(
tab, *page_action_controller());
partitioned_storage_page_action_controller_->Init();
}
}
#endif
}
@@ -22,6 +22,9 @@ class TabDataWebContentsObserver;
#endif
#if BUILDFLAG(ENABLE_CONTAINERS)
namespace containers {
class ContainerTabTracker;
}
namespace page_actions {
class PartitionedStoragePageActionController;
}
@@ -59,6 +62,7 @@ class BraveTabFeatures : public TabFeatures {
std::unique_ptr<psst::PsstTabWebContentsObserver> psst_web_contents_observer_;
#endif
#if BUILDFLAG(ENABLE_CONTAINERS)
std::unique_ptr<containers::ContainerTabTracker> container_tab_tracker_;
std::unique_ptr<page_actions::PartitionedStoragePageActionController>
partitioned_storage_page_action_controller_;
#endif
+4 -1
View File
@@ -56,7 +56,10 @@ test("brave_components_unittests") {
}
if (enable_containers) {
deps += [ "//brave/components/containers/content/browser:unit_tests" ]
deps += [
"//brave/components/containers/content/browser:unit_tests",
"//brave/components/containers/core/browser:unit_tests",
]
}
if (enable_psst) {
@@ -12,6 +12,7 @@ static_library("browser") {
"containers_service.h",
"containers_settings_handler.h",
"prefs_registration.h",
"unknown_container.h",
]
sources = [
@@ -21,6 +22,7 @@ static_library("browser") {
"prefs.cc",
"prefs.h",
"prefs_registration.cc",
"unknown_container.cc",
]
friend = [
@@ -39,6 +41,7 @@ static_library("browser") {
"//components/user_prefs",
"//net/base/registry_controlled_domains",
"//services/network/public/cpp",
"//skia",
"//third_party/re2",
"//url",
]
@@ -6,22 +6,52 @@
#include "brave/components/containers/core/browser/containers_service.h"
#include "base/check_deref.h"
#include "base/types/to_address.h"
#include "brave/components/containers/core/browser/pref_names.h"
#include "brave/components/containers/core/browser/prefs.h"
#include "brave/components/containers/core/browser/unknown_container.h"
#include "brave/components/containers/core/mojom/containers.mojom.h"
#include "components/prefs/pref_service.h"
namespace containers {
ContainersService::ContainersService(PrefService* prefs)
: prefs_(CHECK_DEREF(prefs)) {}
: prefs_(CHECK_DEREF(prefs)) {
pref_change_registrar_.Init(base::to_address(prefs_));
pref_change_registrar_.Add(
prefs::kContainersList,
base::BindRepeating(&ContainersService::OnSyncedContainersChanged,
base::Unretained(this)));
}
ContainersService::~ContainersService() = default;
void ContainersService::Shutdown() {
pref_change_registrar_.RemoveAll();
}
void ContainersService::MarkContainerUsed(std::string_view container_id) {
CHECK(!container_id.empty());
if (HasLocallyUsedContainerInPrefs(*prefs_, container_id)) {
return;
}
auto container = GetContainerFromPrefs(*prefs_, container_id);
if (!container) {
container = CreateUnknownContainer(container_id);
}
SetLocallyUsedContainerToPrefs(container, *prefs_);
}
mojom::ContainerPtr ContainersService::GetRuntimeContainerById(
std::string_view id) const {
if (auto container = GetContainerFromPrefs(*prefs_, id)) {
return container;
}
if (auto container = GetLocallyUsedContainerFromPrefs(*prefs_, id)) {
return container;
}
return nullptr;
}
@@ -29,4 +59,21 @@ std::vector<mojom::ContainerPtr> ContainersService::GetContainers() const {
return GetContainersFromPrefs(*prefs_);
}
void ContainersService::OnSyncedContainersChanged() {
RefreshLocallyUsedContainersFromSyncedList();
}
void ContainersService::RefreshLocallyUsedContainersFromSyncedList() {
for (const auto& used_container :
GetLocallyUsedContainersFromPrefs(*prefs_)) {
if (auto container = GetContainerFromPrefs(*prefs_, used_container->id)) {
SetLocallyUsedContainerToPrefs(container, *prefs_);
} else {
// A container may be absent from the synced list if it was deleted. We
// don't remove the used-container snapshot in this case here. It will be
// removed with a separate cleanup logic later.
}
}
}
} // namespace containers
@@ -6,7 +6,7 @@
#ifndef BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_CONTAINERS_SERVICE_H_
#define BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_CONTAINERS_SERVICE_H_
#include <string>
#include <string_view>
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
@@ -19,9 +19,6 @@ class PrefService;
namespace containers {
// Handles container-related operations.
//
// TODO(https://github.com/brave/brave-browser/issues/53604): Add a local cache
// for recently used containers.
class ContainersService : public KeyedService {
public:
explicit ContainersService(PrefService* prefs);
@@ -30,17 +27,31 @@ class ContainersService : public KeyedService {
ContainersService(const ContainersService&) = delete;
ContainersService& operator=(const ContainersService&) = delete;
// Returns the runtime container with the given `id`.
//
// TODO(https://github.com/brave/brave-browser/issues/53604): Will fallback to
// local used-containers cache.
void Shutdown() override;
// Caches a used-container snapshot: synced metadata when the id exists in
// the synced list, otherwise a placeholder from `CreateUnknownContainer`.
void MarkContainerUsed(std::string_view container_id);
// Returns the runtime container with the given `id`. Runtime containers are
// containers that are currently in use by the user. This can be a synced
// container or a removed, but still used container.
mojom::ContainerPtr GetRuntimeContainerById(std::string_view id) const;
// Returns the list of user-editable containers.
std::vector<mojom::ContainerPtr> GetContainers() const;
private:
// Called when the synced containers list changes.
void OnSyncedContainersChanged();
// Refreshes used-container snapshots from the synced containers list so they
// do not stay stale (names, icons, etc.). This is called when the synced
// containers list changes.
void RefreshLocallyUsedContainersFromSyncedList();
raw_ref<PrefService> prefs_;
PrefChangeRegistrar pref_change_registrar_;
base::WeakPtrFactory<ContainersService> weak_factory_{this};
};
@@ -12,9 +12,11 @@
#include "base/test/scoped_feature_list.h"
#include "brave/components/containers/core/browser/prefs.h"
#include "brave/components/containers/core/browser/prefs_registration.h"
#include "brave/components/containers/core/browser/unknown_container.h"
#include "brave/components/containers/core/common/features.h"
#include "brave/components/containers/core/mojom/containers.mojom.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace containers {
@@ -23,11 +25,20 @@ namespace {
mojom::ContainerPtr MakeContainer(std::string id,
std::string name,
mojom::Icon icon,
SkColor color) {
mojom::Icon icon = mojom::Icon::kDefault,
SkColor color = SK_ColorBLUE) {
return mojom::Container::New(std::move(id), std::move(name), icon, color);
}
void ExpectContainer(const mojom::ContainerPtr& container,
const std::string& id,
const std::string& name,
const mojom::Icon& icon = mojom::Icon::kDefault,
const SkColor& color = SK_ColorBLUE) {
ASSERT_TRUE(container);
EXPECT_THAT(*container, testing::FieldsAre(id, name, icon, color));
}
} // namespace
class ContainersServiceTest : public testing::Test {
@@ -45,33 +56,110 @@ class ContainersServiceTest : public testing::Test {
};
TEST_F(ContainersServiceTest, GetRuntimeContainerById) {
auto container = MakeContainer("container-id", "Personal",
mojom::Icon::kPersonal, SK_ColorBLUE);
auto container = MakeContainer("container-id", "Personal");
std::vector<mojom::ContainerPtr> synced_containers;
synced_containers.push_back(container.Clone());
SetContainersToPrefs(std::move(synced_containers), prefs_);
auto runtime_container = service_->GetRuntimeContainerById("container-id");
ASSERT_TRUE(runtime_container);
EXPECT_EQ(runtime_container->name, "Personal");
EXPECT_EQ(runtime_container->icon, mojom::Icon::kPersonal);
EXPECT_EQ(runtime_container->background_color, SK_ColorBLUE);
ExpectContainer(runtime_container, "container-id", "Personal");
EXPECT_FALSE(service_->GetRuntimeContainerById("non-existent-container-id"));
}
TEST_F(ContainersServiceTest, GetContainers) {
auto container =
MakeContainer("container-id", "Work", mojom::Icon::kWork, SK_ColorGREEN);
auto container = MakeContainer("container-id", "Work");
std::vector<mojom::ContainerPtr> containers;
containers.push_back(container.Clone());
SetContainersToPrefs(std::move(containers), prefs_);
auto containers_list = service_->GetContainers();
ASSERT_EQ(containers_list.size(), 1u);
EXPECT_EQ(containers_list[0]->name, "Work");
EXPECT_EQ(containers_list[0]->icon, mojom::Icon::kWork);
EXPECT_EQ(containers_list[0]->background_color, SK_ColorGREEN);
ExpectContainer(containers_list[0], "container-id", "Work");
}
TEST_F(ContainersServiceTest, MarkContainerUsed_PersistsSnapshot) {
auto container = MakeContainer("used-id", "Local");
std::vector<mojom::ContainerPtr> synced;
synced.push_back(container.Clone());
SetContainersToPrefs(synced, prefs_);
service_->MarkContainerUsed("used-id");
auto used = GetLocallyUsedContainerFromPrefs(prefs_, "used-id");
ExpectContainer(used, "used-id", "Local");
}
TEST_F(ContainersServiceTest, MarkContainerUsed_PersistsUnknownWhenNotSynced) {
SetContainersToPrefs({}, prefs_);
service_->MarkContainerUsed("unknown-id");
auto used = GetLocallyUsedContainerFromPrefs(prefs_, "unknown-id");
ExpectContainer(used, "unknown-id", "unknown-", mojom::Icon::kDefault,
kUnknownContainerBackgroundColor);
}
TEST_F(ContainersServiceTest,
MarkContainerUsed_DoesNotUpsertWhenAlreadyInUsedPrefs) {
auto container = MakeContainer("used-id", "Local");
std::vector<mojom::ContainerPtr> synced;
synced.push_back(container.Clone());
SetContainersToPrefs(synced, prefs_);
service_->MarkContainerUsed("used-id");
SetLocallyUsedContainerToPrefs(
MakeContainer("used-id", "Stale", mojom::Icon::kWork, SK_ColorRED),
prefs_);
service_->MarkContainerUsed("used-id");
auto used = GetLocallyUsedContainerFromPrefs(prefs_, "used-id");
ExpectContainer(used, "used-id", "Stale", mojom::Icon::kWork, SK_ColorRED);
}
TEST_F(ContainersServiceTest,
GetRuntimeContainerById_FallsBackToUsedWhenNotInSyncedList) {
SetContainersToPrefs({}, prefs_);
SetLocallyUsedContainerToPrefs(MakeContainer("cached-id", "CachedName"),
prefs_);
auto runtime = service_->GetRuntimeContainerById("cached-id");
ExpectContainer(runtime, "cached-id", "CachedName");
EXPECT_TRUE(service_->GetContainers().empty());
}
TEST_F(ContainersServiceTest,
OnSyncedContainersChanged_RefreshesUsedSnapshotWhenIdStillSynced) {
std::vector<mojom::ContainerPtr> v1;
v1.push_back(MakeContainer("c1", "SyncedV1"));
SetContainersToPrefs(v1, prefs_);
SetLocallyUsedContainerToPrefs(
MakeContainer("c1", "Stale", mojom::Icon::kWork, SK_ColorRED), prefs_);
auto used = GetLocallyUsedContainerFromPrefs(prefs_, "c1");
ExpectContainer(used, "c1", "Stale", mojom::Icon::kWork, SK_ColorRED);
std::vector<mojom::ContainerPtr> v2;
v2.push_back(MakeContainer("c1", "SyncedV2"));
SetContainersToPrefs(v2, prefs_);
auto used_after_sync = GetLocallyUsedContainerFromPrefs(prefs_, "c1");
ExpectContainer(used_after_sync, "c1", "SyncedV2");
}
TEST_F(ContainersServiceTest,
OnSyncedContainersChanged_PreservesUsedWhenIdNotInSyncedList) {
SetLocallyUsedContainerToPrefs(MakeContainer("gone-id", "Cached"), prefs_);
std::vector<mojom::ContainerPtr> synced;
synced.push_back(MakeContainer("other-id", "Other"));
SetContainersToPrefs(synced, prefs_);
auto used = GetLocallyUsedContainerFromPrefs(prefs_, "gone-id");
ExpectContainer(used, "gone-id", "Cached");
}
} // namespace containers
@@ -8,9 +8,13 @@
namespace containers::prefs {
// Preference key for storing the list of containers.
// Syncable list of containers.
inline constexpr char kContainersList[] = "brave.containers.list";
// Local-only dictionary of container snapshots that are still referenced by
// this profile even if they disappear from the synced containers list.
inline constexpr char kLocallyUsedContainers[] = "brave.containers.used";
} // namespace containers::prefs
#endif // BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_PREF_NAMES_H_
+67 -19
View File
@@ -12,6 +12,7 @@
#include "brave/components/containers/core/common/features.h"
#include "brave/components/containers/core/mojom/containers.mojom.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
namespace containers {
@@ -39,13 +40,10 @@ base::DictValue ContainerToDict(const mojom::ContainerPtr& container) {
.Set("background_color", static_cast<int>(container->background_color));
}
} // namespace
std::vector<mojom::ContainerPtr> GetContainersFromPrefs(
const PrefService& prefs) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
std::vector<mojom::ContainerPtr> GetContainersFromList(
const base::ListValue& list) {
std::vector<mojom::ContainerPtr> containers;
for (const auto& container : prefs.GetList(prefs::kContainersList)) {
for (const auto& container : list) {
if (!container.is_dict()) {
LOG(ERROR) << "Container is not a dictionary";
continue;
@@ -58,22 +56,20 @@ std::vector<mojom::ContainerPtr> GetContainersFromPrefs(
return containers;
}
} // namespace
std::vector<mojom::ContainerPtr> GetContainersFromPrefs(
const PrefService& prefs) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
return GetContainersFromList(prefs.GetList(prefs::kContainersList));
}
mojom::ContainerPtr GetContainerFromPrefs(const PrefService& prefs,
std::string_view id) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
for (const auto& container : prefs.GetList(prefs::kContainersList)) {
if (!container.is_dict()) {
LOG(ERROR) << "Container is not a dictionary";
continue;
}
const auto* container_id = container.GetDict().FindString("id");
if (!container_id || *container_id != id) {
continue;
}
if (auto parsed = ContainerFromDict(container.GetDict())) {
return parsed;
for (auto& container : GetContainersFromPrefs(prefs)) {
if (container->id == id) {
return std::move(container);
}
}
return nullptr;
@@ -89,4 +85,56 @@ void SetContainersToPrefs(const std::vector<mojom::ContainerPtr>& containers,
prefs.SetList(prefs::kContainersList, std::move(list));
}
std::vector<mojom::ContainerPtr> GetLocallyUsedContainersFromPrefs(
const PrefService& prefs) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
std::vector<mojom::ContainerPtr> containers;
for (const auto item : prefs.GetDict(prefs::kLocallyUsedContainers)) {
if (!item.second.is_dict()) {
LOG(ERROR) << "Used container snapshot is not a dictionary";
continue;
}
if (auto parsed = ContainerFromDict(item.second.GetDict())) {
containers.push_back(std::move(parsed));
}
}
return containers;
}
mojom::ContainerPtr GetLocallyUsedContainerFromPrefs(const PrefService& prefs,
std::string_view id) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
if (const auto* value =
prefs.GetDict(prefs::kLocallyUsedContainers).FindDict(id)) {
return ContainerFromDict(*value);
}
return nullptr;
}
void SetLocallyUsedContainerToPrefs(const mojom::ContainerPtr& container,
PrefService& prefs) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
CHECK(container);
ScopedDictPrefUpdate update(&prefs, prefs::kLocallyUsedContainers);
update->Set(container->id, ContainerToDict(container));
}
bool HasLocallyUsedContainerInPrefs(const PrefService& prefs,
std::string_view id) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
return prefs.GetDict(prefs::kLocallyUsedContainers).contains(id);
}
void RemoveLocallyUsedContainerFromPrefs(std::string_view id,
PrefService& prefs) {
CHECK(base::FeatureList::IsEnabled(features::kContainers));
if (!prefs.GetDict(prefs::kLocallyUsedContainers).contains(id)) {
return;
}
ScopedDictPrefUpdate update(&prefs, prefs::kLocallyUsedContainers);
update->Remove(id);
}
} // namespace containers
@@ -28,6 +28,27 @@ mojom::ContainerPtr GetContainerFromPrefs(const PrefService& prefs,
void SetContainersToPrefs(const std::vector<mojom::ContainerPtr>& containers,
PrefService& prefs);
// Returns the list of locally used containers.
std::vector<mojom::ContainerPtr> GetLocallyUsedContainersFromPrefs(
const PrefService& prefs);
// Returns the locally used container snapshot with `id`, or a null
// mojom::ContainerPtr if it is not present.
mojom::ContainerPtr GetLocallyUsedContainerFromPrefs(const PrefService& prefs,
std::string_view id);
// Upserts a locally used container snapshot.
void SetLocallyUsedContainerToPrefs(const mojom::ContainerPtr& container,
PrefService& prefs);
// Returns true if a locally used container snapshot is present.
bool HasLocallyUsedContainerInPrefs(const PrefService& prefs,
std::string_view id);
// Removes a locally used container snapshot.
void RemoveLocallyUsedContainerFromPrefs(std::string_view id,
PrefService& prefs);
} // namespace containers
#endif // BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_PREFS_H_
@@ -13,6 +13,7 @@ namespace containers {
void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kContainersList,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterDictionaryPref(prefs::kLocallyUsedContainers);
}
} // namespace containers
@@ -120,4 +120,44 @@ TEST_F(ContainersPrefsTest, GetContainerById) {
EXPECT_FALSE(GetContainerFromPrefs(prefs_, "missing-id"));
}
TEST_F(ContainersPrefsTest, SetAndGetLocallyUsedContainer) {
auto container = mojom::Container::New("used-id", "Used Container",
mojom::Icon::kShopping, SK_ColorBLUE);
EXPECT_FALSE(HasLocallyUsedContainerInPrefs(prefs_, "used-id"));
SetLocallyUsedContainerToPrefs(container, prefs_);
EXPECT_TRUE(HasLocallyUsedContainerInPrefs(prefs_, "used-id"));
EXPECT_FALSE(HasLocallyUsedContainerInPrefs(prefs_, "other-id"));
auto retrieved = GetLocallyUsedContainerFromPrefs(prefs_, "used-id");
ASSERT_TRUE(retrieved);
EXPECT_EQ(retrieved->name, "Used Container");
EXPECT_EQ(retrieved->icon, mojom::Icon::kShopping);
EXPECT_EQ(retrieved->background_color, SK_ColorBLUE);
auto all_used = GetLocallyUsedContainersFromPrefs(prefs_);
ASSERT_EQ(all_used.size(), 1u);
EXPECT_EQ(all_used[0]->id, "used-id");
}
TEST_F(ContainersPrefsTest, UpdateAndRemoveLocallyUsedContainer) {
SetLocallyUsedContainerToPrefs(
mojom::Container::New("used-id", "Used Container", mojom::Icon::kShopping,
SK_ColorBLUE),
prefs_);
SetLocallyUsedContainerToPrefs(
mojom::Container::New("used-id", "Updated Container", mojom::Icon::kWork,
SK_ColorRED),
prefs_);
auto retrieved = GetLocallyUsedContainerFromPrefs(prefs_, "used-id");
ASSERT_TRUE(retrieved);
EXPECT_EQ(retrieved->name, "Updated Container");
EXPECT_EQ(retrieved->icon, mojom::Icon::kWork);
EXPECT_EQ(retrieved->background_color, SK_ColorRED);
RemoveLocallyUsedContainerFromPrefs("used-id", prefs_);
EXPECT_FALSE(GetLocallyUsedContainerFromPrefs(prefs_, "used-id"));
EXPECT_TRUE(GetLocallyUsedContainersFromPrefs(prefs_).empty());
}
} // namespace containers
@@ -0,0 +1,22 @@
// 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/containers/core/browser/unknown_container.h"
#include <string>
#include "brave/components/containers/core/mojom/containers.mojom.h"
namespace containers {
mojom::ContainerPtr CreateUnknownContainer(std::string_view container_id) {
// Use first 8 characters of the container ID as the name.
std::string_view name = container_id.substr(0, 8);
return mojom::Container::New(std::string(container_id), std::string(name),
mojom::Icon::kDefault,
kUnknownContainerBackgroundColor);
}
} // namespace containers
@@ -0,0 +1,28 @@
// 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_CONTAINERS_CORE_BROWSER_UNKNOWN_CONTAINER_H_
#define BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_UNKNOWN_CONTAINER_H_
#include <string_view>
#include "brave/components/containers/core/mojom/containers.mojom-forward.h"
#include "third_party/skia/include/core/SkColor.h"
namespace containers {
// Background color to represent an unknown container.
inline constexpr SkColor kUnknownContainerBackgroundColor =
SkColorSetRGB(0xb7, 0x4d, 0x49);
// Builds an "unknown" Container for the given `container_id`. The unknown
// container is a placeholder for a container that is not present in the synced
// and locally used lists. This exists to handle unknown containers gracefully
// at runtime.
mojom::ContainerPtr CreateUnknownContainer(std::string_view container_id);
} // namespace containers
#endif // BRAVE_COMPONENTS_CONTAINERS_CORE_BROWSER_UNKNOWN_CONTAINER_H_
+1 -4
View File
@@ -620,10 +620,7 @@ test("brave_unit_tests") {
}
if (enable_containers) {
deps += [
"//brave/browser/ui/containers:unit_tests",
"//brave/components/containers/core/browser:unit_tests",
]
deps += [ "//brave/browser/ui/containers:unit_tests" ]
}
if (enable_omaha4) {