Saving and reading home tab icons to proto

Icons for the home tab within the tab strip specified in the manifest are saved (without filtering) through proto. Icons are saved as ImageResource, keeping the same shape it is set as in the manifest.

Change-Id: I2cce28fc171e2fc238cf2bc8fdc4205a2b6a0fd2
Bug: 1381377
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4136334
Reviewed-by: Peter Beverloo <[email protected]>
Reviewed-by: Eric Willigers <[email protected]>
Commit-Queue: Eric Willigers <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1094699}
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index d7dffea..74b4b758 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -387,6 +387,7 @@
     "//components/webapps/common:mojo_bindings",
     "//components/webapps/services/web_app_origin_association:lib",
     "//components/webapps/services/web_app_origin_association:service",
+    "//content/browser/background_fetch:background_fetch_proto",
     "//content/public/browser",
     "//mojo/core/embedder",
     "//mojo/core/embedder:features",
diff --git a/chrome/browser/web_applications/DEPS b/chrome/browser/web_applications/DEPS
index 89357b54..242c13f 100644
--- a/chrome/browser/web_applications/DEPS
+++ b/chrome/browser/web_applications/DEPS
@@ -4,6 +4,7 @@
   "+components/services/storage/indexed_db/locks",
   "+components/web_package",
   "+components/webapps/services/web_app_origin_association",
+  "+content/browser/background_fetch",
   "+mojo/core/embedder",
   "+mojo/public/cpp/bindings",
   "+services/network/public/cpp",
diff --git a/chrome/browser/web_applications/proto/BUILD.gn b/chrome/browser/web_applications/proto/BUILD.gn
index 3fbfadd..88c14676 100644
--- a/chrome/browser/web_applications/proto/BUILD.gn
+++ b/chrome/browser/web_applications/proto/BUILD.gn
@@ -16,6 +16,7 @@
   ]
   link_deps = [
     "//chrome/browser/ash/system_web_apps/types:proto",
+    "//content/browser/background_fetch:background_fetch_proto",
     "//components/sync/protocol",
   ]
 }
diff --git a/chrome/browser/web_applications/proto/web_app_tab_strip.proto b/chrome/browser/web_applications/proto/web_app_tab_strip.proto
index 0d5c5f4e..55d20bf 100644
--- a/chrome/browser/web_applications/proto/web_app_tab_strip.proto
+++ b/chrome/browser/web_applications/proto/web_app_tab_strip.proto
@@ -4,10 +4,16 @@
 
 syntax = "proto2";
 
+import "content/browser/background_fetch/background_fetch.proto";
+
 option optimize_for = LITE_RUNTIME;
 
 package web_app.proto;
 
+message HomeTabParams {
+  repeated content.proto.ImageResource icons = 1;
+}
+
 message NewTabButtonParams {
   optional string url = 1;
 }
@@ -28,6 +34,6 @@
 
   oneof home_tab {
     Visibility home_tab_visibility = 3;
-    // TODO(crbug.com/897314): Add HomeTabParams containing icons.
+    HomeTabParams home_tab_params = 4;
   }
 }
diff --git a/chrome/browser/web_applications/test/web_app_test_utils.cc b/chrome/browser/web_applications/test/web_app_test_utils.cc
index 96f45ee..8eb901f 100644
--- a/chrome/browser/web_applications/test/web_app_test_utils.cc
+++ b/chrome/browser/web_applications/test/web_app_test_utils.cc
@@ -310,6 +310,56 @@
   return results;
 }
 
+std::vector<blink::Manifest::ImageResource> CreateRandomHomeTabIcons(
+    RandomHelper& random) {
+  std::vector<blink::Manifest::ImageResource> icons;
+
+  for (int i = random.next_uint(4) + 1; i >= 0; --i) {
+    blink::Manifest::ImageResource icon;
+
+    int mime_type = random.next_uint(3);
+    switch (mime_type) {
+      case 0:
+        icon.src = GURL("https://example.com/image" + base::NumberToString(i) +
+                        ".png");
+        icon.type = base::UTF8ToUTF16(std::string("image/png"));
+        break;
+      case 1:
+        icon.src = GURL("https://example.com/image" + base::NumberToString(i) +
+                        ".svg");
+        icon.type = base::UTF8ToUTF16(std::string("image/svg+xml"));
+        break;
+      case 2:
+        icon.src = GURL("https://example.com/image" + base::NumberToString(i) +
+                        ".webp");
+        icon.type = base::UTF8ToUTF16(std::string("image/webp"));
+        break;
+    }
+
+    // Icon sizes can be non square
+    std::vector<gfx::Size> sizes;
+    for (int j = random.next_uint(3) + 1; j > 0; --j) {
+      sizes.emplace_back(j * random.next_uint(200), j * random.next_uint(200));
+    }
+    icon.sizes = std::move(sizes);
+
+    std::vector<blink::mojom::ManifestImageResource_Purpose> purposes = {
+        blink::mojom::ManifestImageResource_Purpose::ANY,
+        blink::mojom::ManifestImageResource_Purpose::MASKABLE,
+        blink::mojom::ManifestImageResource_Purpose::MONOCHROME};
+
+    std::vector<blink::mojom::ManifestImageResource_Purpose> purpose;
+
+    for (int j = random.next_uint(purposes.size()); j >= 0; --j) {
+      unsigned index = random.next_uint(purposes.size());
+      purpose.push_back(purposes[index]);
+      purposes.erase(purposes.begin() + index);
+    }
+    icon.purpose = std::move(purpose);
+    icons.push_back(std::move(icon));
+  }
+  return icons;
+}
 }  // namespace
 
 std::string GetExternalPrefMigrationTestName(
@@ -627,8 +677,18 @@
 
   if (random.next_bool()) {
     blink::Manifest::TabStrip tab_strip;
-    tab_strip.home_tab =
-        random.next_enum<blink::mojom::TabStripMemberVisibility>();
+
+    if (random.next_bool()) {
+      blink::Manifest::HomeTabParams home_tab_params;
+      if (random.next_bool()) {
+        home_tab_params.icons = CreateRandomHomeTabIcons(random);
+      }
+      tab_strip.home_tab = std::move(home_tab_params);
+    } else {
+      tab_strip.home_tab =
+          random.next_enum<blink::mojom::TabStripMemberVisibility>();
+    }
+
     if (random.next_bool()) {
       blink::Manifest::NewTabButtonParams new_tab_button_params;
       if (random.next_bool()) {
diff --git a/chrome/browser/web_applications/web_app.cc b/chrome/browser/web_applications/web_app.cc
index 5505f8a..10da49f 100644
--- a/chrome/browser/web_applications/web_app.cc
+++ b/chrome/browser/web_applications/web_app.cc
@@ -155,6 +155,30 @@
   return base::Value(std::move(debug_dict));
 }
 
+base::Value ImageResourceDebugValue(
+    const blink::Manifest::ImageResource& icon) {
+  const char* const kPurposeStrings[] = {"Any", "Monochrome", "Maskable"};
+
+  base::Value root(base::Value::Type::DICT);
+  root.SetStringKey("src", icon.src.spec());
+  root.SetStringKey("type", icon.type);
+
+  base::Value sizes_json(base::Value::Type::LIST);
+  for (const auto& size : icon.sizes) {
+    std::string size_formatted = base::NumberToString(size.width()) + "x" +
+                                 base::NumberToString(size.height());
+    sizes_json.Append(base::Value(size_formatted));
+  }
+  root.SetKey("sizes", std::move(sizes_json));
+
+  base::Value purpose_json(base::Value::Type::LIST);
+  for (const auto& purpose : icon.purpose) {
+    purpose_json.Append(kPurposeStrings[static_cast<int>(purpose)]);
+  }
+  root.SetKey("purpose", std::move(purpose_json));
+  return root;
+}
+
 }  // namespace
 
 WebApp::WebApp(const AppId& app_id)
@@ -956,8 +980,18 @@
           "home_tab", base::StreamableToString(absl::get<TabStrip::Visibility>(
                           tab_strip_.value().home_tab)));
     } else {
-      tab_strip_json.Set("home_tab", base::Value::Dict());
-      // TODO(crbug.com/897314): Add debug info for home tab icons.
+      base::Value::Dict home_tab_json;
+      base::Value icons_json(base::Value::Type::LIST);
+      absl::optional<std::vector<blink::Manifest::ImageResource>> icons =
+          absl::get<blink::Manifest::HomeTabParams>(tab_strip_.value().home_tab)
+              .icons;
+
+      for (auto& icon : *icons) {
+        icons_json.Append(ImageResourceDebugValue(icon));
+      }
+
+      home_tab_json.Set("icons", std::move(icons_json));
+      tab_strip_json.Set("home_tab", std::move(home_tab_json));
     }
     root.Set("tab_strip", std::move(tab_strip_json));
   } else {
diff --git a/chrome/browser/web_applications/web_app_database.cc b/chrome/browser/web_applications/web_app_database.cc
index 9d816aa..48b78bb 100644
--- a/chrome/browser/web_applications/web_app_database.cc
+++ b/chrome/browser/web_applications/web_app_database.cc
@@ -738,6 +738,15 @@
     if (absl::holds_alternative<TabStrip::Visibility>(tab_strip.home_tab)) {
       mutable_tab_strip->set_home_tab_visibility(TabStripVisibilityToProto(
           absl::get<TabStrip::Visibility>(tab_strip.home_tab)));
+    } else {
+      auto* mutable_home_tab_params =
+          mutable_tab_strip->mutable_home_tab_params();
+      absl::optional<std::vector<blink::Manifest::ImageResource>> icons =
+          absl::get<blink::Manifest::HomeTabParams>(tab_strip.home_tab).icons;
+      for (const auto& image_resource : *icons) {
+        *(mutable_home_tab_params->add_icons()) =
+            AppImageResourceToProto(image_resource);
+      }
     }
 
     if (absl::holds_alternative<TabStrip::Visibility>(
@@ -1375,7 +1384,14 @@
       tab_strip.home_tab = ProtoToTabStripVisibility(
           local_data.tab_strip().home_tab_visibility());
     } else {
-      tab_strip.home_tab = blink::Manifest::HomeTabParams();
+      absl::optional<std::vector<blink::Manifest::ImageResource>> icons =
+          ParseAppImageResource(
+              "WebApp", local_data.tab_strip().home_tab_params().icons());
+      blink::Manifest::HomeTabParams home_tab_params;
+      if (!icons->empty()) {
+        home_tab_params.icons = std::move(*icons);
+      }
+      tab_strip.home_tab = std::move(home_tab_params);
     }
 
     if (local_data.tab_strip().has_new_tab_button_visibility()) {
diff --git a/chrome/browser/web_applications/web_app_database_unittest.cc b/chrome/browser/web_applications/web_app_database_unittest.cc
index 02a295f..21d406ad 100644
--- a/chrome/browser/web_applications/web_app_database_unittest.cc
+++ b/chrome/browser/web_applications/web_app_database_unittest.cc
@@ -458,6 +458,7 @@
   EXPECT_FALSE(app_copy->manifest_id().has_value());
   EXPECT_FALSE(app_copy->IsStorageIsolated());
   EXPECT_TRUE(app_copy->permissions_policy().empty());
+  EXPECT_FALSE(app_copy->tab_strip());
 }
 
 TEST_F(WebAppDatabaseTest, WebAppWithManyIcons) {
diff --git a/chrome/browser/web_applications/web_app_proto_utils.cc b/chrome/browser/web_applications/web_app_proto_utils.cc
index 6bb92a2..24a1feb 100644
--- a/chrome/browser/web_applications/web_app_proto_utils.cc
+++ b/chrome/browser/web_applications/web_app_proto_utils.cc
@@ -3,10 +3,13 @@
 // found in the LICENSE file.
 
 #include "chrome/browser/web_applications/web_app_proto_utils.h"
+#include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
 #include "chrome/browser/web_applications/user_display_mode.h"
 #include "components/services/app_service/public/cpp/icon_info.h"
+#include "third_party/blink/public/common/manifest/manifest.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
+#include "ui/gfx/geometry/size.h"
 
 namespace web_app {
 
@@ -40,6 +43,19 @@
   }
 }
 
+content::proto::ImageResource_Purpose
+ManifestImageResourcePurposeToImageResoucePurposeProto(
+    blink::mojom::ManifestImageResource_Purpose purpose) {
+  switch (purpose) {
+    case blink::mojom::ManifestImageResource_Purpose::ANY:
+      return content::proto::ImageResource_Purpose_ANY;
+    case blink::mojom::ManifestImageResource_Purpose::MONOCHROME:
+      return content::proto::ImageResource_Purpose_MONOCHROME;
+    case blink::mojom::ManifestImageResource_Purpose::MASKABLE:
+      return content::proto::ImageResource_Purpose_MASKABLE;
+  }
+}
+
 }  // namespace
 
 absl::optional<std::vector<apps::IconInfo>> ParseAppIconInfos(
@@ -80,6 +96,67 @@
   return manifest_icons;
 }
 
+absl::optional<std::vector<blink::Manifest::ImageResource>>
+ParseAppImageResource(const char* container_name_for_logging,
+                      const RepeatedImageResourceProto& manifest_icons_proto) {
+  std::vector<blink::Manifest::ImageResource> manifest_icons;
+  for (const content::proto::ImageResource& image_resource_proto :
+       manifest_icons_proto) {
+    blink::Manifest::ImageResource image_resource;
+
+    if (!image_resource_proto.has_src()) {
+      DLOG(ERROR) << container_name_for_logging
+                  << " ImageResource has missing url";
+      return absl::nullopt;
+    }
+    image_resource.src = GURL(image_resource_proto.src());
+
+    if (!image_resource.src.is_valid()) {
+      DLOG(ERROR) << container_name_for_logging
+                  << " ImageResource has invalid url: "
+                  << image_resource.src.possibly_invalid_spec();
+      return absl::nullopt;
+    }
+
+    if (image_resource_proto.has_type()) {
+      image_resource.type = base::ASCIIToUTF16(image_resource_proto.type());
+    }
+
+    if (!image_resource_proto.sizes().empty()) {
+      std::vector<gfx::Size> sizes;
+      for (const auto& size_proto : image_resource_proto.sizes()) {
+        sizes.emplace_back(size_proto.width(), size_proto.height());
+      }
+      image_resource.sizes = std::move(sizes);
+    }
+
+    std::vector<blink::mojom::ManifestImageResource_Purpose> purpose;
+    if (!image_resource_proto.purpose().empty()) {
+      for (const auto& purpose_proto : image_resource_proto.purpose()) {
+        switch (purpose_proto) {
+          case content::proto::ImageResource_Purpose_ANY:
+            purpose.push_back(blink::mojom::ManifestImageResource_Purpose::ANY);
+            break;
+          case content::proto::ImageResource_Purpose_MASKABLE:
+            purpose.push_back(
+                blink::mojom::ManifestImageResource_Purpose::MASKABLE);
+            break;
+          case content::proto::ImageResource_Purpose_MONOCHROME:
+            purpose.push_back(
+                blink::mojom::ManifestImageResource_Purpose::MONOCHROME);
+            break;
+        }
+      }
+    } else {
+      purpose.push_back(blink::mojom::ManifestImageResource_Purpose::ANY);
+    }
+    image_resource.purpose = std::move(purpose);
+    manifest_icons.push_back(std::move(image_resource));
+  }
+
+  return manifest_icons;
+}
+
 sync_pb::WebAppSpecifics WebAppToSyncProto(const WebApp& app) {
   DCHECK(!app.start_url().is_empty());
   DCHECK(app.start_url().is_valid());
@@ -120,6 +197,31 @@
   return icon_info_proto;
 }
 
+content::proto::ImageResource AppImageResourceToProto(
+    const blink::Manifest::ImageResource& image_resource) {
+  content::proto::ImageResource image_resource_proto;
+  DCHECK(!image_resource.src.is_empty());
+
+  image_resource_proto.set_src(image_resource.src.spec());
+
+  if (!image_resource.type.empty()) {
+    image_resource_proto.set_type(base::UTF16ToASCII(image_resource.type));
+  }
+
+  for (const auto& size : image_resource.sizes) {
+    content::proto::ImageResource::Size size_proto;
+    size_proto.set_width(size.width());
+    size_proto.set_height(size.height());
+    *(image_resource_proto.add_sizes()) = size_proto;
+  }
+
+  for (const auto& purpose : image_resource.purpose) {
+    image_resource_proto.add_purpose(
+        ManifestImageResourcePurposeToImageResoucePurposeProto(purpose));
+  }
+  return image_resource_proto;
+}
+
 absl::optional<WebApp::SyncFallbackData> ParseSyncFallbackDataStruct(
     const sync_pb::WebAppSpecifics& sync_proto) {
   WebApp::SyncFallbackData parsed_sync_fallback_data;
diff --git a/chrome/browser/web_applications/web_app_proto_utils.h b/chrome/browser/web_applications/web_app_proto_utils.h
index 1a480b22..a07227e 100644
--- a/chrome/browser/web_applications/web_app_proto_utils.h
+++ b/chrome/browser/web_applications/web_app_proto_utils.h
@@ -10,6 +10,7 @@
 #include "chrome/browser/web_applications/proto/web_app.pb.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "components/sync/protocol/web_app_specifics.pb.h"
+#include "content/browser/background_fetch/background_fetch.pb.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace apps {
@@ -23,16 +24,27 @@
 using RepeatedIconInfosProto =
     const ::google::protobuf::RepeatedPtrField<::sync_pb::WebAppIconInfo>;
 
+using RepeatedImageResourceProto =
+    const ::google::protobuf::RepeatedPtrField<content::proto::ImageResource>;
+
 absl::optional<std::vector<apps::IconInfo>> ParseAppIconInfos(
     const char* container_name_for_logging,
     const RepeatedIconInfosProto& manifest_icons_proto);
 
+absl::optional<std::vector<blink::Manifest::ImageResource>>
+ParseAppImageResource(const char* container_name_for_logging,
+                      const RepeatedImageResourceProto& manifest_icons_proto);
+
 // Use the given |app| to populate a |WebAppSpecifics| sync proto.
 sync_pb::WebAppSpecifics WebAppToSyncProto(const WebApp& app);
 
 // Use the given |icon_info| to populate a |WebAppIconInfo| sync proto.
 sync_pb::WebAppIconInfo AppIconInfoToSyncProto(const apps::IconInfo& icon_info);
 
+// Use the given |image_resource| to populate a |ImageResource| proto.
+content::proto::ImageResource AppImageResourceToProto(
+    const blink::Manifest::ImageResource& image_resource);
+
 absl::optional<WebApp::SyncFallbackData> ParseSyncFallbackDataStruct(
     const sync_pb::WebAppSpecifics& sync_proto);