Adds service to get metadata about images to display on NTP background.

Creates a service that uses SimpleURLLoader to asynchronously request
metadata about images from a specified collection, and makes them
available to the Local NTP in a javascript variable.

Bug: 839152
Change-Id: Ifc2d14bd451f02c42542b3cd5a50bef5c50eb0d4
Reviewed-on: https://chromium-review.googlesource.com/1074339
Commit-Queue: Ramya Nagarajan <[email protected]>
Reviewed-by: Ilya Sherman <[email protected]>
Reviewed-by: Marc Treib <[email protected]>
Reviewed-by: Christian Dullweber <[email protected]>
Cr-Commit-Position: refs/heads/master@{#562736}
diff --git a/chrome/browser/search/background/ntp_background.proto b/chrome/browser/search/background/ntp_background.proto
index 1821c1a9..9d30eca 100644
--- a/chrome/browser/search/background/ntp_background.proto
+++ b/chrome/browser/search/background/ntp_background.proto
@@ -19,7 +19,7 @@
 }
 
 message Image {
-  // A unique ID for the image. Useful for analytics.
+  // A unique ID for the image.
   optional fixed64 asset_id = 1;
 
   // The image URL.
@@ -62,3 +62,23 @@
   // A list of every available collection for the given language and region.
   repeated Collection collections = 1;
 }
+
+message GetImagesInCollectionRequest {
+  // Deprecated or unused tag numbers
+  reserved 4;
+
+  // The id of the collection being requested.
+  optional string collection_id = 1;
+
+  // The language that should be used for content. e.g. "en-US"
+  optional string language = 2;
+
+  // The approximate permanent location of the user e.g. "us".
+  optional string region = 3;
+}
+
+message GetImagesInCollectionResponse {
+  // A list of all the images in the requested collection, filtered by language
+  // and region.
+  repeated Image images = 1;
+}
diff --git a/chrome/browser/search/background/ntp_background_data.cc b/chrome/browser/search/background/ntp_background_data.cc
index 728ae9f..57ca655 100644
--- a/chrome/browser/search/background/ntp_background_data.cc
+++ b/chrome/browser/search/background/ntp_background_data.cc
@@ -34,3 +34,35 @@
 
   return collection_info;
 }
+
+CollectionImage::CollectionImage() = default;
+CollectionImage::CollectionImage(const CollectionImage&) = default;
+CollectionImage::CollectionImage(CollectionImage&&) = default;
+CollectionImage::~CollectionImage() = default;
+
+CollectionImage& CollectionImage::operator=(const CollectionImage&) = default;
+CollectionImage& CollectionImage::operator=(CollectionImage&&) = default;
+
+bool operator==(const CollectionImage& lhs, const CollectionImage& rhs) {
+  return lhs.collection_id == rhs.collection_id &&
+         lhs.asset_id == rhs.asset_id && lhs.image_url == rhs.image_url &&
+         lhs.attribution == rhs.attribution;
+}
+
+bool operator!=(const CollectionImage& lhs, const CollectionImage& rhs) {
+  return !(lhs == rhs);
+}
+
+CollectionImage CollectionImage::CreateFromProto(
+    std::string collection_id,
+    const ntp::background::Image& image) {
+  CollectionImage collection_image;
+  collection_image.collection_id = collection_id;
+  collection_image.asset_id = image.asset_id();
+  collection_image.image_url = GURL(image.image_url());
+  for (const auto& attribution : image.attribution()) {
+    collection_image.attribution.push_back(attribution.text());
+  }
+
+  return collection_image;
+}
diff --git a/chrome/browser/search/background/ntp_background_data.h b/chrome/browser/search/background/ntp_background_data.h
index 9e0e543f..a0bfd66 100644
--- a/chrome/browser/search/background/ntp_background_data.h
+++ b/chrome/browser/search/background/ntp_background_data.h
@@ -12,8 +12,8 @@
 
 // Background images are organized into collections, according to a theme. This
 // struct contains the data required to display information about a collection,
-// including a representative image. The complete set of images must be
-// requested separately, by referencing the identifier for this collection.
+// including a representative image. The complete set of CollectionImages must
+// be requested separately, by referencing the identifier for this collection.
 struct CollectionInfo {
   CollectionInfo();
   CollectionInfo(const CollectionInfo&);
@@ -37,4 +37,31 @@
 bool operator==(const CollectionInfo& lhs, const CollectionInfo& rhs);
 bool operator!=(const CollectionInfo& lhs, const CollectionInfo& rhs);
 
+// Represents an image within a collection. The associated collection_id may be
+// used to get CollectionInfo.
+struct CollectionImage {
+  CollectionImage();
+  CollectionImage(const CollectionImage&);
+  CollectionImage(CollectionImage&&);
+  ~CollectionImage();
+
+  CollectionImage& operator=(const CollectionImage&);
+  CollectionImage& operator=(CollectionImage&&);
+
+  static CollectionImage CreateFromProto(std::string collection_id,
+                                         const ntp::background::Image& image);
+
+  // A unique identifier for the collection the image is in.
+  std::string collection_id;
+  // A unique identifier for the image.
+  uint64_t asset_id;
+  // The image URL.
+  GURL image_url;
+  // The attribution list for the image.
+  std::vector<std::string> attribution;
+};
+
+bool operator==(const CollectionImage& lhs, const CollectionImage& rhs);
+bool operator!=(const CollectionImage& lhs, const CollectionImage& rhs);
+
 #endif  // CHROME_BROWSER_SEARCH_BACKGROUND_NTP_BACKGROUND_DATA_H_
diff --git a/chrome/browser/search/background/ntp_background_service.cc b/chrome/browser/search/background/ntp_background_service.cc
index 844cd478..b9609ef 100644
--- a/chrome/browser/search/background/ntp_background_service.cc
+++ b/chrome/browser/search/background/ntp_background_service.cc
@@ -19,24 +19,31 @@
 constexpr char kProtoMimeType[] = "application/x-protobuf";
 
 // The url to download the proto of the complete list of wallpaper collections.
-constexpr char kBackdropCollectionsUrl[] =
+constexpr char kCollectionsUrl[] =
     "https://clients3.google.com/cast/chromecast/home/wallpaper/"
     "collections?rt=b";
-
+// The url to download the metadata of the images in a collection.
+constexpr char kCollectionImagesUrl[] =
+    "https://clients3.google.com/cast/chromecast/home/wallpaper/"
+    "collection-images?rt=b";
 }  // namespace
 
 NtpBackgroundService::NtpBackgroundService(
     scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
-    const base::Optional<GURL>& api_url_override)
+    const base::Optional<GURL>& collections_api_url_override,
+    const base::Optional<GURL>& collection_images_api_url_override)
     : url_loader_factory_(url_loader_factory) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  api_url_ = api_url_override.value_or(GURL(kBackdropCollectionsUrl));
+  collections_api_url_ =
+      collections_api_url_override.value_or(GURL(kCollectionsUrl));
+  collection_images_api_url_ =
+      collection_images_api_url_override.value_or(GURL(kCollectionImagesUrl));
 }
 
 NtpBackgroundService::~NtpBackgroundService() = default;
 
 void NtpBackgroundService::FetchCollectionInfo() {
-  if (simple_loader_ != nullptr)
+  if (collections_loader_ != nullptr)
     return;
 
   net::NetworkTrafficAnnotationTag traffic_annotation =
@@ -76,14 +83,14 @@
   request.SerializeToString(&serialized_proto);
 
   auto resource_request = std::make_unique<network::ResourceRequest>();
-  resource_request->url = api_url_;
+  resource_request->url = collections_api_url_;
   resource_request->method = "POST";
   resource_request->load_flags = net::LOAD_DO_NOT_SEND_AUTH_DATA;
 
-  simple_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
-                                                    traffic_annotation);
-  simple_loader_->AttachStringForUpload(serialized_proto, kProtoMimeType);
-  simple_loader_->DownloadToString(
+  collections_loader_ = network::SimpleURLLoader::Create(
+      std::move(resource_request), traffic_annotation);
+  collections_loader_->AttachStringForUpload(serialized_proto, kProtoMimeType);
+  collections_loader_->DownloadToString(
       url_loader_factory_.get(),
       base::BindOnce(&NtpBackgroundService::OnCollectionInfoFetchComplete,
                      base::Unretained(this)),
@@ -95,14 +102,14 @@
   collection_info_.clear();
   // The loader will be deleted when the request is handled.
   std::unique_ptr<network::SimpleURLLoader> loader_deleter(
-      std::move(simple_loader_));
+      std::move(collections_loader_));
 
   if (!response_body) {
     // This represents network errors (i.e. the server did not provide a
     // response).
     DLOG(WARNING) << "Request failed with error: "
                   << loader_deleter->NetError();
-    NotifyObservers();
+    NotifyObservers(FetchComplete::COLLECTION_INFO);
     return;
   }
 
@@ -111,7 +118,7 @@
     DLOG(WARNING)
         << "Deserializing Backdrop wallpaper proto for collection info "
            "failed.";
-    NotifyObservers();
+    NotifyObservers(FetchComplete::COLLECTION_INFO);
     return;
   }
 
@@ -120,7 +127,97 @@
         CollectionInfo::CreateFromProto(collections_response.collections(i)));
   }
 
-  NotifyObservers();
+  NotifyObservers(FetchComplete::COLLECTION_INFO);
+}
+
+void NtpBackgroundService::FetchCollectionImageInfo(
+    const std::string& collection_id) {
+  if (image_info_loader_ != nullptr)
+    return;
+
+  net::NetworkTrafficAnnotationTag traffic_annotation =
+      net::DefineNetworkTrafficAnnotation("backdrop_collection_images_download",
+                                          R"(
+        semantics {
+          sender: "Desktop NTP Background Selector"
+          description:
+            "The Chrome Desktop New Tab Page background selector displays a "
+            "rich set of wallpapers for users to choose from. Each wallpaper "
+            "belongs to a collection (e.g. Arts, Landscape etc.). The list of "
+            "all available collections is obtained from the Backdrop wallpaper "
+            "service."
+          trigger:
+            "Clicking the the settings (gear) icon on the New Tab page."
+          data:
+            "The Backdrop protocol buffer messages. No user data is included."
+          destination: GOOGLE_OWNED_SERVICE
+        }
+        policy {
+          cookies_allowed: NO
+          setting:
+            "Users can control this feature by selecting a non-Google default "
+            "search engine in Chrome settings under 'Search Engine'."
+          chrome_policy {
+            DefaultSearchProviderEnabled {
+              policy_options {mode: MANDATORY}
+              DefaultSearchProviderEnabled: false
+            }
+          }
+        })");
+
+  requested_collection_id_ = collection_id;
+  ntp::background::GetImagesInCollectionRequest request;
+  request.set_collection_id(collection_id);
+  // The language field may include the country code (e.g. "en-US").
+  request.set_language(g_browser_process->GetApplicationLocale());
+  std::string serialized_proto;
+  request.SerializeToString(&serialized_proto);
+
+  auto resource_request = std::make_unique<network::ResourceRequest>();
+  resource_request->url = collection_images_api_url_;
+  resource_request->method = "POST";
+  resource_request->load_flags = net::LOAD_DO_NOT_SEND_AUTH_DATA;
+
+  image_info_loader_ = network::SimpleURLLoader::Create(
+      std::move(resource_request), traffic_annotation);
+  image_info_loader_->AttachStringForUpload(serialized_proto, kProtoMimeType);
+  image_info_loader_->DownloadToString(
+      url_loader_factory_.get(),
+      base::BindOnce(&NtpBackgroundService::OnCollectionImageInfoFetchComplete,
+                     base::Unretained(this)),
+      1024 * 1024);
+}
+
+void NtpBackgroundService::OnCollectionImageInfoFetchComplete(
+    std::unique_ptr<std::string> response_body) {
+  collection_images_.clear();
+  // The loader will be deleted when the request is handled.
+  std::unique_ptr<network::SimpleURLLoader> loader_deleter(
+      std::move(image_info_loader_));
+
+  if (!response_body) {
+    // This represents network errors (i.e. the server did not provide a
+    // response).
+    DLOG(WARNING) << "Request failed with error: "
+                  << loader_deleter->NetError();
+    NotifyObservers(FetchComplete::COLLECTION_IMAGE_INFO);
+    return;
+  }
+
+  ntp::background::GetImagesInCollectionResponse images_response;
+  if (!images_response.ParseFromString(*response_body)) {
+    DLOG(WARNING)
+        << "Deserializing Backdrop wallpaper proto for image info failed.";
+    NotifyObservers(FetchComplete::COLLECTION_IMAGE_INFO);
+    return;
+  }
+
+  for (int i = 0; i < images_response.images_size(); ++i) {
+    collection_images_.push_back(CollectionImage::CreateFromProto(
+        requested_collection_id_, images_response.images(i)));
+  }
+
+  NotifyObservers(FetchComplete::COLLECTION_IMAGE_INFO);
 }
 
 void NtpBackgroundService::AddObserver(NtpBackgroundServiceObserver* observer) {
@@ -132,12 +229,23 @@
   observers_.RemoveObserver(observer);
 }
 
-void NtpBackgroundService::NotifyObservers() {
+void NtpBackgroundService::NotifyObservers(FetchComplete fetch_complete) {
   for (auto& observer : observers_) {
-    observer.OnCollectionInfoAvailable();
+    switch (fetch_complete) {
+      case FetchComplete::COLLECTION_INFO:
+        observer.OnCollectionInfoAvailable();
+        break;
+      case FetchComplete::COLLECTION_IMAGE_INFO:
+        observer.OnCollectionImagesAvailable();
+        break;
+    }
   }
 }
 
-GURL NtpBackgroundService::GetLoadURLForTesting() const {
-  return api_url_;
+GURL NtpBackgroundService::GetCollectionsLoadURLForTesting() const {
+  return collections_api_url_;
+}
+
+GURL NtpBackgroundService::GetImagesURLForTesting() const {
+  return collection_images_api_url_;
 }
diff --git a/chrome/browser/search/background/ntp_background_service.h b/chrome/browser/search/background/ntp_background_service.h
index bbc7bba..3ba90541 100644
--- a/chrome/browser/search/background/ntp_background_service.h
+++ b/chrome/browser/search/background/ntp_background_service.h
@@ -28,43 +28,73 @@
  public:
   NtpBackgroundService(
       scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
-      const base::Optional<GURL>& api_url_override);
+      const base::Optional<GURL>& collections_api_url_override,
+      const base::Optional<GURL>& collection_images_api_url_override);
   ~NtpBackgroundService() override;
 
   // Requests an asynchronous fetch from the network. After the update
-  // completes, OnCollectionInfoUpdated will be called on the observers.
+  // completes, OnCollectionInfoAvailable will be called on the observers.
   void FetchCollectionInfo();
 
-  // Returns the currently cached CollectionInfo, if any.
-  const std::vector<CollectionInfo>& collection_info() const {
-    return collection_info_;
-  }
+  // Requests an asynchronous fetch of metadata about images in the specified
+  // collection. After the update completes, OnCollectionImagesAvailable will be
+  // called on the observers. Requests that are made while an asynchronous fetch
+  // is in progress will be dropped until the currently active loader completes.
+  void FetchCollectionImageInfo(const std::string& collection_id);
 
   // Add/remove observers. All observers must unregister themselves before the
   // NtpBackgroundService is destroyed.
   void AddObserver(NtpBackgroundServiceObserver* observer);
   void RemoveObserver(NtpBackgroundServiceObserver* observer);
 
-  GURL GetLoadURLForTesting() const;
+  // Returns the currently cached CollectionInfo, if any.
+  const std::vector<CollectionInfo>& collection_info() const {
+    return collection_info_;
+  }
+
+  // Returns the currently cached CollectionImages, if any.
+  const std::vector<CollectionImage>& collection_images() const {
+    return collection_images_;
+  }
+
+  GURL GetCollectionsLoadURLForTesting() const;
+  GURL GetImagesURLForTesting() const;
 
  private:
-  GURL api_url_;
+  GURL collections_api_url_;
+  GURL collection_images_api_url_;
 
   // Used to download the proto from the Backdrop service.
   scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
-  std::unique_ptr<network::SimpleURLLoader> simple_loader_;
+  std::unique_ptr<network::SimpleURLLoader> collections_loader_;
+  std::unique_ptr<network::SimpleURLLoader> image_info_loader_;
 
   base::ObserverList<NtpBackgroundServiceObserver, true> observers_;
 
   // Callback that processes the response from the FetchCollectionInfo request,
-  // refreshing the contents of collection_info_data_ with server-provided data.
+  // refreshing the contents of collection_info_ with server-provided data.
   void OnCollectionInfoFetchComplete(
       const std::unique_ptr<std::string> response_body);
 
-  void NotifyObservers();
+  // Callback that processes the response from the FetchCollectionImages
+  // request, refreshing the contents of collection_images_ with
+  // server-provided data.
+  void OnCollectionImageInfoFetchComplete(
+      const std::unique_ptr<std::string> response_body);
+
+  enum class FetchComplete {
+    // Indicates that asynchronous fetch of CollectionInfo has completed.
+    COLLECTION_INFO,
+    // Indicates that asynchronous fetch of CollectionImages has completed.
+    COLLECTION_IMAGE_INFO
+  };
+  void NotifyObservers(FetchComplete fetch_complete);
 
   std::vector<CollectionInfo> collection_info_;
 
+  std::vector<CollectionImage> collection_images_;
+  std::string requested_collection_id_;
+
   DISALLOW_COPY_AND_ASSIGN(NtpBackgroundService);
 };
 
diff --git a/chrome/browser/search/background/ntp_background_service_factory.cc b/chrome/browser/search/background/ntp_background_service_factory.cc
index 1d6aecc3..ac365ebb 100644
--- a/chrome/browser/search/background/ntp_background_service_factory.cc
+++ b/chrome/browser/search/background/ntp_background_service_factory.cc
@@ -42,15 +42,24 @@
     return nullptr;
   }
 
-  std::string api_url = base::GetFieldTrialParamValueByFeature(
+  std::string collections_api_url = base::GetFieldTrialParamValueByFeature(
       features::kNtpBackgrounds, "background-collections-api-url");
-  base::Optional<GURL> override_api_url;
-  if (!api_url.empty()) {
-    override_api_url = GURL(api_url);
+  std::string collection_images_api_url =
+      base::GetFieldTrialParamValueByFeature(
+          features::kNtpBackgrounds, "background-collection-images-api-url");
+  base::Optional<GURL> collection_api_url_override;
+  base::Optional<GURL> collection_images_api_url_override;
+  if (!collections_api_url.empty()) {
+    collection_api_url_override = GURL(collections_api_url);
+  }
+  if (!collection_images_api_url.empty()) {
+    collection_images_api_url_override = GURL(collection_images_api_url);
   }
 
   auto url_loader_factory =
       content::BrowserContext::GetDefaultStoragePartition(context)
           ->GetURLLoaderFactoryForBrowserProcess();
-  return new NtpBackgroundService(url_loader_factory, override_api_url);
+  return new NtpBackgroundService(url_loader_factory,
+                                  collection_api_url_override,
+                                  collection_images_api_url_override);
 }
diff --git a/chrome/browser/search/background/ntp_background_service_observer.h b/chrome/browser/search/background/ntp_background_service_observer.h
index 017a3a8..8c99c710 100644
--- a/chrome/browser/search/background/ntp_background_service_observer.h
+++ b/chrome/browser/search/background/ntp_background_service_observer.h
@@ -8,12 +8,19 @@
 // Observer for NtpBackgroundService.
 class NtpBackgroundServiceObserver {
  public:
-  // Called when the CollectionInfoData is updated, usually as the result of a
+  // Called when the CollectionInfo is updated, usually as the result of a
   // FetchCollectionInfo() call on the service. Note that this is called after
   // each FetchCollectionInfo(), even if the network request failed, or if it
   // didn't result in an actual change to the cached data. You can get the new
-  // data via NtpBackgroundService::collection_info_data().
+  // data via NtpBackgroundService::collection_info().
   virtual void OnCollectionInfoAvailable() = 0;
+
+  // Called when the CollectionImages are updated, usually as the result of a
+  // FetchCollectionImageInfo() call on the service. Note that this is called
+  // after each FetchCollectionImage(), even if the network request failed, or
+  // if it didn't result in an actual change to the cached data. You can get the
+  // new data via NtpBackgroundService::collection_images().
+  virtual void OnCollectionImagesAvailable() = 0;
 };
 
 #endif  // CHROME_BROWSER_SEARCH_BACKGROUND_NTP_BACKGROUND_SERVICE_OBSERVER_H_
diff --git a/chrome/browser/search/background/ntp_background_service_unittest.cc b/chrome/browser/search/background/ntp_background_service_unittest.cc
index 9bd7861..4e1ba3f 100644
--- a/chrome/browser/search/background/ntp_background_service_unittest.cc
+++ b/chrome/browser/search/background/ntp_background_service_unittest.cc
@@ -18,6 +18,8 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
+using testing::Eq;
+
 class NtpBackgroundServiceTest : public testing::Test {
  public:
   NtpBackgroundServiceTest()
@@ -32,20 +34,20 @@
     testing::Test::SetUp();
 
     service_ = std::make_unique<NtpBackgroundService>(
-        test_shared_loader_factory_, base::nullopt);
+        test_shared_loader_factory_, base::nullopt, base::nullopt);
   }
 
-  void SetUpResponseWithData(const std::string& response) {
+  void SetUpResponseWithData(const GURL& load_url,
+                             const std::string& response) {
     test_url_loader_factory_.SetInterceptor(base::BindLambdaForTesting(
         [&](const network::ResourceRequest& request) {}));
-    test_url_loader_factory_.AddResponse(
-        service_->GetLoadURLForTesting().spec(), response);
+    test_url_loader_factory_.AddResponse(load_url.spec(), response);
   }
 
-  void SetUpResponseWithNetworkError() {
+  void SetUpResponseWithNetworkError(const GURL& load_url) {
     test_url_loader_factory_.AddResponse(
-        service_->GetLoadURLForTesting(), network::ResourceResponseHead(),
-        std::string(), network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
+        load_url, network::ResourceResponseHead(), std::string(),
+        network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
   }
 
   NtpBackgroundService* service() { return service_.get(); }
@@ -61,27 +63,26 @@
 };
 
 TEST_F(NtpBackgroundServiceTest, CollectionInfoNetworkError) {
-  SetUpResponseWithNetworkError();
+  SetUpResponseWithNetworkError(service()->GetCollectionsLoadURLForTesting());
 
   ASSERT_TRUE(service()->collection_info().empty());
 
   base::RunLoop loop;
-  loop.QuitWhenIdle();
   service()->FetchCollectionInfo();
-  loop.Run();
+  loop.RunUntilIdle();
 
   ASSERT_TRUE(service()->collection_info().empty());
 }
 
 TEST_F(NtpBackgroundServiceTest, BadCollectionsResponse) {
-  SetUpResponseWithData("bad serialized GetCollectionsResponse");
+  SetUpResponseWithData(service()->GetCollectionsLoadURLForTesting(),
+                        "bad serialized GetCollectionsResponse");
 
   ASSERT_TRUE(service()->collection_info().empty());
 
   base::RunLoop loop;
-  loop.QuitWhenIdle();
   service()->FetchCollectionInfo();
-  loop.Run();
+  loop.RunUntilIdle();
 
   EXPECT_TRUE(service()->collection_info().empty());
 }
@@ -90,20 +91,20 @@
   ntp::background::Collection collection;
   collection.set_collection_id("shapes");
   collection.set_collection_name("Shapes");
-  collection.add_preview()->set_image_url("image url");
+  collection.add_preview()->set_image_url("https://wallpapers.co/some_image");
   ntp::background::GetCollectionsResponse response;
   *response.add_collections() = collection;
   std::string response_string;
   response.SerializeToString(&response_string);
 
-  SetUpResponseWithData(response_string);
+  SetUpResponseWithData(service()->GetCollectionsLoadURLForTesting(),
+                        response_string);
 
   ASSERT_TRUE(service()->collection_info().empty());
 
   base::RunLoop loop;
-  loop.QuitWhenIdle();
   service()->FetchCollectionInfo();
-  loop.Run();
+  loop.RunUntilIdle();
 
   CollectionInfo collection_info;
   collection_info.collection_id = collection.collection_id();
@@ -111,5 +112,109 @@
   collection_info.preview_image_url = GURL(collection.preview(0).image_url());
 
   EXPECT_FALSE(service()->collection_info().empty());
-  EXPECT_THAT(service()->collection_info().at(0), testing::Eq(collection_info));
+  EXPECT_THAT(service()->collection_info().at(0), Eq(collection_info));
+}
+
+TEST_F(NtpBackgroundServiceTest, CollectionImagesNetworkError) {
+  SetUpResponseWithNetworkError(service()->GetImagesURLForTesting());
+
+  ASSERT_TRUE(service()->collection_images().empty());
+
+  base::RunLoop loop;
+  service()->FetchCollectionImageInfo("shapes");
+  loop.RunUntilIdle();
+
+  ASSERT_TRUE(service()->collection_images().empty());
+}
+
+TEST_F(NtpBackgroundServiceTest, BadCollectionImagesResponse) {
+  SetUpResponseWithData(service()->GetImagesURLForTesting(),
+                        "bad serialized GetImagesInCollectionResponse");
+
+  ASSERT_TRUE(service()->collection_images().empty());
+
+  base::RunLoop loop;
+  service()->FetchCollectionImageInfo("shapes");
+  loop.RunUntilIdle();
+
+  EXPECT_TRUE(service()->collection_images().empty());
+}
+
+TEST_F(NtpBackgroundServiceTest, GoodCollectionImagesResponse) {
+  ntp::background::Image image;
+  image.set_asset_id(12345);
+  image.set_image_url("https://wallpapers.co/some_image");
+  image.add_attribution()->set_text("attribution text");
+  ntp::background::GetImagesInCollectionResponse response;
+  *response.add_images() = image;
+  std::string response_string;
+  response.SerializeToString(&response_string);
+
+  SetUpResponseWithData(service()->GetImagesURLForTesting(), response_string);
+
+  ASSERT_TRUE(service()->collection_images().empty());
+
+  base::RunLoop loop;
+  service()->FetchCollectionImageInfo("shapes");
+  loop.RunUntilIdle();
+
+  CollectionImage collection_image;
+  collection_image.collection_id = "shapes";
+  collection_image.asset_id = image.asset_id();
+  collection_image.image_url = GURL(image.image_url());
+  collection_image.attribution.push_back(image.attribution(0).text());
+
+  EXPECT_FALSE(service()->collection_images().empty());
+  EXPECT_THAT(service()->collection_images().at(0), Eq(collection_image));
+}
+
+TEST_F(NtpBackgroundServiceTest, MultipleRequests) {
+  ntp::background::Collection collection;
+  collection.set_collection_id("shapes");
+  collection.set_collection_name("Shapes");
+  collection.add_preview()->set_image_url("https://wallpapers.co/some_image");
+  ntp::background::GetCollectionsResponse collection_response;
+  *collection_response.add_collections() = collection;
+  std::string collection_response_string;
+  collection_response.SerializeToString(&collection_response_string);
+
+  ntp::background::Image image;
+  image.set_asset_id(12345);
+  image.set_image_url("https://wallpapers.co/some_image");
+  image.add_attribution()->set_text("attribution text");
+  ntp::background::GetImagesInCollectionResponse image_response;
+  *image_response.add_images() = image;
+  std::string image_response_string;
+  image_response.SerializeToString(&image_response_string);
+
+  SetUpResponseWithData(service()->GetCollectionsLoadURLForTesting(),
+                        collection_response_string);
+  SetUpResponseWithData(service()->GetImagesURLForTesting(),
+                        image_response_string);
+
+  ASSERT_TRUE(service()->collection_info().empty());
+  ASSERT_TRUE(service()->collection_images().empty());
+
+  base::RunLoop loop;
+  service()->FetchCollectionInfo();
+  service()->FetchCollectionImageInfo("shapes");
+  // Subsequent requests are ignored while the loader is in use.
+  service()->FetchCollectionImageInfo("colors");
+  loop.RunUntilIdle();
+
+  CollectionInfo collection_info;
+  collection_info.collection_id = collection.collection_id();
+  collection_info.collection_name = collection.collection_name();
+  collection_info.preview_image_url = GURL(collection.preview(0).image_url());
+
+  CollectionImage collection_image;
+  collection_image.collection_id = "shapes";
+  collection_image.asset_id = image.asset_id();
+  collection_image.image_url = GURL(image.image_url());
+  collection_image.attribution.push_back(image.attribution(0).text());
+
+  EXPECT_FALSE(service()->collection_info().empty());
+  EXPECT_THAT(service()->collection_info().at(0), Eq(collection_info));
+  EXPECT_FALSE(service()->collection_images().empty());
+  EXPECT_THAT(service()->collection_images().at(0), Eq(collection_image));
 }
diff --git a/chrome/browser/search/local_ntp_source.cc b/chrome/browser/search/local_ntp_source.cc
index 1422a6b..ddcd762 100644
--- a/chrome/browser/search/local_ntp_source.cc
+++ b/chrome/browser/search/local_ntp_source.cc
@@ -74,6 +74,7 @@
 const char kMainHtmlFilename[] = "local-ntp.html";
 const char kNtpBackgroundCollectionScriptFilename[] =
     "ntp-background-collections.js";
+const char kNtpBackgroundImageScriptFilename[] = "ntp-background-images.js";
 const char kOneGoogleBarScriptFilename[] = "one-google.js";
 const char kDoodleScriptFilename[] = "doodle.js";
 
@@ -94,6 +95,7 @@
     {"images/close_3_mask.png", IDR_CLOSE_3_MASK, "image/png"},
     {"images/ntp_default_favicon.png", IDR_NTP_DEFAULT_FAVICON, "image/png"},
     {kNtpBackgroundCollectionScriptFilename, kLocalResource, "text/javascript"},
+    {kNtpBackgroundImageScriptFilename, kLocalResource, "text/javascript"},
     {kOneGoogleBarScriptFilename, kLocalResource, "text/javascript"},
     {kDoodleScriptFilename, kLocalResource, "text/javascript"},
 };
@@ -220,10 +222,10 @@
 }
 
 base::Value ConvertCollectionInfoToDict(
-    const std::vector<CollectionInfo> collection_info_data) {
+    const std::vector<CollectionInfo>& collection_info) {
   base::Value collections(base::Value::Type::LIST);
-  collections.GetList().reserve(collection_info_data.size());
-  for (const CollectionInfo& collection : collection_info_data) {
+  collections.GetList().reserve(collection_info.size());
+  for (const CollectionInfo& collection : collection_info) {
     base::Value dict(base::Value::Type::DICTIONARY);
     dict.SetKey("collectionId", base::Value(collection.collection_id));
     dict.SetKey("collectionName", base::Value(collection.collection_name));
@@ -234,6 +236,23 @@
   return collections;
 }
 
+base::Value ConvertCollectionImageToDict(
+    const std::vector<CollectionImage>& collection_image) {
+  base::Value images(base::Value::Type::LIST);
+  images.GetList().reserve(collection_image.size());
+  for (const CollectionImage& image : collection_image) {
+    base::Value dict(base::Value::Type::DICTIONARY);
+    dict.SetKey("imageUrl", base::Value(image.image_url.spec()));
+    base::Value attributions(base::Value::Type::LIST);
+    for (const auto& attribution : image.attribution) {
+      attributions.GetList().push_back(base::Value(attribution));
+    }
+    dict.SetKey("attributions", std::move(attributions));
+    images.GetList().push_back(std::move(dict));
+  }
+  return images;
+}
+
 std::unique_ptr<base::DictionaryValue> ConvertOGBDataToDict(
     const OneGoogleBarData& og) {
   auto result = std::make_unique<base::DictionaryValue>();
@@ -568,11 +587,30 @@
       return;
     }
 
-    ntp_background_requests_.emplace_back(base::TimeTicks::Now(), callback);
+    ntp_background_collections_requests_.emplace_back(base::TimeTicks::Now(),
+                                                      callback);
     ntp_background_service_->FetchCollectionInfo();
     return;
   }
 
+  if (stripped_path == kNtpBackgroundImageScriptFilename) {
+    if (!ntp_background_service_) {
+      callback.Run(nullptr);
+      return;
+    }
+    std::string collection_id_param;
+    GURL path_url = GURL(chrome::kChromeSearchLocalNtpUrl).Resolve(path);
+    if (net::GetValueForKeyInQuery(path_url, "collection_id",
+                                   &collection_id_param)) {
+      ntp_background_image_info_requests_.emplace_back(base::TimeTicks::Now(),
+                                                       callback);
+      ntp_background_service_->FetchCollectionImageInfo(collection_id_param);
+    } else {
+      callback.Run(nullptr);
+    }
+    return;
+  }
+
   if (stripped_path == kOneGoogleBarScriptFilename) {
     if (!one_google_bar_service_) {
       callback.Run(nullptr);
@@ -695,7 +733,7 @@
 void LocalNtpSource::OnCollectionInfoAvailable() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
-  if (ntp_background_requests_.empty())
+  if (ntp_background_collections_requests_.empty())
     return;
 
   scoped_refptr<base::RefCountedString> result;
@@ -707,7 +745,7 @@
   result = base::RefCountedString::TakeString(&js);
 
   base::TimeTicks now = base::TimeTicks::Now();
-  for (const auto& request : ntp_background_requests_) {
+  for (const auto& request : ntp_background_collections_requests_) {
     request.callback.Run(result);
     base::TimeDelta delta = now - request.start_time;
     UMA_HISTOGRAM_MEDIUM_TIMES(
@@ -723,7 +761,39 @@
           delta);
     }
   }
-  ntp_background_requests_.clear();
+  ntp_background_collections_requests_.clear();
+}
+
+void LocalNtpSource::OnCollectionImagesAvailable() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  if (ntp_background_image_info_requests_.empty())
+    return;
+
+  scoped_refptr<base::RefCountedString> result;
+  std::string js;
+  base::JSONWriter::Write(ConvertCollectionImageToDict(
+                              ntp_background_service_->collection_images()),
+                          &js);
+  js = "var coll_img = " + js + ";";
+  result = base::RefCountedString::TakeString(&js);
+
+  base::TimeTicks now = base::TimeTicks::Now();
+  for (const auto& request : ntp_background_image_info_requests_) {
+    request.callback.Run(result);
+    base::TimeDelta delta = now - request.start_time;
+    UMA_HISTOGRAM_MEDIUM_TIMES(
+        "NewTabPage.BackgroundService.Images.RequestLatency", delta);
+    // Any response where no images are returned is considered a failure.
+    if (ntp_background_service_->collection_images().empty()) {
+      UMA_HISTOGRAM_MEDIUM_TIMES(
+          "NewTabPage.BackgroundService.Images.RequestLatency.Failure", delta);
+    } else {
+      UMA_HISTOGRAM_MEDIUM_TIMES(
+          "NewTabPage.BackgroundService.Images.RequestLatency.Success", delta);
+    }
+  }
+  ntp_background_image_info_requests_.clear();
 }
 
 void LocalNtpSource::OnOneGoogleBarDataUpdated() {
diff --git a/chrome/browser/search/local_ntp_source.h b/chrome/browser/search/local_ntp_source.h
index 5fe6b67..1705666 100644
--- a/chrome/browser/search/local_ntp_source.h
+++ b/chrome/browser/search/local_ntp_source.h
@@ -89,6 +89,7 @@
 
   // Overridden from NtpBackgroundServiceObserver:
   void OnCollectionInfoAvailable() override;
+  void OnCollectionImagesAvailable() override;
 
   // Overridden from OneGoogleBarServiceObserver:
   void OnOneGoogleBarDataUpdated() override;
@@ -98,7 +99,8 @@
 
   Profile* const profile_;
 
-  std::vector<NtpBackgroundRequest> ntp_background_requests_;
+  std::vector<NtpBackgroundRequest> ntp_background_collections_requests_;
+  std::vector<NtpBackgroundRequest> ntp_background_image_info_requests_;
 
   NtpBackgroundService* ntp_background_service_;