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:
@@ -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(¶ms);
|
||||
|
||||
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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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_
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user