diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2022-09-29 16:16:15 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2022-11-09 10:04:06 +0000 |
commit | a95a7417ad456115a1ef2da4bb8320531c0821f1 (patch) | |
tree | edcd59279e486d2fd4a8f88a7ed025bcf925c6e6 /chromium/components/user_notes | |
parent | 33fc33aa94d4add0878ec30dc818e34e1dd3cc2a (diff) | |
download | qtwebengine-chromium-a95a7417ad456115a1ef2da4bb8320531c0821f1.tar.gz |
BASELINE: Update Chromium to 106.0.5249.126
Change-Id: Ib0bb21c437a7d1686e21c33f2d329f2ac425b7ab
Reviewed-on: https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/438936
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/user_notes')
36 files changed, 2468 insertions, 326 deletions
diff --git a/chromium/components/user_notes/browser/frame_user_note_changes.cc b/chromium/components/user_notes/browser/frame_user_note_changes.cc index dbe4bd84444..65bbea84411 100644 --- a/chromium/components/user_notes/browser/frame_user_note_changes.cc +++ b/chromium/components/user_notes/browser/frame_user_note_changes.cc @@ -4,7 +4,7 @@ #include "components/user_notes/browser/frame_user_note_changes.h" -#include "base/barrier_closure.h" +#include "base/barrier_callback.h" #include "components/user_notes/browser/user_note_manager.h" #include "components/user_notes/model/user_note.h" #include "components/user_notes/model/user_note_target.h" @@ -18,7 +18,8 @@ FrameUserNoteChanges::FrameUserNoteChanges( const ChangeList& notes_added, const ChangeList& notes_modified, const ChangeList& notes_removed) - : service_(service), + : id_(base::UnguessableToken::Create()), + service_(service), rfh_(rfh), notes_added_(notes_added), notes_modified_(notes_modified), @@ -34,7 +35,8 @@ FrameUserNoteChanges::FrameUserNoteChanges( ChangeList&& notes_added, ChangeList&& notes_modified, ChangeList&& notes_removed) - : service_(service), + : id_(base::UnguessableToken::Create()), + service_(service), rfh_(rfh), notes_added_(std::move(notes_added)), notes_modified_(std::move(notes_modified)), @@ -60,6 +62,11 @@ void FrameUserNoteChanges::Apply(base::OnceClosure callback) { manager->RemoveNote(note_id); } + if (notes_added_.empty()) { + std::move(callback).Run(); + return; + } + // For added notes, the async highlight creation on the renderer side must be // awaited, because the order in which notes are shown in the Notes UI depends // on the order of the corresponding highlights in the page. Use a barrier @@ -79,7 +86,7 @@ void FrameUserNoteChanges::Apply(base::OnceClosure callback) { std::unique_ptr<UserNoteInstance> FrameUserNoteChanges::MakeNoteInstance( const UserNote* note_model, UserNoteManager* manager) const { - return std::make_unique<UserNoteInstance>(note_model->GetSafeRef(), manager); + return UserNoteInstance::Create(note_model->GetSafeRef(), manager); } } // namespace user_notes diff --git a/chromium/components/user_notes/browser/frame_user_note_changes.h b/chromium/components/user_notes/browser/frame_user_note_changes.h index 053570bfc3b..113e6a1392f 100644 --- a/chromium/components/user_notes/browser/frame_user_note_changes.h +++ b/chromium/components/user_notes/browser/frame_user_note_changes.h @@ -42,11 +42,16 @@ class FrameUserNoteChanges { FrameUserNoteChanges(FrameUserNoteChanges&& other); virtual ~FrameUserNoteChanges(); + const base::UnguessableToken& id() const { return id_; } + const ChangeList& notes_added() const { return notes_added_; } + const ChangeList& notes_modified() const { return notes_modified_; } + const content::RenderFrameHost* render_frame_host() const { return rfh_; } + // Kicks off the asynchronous logic to add and remove highlights in the frame // as necessary. Invokes the provided callback after the changes have fully // propagated to the note manager and the new notes have had their highlights - // created in the web page. - void Apply(base::OnceClosure callback); + // created in the web page. Marked virtual for tests to override. + virtual void Apply(base::OnceClosure callback); protected: // Called by `Apply()` to construct a new note instance pointing to the @@ -58,6 +63,9 @@ class FrameUserNoteChanges { private: FRIEND_TEST_ALL_PREFIXES(UserNoteUtilsTest, CalculateNoteChanges); + // An internal ID for this change, so it can be stored and retrieved later. + base::UnguessableToken id_; + base::SafeRef<UserNoteService> service_; raw_ptr<content::RenderFrameHost> rfh_; ChangeList notes_added_; diff --git a/chromium/components/user_notes/browser/frame_user_note_changes_unittest.cc b/chromium/components/user_notes/browser/frame_user_note_changes_unittest.cc index 5fabf4e8c4a..301c17ae729 100644 --- a/chromium/components/user_notes/browser/frame_user_note_changes_unittest.cc +++ b/chromium/components/user_notes/browser/frame_user_note_changes_unittest.cc @@ -9,6 +9,7 @@ #include "base/memory/safe_ref.h" #include "base/test/bind.h" +#include "base/test/gmock_callback_support.h" #include "base/unguessable_token.h" #include "components/user_notes/browser/user_note_base_test.h" #include "components/user_notes/browser/user_note_instance.h" @@ -34,7 +35,7 @@ class MockUserNoteInstance : public UserNoteInstance { MOCK_METHOD(void, InitializeHighlightIfNeeded, - (base::OnceClosure callback), + (UserNoteInstance::AttachmentFinishedCallback callback), (override)); }; diff --git a/chromium/components/user_notes/browser/user_note_base_test.cc b/chromium/components/user_notes/browser/user_note_base_test.cc index 5a83fda9e46..6b48fffff8b 100644 --- a/chromium/components/user_notes/browser/user_note_base_test.cc +++ b/chromium/components/user_notes/browser/user_note_base_test.cc @@ -5,6 +5,7 @@ #include "components/user_notes/browser/user_note_base_test.h" #include <memory> +#include <string> #include <vector> #include "components/user_notes/model/user_note_model_test_utils.h" @@ -12,6 +13,7 @@ #include "content/public/browser/page.h" #include "content/public/browser/render_frame_host.h" #include "content/public/test/navigation_simulator.h" +#include "services/service_manager/public/cpp/interface_provider.h" #include "testing/gtest/include/gtest/gtest.h" namespace user_notes { @@ -22,6 +24,14 @@ const char kBaseUrl[] = "https://www.example.com/"; } // namespace +MockAnnotationAgentContainer::MockAnnotationAgentContainer() = default; + +MockAnnotationAgentContainer::~MockAnnotationAgentContainer() = default; + +MockAnnotationAgent::MockAnnotationAgent() = default; + +MockAnnotationAgent::~MockAnnotationAgent() = default; + UserNoteBaseTest::UserNoteBaseTest() { scoped_feature_list_.InitAndEnableFeature(user_notes::kUserNotes); } @@ -30,7 +40,12 @@ UserNoteBaseTest::~UserNoteBaseTest() = default; void UserNoteBaseTest::SetUp() { content::RenderViewHostTestHarness::SetUp(); - note_service_ = std::make_unique<UserNoteService>(/*delegate=*/nullptr); + CreateService(); +} + +void UserNoteBaseTest::CreateService() { + note_service_ = std::make_unique<UserNoteService>(/*delegate=*/nullptr, + /*storage=*/nullptr); } void UserNoteBaseTest::TearDown() { @@ -61,7 +76,8 @@ void UserNoteBaseTest::AddPartialNotesToService(size_t count) { } } -UserNoteManager* UserNoteBaseTest::ConfigureNewManager() { +UserNoteManager* UserNoteBaseTest::ConfigureNewManager( + MockAnnotationAgentContainer* mock_container) { // Create a test frame and navigate it to a unique URL. std::unique_ptr<content::WebContents> wc = CreateTestWebContents(); content::RenderFrameHostTester::For(wc->GetPrimaryMainFrame()) @@ -70,6 +86,17 @@ UserNoteManager* UserNoteBaseTest::ConfigureNewManager() { wc.get(), GURL(kBaseUrl + base::NumberToString(web_contents_list_.size()))); + std::unique_ptr<service_manager::InterfaceProvider::TestApi> test_api; + if (mock_container) { + CHECK(!mock_container->is_bound()); + test_api = std::make_unique<service_manager::InterfaceProvider::TestApi>( + wc->GetPrimaryMainFrame()->GetRemoteInterfaces()); + test_api->SetBinderForName( + blink::mojom::AnnotationAgentContainer::Name_, + base::BindRepeating(&MockAnnotationAgentContainer::Bind, + base::Unretained(mock_container))); + } + // Create and attach a `UserNoteManager` to the primary page. content::Page& page = wc->GetPrimaryPage(); UserNoteManager::CreateForPage(page, note_service_->GetSafeRef()); @@ -77,6 +104,8 @@ UserNoteManager* UserNoteBaseTest::ConfigureNewManager() { DCHECK(note_manager); web_contents_list_.emplace_back(std::move(wc)); + CHECK(!mock_container || mock_container->is_bound()); + return note_manager; } @@ -85,8 +114,8 @@ void UserNoteBaseTest::AddNewInstanceToManager(UserNoteManager* manager, DCHECK(manager); const auto& entry_it = note_service_->model_map_.find(note_id); ASSERT_FALSE(entry_it == note_service_->model_map_.end()); - manager->AddNoteInstance(std::make_unique<UserNoteInstance>( - entry_it->second.model->GetSafeRef(), manager)); + manager->AddNoteInstance( + UserNoteInstance::Create(entry_it->second.model->GetSafeRef(), manager)); } size_t UserNoteBaseTest::ManagerCountForId( diff --git a/chromium/components/user_notes/browser/user_note_base_test.h b/chromium/components/user_notes/browser/user_note_base_test.h index 9c18b56af16..073013b789e 100644 --- a/chromium/components/user_notes/browser/user_note_base_test.h +++ b/chromium/components/user_notes/browser/user_note_base_test.h @@ -14,9 +14,63 @@ #include "components/user_notes/browser/user_note_service.h" #include "content/public/browser/web_contents.h" #include "content/public/test/test_renderer_host.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "third_party/blink/public/mojom/annotation/annotation.mojom.h" namespace user_notes { +// Mock implementation of the renderer's AnnotationAgentContainer interface. +// Tests can use this to simulate and make checks on the renderer end that a +// UserNoteManager interacts with. +class MockAnnotationAgentContainer + : public blink::mojom::AnnotationAgentContainer { + public: + MockAnnotationAgentContainer(); + ~MockAnnotationAgentContainer() override; + + // blink::mojom::AnnotationAgentContainer + MOCK_METHOD4(CreateAgent, + void(mojo::PendingRemote<blink::mojom::AnnotationAgentHost>, + mojo::PendingReceiver<blink::mojom::AnnotationAgent>, + blink::mojom::AnnotationType, + const std::string& /*serialized_selector*/)); + + MOCK_METHOD2(CreateAgentFromSelection, + void(blink::mojom::AnnotationType, + CreateAgentFromSelectionCallback)); + + void Bind(mojo::ScopedMessagePipeHandle handle) { + is_bound_ = true; + receiver_.Bind( + mojo::PendingReceiver<blink::mojom::AnnotationAgentContainer>( + std::move(handle))); + } + + bool is_bound() const { return is_bound_; } + + private: + mojo::Receiver<blink::mojom::AnnotationAgentContainer> receiver_{this}; + bool is_bound_ = false; +}; + +// Similar to above but for the agent interface. +class MockAnnotationAgent : public blink::mojom::AnnotationAgent { + public: + MockAnnotationAgent(); + ~MockAnnotationAgent() override; + + // blink::mojom::AnnotationAgent + MOCK_METHOD0(ScrollIntoView, void()); + + mojo::PendingRemote<blink::mojom::AnnotationAgent> + BindNewPipeAndPassRemote() { + return receiver_.BindNewPipeAndPassRemote(); + } + + private: + mojo::Receiver<blink::mojom::AnnotationAgent> receiver_{this}; +}; + // A base test harness for User Notes unit tests. The harness sets up a note // service and exposes methods to create new note models, as well as methods to // create and manipulate note managers attached to mock pages. @@ -30,11 +84,19 @@ class UserNoteBaseTest : public content::RenderViewHostTestHarness { void TearDown() override; + // Called by SetUp. Creates a basic service with a null delegate and storage. + // Can be overridden to create a service with a delegate and / or storage. + virtual void CreateService(); + void AddNewNotesToService(size_t count); void AddPartialNotesToService(size_t count); - UserNoteManager* ConfigureNewManager(); + // Creates and returns a new UserNoteManager for a new WebContents. Callers + // can optionally pass a MockAnnotationAgentContainer which the new manager's + // annotation_agent_container_ will bind to. + UserNoteManager* ConfigureNewManager( + MockAnnotationAgentContainer* mock_container = nullptr); void AddNewInstanceToManager(UserNoteManager* manager, base::UnguessableToken note_id); diff --git a/chromium/components/user_notes/browser/user_note_instance.cc b/chromium/components/user_notes/browser/user_note_instance.cc index c55eb615b08..5ff6002bf03 100644 --- a/chromium/components/user_notes/browser/user_note_instance.cc +++ b/chromium/components/user_notes/browser/user_note_instance.cc @@ -10,45 +10,101 @@ namespace user_notes { UserNoteInstance::UserNoteInstance(base::SafeRef<UserNote> model, - UserNoteManager* parent_manager) - : UserNoteInstance(model, parent_manager, gfx::Rect()) {} - -UserNoteInstance::UserNoteInstance(base::SafeRef<UserNote> model, UserNoteManager* parent_manager, - gfx::Rect rect) - : model_(model), + PassKey pass_key) + : model_(std::move(model)), parent_manager_(parent_manager), - rect_(rect), receiver_(this) {} +UserNoteInstance::UserNoteInstance(base::SafeRef<UserNote> model, + UserNoteManager* parent_manager) + : UserNoteInstance(std::move(model), parent_manager, PassKey()) {} + +// static +std::unique_ptr<UserNoteInstance> UserNoteInstance::Create( + base::SafeRef<UserNote> model, + UserNoteManager* parent_manager) { + return std::make_unique<UserNoteInstance>(std::move(model), parent_manager, + PassKey()); +} + UserNoteInstance::~UserNoteInstance() = default; bool UserNoteInstance::IsDetached() const { - return is_initialized_ && rect_.IsEmpty() && + return finished_attachment_ && rect_.IsEmpty() && model_->target().type() == UserNoteTarget::TargetType::kPageText; } -void UserNoteInstance::InitializeHighlightIfNeeded(base::OnceClosure callback) { - DCHECK(!is_initialized_); - - if (model_->target().type() == UserNoteTarget::TargetType::kPage) { - // Page-level notes are not associated with text in the page, so there is no - // highlight to create on the renderer side. - is_initialized_ = true; - DCHECK(callback); - std::move(callback).Run(); - } else { - did_finish_attachment_callback_ = std::move(callback); - InitializeHighlightInternal(); +void UserNoteInstance::BindToHighlight( + mojo::PendingReceiver<blink::mojom::AnnotationAgentHost> host_receiver, + mojo::PendingRemote<blink::mojom::AnnotationAgent> agent_remote, + AttachmentFinishedCallback callback) { + DCHECK_EQ(model_->target().type(), UserNoteTarget::TargetType::kPageText); + DCHECK(!agent_.is_bound()); + DCHECK(!receiver_.is_bound()); + DCHECK(agent_remote.is_valid()); + DCHECK(host_receiver.is_valid()); + + did_finish_attachment_callback_ = std::move(callback); + agent_.Bind(std::move(agent_remote)); + // base::Unretained since note instances are always destroyed before the + // manager (the manager's destructor explicitly destroys them). + agent_.set_disconnect_handler( + base::BindOnce(&UserNoteManager::RemoveNote, + base::Unretained(parent_manager_), model_->id())); + + receiver_.Bind(std::move(host_receiver)); +} + +void UserNoteInstance::InitializeHighlightIfNeeded( + AttachmentFinishedCallback callback) { + // If the UserNoteInstance was instantiated to create a new note, the + // highlight will already be initialized in the renderer. In this case, + // BindToHighlight must already have been called and so a `callback` must not + // be passed to this method since a callback was already passed in + // BindToHighlight. + if (agent_.is_bound()) { + DCHECK(receiver_.is_bound()); + DCHECK(!callback); + DCHECK_EQ(model_->target().type(), UserNoteTarget::TargetType::kPageText); + DCHECK_EQ(finished_attachment_, did_finish_attachment_callback_.is_null()); + return; + } + + DCHECK(callback); + + switch (model_->target().type()) { + case UserNoteTarget::TargetType::kPage: { + // Page-level notes are not associated with text in the page, so there is + // no highlight to create on the renderer side. Also, this instance may + // already have been initialized as part of generating a new note. In + // these cases, do nothing. + std::move(callback).Run(); + } break; + case UserNoteTarget::TargetType::kPageText: { + DCHECK(!did_finish_attachment_callback_); + did_finish_attachment_callback_ = std::move(callback); + InitializeHighlightInternal(); + } break; } } +void UserNoteInstance::OnNoteSelected() { + if (!agent_) + return; + agent_->ScrollIntoView(); +} + void UserNoteInstance::DidFinishAttachment(const gfx::Rect& rect) { - is_initialized_ = true; + finished_attachment_ = true; rect_ = rect; - DCHECK(did_finish_attachment_callback_); - std::move(did_finish_attachment_callback_).Run(); + if (did_finish_attachment_callback_) + std::move(did_finish_attachment_callback_).Run(); +} + +void UserNoteInstance::OnWebHighlightFocused() { + parent_manager_->OnWebHighlightFocused(model_->id()); } void UserNoteInstance::OnNoteDetached() { @@ -67,7 +123,7 @@ void UserNoteInstance::InitializeHighlightInternal() { // Set a disconnect handler because the renderer can close the pipe at any // moment to signal that the highlight has been removed from the page. It's ok - // to use base::unretained here because the note instances are always + // to use base::Unretained here because the note instances are always // destroyed before the manager (the manager's destructor explicitly destroys // them). agent_.set_disconnect_handler( diff --git a/chromium/components/user_notes/browser/user_note_instance.h b/chromium/components/user_notes/browser/user_note_instance.h index 0f8ac9398ef..7428144d2f7 100644 --- a/chromium/components/user_notes/browser/user_note_instance.h +++ b/chromium/components/user_notes/browser/user_note_instance.h @@ -5,8 +5,11 @@ #ifndef COMPONENTS_USER_NOTES_BROWSER_USER_NOTE_INSTANCE_H_ #define COMPONENTS_USER_NOTES_BROWSER_USER_NOTE_INSTANCE_H_ +#include <memory> + #include "base/barrier_closure.h" #include "base/memory/safe_ref.h" +#include "base/types/pass_key.h" #include "components/user_notes/model/user_note.h" #include "mojo/public/cpp/bindings/receiver.h" #include "mojo/public/cpp/bindings/remote.h" @@ -21,15 +24,22 @@ class UserNoteManager; // page. class UserNoteInstance : public blink::mojom::AnnotationAgentHost { public: - // The main constructor. - UserNoteInstance(base::SafeRef<UserNote> model, - UserNoteManager* parent_manager); + using PassKey = base::PassKey<UserNoteInstance>; - // A constructor for when the bounding rect of the highlight is known in - // advance, for example during the note creation process. + // The callback type invoked when attachment in the renderer is completed. + using AttachmentFinishedCallback = base::OnceClosure; + + // Creates a UserNoteInstance. This instance will attach to a highlight in + // the renderer (if the note is a non-page note) when + // InitializeHighlightIfNeeded is called. + static std::unique_ptr<UserNoteInstance> Create( + base::SafeRef<UserNote> model, + UserNoteManager* parent_manager); + + // Use the Create static methods above. Public for use with std::make_unique. UserNoteInstance(base::SafeRef<UserNote> model, UserNoteManager* parent_manager, - gfx::Rect rect); + PassKey pass_key); ~UserNoteInstance() override; UserNoteInstance(const UserNoteInstance&) = delete; @@ -45,20 +55,39 @@ class UserNoteInstance : public blink::mojom::AnnotationAgentHost { // 3) Its bounding rect is empty. bool IsDetached() const; - // If this note is a text-level note, this method kicks off the asynchronous - // process to set up the Mojo connection with the corresponding agent in the - // renderer process. Otherwise, it invokes the provided callback. - // Marked virtual for tests to override. - virtual void InitializeHighlightIfNeeded(base::OnceClosure callback); + // Binds this instance to an existing highlight in the renderer. Must be + // called before the instance is added to the manager. + void BindToHighlight( + mojo::PendingReceiver<blink::mojom::AnnotationAgentHost> host_receiver, + mojo::PendingRemote<blink::mojom::AnnotationAgent> agent_remote, + AttachmentFinishedCallback callback); + + // If this note is a text-level note and hasn't yet been bound to a highlight + // (via BindToHighlight), this method kicks off the asynchronous process to + // set up the Mojo connection with the corresponding agent in the renderer + // process. Otherwise, it invokes the provided callback. Marked virtual for + // tests to override. + virtual void InitializeHighlightIfNeeded(AttachmentFinishedCallback callback); + + void OnNoteSelected(); // blink::mojom::AnnotationAgentHost implementation. void DidFinishAttachment(const gfx::Rect& rect) override; + // TODO(corising) and TODO(bokan): add the following method to + // AnnotationAgentHost interface once the caller is implemented in Blink. + void OnWebHighlightFocused(); + // TODO(gujen) and TODO(bokan): add the following method to the // AnnotationAgentHost interface so it's called when a note becomes detached. // Mark this one as override. void OnNoteDetached(); + protected: + // For mock descendants in tests since they can't instantiate the PassKey. + UserNoteInstance(base::SafeRef<UserNote> model, + UserNoteManager* parent_manager); + private: friend class UserNoteInstanceTest; @@ -79,11 +108,12 @@ class UserNoteInstance : public blink::mojom::AnnotationAgentHost { gfx::Rect rect_; // Callback to invoke after the renderer agent has initialized. - base::OnceClosure did_finish_attachment_callback_; + AttachmentFinishedCallback did_finish_attachment_callback_; - // Stores whether this note instance has had its highlight initialized in the - // renderer process. - bool is_initialized_ = false; + // Stores whether this note instance has had its highlight initialized + // ("attached") in the renderer process. This will be true, even if + // attachment failed. + bool finished_attachment_ = false; // Receiver and agent for communication with the renderer process. mojo::Receiver<blink::mojom::AnnotationAgentHost> receiver_; diff --git a/chromium/components/user_notes/browser/user_note_instance_unittest.cc b/chromium/components/user_notes/browser/user_note_instance_unittest.cc index 70fe08739e0..55543c03e95 100644 --- a/chromium/components/user_notes/browser/user_note_instance_unittest.cc +++ b/chromium/components/user_notes/browser/user_note_instance_unittest.cc @@ -58,7 +58,7 @@ class UserNoteInstanceTest : public UserNoteBaseTest { // Original text, target URL and selector are not used in these tests. auto target = std::make_unique<UserNoteTarget>( - type, /*original_text=*/"", GURL("https://www.example.com/"), + type, /*original_text=*/u"", GURL("https://www.example.com/"), /*selector=*/""); UserNoteService::ModelMapEntry entry( std::make_unique<UserNote>(id, GetTestUserNoteMetadata(), @@ -75,8 +75,8 @@ class UserNoteInstanceTest : public UserNoteBaseTest { entry_it->second.model->GetSafeRef(), manager, simulate_attach_rect); } - bool IsNoteInstanceInitialized(UserNoteInstance* instance) { - return instance->is_initialized_; + bool IsAttachmentFinished(UserNoteInstance* instance) { + return instance->finished_attachment_; } }; @@ -99,7 +99,7 @@ TEST_F(UserNoteInstanceTest, InitializeHighlightSkipForPageLevelNote) { // The mocks ensure the callback is invoked synchronously, so verifications // can happen immediately. EXPECT_TRUE(callback_called); - EXPECT_TRUE(IsNoteInstanceInitialized(instance.get())); + EXPECT_FALSE(IsAttachmentFinished(instance.get())); EXPECT_EQ(instance->rect(), empty_rect); } @@ -124,7 +124,7 @@ TEST_F(UserNoteInstanceTest, InitializeHighlightTextNote) { // The mocks ensure the callback is invoked synchronously, so verifications // can happen immediately. EXPECT_TRUE(callback_called); - EXPECT_TRUE(IsNoteInstanceInitialized(instance.get())); + EXPECT_TRUE(IsAttachmentFinished(instance.get())); EXPECT_EQ(instance->rect(), rect); } diff --git a/chromium/components/user_notes/browser/user_note_manager.cc b/chromium/components/user_notes/browser/user_note_manager.cc index 915e1b8175d..3f8e1e0bd05 100644 --- a/chromium/components/user_notes/browser/user_note_manager.cc +++ b/chromium/components/user_notes/browser/user_note_manager.cc @@ -14,7 +14,7 @@ namespace user_notes { UserNoteManager::UserNoteManager(content::Page& page, base::SafeRef<UserNoteService> service) - : PageUserData<UserNoteManager>(page), service_(service) { + : PageUserData<UserNoteManager>(page), service_(std::move(service)) { // TODO(crbug.com/1313967): If / when user notes are supported in subframes, // caching the agent container of the primary main frame will not work. In // that case, the frame's container will probably need to be fetched on each @@ -62,13 +62,17 @@ void UserNoteManager::RemoveNote(const base::UnguessableToken& id) { instance_map_.erase(entry_it); } +void UserNoteManager::OnWebHighlightFocused(const base::UnguessableToken& id) { + service_->OnWebHighlightFocused(id, &page().GetMainDocument()); +} + void UserNoteManager::AddNoteInstance(std::unique_ptr<UserNoteInstance> note) { AddNoteInstance(std::move(note), base::DoNothing()); } void UserNoteManager::AddNoteInstance( std::unique_ptr<UserNoteInstance> note_instance, - base::OnceClosure initialize_callback) { + UserNoteInstance::AttachmentFinishedCallback initialize_callback) { // TODO(crbug.com/1313967): This DCHECK is only applicable if notes are only // supported in the top-level frame. If notes are ever supported in subframes, // it is possible for the same note ID to be added to the same page more than @@ -86,6 +90,11 @@ void UserNoteManager::AddNoteInstance( std::move(initialize_callback)); } +void UserNoteManager::OnAddNoteRequested(content::RenderFrameHost* frame, + bool has_selected_text) { + service_->OnAddNoteRequested(frame, has_selected_text); +} + PAGE_USER_DATA_KEY_IMPL(UserNoteManager); } // namespace user_notes diff --git a/chromium/components/user_notes/browser/user_note_manager.h b/chromium/components/user_notes/browser/user_note_manager.h index 9482866c2c2..46e55ed2a4d 100644 --- a/chromium/components/user_notes/browser/user_note_manager.h +++ b/chromium/components/user_notes/browser/user_note_manager.h @@ -21,6 +21,7 @@ namespace content { class Page; +class RenderFrameHost; } // namespace content namespace user_notes { @@ -35,8 +36,7 @@ class UserNoteManager : public content::PageUserData<UserNoteManager> { UserNoteManager(const UserNoteManager&) = delete; UserNoteManager& operator=(const UserNoteManager&) = delete; - const mojo::Remote<blink::mojom::AnnotationAgentContainer>& - note_agent_container() { + mojo::Remote<blink::mojom::AnnotationAgentContainer>& note_agent_container() { return note_agent_container_; }; @@ -50,14 +50,22 @@ class UserNoteManager : public content::PageUserData<UserNoteManager> { // Destroys the note instance associated with the given GUID. void RemoveNote(const base::UnguessableToken& id); + // Notifies the service that the web highlight has been focused for the given + // id and RenderFrameHost. + void OnWebHighlightFocused(const base::UnguessableToken& id); + // Stores the given note instance into this object's instance map, then kicks // off its asynchronous initialization in the renderer process, passing it the // provided callback for when it finishes. // TODO(gujen): Remove the overload without the callback after tests are // fixed. void AddNoteInstance(std::unique_ptr<UserNoteInstance> note); - void AddNoteInstance(std::unique_ptr<UserNoteInstance> note, - base::OnceClosure initialize_callback); + void AddNoteInstance( + std::unique_ptr<UserNoteInstance> note, + UserNoteInstance::AttachmentFinishedCallback initialize_callback); + + void OnAddNoteRequested(content::RenderFrameHost* frame, + bool has_selected_text); private: friend class content::PageUserData<UserNoteManager>; diff --git a/chromium/components/user_notes/browser/user_note_service.cc b/chromium/components/user_notes/browser/user_note_service.cc index c6a70fcc413..6f08bf35555 100644 --- a/chromium/components/user_notes/browser/user_note_service.cc +++ b/chromium/components/user_notes/browser/user_note_service.cc @@ -4,18 +4,29 @@ #include "components/user_notes/browser/user_note_service.h" +#include "base/bind.h" #include "base/notreached.h" +#include "base/strings/utf_string_conversions.h" +#include "components/user_notes/browser/frame_user_note_changes.h" #include "components/user_notes/browser/user_note_manager.h" +#include "components/user_notes/browser/user_note_utils.h" +#include "components/user_notes/interfaces/user_note_metadata_snapshot.h" #include "components/user_notes/interfaces/user_notes_ui.h" #include "components/user_notes/user_notes_features.h" #include "content/public/browser/render_frame_host.h" +#include "content/public/browser/weak_document_ptr.h" #include "ui/gfx/geometry/rect.h" namespace user_notes { UserNoteService::UserNoteService( - std::unique_ptr<UserNoteServiceDelegate> delegate) - : delegate_(std::move(delegate)) {} + std::unique_ptr<UserNoteServiceDelegate> delegate, + std::unique_ptr<UserNoteStorage> storage) + : delegate_(std::move(delegate)), storage_(std::move(storage)) { + // storage_ can be null in tests. + if (storage_) + storage_->AddObserver(this); +} UserNoteService::~UserNoteService() = default; @@ -34,6 +45,13 @@ bool UserNoteService::IsNoteInProgress(const base::UnguessableToken& id) const { } void UserNoteService::OnFrameNavigated(content::RenderFrameHost* rfh) { + // TODO(crbug.com/1313967): On browser startup, this method will be called + // once for each tab that's being restored, potentially slowing down the + // startup process and delaying browser responsiveness. This method should + // probably be disabled during browser startup and re-enabled after all tabs + // have been restored, so that note fetching for all restored tabs can be + // batched into a single operation. + DCHECK(IsUserNotesEnabled()); // For now, Notes are only supported in the main frame. @@ -50,7 +68,13 @@ void UserNoteService::OnFrameNavigated(content::RenderFrameHost* rfh) { } DCHECK(UserNoteManager::GetForPage(rfh->GetPage())); - NOTIMPLEMENTED(); + + std::vector<content::WeakDocumentPtr> frames = {rfh->GetWeakDocumentPtr()}; + UserNoteStorage::UrlSet urls = {rfh->GetLastCommittedURL()}; + storage_->GetNoteMetadataForUrls( + std::move(urls), + base::BindOnce(&UserNoteService::OnNoteMetadataFetchedForNavigation, + weak_ptr_factory_.GetWeakPtr(), frames)); } void UserNoteService::OnNoteInstanceAddedToPage( @@ -104,66 +128,92 @@ void UserNoteService::OnNoteInstanceRemovedFromPage( } void UserNoteService::OnAddNoteRequested(content::RenderFrameHost* frame, - std::string original_text, - std::string selector, - gfx::Rect rect) { + bool has_selected_text) { DCHECK(IsUserNotesEnabled()); DCHECK(frame); UserNoteManager* manager = UserNoteManager::GetForPage(frame->GetPage()); DCHECK(manager); - // TODO(gujen): This partial note creation logic will be moved to an API - // exposed by the storage layer in order to keep the creation of UserNote - // models centralized. However, until the storage layer is finished, manually - // create a partial note here. - base::Time now = base::Time::Now(); - int note_version = 1; - auto metadata = std::make_unique<UserNoteMetadata>(now, now, note_version); - auto body = std::make_unique<UserNoteBody>(/*plain_text_value=*/""); - auto target = std::make_unique<UserNoteTarget>( - UserNoteTarget::TargetType::kPageText, original_text, - GURL(frame->GetLastCommittedURL()), selector); - auto partial_note = std::make_unique<UserNote>( - base::UnguessableToken::Create(), std::move(metadata), std::move(body), - std::move(target)); - UserNote* partial_note_raw = partial_note.get(); - - // Store the partial note model into the creation map (not the model map) - // until it is finalized. - UserNoteService::ModelMapEntry entry(std::move(partial_note)); - entry.managers.emplace(manager); - DCHECK(creation_map_.find(entry.model->id()) == creation_map_.end()) - << "Attempted to create a partial note that already exists"; - creation_map_.emplace(entry.model->id(), std::move(entry)); - - // Create an instance for this note so the highlight can be shown in the page, - // and add it to the page's note manager. The instance's initialization does - // not need to be awaited, since the highlight's rect is already known. - auto instance = std::make_unique<UserNoteInstance>( - partial_note_raw->GetSafeRef(), manager, rect); - UserNoteInstance* instance_raw = instance.get(); - manager->AddNoteInstance(std::move(instance), base::DoNothing()); + // TODO(crbug.com/1313967): `has_selected_text` is used to determine whether + // or not to create a page-level note. This will need to be reassessed when + // page-level UX is finalized. + if (has_selected_text) { + auto create_agent_callback = base::BindOnce( + [](base::SafeRef<UserNoteService> service, + content::WeakDocumentPtr document, + mojo::PendingReceiver<blink::mojom::AnnotationAgentHost> + host_receiver, + mojo::PendingRemote<blink::mojom::AnnotationAgent> agent_remote, + const std::string& serialized_selector, + const std::u16string& selected_text) { + if (agent_remote.is_valid() != host_receiver.is_valid()) { + mojo::ReportBadMessage( + "User note creation received only one invalid remote/receiver"); + return; + } + + if (agent_remote.is_valid() == serialized_selector.empty()) { + mojo::ReportBadMessage( + "User note creation received unexpected selector for mojo " + "binding result"); + return; + } + + if (agent_remote.is_valid() == selected_text.empty()) { + mojo::ReportBadMessage( + "User note creation received unexpected text for mojo binding " + "result"); + return; + } + + service->InitializeNewNoteForCreation( + document, /*is_page_level=*/false, std::move(host_receiver), + std::move(agent_remote), serialized_selector, selected_text); + }, + // SafeRef is safe since the service owns the UserNoteManager which + // owns the mojo binding so if we receive this callback both manager + // and service must still be live. + weak_ptr_factory_.GetSafeRef(), frame->GetWeakDocumentPtr()); + + manager->note_agent_container()->CreateAgentFromSelection( + blink::mojom::AnnotationType::kUserNote, + std::move(create_agent_callback)); + } else { + InitializeNewNoteForCreation(frame->GetWeakDocumentPtr(), + /*is_page_level=*/true, mojo::NullReceiver(), + mojo::NullRemote(), + /*serialized_selector=*/"", + /*selected_text=*/std::u16string()); + } +} - // Finally, notify the UI layer that it should start the note creation UX for - // this note. The UI layer will eventually call either `OnNoteCreationDone` or - // `OnNoteCreationCancelled`, in which the partial note will be finalized or - // deleted, respectively. - UserNotesUI* ui = delegate_->GetUICoordinatorForFrame(frame); - ui->StartNoteCreation(instance_raw); +void UserNoteService::OnWebHighlightFocused(const base::UnguessableToken& id, + content::RenderFrameHost* rfh) { + DCHECK(IsUserNotesEnabled()); + DCHECK(rfh); + UserNotesUI* ui = delegate_->GetUICoordinatorForFrame(rfh); + DCHECK(ui); + ui->FocusNote(id); } -void UserNoteService::OnNoteFocused(const base::UnguessableToken& id) { +void UserNoteService::OnNoteSelected(const base::UnguessableToken& id, + content::RenderFrameHost* rfh) { DCHECK(IsUserNotesEnabled()); - NOTIMPLEMENTED(); + DCHECK(rfh); + UserNoteManager* manager = UserNoteManager::GetForPage(rfh->GetPage()); + DCHECK(manager); + UserNoteInstance* note_instance = manager->GetNoteInstance(id); + DCHECK(note_instance); + note_instance->OnNoteSelected(); } void UserNoteService::OnNoteDeleted(const base::UnguessableToken& id) { DCHECK(IsUserNotesEnabled()); - NOTIMPLEMENTED(); + storage_->DeleteNote(id); } void UserNoteService::OnNoteCreationDone(const base::UnguessableToken& id, - const std::string& note_content) { + const std::u16string& note_content) { DCHECK(IsUserNotesEnabled()); // Retrieve the partial note from the creation map and send it to the storage @@ -174,11 +224,10 @@ void UserNoteService::OnNoteCreationDone(const base::UnguessableToken& id, const auto& creation_entry_it = creation_map_.find(id); DCHECK(creation_entry_it != creation_map_.end()) << "Attempted to complete the creation of a note that doesn't exist"; - // TODO(gujen): Call - // UserNoteStorage::UpdateNote(entry.model, content, /*is_creation=*/true). - - // TODO(gujen): Make sure to transfer the model from the creation map to the - // model map in the OnNotesChanged() event sent by the storage layer. + const UserNote* note = creation_entry_it->second.model.get(); + if (!note) + return; + storage_->UpdateNote(note, note_content, /*is_creation=*/true); } void UserNoteService::OnNoteCreationCancelled( @@ -198,10 +247,276 @@ void UserNoteService::OnNoteCreationCancelled( (*entry_it->second.managers.begin())->RemoveNote(id); } -void UserNoteService::OnNoteUpdated(const base::UnguessableToken& id, - const std::string& note_content) { +void UserNoteService::OnNoteEdited(const base::UnguessableToken& id, + const std::u16string& note_content) { DCHECK(IsUserNotesEnabled()); - NOTIMPLEMENTED(); + const UserNote* note = GetNoteModel(id); + if (!note) + return; + storage_->UpdateNote(note, note_content); +} + +void UserNoteService::OnNotesChanged() { + std::vector<content::RenderFrameHost*> all_frames = + delegate_->GetAllFramesForUserNotes(); + UserNoteStorage::UrlSet urls; + std::vector<content::WeakDocumentPtr> all_frames_weak; + all_frames_weak.reserve(all_frames.size()); + + for (content::RenderFrameHost* frame : all_frames) { + urls.emplace(frame->GetLastCommittedURL()); + all_frames_weak.emplace_back(frame->GetWeakDocumentPtr()); + } + + storage_->GetNoteMetadataForUrls( + std::move(urls), + base::BindOnce(&UserNoteService::OnNoteMetadataFetched, + weak_ptr_factory_.GetWeakPtr(), all_frames_weak)); +} + +void UserNoteService::InitializeNewNoteForCreation( + content::WeakDocumentPtr document, + bool is_page_level, + mojo::PendingReceiver<blink::mojom::AnnotationAgentHost> host_receiver, + mojo::PendingRemote<blink::mojom::AnnotationAgent> agent_remote, + const std::string& serialized_selector, + const std::u16string& selected_text) { + content::RenderFrameHost* frame = document.AsRenderFrameHostIfValid(); + if (!frame) + return; + + UserNoteManager* manager = UserNoteManager::GetForPage(frame->GetPage()); + DCHECK(manager); + + // If attachment succeeded, the returned mojo endpoints must all be valid and + // the selector/text must be non empty. If attachment failed (or wasn't + // attempted since the note is a kPage type) these will all be invalid/empty. + bool has_renderer_agent = agent_remote.is_valid(); + + DCHECK_EQ(has_renderer_agent, host_receiver.is_valid()); + DCHECK_NE(has_renderer_agent, serialized_selector.empty()); + DCHECK_NE(has_renderer_agent, selected_text.empty()); + + // If this is a page-level note, we must not have a renderer agent. If we + // received a renderer agent, it must be a text-level note. + DCHECK(!is_page_level || !has_renderer_agent); + DCHECK(!has_renderer_agent || !is_page_level); + + // If this is a text-targeted note and we didn't receive back an agent, + // selector generation must have failed. For now, simply abort. + // TODO(crbug.com/1313967): Decide how to handle the case where a selector + // for the selected text couldn't be generated. ( + if (!is_page_level && !has_renderer_agent) + return; + + auto target = std::make_unique<UserNoteTarget>( + is_page_level ? UserNoteTarget::TargetType::kPage + : UserNoteTarget::TargetType::kPageText, + selected_text, GURL(frame->GetLastCommittedURL()), serialized_selector); + + // TODO(gujen): This partial note creation logic will be moved to an API + // exposed by the storage layer in order to keep the creation of UserNote + // models centralized. However, until the storage layer is finished, manually + // create a partial note here. + base::Time now = base::Time::Now(); + int note_version = 1; + auto metadata = std::make_unique<UserNoteMetadata>(now, now, note_version); + auto body = std::make_unique<UserNoteBody>(/*plain_text_value=*/u""); + + auto partial_note = std::make_unique<UserNote>( + base::UnguessableToken::Create(), std::move(metadata), std::move(body), + std::move(target)); + + std::unique_ptr<UserNoteInstance> instance = + UserNoteInstance::Create(partial_note->GetSafeRef(), manager); + + // When attachment completes the instance will have received its rect on the + // page so the UI note creation flow can begin at that point. Note, this + // callback is guaranteed to be invoked after the current method returns (so + // after the creation map is updated and instance added to its manager). + auto attachment_finished_callback = base::BindOnce( + [](base::SafeRef<UserNoteService> service, + content::WeakDocumentPtr document, UserNoteInstance& instance) { + content::RenderFrameHost* frame = document.AsRenderFrameHostIfValid(); + + // TODO(bokan): delegate_ can be nullptr in unit tests - should we mock + // it? + if (!service->delegate_ || !frame) + return; + + // Finally, notify the UI layer that it should start the note creation + // UX for this note. The UI layer will eventually call either + // `OnNoteCreationDone` or `OnNoteCreationCancelled`, in which the + // partial note will be finalized or deleted, respectively. + if (UserNotesUI* ui = + service->delegate_->GetUICoordinatorForFrame(frame)) { + ui->StartNoteCreation(&instance); + } + }, + // SafeRef is safe for the service since it owns the manager which owns + // the instance. + weak_ptr_factory_.GetSafeRef(), frame->GetWeakDocumentPtr(), + // std::ref is safe since it owns the mojo endpoint which will cause this + // invocation (so if it's deleted this callback won't be invoked). + std::ref(*instance)); + + if (!is_page_level) { + DCHECK(has_renderer_agent); + instance->BindToHighlight(std::move(host_receiver), std::move(agent_remote), + std::move(attachment_finished_callback)); + // Silence use-after-move warning; if this path is taken, we want to pass a + // null callback in AddNoteInstance. + attachment_finished_callback = base::NullCallback(); + } + + // Store the partial note model into the creation map (not the model map) + // until it is finalized. + UserNoteService::ModelMapEntry entry(std::move(partial_note)); + entry.managers.emplace(manager); + DCHECK(creation_map_.find(entry.model->id()) == creation_map_.end()) + << "Attempted to create a partial note that already exists"; + creation_map_.emplace(entry.model->id(), std::move(entry)); + + manager->AddNoteInstance(std::move(instance), + std::move(attachment_finished_callback)); +} + +void UserNoteService::OnNoteMetadataFetchedForNavigation( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot) { + DCHECK(all_frames.size() == 1u); + + content::RenderFrameHost* rfh = all_frames[0].AsRenderFrameHostIfValid(); + + if (!rfh) { + // The navigated frame is no longer valid. + return; + } + + if (delegate_->IsFrameInActiveTab(rfh)) { + UserNotesUI* ui = delegate_->GetUICoordinatorForFrame(rfh); + DCHECK(ui); + + // TODO(crbug.com/1313967): For now, always invalidate the UI if the tab is + // in the foreground. This is to fix edge cases around back/forward + // navigations, where the Page (and attached UserNoteManager) is kept alive + // in the BFCache. If the notes didn't change on disk by the time the user + // does a back/forward navigation, Invalidate() will never get called + // because there won't be any diff between the instances in the Page and the + // notes on disk. Ideally, Invalidate() should only be called if this is a + // back/forward navigation and the notes didn't change, but there's no way + // to know whether the notes changed until further down the callback stack. + // Since Invalidate() is cheap enough, always calling it here is considered + // an acceptable fix for now. + ui->Invalidate(); + + if (!metadata_snapshot.IsEmpty()) { + // TODO(crbug.com/1313967): For now, automatically activate User Notes UI + // when the user navigates to a page with notes. Before launch though, + // this should be changed to a popup / notification that the user must + // interact with to launch the notes UI. + ui->Show(); + } + } + + if (!metadata_snapshot.IsEmpty()) { + OnNoteMetadataFetched(all_frames, std::move(metadata_snapshot)); + } +} + +void UserNoteService::OnNoteMetadataFetched( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot) { + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes = + CalculateNoteChanges(*this, all_frames, metadata_snapshot); + + // All added and modified notes must be fetched from storage to eventually be + // put in the model map. For removed notes there is no need to update the + // model map at this point; it will be done later when applying the changes. + IdSet notes_to_fetch; + IdSet new_notes; + + for (const std::unique_ptr<FrameUserNoteChanges>& diff : note_changes) { + for (const base::UnguessableToken& note_id : diff->notes_added()) { + notes_to_fetch.emplace(note_id); + new_notes.emplace(note_id); + } + for (const base::UnguessableToken& note_id : diff->notes_modified()) { + notes_to_fetch.emplace(note_id); + } + } + + storage_->GetNotesById( + std::move(notes_to_fetch), + base::BindOnce(&UserNoteService::OnNoteModelsFetched, + weak_ptr_factory_.GetWeakPtr(), std::move(new_notes), + std::move(note_changes))); +} + +void UserNoteService::OnNoteModelsFetched( + const IdSet& new_notes, + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes, + std::vector<std::unique_ptr<UserNote>> notes) { + // Update the model map with the new models. + for (std::unique_ptr<UserNote>& note : notes) { + base::UnguessableToken id = note->id(); + const auto& new_note_it = new_notes.find(id); + const auto& creation_entry_it = creation_map_.find(id); + const auto& model_entry_it = model_map_.find(id); + + if (creation_entry_it != creation_map_.end()) { + // This note was authored locally. It could also be in the list of new + // notes if the URL it's attached to was loaded in multiple tabs, but it + // cannot exist in the model map yet. Move it there from the creation map. + DCHECK(model_entry_it == model_map_.end()); + creation_entry_it->second.model->Update(std::move(note)); + model_map_.emplace(id, std::move(creation_entry_it->second)); + creation_map_.erase(creation_entry_it); + } else if (new_note_it == new_notes.end() || + model_entry_it != model_map_.end()) { + // Either this note was updated or the URL it is attached to was already + // loaded in another tab. Either way, its model already exists in the + // model map, so simply update it with the latest model. + DCHECK(creation_entry_it == creation_map_.end()); + DCHECK(model_entry_it != model_map_.end()); + model_entry_it->second.model->Update(std::move(note)); + } else { + // This is a new note that wasn't authored locally. Simply add the model + // to the model map. + DCHECK(new_note_it != new_notes.end()); + DCHECK(model_entry_it == model_map_.end()); + UserNoteService::ModelMapEntry entry(std::move(note)); + model_map_.emplace(id, std::move(entry)); + } + } + + // Now that the creation and model maps have been updated, apply all the diffs + // to propagate the changes to the webpages and UI. + for (std::unique_ptr<FrameUserNoteChanges>& diff : note_changes) { + FrameUserNoteChanges* diff_raw = diff.get(); + note_changes_in_progress_.emplace(diff->id(), std::move(diff)); + diff_raw->Apply(base::BindOnce(&UserNoteService::OnFrameChangesApplied, + weak_ptr_factory_.GetWeakPtr(), + diff_raw->id())); + } +} + +void UserNoteService::OnFrameChangesApplied(base::UnguessableToken change_id) { + const auto& changes_it = note_changes_in_progress_.find(change_id); + DCHECK(changes_it != note_changes_in_progress_.end()); + + // If this set of changes was for a page that's in an active tab, notify the + // UI to reload the notes it's displaying. + const std::unique_ptr<FrameUserNoteChanges>& frame_changes = + changes_it->second; + if (delegate_->IsFrameInActiveTab(frame_changes->render_frame_host())) { + UserNotesUI* ui = + delegate_->GetUICoordinatorForFrame(frame_changes->render_frame_host()); + DCHECK(ui); + ui->Invalidate(); + } + + note_changes_in_progress_.erase(changes_it); } UserNoteService::ModelMapEntry::ModelMapEntry(std::unique_ptr<UserNote> model) diff --git a/chromium/components/user_notes/browser/user_note_service.h b/chromium/components/user_notes/browser/user_note_service.h index 277e0e5beba..dbd76e1e7c2 100644 --- a/chromium/components/user_notes/browser/user_note_service.h +++ b/chromium/components/user_notes/browser/user_note_service.h @@ -11,14 +11,20 @@ #include <string> #include <unordered_map> #include <unordered_set> +#include <vector> #include "base/gtest_prod_util.h" #include "base/memory/safe_ref.h" #include "base/memory/weak_ptr.h" #include "base/unguessable_token.h" #include "components/user_notes/interfaces/user_note_service_delegate.h" +#include "components/user_notes/interfaces/user_note_storage.h" #include "components/user_notes/interfaces/user_notes_ui_delegate.h" #include "components/user_notes/model/user_note.h" +#include "content/public/browser/weak_document_ptr.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "third_party/blink/public/mojom/annotation/annotation.mojom.h" class UserNoteUICoordinatorTest; @@ -26,19 +32,23 @@ namespace content { class RenderFrameHost; } // namespace content -namespace gfx { -class Rect; -} // namespace gfx - namespace user_notes { +class FrameUserNoteChanges; class UserNoteManager; +class UserNoteMetadataSnapshot; // Keyed service coordinating the different parts (Renderer, UI layer, storage // layer) of the User Notes feature for the current user profile. -class UserNoteService : public KeyedService, public UserNotesUIDelegate { +class UserNoteService : public KeyedService, + public UserNotesUIDelegate, + public UserNoteStorage::Observer { public: - explicit UserNoteService(std::unique_ptr<UserNoteServiceDelegate> delegate); + using IdSet = + std::unordered_set<base::UnguessableToken, base::UnguessableTokenHash>; + + UserNoteService(std::unique_ptr<UserNoteServiceDelegate> delegate, + std::unique_ptr<UserNoteStorage> storage); ~UserNoteService() override; UserNoteService(const UserNoteService&) = delete; UserNoteService& operator=(const UserNoteService&) = delete; @@ -73,23 +83,26 @@ class UserNoteService : public KeyedService, public UserNotesUIDelegate { // Called by a note manager when the user selects "Add a note" from the // associated page's context menu. Kicks off the note creation process. - // TODO(gujen) and TODO(bokan): Make the renderer notify the manager and the - // manager call this method. Also consider pre-creating the agent on the - // renderer side so it is immediately ready when the browser side receives the - // request. void OnAddNoteRequested(content::RenderFrameHost* frame, - std::string original_text, - std::string selector, - gfx::Rect rect); + bool has_selected_text); + + // Called by a note manager when a user selects a web highlight in the page. + // This causes the associated note to become focused in the UserNotesUI. + void OnWebHighlightFocused(const base::UnguessableToken& id, + content::RenderFrameHost* rfh); // UserNotesUIDelegate implementation. - void OnNoteFocused(const base::UnguessableToken& id) override; + void OnNoteSelected(const base::UnguessableToken& id, + content::RenderFrameHost* rfh) override; void OnNoteDeleted(const base::UnguessableToken& id) override; void OnNoteCreationDone(const base::UnguessableToken& id, - const std::string& note_content) override; + const std::u16string& note_content) override; void OnNoteCreationCancelled(const base::UnguessableToken& id) override; - void OnNoteUpdated(const base::UnguessableToken& id, - const std::string& note_content) override; + void OnNoteEdited(const base::UnguessableToken& id, + const std::u16string& note_content) override; + + // UserNoteStorage implementation + void OnNotesChanged() override; private: struct ModelMapEntry { @@ -103,10 +116,45 @@ class UserNoteService : public KeyedService, public UserNotesUIDelegate { std::unordered_set<UserNoteManager*> managers; }; + friend class MockUserNoteService; friend class UserNoteBaseTest; friend class UserNoteInstanceTest; friend class UserNoteUtilsTest; friend class ::UserNoteUICoordinatorTest; + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationSomeNotes); + FRIEND_TEST_ALL_PREFIXES( + UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationSomeNotesBackground); + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationNoNotes); + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationNoNotesBackground); + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, OnNoteMetadataFetched); + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, OnNoteModelsFetched); + FRIEND_TEST_ALL_PREFIXES(UserNoteServiceTest, OnFrameChangesApplied); + + void InitializeNewNoteForCreation( + content::WeakDocumentPtr document, + bool is_page_level, + mojo::PendingReceiver<blink::mojom::AnnotationAgentHost> host_receiver, + mojo::PendingRemote<blink::mojom::AnnotationAgent> agent_remote, + const std::string& serialized_selector, + const std::u16string& selected_text); + + // Private helpers used when processing note storage changes. Marked virtual + // for tests to override. + virtual void OnNoteMetadataFetchedForNavigation( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot); + virtual void OnNoteMetadataFetched( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot); + virtual void OnNoteModelsFetched( + const IdSet& new_notes, + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes, + std::vector<std::unique_ptr<UserNote>> notes); + virtual void OnFrameChangesApplied(base::UnguessableToken change_id); // Source of truth for the in-memory note models. Any note currently being // displayed in a tab is stored in this data structure. Each entry also @@ -127,7 +175,15 @@ class UserNoteService : public KeyedService, public UserNotesUIDelegate { base::UnguessableTokenHash> creation_map_; + // A place to store the user note changes of a frame while they are being + // applied. + std::unordered_map<base::UnguessableToken, + std::unique_ptr<FrameUserNoteChanges>, + base::UnguessableTokenHash> + note_changes_in_progress_; + std::unique_ptr<UserNoteServiceDelegate> delegate_; + std::unique_ptr<UserNoteStorage> storage_; base::WeakPtrFactory<UserNoteService> weak_ptr_factory_{this}; }; diff --git a/chromium/components/user_notes/browser/user_note_service_unittest.cc b/chromium/components/user_notes/browser/user_note_service_unittest.cc index ec981c468f3..d5904cb9c59 100644 --- a/chromium/components/user_notes/browser/user_note_service_unittest.cc +++ b/chromium/components/user_notes/browser/user_note_service_unittest.cc @@ -4,22 +4,296 @@ #include "components/user_notes/browser/user_note_service.h" +#include <algorithm> +#include <memory> #include <vector> #include "base/unguessable_token.h" +#include "components/user_notes/browser/frame_user_note_changes.h" #include "components/user_notes/browser/user_note_base_test.h" #include "components/user_notes/browser/user_note_manager.h" #include "components/user_notes/browser/user_note_service.h" +#include "components/user_notes/interfaces/user_note_service_delegate.h" +#include "components/user_notes/interfaces/user_notes_ui.h" +#include "components/user_notes/model/user_note_metadata.h" +#include "components/user_notes/model/user_note_model_test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using testing::_; +using testing::Invoke; +using testing::Mock; namespace user_notes { +using IdList = std::vector<base::UnguessableToken>; + +class MockUserNoteServiceDelegate : public UserNoteServiceDelegate { + public: + MOCK_METHOD(std::vector<content::RenderFrameHost*>, + GetAllFramesForUserNotes, + (), + (override)); + MOCK_METHOD(UserNotesUI*, + GetUICoordinatorForFrame, + (const content::RenderFrameHost* rfh), + (override)); + MOCK_METHOD(bool, + IsFrameInActiveTab, + (const content::RenderFrameHost* rfh), + (override)); + + void SetFramesForUserNotes( + const std::vector<content::RenderFrameHost*>& frames) { + frames_ = frames; + } + + std::vector<content::RenderFrameHost*> MockGetAllFramesForUserNotes() { + return frames_; + } + + private: + std::vector<content::RenderFrameHost*> frames_; +}; + +class MockUserNoteStorage : public UserNoteStorage { + public: + MOCK_METHOD(void, + GetNoteMetadataForUrls, + (const UserNoteStorage::UrlSet& urls, + base::OnceCallback<void(UserNoteMetadataSnapshot)> callback), + (override)); + + MOCK_METHOD(void, + GetNotesById, + (const UserNoteStorage::IdSet& ids, + base::OnceCallback<void(std::vector<std::unique_ptr<UserNote>>)> + callback), + (override)); + + // The following must be mocked even though they're not used in the tests, + // because they're abstract. + MOCK_METHOD(void, + UpdateNote, + (const UserNote* model, + std::u16string note_body_text, + bool is_creation), + (override)); + + MOCK_METHOD(void, + DeleteNote, + (const base::UnguessableToken& guid), + (override)); + + MOCK_METHOD(void, DeleteAllForUrl, (const GURL& url), (override)); + + MOCK_METHOD(void, + DeleteAllForOrigin, + (const url::Origin& origin), + (override)); + + MOCK_METHOD(void, DeleteAllNotes, (), (override)); + + MOCK_METHOD(void, AddObserver, (Observer * observer), (override)); + MOCK_METHOD(void, RemoveObserver, (Observer * observer), (override)); + + const UserNoteStorage::UrlSet& requested_metadata_urls() { + return requested_metadata_urls_; + } + const UserNoteStorage::IdSet& requested_model_ids() { + return requested_model_ids_; + } + + void MockGetNoteMetadataForUrls( + const UserNoteStorage::UrlSet& urls, + base::OnceCallback<void(UserNoteMetadataSnapshot)> callback) { + requested_metadata_urls_ = urls; + std::move(callback).Run(UserNoteMetadataSnapshot()); + } + + void MockGetNotesById( + const UserNoteStorage::IdSet& ids, + base::OnceCallback<void(std::vector<std::unique_ptr<UserNote>>)> + callback) { + requested_model_ids_ = ids; + std::move(callback).Run({}); + } + + private: + UserNoteStorage::UrlSet requested_metadata_urls_; + UserNoteStorage::IdSet requested_model_ids_; +}; + +// Partially mock the object under test so tests can control side effects. +class MockUserNoteService : public UserNoteService { + public: + MockUserNoteService(std::unique_ptr<UserNoteServiceDelegate> delegate, + std::unique_ptr<UserNoteStorage> storage) + : UserNoteService(std::move(delegate), std::move(storage)) {} + + const UserNoteService::IdSet& computed_new_notes() { + return computed_new_notes_; + } + + const IdList& changes_applied() { return changes_applied_; } + + MOCK_METHOD(void, + OnNoteMetadataFetchedForNavigation, + (const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot), + (override)); + MOCK_METHOD(void, + OnNoteMetadataFetched, + (const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot), + (override)); + MOCK_METHOD(void, + OnNoteModelsFetched, + (const UserNoteService::IdSet& new_notes, + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes, + std::vector<std::unique_ptr<UserNote>> notes), + (override)); + MOCK_METHOD(void, + OnFrameChangesApplied, + (base::UnguessableToken change_id), + (override)); + + void MockOnNoteModelsFetched( + const UserNoteService::IdSet& new_notes, + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes, + std::vector<std::unique_ptr<UserNote>> notes) { + computed_new_notes_ = new_notes; + } + + void MockOnFrameChangesApplied(base::UnguessableToken change_id) { + changes_applied_.emplace_back(change_id); + } + + void CallBaseClassOnNoteMetadataFetchedForNavigation( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot) { + UserNoteService::OnNoteMetadataFetchedForNavigation( + all_frames, std::move(metadata_snapshot)); + } + + void CallBaseClassOnNoteMetadataFetched( + const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot) { + UserNoteService::OnNoteMetadataFetched(all_frames, + std::move(metadata_snapshot)); + } + + void CallBaseClassOnNoteModelsFetched( + const UserNoteService::IdSet& new_notes, + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes, + std::vector<std::unique_ptr<UserNote>> notes) { + UserNoteService::OnNoteModelsFetched(new_notes, std::move(note_changes), + std::move(notes)); + } + + void CallBaseClassOnFrameChangesApplied(base::UnguessableToken change_id) { + UserNoteService::OnFrameChangesApplied(change_id); + } + + private: + UserNoteService::IdSet computed_new_notes_; + IdList changes_applied_; +}; + +class MockUserNoteInstance : public UserNoteInstance { + public: + explicit MockUserNoteInstance(base::SafeRef<UserNote> model_ref, + UserNoteManager* manager) + : UserNoteInstance(model_ref, manager) {} + + void InitializeHighlightInternal() override { + DidFinishAttachment(gfx::Rect()); + } +}; + +class MockFrameUserNoteChanges : public FrameUserNoteChanges { + public: + MockFrameUserNoteChanges(base::SafeRef<UserNoteService> service, + content::RenderFrameHost* rfh, + const ChangeList& notes_added, + const ChangeList& notes_modified, + const ChangeList& notes_removed) + : FrameUserNoteChanges(service, + rfh, + notes_added, + notes_modified, + notes_removed) {} + + private: + std::unique_ptr<UserNoteInstance> MakeNoteInstance( + const UserNote* note_model, + UserNoteManager* manager) const override { + return std::make_unique<MockUserNoteInstance>(note_model->GetSafeRef(), + manager); + } +}; + +class MockUserNotesUI : public UserNotesUI { + public: + MOCK_METHOD(void, Invalidate, (), (override)); + + // The following methods are not used for these tests but they still need to + // be mocked because they are sbstract. + MOCK_METHOD(void, + FocusNote, + (const base::UnguessableToken& guid), + (override)); + MOCK_METHOD(void, + StartNoteCreation, + (UserNoteInstance * instance), + (override)); + MOCK_METHOD(void, Show, (), (override)); +}; + class UserNoteServiceTest : public UserNoteBaseTest { protected: void SetUp() override { UserNoteBaseTest::SetUp(); AddNewNotesToService(2); } + + void CreateService() override { + auto service_delegate = std::make_unique<MockUserNoteServiceDelegate>(); + service_delegate_ = service_delegate.get(); + + auto storage = std::make_unique<MockUserNoteStorage>(); + EXPECT_CALL(*storage, UpdateNote).Times(0); + EXPECT_CALL(*storage, DeleteNote).Times(0); + EXPECT_CALL(*storage, DeleteAllForUrl).Times(0); + EXPECT_CALL(*storage, DeleteAllForOrigin).Times(0); + EXPECT_CALL(*storage, DeleteAllNotes).Times(0); + storage_ = storage.get(); + + note_service_ = std::make_unique<MockUserNoteService>( + std::move(service_delegate), std::move(storage)); + mock_service_ = (MockUserNoteService*)note_service_.get(); + } + + std::vector<content::RenderFrameHost*> GetAllFramesInUse() { + std::vector<content::RenderFrameHost*> frames; + for (const std::unique_ptr<content::WebContents>& wc : web_contents_list_) { + frames.emplace_back(wc->GetPrimaryMainFrame()); + } + return frames; + } + + std::vector<content::WeakDocumentPtr> GetAllFramesInUseAsWeakPtr() { + std::vector<content::WeakDocumentPtr> weak_documents; + for (content::RenderFrameHost* rfh : GetAllFramesInUse()) { + weak_documents.emplace_back(rfh->GetWeakDocumentPtr()); + } + return weak_documents; + } + + raw_ptr<MockUserNoteService> mock_service_; + raw_ptr<MockUserNoteServiceDelegate> service_delegate_; + raw_ptr<MockUserNoteStorage> storage_; }; // Tests that note models are returned correctly by the service. @@ -177,4 +451,779 @@ TEST_F(UserNoteServiceTest, RemovePartialNoteInstance) { EXPECT_FALSE(note_service_->IsNoteInProgress(note_ids_[2])); } +// Tests that the service requests the metadata snapshot for the right URLs when +// it gets notified that notes have changed on disk. +TEST_F(UserNoteServiceTest, OnNotesChanged) { + // Initial setup. + AddNewNotesToService(2); + UserNoteManager* manager1 = ConfigureNewManager(); + UserNoteManager* manager2 = ConfigureNewManager(); + AddNewInstanceToManager(manager1, note_ids_[0]); + AddNewInstanceToManager(manager1, note_ids_[1]); + AddNewInstanceToManager(manager2, note_ids_[2]); + AddNewInstanceToManager(manager2, note_ids_[3]); + service_delegate_->SetFramesForUserNotes(GetAllFramesInUse()); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[1]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[2]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[3]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[1], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[2], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[3], manager2)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes) + .Times(1) + .WillOnce( + Invoke(service_delegate_.get(), + &MockUserNoteServiceDelegate::MockGetAllFramesForUserNotes)); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab).Times(0); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls) + .Times(1) + .WillOnce(Invoke(storage_.get(), + &MockUserNoteStorage::MockGetNoteMetadataForUrls)); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation).Times(0); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(1); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Simulate notes changing on disk. + note_service_->OnNotesChanged(); + + // Mocks ensure callbacks are invoked synchronously, so expectations can be + // immediately verified. + const UserNoteStorage::UrlSet& fetched_urls = + storage_->requested_metadata_urls(); + EXPECT_EQ(fetched_urls.size(), web_contents_list_.size()); + for (size_t i = 0; i < fetched_urls.size(); ++i) { + const auto& url_it = fetched_urls.find( + web_contents_list_[i]->GetPrimaryMainFrame()->GetLastCommittedURL()); + EXPECT_NE(url_it, fetched_urls.end()); + } +} + +// Tests that the service correctly fetches note metadata for the navigated +// frame when a navigation occurs. +TEST_F(UserNoteServiceTest, OnFrameNavigated) { + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(); + AddNewInstanceToManager(manager, note_ids_[0]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)).Times(0); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls) + .Times(1) + .WillOnce(Invoke(storage_.get(), + &MockUserNoteStorage::MockGetNoteMetadataForUrls)); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + std::vector<content::WeakDocumentPtr> all_frames_result; + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation) + .Times(1) + .WillOnce([&](const std::vector<content::WeakDocumentPtr>& all_frames, + UserNoteMetadataSnapshot metadata_snapshot) { + all_frames_result.assign(all_frames.begin(), all_frames.end()); + }); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(0); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Pretend there was a navigation. + content::RenderFrameHost* frame = + web_contents_list_[0]->GetPrimaryMainFrame(); + note_service_->OnFrameNavigated(frame); + + // Mocks ensure callbacks are invoked synchronously, so expectations can be + // immediately verified. + ASSERT_EQ(all_frames_result.size(), 1u); + content::RenderFrameHost* rfh = + all_frames_result[0].AsRenderFrameHostIfValid(); + EXPECT_NE(rfh, nullptr); + EXPECT_EQ(rfh, frame); + + const UserNoteStorage::UrlSet& requested_urls = + storage_->requested_metadata_urls(); + ASSERT_EQ(requested_urls.size(), 1u); + EXPECT_EQ(*(requested_urls.begin()), frame->GetLastCommittedURL()); +} + +// After a navigation to a document that has user notes in the foreground, the +// service should request the notes UI to show itself and fetch the notes +// metadata. +// TODO(crbug.com/1313967): This test will need to be changed when notes UI is +// no longer automatically shown on navigation. +TEST_F(UserNoteServiceTest, OnNoteMetadataFetchedForNavigationSomeNotes) { + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(); + AddNewInstanceToManager(manager, note_ids_[0]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager)); + + // Configure UI mock. + auto mock_ui = std::make_unique<MockUserNotesUI>(); + EXPECT_CALL(*mock_ui, Invalidate).Times(1); + EXPECT_CALL(*mock_ui, FocusNote).Times(0); + EXPECT_CALL(*mock_ui, StartNoteCreation).Times(0); + EXPECT_CALL(*mock_ui, Show).Times(1); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)) + .Times(1) + .WillOnce(Invoke([&mock_ui](const content::RenderFrameHost* frame) { + return mock_ui.get(); + })); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)) + .Times(1) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return true; })); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService:: + CallBaseClassOnNoteMetadataFetchedForNavigation)); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(1); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Create a non-empty metadata snapshot. + UserNoteMetadataSnapshot snapshot; + GURL url = + web_contents_list_[0]->GetPrimaryMainFrame()->GetLastCommittedURL(); + snapshot.AddEntry( + url, note_ids_[0], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + + // Simulate the service receiving the metadata snapshot after a navigation. + note_service_->OnNoteMetadataFetchedForNavigation( + GetAllFramesInUseAsWeakPtr(), std::move(snapshot)); +} + +// After a navigation to a document that has user notes, but isn't in the +// foreground, the service should not request the notes UI to show itself. +// TODO(crbug.com/1313967): This test will need to be changed when notes UI is +// no longer automatically shown on navigation. +TEST_F(UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationSomeNotesBackground) { + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(); + AddNewInstanceToManager(manager, note_ids_[0]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)) + .Times(1) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return false; })); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService:: + CallBaseClassOnNoteMetadataFetchedForNavigation)); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(1); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Create a non-empty metadata snapshot. + UserNoteMetadataSnapshot snapshot; + GURL url = + web_contents_list_[0]->GetPrimaryMainFrame()->GetLastCommittedURL(); + snapshot.AddEntry( + url, note_ids_[0], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + + // Simulate the service receiving the metadata snapshot after a navigation. + note_service_->OnNoteMetadataFetchedForNavigation( + GetAllFramesInUseAsWeakPtr(), std::move(snapshot)); +} + +// After a navigation to a document that doesn't have user notes but is in the +// active tab, the service should not request the notes UI to show itself, but +// should Invalidate the notes displayed in the UI. +// TODO(crbug.com/1313967): This test will need to be changed when notes UI is +// no longer automatically shown on navigation. +TEST_F(UserNoteServiceTest, OnNoteMetadataFetchedForNavigationNoNotes) { + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(); + AddNewInstanceToManager(manager, note_ids_[0]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager)); + + // Configure UI mock. + auto mock_ui = std::make_unique<MockUserNotesUI>(); + EXPECT_CALL(*mock_ui, Invalidate).Times(1); + EXPECT_CALL(*mock_ui, FocusNote).Times(0); + EXPECT_CALL(*mock_ui, StartNoteCreation).Times(0); + EXPECT_CALL(*mock_ui, Show).Times(0); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)) + .Times(1) + .WillOnce(Invoke([&mock_ui](const content::RenderFrameHost* frame) { + return mock_ui.get(); + })); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)) + .Times(1) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return true; })); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService:: + CallBaseClassOnNoteMetadataFetchedForNavigation)); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(0); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Create a non-empty metadata snapshot. + UserNoteMetadataSnapshot snapshot; + GURL url = + web_contents_list_[0]->GetPrimaryMainFrame()->GetLastCommittedURL(); + snapshot.AddEntry( + url, note_ids_[0], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + + // Simulate the service receiving the empty metadata snapshot after a + // navigation. + note_service_->OnNoteMetadataFetchedForNavigation( + GetAllFramesInUseAsWeakPtr(), UserNoteMetadataSnapshot()); +} + +// After a navigation to a document that doesn't have user notes, but isn't in +// the foreground, the service should not request the notes UI to show itself +// nor to Invalidate the notes. +// TODO(crbug.com/1313967): This test will need to be changed when notes UI is +// no longer automatically shown on navigation. +TEST_F(UserNoteServiceTest, + OnNoteMetadataFetchedForNavigationNoNotesBackground) { + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(); + AddNewInstanceToManager(manager, note_ids_[0]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)) + .Times(1) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return false; })); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService:: + CallBaseClassOnNoteMetadataFetchedForNavigation)); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(0); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Create a non-empty metadata snapshot. + UserNoteMetadataSnapshot snapshot; + GURL url = + web_contents_list_[0]->GetPrimaryMainFrame()->GetLastCommittedURL(); + snapshot.AddEntry( + url, note_ids_[0], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + + // Simulate the service receiving the empty metadata snapshot after a + // navigation. + note_service_->OnNoteMetadataFetchedForNavigation( + GetAllFramesInUseAsWeakPtr(), UserNoteMetadataSnapshot()); +} + +// Tests that the service requests the right models from the storage after +// receiving the metadata snapshot. +TEST_F(UserNoteServiceTest, OnNoteMetadataFetched) { + // Initial setup. + AddNewNotesToService(2); + AddPartialNotesToService(1); + UserNoteManager* manager1 = ConfigureNewManager(); + UserNoteManager* manager2 = ConfigureNewManager(); + AddNewInstanceToManager(manager1, note_ids_[0]); + AddNewInstanceToManager(manager1, note_ids_[1]); + AddNewInstanceToManager(manager2, note_ids_[2]); + AddNewInstanceToManager(manager2, note_ids_[3]); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[1]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[2]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[3]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[1], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[2], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[3], manager2)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab).Times(0); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById) + .Times(1) + .WillOnce(Invoke(storage_.get(), &MockUserNoteStorage::MockGetNotesById)); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation).Times(0); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched) + .Times(1) + .WillOnce( + Invoke(mock_service_.get(), + &MockUserNoteService::CallBaseClassOnNoteMetadataFetched)); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService::MockOnNoteModelsFetched)); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied).Times(0); + + // Create a fake metadata snapshot with some updated and new notes, including + // one new note that wasn't in the creation map so simulate receiving it from + // Sync. + note_ids_.emplace_back(base::UnguessableToken::Create()); + UserNoteMetadataSnapshot snapshot; + GURL url1 = + web_contents_list_[0]->GetPrimaryMainFrame()->GetLastCommittedURL(); + GURL url2 = + web_contents_list_[1]->GetPrimaryMainFrame()->GetLastCommittedURL(); + snapshot.AddEntry( + url1, note_ids_[0], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + snapshot.AddEntry( + url1, note_ids_[4], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + snapshot.AddEntry( + url2, note_ids_[2], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + snapshot.AddEntry( + url2, note_ids_[5], + std::make_unique<UserNoteMetadata>(base::Time::Now(), base::Time::Now(), + /*min_note_version=*/1)); + + // Simulate the storage returning the metadata snapshot to the service + // callback. + note_service_->OnNoteMetadataFetched(GetAllFramesInUseAsWeakPtr(), + std::move(snapshot)); + + // Mocks ensure callbacks are invoked synchronously, so expectations can be + // immediately verified. + const UserNoteStorage::IdSet& fetched_ids = storage_->requested_model_ids(); + EXPECT_EQ(fetched_ids.size(), 4u); + EXPECT_NE(std::find(fetched_ids.begin(), fetched_ids.end(), note_ids_[0]), + fetched_ids.end()); + EXPECT_NE(std::find(fetched_ids.begin(), fetched_ids.end(), note_ids_[2]), + fetched_ids.end()); + EXPECT_NE(std::find(fetched_ids.begin(), fetched_ids.end(), note_ids_[4]), + fetched_ids.end()); + EXPECT_NE(std::find(fetched_ids.begin(), fetched_ids.end(), note_ids_[5]), + fetched_ids.end()); + + const UserNoteService::IdSet& computed_new_notes = + mock_service_->computed_new_notes(); + EXPECT_EQ(computed_new_notes.size(), 2u); + EXPECT_NE(computed_new_notes.find(note_ids_[4]), computed_new_notes.end()); + EXPECT_NE(computed_new_notes.find(note_ids_[5]), computed_new_notes.end()); +} + +// Tests that the service correctly updates the models in the model map and +// applies the necessary note changes. +TEST_F(UserNoteServiceTest, OnNoteModelsFetched) { + // Initial setup. + AddNewNotesToService(2); + AddPartialNotesToService(1); + UserNoteManager* manager1 = ConfigureNewManager(); + UserNoteManager* manager2 = ConfigureNewManager(); + AddNewInstanceToManager(manager1, note_ids_[0]); + AddNewInstanceToManager(manager1, note_ids_[1]); + AddNewInstanceToManager(manager2, note_ids_[2]); + AddNewInstanceToManager(manager2, note_ids_[3]); + + // For note 1 to be correctly deleted later in the test, its target URL must + // be changed to match the parent frame. + content::RenderFrameHost* frame1 = + web_contents_list_[0]->GetPrimaryMainFrame(); + const auto& note_1_entry_it = note_service_->model_map_.find(note_ids_[1]); + note_1_entry_it->second.model->Update(std::make_unique<UserNote>( + note_ids_[1], GetTestUserNoteMetadata(), GetTestUserNoteBody(), + GetTestUserNotePageTarget(frame1->GetLastCommittedURL().spec()))); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[1]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[2]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[3]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[1], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[2], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[3], manager2)); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame).Times(0); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab).Times(0); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation).Times(0); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(0); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched) + .Times(1) + .WillOnce(Invoke(mock_service_.get(), + &MockUserNoteService::CallBaseClassOnNoteModelsFetched)); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied) + .Times(2) + .WillRepeatedly(Invoke(mock_service_.get(), + &MockUserNoteService::MockOnFrameChangesApplied)); + + // Prepare the fake input for passing to the method under test. 2 notes are + // simulated as having been updated, 2 notes as created, and one as having + // been removed. One of the created notes is not present in the creation map + // to simulate receiving it from Sync. + note_ids_.emplace_back(base::UnguessableToken::Create()); + UserNoteService::IdSet new_notes; + new_notes.emplace(note_ids_[4]); + new_notes.emplace(note_ids_[5]); + + content::RenderFrameHost* frame2 = + web_contents_list_[1]->GetPrimaryMainFrame(); + auto change1 = std::make_unique<MockFrameUserNoteChanges>( + note_service_->GetSafeRef(), frame1, /*added=*/IdList{note_ids_[4]}, + /*modified=*/IdList{note_ids_[0]}, /*removed=*/IdList{note_ids_[1]}); + auto change2 = std::make_unique<MockFrameUserNoteChanges>( + note_service_->GetSafeRef(), frame2, /*added=*/IdList{note_ids_[5]}, + /*modified=*/IdList{note_ids_[2]}, /*removed=*/IdList{}); + base::UnguessableToken change1_id = change1->id(); + base::UnguessableToken change2_id = change2->id(); + std::vector<std::unique_ptr<FrameUserNoteChanges>> note_changes; + note_changes.emplace_back(std::move(change1)); + note_changes.emplace_back(std::move(change2)); + + const std::u16string kText0 = u"updated note 0"; + const std::u16string kText2 = u"updated note 2"; + const std::u16string kText4 = u"new note 4"; + const std::u16string kText5 = u"new note 5"; + const std::string url1 = frame1->GetLastCommittedURL().spec(); + const std::string url2 = frame2->GetLastCommittedURL().spec(); + auto note0 = std::make_unique<UserNote>( + note_ids_[0], GetTestUserNoteMetadata(), + std::make_unique<UserNoteBody>(kText0), GetTestUserNotePageTarget(url1)); + auto note2 = std::make_unique<UserNote>( + note_ids_[2], GetTestUserNoteMetadata(), + std::make_unique<UserNoteBody>(kText2), GetTestUserNotePageTarget(url2)); + auto note4 = std::make_unique<UserNote>( + note_ids_[4], GetTestUserNoteMetadata(), + std::make_unique<UserNoteBody>(kText4), GetTestUserNotePageTarget(url1)); + auto note5 = std::make_unique<UserNote>( + note_ids_[5], GetTestUserNoteMetadata(), + std::make_unique<UserNoteBody>(kText5), GetTestUserNotePageTarget(url2)); + std::vector<std::unique_ptr<UserNote>> note_models; + note_models.emplace_back(std::move(note0)); + note_models.emplace_back(std::move(note4)); + note_models.emplace_back(std::move(note2)); + note_models.emplace_back(std::move(note5)); + + // Simulate the storage returning the updated note models to the service + // callback. + note_service_->OnNoteModelsFetched(new_notes, std::move(note_changes), + std::move(note_models)); + + // Mocks ensure callbacks are invoked synchronously, so expectations can be + // immediately verified. + const IdList& changes_applied = mock_service_->changes_applied(); + EXPECT_EQ(changes_applied.size(), 2u); + EXPECT_NE( + std::find(changes_applied.begin(), changes_applied.end(), change1_id), + changes_applied.end()); + EXPECT_NE( + std::find(changes_applied.begin(), changes_applied.end(), change2_id), + changes_applied.end()); + + EXPECT_EQ(ModelMapSize(), 5u); + EXPECT_EQ(CreationMapSize(), 0u); + EXPECT_FALSE(DoesModelExist(note_ids_[1])); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[2]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[3]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[4]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[5]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[2], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[3], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[4], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[5], manager2)); + + std::u16string actual_text_0 = + note_service_->GetNoteModel(note_ids_[0])->body().plain_text_value(); + std::u16string actual_text_2 = + note_service_->GetNoteModel(note_ids_[2])->body().plain_text_value(); + std::u16string actual_text_4 = + note_service_->GetNoteModel(note_ids_[4])->body().plain_text_value(); + std::u16string actual_text_5 = + note_service_->GetNoteModel(note_ids_[5])->body().plain_text_value(); + EXPECT_EQ(actual_text_0, kText0); + EXPECT_EQ(actual_text_2, kText2); + EXPECT_EQ(actual_text_4, kText4); + EXPECT_EQ(actual_text_5, kText5); + + EXPECT_EQ(note_service_->note_changes_in_progress_.size(), 2u); + EXPECT_NE(note_service_->note_changes_in_progress_.find(change1_id), + note_service_->note_changes_in_progress_.end()); + EXPECT_NE(note_service_->note_changes_in_progress_.find(change2_id), + note_service_->note_changes_in_progress_.end()); +} + +// Tests that the service correctly finalizes frame changes that have been +// applied and notifies the UI to update itself when needed. +TEST_F(UserNoteServiceTest, OnFrameChangesApplied) { + // Initial setup. + AddNewNotesToService(2); + AddPartialNotesToService(1); + UserNoteManager* manager1 = ConfigureNewManager(); + UserNoteManager* manager2 = ConfigureNewManager(); + AddNewInstanceToManager(manager1, note_ids_[0]); + AddNewInstanceToManager(manager1, note_ids_[1]); + AddNewInstanceToManager(manager2, note_ids_[2]); + AddNewInstanceToManager(manager2, note_ids_[3]); + + content::RenderFrameHost* frame1 = + web_contents_list_[0]->GetPrimaryMainFrame(); + content::RenderFrameHost* frame2 = + web_contents_list_[1]->GetPrimaryMainFrame(); + auto change1 = std::make_unique<FrameUserNoteChanges>( + note_service_->GetSafeRef(), frame1, /*added=*/IdList{}, + /*modified=*/IdList{note_ids_[0]}, /*removed=*/IdList{}); + auto change2 = std::make_unique<FrameUserNoteChanges>( + note_service_->GetSafeRef(), frame2, /*added=*/IdList{}, + /*modified=*/IdList{note_ids_[2]}, /*removed=*/IdList{}); + base::UnguessableToken change1_id = change1->id(); + base::UnguessableToken change2_id = change2->id(); + note_service_->note_changes_in_progress_.emplace(change1_id, + std::move(change1)); + note_service_->note_changes_in_progress_.emplace(change2_id, + std::move(change2)); + + // Verify initial setup. + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(note_service_->note_changes_in_progress_.size(), 2u); + EXPECT_EQ(ManagerCountForId(note_ids_[0]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[1]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[2]), 1u); + EXPECT_EQ(ManagerCountForId(note_ids_[3]), 1u); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[0], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[1], manager1)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[2], manager2)); + EXPECT_TRUE(DoesManagerExistForId(note_ids_[3], manager2)); + + // Configure UI mock. + auto mock_ui = std::make_unique<MockUserNotesUI>(); + EXPECT_CALL(*mock_ui, Invalidate).Times(1); + EXPECT_CALL(*mock_ui, FocusNote).Times(0); + EXPECT_CALL(*mock_ui, StartNoteCreation).Times(0); + EXPECT_CALL(*mock_ui, Show).Times(0); + + // Configure service delegate mock. + EXPECT_CALL(*service_delegate_, GetAllFramesForUserNotes).Times(0); + EXPECT_CALL(*service_delegate_, GetUICoordinatorForFrame(_)) + .Times(1) + .WillOnce(Invoke([&mock_ui](const content::RenderFrameHost* frame) { + return mock_ui.get(); + })); + EXPECT_CALL(*service_delegate_, IsFrameInActiveTab(_)) + .Times(2) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return true; })) + .WillOnce( + Invoke([](const content::RenderFrameHost* frame) { return false; })); + + // Configure storage mock. + EXPECT_CALL(*storage_, GetNoteMetadataForUrls).Times(0); + EXPECT_CALL(*storage_, GetNotesById).Times(0); + + // Configure service mock. + EXPECT_CALL(*mock_service_, OnNoteMetadataFetchedForNavigation).Times(0); + EXPECT_CALL(*mock_service_, OnNoteMetadataFetched).Times(0); + EXPECT_CALL(*mock_service_, OnNoteModelsFetched).Times(0); + EXPECT_CALL(*mock_service_, OnFrameChangesApplied) + .Times(2) + .WillRepeatedly( + Invoke(mock_service_.get(), + &MockUserNoteService::CallBaseClassOnFrameChangesApplied)); + + // Simulate the first change being applied. It should invalidate the UI since + // it is simulated as being in an active tab. + note_service_->OnFrameChangesApplied(change1_id); + + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(note_service_->note_changes_in_progress_.size(), 1u); + EXPECT_EQ(note_service_->note_changes_in_progress_.find(change1_id), + note_service_->note_changes_in_progress_.end()); + EXPECT_NE(note_service_->note_changes_in_progress_.find(change2_id), + note_service_->note_changes_in_progress_.end()); + + Mock::VerifyAndClearExpectations(mock_ui.get()); + + // Simulate the second change being applied. + EXPECT_CALL(*mock_ui, Invalidate).Times(0); + EXPECT_CALL(*mock_ui, FocusNote).Times(0); + EXPECT_CALL(*mock_ui, StartNoteCreation).Times(0); + EXPECT_CALL(*mock_ui, Show).Times(0); + + note_service_->OnFrameChangesApplied(change2_id); + + EXPECT_EQ(ModelMapSize(), 4u); + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(note_service_->note_changes_in_progress_.size(), 0u); +} + +// Verify the creation flow is started and a partial note inserted into the +// creation map when "Add Note" is requested (as it would be from the context +// menu). The creation should begin synchronously when there's no selection +// since the renderer doesn't need to create a highlight and selector. +TEST_F(UserNoteServiceTest, OnAddNoteRequestedWithoutSelection) { + // Initial setup. + ConfigureNewManager(); + + // Verify initial setup. + ASSERT_EQ(web_contents_list_.size(), 1ul); + ASSERT_EQ(ModelMapSize(), 2u); + ASSERT_EQ(CreationMapSize(), 0u); + + content::RenderFrameHost* rfh = web_contents_list_[0]->GetPrimaryMainFrame(); + + // Simulate the "Add Note" context menu item being invoked while there's no + // selection in the renderer. + note_service_->OnAddNoteRequested(rfh, /*has_selected_text=*/false); + + // Since there's no selection, a new partial note should be added to the + // creation map synchronously. The model map should be unchanged. + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(ModelMapSize(), 2u); +} + +// Verify the creation flow is started when "Add Note" is requested with a +// selection in the renderer. +TEST_F(UserNoteServiceTest, OnAddNoteRequestedWithSelection) { + MockAnnotationAgentContainer container; + + // Initial setup. + UserNoteManager* manager = ConfigureNewManager(&container); + + // Verify initial setup. + ASSERT_TRUE(container.is_bound()); + ASSERT_EQ(web_contents_list_.size(), 1ul); + ASSERT_EQ(ModelMapSize(), 2u); + ASSERT_EQ(CreationMapSize(), 0u); + + content::RenderFrameHost* rfh = web_contents_list_[0]->GetPrimaryMainFrame(); + + // Simulate the "Add Note" context menu item being invoked while there's no + // selection in the renderer. + note_service_->OnAddNoteRequested(rfh, /*has_selected_text=*/true); + + // Since there's a selection, a partial note won't be created until the + // renderer replies with a selector. + EXPECT_EQ(CreationMapSize(), 0u); + EXPECT_EQ(ModelMapSize(), 2u); + + mojo::Remote<blink::mojom::AnnotationAgentHost> host; + MockAnnotationAgent agent; + EXPECT_CALL(container, + CreateAgentFromSelection(blink::mojom::AnnotationType::kUserNote, + testing::_)) + .WillOnce( + [&](blink::mojom::AnnotationType type, + MockAnnotationAgentContainer::CreateAgentFromSelectionCallback + cb) { + std::move(cb).Run(host.BindNewPipeAndPassReceiver(), + agent.BindNewPipeAndPassRemote(), + /*serialized_selector=*/"FOO", + /*selected_text=*/std::u16string(u"FOO")); + }); + manager->note_agent_container().FlushForTesting(); + testing::Mock::VerifyAndClearExpectations(&container); + + // Now that the renderer replied, a new partial note should be added to the + // creation map. + EXPECT_EQ(CreationMapSize(), 1u); + EXPECT_EQ(ModelMapSize(), 2u); +} + } // namespace user_notes diff --git a/chromium/components/user_notes/browser/user_note_utils.cc b/chromium/components/user_notes/browser/user_note_utils.cc index abef4b06238..f60da07e1c7 100644 --- a/chromium/components/user_notes/browser/user_note_utils.cc +++ b/chromium/components/user_notes/browser/user_note_utils.cc @@ -11,21 +11,29 @@ #include "components/user_notes/interfaces/user_note_metadata_snapshot.h" #include "components/user_notes/model/user_note_metadata.h" #include "content/public/browser/render_frame_host.h" +#include "content/public/browser/weak_document_ptr.h" #include "url/gurl.h" namespace user_notes { -std::vector<FrameUserNoteChanges> CalculateNoteChanges( +std::vector<std::unique_ptr<FrameUserNoteChanges>> CalculateNoteChanges( const UserNoteService& note_service, - const std::vector<content::RenderFrameHost*>& rfhs, + const std::vector<content::WeakDocumentPtr>& documents, const UserNoteMetadataSnapshot& metadata_snapshot) { - std::vector<FrameUserNoteChanges> result; + std::vector<std::unique_ptr<FrameUserNoteChanges>> result; // Create an empty metadata map to simplify the algorithm below for cases // where there's no entry in the metadata snapshot for a frame's URL. UserNoteMetadataSnapshot::IdToMetadataMap empty_map; - for (content::RenderFrameHost* rfh : rfhs) { + for (const content::WeakDocumentPtr& document : documents) { + content::RenderFrameHost* rfh = document.AsRenderFrameHostIfValid(); + + if (!rfh) { + // The frame is no longer valid. + continue; + } + // Notes should only be processed for the primary page. DCHECK(rfh->GetMainFrame()->IsInPrimaryMainFrame()); @@ -66,12 +74,14 @@ std::vector<FrameUserNoteChanges> CalculateNoteChanges( if (url == model.target().target_page()) { // Note has been removed. - removed.emplace_back(base::UnguessableToken(model.id())); + removed.emplace_back(model.id()); } } else if (metadata_it->second->modification_date() > - model.metadata().modification_date()) { - // Note has been modified. - modified.emplace_back(base::UnguessableToken(model.id())); + model.metadata().modification_date() || + note_service.IsNoteInProgress(model.id())) { + // This note has been modified or created locally (which is treated as a + // modification since both the instance and the partial model exist). + modified.emplace_back(model.id()); } } @@ -81,14 +91,14 @@ std::vector<FrameUserNoteChanges> CalculateNoteChanges( const base::UnguessableToken& id = metadata_it.first; if (!notes_manager->GetNoteInstance(id)) { // This is a new note. - added.emplace_back(base::UnguessableToken(id)); + added.emplace_back(id); } } if (!added.empty() || !removed.empty() || !modified.empty()) { - result.emplace_back( - FrameUserNoteChanges(note_service.GetSafeRef(), rfh, std::move(added), - std::move(modified), std::move(removed))); + result.emplace_back(std::make_unique<FrameUserNoteChanges>( + note_service.GetSafeRef(), rfh, std::move(added), std::move(modified), + std::move(removed))); } } diff --git a/chromium/components/user_notes/browser/user_note_utils.h b/chromium/components/user_notes/browser/user_note_utils.h index a13806f4d80..ecc95c831ae 100644 --- a/chromium/components/user_notes/browser/user_note_utils.h +++ b/chromium/components/user_notes/browser/user_note_utils.h @@ -10,7 +10,7 @@ #include <vector> namespace content { -class RenderFrameHost; +class WeakDocumentPtr; } // namespace content namespace user_notes { @@ -23,9 +23,9 @@ class UserNoteService; // actually contain based on the provided metadata snapshot. A // `FrameUserNoteChanges` object is generated for each frame where notes // don't match the metadata. -std::vector<FrameUserNoteChanges> CalculateNoteChanges( +std::vector<std::unique_ptr<FrameUserNoteChanges>> CalculateNoteChanges( const UserNoteService& note_service, - const std::vector<content::RenderFrameHost*>& rfhs, + const std::vector<content::WeakDocumentPtr>& documents, const UserNoteMetadataSnapshot& metadata_snapshot); } // namespace user_notes diff --git a/chromium/components/user_notes/browser/user_note_utils_unittest.cc b/chromium/components/user_notes/browser/user_note_utils_unittest.cc index fcaf1800ed9..7600daa5b8e 100644 --- a/chromium/components/user_notes/browser/user_note_utils_unittest.cc +++ b/chromium/components/user_notes/browser/user_note_utils_unittest.cc @@ -227,8 +227,9 @@ class UserNoteUtilsTest content::RenderViewHostTestHarness::SetUp(); // Create the note service and the note models that will be used in this - // test case. A service delegate isn't needed for these tests. - note_service_ = std::make_unique<UserNoteService>(/*delegate=*/nullptr); + // test case. A service delegate and storage aren't needed for these tests. + note_service_ = std::make_unique<UserNoteService>(/*delegate=*/nullptr, + /*storage=*/nullptr); for (const NoteConfig& note : GetParam().notes) { CreateNewNoteAndAddToService(note); } @@ -308,8 +309,8 @@ class UserNoteUtilsTest note_service_->model_map_.find(token_it->second); UserNote* model = note_entry_it->second.model.get(); note_manager->instance_map_.emplace( - model->id(), std::make_unique<UserNoteInstance>(model->GetSafeRef(), - note_manager)); + model->id(), + UserNoteInstance::Create(model->GetSafeRef(), note_manager)); } } @@ -692,28 +693,28 @@ TEST_P(UserNoteUtilsTest, CalculateNoteChanges) { } // Round up the test frames as if they were the user's open tabs. - std::vector<content::RenderFrameHost*> frame_hosts; - frame_hosts.reserve(frame_to_config_.size()); + std::vector<content::WeakDocumentPtr> weak_documents; + weak_documents.reserve(frame_to_config_.size()); for (const auto& config_it : frame_to_config_) { - frame_hosts.push_back(config_it.first); + weak_documents.emplace_back(config_it.first->GetWeakDocumentPtr()); } // Calculate the diff between the notes in the frames and the notes in the // metadata. - const std::vector<FrameUserNoteChanges>& actual_diffs = - CalculateNoteChanges(*note_service_, frame_hosts, metadata_snapshot); + const std::vector<std::unique_ptr<FrameUserNoteChanges>>& actual_diffs = + CalculateNoteChanges(*note_service_, weak_documents, metadata_snapshot); std::unordered_set<content::RenderFrameHost*> frames_with_diff; - for (const FrameUserNoteChanges& diff : actual_diffs) { + for (const std::unique_ptr<FrameUserNoteChanges>& diff : actual_diffs) { // Find the frame config for this diff's frame. - const auto config_it = frame_to_config_.find(diff.rfh_); + const auto config_it = frame_to_config_.find(diff->rfh_); DCHECK(config_it != frame_to_config_.end()); FrameConfig frame_config = config_it->second; // Make sure there is at most one diff per frame. - EXPECT_TRUE(frames_with_diff.find(diff.rfh_) == frames_with_diff.end()) + EXPECT_TRUE(frames_with_diff.find(diff->rfh_) == frames_with_diff.end()) << "More than one diff generated for frame " << frame_config.test_id; - frames_with_diff.emplace(diff.rfh_); + frames_with_diff.emplace(diff->rfh_); // Verify that a diff was expected for this frame. EXPECT_TRUE(frame_config.expect_diff) @@ -723,19 +724,19 @@ TEST_P(UserNoteUtilsTest, CalculateNoteChanges) { // Verify added, modified and removed notes are as expected. Use copies to // prevent any side effect of sorting in place. NoteIdList actual_added = - ConvertToSortedTestIds(diff.notes_added_, token_to_test_id_); + ConvertToSortedTestIds(diff->notes_added_, token_to_test_id_); NoteIdList expected_added = CopyAndSort(frame_config.added); EXPECT_EQ(actual_added, expected_added) << "Unexpected ADDED results for frame " << frame_config.test_id; NoteIdList actual_modified = - ConvertToSortedTestIds(diff.notes_modified_, token_to_test_id_); + ConvertToSortedTestIds(diff->notes_modified_, token_to_test_id_); NoteIdList expected_modified = CopyAndSort(frame_config.modified); EXPECT_EQ(actual_modified, expected_modified) << "Unexpected MODIFIED results for frame " << frame_config.test_id; NoteIdList actual_removed = - ConvertToSortedTestIds(diff.notes_removed_, token_to_test_id_); + ConvertToSortedTestIds(diff->notes_removed_, token_to_test_id_); NoteIdList expected_removed = CopyAndSort(frame_config.removed); EXPECT_EQ(actual_removed, expected_removed) << "Unexpected REMOVED results for frame " << frame_config.test_id; diff --git a/chromium/components/user_notes/interfaces/BUILD.gn b/chromium/components/user_notes/interfaces/BUILD.gn index 9dcfa68a9e3..d9a0d69c96d 100644 --- a/chromium/components/user_notes/interfaces/BUILD.gn +++ b/chromium/components/user_notes/interfaces/BUILD.gn @@ -8,6 +8,7 @@ source_set("interfaces") { "user_note_metadata_snapshot.h", "user_note_service_delegate.h", "user_note_storage.h", + "user_notes_ui.cc", "user_notes_ui.h", "user_notes_ui_delegate.h", ] diff --git a/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.cc b/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.cc index 23d34d70402..7b055d7e726 100644 --- a/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.cc +++ b/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.cc @@ -15,6 +15,10 @@ UserNoteMetadataSnapshot::UserNoteMetadataSnapshot( UserNoteMetadataSnapshot::~UserNoteMetadataSnapshot() = default; +bool UserNoteMetadataSnapshot::IsEmpty() { + return url_map_.size() == 0; +} + void UserNoteMetadataSnapshot::AddEntry( const GURL& url, const base::UnguessableToken& id, diff --git a/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.h b/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.h index c71a4568c15..4433ff4cdbd 100644 --- a/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.h +++ b/chromium/components/user_notes/interfaces/user_note_metadata_snapshot.h @@ -16,8 +16,6 @@ namespace user_notes { class UserNoteMetadata; -namespace { - // In order to have GURL as a key in a hashmap, GURL hashing mechanism is // needed. struct GURLHash { @@ -26,8 +24,6 @@ struct GURLHash { } }; -} // namespace - // A class that encapsulates an // `unordered_map<GURL, unordered_map<ID, UserNoteMetadata>>`. This represents // a snapshot of the note metadata contained in the database for a set of URLs. @@ -49,6 +45,10 @@ class UserNoteMetadataSnapshot { UserNoteMetadataSnapshot& operator=(const UserNoteMetadataSnapshot&) = delete; ~UserNoteMetadataSnapshot(); + // Returns false if there's at least one entry in the snapshot, true + // otherwise. + bool IsEmpty(); + // Adds a metadata entry to this class, based on the URL the note is attached // to and its ID. void AddEntry(const GURL& url, diff --git a/chromium/components/user_notes/interfaces/user_note_service_delegate.h b/chromium/components/user_notes/interfaces/user_note_service_delegate.h index 75d3b92abf0..d7e215c1cfc 100644 --- a/chromium/components/user_notes/interfaces/user_note_service_delegate.h +++ b/chromium/components/user_notes/interfaces/user_note_service_delegate.h @@ -33,6 +33,10 @@ class UserNoteServiceDelegate { // example to bring a note into focus or start the note creation flow. virtual UserNotesUI* GetUICoordinatorForFrame( const content::RenderFrameHost* rfh) = 0; + + // Called by the `UserNoteService` to determine whether the given frame is + // part of the active tab of its owning browser window. + virtual bool IsFrameInActiveTab(const content::RenderFrameHost* rfh) = 0; }; } // namespace user_notes diff --git a/chromium/components/user_notes/interfaces/user_note_storage.h b/chromium/components/user_notes/interfaces/user_note_storage.h index e612a729b76..baf3513a95b 100644 --- a/chromium/components/user_notes/interfaces/user_note_storage.h +++ b/chromium/components/user_notes/interfaces/user_note_storage.h @@ -7,7 +7,8 @@ #include <memory> #include <string> -#include <unordered_map> +#include <unordered_set> +#include <vector> #include "base/callback.h" #include "base/unguessable_token.h" @@ -22,9 +23,14 @@ namespace user_notes { // Interface that callers can use to interact with the UserNotes in storage. class UserNoteStorage { public: + using UrlSet = std::unordered_set<GURL, GURLHash>; + using IdSet = + std::unordered_set<base::UnguessableToken, base::UnguessableTokenHash>; + // Observer class for the notes storage. Notifies implementers when the notes // have changed on disk so they can update their model. class Observer { + public: // Called when notes have changed on disk. virtual void OnNotesChanged() = 0; }; @@ -34,23 +40,28 @@ class UserNoteStorage { UserNoteStorage& operator=(const UserNoteStorage&) = delete; virtual ~UserNoteStorage() = default; + // Adds an observer. + virtual void AddObserver(Observer* observer) = 0; + // Removes an observer. + virtual void RemoveObserver(Observer* observer) = 0; + // Fetches all `UserNoteMetadata` entries for the given URLs from disk. The // results are returned via `callback`, mapped by URL and by note // ID. virtual void GetNoteMetadataForUrls( - std::vector<GURL> urls, + const UrlSet& urls, base::OnceCallback<void(UserNoteMetadataSnapshot)> callback) = 0; // Fetches all `UserNotes` corresponding to the given IDs from disk. The // results are returned via `callback`. virtual void GetNotesById( - std::vector<base::UnguessableToken> ids, + const IdSet& ids, base::OnceCallback<void(std::vector<std::unique_ptr<UserNote>>)> callback) = 0; // Saves a brand-new note or a modified note to disk. virtual void UpdateNote(const UserNote* model, - std::string note_body_text, + std::u16string note_body_text, bool is_creation = false) = 0; // Deletes a note from disk. diff --git a/chromium/components/user_notes/interfaces/user_notes_ui.cc b/chromium/components/user_notes/interfaces/user_notes_ui.cc new file mode 100644 index 00000000000..6bcc29d79ed --- /dev/null +++ b/chromium/components/user_notes/interfaces/user_notes_ui.cc @@ -0,0 +1,13 @@ +// Copyright 2022 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/user_notes/interfaces/user_notes_ui.h" + +namespace user_notes { + +// UserNotesUI must implement the user data key for its subclasses, to ensure +// their instances can be retrieved in an implementation-agnostic way. +const int UserNotesUI::kUserDataKey; + +} // namespace user_notes diff --git a/chromium/components/user_notes/interfaces/user_notes_ui.h b/chromium/components/user_notes/interfaces/user_notes_ui.h index 4d4d7fdca7a..1141dd53308 100644 --- a/chromium/components/user_notes/interfaces/user_notes_ui.h +++ b/chromium/components/user_notes/interfaces/user_notes_ui.h @@ -5,8 +5,8 @@ #ifndef COMPONENTS_USER_NOTES_INTERFACES_USER_NOTES_UI_H_ #define COMPONENTS_USER_NOTES_INTERFACES_USER_NOTES_UI_H_ -#include <string> - +#include "base/supports_user_data.h" +#include "base/unguessable_token.h" #include "ui/gfx/geometry/rect.h" namespace user_notes { @@ -15,16 +15,18 @@ class UserNoteInstance; // Interface that the UI layer of User Notes must implement. Used by the // business logic in the service to send commands to the UI. -class UserNotesUI { +class UserNotesUI : public base::SupportsUserData::Data { public: + static const void* UserDataKey() { return &kUserDataKey; } + UserNotesUI() = default; UserNotesUI(const UserNotesUI&) = delete; UserNotesUI& operator=(const UserNotesUI&) = delete; - virtual ~UserNotesUI() = default; + ~UserNotesUI() override = default; // Called when a note in the UI should be scrolled to / brought to the // foreground, and focused. - virtual void FocusNote(const std::string& guid) = 0; + virtual void FocusNote(const base::UnguessableToken& guid) = 0; // Called when the note creation UX should be shown in the UI layer. |bounds| // corresponds to the location in the webpage where the associated highlight @@ -40,6 +42,9 @@ class UserNotesUI { // Called by the UserNoteService when the user triggers one of the feature's // entry points, indicating the Notes UI should show itself. virtual void Show() = 0; + + private: + static const int kUserDataKey = 0; }; } // namespace user_notes diff --git a/chromium/components/user_notes/interfaces/user_notes_ui_delegate.h b/chromium/components/user_notes/interfaces/user_notes_ui_delegate.h index 6ce5d5a8828..bdc8f05f013 100644 --- a/chromium/components/user_notes/interfaces/user_notes_ui_delegate.h +++ b/chromium/components/user_notes/interfaces/user_notes_ui_delegate.h @@ -9,6 +9,10 @@ #include "base/unguessable_token.h" +namespace content { +class RenderFrameHost; +} // namespace content + namespace user_notes { // Interface used by the UI layer (e.g. Side Panel on desktop) to delegate @@ -20,22 +24,23 @@ class UserNotesUIDelegate { UserNotesUIDelegate& operator=(const UserNotesUIDelegate&) = delete; virtual ~UserNotesUIDelegate() = default; - // Called when a note in the UI is focused. - virtual void OnNoteFocused(const base::UnguessableToken& id) = 0; + // Called when a note in the UI is selected (i.e. via mouse press). + virtual void OnNoteSelected(const base::UnguessableToken& id, + content::RenderFrameHost* rfh) = 0; // Called when the user deletes a note in the UI. virtual void OnNoteDeleted(const base::UnguessableToken& id) = 0; // Called when the user successfully creates a new note in the UI. virtual void OnNoteCreationDone(const base::UnguessableToken& id, - const std::string& note_content) = 0; + const std::u16string& note_content) = 0; // Called when the user aborts the note creation process in the UI. virtual void OnNoteCreationCancelled(const base::UnguessableToken& id) = 0; - // Called when the user updates an existing note in the UI. - virtual void OnNoteUpdated(const base::UnguessableToken& id, - const std::string& note_content) = 0; + // Called when the user edits an existing note in the UI. + virtual void OnNoteEdited(const base::UnguessableToken& id, + const std::u16string& note_content) = 0; }; } // namespace user_notes diff --git a/chromium/components/user_notes/model/user_note.cc b/chromium/components/user_notes/model/user_note.cc index 1259680d8df..a17e37c58a2 100644 --- a/chromium/components/user_notes/model/user_note.cc +++ b/chromium/components/user_notes/model/user_note.cc @@ -21,4 +21,11 @@ base::SafeRef<UserNote> UserNote::GetSafeRef() const { return weak_ptr_factory_.GetSafeRef(); } +void UserNote::Update(std::unique_ptr<UserNote> new_model) { + DCHECK(new_model->id() == id_); + metadata_ = std::move(new_model->metadata_); + body_ = std::move(new_model->body_); + target_ = std::move(new_model->target_); +} + } // namespace user_notes diff --git a/chromium/components/user_notes/model/user_note.h b/chromium/components/user_notes/model/user_note.h index b1746c4f8b2..9d008197e9c 100644 --- a/chromium/components/user_notes/model/user_note.h +++ b/chromium/components/user_notes/model/user_note.h @@ -35,6 +35,9 @@ class UserNote { const UserNoteBody& body() const { return *body_; } const UserNoteTarget& target() const { return *target_; } + // Consumes the provided model to update this one. + void Update(std::unique_ptr<UserNote> new_model); + private: // The unique (among the user's notes) ID for this note. base::UnguessableToken id_; diff --git a/chromium/components/user_notes/model/user_note_body.cc b/chromium/components/user_notes/model/user_note_body.cc index d0d8a5d326d..26c0a4ca752 100644 --- a/chromium/components/user_notes/model/user_note_body.cc +++ b/chromium/components/user_notes/model/user_note_body.cc @@ -6,7 +6,7 @@ namespace user_notes { -UserNoteBody::UserNoteBody(const std::string& plain_text_value) +UserNoteBody::UserNoteBody(const std::u16string& plain_text_value) : plain_text_value_(plain_text_value) {} UserNoteBody::~UserNoteBody() = default; diff --git a/chromium/components/user_notes/model/user_note_body.h b/chromium/components/user_notes/model/user_note_body.h index b80acae5859..ed07086af5d 100644 --- a/chromium/components/user_notes/model/user_note_body.h +++ b/chromium/components/user_notes/model/user_note_body.h @@ -14,21 +14,21 @@ class UserNoteBody { public: enum BodyType { PLAIN_TEXT = 0, RICH_TEXT, IMAGE }; - explicit UserNoteBody(const std::string& plain_text_value); + explicit UserNoteBody(const std::u16string& plain_text_value); ~UserNoteBody(); UserNoteBody(const UserNoteBody&) = delete; UserNoteBody& operator=(const UserNoteBody&) = delete; BodyType type() const { return type_; } - const std::string& plain_text_value() const { return plain_text_value_; } + const std::u16string& plain_text_value() const { return plain_text_value_; } private: // The type of body this note has. Currently only plain text is supported. BodyType type_ = BodyType::PLAIN_TEXT; // The note body in plain text - std::string plain_text_value_; + std::u16string plain_text_value_; }; } // namespace user_notes diff --git a/chromium/components/user_notes/model/user_note_model_test_utils.cc b/chromium/components/user_notes/model/user_note_model_test_utils.cc index 41d9de23997..789714097b7 100644 --- a/chromium/components/user_notes/model/user_note_model_test_utils.cc +++ b/chromium/components/user_notes/model/user_note_model_test_utils.cc @@ -15,13 +15,13 @@ std::unique_ptr<UserNoteMetadata> GetTestUserNoteMetadata() { } std::unique_ptr<UserNoteBody> GetTestUserNoteBody() { - return std::make_unique<UserNoteBody>("test note"); + return std::make_unique<UserNoteBody>(u"test note"); } std::unique_ptr<UserNoteTarget> GetTestUserNotePageTarget( const std::string& url) { return std::make_unique<UserNoteTarget>(UserNoteTarget::TargetType::kPage, - /*original_text=*/"", GURL(url), + /*original_text=*/u"", GURL(url), /*selector=*/""); } diff --git a/chromium/components/user_notes/model/user_note_target.cc b/chromium/components/user_notes/model/user_note_target.cc index 1630d62bf1b..1eeaba815f9 100644 --- a/chromium/components/user_notes/model/user_note_target.cc +++ b/chromium/components/user_notes/model/user_note_target.cc @@ -7,7 +7,7 @@ namespace user_notes { UserNoteTarget::UserNoteTarget(TargetType type, - const std::string& original_text, + const std::u16string& original_text, GURL target_page, const std::string& selector) : type_(type), diff --git a/chromium/components/user_notes/model/user_note_target.h b/chromium/components/user_notes/model/user_note_target.h index 0ddb50ba8c9..ec9616d84d1 100644 --- a/chromium/components/user_notes/model/user_note_target.h +++ b/chromium/components/user_notes/model/user_note_target.h @@ -17,7 +17,7 @@ class UserNoteTarget { enum TargetType { kPage = 0, kPageText }; explicit UserNoteTarget(TargetType type, - const std::string& original_text, + const std::u16string& original_text, GURL target_page, const std::string& selector); ~UserNoteTarget(); @@ -25,7 +25,7 @@ class UserNoteTarget { UserNoteTarget& operator=(const UserNoteTarget&) = delete; TargetType type() const { return type_; } - const std::string& original_text() const { return original_text_; } + const std::u16string& original_text() const { return original_text_; } const GURL& target_page() const { return target_page_; } const std::string& selector() const { return selector_; } @@ -35,7 +35,7 @@ class UserNoteTarget { // The original text to which the note was attached. Useful if the page // changes. Empty for `TargetType::PAGE`. - std::string original_text_; + std::u16string original_text_; // The URL of the page the note is attached to. GURL target_page_; diff --git a/chromium/components/user_notes/storage/user_note_database.cc b/chromium/components/user_notes/storage/user_note_database.cc index 85bfccc9e30..b75bb9aabe6 100644 --- a/chromium/components/user_notes/storage/user_note_database.cc +++ b/chromium/components/user_notes/storage/user_note_database.cc @@ -5,6 +5,7 @@ #include "components/user_notes/storage/user_note_database.h" #include "base/files/file_util.h" +#include "base/json/values_util.h" #include "sql/error_delegate_util.h" #include "sql/meta_table.h" #include "sql/statement.h" @@ -70,33 +71,146 @@ bool UserNoteDatabase::Init() { } UserNoteMetadataSnapshot UserNoteDatabase::GetNoteMetadataForUrls( - std::vector<GURL> urls) { + const UserNoteStorage::UrlSet& urls) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - // TODO(gayane): Implement. + if (!EnsureDBInit()) + return UserNoteMetadataSnapshot(); + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return UserNoteMetadataSnapshot(); + + UserNoteMetadataSnapshot metadata_snapshot; + for (GURL url : urls) { + sql::Statement statement( + db_.GetCachedStatement(SQL_FROM_HERE, + "SELECT id, creation_date, modification_date " + "FROM notes WHERE url = ?")); + + if (!statement.is_valid()) + continue; + + statement.BindString(0, url.spec()); + + while (statement.Step()) { + DCHECK_EQ(3, statement.ColumnCount()); + + std::string id = statement.ColumnString(0); + base::StringPiece string_piece(id); + uint64_t high = 0; + uint64_t low = 0; + if (!base::HexStringToUInt64(string_piece.substr(0, 16), &high) || + !base::HexStringToUInt64(string_piece.substr(16, 16), &low)) { + continue; + } + base::UnguessableToken token = + base::UnguessableToken::Deserialize(high, low); + + base::Time creation_date = statement.ColumnTime(1); + base::Time modification_date = statement.ColumnTime(2); + + auto metadata = std::make_unique<UserNoteMetadata>( + creation_date, modification_date, /*min_note_version=*/1); + metadata_snapshot.AddEntry(url, token, std::move(metadata)); + } + } - return UserNoteMetadataSnapshot(); + transaction.Commit(); + + return metadata_snapshot; } std::vector<std::unique_ptr<UserNote>> UserNoteDatabase::GetNotesById( - std::vector<base::UnguessableToken> ids) { + const UserNoteStorage::IdSet& ids) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + std::vector<std::unique_ptr<UserNote>> user_notes; + + if (!EnsureDBInit()) + return user_notes; + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return user_notes; + + for (const base::UnguessableToken& id : ids) { + auto user_note = GetNoteById(id); + if (!user_note) + continue; + + user_notes.emplace_back(std::move(user_note)); + } + + transaction.Commit(); + + return user_notes; +} - // TODO(gayane): Implement. +std::unique_ptr<UserNote> UserNoteDatabase::GetNoteById( + const base::UnguessableToken& id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - return std::vector<std::unique_ptr<UserNote>>(); + // Get creation_date, modification_date, url and type from notes. + sql::Statement statement_notes( + db_.GetCachedStatement(SQL_FROM_HERE, + "SELECT creation_date, modification_date, url, " + "type FROM notes WHERE id = ?")); + if (!statement_notes.is_valid()) + return nullptr; + statement_notes.BindString(0, id.ToString()); + if (!statement_notes.Step()) + return nullptr; + DCHECK_EQ(4, statement_notes.ColumnCount()); + base::Time creation_date = statement_notes.ColumnTime(0); + base::Time modification_date = statement_notes.ColumnTime(1); + std::string url = statement_notes.ColumnString(2); + int type = statement_notes.ColumnInt(3); + auto metadata = std::make_unique<UserNoteMetadata>( + creation_date, modification_date, /*min_note_version=*/1); + + // Get plain_text from notes_body. + sql::Statement statement_notes_body(db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT plain_text FROM notes_body WHERE note_id = ?")); + if (!statement_notes_body.is_valid()) + return nullptr; + statement_notes_body.BindString(0, id.ToString()); + if (!statement_notes_body.Step()) + return nullptr; + DCHECK_EQ(1, statement_notes_body.ColumnCount()); + auto body = + std::make_unique<UserNoteBody>(statement_notes_body.ColumnString16(0)); + + // Get original_text and selector from notes_text_target. + sql::Statement statement_notes_text_target( + db_.GetCachedStatement(SQL_FROM_HERE, + "SELECT original_text, selector FROM " + "notes_text_target WHERE note_id = ?")); + if (!statement_notes_text_target.is_valid()) + return nullptr; + statement_notes_text_target.BindString(0, id.ToString()); + if (!statement_notes_text_target.Step()) + return nullptr; + DCHECK_EQ(2, statement_notes_text_target.ColumnCount()); + std::u16string original_text = statement_notes_text_target.ColumnString16(0); + std::string selector = statement_notes_text_target.ColumnString(1); + auto target = std::make_unique<UserNoteTarget>( + static_cast<UserNoteTarget::TargetType>(type), original_text, GURL(url), + selector); + + return std::make_unique<UserNote>(id, std::move(metadata), std::move(body), + std::move(target)); } -void UserNoteDatabase::CreateNote(const UserNote* model, - std::string note_body_text) { +bool UserNoteDatabase::CreateNote(const UserNote* model, + std::u16string note_body_text) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!EnsureDBInit()) - return; + return false; sql::Transaction transaction(&db_); if (!transaction.Begin()) - return; + return false; sql::Statement create_note(db_.GetCachedStatement(SQL_FROM_HERE, "INSERT INTO notes(" @@ -109,7 +223,7 @@ void UserNoteDatabase::CreateNote(const UserNote* model, "VALUES(?,?,?,?,?,?)")); if (!create_note.is_valid()) - return; + return false; // TODO: possibly the time should be passed to this function, for example for // sync to add notes with past creation date. @@ -122,57 +236,55 @@ void UserNoteDatabase::CreateNote(const UserNote* model, create_note.BindInt(5, model->target().type()); if (!create_note.Run()) - return; + return false; - if (model->target().type() == UserNoteTarget::TargetType::kPageText) { - sql::Statement notes_text_target(db_.GetCachedStatement( - SQL_FROM_HERE, - "INSERT INTO notes_text_target(note_id, original_text, selector) " - "VALUES(?,?,?)")); - if (!notes_text_target.is_valid()) - return; + sql::Statement notes_text_target(db_.GetCachedStatement( + SQL_FROM_HERE, + "INSERT INTO notes_text_target(note_id, original_text, selector) " + "VALUES(?,?,?)")); + if (!notes_text_target.is_valid()) + return false; - notes_text_target.BindString(0, model->id().ToString()); - notes_text_target.BindString(1, model->target().original_text()); - notes_text_target.BindString(2, model->target().selector()); + notes_text_target.BindString(0, model->id().ToString()); + notes_text_target.BindString16(1, model->target().original_text()); + notes_text_target.BindString(2, model->target().selector()); - if (!notes_text_target.Run()) - return; - } + if (!notes_text_target.Run()) + return false; sql::Statement notes_body(db_.GetCachedStatement( SQL_FROM_HERE, "INSERT INTO notes_body(note_id, type, plain_text) " "VALUES(?,?,?)")); if (!notes_body.is_valid()) - return; + return false; notes_body.BindString(0, model->id().ToString()); notes_body.BindInt(1, UserNoteBody::BodyType::PLAIN_TEXT); - notes_body.BindString(2, note_body_text); + notes_body.BindString16(2, note_body_text); if (!notes_body.Run()) - return; + return false; transaction.Commit(); + return true; } -void UserNoteDatabase::UpdateNote(const UserNote* model, - std::string note_body_text, +bool UserNoteDatabase::UpdateNote(const UserNote* model, + std::u16string note_body_text, bool is_creation) { if (is_creation) { - CreateNote(model, note_body_text); - return; + return CreateNote(model, note_body_text); } DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!EnsureDBInit()) - return; + return false; sql::Transaction transaction(&db_); if (!transaction.Begin()) - return; + return false; // Only the text of the note body can be modified. // TODO(crbug.com/1313967): This will need to be updated if in the future we @@ -180,85 +292,185 @@ void UserNoteDatabase::UpdateNote(const UserNote* model, sql::Statement update_notes_body(db_.GetCachedStatement( SQL_FROM_HERE, "UPDATE notes_body SET plain_text = ? WHERE note_id = ?")); if (!update_notes_body.is_valid()) - return; + return false; - update_notes_body.BindString(0, note_body_text); + update_notes_body.BindString16(0, note_body_text); update_notes_body.BindString(1, model->id().ToString()); if (!update_notes_body.Run()) - return; + return false; sql::Statement update_modification_date(db_.GetCachedStatement( SQL_FROM_HERE, "UPDATE notes SET modification_date = ? WHERE id = ?")); if (!update_modification_date.is_valid()) - return; + return false; update_modification_date.BindTime(0, base::Time::Now()); update_modification_date.BindString(1, model->id().ToString()); if (!update_modification_date.Run()) - return; + return false; transaction.Commit(); + + return true; } -void UserNoteDatabase::DeleteNote(const base::UnguessableToken& id) { +bool UserNoteDatabase::DeleteNoteWithStringId(std::string id) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - if (!EnsureDBInit()) - return; - - sql::Transaction transaction(&db_); - if (!transaction.Begin()) - return; - sql::Statement delete_notes_body(db_.GetCachedStatement( SQL_FROM_HERE, "DELETE FROM notes_body WHERE note_id = ?")); if (!delete_notes_body.is_valid()) - return; + return false; - delete_notes_body.BindString(0, id.ToString()); + delete_notes_body.BindString(0, id); if (!delete_notes_body.Run()) - return; + return false; sql::Statement delete_notes_text_target(db_.GetCachedStatement( SQL_FROM_HERE, "DELETE FROM notes_text_target WHERE note_id = ?")); if (!delete_notes_text_target.is_valid()) - return; + return false; - delete_notes_text_target.BindString(0, id.ToString()); + delete_notes_text_target.BindString(0, id); if (!delete_notes_text_target.Run()) - return; + return false; sql::Statement delete_notes( db_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM notes WHERE id = ?")); if (!delete_notes.is_valid()) - return; + return false; - delete_notes.BindString(0, id.ToString()); + delete_notes.BindString(0, id); if (!delete_notes.Run()) - return; + return false; + + return true; +} +bool UserNoteDatabase::DeleteNote(const base::UnguessableToken& id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!EnsureDBInit()) + return false; + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return false; + + bool is_notes_deleted = DeleteNoteWithStringId(id.ToString()); transaction.Commit(); + return is_notes_deleted; } -void UserNoteDatabase::DeleteAllForUrl(const GURL& url) { +bool UserNoteDatabase::DeleteAllForUrl(const GURL& url) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - // TODO(gayane): Implement. + if (!EnsureDBInit()) + return false; + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return false; + + sql::Statement statement(db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT id FROM notes WHERE url = ?")); + + if (!statement.is_valid()) + return false; + + statement.BindString(0, url.spec()); + + std::vector<std::string> ids; + while (statement.Step()) + ids.emplace_back(statement.ColumnString(0)); + + if (!statement.Succeeded()) + return false; + + for (const std::string& id : ids) { + if (!DeleteNoteWithStringId(id)) + return false; + } + + transaction.Commit(); + return true; } -void UserNoteDatabase::DeleteAllForOrigin(const url::Origin& page) { +bool UserNoteDatabase::DeleteAllForOrigin(const url::Origin& page) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - // TODO(gayane): Implement. + if (!EnsureDBInit()) + return false; + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return false; + + sql::Statement statement(db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT id FROM notes WHERE origin = ?")); + + if (!statement.is_valid()) + return false; + + statement.BindString(0, page.Serialize()); + + std::vector<std::string> ids; + while (statement.Step()) { + ids.emplace_back(statement.ColumnString(0)); + } + + if (!statement.Succeeded()) + return false; + + for (const std::string& id : ids) { + if (!DeleteNoteWithStringId(id)) + return false; + } + + transaction.Commit(); + return true; } -void UserNoteDatabase::DeleteAllNotes() { +bool UserNoteDatabase::DeleteAllNotes() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - // TODO(gayane): Implement. + if (!EnsureDBInit()) + return false; + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return false; + + sql::Statement delete_notes_body( + db_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM notes_body")); + + if (!delete_notes_body.is_valid()) + return false; + + if (!delete_notes_body.Run()) + return false; + + sql::Statement delete_notes_text_target( + db_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM notes_text_target")); + if (!delete_notes_text_target.is_valid()) + return false; + + if (!delete_notes_text_target.Run()) + return false; + + sql::Statement delete_notes( + db_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM notes")); + if (!delete_notes.is_valid()) + return false; + + if (!delete_notes.Run()) + return false; + + transaction.Commit(); + return true; } bool UserNoteDatabase::EnsureDBInit() { diff --git a/chromium/components/user_notes/storage/user_note_database.h b/chromium/components/user_notes/storage/user_note_database.h index 75535ca4053..1831b945bee 100644 --- a/chromium/components/user_notes/storage/user_note_database.h +++ b/chromium/components/user_notes/storage/user_note_database.h @@ -9,9 +9,11 @@ #include "base/callback.h" #include "base/files/file_path.h" +#include "base/observer_list.h" #include "base/sequence_checker.h" #include "base/thread_annotations.h" #include "components/user_notes/interfaces/user_note_metadata_snapshot.h" +#include "components/user_notes/interfaces/user_note_storage.h" #include "components/user_notes/model/user_note.h" #include "sql/database.h" #include "url/gurl.h" @@ -32,27 +34,28 @@ class UserNoteDatabase { // Initialises internal database. Must be called prior to any other usage. bool Init(); - UserNoteMetadataSnapshot GetNoteMetadataForUrls(std::vector<GURL> urls); + UserNoteMetadataSnapshot GetNoteMetadataForUrls( + const UserNoteStorage::UrlSet& urls); std::vector<std::unique_ptr<UserNote>> GetNotesById( - std::vector<base::UnguessableToken> ids); + const UserNoteStorage::IdSet& ids); - void UpdateNote(const UserNote* model, - std::string note_body_text, + bool UpdateNote(const UserNote* model, + std::u16string note_body_text, bool is_creation); - void DeleteNote(const base::UnguessableToken& id); + bool DeleteNote(const base::UnguessableToken& id); - void DeleteAllForUrl(const GURL& url); + bool DeleteAllForUrl(const GURL& url); - void DeleteAllForOrigin(const url::Origin& origin); + bool DeleteAllForOrigin(const url::Origin& origin); - void DeleteAllNotes(); + bool DeleteAllNotes(); private: - FRIEND_TEST_ALL_PREFIXES(UserNoteDatabaseTest, UpdateNote); - FRIEND_TEST_ALL_PREFIXES(UserNoteDatabaseTest, CreateNote); - FRIEND_TEST_ALL_PREFIXES(UserNoteDatabaseTest, DeleteNote); + FRIEND_TEST_ALL_PREFIXES(UserNoteDatabaseTest, GetNotesById); + FRIEND_TEST_ALL_PREFIXES(UserNoteDatabaseTest, GetNoteMetadataForUrls); + friend class UserNoteDatabaseTest; // Initialises internal database if needed. bool EnsureDBInit(); @@ -64,10 +67,14 @@ class UserNoteDatabase { bool InitSchema(); // Called by UpdateNote() with is_creation=true to create a new note. - void CreateNote(const UserNote* model, std::string note_body_text); + bool CreateNote(const UserNote* model, std::u16string note_body_text); bool CreateSchema(); + bool DeleteNoteWithStringId(std::string id); + + std::unique_ptr<UserNote> GetNoteById(const base::UnguessableToken& id); + sql::Database db_ GUARDED_BY_CONTEXT(sequence_checker_); const base::FilePath db_file_path_; diff --git a/chromium/components/user_notes/storage/user_note_database_unittest.cc b/chromium/components/user_notes/storage/user_note_database_unittest.cc index e7bdde4409a..c84a9f5af3a 100644 --- a/chromium/components/user_notes/storage/user_note_database_unittest.cc +++ b/chromium/components/user_notes/storage/user_note_database_unittest.cc @@ -4,6 +4,10 @@ #include "components/user_notes/storage/user_note_database.h" +#include <algorithm> +#include <vector> + +#include "base/containers/contains.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/path_service.h" @@ -29,6 +33,47 @@ class UserNoteDatabaseTest : public testing::Test { return temp_directory_.GetPath().Append(kDatabaseName); } + void check_is_removed_from_db(UserNoteDatabase* user_note_db, + const base::UnguessableToken& id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db->sequence_checker_); + + sql::Statement statement_notes_body(user_note_db->db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT note_id FROM notes_body WHERE note_id = ?")); + EXPECT_TRUE(statement_notes_body.is_valid()); + statement_notes_body.BindString(0, id.ToString()); + EXPECT_FALSE(statement_notes_body.Step()); + + sql::Statement statement_notes(user_note_db->db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT id FROM notes WHERE id = ?")); + EXPECT_TRUE(statement_notes.is_valid()); + statement_notes.BindString(0, id.ToString()); + EXPECT_FALSE(statement_notes.Step()); + + sql::Statement statement_notes_text_target( + user_note_db->db_.GetCachedStatement( + SQL_FROM_HERE, + "SELECT note_id FROM notes_text_target WHERE note_id = ?")); + EXPECT_TRUE(statement_notes_text_target.is_valid()); + statement_notes_text_target.BindString(0, id.ToString()); + EXPECT_FALSE(statement_notes_text_target.Step()); + } + + void check_notes_body_from_db(UserNoteDatabase* user_note_db, + const base::UnguessableToken& id, + const std::u16string& text) { + DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db->sequence_checker_); + + sql::Statement statement(user_note_db->db_.GetCachedStatement( + SQL_FROM_HERE, "SELECT plain_text FROM notes_body WHERE note_id = ?")); + + EXPECT_TRUE(statement.is_valid()); + statement.BindString(0, id.ToString()); + EXPECT_TRUE(statement.Step()); + + EXPECT_EQ(1, statement.ColumnCount()); + EXPECT_EQ(text, statement.ColumnString16(0)); + } + private: base::ScopedTempDir temp_directory_; }; @@ -113,85 +158,220 @@ TEST_F(UserNoteDatabaseTest, DatabaseHasSchemaNoMeta) { TEST_F(UserNoteDatabaseTest, CreateNote) { UserNoteDatabase user_note_db(db_dir()); EXPECT_TRUE(user_note_db.Init()); - DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db.sequence_checker_); base::UnguessableToken note_id = base::UnguessableToken::Create(); UserNote* user_note = new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), GetTestUserNotePageTarget()); - user_note_db.UpdateNote(user_note, "new test note", /*is_creation=*/true); - - sql::Statement statement(user_note_db.db_.GetCachedStatement( - SQL_FROM_HERE, "SELECT plain_text FROM notes_body WHERE note_id = ?")); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); - EXPECT_TRUE(statement.is_valid()); - statement.BindString(0, note_id.ToString()); - EXPECT_TRUE(statement.Step()); - - EXPECT_EQ(1, statement.ColumnCount()); - EXPECT_EQ("new test note", statement.ColumnString(0)); + check_notes_body_from_db(&user_note_db, note_id, u"new test note"); delete user_note; } TEST_F(UserNoteDatabaseTest, UpdateNote) { UserNoteDatabase user_note_db(db_dir()); EXPECT_TRUE(user_note_db.Init()); - DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db.sequence_checker_); base::UnguessableToken note_id = base::UnguessableToken::Create(); UserNote* user_note = new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), GetTestUserNotePageTarget()); - user_note_db.UpdateNote(user_note, "new test note", /*is_creation=*/true); - user_note_db.UpdateNote(user_note, "edit test note", false); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + bool update_note = + user_note_db.UpdateNote(user_note, u"edit test note", false); + EXPECT_TRUE(create_note); + EXPECT_TRUE(update_note); - sql::Statement statement(user_note_db.db_.GetCachedStatement( - SQL_FROM_HERE, "SELECT plain_text FROM notes_body WHERE note_id = ?")); - - EXPECT_TRUE(statement.is_valid()); - statement.BindString(0, note_id.ToString()); - EXPECT_TRUE(statement.Step()); - - EXPECT_EQ(1, statement.ColumnCount()); - EXPECT_EQ("edit test note", statement.ColumnString(0)); + check_notes_body_from_db(&user_note_db, note_id, u"edit test note"); delete user_note; } TEST_F(UserNoteDatabaseTest, DeleteNote) { UserNoteDatabase user_note_db(db_dir()); EXPECT_TRUE(user_note_db.Init()); - DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db.sequence_checker_); base::UnguessableToken note_id = base::UnguessableToken::Create(); UserNote* user_note = new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), GetTestUserNotePageTarget()); - user_note_db.UpdateNote(user_note, "new test note", /*is_creation=*/true); - user_note_db.DeleteNote(note_id); - - sql::Statement statement_notes_body(user_note_db.db_.GetCachedStatement( - SQL_FROM_HERE, "SELECT note_id FROM notes_body WHERE note_id = ?")); - EXPECT_TRUE(statement_notes_body.is_valid()); - statement_notes_body.BindString(0, note_id.ToString()); - EXPECT_FALSE(statement_notes_body.Step()); - - sql::Statement statement_notes(user_note_db.db_.GetCachedStatement( - SQL_FROM_HERE, "SELECT id FROM notes WHERE id = ?")); - EXPECT_TRUE(statement_notes.is_valid()); - statement_notes.BindString(0, note_id.ToString()); - EXPECT_FALSE(statement_notes.Step()); - - sql::Statement statement_notes_text_target( - user_note_db.db_.GetCachedStatement( - SQL_FROM_HERE, - "SELECT note_id FROM notes_text_target WHERE note_id = ?")); - EXPECT_TRUE(statement_notes_text_target.is_valid()); - statement_notes_text_target.BindString(0, note_id.ToString()); - EXPECT_FALSE(statement_notes_text_target.Step()); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); + bool delete_note = user_note_db.DeleteNote(note_id); + EXPECT_TRUE(delete_note); + + check_is_removed_from_db(&user_note_db, note_id); delete user_note; } +TEST_F(UserNoteDatabaseTest, GetNotesById) { + UserNoteDatabase user_note_db(db_dir()); + EXPECT_TRUE(user_note_db.Init()); + DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db.sequence_checker_); + + UserNoteStorage::IdSet id_set; + std::vector<base::UnguessableToken> ids; + for (int i = 0; i < 3; i++) { + base::UnguessableToken note_id = base::UnguessableToken::Create(); + ids.emplace_back(note_id); + id_set.emplace(note_id); + std::u16string original_text = + u"original text " + base::NumberToString16(i); + std::string selector = "selector " + base::NumberToString(i); + std::u16string body = u"new test note " + base::NumberToString16(i); + GURL url = GURL("https://www.test.com/"); + auto test_target = std::make_unique<UserNoteTarget>( + UserNoteTarget::TargetType::kPageText, original_text, url, selector); + UserNote* user_note = + new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), + std::move(test_target)); + bool create_note = + user_note_db.UpdateNote(user_note, body, /*is_creation=*/true); + EXPECT_TRUE(create_note); + delete user_note; + } + + std::vector<std::unique_ptr<UserNote>> notes = + user_note_db.GetNotesById(id_set); + EXPECT_EQ(3u, notes.size()); + + for (std::unique_ptr<UserNote>& note : notes) { + const auto& vector_it = std::find(ids.begin(), ids.end(), note->id()); + EXPECT_NE(vector_it, ids.end()); + EXPECT_NE(id_set.find(note->id()), id_set.end()); + int i = vector_it - ids.begin(); + EXPECT_EQ("https://www.test.com/", note->target().target_page().spec()); + EXPECT_EQ(u"original text " + base::NumberToString16(i), + note->target().original_text()); + EXPECT_EQ("selector " + base::NumberToString(i), note->target().selector()); + EXPECT_EQ(u"new test note " + base::NumberToString16(i), + note->body().plain_text_value()); + EXPECT_EQ(UserNoteTarget::TargetType::kPageText, note->target().type()); + } +} + +TEST_F(UserNoteDatabaseTest, DeleteAllNotes) { + UserNoteDatabase user_note_db(db_dir()); + EXPECT_TRUE(user_note_db.Init()); + + UserNoteStorage::IdSet ids; + for (int i = 0; i < 3; i++) { + base::UnguessableToken note_id = base::UnguessableToken::Create(); + ids.emplace(note_id); + UserNote* user_note = + new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), + GetTestUserNotePageTarget()); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); + delete user_note; + } + bool delete_notes = user_note_db.DeleteAllNotes(); + EXPECT_TRUE(delete_notes); + + for (const base::UnguessableToken& id : ids) { + check_is_removed_from_db(&user_note_db, id); + } +} + +TEST_F(UserNoteDatabaseTest, DeleteAllForOrigin) { + UserNoteDatabase user_note_db(db_dir()); + EXPECT_TRUE(user_note_db.Init()); + + UserNoteStorage::IdSet ids; + for (int i = 0; i < 3; i++) { + base::UnguessableToken note_id = base::UnguessableToken::Create(); + ids.emplace(note_id); + UserNote* user_note = + new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), + GetTestUserNotePageTarget("https://www.test.com")); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); + delete user_note; + } + + bool delete_notes = user_note_db.DeleteAllForOrigin( + url::Origin::Create(GURL("https://www.test.com"))); + EXPECT_TRUE(delete_notes); + + for (const base::UnguessableToken& id : ids) { + check_is_removed_from_db(&user_note_db, id); + } +} + +TEST_F(UserNoteDatabaseTest, DeleteAllForUrl) { + UserNoteDatabase user_note_db(db_dir()); + EXPECT_TRUE(user_note_db.Init()); + + UserNoteStorage::IdSet ids; + for (int i = 0; i < 3; i++) { + base::UnguessableToken note_id = base::UnguessableToken::Create(); + ids.emplace(note_id); + UserNote* user_note = + new UserNote(note_id, GetTestUserNoteMetadata(), GetTestUserNoteBody(), + GetTestUserNotePageTarget("https://www.test.com")); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); + delete user_note; + } + bool delete_notes = + user_note_db.DeleteAllForUrl(GURL("https://www.test.com")); + EXPECT_TRUE(delete_notes); + + for (const base::UnguessableToken& id : ids) { + check_is_removed_from_db(&user_note_db, id); + } +} + +TEST_F(UserNoteDatabaseTest, GetNoteMetadataForUrls) { + UserNoteDatabase user_note_db(db_dir()); + EXPECT_TRUE(user_note_db.Init()); + DCHECK_CALLED_ON_VALID_SEQUENCE(user_note_db.sequence_checker_); + + UserNoteStorage::IdSet ids; + base::Time time = base::Time::FromDoubleT(1600000000); + int note_version = 1; + for (int i = 0; i < 3; i++) { + base::UnguessableToken note_id = base::UnguessableToken::Create(); + ids.emplace(note_id); + auto note_metadata = + std::make_unique<UserNoteMetadata>(time, time, note_version); + UserNote* user_note = + new UserNote(note_id, std::move(note_metadata), GetTestUserNoteBody(), + GetTestUserNotePageTarget("https://www.test.com")); + bool create_note = user_note_db.UpdateNote(user_note, u"new test note", + /*is_creation=*/true); + EXPECT_TRUE(create_note); + delete user_note; + } + + GURL url1 = GURL("https://www.test.com"); + GURL url2 = GURL("https://www.test.com"); + GURL url3 = GURL("https://www.test.com/2"); + UserNoteStorage::UrlSet urls{url1, url2, url3}; + UserNoteMetadataSnapshot metadata_snapshot = + user_note_db.GetNoteMetadataForUrls(urls); + const UserNoteMetadataSnapshot::IdToMetadataMap* metadata_map = + metadata_snapshot.GetMapForUrl(url1); + EXPECT_EQ(3u, metadata_map->size()); + EXPECT_EQ(3u, metadata_snapshot.GetMapForUrl(url2)->size()); + EXPECT_EQ(nullptr, metadata_snapshot.GetMapForUrl(url3)); + + for (const auto& metadata_it : *metadata_map) { + EXPECT_TRUE(base::Contains(ids, metadata_it.first)); + EXPECT_EQ(time, metadata_it.second->creation_date()); + EXPECT_EQ(time, metadata_it.second->modification_date()); + EXPECT_EQ(note_version, metadata_it.second->min_note_version()); + } +} + } // namespace user_notes diff --git a/chromium/components/user_notes/storage/user_note_storage_impl.cc b/chromium/components/user_notes/storage/user_note_storage_impl.cc index afdc176e096..832f6760d32 100644 --- a/chromium/components/user_notes/storage/user_note_storage_impl.cc +++ b/chromium/components/user_notes/storage/user_note_storage_impl.cc @@ -4,6 +4,10 @@ #include "components/user_notes/storage/user_note_storage_impl.h" +#include <unordered_set> +#include <vector> + +#include "base/bind.h" #include "base/callback.h" #include "base/files/file_path.h" #include "base/memory/scoped_refptr.h" @@ -21,11 +25,24 @@ UserNoteStorageImpl::UserNoteStorageImpl( {base::MayBlock(), base::TaskPriority::USER_VISIBLE, base::TaskShutdownBehavior::BLOCK_SHUTDOWN}), path_to_database_dir) { - database_.AsyncCall(&UserNoteDatabase::Init); + // An empty `Then()` is needed to satisfy a DCHECK in `AsyncCall` because + // `UserNoteDatabase::Init` returns a value. + database_.AsyncCall(&UserNoteDatabase::Init) + .Then(base::BindOnce([](bool result) {})); +} + +UserNoteStorageImpl::~UserNoteStorageImpl() = default; + +void UserNoteStorageImpl::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void UserNoteStorageImpl::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); } void UserNoteStorageImpl::GetNoteMetadataForUrls( - std::vector<GURL> urls, + const UserNoteStorage::UrlSet& urls, base::OnceCallback<void(UserNoteMetadataSnapshot)> callback) { database_.AsyncCall(&UserNoteDatabase::GetNoteMetadataForUrls) .WithArgs(std::move(urls)) @@ -33,7 +50,7 @@ void UserNoteStorageImpl::GetNoteMetadataForUrls( } void UserNoteStorageImpl::GetNotesById( - std::vector<base::UnguessableToken> ids, + const UserNoteStorage::IdSet& ids, base::OnceCallback<void(std::vector<std::unique_ptr<UserNote>>)> callback) { database_.AsyncCall(&UserNoteDatabase::GetNotesById) .WithArgs(std::move(ids)) @@ -41,26 +58,47 @@ void UserNoteStorageImpl::GetNotesById( } void UserNoteStorageImpl::UpdateNote(const UserNote* model, - std::string note_body_text, + std::u16string note_body_text, bool is_creation) { database_.AsyncCall(&UserNoteDatabase::UpdateNote) - .WithArgs(model, note_body_text, is_creation); + .WithArgs(model, note_body_text, is_creation) + .Then(base::BindOnce(&UserNoteStorageImpl::OnNotesChanged, + weak_factory_.GetWeakPtr())); } void UserNoteStorageImpl::DeleteNote(const base::UnguessableToken& id) { - database_.AsyncCall(&UserNoteDatabase::DeleteNote).WithArgs(id); + database_.AsyncCall(&UserNoteDatabase::DeleteNote) + .WithArgs(id) + .Then(base::BindOnce(&UserNoteStorageImpl::OnNotesChanged, + weak_factory_.GetWeakPtr())); } void UserNoteStorageImpl::DeleteAllForUrl(const GURL& url) { - database_.AsyncCall(&UserNoteDatabase::DeleteAllForUrl).WithArgs(url); + database_.AsyncCall(&UserNoteDatabase::DeleteAllForUrl) + .WithArgs(url) + .Then(base::BindOnce(&UserNoteStorageImpl::OnNotesChanged, + weak_factory_.GetWeakPtr())); } void UserNoteStorageImpl::DeleteAllForOrigin(const url::Origin& origin) { - database_.AsyncCall(&UserNoteDatabase::DeleteAllForOrigin).WithArgs(origin); + database_.AsyncCall(&UserNoteDatabase::DeleteAllForOrigin) + .WithArgs(origin) + .Then(base::BindOnce(&UserNoteStorageImpl::OnNotesChanged, + weak_factory_.GetWeakPtr())); } void UserNoteStorageImpl::DeleteAllNotes() { - database_.AsyncCall(&UserNoteDatabase::DeleteAllNotes); + database_.AsyncCall(&UserNoteDatabase::DeleteAllNotes) + .Then(base::BindOnce(&UserNoteStorageImpl::OnNotesChanged, + weak_factory_.GetWeakPtr())); +} + +void UserNoteStorageImpl::OnNotesChanged(bool notes_changed) { + if (!notes_changed) + return; + + for (auto& observer : observers_) + observer.OnNotesChanged(); } } // namespace user_notes diff --git a/chromium/components/user_notes/storage/user_note_storage_impl.h b/chromium/components/user_notes/storage/user_note_storage_impl.h index b12a0852979..1ecb4229da1 100644 --- a/chromium/components/user_notes/storage/user_note_storage_impl.h +++ b/chromium/components/user_notes/storage/user_note_storage_impl.h @@ -28,18 +28,24 @@ class UserNoteStorageImpl : public UserNoteStorage { UserNoteStorageImpl(const UserNoteStorageImpl& other) = delete; UserNoteStorageImpl& operator=(const UserNoteStorageImpl& other) = delete; + using Observer = UserNoteStorage::Observer; + // Implement UserNoteStorage + void AddObserver(Observer* observer) override; + + void RemoveObserver(Observer* observer) override; + void GetNoteMetadataForUrls( - std::vector<GURL> urls, + const UserNoteStorage::UrlSet& urls, base::OnceCallback<void(UserNoteMetadataSnapshot)> callback) override; void GetNotesById( - std::vector<base::UnguessableToken> ids, + const UserNoteStorage::IdSet& ids, base::OnceCallback<void(std::vector<std::unique_ptr<UserNote>>)> callback) override; void UpdateNote(const UserNote* model, - std::string note_body_text, + std::u16string note_body_text, bool is_creation = false) override; void DeleteNote(const base::UnguessableToken& id) override; @@ -51,9 +57,14 @@ class UserNoteStorageImpl : public UserNoteStorage { void DeleteAllNotes() override; private: + void OnNotesChanged(bool notes_changed); + base::ObserverList<Observer>::Unchecked observers_; + // Owns and manages access to the UserNotesDatabase living on a different // sequence. base::SequenceBound<UserNoteDatabase> database_; + + base::WeakPtrFactory<UserNoteStorageImpl> weak_factory_{this}; }; } // namespace user_notes |