summaryrefslogtreecommitdiff
path: root/chromium/components/user_notes
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2022-09-29 16:16:15 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2022-11-09 10:04:06 +0000
commita95a7417ad456115a1ef2da4bb8320531c0821f1 (patch)
treeedcd59279e486d2fd4a8f88a7ed025bcf925c6e6 /chromium/components/user_notes
parent33fc33aa94d4add0878ec30dc818e34e1dd3cc2a (diff)
downloadqtwebengine-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')
-rw-r--r--chromium/components/user_notes/browser/frame_user_note_changes.cc15
-rw-r--r--chromium/components/user_notes/browser/frame_user_note_changes.h12
-rw-r--r--chromium/components/user_notes/browser/frame_user_note_changes_unittest.cc3
-rw-r--r--chromium/components/user_notes/browser/user_note_base_test.cc37
-rw-r--r--chromium/components/user_notes/browser/user_note_base_test.h64
-rw-r--r--chromium/components/user_notes/browser/user_note_instance.cc104
-rw-r--r--chromium/components/user_notes/browser/user_note_instance.h60
-rw-r--r--chromium/components/user_notes/browser/user_note_instance_unittest.cc10
-rw-r--r--chromium/components/user_notes/browser/user_note_manager.cc13
-rw-r--r--chromium/components/user_notes/browser/user_note_manager.h16
-rw-r--r--chromium/components/user_notes/browser/user_note_service.cc425
-rw-r--r--chromium/components/user_notes/browser/user_note_service.h90
-rw-r--r--chromium/components/user_notes/browser/user_note_service_unittest.cc1049
-rw-r--r--chromium/components/user_notes/browser/user_note_utils.cc34
-rw-r--r--chromium/components/user_notes/browser/user_note_utils.h6
-rw-r--r--chromium/components/user_notes/browser/user_note_utils_unittest.cc33
-rw-r--r--chromium/components/user_notes/interfaces/BUILD.gn1
-rw-r--r--chromium/components/user_notes/interfaces/user_note_metadata_snapshot.cc4
-rw-r--r--chromium/components/user_notes/interfaces/user_note_metadata_snapshot.h8
-rw-r--r--chromium/components/user_notes/interfaces/user_note_service_delegate.h4
-rw-r--r--chromium/components/user_notes/interfaces/user_note_storage.h19
-rw-r--r--chromium/components/user_notes/interfaces/user_notes_ui.cc13
-rw-r--r--chromium/components/user_notes/interfaces/user_notes_ui.h15
-rw-r--r--chromium/components/user_notes/interfaces/user_notes_ui_delegate.h17
-rw-r--r--chromium/components/user_notes/model/user_note.cc7
-rw-r--r--chromium/components/user_notes/model/user_note.h3
-rw-r--r--chromium/components/user_notes/model/user_note_body.cc2
-rw-r--r--chromium/components/user_notes/model/user_note_body.h6
-rw-r--r--chromium/components/user_notes/model/user_note_model_test_utils.cc4
-rw-r--r--chromium/components/user_notes/model/user_note_target.cc2
-rw-r--r--chromium/components/user_notes/model/user_note_target.h6
-rw-r--r--chromium/components/user_notes/storage/user_note_database.cc336
-rw-r--r--chromium/components/user_notes/storage/user_note_database.h31
-rw-r--r--chromium/components/user_notes/storage/user_note_database_unittest.cc272
-rw-r--r--chromium/components/user_notes/storage/user_note_storage_impl.cc56
-rw-r--r--chromium/components/user_notes/storage/user_note_storage_impl.h17
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