[Shortcut] Create shortcut updater.

This CL creates the shortcut updater to handle updates of shortcuts.

BUG=1412708

Change-Id: I8c04e58d28f7400997ce4654a0952d7bc678879e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4556245
Reviewed-by: Tim Sergeant <tsergeant@chromium.org>
Auto-Submit: Maggie Cai <mxcai@chromium.org>
Commit-Queue: Tim Sergeant <tsergeant@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1149445}
diff --git a/components/services/app_service/public/cpp/shortcut/BUILD.gn b/components/services/app_service/public/cpp/shortcut/BUILD.gn
index dd77ffc..7f05d2e 100644
--- a/components/services/app_service/public/cpp/shortcut/BUILD.gn
+++ b/components/services/app_service/public/cpp/shortcut/BUILD.gn
@@ -11,6 +11,8 @@
     "shortcut.h",
     "shortcut_registry_cache.cc",
     "shortcut_registry_cache.h",
+    "shortcut_update.cc",
+    "shortcut_update.h",
   ]
 
   defines = [ "IS_SHORTCUT_IMPL" ]
@@ -19,6 +21,7 @@
     "//base",
     "//components/crx_file",
     "//components/services/app_service/public/cpp:macros",
+    "//third_party/abseil-cpp:absl",
   ]
 }
 
@@ -28,6 +31,7 @@
   sources = [
     "shortcut_registry_cache_unittest.cc",
     "shortcut_unittest.cc",
+    "shortcut_update_unittest.cc",
   ]
 
   deps = [
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut.cc b/components/services/app_service/public/cpp/shortcut/shortcut.cc
index f139eab..61cb91f 100644
--- a/components/services/app_service/public/cpp/shortcut/shortcut.cc
+++ b/components/services/app_service/public/cpp/shortcut/shortcut.cc
@@ -34,7 +34,9 @@
 std::string Shortcut::ToString() const {
   std::stringstream out;
   out << "shortcut_id: " << shortcut_id << std::endl;
-  out << "- name: " << name << std::endl;
+  if (name.has_value()) {
+    out << "- name: " << name.value() << std::endl;
+  }
   out << "- shortcut_source: " << EnumToString(shortcut_source) << std::endl;
   out << "- host_app_id: " << host_app_id << std::endl;
   out << "- local_id: " << local_id << std::endl;
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut.h b/components/services/app_service/public/cpp/shortcut/shortcut.h
index 5622d593..3020da43 100644
--- a/components/services/app_service/public/cpp/shortcut/shortcut.h
+++ b/components/services/app_service/public/cpp/shortcut/shortcut.h
@@ -13,6 +13,7 @@
 #include "base/component_export.h"
 #include "base/types/strong_alias.h"
 #include "components/services/app_service/public/cpp/macros.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace apps {
 
@@ -47,7 +48,7 @@
   // - local_id: shortcut_1
   std::string ToString() const;
   // Name of the shortcut.
-  std::string name;
+  absl::optional<std::string> name;
   // Shortcut creation source.
   ShortcutSource shortcut_source;
 
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache.cc b/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache.cc
index b6789d44..c03d77c 100644
--- a/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache.cc
+++ b/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache.cc
@@ -9,6 +9,7 @@
 
 #include "base/check.h"
 #include "base/containers/contains.h"
+#include "components/services/app_service/public/cpp/shortcut/shortcut_update.h"
 
 namespace apps {
 
@@ -23,8 +24,11 @@
   is_updating_ = true;
   const ShortcutId shortcut_id = delta->shortcut_id;
 
-  states_.emplace(shortcut_id, delta->Clone());
-  // TODO(crbug.com/1412708): Handle delta merge.
+  if (HasShortcut(shortcut_id)) {
+    ShortcutUpdate::Merge(states_[shortcut_id].get(), delta.get());
+  } else {
+    states_.emplace(shortcut_id, delta->Clone());
+  }
   // TODO(crbug.com/1412708): Update observer.
   is_updating_ = false;
 }
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache_unittest.cc b/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache_unittest.cc
index 34fd67cdf..e192fec 100644
--- a/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache_unittest.cc
+++ b/components/services/app_service/public/cpp/shortcut/shortcut_registry_cache_unittest.cc
@@ -44,4 +44,35 @@
   EXPECT_EQ(cache().GetAllShortcuts().size(), 1u);
 }
 
+TEST_F(ShortcutRegistryCacheTest, UpdateShortcut) {
+  std::string host_app_id = "host_app_id";
+  std::string local_id = "local_id";
+  auto shortcut = std::make_unique<Shortcut>(host_app_id, local_id);
+  ShortcutId shortcut_id = shortcut->shortcut_id;
+  shortcut->name = "name";
+  shortcut->shortcut_source = ShortcutSource::kUser;
+
+  EXPECT_FALSE(cache().HasShortcut(shortcut_id));
+  cache().UpdateShortcut(std::move(shortcut));
+  ASSERT_TRUE(cache().HasShortcut(shortcut_id));
+
+  EXPECT_EQ(cache().GetAllShortcuts().size(), 1u);
+
+  auto shortcut_delta = std::make_unique<Shortcut>(host_app_id, local_id);
+  shortcut_delta->name = "new name";
+  shortcut_delta->shortcut_source = ShortcutSource::kDeveloper;
+
+  cache().UpdateShortcut(std::move(shortcut_delta));
+
+  EXPECT_EQ(cache().GetAllShortcuts().size(), 1u);
+
+  ShortcutView stored_shortcut = cache().GetShortcut(shortcut_id);
+
+  ASSERT_TRUE(stored_shortcut);
+  EXPECT_EQ(stored_shortcut->shortcut_id, shortcut_id);
+  EXPECT_EQ(stored_shortcut->name, "new name");
+  EXPECT_EQ(stored_shortcut->shortcut_source, ShortcutSource::kDeveloper);
+  EXPECT_EQ(stored_shortcut->host_app_id, host_app_id);
+  EXPECT_EQ(stored_shortcut->local_id, local_id);
+}
 }  // namespace apps
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut_update.cc b/components/services/app_service/public/cpp/shortcut/shortcut_update.cc
new file mode 100644
index 0000000..7f9a6ad
--- /dev/null
+++ b/components/services/app_service/public/cpp/shortcut/shortcut_update.cc
@@ -0,0 +1,86 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/services/app_service/public/cpp/shortcut/shortcut_update.h"
+
+#include "base/check.h"
+#include "base/check_op.h"
+#include "base/logging.h"
+#include "base/strings/string_util.h"
+#include "components/services/app_service/public/cpp/macros.h"
+#include "components/services/app_service/public/cpp/shortcut/shortcut.h"
+
+namespace apps {
+
+ShortcutUpdate::ShortcutUpdate(const Shortcut* state, const Shortcut* delta)
+    : state_(state), delta_(delta) {
+  CHECK(state_ || delta_);
+  if (state_ && delta_) {
+    CHECK_EQ(state_->shortcut_id, delta->shortcut_id);
+    CHECK_EQ(state_->host_app_id, delta->host_app_id);
+    CHECK_EQ(state_->local_id, delta->local_id);
+  }
+}
+
+void ShortcutUpdate::Merge(Shortcut* state, const Shortcut* delta) {
+  CHECK(state);
+  if (!delta) {
+    return;
+  }
+
+  if (delta->shortcut_id != state->shortcut_id) {
+    LOG(ERROR) << "inconsistent shortcut_id: " << delta->shortcut_id << " vs "
+               << state->shortcut_id;
+    return;
+  }
+
+  if (delta->host_app_id != state->host_app_id) {
+    LOG(ERROR) << "inconsistent host_app_id: " << delta->host_app_id << " vs "
+               << state->host_app_id;
+    return;
+  }
+
+  if (delta->local_id != state->local_id) {
+    LOG(ERROR) << "inconsistent local_id: " << delta->local_id << " vs "
+               << state->local_id;
+    return;
+  }
+
+  SET_OPTIONAL_VALUE(name);
+  SET_ENUM_VALUE(shortcut_source, ShortcutSource::kUnknown);
+
+  // When adding new fields to the Shortcut struct, this function should also
+  // be updated.
+}
+
+const ShortcutId& ShortcutUpdate::ShortcutId() const {
+  return delta_ ? delta_->shortcut_id : state_->shortcut_id;
+}
+
+const std::string& ShortcutUpdate::HostAppId() const {
+  return delta_ ? delta_->host_app_id : state_->host_app_id;
+}
+
+const std::string& ShortcutUpdate::LocalId() const {
+  return delta_ ? delta_->local_id : state_->local_id;
+}
+
+const std::string& ShortcutUpdate::Name() const {
+  GET_VALUE_WITH_FALLBACK(name, base::EmptyString())
+}
+
+bool ShortcutUpdate::NameChanged() const {
+  RETURN_OPTIONAL_VALUE_CHANGED(name);
+}
+
+ShortcutSource ShortcutUpdate::ShortcutSource() const {
+  GET_VALUE_WITH_DEFAULT_VALUE(shortcut_source, ShortcutSource::kUnknown);
+}
+
+bool ShortcutUpdate::ShortcutSourceChanged() const {
+  IS_VALUE_CHANGED_WITH_DEFAULT_VALUE(shortcut_source,
+                                      ShortcutSource::kUnknown);
+}
+
+}  // namespace apps
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut_update.h b/components/services/app_service/public/cpp/shortcut/shortcut_update.h
new file mode 100644
index 0000000..4007607
--- /dev/null
+++ b/components/services/app_service/public/cpp/shortcut/shortcut_update.h
@@ -0,0 +1,51 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_SHORTCUT_SHORTCUT_UPDATE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_SHORTCUT_SHORTCUT_UPDATE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/allocator/partition_allocator/pointers/raw_ptr.h"
+#include "base/component_export.h"
+#include "components/services/app_service/public/cpp/macros.h"
+#include "components/services/app_service/public/cpp/shortcut/shortcut.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace apps {
+
+class COMPONENT_EXPORT(SHORTCUT) ShortcutUpdate {
+ public:
+  static void Merge(Shortcut* state, const Shortcut* delta);
+
+  // At most one of |state| or |delta| may be nullptr.
+  ShortcutUpdate(const Shortcut* state, const Shortcut* delta);
+
+  ShortcutUpdate(const ShortcutUpdate&) = delete;
+  ShortcutUpdate& operator=(const ShortcutUpdate&) = delete;
+
+  const ShortcutId& ShortcutId() const;
+  const std::string& HostAppId() const;
+  const std::string& LocalId() const;
+
+  const std::string& Name() const;
+  bool NameChanged() const;
+
+  ShortcutSource ShortcutSource() const;
+  bool ShortcutSourceChanged() const;
+
+ private:
+  raw_ptr<const Shortcut> state_ = nullptr;
+  raw_ptr<const Shortcut> delta_ = nullptr;
+};
+
+// For logging and debug purposes.
+COMPONENT_EXPORT(SHORTCUT)
+std::ostream& operator<<(std::ostream& out,
+                         const ShortcutUpdate& shortcut_update);
+
+}  // namespace apps
+
+#endif  // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_SHORTCUT_SHORTCUT_UPDATE_H_
diff --git a/components/services/app_service/public/cpp/shortcut/shortcut_update_unittest.cc b/components/services/app_service/public/cpp/shortcut/shortcut_update_unittest.cc
new file mode 100644
index 0000000..64e853f1
--- /dev/null
+++ b/components/services/app_service/public/cpp/shortcut/shortcut_update_unittest.cc
@@ -0,0 +1,94 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/services/app_service/public/cpp/shortcut/shortcut_update.h"
+
+#include "components/services/app_service/public/cpp/shortcut/shortcut.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace apps {
+
+class ShortcutUpdateTest : public testing::Test {
+ protected:
+  std::string host_app_id_ = "host_app_id";
+  std::string local_id_ = "local_id";
+  ShortcutId shortcut_id_ = GenerateShortcutId(host_app_id_, local_id_);
+};
+
+TEST_F(ShortcutUpdateTest, StateIsNonNull) {
+  Shortcut shortcut = Shortcut(host_app_id_, local_id_);
+  shortcut.name = "Name";
+  shortcut.shortcut_source = ShortcutSource::kDeveloper;
+  ShortcutUpdate u(&shortcut, nullptr);
+
+  EXPECT_EQ(u.HostAppId(), host_app_id_);
+  EXPECT_EQ(u.LocalId(), local_id_);
+  EXPECT_EQ(u.ShortcutId(), shortcut_id_);
+
+  EXPECT_EQ(u.Name(), "Name");
+  EXPECT_EQ(u.NameChanged(), false);
+
+  EXPECT_EQ(u.ShortcutSource(), ShortcutSource::kDeveloper);
+  EXPECT_EQ(u.ShortcutSourceChanged(), false);
+}
+
+TEST_F(ShortcutUpdateTest, DeltaIsNonNull) {
+  Shortcut shortcut = Shortcut(host_app_id_, local_id_);
+  shortcut.name = "Name";
+  shortcut.shortcut_source = ShortcutSource::kDeveloper;
+  ShortcutUpdate u(nullptr, &shortcut);
+
+  EXPECT_EQ(u.HostAppId(), host_app_id_);
+  EXPECT_EQ(u.LocalId(), local_id_);
+  EXPECT_EQ(u.ShortcutId(), shortcut_id_);
+
+  EXPECT_EQ(u.Name(), "Name");
+  EXPECT_EQ(u.NameChanged(), true);
+
+  EXPECT_EQ(u.ShortcutSource(), ShortcutSource::kDeveloper);
+  EXPECT_EQ(u.ShortcutSourceChanged(), true);
+}
+
+TEST_F(ShortcutUpdateTest, StateAndDeltaAreNonNull) {
+  Shortcut shortcut_state = Shortcut(host_app_id_, local_id_);
+  shortcut_state.name = "Name";
+  shortcut_state.shortcut_source = ShortcutSource::kDeveloper;
+
+  Shortcut shortcut_delta = Shortcut(host_app_id_, local_id_);
+  shortcut_delta.name = "New name";
+  shortcut_delta.shortcut_source = ShortcutSource::kUser;
+
+  ShortcutUpdate u(&shortcut_state, &shortcut_delta);
+
+  EXPECT_EQ(u.HostAppId(), host_app_id_);
+  EXPECT_EQ(u.LocalId(), local_id_);
+  EXPECT_EQ(u.ShortcutId(), shortcut_id_);
+
+  EXPECT_EQ(u.Name(), "New name");
+  EXPECT_EQ(u.NameChanged(), true);
+
+  EXPECT_EQ(u.ShortcutSource(), ShortcutSource::kUser);
+  EXPECT_EQ(u.ShortcutSourceChanged(), true);
+}
+
+TEST_F(ShortcutUpdateTest, Merge) {
+  Shortcut shortcut_state = Shortcut(host_app_id_, local_id_);
+  shortcut_state.name = "Name";
+  shortcut_state.shortcut_source = ShortcutSource::kDeveloper;
+
+  Shortcut shortcut_delta = Shortcut(host_app_id_, local_id_);
+  shortcut_delta.name = "New name";
+  shortcut_delta.shortcut_source = ShortcutSource::kUser;
+
+  ShortcutUpdate::Merge(&shortcut_state, &shortcut_delta);
+
+  EXPECT_EQ(shortcut_state.shortcut_id,
+            GenerateShortcutId(host_app_id_, local_id_));
+  EXPECT_EQ(shortcut_state.name, "New name");
+  EXPECT_EQ(shortcut_state.shortcut_source, ShortcutSource::kUser);
+  EXPECT_EQ(shortcut_state.host_app_id, host_app_id_);
+  EXPECT_EQ(shortcut_state.local_id, local_id_);
+}
+
+}  // namespace apps