diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-05-15 10:20:33 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-05-15 10:28:57 +0000 |
commit | d17ea114e5ef69ad5d5d7413280a13e6428098aa (patch) | |
tree | 2c01a75df69f30d27b1432467cfe7c1467a498da /chromium/components/ntp_snippets | |
parent | 8c5c43c7b138c9b4b0bf56d946e61d3bbc111bec (diff) | |
download | qtwebengine-chromium-d17ea114e5ef69ad5d5d7413280a13e6428098aa.tar.gz |
BASELINE: Update Chromium to 67.0.3396.47
Change-Id: Idcb1341782e417561a2473eeecc82642dafda5b7
Reviewed-by: Michal Klocek <michal.klocek@qt.io>
Diffstat (limited to 'chromium/components/ntp_snippets')
67 files changed, 2963 insertions, 1582 deletions
diff --git a/chromium/components/ntp_snippets/BUILD.gn b/chromium/components/ntp_snippets/BUILD.gn index eb88be97120..88f8092c998 100644 --- a/chromium/components/ntp_snippets/BUILD.gn +++ b/chromium/components/ntp_snippets/BUILD.gn @@ -4,6 +4,8 @@ import("//build/config/ui.gni") +import("//third_party/protobuf/proto_library.gni") + if (is_android) { import("//build/config/android/config.gni") import("//build/config/android/rules.gni") @@ -44,15 +46,23 @@ static_library("ntp_snippets") { "content_suggestions_provider.h", "content_suggestions_service.cc", "content_suggestions_service.h", + "contextual/cluster.cc", + "contextual/cluster.h", "contextual/contextual_content_suggestions_service.cc", "contextual/contextual_content_suggestions_service.h", - "contextual/contextual_json_request.cc", - "contextual/contextual_json_request.h", + "contextual/contextual_content_suggestions_service_proxy.cc", + "contextual/contextual_content_suggestions_service_proxy.h", "contextual/contextual_suggestion.cc", "contextual/contextual_suggestion.h", + "contextual/contextual_suggestions_fetch.cc", + "contextual/contextual_suggestions_fetch.h", "contextual/contextual_suggestions_fetcher.h", "contextual/contextual_suggestions_fetcher_impl.cc", "contextual/contextual_suggestions_fetcher_impl.h", + "contextual/contextual_suggestions_metrics_reporter.cc", + "contextual/contextual_suggestions_metrics_reporter.h", + "contextual/contextual_suggestions_ukm_entry.cc", + "contextual/contextual_suggestions_ukm_entry.h", "features.cc", "features.h", "logger.cc", @@ -136,6 +146,7 @@ static_library("ntp_snippets") { ] deps = [ + ":explore_protos", "//components/bookmarks/browser", "//components/data_use_measurement/core", "//components/favicon/core", @@ -154,13 +165,19 @@ static_library("ntp_snippets") { "//components/sessions", "//components/strings", "//components/sync_sessions", + + # "//components/ukm/content", "//components/url_formatter", "//components/variations", "//components/variations/net", "//components/variations/service", "//components/web_resource", "//services/identity/public/cpp", + "//services/metrics/public/cpp:metrics_cpp", + "//services/network/public/cpp", + "//services/network/public/mojom", "//third_party/icu/", + "//third_party/protobuf:protobuf_lite", "//ui/gfx", ] } @@ -172,10 +189,19 @@ if (is_android) { "category_info.h", "category_status.h", "content_suggestions_service.cc", + "contextual/contextual_suggestions_metrics_reporter.h", ] } } +proto_library("explore_protos") { + sources = [ + "contextual/proto/chrome_search_api_request_context.proto", + "contextual/proto/get_pivots_request.proto", + "contextual/proto/get_pivots_response.proto", + ] +} + source_set("unit_tests") { testonly = true sources = [ @@ -188,9 +214,11 @@ source_set("unit_tests") { "category_unittest.cc", "content_suggestions_metrics_unittest.cc", "content_suggestions_service_unittest.cc", + "contextual/contextual_content_suggestions_service_proxy_unittest.cc", "contextual/contextual_content_suggestions_service_unittest.cc", - "contextual/contextual_json_request_unittest.cc", "contextual/contextual_suggestions_fetcher_impl_unittest.cc", + "contextual/contextual_suggestions_metrics_reporter_unittest.cc", + "contextual/contextual_suggestions_ukm_entry_unittest.cc", "logger_unittest.cc", "offline_pages/recent_tab_suggestions_provider_unittest.cc", "physical_web_pages/physical_web_page_suggestions_provider_unittest.cc", @@ -218,6 +246,7 @@ source_set("unit_tests") { } deps = [ + ":explore_protos", ":ntp_snippets", ":test_support", "//base", @@ -227,6 +256,7 @@ source_set("unit_tests") { "//components/gcm_driver", "//components/gcm_driver/instance_id", "//components/image_fetcher/core", + "//components/image_fetcher/core:test_support", "//components/leveldb_proto:test_support", "//components/ntp_snippets/remote/proto", "//components/offline_pages/core", @@ -243,12 +273,16 @@ source_set("unit_tests") { "//components/sync:test_support_driver", "//components/sync_preferences:test_support", "//components/sync_sessions", + "//components/ukm:test_support", "//components/variations:test_support", "//components/web_resource:web_resource", "//google_apis/gcm", "//net:test_support", "//services/identity/public/cpp", "//services/identity/public/cpp:test_support", + "//services/network:test_support", + "//services/network/public/cpp", + "//services/network/public/mojom", "//testing/gtest", "//third_party/icu/", "//ui/gfx:test_support", diff --git a/chromium/components/ntp_snippets/DEPS b/chromium/components/ntp_snippets/DEPS index 1fbc017dcb0..33dd6c93fa2 100644 --- a/chromium/components/ntp_snippets/DEPS +++ b/chromium/components/ntp_snippets/DEPS @@ -15,6 +15,7 @@ include_rules = [ "+components/strings/grit/components_strings.h", "+components/sync_preferences/testing_pref_service_syncable.h", "+components/sync/driver", + "+components/ukm", "+components/url_formatter", "+components/variations", "+components/version_info", @@ -26,6 +27,11 @@ include_rules = [ "+net/url_request", "+google_apis", "+services/identity/public/cpp", + "+services/metrics/public/cpp", + "+services/network/public/cpp", + "+services/network/public/mojom", + "+services/network/test", + "+third_party/protobuf", "+ui/base", "+ui/gfx", ] diff --git a/chromium/components/ntp_snippets/OWNERS b/chromium/components/ntp_snippets/OWNERS index 3ac3779dd87..ed0519eb118 100644 --- a/chromium/components/ntp_snippets/OWNERS +++ b/chromium/components/ntp_snippets/OWNERS @@ -1,7 +1,10 @@ bauerb@chromium.org +fgorski@chromium.org jkrcal@chromium.org +pnoland@chromium.org treib@chromium.org tschumann@chromium.org +zea@chromium.org # Unsure who to ask? Choose from the above. markusheintz@chromium.org diff --git a/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.cc b/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.cc index c0be92cb0aa..8ecaa37a68e 100644 --- a/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.cc @@ -116,6 +116,13 @@ void BookmarkSuggestionsProvider::FetchSuggestionImage( FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); } +void BookmarkSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); +} + void BookmarkSuggestionsProvider::Fetch( const Category& category, const std::set<std::string>& known_suggestion_ids, diff --git a/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.h b/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.h index ad2aca0b300..095d2616465 100644 --- a/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.h +++ b/chromium/components/ntp_snippets/bookmarks/bookmark_suggestions_provider.h @@ -33,6 +33,8 @@ class BookmarkSuggestionsProvider : public ContentSuggestionsProvider, void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; diff --git a/chromium/components/ntp_snippets/breaking_news/breaking_news_gcm_app_handler_unittest.cc b/chromium/components/ntp_snippets/breaking_news/breaking_news_gcm_app_handler_unittest.cc index b262a3ff873..87daac8bfdb 100644 --- a/chromium/components/ntp_snippets/breaking_news/breaking_news_gcm_app_handler_unittest.cc +++ b/chromium/components/ntp_snippets/breaking_news/breaking_news_gcm_app_handler_unittest.cc @@ -204,13 +204,13 @@ void ParseJson( ACTION_TEMPLATE(InvokeCallbackArgument, HAS_1_TEMPLATE_PARAMS(int, k), AND_1_VALUE_PARAMS(p0)) { - ::std::tr1::get<k>(args).Run(p0); + std::get<k>(args).Run(p0); } ACTION_TEMPLATE(InvokeCallbackArgument, HAS_1_TEMPLATE_PARAMS(int, k), AND_2_VALUE_PARAMS(p0, p1)) { - ::std::tr1::get<k>(args).Run(p0, p1); + std::get<k>(args).Run(p0, p1); } base::Time GetDummyNow() { @@ -254,9 +254,6 @@ class BreakingNewsGCMAppHandlerTest : public testing::Test { std::unique_ptr<BreakingNewsGCMAppHandler> MakeHandler( scoped_refptr<TestMockTimeTaskRunner> timer_mock_task_runner) { - tick_clock_ = timer_mock_task_runner->GetMockTickClock(); - clock_ = timer_mock_task_runner->GetMockClock(); - message_loop_.SetTaskRunner(timer_mock_task_runner); // TODO(vitaliii): Initialize MockSubscriptionManager in the constructor, so @@ -265,18 +262,19 @@ class BreakingNewsGCMAppHandlerTest : public testing::Test { std::make_unique<NiceMock<MockSubscriptionManager>>(); mock_subscription_manager_ = wrapped_mock_subscription_manager.get(); - auto token_validation_timer = - std::make_unique<base::OneShotTimer>(tick_clock_.get()); + auto token_validation_timer = std::make_unique<base::OneShotTimer>( + timer_mock_task_runner->GetMockTickClock()); token_validation_timer->SetTaskRunner(timer_mock_task_runner); - auto forced_subscription_timer = - std::make_unique<base::OneShotTimer>(tick_clock_.get()); + auto forced_subscription_timer = std::make_unique<base::OneShotTimer>( + timer_mock_task_runner->GetMockTickClock()); forced_subscription_timer->SetTaskRunner(timer_mock_task_runner); return std::make_unique<BreakingNewsGCMAppHandler>( mock_gcm_driver_.get(), mock_instance_id_driver_.get(), pref_service(), std::move(wrapped_mock_subscription_manager), base::Bind(&ParseJson), - clock_.get(), std::move(token_validation_timer), + timer_mock_task_runner->GetMockClock(), + std::move(token_validation_timer), std::move(forced_subscription_timer)); } @@ -312,8 +310,6 @@ class BreakingNewsGCMAppHandlerTest : public testing::Test { std::unique_ptr<StrictMock<MockGCMDriver>> mock_gcm_driver_; std::unique_ptr<StrictMock<MockInstanceIDDriver>> mock_instance_id_driver_; std::unique_ptr<StrictMock<MockInstanceID>> mock_instance_id_; - std::unique_ptr<TickClock> tick_clock_; - std::unique_ptr<Clock> clock_; }; TEST_F(BreakingNewsGCMAppHandlerTest, @@ -931,7 +927,8 @@ TEST_F(BreakingNewsGCMAppHandlerTest, ShouldReportMissingAction) { histogram_tester.GetAllSamples( "NewTabPage.ContentSuggestions.BreakingNews.ReceivedMessageAction"), ElementsAre(base::Bucket( - /*min=*/metrics::ReceivedMessageAction::NO_ACTION, /*count=*/1))); + /*min=*/static_cast<int>(metrics::ReceivedMessageAction::NO_ACTION), + /*count=*/1))); } TEST_F(BreakingNewsGCMAppHandlerTest, ShouldReportInvalidAction) { @@ -957,7 +954,8 @@ TEST_F(BreakingNewsGCMAppHandlerTest, ShouldReportInvalidAction) { histogram_tester.GetAllSamples( "NewTabPage.ContentSuggestions.BreakingNews.ReceivedMessageAction"), ElementsAre( - base::Bucket(/*min=*/metrics::ReceivedMessageAction::INVALID_ACTION, + base::Bucket(/*min=*/static_cast<int>( + metrics::ReceivedMessageAction::INVALID_ACTION), /*count=*/1))); } @@ -984,7 +982,8 @@ TEST_F(BreakingNewsGCMAppHandlerTest, ShouldReportPushToRefreshAction) { histogram_tester.GetAllSamples( "NewTabPage.ContentSuggestions.BreakingNews.ReceivedMessageAction"), ElementsAre( - base::Bucket(/*min=*/metrics::ReceivedMessageAction::PUSH_TO_REFRESH, + base::Bucket(/*min=*/static_cast<int>( + metrics::ReceivedMessageAction::PUSH_TO_REFRESH), /*count=*/1))); } @@ -1012,7 +1011,9 @@ TEST_F(BreakingNewsGCMAppHandlerTest, ShouldReportPushByValueAction) { histogram_tester.GetAllSamples( "NewTabPage.ContentSuggestions.BreakingNews.ReceivedMessageAction"), ElementsAre(base::Bucket( - /*min=*/metrics::ReceivedMessageAction::PUSH_BY_VALUE, /*count=*/1))); + /*min=*/static_cast<int>( + metrics::ReceivedMessageAction::PUSH_BY_VALUE), + /*count=*/1))); } TEST_F(BreakingNewsGCMAppHandlerTest, diff --git a/chromium/components/ntp_snippets/breaking_news/breaking_news_metrics.h b/chromium/components/ntp_snippets/breaking_news/breaking_news_metrics.h index 1b787ed677c..a80e78aef83 100644 --- a/chromium/components/ntp_snippets/breaking_news/breaking_news_metrics.h +++ b/chromium/components/ntp_snippets/breaking_news/breaking_news_metrics.h @@ -20,7 +20,7 @@ void OnUnsubscriptionRequestCompleted(const Status& status); // don't remove or reorder elements, only add new ones at the end (before // COUNT), and keep in sync with ContentSuggestionsBreakingNewsMessageAction in // enums.xml. -enum ReceivedMessageAction { +enum class ReceivedMessageAction { NO_ACTION = 0, INVALID_ACTION = 1, PUSH_BY_VALUE = 2, diff --git a/chromium/components/ntp_snippets/breaking_news/subscription_json_request_unittest.cc b/chromium/components/ntp_snippets/breaking_news/subscription_json_request_unittest.cc index 2625529a0d0..8ff89d1a5b6 100644 --- a/chromium/components/ntp_snippets/breaking_news/subscription_json_request_unittest.cc +++ b/chromium/components/ntp_snippets/breaking_news/subscription_json_request_unittest.cc @@ -5,7 +5,6 @@ #include "components/ntp_snippets/breaking_news/subscription_json_request.h" #include "base/json/json_reader.h" -#include "base/memory/ptr_util.h" #include "base/message_loop/message_loop.h" #include "base/test/gtest_util.h" #include "base/test/mock_callback.h" diff --git a/chromium/components/ntp_snippets/callbacks.h b/chromium/components/ntp_snippets/callbacks.h index 2d9efcec22a..16d417af4dd 100644 --- a/chromium/components/ntp_snippets/callbacks.h +++ b/chromium/components/ntp_snippets/callbacks.h @@ -29,6 +29,9 @@ using FetchDoneCallback = // ContentSuggestionsProvider. using ImageFetchedCallback = base::OnceCallback<void(const gfx::Image&)>; +using ImageDataFetchedCallback = + base::OnceCallback<void(const std::string& image_data)>; + // Returns the list of dismissed suggestions when invoked. Currently only used // for debugging methods to check the internal state of a provider. using DismissedSuggestionsCallback = base::OnceCallback<void( diff --git a/chromium/components/ntp_snippets/content_suggestions_metrics.cc b/chromium/components/ntp_snippets/content_suggestions_metrics.cc index feec62e796a..5c6746ce293 100644 --- a/chromium/components/ntp_snippets/content_suggestions_metrics.cc +++ b/chromium/components/ntp_snippets/content_suggestions_metrics.cc @@ -79,7 +79,7 @@ const char kPerCategoryHistogramFormat[] = "%s.%s"; // and contains exactly the values to be recorded in UMA. Don't remove or // reorder elements, only add new ones at the end (before COUNT), and keep in // sync with ContentSuggestionsCategory in histograms.xml. -enum HistogramCategories { +enum class HistogramCategories { EXPERIMENTAL, RECENT_TABS, DOWNLOADS, diff --git a/chromium/components/ntp_snippets/content_suggestions_provider.h b/chromium/components/ntp_snippets/content_suggestions_provider.h index c22826f8880..aa67c151bba 100644 --- a/chromium/components/ntp_snippets/content_suggestions_provider.h +++ b/chromium/components/ntp_snippets/content_suggestions_provider.h @@ -95,6 +95,15 @@ class ContentSuggestionsProvider { virtual void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) = 0; + // Fetches the image data for the suggestion with the given ID and returns it + // through the callback. This fetch may occur locally or from the internet. + // If that suggestion doesn't exist, doesn't have an image or if the fetch + // fails, the callback gets empty data. The callback will not be called + // synchronously. + virtual void FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) = 0; + // Fetches more suggestions for the given category. The new suggestions // will not include any suggestion of the |known_suggestion_ids| sets. // As a result of this call, the provider: diff --git a/chromium/components/ntp_snippets/content_suggestions_service.cc b/chromium/components/ntp_snippets/content_suggestions_service.cc index 0cefd1e1cb7..0fc12b6b729 100644 --- a/chromium/components/ntp_snippets/content_suggestions_service.cc +++ b/chromium/components/ntp_snippets/content_suggestions_service.cc @@ -11,7 +11,6 @@ #include "base/bind.h" #include "base/location.h" -#include "base/memory/ptr_util.h" #include "base/metrics/histogram_macros.h" #include "base/stl_util.h" #include "base/strings/string_number_conversions.h" @@ -163,6 +162,20 @@ void ContentSuggestionsService::FetchSuggestionImage( suggestion_id, std::move(callback)); } +void ContentSuggestionsService::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + if (!providers_by_category_.count(suggestion_id.category())) { + LOG(WARNING) << "Requested image for suggestion " << suggestion_id + << " for unavailable category " << suggestion_id.category(); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); + return; + } + providers_by_category_[suggestion_id.category()]->FetchSuggestionImageData( + suggestion_id, std::move(callback)); +} + // TODO(jkrcal): Split the favicon fetching into a separate class. void ContentSuggestionsService::FetchSuggestionFavicon( const ContentSuggestion::ID& suggestion_id, diff --git a/chromium/components/ntp_snippets/content_suggestions_service.h b/chromium/components/ntp_snippets/content_suggestions_service.h index 0a0c4c49170..6cc866c2933 100644 --- a/chromium/components/ntp_snippets/content_suggestions_service.h +++ b/chromium/components/ntp_snippets/content_suggestions_service.h @@ -92,7 +92,7 @@ class ContentSuggestionsService : public KeyedService, virtual ~Observer() = default; }; - enum State { + enum class State { ENABLED, DISABLED, }; @@ -143,6 +143,13 @@ class ContentSuggestionsService : public KeyedService, void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback); + // Fetches the image data for the suggestion with the given |suggestion_id| + // and runs the |callback|. If that suggestion doesn't exist or the fetch + // fails, the callback gets empty data. The callback will not be called + // synchronously. + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback); + // Fetches the favicon from local cache (if larger than or equal to // |minimum_size_in_pixel|) or from Google server (if there is no icon in the // cache) and returns the results in the callback. If that suggestion doesn't diff --git a/chromium/components/ntp_snippets/contextual/cluster.cc b/chromium/components/ntp_snippets/contextual/cluster.cc new file mode 100644 index 00000000000..b706a794850 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/cluster.cc @@ -0,0 +1,46 @@ +// Copyright 2018 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/ntp_snippets/contextual/cluster.h" + +#include "components/ntp_snippets/contextual/contextual_suggestion.h" + +namespace contextual_suggestions { + +Cluster::Cluster() = default; + +// MSVC doesn't support defaulted move constructors, so we have to define it +// ourselves. +Cluster::Cluster(Cluster&& other) noexcept + : title(std::move(other.title)), + suggestions(std::move(other.suggestions)) {} + +Cluster::~Cluster() = default; + +ClusterBuilder::ClusterBuilder(const std::string& title) { + cluster_.title = title; +} + +ClusterBuilder::ClusterBuilder(const ClusterBuilder& other) { + cluster_.title = other.cluster_.title; + for (const auto& suggestion : other.cluster_.suggestions) { + AddSuggestion(SuggestionBuilder(suggestion.url) + .Title(suggestion.title) + .PublisherName(suggestion.publisher_name) + .Snippet(suggestion.snippet) + .ImageId(suggestion.image_id) + .Build()); + } +} + +ClusterBuilder& ClusterBuilder::AddSuggestion(ContextualSuggestion suggestion) { + cluster_.suggestions.emplace_back(std::move(suggestion)); + return *this; +} + +Cluster ClusterBuilder::Build() { + return std::move(cluster_); +} + +} // namespace contextual_suggestions
\ No newline at end of file diff --git a/chromium/components/ntp_snippets/contextual/cluster.h b/chromium/components/ntp_snippets/contextual/cluster.h new file mode 100644 index 00000000000..621253867c1 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/cluster.h @@ -0,0 +1,49 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CLUSTER_H_ +#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CLUSTER_H_ + +#include "components/ntp_snippets/contextual/contextual_suggestion.h" + +using ntp_snippets::ContextualSuggestion; +using ntp_snippets::SuggestionBuilder; + +namespace contextual_suggestions { + +// A structure representing a suggestion cluster. +struct Cluster { + Cluster(); + Cluster(Cluster&&) noexcept; + ~Cluster(); + + std::string title; + std::vector<ContextualSuggestion> suggestions; + + private: + DISALLOW_COPY_AND_ASSIGN(Cluster); +}; + +// Allows concise construction of a cluster and copying, which cluster +// doesn't allow. +class ClusterBuilder { + public: + ClusterBuilder(const std::string& title); + + // Allow copying for ease of validation when testing. + ClusterBuilder(const ClusterBuilder& other); + ClusterBuilder& AddSuggestion(ContextualSuggestion suggestion); + Cluster Build(); + + private: + Cluster cluster_; +}; + +using FetchClustersCallback = + base::OnceCallback<void(std::string peek_text, + std::vector<Cluster> clusters)>; + +} // namespace contextual_suggestions + +#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CLUSTER_H_
\ No newline at end of file diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.cc b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.cc index dd12e915788..d7bdc7ac3ba 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.cc +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.cc @@ -11,70 +11,87 @@ #include "base/bind.h" #include "base/memory/ptr_util.h" +#include "components/ntp_snippets/contextual/cluster.h" #include "components/ntp_snippets/remote/cached_image_fetcher.h" #include "components/ntp_snippets/remote/remote_suggestions_database.h" #include "components/ntp_snippets/remote/remote_suggestions_provider_impl.h" +#include "contextual_content_suggestions_service_proxy.h" #include "ui/gfx/image/image.h" namespace ntp_snippets { +using contextual_suggestions::Cluster; +using contextual_suggestions::ContextualSuggestionsMetricsReporterProvider; +using contextual_suggestions::FetchClustersCallback; + +namespace { +bool IsEligibleURL(const GURL& url) { + return url.is_valid() && url.SchemeIsHTTPOrHTTPS() && !url.HostIsIPAddress(); +} + +} // namespace + ContextualContentSuggestionsService::ContextualContentSuggestionsService( std::unique_ptr<ContextualSuggestionsFetcher> contextual_suggestions_fetcher, std::unique_ptr<CachedImageFetcher> image_fetcher, - std::unique_ptr<RemoteSuggestionsDatabase> contextual_suggestions_database) + std::unique_ptr<RemoteSuggestionsDatabase> contextual_suggestions_database, + std::unique_ptr<ContextualSuggestionsMetricsReporterProvider> + metrics_reporter_provider) : contextual_suggestions_database_( std::move(contextual_suggestions_database)), contextual_suggestions_fetcher_( std::move(contextual_suggestions_fetcher)), - image_fetcher_(std::move(image_fetcher)) {} + image_fetcher_(std::move(image_fetcher)), + metrics_reporter_provider_(std::move(metrics_reporter_provider)) {} ContextualContentSuggestionsService::~ContextualContentSuggestionsService() = default; -void ContextualContentSuggestionsService::FetchContextualSuggestions( +void ContextualContentSuggestionsService::FetchContextualSuggestionClusters( const GURL& url, - FetchContextualSuggestionsCallback callback) { - contextual_suggestions_fetcher_->FetchContextualSuggestions( - url, - base::BindOnce( - &ContextualContentSuggestionsService::DidFetchContextualSuggestions, - base::Unretained(this), url, std::move(callback))); + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) { + // TODO(pnoland): Also check that the url is safe. + if (IsEligibleURL(url)) { + contextual_suggestions_fetcher_->FetchContextualSuggestionsClusters( + url, std::move(callback), std::move(metrics_callback)); + } else { + std::move(callback).Run("", {}); + } } void ContextualContentSuggestionsService::FetchContextualSuggestionImage( const ContentSuggestion::ID& suggestion_id, + const GURL& image_url, + ImageFetchedCallback callback) { + image_fetcher_->FetchSuggestionImage(suggestion_id, image_url, + ImageDataFetchedCallback(), + std::move(callback)); +} + +void ContextualContentSuggestionsService::FetchContextualSuggestionImageLegacy( + const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) { const std::string& id_within_category = suggestion_id.id_within_category(); auto image_url_iterator = image_url_by_id_.find(id_within_category); - if (image_url_iterator != image_url_by_id_.end()) { - GURL image_url = image_url_iterator->second; - image_fetcher_->FetchSuggestionImage(suggestion_id, image_url, - std::move(callback)); - } else { + if (image_url_iterator == image_url_by_id_.end()) { DVLOG(1) << "FetchContextualSuggestionImage unknown image" << " id_within_category: " << id_within_category; std::move(callback).Run(gfx::Image()); + return; } + + GURL image_url = image_url_iterator->second; + FetchContextualSuggestionImage(suggestion_id, image_url, std::move(callback)); } -// TODO(gaschler): Cache contextual suggestions at run-time. -void ContextualContentSuggestionsService::DidFetchContextualSuggestions( - const GURL& url, - FetchContextualSuggestionsCallback callback, - Status status, - ContextualSuggestionsFetcher::OptionalSuggestions fetched_suggestions) { - std::vector<ContentSuggestion> suggestions; - if (fetched_suggestions.has_value()) { - for (const std::unique_ptr<ContextualSuggestion>& suggestion : - fetched_suggestions.value()) { - suggestions.emplace_back(suggestion->ToContentSuggestion()); - ContentSuggestion::ID id = suggestions.back().id(); - GURL image_url = suggestion->salient_image_url(); - image_url_by_id_[id.id_within_category()] = image_url; - } - } - std::move(callback).Run(status, url, std::move(suggestions)); +std::unique_ptr< + contextual_suggestions::ContextualContentSuggestionsServiceProxy> +ContextualContentSuggestionsService::CreateProxy() { + return std::make_unique< + contextual_suggestions::ContextualContentSuggestionsServiceProxy>( + this, metrics_reporter_provider_->CreateMetricsReporter()); } } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.h b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.h index e24fa661c24..7f40448378d 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.h +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service.h @@ -15,10 +15,21 @@ #include "components/image_fetcher/core/image_fetcher.h" #include "components/keyed_service/core/keyed_service.h" #include "components/ntp_snippets/callbacks.h" +#include "components/ntp_snippets/content_suggestion.h" #include "components/ntp_snippets/contextual/contextual_suggestions_fetcher.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "services/metrics/public/cpp/ukm_source_id.h" + +namespace contextual_suggestions { +class ContextualContentSuggestionsServiceProxy; +} namespace ntp_snippets { +using contextual_suggestions::Cluster; +using contextual_suggestions::FetchClustersCallback; +using contextual_suggestions::ReportFetchMetricsCallback; + class CachedImageFetcher; class RemoteSuggestionsDatabase; @@ -31,31 +42,36 @@ class ContextualContentSuggestionsService : public KeyedService { contextual_suggestions_fetcher, std::unique_ptr<CachedImageFetcher> image_fetcher, std::unique_ptr<RemoteSuggestionsDatabase> - contextual_suggestions_database); + contextual_suggestions_database, + std::unique_ptr< + contextual_suggestions::ContextualSuggestionsMetricsReporterProvider> + metrics_reporter_provider); ~ContextualContentSuggestionsService() override; - using FetchContextualSuggestionsCallback = - base::OnceCallback<void(Status status_code, - const GURL& url, - std::vector<ContentSuggestion> suggestions)>; - // Asynchronously fetches contextual suggestions for the given URL. - void FetchContextualSuggestions(const GURL& url, - FetchContextualSuggestionsCallback callback); + virtual void FetchContextualSuggestionClusters( + const GURL& url, + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback); + + // Fetches an image pointed to by |url| and internally caches it using + // |suggestion_id|. Asynchronous if cache or network is queried. + virtual void FetchContextualSuggestionImage( + const ContentSuggestion::ID& suggestion_id, + const GURL& url, + ImageFetchedCallback callback); // Fetches an image for a given contextual suggestion ID. // Asynchronous if cache or network is queried. - void FetchContextualSuggestionImage( + virtual void FetchContextualSuggestionImageLegacy( const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback); - private: - void DidFetchContextualSuggestions( - const GURL& url, - FetchContextualSuggestionsCallback callback, - Status status, - ContextualSuggestionsFetcher::OptionalSuggestions fetched_suggestions); + std::unique_ptr< + contextual_suggestions::ContextualContentSuggestionsServiceProxy> + CreateProxy(); + private: // Cache for images of contextual suggestions, needed by CachedImageFetcher. std::unique_ptr<RemoteSuggestionsDatabase> contextual_suggestions_database_; @@ -64,6 +80,10 @@ class ContextualContentSuggestionsService : public KeyedService { std::unique_ptr<CachedImageFetcher> image_fetcher_; + std::unique_ptr< + contextual_suggestions::ContextualSuggestionsMetricsReporterProvider> + metrics_reporter_provider_; + // Look up by ContentSuggestion::ID::id_within_category() aka std::string to // get image URL. std::map<std::string, GURL> image_url_by_id_; diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.cc b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.cc new file mode 100644 index 00000000000..9f3a2e3fb66 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.cc @@ -0,0 +1,140 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.h" + +#include <utility> + +#include "base/bind.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread_task_runner_handle.h" +#include "ui/gfx/image/image.h" +#include "url/gurl.h" + +namespace contextual_suggestions { + +namespace { +// TODO(pnoland): check if this is the correct base URL for all images. +static constexpr char kImageURLFormat[] = + "http://www.google.com/images?q=tbn:%s"; + +GURL ImageUrlFromId(const std::string& image_id) { + return GURL(base::StringPrintf(kImageURLFormat, image_id.c_str())); +} +} // namespace + +ContextualContentSuggestionsServiceProxy:: + ContextualContentSuggestionsServiceProxy( + ntp_snippets::ContextualContentSuggestionsService* service, + std::unique_ptr<ContextualSuggestionsMetricsReporter> metrics_reporter) + : service_(service), + metrics_reporter_(std::move(metrics_reporter)), + last_ukm_source_id_(ukm::kInvalidSourceId), + weak_ptr_factory_(this) {} + +ContextualContentSuggestionsServiceProxy:: + ~ContextualContentSuggestionsServiceProxy() {} + +void ContextualContentSuggestionsServiceProxy::FetchContextualSuggestions( + const GURL& url, + ClustersCallback callback) { + service_->FetchContextualSuggestionClusters( + url, + base::BindOnce( + &ContextualContentSuggestionsServiceProxy::CacheSuggestions, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)), + base::BindRepeating( + &ContextualContentSuggestionsServiceProxy::ReportEvent, + weak_ptr_factory_.GetWeakPtr(), last_ukm_source_id_)); +} + +void ContextualContentSuggestionsServiceProxy::FetchContextualSuggestionImage( + const std::string& suggestion_id, + ntp_snippets::ImageFetchedCallback callback) { + auto suggestion_iter = suggestions_.find(suggestion_id); + if (suggestion_iter == suggestions_.end()) { + DVLOG(1) << "Unkown suggestion ID: " << suggestion_id; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); + return; + } + + FetchImageImpl(suggestion_iter->second.image_id, std::move(callback)); +} + +void ContextualContentSuggestionsServiceProxy::FetchContextualSuggestionFavicon( + const std::string& suggestion_id, + ntp_snippets::ImageFetchedCallback callback) { + auto suggestion_iter = suggestions_.find(suggestion_id); + if (suggestion_iter == suggestions_.end()) { + DVLOG(1) << "Unkown suggestion ID: " << suggestion_id; + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); + return; + } + + FetchImageImpl(suggestion_iter->second.favicon_image_id, std::move(callback)); +} + +void ContextualContentSuggestionsServiceProxy::ClearState() { + suggestions_.clear(); + weak_ptr_factory_.InvalidateWeakPtrs(); +} + +void ContextualContentSuggestionsServiceProxy::ReportEvent( + ukm::SourceId ukm_source_id, + ContextualSuggestionsEvent event) { + // TODO(pnoland): investigate how we can get into this state(one known + // example is if we switch tabs and there's no committed navigation in the new + // tab) and prevent it from happening. Replace the early return with a DCHECK + // once this is done. + if (ukm_source_id == ukm::kInvalidSourceId) { + return; + } + + // Flush the previous page (if any) and setup the new page. + if (ukm_source_id != last_ukm_source_id_) { + if (last_ukm_source_id_ != ukm::kInvalidSourceId) + metrics_reporter_->Flush(); + last_ukm_source_id_ = ukm_source_id; + metrics_reporter_->SetupForPage(ukm_source_id); + } + + metrics_reporter_->RecordEvent(event); +} + +void ContextualContentSuggestionsServiceProxy::FlushMetrics() { + if (last_ukm_source_id_ != ukm::kInvalidSourceId) + metrics_reporter_->Flush(); + last_ukm_source_id_ = ukm::kInvalidSourceId; +} + +void ContextualContentSuggestionsServiceProxy::FetchImageImpl( + const std::string& image_id, + ntp_snippets::ImageFetchedCallback callback) { + GURL image_url = ImageUrlFromId(image_id); + + ntp_snippets::ContentSuggestion::ID synthetic_cache_id( + ntp_snippets::Category::FromKnownCategory( + ntp_snippets::KnownCategories::CONTEXTUAL), + image_id); + + service_->FetchContextualSuggestionImage(synthetic_cache_id, image_url, + std::move(callback)); +} + +void ContextualContentSuggestionsServiceProxy::CacheSuggestions( + ClustersCallback callback, + std::string peek_text, + std::vector<Cluster> clusters) { + suggestions_.clear(); + for (auto& cluster : clusters) { + for (auto& suggestion : cluster.suggestions) { + suggestions_.emplace(std::make_pair(suggestion.id, suggestion)); + } + } + std::move(callback).Run(peek_text, std::move(clusters)); +} + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.h b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.h new file mode 100644 index 00000000000..e448659558a --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.h @@ -0,0 +1,89 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_CONTENT_SUGGESTIONS_SERVICE_PROXY_H_ +#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_CONTENT_SUGGESTIONS_SERVICE_PROXY_H_ + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "components/ntp_snippets/contextual/cluster.h" +#include "components/ntp_snippets/contextual/contextual_content_suggestions_service.h" + +class GURL; + +namespace contextual_suggestions { + +// Class providing access to the Contextual Content Suggestions Service and +// caching the list of current suggestions shown in a tab. It is owned by a UI +// object. There could be multiple instances of serivce proxy. Either can be +// torn down with a part of UI that owns it, which doesn't affect other proxies. +class ContextualContentSuggestionsServiceProxy { + public: + using ClustersCallback = ntp_snippets::FetchClustersCallback; + using Cluster = ntp_snippets::Cluster; + + ContextualContentSuggestionsServiceProxy( + ntp_snippets::ContextualContentSuggestionsService* service, + std::unique_ptr<ContextualSuggestionsMetricsReporter> metrics_reporter); + ~ContextualContentSuggestionsServiceProxy(); + + // Fetches contextual suggestions for a given |url|. + void FetchContextualSuggestions(const GURL& url, ClustersCallback callback); + + // Fetches an image for a contextual suggestion with specified + // |suggestion_id|. + void FetchContextualSuggestionImage( + const std::string& suggestion_id, + ntp_snippets::ImageFetchedCallback callback); + + // Fetches a favicon for a contextual suggestion with specified + // |suggestion_id|. + void FetchContextualSuggestionFavicon( + const std::string& suggestion_id, + ntp_snippets::ImageFetchedCallback callback); + + // Clears the state of the proxy. + void ClearState(); + + // Reports user interface event to the service. + void ReportEvent(ukm::SourceId, ContextualSuggestionsEvent event); + + // Ensures that all metrics are properly flushed. + void FlushMetrics(); + + private: + void FetchImageImpl(const std::string& image_id, + ntp_snippets::ImageFetchedCallback callback); + + void CacheSuggestions(ClustersCallback callback, + std::string peek_text, + std::vector<Cluster> clusters); + // Pointer to the service. + ntp_snippets::ContextualContentSuggestionsService* service_; + // Cache of contextual suggestions. + std::map<std::string, ntp_snippets::ContextualSuggestion> suggestions_; + + // Sink for reporting metrics for this proxy. + std::unique_ptr<contextual_suggestions::ContextualSuggestionsMetricsReporter> + metrics_reporter_; + + // The most recent SourceId in use by metrics_reporter_, or + // ukm::kInvalidSourceId. + ukm::SourceId last_ukm_source_id_; + + // Weak pointer factory to cancel pending callbacks. + base::WeakPtrFactory<ContextualContentSuggestionsServiceProxy> + weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(ContextualContentSuggestionsServiceProxy); +}; + +} // namespace contextual_suggestions + +#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_CONTENT_SUGGESTIONS_SERVICE_PROXY_H_ diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy_unittest.cc new file mode 100644 index 00000000000..24d1bafe033 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_proxy_unittest.cc @@ -0,0 +1,116 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_content_suggestions_service_proxy.h" + +#include <memory> +#include <string> + +#include "base/bind.h" +#include "components/ntp_snippets/contextual/contextual_content_suggestions_service.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "components/ntp_snippets/remote/cached_image_fetcher.h" +#include "components/ntp_snippets/remote/remote_suggestions_database.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::IsEmpty; +using testing::Pointee; + +namespace contextual_suggestions { + +using Cluster = ntp_snippets::Cluster; +using ClustersCallback = ntp_snippets::FetchClustersCallback; + +namespace { + +static const std::string kTestPeekText("Test peek test"); +static const std::string kValidFromUrl = "http://some.url"; + +class FakeContextualContentSuggestionsService + : public ntp_snippets::ContextualContentSuggestionsService { + public: + FakeContextualContentSuggestionsService(); + ~FakeContextualContentSuggestionsService() override; + + void FetchContextualSuggestionClusters( + const GURL& url, + ClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) override { + clusters_callback_ = std::move(callback); + } + + void RunClustersCallback(std::string peek_text, + std::vector<Cluster> clusters) { + std::move(clusters_callback_) + .Run(std::move(peek_text), std::move(clusters)); + } + + private: + ClustersCallback clusters_callback_; +}; + +FakeContextualContentSuggestionsService:: + FakeContextualContentSuggestionsService() + : ContextualContentSuggestionsService(nullptr, nullptr, nullptr, nullptr) {} + +FakeContextualContentSuggestionsService:: + ~FakeContextualContentSuggestionsService() {} + +// GMock does not support movable-only types (Cluster). +// Instead WrappedRun is used as callback and it redirects the call to a +// method without movable-only types, which is then mocked. +class MockClustersCallback { + public: + void WrappedRun(std::string peek_text, std::vector<Cluster> clusters) { + Run(peek_text, &clusters); + } + + ClustersCallback ToOnceCallback() { + return base::BindOnce(&MockClustersCallback::WrappedRun, + base::Unretained(this)); + } + + MOCK_METHOD2(Run, + void(const std::string& peek_text, + std::vector<Cluster>* clusters)); +}; + +} // namespace + +class ContextualContentSuggestionsServiceProxyTest : public testing::Test { + public: + void SetUp() override; + + FakeContextualContentSuggestionsService* service() { return service_.get(); } + + ContextualContentSuggestionsServiceProxy* proxy() { return proxy_.get(); } + + private: + std::unique_ptr<FakeContextualContentSuggestionsService> service_; + std::unique_ptr<ContextualContentSuggestionsServiceProxy> proxy_; +}; + +void ContextualContentSuggestionsServiceProxyTest::SetUp() { + service_ = std::make_unique<FakeContextualContentSuggestionsService>(); + auto metrics_reporter = + std::make_unique<ContextualSuggestionsMetricsReporter>(); + proxy_ = std::make_unique<ContextualContentSuggestionsServiceProxy>( + service_.get(), std::move(metrics_reporter)); +} + +TEST_F(ContextualContentSuggestionsServiceProxyTest, + FetchSuggestionsWhenEmpty) { + MockClustersCallback mock_cluster_callback; + + proxy()->FetchContextualSuggestions(GURL(kValidFromUrl), + mock_cluster_callback.ToOnceCallback()); + EXPECT_CALL(mock_cluster_callback, Run(kTestPeekText, Pointee(IsEmpty()))); + service()->RunClustersCallback(kTestPeekText, std::vector<Cluster>()); +} + +// TODO(fgorski): More tests will be added, once we have the suggestions +// restructured. + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_unittest.cc index 9c3e7dc67d4..65553517b75 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_unittest.cc +++ b/chromium/components/ntp_snippets/contextual/contextual_content_suggestions_service_unittest.cc @@ -9,6 +9,7 @@ #include <vector> #include "base/bind.h" +#include "base/bind_helpers.h" #include "base/macros.h" #include "base/message_loop/message_loop.h" #include "base/run_loop.h" @@ -18,6 +19,7 @@ #include "components/ntp_snippets/content_suggestion.h" #include "components/ntp_snippets/contextual/contextual_suggestion.h" #include "components/ntp_snippets/contextual/contextual_suggestions_fetcher.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" #include "components/ntp_snippets/remote/cached_image_fetcher.h" #include "components/ntp_snippets/remote/json_to_categories.h" #include "components/ntp_snippets/remote/remote_suggestions_database.h" @@ -28,6 +30,8 @@ #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_unittest_util.h" +using contextual_suggestions::ClusterBuilder; +using contextual_suggestions::ReportFetchMetricsCallback; using testing::_; using testing::AllOf; using testing::ElementsAre; @@ -40,37 +44,37 @@ namespace ntp_snippets { namespace { -ACTION_TEMPLATE(MoveArg, - HAS_1_TEMPLATE_PARAMS(int, k), - AND_1_VALUE_PARAMS(out)) { - *out = std::move(*::testing::get<k>(args)); +class MockClustersCallback { + public: + void Done(std::string peek_text, std::vector<Cluster> clusters) { + EXPECT_FALSE(has_run); + has_run = true; + response_peek_text = peek_text; + response_clusters = std::move(clusters); + } + + bool has_run = false; + std::string response_peek_text; + std::vector<Cluster> response_clusters; }; // Always fetches the result that was set by SetFakeResponse. class FakeContextualSuggestionsFetcher : public ContextualSuggestionsFetcher { public: - void FetchContextualSuggestions( + void FetchContextualSuggestionsClusters( const GURL& url, - SuggestionsAvailableCallback callback) override { - std::move(callback).Run(fake_status_, std::move(fake_suggestions_)); - fake_suggestions_ = base::nullopt; + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) override { + std::move(callback).Run("peek text", std::move(fake_suggestions_)); + fake_suggestions_.clear(); } - void SetFakeResponse(Status fake_status, - OptionalSuggestions fake_suggestions) { - fake_status_ = fake_status; + void SetFakeResponse(std::vector<Cluster> fake_suggestions) { fake_suggestions_ = std::move(fake_suggestions); } - const std::string& GetLastStatusForTesting() const override { return empty_; } - const std::string& GetLastJsonForTesting() const override { return empty_; } - const GURL& GetFetchUrlForTesting() const override { return empty_url_; } - private: - std::string empty_; - GURL empty_url_; - Status fake_status_ = Status::Success(); - OptionalSuggestions fake_suggestions_; + std::vector<Cluster> fake_suggestions_; }; // Always fetches a fake image if the given URL is valid. @@ -83,6 +87,7 @@ class FakeCachedImageFetcher : public CachedImageFetcher { void FetchSuggestionImage(const ContentSuggestion::ID&, const GURL& image_url, + ImageDataFetchedCallback image_data_callback, ImageFetchedCallback callback) override { gfx::Image image; if (image_url.is_valid()) { @@ -92,29 +97,6 @@ class FakeCachedImageFetcher : public CachedImageFetcher { } }; -// GMock does not support movable-only types (ContentSuggestion). -// Instead WrappedRun is used as callback and it redirects the call to a -// method without movable-only types, which is then mocked. -class MockFetchContextualSuggestionsCallback { - public: - void WrappedRun(Status status, - const GURL& url, - std::vector<ContentSuggestion> suggestions) { - Run(status, url, &suggestions); - } - - ContextualContentSuggestionsService::FetchContextualSuggestionsCallback - ToOnceCallback() { - return base::BindOnce(&MockFetchContextualSuggestionsCallback::WrappedRun, - base::Unretained(this)); - } - - MOCK_METHOD3(Run, - void(Status status_code, - const GURL& url, - std::vector<ContentSuggestion>* suggestions)); -}; - } // namespace class ContextualContentSuggestionsServiceTest : public testing::Test { @@ -124,10 +106,13 @@ class ContextualContentSuggestionsServiceTest : public testing::Test { std::unique_ptr<FakeContextualSuggestionsFetcher> fetcher = std::make_unique<FakeContextualSuggestionsFetcher>(); fetcher_ = fetcher.get(); + auto metrics_reporter_provider = std::make_unique< + contextual_suggestions::ContextualSuggestionsMetricsReporterProvider>(); source_ = std::make_unique<ContextualContentSuggestionsService>( std::move(fetcher), std::make_unique<FakeCachedImageFetcher>(&pref_service_), - std::unique_ptr<RemoteSuggestionsDatabase>()); + std::unique_ptr<RemoteSuggestionsDatabase>(), + std::move(metrics_reporter_provider)); } FakeContextualSuggestionsFetcher* fetcher() { return fetcher_; } @@ -142,55 +127,6 @@ class ContextualContentSuggestionsServiceTest : public testing::Test { DISALLOW_COPY_AND_ASSIGN(ContextualContentSuggestionsServiceTest); }; -TEST_F(ContextualContentSuggestionsServiceTest, - ShouldFetchContextualSuggestion) { - MockFetchContextualSuggestionsCallback mock_suggestions_callback; - const std::string kValidFromUrl = "http://some.url"; - const std::string kToUrl = "http://another.url"; - ContextualSuggestionsFetcher::OptionalSuggestions contextual_suggestions = - ContextualSuggestion::PtrVector(); - contextual_suggestions->push_back( - ContextualSuggestion::CreateForTesting(kToUrl, "")); - fetcher()->SetFakeResponse(Status::Success(), - std::move(contextual_suggestions)); - EXPECT_CALL(mock_suggestions_callback, - Run(Property(&Status::IsSuccess, true), GURL(kValidFromUrl), - Pointee(ElementsAre(AllOf( - Property(&ContentSuggestion::id, - Property(&ContentSuggestion::ID::category, - Category::FromKnownCategory( - KnownCategories::CONTEXTUAL))), - Property(&ContentSuggestion::url, GURL(kToUrl))))))); - source()->FetchContextualSuggestions( - GURL(kValidFromUrl), mock_suggestions_callback.ToOnceCallback()); - base::RunLoop().RunUntilIdle(); -} - -TEST_F(ContextualContentSuggestionsServiceTest, - ShouldRunCallbackOnEmptyResults) { - MockFetchContextualSuggestionsCallback mock_suggestions_callback; - const std::string kEmpty; - fetcher()->SetFakeResponse(Status::Success(), - ContextualSuggestion::PtrVector()); - EXPECT_CALL(mock_suggestions_callback, Run(Property(&Status::IsSuccess, true), - GURL(kEmpty), Pointee(IsEmpty()))); - source()->FetchContextualSuggestions( - GURL(kEmpty), mock_suggestions_callback.ToOnceCallback()); - base::RunLoop().RunUntilIdle(); -} - -TEST_F(ContextualContentSuggestionsServiceTest, ShouldRunCallbackOnError) { - MockFetchContextualSuggestionsCallback mock_suggestions_callback; - const std::string kEmpty; - fetcher()->SetFakeResponse(Status(StatusCode::TEMPORARY_ERROR, ""), - ContextualSuggestion::PtrVector()); - EXPECT_CALL(mock_suggestions_callback, - Run(Property(&Status::IsSuccess, false), GURL(kEmpty), - Pointee(IsEmpty()))); - source()->FetchContextualSuggestions( - GURL(kEmpty), mock_suggestions_callback.ToOnceCallback()); - base::RunLoop().RunUntilIdle(); -} TEST_F(ContextualContentSuggestionsServiceTest, ShouldFetchEmptyImageIfNotFound) { @@ -200,39 +136,55 @@ TEST_F(ContextualContentSuggestionsServiceTest, Category::FromKnownCategory(KnownCategories::CONTEXTUAL), kEmpty); EXPECT_CALL(mock_image_fetched_callback, Run(Property(&gfx::Image::IsEmpty, true))); - source()->FetchContextualSuggestionImage(id, - mock_image_fetched_callback.Get()); + source()->FetchContextualSuggestionImageLegacy( + id, mock_image_fetched_callback.Get()); // TODO(gaschler): Verify with a mock that the image fetcher is not called if // the id is unknown. base::RunLoop().RunUntilIdle(); } TEST_F(ContextualContentSuggestionsServiceTest, - ShouldFetchImageForPreviouslyFetchedSuggestion) { - const std::string kValidFromUrl = "http://some.url"; - const std::string kToUrl = "http://another.url"; - const std::string kValidImageUrl = "http://some.url/image.png"; - ContextualSuggestionsFetcher::OptionalSuggestions contextual_suggestions = - ContextualSuggestion::PtrVector(); - contextual_suggestions->push_back( - ContextualSuggestion::CreateForTesting(kToUrl, kValidImageUrl)); - fetcher()->SetFakeResponse(Status::Success(), - std::move(contextual_suggestions)); - MockFetchContextualSuggestionsCallback mock_suggestions_callback; - std::vector<ContentSuggestion> suggestions; - EXPECT_CALL(mock_suggestions_callback, Run(_, _, _)) - .WillOnce(MoveArg<2>(&suggestions)); - source()->FetchContextualSuggestions( - GURL(kValidFromUrl), mock_suggestions_callback.ToOnceCallback()); + ShouldFetchContextualSuggestionsClusters) { + MockClustersCallback mock_callback; + std::vector<Cluster> clusters; + GURL context_url("http://www.from.url"); + + clusters.emplace_back(ClusterBuilder("Title") + .AddSuggestion(SuggestionBuilder(context_url) + .Title("Title1") + .PublisherName("from.url") + .Snippet("Summary") + .ImageId("abc") + .Build()) + .Build()); + + fetcher()->SetFakeResponse(std::move(clusters)); + source()->FetchContextualSuggestionClusters( + context_url, + base::BindOnce(&MockClustersCallback::Done, + base::Unretained(&mock_callback)), + base::DoNothing()); base::RunLoop().RunUntilIdle(); - ASSERT_THAT(suggestions, Not(IsEmpty())); - base::MockCallback<ImageFetchedCallback> mock_image_fetched_callback; - EXPECT_CALL(mock_image_fetched_callback, - Run(Property(&gfx::Image::IsEmpty, false))); - source()->FetchContextualSuggestionImage(suggestions[0].id(), - mock_image_fetched_callback.Get()); - base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(mock_callback.has_run); +} + +TEST_F(ContextualContentSuggestionsServiceTest, ShouldRejectInvalidUrls) { + std::vector<Cluster> clusters; + for (GURL invalid_url : + {GURL("htp:/"), GURL("www.foobar"), GURL("http://127.0.0.1/"), + GURL("file://some.file"), GURL("chrome://settings"), GURL("")}) { + MockClustersCallback mock_callback; + source()->FetchContextualSuggestionClusters( + invalid_url, + base::BindOnce(&MockClustersCallback::Done, + base::Unretained(&mock_callback)), + base::DoNothing()); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(mock_callback.has_run); + EXPECT_EQ(mock_callback.response_peek_text, ""); + EXPECT_EQ(mock_callback.response_clusters.size(), 0u); + } } } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_json_request.cc b/chromium/components/ntp_snippets/contextual/contextual_json_request.cc deleted file mode 100644 index cd7d29cfd8f..00000000000 --- a/chromium/components/ntp_snippets/contextual/contextual_json_request.cc +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2016 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/ntp_snippets/contextual/contextual_json_request.h" - -#include <algorithm> -#include <utility> -#include <vector> - -#include "base/json/json_writer.h" -#include "base/strings/stringprintf.h" -#include "base/values.h" -#include "components/data_use_measurement/core/data_use_user_data.h" -#include "components/ntp_snippets/category_info.h" -#include "components/ntp_snippets/features.h" -#include "components/strings/grit/components_strings.h" -#include "components/variations/net/variations_http_headers.h" -#include "components/variations/variations_associated_data.h" -#include "net/base/load_flags.h" -#include "net/http/http_response_headers.h" -#include "net/http/http_status_code.h" -#include "net/traffic_annotation/network_traffic_annotation.h" -#include "net/url_request/url_fetcher.h" -#include "net/url_request/url_request_context_getter.h" -#include "third_party/icu/source/common/unicode/uloc.h" -#include "third_party/icu/source/common/unicode/utypes.h" - -using net::URLFetcher; -using net::URLRequestContextGetter; -using net::HttpRequestHeaders; -using net::URLRequestStatus; - -namespace ntp_snippets { - -namespace internal { - -namespace { - -const int k5xxRetries = 2; - -} // namespace - -ContextualJsonRequest::ContextualJsonRequest(const ParseJSONCallback& callback) - : parse_json_callback_(callback), weak_ptr_factory_(this) {} - -ContextualJsonRequest::~ContextualJsonRequest() { - DLOG_IF(ERROR, !request_completed_callback_.is_null()) - << "The CompletionCallback was never called!"; -} - -void ContextualJsonRequest::Start(CompletedCallback callback) { - request_completed_callback_ = std::move(callback); - url_fetcher_->Start(); -} - -std::string ContextualJsonRequest::GetResponseString() const { - std::string response; - url_fetcher_->GetResponseAsString(&response); - return response; -} - -// URLFetcherDelegate overrides -void ContextualJsonRequest::OnURLFetchComplete(const net::URLFetcher* source) { - DCHECK_EQ(url_fetcher_.get(), source); - const URLRequestStatus& status = url_fetcher_->GetStatus(); - int response = url_fetcher_->GetResponseCode(); - // TODO(gaschler): Add UMA metrics for response status code - - if (!status.is_success()) { - std::move(request_completed_callback_) - .Run(/*result=*/nullptr, FetchResult::URL_REQUEST_STATUS_ERROR, - /*error_details=*/base::StringPrintf(" %d", status.error())); - } else if (response != net::HTTP_OK) { - // TODO(jkrcal): https://crbug.com/609084 - // We need to deal with the edge case again where the auth - // token expires just before we send the request (in which case we need to - // fetch a new auth token). We should extract that into a common class - // instead of adding it to every single class that uses auth tokens. - std::move(request_completed_callback_) - .Run(/*result=*/nullptr, FetchResult::HTTP_ERROR, - /*error_details=*/base::StringPrintf(" %d", response)); - } else { - ParseJsonResponse(); - } -} - -void ContextualJsonRequest::ParseJsonResponse() { - std::string json_string; - bool stores_result_to_string = - url_fetcher_->GetResponseAsString(&json_string); - DCHECK(stores_result_to_string); - - parse_json_callback_.Run(json_string, - base::Bind(&ContextualJsonRequest::OnJsonParsed, - weak_ptr_factory_.GetWeakPtr()), - base::Bind(&ContextualJsonRequest::OnJsonError, - weak_ptr_factory_.GetWeakPtr())); -} - -void ContextualJsonRequest::OnJsonParsed(std::unique_ptr<base::Value> result) { - std::move(request_completed_callback_) - .Run(std::move(result), FetchResult::SUCCESS, - /*error_details=*/std::string()); -} - -void ContextualJsonRequest::OnJsonError(const std::string& error) { - std::string json_string; - url_fetcher_->GetResponseAsString(&json_string); - LOG(WARNING) << "Received invalid JSON (" << error << "): " << json_string; - std::move(request_completed_callback_) - .Run(/*result=*/nullptr, FetchResult::JSON_PARSE_ERROR, - /*error_details=*/base::StringPrintf(" (error %s)", error.c_str())); -} - -ContextualJsonRequest::Builder::Builder() = default; -ContextualJsonRequest::Builder::Builder(ContextualJsonRequest::Builder&&) = - default; -ContextualJsonRequest::Builder::~Builder() = default; - -std::unique_ptr<ContextualJsonRequest> ContextualJsonRequest::Builder::Build() - const { - DCHECK(url_request_context_getter_); - auto request = std::make_unique<ContextualJsonRequest>(parse_json_callback_); - std::string body = BuildBody(); - std::string headers = BuildHeaders(); - request->url_fetcher_ = BuildURLFetcher(request.get(), headers, body); - - // Log the request for debugging network issues. - VLOG(1) << "Sending a NTP snippets request to " << url_ << ":\n" - << headers << "\n" - << body; - - return request; -} - -ContextualJsonRequest::Builder& -ContextualJsonRequest::Builder::SetAuthentication( - const std::string& account_id, - const std::string& auth_header) { - auth_header_ = auth_header; - return *this; -} - -ContextualJsonRequest::Builder& -ContextualJsonRequest::Builder::SetParseJsonCallback( - ParseJSONCallback callback) { - parse_json_callback_ = callback; - return *this; -} - -ContextualJsonRequest::Builder& ContextualJsonRequest::Builder::SetUrl( - const GURL& url) { - url_ = url; - return *this; -} - -ContextualJsonRequest::Builder& -ContextualJsonRequest::Builder::SetUrlRequestContextGetter( - const scoped_refptr<net::URLRequestContextGetter>& context_getter) { - url_request_context_getter_ = context_getter; - return *this; -} - -ContextualJsonRequest::Builder& ContextualJsonRequest::Builder::SetContentUrl( - const GURL& url) { - content_url_ = url; - return *this; -} - -std::string ContextualJsonRequest::Builder::BuildHeaders() const { - net::HttpRequestHeaders headers; - headers.SetHeader("Content-Type", "application/json; charset=UTF-8"); - if (!auth_header_.empty()) { - headers.SetHeader("Authorization", auth_header_); - } - // Add X-Client-Data header with experiment IDs from field trials. - // Note: It's OK to pass SignedIn::kNo if it's unknown, as it does not affect - // transmission of experiments coming from the variations server. - variations::AppendVariationHeaders(url_, variations::InIncognito::kNo, - variations::SignedIn::kNo, &headers); - return headers.ToString(); -} - -std::string ContextualJsonRequest::Builder::BuildBody() const { - auto request = std::make_unique<base::DictionaryValue>(); - - request->SetString("url", content_url_.spec()); - auto categories = std::make_unique<base::ListValue>(); - categories->AppendString("RELATED_ARTICLES"); - categories->AppendString("PUBLIC_DEBATE"); - request->Set("categories", std::move(categories)); - - std::string request_json; - bool success = base::JSONWriter::WriteWithOptions( - *request, base::JSONWriter::OPTIONS_PRETTY_PRINT, &request_json); - DCHECK(success); - return request_json; -} - -std::unique_ptr<net::URLFetcher> -ContextualJsonRequest::Builder::BuildURLFetcher( - net::URLFetcherDelegate* delegate, - const std::string& headers, - const std::string& body) const { - net::NetworkTrafficAnnotationTag traffic_annotation = - net::DefineNetworkTrafficAnnotation("ntp_contextual_suggestions_fetch", - R"( - semantics { - sender: "New Tab Page Contextual Suggestions Fetch" - description: - "Chromium can show contextual suggestions that are related to the " - "currently visited page on the New Tab page. " - trigger: - "Triggered when Home sheet is pulled up." - data: - "Only for a white-listed signed-in test user, the URL of the " - "current tab." - destination: GOOGLE_OWNED_SERVICE - } - policy { - cookies_allowed: NO - setting: - "This feature can be disabled by the flag " - "contextual-suggestions-carousel." - chrome_policy { - NTPContentSuggestionsEnabled { - NTPContentSuggestionsEnabled: False - } - } - })"); - std::unique_ptr<net::URLFetcher> url_fetcher = net::URLFetcher::Create( - url_, net::URLFetcher::POST, delegate, traffic_annotation); - url_fetcher->SetRequestContext(url_request_context_getter_.get()); - url_fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | - net::LOAD_DO_NOT_SAVE_COOKIES); - data_use_measurement::DataUseUserData::AttachToFetcher( - url_fetcher.get(), - data_use_measurement::DataUseUserData::NTP_SNIPPETS_SUGGESTIONS); - - url_fetcher->SetExtraRequestHeaders(headers); - url_fetcher->SetUploadData("application/json", body); - - // Fetchers are sometimes cancelled because a network change was detected. - url_fetcher->SetAutomaticallyRetryOnNetworkChanges(3); - url_fetcher->SetMaxRetriesOn5xx(k5xxRetries); - return url_fetcher; -} - -} // namespace internal - -} // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_json_request.h b/chromium/components/ntp_snippets/contextual/contextual_json_request.h deleted file mode 100644 index c62ccd203cf..00000000000 --- a/chromium/components/ntp_snippets/contextual/contextual_json_request.h +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2016 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. - -#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_JSON_REQUEST_H_ -#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_JSON_REQUEST_H_ - -#include <memory> -#include <string> -#include <utility> - -#include "base/callback.h" -#include "base/memory/weak_ptr.h" -#include "components/ntp_snippets/remote/json_request.h" -#include "components/ntp_snippets/status.h" -#include "net/http/http_request_headers.h" - -namespace base { -class Value; -} // namespace base - -namespace ntp_snippets { - -namespace internal { - -// A request to query contextual suggestions. -class ContextualJsonRequest : public net::URLFetcherDelegate { - public: - // A client can expect error_details only, if there was any error during the - // fetching or parsing. In successful cases, it will be an empty string. - using CompletedCallback = - base::OnceCallback<void(std::unique_ptr<base::Value> result, - FetchResult result_code, - const std::string& error_details)>; - - // Builds authenticated and non-authenticated ContextualJsonRequests. - class Builder { - public: - Builder(); - Builder(Builder&&); - ~Builder(); - - // Builds a Request object that contains all data to fetch new snippets. - std::unique_ptr<ContextualJsonRequest> Build() const; - - Builder& SetAuthentication(const std::string& account_id, - const std::string& auth_header); - Builder& SetParseJsonCallback(ParseJSONCallback callback); - Builder& SetUrl(const GURL& url); - Builder& SetUrlRequestContextGetter( - const scoped_refptr<net::URLRequestContextGetter>& context_getter); - Builder& SetContentUrl(const GURL& url); - - // These preview methods allow to inspect the Request without exposing it - // publicly. - std::string PreviewRequestBodyForTesting() { return BuildBody(); } - std::string PreviewRequestHeadersForTesting() { return BuildHeaders(); } - - private: - std::string BuildHeaders() const; - std::string BuildBody() const; - std::unique_ptr<net::URLFetcher> BuildURLFetcher( - net::URLFetcherDelegate* request, - const std::string& headers, - const std::string& body) const; - - std::string auth_header_; - ParseJSONCallback parse_json_callback_; - GURL url_; - scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; - - // The URL for which to fetch contextual suggestions for. - GURL content_url_; - - DISALLOW_COPY_AND_ASSIGN(Builder); - }; - - ContextualJsonRequest(const ParseJSONCallback& callback); - ContextualJsonRequest(ContextualJsonRequest&&); - ~ContextualJsonRequest() override; - - void Start(CompletedCallback callback); - - std::string GetResponseString() const; - - private: - // URLFetcherDelegate implementation. - void OnURLFetchComplete(const net::URLFetcher* source) override; - - void ParseJsonResponse(); - void OnJsonParsed(std::unique_ptr<base::Value> result); - void OnJsonError(const std::string& error); - - // The fetcher for downloading the snippets. Only non-null if a fetch is - // currently ongoing. - std::unique_ptr<net::URLFetcher> url_fetcher_; - - // This callback is called to parse a json string. It contains callbacks for - // error and success cases. - ParseJSONCallback parse_json_callback_; - - // The callback to notify when URLFetcher finished and results are available. - CompletedCallback request_completed_callback_; - - base::WeakPtrFactory<ContextualJsonRequest> weak_ptr_factory_; - - DISALLOW_COPY_AND_ASSIGN(ContextualJsonRequest); -}; - -} // namespace internal - -} // namespace ntp_snippets - -#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_JSON_REQUEST_H_ diff --git a/chromium/components/ntp_snippets/contextual/contextual_json_request_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_json_request_unittest.cc deleted file mode 100644 index d501dd15656..00000000000 --- a/chromium/components/ntp_snippets/contextual/contextual_json_request_unittest.cc +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2017 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/ntp_snippets/contextual/contextual_json_request.h" - -#include <utility> - -#include "base/json/json_reader.h" -#include "base/memory/ptr_util.h" -#include "base/message_loop/message_loop.h" -#include "base/test/test_mock_time_task_runner.h" -#include "base/values.h" -#include "components/ntp_snippets/features.h" -#include "net/url_request/url_request_test_util.h" -#include "testing/gmock/include/gmock/gmock.h" -#include "testing/gtest/include/gtest/gtest.h" - -namespace ntp_snippets { - -namespace internal { - -namespace { - -using testing::_; -using testing::Eq; -using testing::StrEq; - -MATCHER_P(EqualsJSON, json, "equals JSON") { - std::unique_ptr<base::Value> expected = base::JSONReader::Read(json); - if (!expected) { - *result_listener << "INTERNAL ERROR: couldn't parse expected JSON"; - return false; - } - - std::string err_msg; - int err_line, err_col; - std::unique_ptr<base::Value> actual = base::JSONReader::ReadAndReturnError( - arg, base::JSON_PARSE_RFC, nullptr, &err_msg, &err_line, &err_col); - if (!actual) { - *result_listener << "input:" << err_line << ":" << err_col << ": " - << "parse error: " << err_msg; - return false; - } - return *expected == *actual; -} - -} // namespace - -class ContextualJsonRequestTest : public testing::Test { - public: - ContextualJsonRequestTest() - : request_context_getter_( - new net::TestURLRequestContextGetter(loop_.task_runner())) {} - - ContextualJsonRequest::Builder CreateDefaultBuilder() { - ContextualJsonRequest::Builder builder; - builder.SetUrl(GURL("http://valid-url.test")) - .SetUrlRequestContextGetter(request_context_getter_.get()); - return builder; - } - - private: - base::MessageLoop loop_; - scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; - - DISALLOW_COPY_AND_ASSIGN(ContextualJsonRequestTest); -}; - -TEST_F(ContextualJsonRequestTest, AuthenticatedRequest) { - ContextualJsonRequest::Builder builder = CreateDefaultBuilder(); - builder.SetAuthentication("0BFUSGAIA", "headerstuff") - .SetContentUrl(GURL("http://my-url.test")) - .Build(); - - EXPECT_THAT(builder.PreviewRequestHeadersForTesting(), - StrEq("Content-Type: application/json; charset=UTF-8\r\n" - "Authorization: headerstuff\r\n" - "\r\n")); - EXPECT_THAT(builder.PreviewRequestBodyForTesting(), - EqualsJSON("{" - " \"categories\": [ \"RELATED_ARTICLES\"," - " \"PUBLIC_DEBATE\" ]," - " \"url\": \"http://my-url.test/\"" - "}")); -} - -} // namespace internal - -} // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestion.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestion.cc index 546d7f13419..20cb47ca881 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestion.cc +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestion.cc @@ -4,91 +4,61 @@ #include "components/ntp_snippets/contextual/contextual_suggestion.h" -#include "base/memory/ptr_util.h" -#include "base/strings/string_number_conversions.h" -#include "base/strings/stringprintf.h" -#include "base/strings/utf_string_conversions.h" -#include "base/values.h" -#include "components/ntp_snippets/category.h" +namespace ntp_snippets { -namespace { +ContextualSuggestion::ContextualSuggestion() = default; -// dict.Get() specialization for base::Time values -bool GetTimeValue(const base::DictionaryValue& dict, - const std::string& key, - base::Time* time) { - // TODO(gaschler): Replace all usages of GetString(key, &str) by - // FindKey(key)->GetString(). - std::string time_value; - return dict.GetString(key, &time_value) && - base::Time::FromString(time_value.c_str(), time); -} +ContextualSuggestion::ContextualSuggestion(const ContextualSuggestion& other) = + default; -// dict.Get() specialization for GURL values -bool GetURLValue(const base::DictionaryValue& dict, - const std::string& key, - GURL* url) { - std::string spec; - if (!dict.GetString(key, &spec)) { - return false; - } - *url = GURL(spec); - return url->is_valid(); -} +// MSVC doesn't support defaulted move constructors, so we have to define it +// ourselves. +ContextualSuggestion::ContextualSuggestion( + ContextualSuggestion&& other) noexcept + : id(std::move(other.id)), + title(std::move(other.title)), + url(std::move(other.url)), + publisher_name(std::move(other.publisher_name)), + snippet(std::move(other.snippet)), + image_id(std::move(other.image_id)), + favicon_image_id(std::move(other.favicon_image_id)) {} -} // namespace +ContextualSuggestion::~ContextualSuggestion() = default; -namespace ntp_snippets { +SuggestionBuilder::SuggestionBuilder(const GURL& url) { + suggestion_.url = url; + suggestion_.id = url.spec(); +} -ContextualSuggestion::ContextualSuggestion(const std::string& id) : id_(id) {} +SuggestionBuilder& SuggestionBuilder::Title(const std::string& title) { + suggestion_.title = title; + return *this; +} -ContextualSuggestion::~ContextualSuggestion() = default; +SuggestionBuilder& SuggestionBuilder::PublisherName( + const std::string& publisher_name) { + suggestion_.publisher_name = publisher_name; + return *this; +} -// static -std::unique_ptr<ContextualSuggestion> -ContextualSuggestion::CreateFromDictionary(const base::DictionaryValue& dict) { - std::string id; - if (!dict.GetString("url", &id) || id.empty()) { - return nullptr; - } - auto suggestion = MakeUnique(id); - GetURLValue(dict, "url", &suggestion->url_); - if (!dict.GetString("title", &suggestion->title_)) { - dict.GetString("source", &suggestion->title_); - } - dict.GetString("snippet", &suggestion->snippet_); - GetTimeValue(dict, "creationTime", &suggestion->publish_date_); - GetURLValue(dict, "imageUrl", &suggestion->salient_image_url_); - if (!dict.GetString("attribution", &suggestion->publisher_name_)) { - dict.GetString("source", &suggestion->publisher_name_); - } - return suggestion; +SuggestionBuilder& SuggestionBuilder::Snippet(const std::string& snippet) { + suggestion_.snippet = snippet; + return *this; } -ContentSuggestion ContextualSuggestion::ToContentSuggestion() const { - ContentSuggestion suggestion( - Category::FromKnownCategory(KnownCategories::CONTEXTUAL), id_, url_); - suggestion.set_title(base::UTF8ToUTF16(title_)); - suggestion.set_snippet_text(base::UTF8ToUTF16(snippet_)); - suggestion.set_publish_date(publish_date_); - suggestion.set_publisher_name(base::UTF8ToUTF16(publisher_name_)); - return suggestion; +SuggestionBuilder& SuggestionBuilder::ImageId(const std::string& image_id) { + suggestion_.image_id = image_id; + return *this; } -// static -std::unique_ptr<ContextualSuggestion> ContextualSuggestion::CreateForTesting( - const std::string& to_url, - const std::string& image_url) { - auto suggestion = MakeUnique({to_url}); - suggestion->url_ = GURL(to_url); - suggestion->salient_image_url_ = GURL(image_url); - return suggestion; +SuggestionBuilder& SuggestionBuilder::FaviconImageId( + const std::string& favicon_image_id) { + suggestion_.favicon_image_id = favicon_image_id; + return *this; } -// static -std::unique_ptr<ContextualSuggestion> ContextualSuggestion::MakeUnique( - const std::string& id) { - return base::WrapUnique(new ContextualSuggestion(id)); +ContextualSuggestion SuggestionBuilder::Build() { + return std::move(suggestion_); } -} // namespace ntp_snippets +} // namespace ntp_snippets
\ No newline at end of file diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestion.h b/chromium/components/ntp_snippets/contextual/contextual_suggestion.h index eb48dbe8fd0..c13753653df 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestion.h +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestion.h @@ -5,75 +5,57 @@ #ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTION_H_ #define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTION_H_ -#include <cstdint> -#include <memory> -#include <string> -#include <vector> - -#include "base/macros.h" -#include "base/optional.h" -#include "base/time/time.h" -#include "components/ntp_snippets/content_suggestion.h" #include "url/gurl.h" -namespace base { -class DictionaryValue; -} // namespace base - namespace ntp_snippets { -class ContextualSuggestion { - public: - using PtrVector = std::vector<std::unique_ptr<ContextualSuggestion>>; - +// Struct containing the data for a single contextual content suggestion. +struct ContextualSuggestion { + ContextualSuggestion(); + ContextualSuggestion(const ContextualSuggestion&); + ContextualSuggestion(ContextualSuggestion&&) noexcept; ~ContextualSuggestion(); - // Creates a ContextualSuggestion from a dictionary. Returns a null pointer if - // the dictionary doesn't correspond to a valid suggestion. - static std::unique_ptr<ContextualSuggestion> CreateFromDictionary( - const base::DictionaryValue& dict); - - // Converts to general content suggestion form. - ContentSuggestion ToContentSuggestion() const; - - // The ID for identifying the suggestion. - const std::string& id() const { return id_; } + // The ID identifying the suggestion. + std::string id; // Title of the suggestion. - const std::string& title() const { return title_; } + std::string title; - // The main URL pointing to the content web page. - const GURL& url() const { return url_; } + // The URL of the suggested page. + GURL url; // The name of the content's publisher. - const std::string& publisher_name() const { return publisher_name_; } + std::string publisher_name; // Summary or relevant extract from the content. - const std::string& snippet() const { return snippet_; } + std::string snippet; - // Link to an image representative of the content. Do not fetch directly, - // but through the service, which uses caching, to avoid multiple - // network requests. - const GURL& salient_image_url() const { return salient_image_url_; } + // Identifier of the image to display alongside the suggestion. This id can + // be used to construct a URL to the image. + std::string image_id; + + // As above, but for identifying the favicon for the site the suggestion + // resides on. + std::string favicon_image_id; +}; + +// Allows compact, precise construction of a ContextualSuggestion. Its main +// purpose is to avoid using a confusing 6 param helper whose argument +// order has to be guessed at. +class SuggestionBuilder { + public: + SuggestionBuilder(const GURL& url); - static std::unique_ptr<ContextualSuggestion> CreateForTesting( - const std::string& to_url, - const std::string& image_url); + SuggestionBuilder& Title(const std::string& title); + SuggestionBuilder& PublisherName(const std::string& publisher_name); + SuggestionBuilder& Snippet(const std::string& snippet); + SuggestionBuilder& ImageId(const std::string& image_id); + SuggestionBuilder& FaviconImageId(const std::string& favicon_image_id); + ContextualSuggestion Build(); private: - ContextualSuggestion(const std::string& id); - - // std::make_unique doesn't work if the ctor is private. - static std::unique_ptr<ContextualSuggestion> MakeUnique( - const std::string& id); - - std::string id_; - std::string title_; - GURL url_; - std::string publisher_name_; - GURL salient_image_url_; - std::string snippet_; - base::Time publish_date_; + ContextualSuggestion suggestion_; }; } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.cc new file mode 100644 index 00000000000..0e42aa0b048 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.cc @@ -0,0 +1,282 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_suggestions_fetch.h" + +#include "base/base64.h" +#include "base/base64url.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "components/ntp_snippets/contextual/contextual_suggestion.h" +#include "components/ntp_snippets/contextual/proto/chrome_search_api_request_context.pb.h" +#include "components/ntp_snippets/contextual/proto/get_pivots_request.pb.h" +#include "components/ntp_snippets/contextual/proto/get_pivots_response.pb.h" +#include "components/variations/net/variations_http_headers.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "third_party/protobuf/src/google/protobuf/io/coded_stream.h" + +using base::Base64Encode; + +namespace contextual_suggestions { + +namespace { + +static constexpr char kFetchEndpoint[] = + "https://www.google.com/httpservice/web/ExploreService/GetPivots/"; + +static constexpr int kNumberOfSuggestionsToFetch = 10; +static constexpr int kMinNumberOfClusters = 1; +static constexpr int kMaxNumberOfClusters = 5; + +// Wrapper for net::UnescapeForHTML that also handles utf8<->16 conversion. +std::string Unescape(const std::string& encoded_text) { + return base::UTF16ToUTF8( + net::UnescapeForHTML(base::UTF8ToUTF16(encoded_text))); +} + +// Converts |item| to a ContextualSuggestion, un-escaping any HTML entities +// encountered. +ContextualSuggestion ItemToSuggestion(const PivotItem& item) { + PivotDocument document = item.document(); + + return SuggestionBuilder(GURL(document.url().raw_url())) + .Title(Unescape(document.title())) + .Snippet(Unescape(document.summary())) + .PublisherName(Unescape(document.site_name())) + .ImageId(document.image().id().encrypted_docid()) + .FaviconImageId(document.favicon_image().id().encrypted_docid()) + .Build(); +} + +Cluster PivotToCluster(const PivotCluster& pivot) { + ClusterBuilder cluster_builder(pivot.label().label()); + for (PivotItem item : pivot.item()) { + cluster_builder.AddSuggestion(ItemToSuggestion(item)); + } + + return cluster_builder.Build(); +} + +std::vector<Cluster> ClustersFromResponse( + const GetPivotsResponse& response_proto) { + std::vector<Cluster> clusters; + Pivots pivots = response_proto.pivots(); + + if (pivots.item_size() > 0) { + // If the first item is a cluster, we can assume they all are. + if (pivots.item()[0].has_cluster()) { + clusters.reserve(response_proto.pivots().item_size()); + for (auto item : response_proto.pivots().item()) { + if (item.has_cluster()) { + PivotCluster pivot_cluster = item.cluster(); + clusters.emplace_back(PivotToCluster(pivot_cluster)); + } + } + } else if (pivots.item()[0].has_document()) { + Cluster single_cluster; + for (auto item : pivots.item()) { + single_cluster.suggestions.emplace_back(ItemToSuggestion(item)); + } + clusters.emplace_back(std::move(single_cluster)); + } + } + + return clusters; +} + +std::string PeekTextFromResponse(const GetPivotsResponse& response_proto) { + return response_proto.pivots().peek_text().text(); +} + +const std::string SerializedPivotsRequest(const std::string& url, + const std::string& bcp_language) { + GetPivotsRequest pivot_request; + + SearchApiRequestContext* search_context = pivot_request.mutable_context(); + search_context->mutable_localization_context()->set_language_code( + bcp_language); + + GetPivotsQuery* query = pivot_request.mutable_query(); + query->add_context()->set_url(url); + + PivotDocumentParams* params = query->mutable_document_params(); + params->set_enabled(true); + params->set_num(kNumberOfSuggestionsToFetch); + params->set_enable_images(true); + params->set_image_aspect_ratio(PivotDocumentParams::SQUARE); + + PivotClusteringParams* cluster_params = query->mutable_clustering_params(); + cluster_params->set_enabled(true); + cluster_params->set_min(kMinNumberOfClusters); + cluster_params->set_max(kMaxNumberOfClusters); + + query->mutable_peek_text_params()->set_enabled(true); + + return pivot_request.SerializeAsString(); +} + +} // namespace + +ContextualSuggestionsFetch::ContextualSuggestionsFetch( + const GURL& url, + const std::string& bcp_language_code) + : url_(url), bcp_language_code_(bcp_language_code) {} + +ContextualSuggestionsFetch::~ContextualSuggestionsFetch() = default; + +// static +const std::string ContextualSuggestionsFetch::GetFetchEndpoint() { + return kFetchEndpoint; +} + +void ContextualSuggestionsFetch::Start( + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback, + const scoped_refptr<network::SharedURLLoaderFactory>& loader_factory) { + request_completed_callback_ = std::move(callback); + url_loader_ = MakeURLLoader(); + url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( + loader_factory.get(), + base::BindOnce(&ContextualSuggestionsFetch::OnURLLoaderComplete, + base::Unretained(this), std::move(metrics_callback))); +} + +std::unique_ptr<network::SimpleURLLoader> +ContextualSuggestionsFetch::MakeURLLoader() const { + // TODO(pnoland, https://crbug.com/831693): Update this once there's an + // opt-out setting. + net::NetworkTrafficAnnotationTag traffic_annotation = + net::DefineNetworkTrafficAnnotation("ntp_contextual_suggestions_fetch", + R"( + semantics { + sender: "Contextual Suggestions Fetch" + description: + "Chromium can show contextual suggestions that are related to the " + "currently visited page." + trigger: + "Triggered when a page is visited and stayed on for more than a " + "few seconds." + data: + "The URL of the current tab and the ui language." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: + "This feature can be disabled by the flag " + "enable-contextual-suggestions-bottom-sheet." + policy_exception_justification: "Not implemented. The feature is " + "currently Android-only and disabled for all enterprise users. " + "A policy will be added before enabling for enterprise users." + })"); + + auto resource_request = std::make_unique<network::ResourceRequest>(); + + resource_request->url = GURL(GetFetchEndpoint()); + // TODO(pnoland): include cookies once the suggestions endpoint can make use + // of them. + resource_request->load_flags = + net::LOAD_BYPASS_CACHE | net::LOAD_DO_NOT_SAVE_COOKIES | + net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SEND_AUTH_DATA; + resource_request->headers = MakeHeaders(); + resource_request->method = "GET"; + + auto simple_loader = network::SimpleURLLoader::Create( + std::move(resource_request), traffic_annotation); + simple_loader->SetAllowHttpErrorResults(true); + return simple_loader; +} + +net::HttpRequestHeaders ContextualSuggestionsFetch::MakeHeaders() const { + net::HttpRequestHeaders headers; + std::string serialized_request_body = + SerializedPivotsRequest(url_.spec(), bcp_language_code_); + std::string base64_encoded_body; + base::Base64UrlEncode(serialized_request_body, + base::Base64UrlEncodePolicy::INCLUDE_PADDING, + &base64_encoded_body); + headers.SetHeader("X-Protobuffer-Request-Payload", base64_encoded_body); + variations::AppendVariationHeaders(url_, variations::InIncognito::kNo, + variations::SignedIn::kNo, &headers); + + UMA_HISTOGRAM_COUNTS_1M( + "ContextualSuggestions.FetchRequestProtoSizeKB", + static_cast<int>(base64_encoded_body.length() / 1024)); + + return headers; +} + +void ContextualSuggestionsFetch::OnURLLoaderComplete( + ReportFetchMetricsCallback metrics_callback, + std::unique_ptr<std::string> result) { + std::vector<Cluster> clusters; + std::string peek_text; + + int32_t response_code = 0; + int32_t error_code = url_loader_->NetError(); + if (result) { + response_code = url_loader_->ResponseInfo()->headers->response_code(); + + if (response_code == net::HTTP_OK) { + // The response comes in the format (length, bytes) where length is a + // varint32 encoded int. Rather than hand-rolling logic to skip the + // length(which we don't actually need), we use EncodedInputStream to + // skip past it, then parse the remainder of the stream. + google::protobuf::io::CodedInputStream coded_stream( + reinterpret_cast<const uint8_t*>(result->data()), result->length()); + uint32_t response_size; + if (coded_stream.ReadVarint32(&response_size) && response_size != 0) { + GetPivotsResponse response_proto; + if (response_proto.MergePartialFromCodedStream(&coded_stream)) { + clusters = ClustersFromResponse(response_proto); + peek_text = PeekTextFromResponse(response_proto); + } + } + } + + UMA_HISTOGRAM_COUNTS_1M("ContextualSuggestions.FetchResponseSizeKB", + static_cast<int>(result->length() / 1024)); + } + + ReportFetchMetrics(error_code, response_code, clusters.size(), + std::move(metrics_callback)); + + std::move(request_completed_callback_).Run(peek_text, std::move(clusters)); +} + +void ContextualSuggestionsFetch::ReportFetchMetrics( + int32_t error_code, + int32_t response_code, + size_t clusters_size, + ReportFetchMetricsCallback metrics_callback) { + ContextualSuggestionsEvent event; + if (error_code != net::OK) { + event = FETCH_ERROR; + } else if (response_code == net::HTTP_SERVICE_UNAVAILABLE) { + event = FETCH_SERVER_BUSY; + } else if (response_code != net::HTTP_OK) { + event = FETCH_ERROR; + } else if (clusters_size == 0) { + event = FETCH_EMPTY; + } else { + event = FETCH_COMPLETED; + } + + base::UmaHistogramSparse("ContextualSuggestions.FetchErrorCode", error_code); + if (response_code > 0) { + base::UmaHistogramSparse("ContextualSuggestions.FetchResponseCode", + response_code); + } + + std::move(metrics_callback).Run(event); +} + +} // namespace contextual_suggestions
\ No newline at end of file diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.h b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.h new file mode 100644 index 00000000000..7ac12818187 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetch.h @@ -0,0 +1,71 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_FETCH_H_ +#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_FETCH_H_ + +#include <memory> +#include <string> +#include <utility> + +#include "base/callback.h" +#include "components/ntp_snippets/contextual/cluster.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "net/http/http_request_headers.h" +#include "url/gurl.h" + +namespace network { +class SimpleURLLoader; +class SharedURLLoaderFactory; +} // namespace network + +namespace contextual_suggestions { + +// A fetch of contextual suggestions. This encapsulates the request-response +// lifecycle. It is also responsible for building and serializing the request +// body protos and parsing the response body protos. +class ContextualSuggestionsFetch { + public: + ContextualSuggestionsFetch(const GURL& url, const std::string& bcp_language); + ~ContextualSuggestionsFetch(); + + // Get the url used to fetch suggestions. + static const std::string GetFetchEndpoint(); + + // Start fetching suggestions using |loader_factory| to construct a + // URLLoader, and calling |callback| when finished. + void Start( + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback, + const scoped_refptr<network::SharedURLLoaderFactory>& loader_factory); + + private: + std::unique_ptr<network::SimpleURLLoader> MakeURLLoader() const; + net::HttpRequestHeaders MakeHeaders() const; + void OnURLLoaderComplete(ReportFetchMetricsCallback metrics_callback, + std::unique_ptr<std::string> result); + void ReportFetchMetrics(int32_t error_code, + int32_t response_code, + size_t clusters_size, + ReportFetchMetricsCallback metrics_callback); + + // The url for which we're fetching suggestions. + const GURL url_; + + // Identifier for the spoken language in BCP47 format. + const std::string bcp_language_code_; + + // The loader for downloading the suggestions. Only non-null if a fetch is + // currently ongoing. + std::unique_ptr<network::SimpleURLLoader> url_loader_; + + // The callback to notify when results are available. + FetchClustersCallback request_completed_callback_; + + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsFetch); +}; + +} // namespace contextual_suggestions + +#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_FETCH_H_ diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher.h b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher.h index ea408067cd1..8fbdcf2f767 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher.h +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher.h @@ -5,38 +5,27 @@ #ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_FETCHER_H_ #define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_FETCHER_H_ -#include <memory> -#include <string> -#include <vector> - -#include "base/callback.h" -#include "base/optional.h" -#include "components/ntp_snippets/category.h" -#include "components/ntp_snippets/category_info.h" +#include "components/ntp_snippets/contextual/cluster.h" #include "components/ntp_snippets/contextual/contextual_suggestion.h" -#include "components/ntp_snippets/status.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" + +using contextual_suggestions::FetchClustersCallback; +using contextual_suggestions::ReportFetchMetricsCallback; namespace ntp_snippets { // Fetches contextual suggestions from the server. class ContextualSuggestionsFetcher { public: - using OptionalSuggestions = base::Optional<ContextualSuggestion::PtrVector>; - - // If fetching fails, the optional will be empty. - using SuggestionsAvailableCallback = - base::OnceCallback<void(Status status, - OptionalSuggestions fetched_suggestions)>; virtual ~ContextualSuggestionsFetcher() = default; - virtual void FetchContextualSuggestions( + // Fetch clusters of suggestions for |url|, calling back to |callback| when + // complete. + virtual void FetchContextualSuggestionsClusters( const GURL& url, - SuggestionsAvailableCallback callback) = 0; - - virtual const std::string& GetLastStatusForTesting() const = 0; - virtual const std::string& GetLastJsonForTesting() const = 0; - virtual const GURL& GetFetchUrlForTesting() const = 0; + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) = 0; }; } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc index 7eb6228cb67..bdafe0b669f 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.cc @@ -4,302 +4,44 @@ #include "components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h" -#include "base/strings/stringprintf.h" -#include "base/strings/utf_string_conversions.h" -#include "base/time/default_clock.h" -#include "base/time/time.h" -#include "base/values.h" -#include "components/ntp_snippets/category.h" -#include "components/strings/grit/components_strings.h" -#include "net/url_request/url_fetcher.h" -#include "net/url_request/url_request_status.h" -#include "services/identity/public/cpp/identity_manager.h" -#include "ui/base/l10n/l10n_util.h" - -using net::HttpRequestHeaders; -using net::URLFetcher; -using net::URLRequestContextGetter; +#include "services/network/public/cpp/shared_url_loader_factory.h" namespace ntp_snippets { -using internal::ContextualJsonRequest; -using internal::FetchResult; - -namespace { - -const char kContentSuggestionsApiScope[] = - "https://www.googleapis.com/auth/chrome-content-suggestions"; -const char kAuthorizationRequestHeaderFormat[] = "Bearer %s"; - -std::string FetchResultToString(FetchResult result) { - switch (result) { - case FetchResult::SUCCESS: - return "OK"; - case FetchResult::URL_REQUEST_STATUS_ERROR: - return "URLRequestStatus error"; - case FetchResult::HTTP_ERROR: - return "HTTP error"; - case FetchResult::JSON_PARSE_ERROR: - return "Received invalid JSON"; - case FetchResult::INVALID_SNIPPET_CONTENT_ERROR: - return "Invalid / empty list."; - case FetchResult::OAUTH_TOKEN_ERROR: - return "Error in obtaining an OAuth2 access token."; - case FetchResult::MISSING_API_KEY: - return "No API key available."; - case FetchResult::RESULT_MAX: - break; - } - NOTREACHED(); - return "Unknown error"; -} - -Status FetchResultToStatus(FetchResult result) { - switch (result) { - case FetchResult::SUCCESS: - return Status::Success(); - // Permanent errors occur if it is more likely that the error originated - // from the client. - case FetchResult::OAUTH_TOKEN_ERROR: - case FetchResult::MISSING_API_KEY: - return Status(StatusCode::PERMANENT_ERROR, FetchResultToString(result)); - // Temporary errors occur if it's more likely that the client behaved - // correctly but the server failed to respond as expected. - // TODO(fhorschig): Revisit HTTP_ERROR once the rescheduling was reworked. - case FetchResult::HTTP_ERROR: - case FetchResult::URL_REQUEST_STATUS_ERROR: - case FetchResult::INVALID_SNIPPET_CONTENT_ERROR: - case FetchResult::JSON_PARSE_ERROR: - return Status(StatusCode::TEMPORARY_ERROR, FetchResultToString(result)); - case FetchResult::RESULT_MAX: - break; - } - NOTREACHED(); - return Status(StatusCode::PERMANENT_ERROR, std::string()); -} - -std::string GetFetchEndpoint() { - return "https://alpha-chromecontentsuggestions-pa.sandbox.googleapis.com/v1" - "/publicdebate" - "/getsuggestions"; -} - -// Creates suggestions from dictionary values in |list| and adds them to -// |suggestions|. Returns true on success, false if anything went wrong. -bool AddSuggestionsFromListValue(bool content_suggestions_api, - const base::ListValue& list, - ContextualSuggestion::PtrVector* suggestions) { - for (const auto& value : list) { - const base::DictionaryValue* dict = nullptr; - if (!value.GetAsDictionary(&dict)) { - return false; - } - - if (DLOG_IS_ON(INFO)) { - std::string s; - dict->GetAsString(&s); - DVLOG(1) << "AddSuggestionsFromListValue " << s; - } - std::unique_ptr<ContextualSuggestion> suggestion = - ContextualSuggestion::CreateFromDictionary(*dict); - suggestions->push_back(std::move(suggestion)); - } - return true; -} - -} // namespace - ContextualSuggestionsFetcherImpl::ContextualSuggestionsFetcherImpl( - identity::IdentityManager* identity_manager, - scoped_refptr<URLRequestContextGetter> url_request_context_getter, - PrefService* pref_service, - const ParseJSONCallback& parse_json_callback) - : identity_manager_(identity_manager), - url_request_context_getter_(std::move(url_request_context_getter)), - parse_json_callback_(parse_json_callback), - fetch_url_(GetFetchEndpoint()) {} + const scoped_refptr<network::SharedURLLoaderFactory>& loader_factory, + const std::string& application_language_code) + : loader_factory_(loader_factory), + bcp_language_code_(application_language_code) {} ContextualSuggestionsFetcherImpl::~ContextualSuggestionsFetcherImpl() = default; -const std::string& ContextualSuggestionsFetcherImpl::GetLastStatusForTesting() - const { - return last_status_; -} -const std::string& ContextualSuggestionsFetcherImpl::GetLastJsonForTesting() - const { - return last_fetch_json_; -} -const GURL& ContextualSuggestionsFetcherImpl::GetFetchUrlForTesting() const { - return fetch_url_; -} - -void ContextualSuggestionsFetcherImpl::FetchContextualSuggestions( +void ContextualSuggestionsFetcherImpl::FetchContextualSuggestionsClusters( const GURL& url, - SuggestionsAvailableCallback callback) { - ContextualJsonRequest::Builder builder; - builder.SetParseJsonCallback(parse_json_callback_) - .SetUrlRequestContextGetter(url_request_context_getter_) - .SetContentUrl(url); - - pending_requests_.emplace(std::move(builder), std::move(callback)); - StartTokenRequest(); -} - -void ContextualSuggestionsFetcherImpl::StartRequest( - ContextualJsonRequest::Builder builder, - SuggestionsAvailableCallback callback, - const std::string& oauth_access_token) { - builder.SetUrl(fetch_url_) - .SetAuthentication(identity_manager_->GetPrimaryAccountInfo().account_id, - base::StringPrintf(kAuthorizationRequestHeaderFormat, - oauth_access_token.c_str())); - DVLOG(1) << "ContextualSuggestionsFetcherImpl::StartRequest"; - std::unique_ptr<ContextualJsonRequest> request = builder.Build(); - ContextualJsonRequest* raw_request = request.get(); - raw_request->Start(base::BindOnce( - &ContextualSuggestionsFetcherImpl::JsonRequestDone, - base::Unretained(this), std::move(request), std::move(callback))); -} + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) { + auto fetch = + std::make_unique<ContextualSuggestionsFetch>(url, bcp_language_code_); + ContextualSuggestionsFetch* fetch_unowned = fetch.get(); + pending_requests_.emplace(std::move(fetch)); -void ContextualSuggestionsFetcherImpl::StartTokenRequest() { - // If there is already an ongoing token request, just wait for that. - if (token_fetcher_) { - return; - } - - OAuth2TokenService::ScopeSet scopes{kContentSuggestionsApiScope}; - token_fetcher_ = identity_manager_->CreateAccessTokenFetcherForPrimaryAccount( - "ntp_snippets", scopes, - base::BindOnce( - &ContextualSuggestionsFetcherImpl::AccessTokenFetchFinished, - base::Unretained(this)), - identity::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable); -} - -void ContextualSuggestionsFetcherImpl::AccessTokenFetchFinished( - const GoogleServiceAuthError& error, - const std::string& access_token) { - // Delete the fetcher only after we leave this method (which is called from - // the fetcher itself). - DCHECK(token_fetcher_); - std::unique_ptr<identity::PrimaryAccountAccessTokenFetcher> - token_fetcher_deleter(std::move(token_fetcher_)); - - if (error.state() != GoogleServiceAuthError::NONE) { - AccessTokenError(error); - return; - } - - DCHECK(!access_token.empty()); - - while (!pending_requests_.empty()) { - std::pair<ContextualJsonRequest::Builder, SuggestionsAvailableCallback> - builder_and_callback = std::move(pending_requests_.front()); - pending_requests_.pop(); - StartRequest(std::move(builder_and_callback.first), - std::move(builder_and_callback.second), access_token); - } -} - -void ContextualSuggestionsFetcherImpl::AccessTokenError( - const GoogleServiceAuthError& error) { - DCHECK_NE(error.state(), GoogleServiceAuthError::NONE); - - LOG(WARNING) << "ContextualSuggestionsFetcherImpl::AccessTokenError " - "Unable to get token: " - << error.ToString(); - - while (!pending_requests_.empty()) { - std::pair<ContextualJsonRequest::Builder, SuggestionsAvailableCallback> - builder_and_callback = std::move(pending_requests_.front()); - - FetchFinished(OptionalSuggestions(), std::move(builder_and_callback.second), - FetchResult::OAUTH_TOKEN_ERROR, - /*error_details=*/ - base::StringPrintf(" (%s)", error.ToString().c_str())); - pending_requests_.pop(); - } -} - -void ContextualSuggestionsFetcherImpl::JsonRequestDone( - std::unique_ptr<ContextualJsonRequest> request, - SuggestionsAvailableCallback callback, - std::unique_ptr<base::Value> result, - FetchResult status_code, - const std::string& error_details) { - DCHECK(request); - - DVLOG(1) << "ContextualSuggestionsFetcherImpl::JsonRequestDone status_code=" - << static_cast<int>(status_code) - << " error_details=" << error_details; - last_fetch_json_ = request->GetResponseString(); - - // TODO(gaschler): Add UMA metrics for fetch duration of the request - - if (!result) { - FetchFinished(OptionalSuggestions(), std::move(callback), status_code, - error_details); - return; - } - - OptionalSuggestions optional_suggestions = ContextualSuggestion::PtrVector(); - if (!JsonToSuggestions(*result, &optional_suggestions.value())) { - DLOG(WARNING) << "Received invalid suggestions: " << last_fetch_json_; - FetchFinished(OptionalSuggestions(), std::move(callback), - FetchResult::INVALID_SNIPPET_CONTENT_ERROR, std::string()); - return; - } - - FetchFinished(std::move(optional_suggestions), std::move(callback), - FetchResult::SUCCESS, std::string()); + FetchClustersCallback internal_callback = base::BindOnce( + &ContextualSuggestionsFetcherImpl::FetchFinished, base::Unretained(this), + fetch_unowned, std::move(callback)); + fetch_unowned->Start(std::move(internal_callback), + std::move(metrics_callback), loader_factory_); } void ContextualSuggestionsFetcherImpl::FetchFinished( - OptionalSuggestions optional_suggestions, - SuggestionsAvailableCallback callback, - FetchResult fetch_result, - const std::string& error_details) { - DCHECK(fetch_result == FetchResult::SUCCESS || - !optional_suggestions.has_value()); - - last_status_ = FetchResultToString(fetch_result) + error_details; - - DVLOG(1) << "Fetch finished: " << last_status_; - - std::move(callback).Run(FetchResultToStatus(fetch_result), - std::move(optional_suggestions)); -} - -bool ContextualSuggestionsFetcherImpl::JsonToSuggestions( - const base::Value& parsed, - ContextualSuggestion::PtrVector* suggestions) { - const base::DictionaryValue* top_dict = nullptr; - if (!parsed.GetAsDictionary(&top_dict)) { - return false; - } - - const base::ListValue* categories_value = nullptr; - if (!top_dict->GetList("categories", &categories_value)) { - return false; - } - - for (const auto& v : *categories_value) { - const base::DictionaryValue* category_value = nullptr; - if (!(v.GetAsDictionary(&category_value))) { - return false; - } - - const base::ListValue* suggestions_list = nullptr; - // Absence of a list of suggestions is treated as an empty list, which - // is permissible. - if (category_value->GetList("suggestions", &suggestions_list)) { - if (!AddSuggestionsFromListValue(true, *suggestions_list, suggestions)) { - return false; - } - } - } - - return true; + ContextualSuggestionsFetch* fetch, + FetchClustersCallback callback, + std::string peek_text, + std::vector<Cluster> clusters) { + auto fetch_iterator = pending_requests_.find(fetch); + CHECK(fetch_iterator != pending_requests_.end()); + pending_requests_.erase(fetch_iterator); + + std::move(callback).Run(peek_text, std::move(clusters)); } } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h index 3a7a7fc236f..bb1041b9464 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl.h @@ -10,93 +10,49 @@ #include <vector> #include "base/callback.h" -#include "base/containers/queue.h" -#include "base/optional.h" -#include "base/time/clock.h" -#include "components/ntp_snippets/category.h" -#include "components/ntp_snippets/category_info.h" -#include "components/ntp_snippets/contextual/contextual_json_request.h" +#include "base/containers/flat_set.h" +#include "base/containers/unique_ptr_adapters.h" #include "components/ntp_snippets/contextual/contextual_suggestion.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_fetch.h" #include "components/ntp_snippets/contextual/contextual_suggestions_fetcher.h" -#include "components/ntp_snippets/status.h" -#include "net/url_request/url_request_context_getter.h" -#include "services/identity/public/cpp/primary_account_access_token_fetcher.h" -class PrefService; +namespace network { +class SharedURLLoaderFactory; +} // namespace network -namespace identity { -class IdentityManager; -class PrimaryAccountAccessTokenFetcher; -} +using contextual_suggestions::Cluster; +using contextual_suggestions::ContextualSuggestionsFetch; namespace ntp_snippets { -// TODO(gaschler): Move authentication that is in common with -// RemoteSuggestionsFetcher to a helper class. class ContextualSuggestionsFetcherImpl : public ContextualSuggestionsFetcher { public: ContextualSuggestionsFetcherImpl( - identity::IdentityManager* identity_manager, - scoped_refptr<net::URLRequestContextGetter> url_request_context_getter, - PrefService* pref_service, - const ParseJSONCallback& parse_json_callback); + const scoped_refptr<network::SharedURLLoaderFactory>& loader_factory, + const std::string& application_language_code); ~ContextualSuggestionsFetcherImpl() override; // ContextualSuggestionsFetcher implementation. - void FetchContextualSuggestions( + void FetchContextualSuggestionsClusters( const GURL& url, - SuggestionsAvailableCallback callback) override; - - const std::string& GetLastStatusForTesting() const override; - const std::string& GetLastJsonForTesting() const override; - const GURL& GetFetchUrlForTesting() const override; + FetchClustersCallback callback, + ReportFetchMetricsCallback metrics_callback) override; private: - void StartRequest(internal::ContextualJsonRequest::Builder builder, - SuggestionsAvailableCallback callback, - const std::string& oauth_access_token); - - void StartTokenRequest(); - - void AccessTokenFetchFinished(const GoogleServiceAuthError& error, - const std::string& access_token); - void AccessTokenError(const GoogleServiceAuthError& error); - - void JsonRequestDone(std::unique_ptr<internal::ContextualJsonRequest> request, - SuggestionsAvailableCallback callback, - std::unique_ptr<base::Value> result, - internal::FetchResult status_code, - const std::string& error_details); - void FetchFinished(OptionalSuggestions optional_suggestions, - SuggestionsAvailableCallback callback, - internal::FetchResult status_code, - const std::string& error_details); - - bool JsonToSuggestions(const base::Value& parsed, - ContextualSuggestion::PtrVector* suggestions); - - // Authentication for signed-in users. - identity::IdentityManager* identity_manager_; - - std::unique_ptr<identity::PrimaryAccountAccessTokenFetcher> token_fetcher_; - - // Holds the URL request context. - scoped_refptr<net::URLRequestContextGetter> url_request_context_getter_; - - // Stores requests that wait for an access token. - base::queue<std::pair<internal::ContextualJsonRequest::Builder, - SuggestionsAvailableCallback>> + void FetchFinished(ContextualSuggestionsFetch* fetch, + FetchClustersCallback callback, + std::string peek_text, + std::vector<Cluster> clusters); + + const scoped_refptr<network::SharedURLLoaderFactory> loader_factory_; + /// BCP47 formatted language code to use. + const std::string bcp_language_code_; + + // Stores requests that are inflight. + base::flat_set<std::unique_ptr<ContextualSuggestionsFetch>, + base::UniquePtrComparator> pending_requests_; - const ParseJSONCallback parse_json_callback_; - - // API endpoint for fetching contextual suggestions. - const GURL fetch_url_; - - // Info on the last finished fetch. - std::string last_status_; - std::string last_fetch_json_; - DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsFetcherImpl); }; diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl_unittest.cc index 77ba581069d..b9de9cde2d5 100644 --- a/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl_unittest.cc +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_fetcher_impl_unittest.cc @@ -7,96 +7,195 @@ #include <utility> #include <vector> -#include "base/json/json_reader.h" -#include "base/optional.h" -#include "base/test/test_mock_time_task_runner.h" -#include "base/threading/thread_task_runner_handle.h" -#include "base/values.h" -#include "components/ntp_snippets/category.h" -#include "components/ntp_snippets/remote/test_utils.h" -#include "components/prefs/testing_pref_service.h" -#include "net/url_request/test_url_fetcher_factory.h" -#include "net/url_request/url_request_test_util.h" -#include "services/identity/public/cpp/identity_test_environment.h" +#include "base/base64.h" +#include "base/bind_helpers.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/test/bind_test_util.h" +#include "base/test/histogram_tester.h" +#include "base/test/scoped_task_environment.h" +#include "components/ntp_snippets/contextual/proto/chrome_search_api_request_context.pb.h" +#include "components/ntp_snippets/contextual/proto/get_pivots_request.pb.h" +#include "components/ntp_snippets/contextual/proto/get_pivots_response.pb.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/url_loader_completion_status.h" +#include "services/network/test/test_url_loader_factory.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" namespace ntp_snippets { +using contextual_suggestions::ClusterBuilder; +using contextual_suggestions::ContextualSuggestionsEvent; +using contextual_suggestions::ExploreContext; +using contextual_suggestions::GetPivotsQuery; +using contextual_suggestions::GetPivotsRequest; +using contextual_suggestions::GetPivotsResponse; +using contextual_suggestions::ImageId; +using contextual_suggestions::PivotCluster; +using contextual_suggestions::PivotClusteringParams; +using contextual_suggestions::PivotDocument; +using contextual_suggestions::PivotDocumentParams; +using contextual_suggestions::PivotItem; +using contextual_suggestions::Pivots; +using network::SharedURLLoaderFactory; +using network::TestURLLoaderFactory; +using testing::ElementsAre; + namespace { -using testing::_; -using testing::AllOf; -using testing::Eq; -using testing::Field; -using testing::IsEmpty; -using testing::Property; -using testing::StartsWith; - -const char kTestEmail[] = "foo@bar.com"; -const char kValidURL[] = "http://valid-url.test"; - -MATCHER(IsEmptySuggestionsList, "is an empty list of suggestions") { - ContextualSuggestionsFetcher::OptionalSuggestions& optional_suggestions = - *arg; - if (!optional_suggestions) { - *result_listener << "expected a suggestion list."; - return false; - } - if (optional_suggestions->size() != 0) { - *result_listener << "expected empty suggestions, got: " - << optional_suggestions->size(); - return false; +class MockClustersCallback { + public: + void Done(std::string peek_text, std::vector<Cluster> clusters) { + EXPECT_FALSE(has_run); + has_run = true; + response_peek_text = peek_text; + response_clusters = std::move(clusters); } - return true; -} -MATCHER_P(IsSingleSuggestion, - url, - "is a list with the single suggestion %(url)s") { - ContextualSuggestionsFetcher::OptionalSuggestions& optional_suggestions = - *arg; - if (!optional_suggestions) { - *result_listener << "expected a suggestion list."; - return false; + bool has_run = false; + std::string response_peek_text; + std::vector<Cluster> response_clusters; +}; + +class MockMetricsCallback { + public: + void Report(ContextualSuggestionsEvent event) { events.push_back(event); } + + std::vector<ContextualSuggestionsEvent> events; +}; + +// TODO(pnoland): de-dupe this and the identical class in +// feed_networking_host_unittest.cc +class TestSharedURLLoaderFactory : public SharedURLLoaderFactory { + public: + void CreateLoaderAndStart(network::mojom::URLLoaderRequest request, + int32_t routing_id, + int32_t request_id, + uint32_t options, + const network::ResourceRequest& url_request, + network::mojom::URLLoaderClientPtr client, + const net::MutableNetworkTrafficAnnotationTag& + traffic_annotation) override { + test_factory_.CreateLoaderAndStart(std::move(request), routing_id, + request_id, options, url_request, + std::move(client), traffic_annotation); } - if (optional_suggestions->size() != 1) { - *result_listener << "expected single suggestion, got: " - << optional_suggestions->size(); - return false; + + std::unique_ptr<network::SharedURLLoaderFactoryInfo> Clone() override { + NOTREACHED(); + return nullptr; } - if (optional_suggestions.value()[0]->url().spec() != url) { - *result_listener << "unexpected url, got: " - << optional_suggestions.value()[0]->url().spec(); - return false; + + TestURLLoaderFactory* test_factory() { return &test_factory_; } + + protected: + ~TestSharedURLLoaderFactory() override = default; + + private: + TestURLLoaderFactory test_factory_; +}; + +Cluster DefaultCluster() { + return ClusterBuilder("Articles") + .AddSuggestion(SuggestionBuilder(GURL("http://www.foobar.com")) + .Title("Title") + .PublisherName("cnn.com") + .Snippet("Summary") + .ImageId("abcdef") + .Build()) + .Build(); +} + +// Returns a single cluster with a single suggestion with preset values. +std::vector<Cluster> DefaultClusters() { + std::vector<Cluster> clusters; + + clusters.emplace_back(DefaultCluster()); + return clusters; +} + +void PopulateDocument(PivotDocument* document, + const ContextualSuggestion& suggestion) { + document->mutable_url()->set_raw_url(suggestion.url.spec()); + document->set_title(suggestion.title); + document->set_summary(suggestion.snippet); + document->set_site_name(suggestion.publisher_name); + + ImageId* image_id = document->mutable_image()->mutable_id(); + image_id->set_encrypted_docid(suggestion.image_id); +} + +// Populates a GetPivotsResponse proto using |peek_text| and |clusters| and +// returns that proto as a serialized string. +std::string SerializedResponseProto(const std::string& peek_text, + std::vector<Cluster> clusters) { + GetPivotsResponse response_proto; + Pivots* pivots = response_proto.mutable_pivots(); + pivots->mutable_peek_text()->set_text(peek_text); + + for (const auto& cluster : clusters) { + PivotItem* root_item = pivots->add_item(); + PivotCluster* pivot_cluster = root_item->mutable_cluster(); + pivot_cluster->mutable_label()->set_label(cluster.title); + for (const ContextualSuggestion& suggestion : cluster.suggestions) { + PopulateDocument(pivot_cluster->add_item()->mutable_document(), + suggestion); + } } - return true; + + // The fetch parsing logic expects the response to come as (length, bytes) + // where length is varint32 encoded, but ignores the actual length read. + // " " is a valid varint32(32) so this works for now. + // TODO(pnoland): Use a CodedOutputStream to prepend the actual size so that + // we're not relying on implementation details. + return " " + response_proto.SerializeAsString(); } -class MockSuggestionsAvailableCallback { - public: - // Workaround for gMock's lack of support for movable arguments. - void WrappedRun( - Status status, - ContextualSuggestionsFetcher::OptionalSuggestions optional_suggestions) { - Run(status, &optional_suggestions); +// Populates a GetPivotsResponse proto with a single, flat list of suggestions +// from |single_cluster| and returns that proto as a serialized string. +std::string SerializedResponseProto(const std::string& peek_text, + Cluster single_cluster) { + GetPivotsResponse response_proto; + Pivots* pivots = response_proto.mutable_pivots(); + pivots->mutable_peek_text()->set_text(peek_text); + + for (const ContextualSuggestion& suggestion : single_cluster.suggestions) { + PopulateDocument(pivots->add_item()->mutable_document(), suggestion); } - MOCK_METHOD2(Run, - void(Status status, - ContextualSuggestionsFetcher::OptionalSuggestions* - optional_suggestions)); -}; + // See explanation above for why we prepend " ". + return " " + response_proto.SerializeAsString(); +} + +void ExpectSuggestionsMatch(ContextualSuggestion actual, + ContextualSuggestion expected) { + EXPECT_EQ(actual.id, expected.id); + EXPECT_EQ(actual.title, expected.title); + EXPECT_EQ(actual.url, expected.url); + EXPECT_EQ(actual.snippet, expected.snippet); + EXPECT_EQ(actual.publisher_name, expected.publisher_name); + EXPECT_EQ(actual.image_id, expected.image_id); + EXPECT_EQ(actual.favicon_image_id, expected.favicon_image_id); +} + +void ExpectClustersMatch(Cluster actual, Cluster expected) { + EXPECT_EQ(actual.title, expected.title); + for (size_t i = 0; i < actual.suggestions.size(); i++) { + ExpectSuggestionsMatch(std::move(actual.suggestions[i]), + std::move(expected.suggestions[i])); + } +} -void ParseJson(const std::string& json, - const SuccessCallback& success_callback, - const ErrorCallback& error_callback) { - base::JSONReader json_reader; - std::unique_ptr<base::Value> value = json_reader.ReadToValue(json); - if (value) { - success_callback.Run(std::move(value)); - } else { - error_callback.Run(json_reader.GetErrorMessage()); +void ExpectResponsesMatch(MockClustersCallback callback, + std::string expected_peek_text, + std::vector<Cluster> expected_clusters) { + EXPECT_EQ(callback.response_peek_text, expected_peek_text); + ASSERT_EQ(callback.response_clusters.size(), expected_clusters.size()); + for (size_t i = 0; i < callback.response_clusters.size(); i++) { + ExpectClustersMatch(std::move(callback.response_clusters[i]), + std::move(expected_clusters[i])); } } @@ -104,166 +203,322 @@ void ParseJson(const std::string& json, class ContextualSuggestionsFetcherTest : public testing::Test { public: - ContextualSuggestionsFetcherTest() - : fake_url_fetcher_factory_(new net::FakeURLFetcherFactory(nullptr)), - mock_task_runner_(new base::TestMockTimeTaskRunner( - base::TestMockTimeTaskRunner::Type::kBoundToThread)) { - scoped_refptr<net::TestURLRequestContextGetter> request_context_getter = - new net::TestURLRequestContextGetter(mock_task_runner_.get()); + ContextualSuggestionsFetcherTest() { + loader_factory_ = base::MakeRefCounted<TestSharedURLLoaderFactory>(); fetcher_ = std::make_unique<ContextualSuggestionsFetcherImpl>( - identity_test_env_.identity_manager(), - std::move(request_context_getter), test_utils_.pref_service(), - base::BindRepeating(&ParseJson)); + loader_factory_, "en"); } - ~ContextualSuggestionsFetcherTest() override { - } - - void FastForwardUntilNoTasksRemain() { - mock_task_runner_->FastForwardUntilNoTasksRemain(); - } - - void InitializeFakeCredentials() { - identity_test_env()->MakePrimaryAccountAvailable(kTestEmail); - } + ~ContextualSuggestionsFetcherTest() override {} void SetFakeResponse(const std::string& response_data, - net::HttpStatusCode response_code, - net::URLRequestStatus::Status status) { - fake_url_fetcher_factory_->SetFakeResponse( - fetcher_->GetFetchUrlForTesting(), response_data, response_code, - status); + net::HttpStatusCode response_code = net::HTTP_OK, + network::URLLoaderCompletionStatus status = + network::URLLoaderCompletionStatus()) { + GURL fetch_url(ContextualSuggestionsFetch::GetFetchEndpoint()); + network::ResourceResponseHead head; + if (response_code >= 0) { + head.headers = base::MakeRefCounted<net::HttpResponseHeaders>( + "HTTP/1.1 " + base::NumberToString(response_code)); + status.decoded_body_length = response_data.length(); + } + + loader_factory_->test_factory()->AddResponse(fetch_url, head, response_data, + status); } - ContextualSuggestionsFetcher::SuggestionsAvailableCallback - ToSuggestionsAvailableCallback(MockSuggestionsAvailableCallback* callback) { - return base::BindOnce(&MockSuggestionsAvailableCallback::WrappedRun, - base::Unretained(callback)); + void SendAndAwaitResponse( + const GURL& context_url, + MockClustersCallback* callback, + MockMetricsCallback* mock_metrics_callback = nullptr) { + ReportFetchMetricsCallback metrics_callback = + mock_metrics_callback + ? base::BindRepeating(&MockMetricsCallback::Report, + base::Unretained(mock_metrics_callback)) + : base::DoNothing(); + fetcher().FetchContextualSuggestionsClusters( + context_url, + base::BindOnce(&MockClustersCallback::Done, base::Unretained(callback)), + metrics_callback); + base::RunLoop().RunUntilIdle(); } ContextualSuggestionsFetcher& fetcher() { return *fetcher_; } - MockSuggestionsAvailableCallback& mock_suggestions_available_callback() { - return mock_suggestions_available_callback_; - } - identity::IdentityTestEnvironment* identity_test_env() { - return &identity_test_env_; + TestURLLoaderFactory* test_factory() { + return loader_factory_->test_factory(); } private: - identity::IdentityTestEnvironment identity_test_env_; - std::unique_ptr<net::FakeURLFetcherFactory> fake_url_fetcher_factory_; + base::test::ScopedTaskEnvironment scoped_task_environment_; + scoped_refptr<TestSharedURLLoaderFactory> loader_factory_; std::unique_ptr<ContextualSuggestionsFetcherImpl> fetcher_; - MockSuggestionsAvailableCallback mock_suggestions_available_callback_; - scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_; - test::RemoteSuggestionsTestUtils test_utils_; DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsFetcherTest); }; -TEST_F(ContextualSuggestionsFetcherTest, ShouldCreateFetcher) { - FastForwardUntilNoTasksRemain(); - EXPECT_THAT(fetcher().GetLastStatusForTesting(), IsEmpty()); +TEST_F(ContextualSuggestionsFetcherTest, SingleSuggestionResponse) { + MockClustersCallback callback; + MockMetricsCallback metrics_callback; + SetFakeResponse(SerializedResponseProto("Peek Text", DefaultClusters())); + + SendAndAwaitResponse(GURL("http://www.article.com"), &callback, + &metrics_callback); + + EXPECT_TRUE(callback.has_run); + ExpectResponsesMatch(std::move(callback), "Peek Text", DefaultClusters()); + EXPECT_EQ(metrics_callback.events, + std::vector<ContextualSuggestionsEvent>( + {contextual_suggestions::FETCH_COMPLETED})); } -TEST_F(ContextualSuggestionsFetcherTest, ShouldFetchSuggestion) { - InitializeFakeCredentials(); - const std::string kValidResponseData = - "{\"categories\" : [{" - " \"id\": 0," - " \"suggestions\" : [{" - " \"title\" : \"Title\"," - " \"summary\" : \"...\"," - " \"url\" : \"http://localhost/foobar\"," - " \"imageUrl\" : \"http://localhost/foobar.jpg\"" - " }]" - "}]}"; - SetFakeResponse(/*response_data=*/kValidResponseData, net::HTTP_OK, - net::URLRequestStatus::SUCCESS); - EXPECT_CALL(mock_suggestions_available_callback(), - Run(Property(&Status::IsSuccess, true), - IsSingleSuggestion("http://localhost/foobar"))); - - fetcher().FetchContextualSuggestions( - GURL(kValidURL), - ToSuggestionsAvailableCallback(&mock_suggestions_available_callback())); - - identity_test_env()->WaitForAccessTokenRequestAndRespondWithToken( - "access_token", base::Time::Max()); - - FastForwardUntilNoTasksRemain(); - EXPECT_THAT(fetcher().GetLastStatusForTesting(), Eq("OK")); - EXPECT_THAT(fetcher().GetLastJsonForTesting(), Eq(kValidResponseData)); +TEST_F(ContextualSuggestionsFetcherTest, + MultipleSuggestionMultipleClusterResponse) { + std::vector<Cluster> clusters; + std::vector<Cluster> clusters_copy; + base::HistogramTester histogram_tester; + + ClusterBuilder cluster1_builder("Category 1"); + cluster1_builder + .AddSuggestion(SuggestionBuilder(GURL("http://www.test.com")) + .Title("Title1") + .PublisherName("test.com") + .Snippet("Summary 1") + .ImageId("abc") + .Build()) + .AddSuggestion(SuggestionBuilder(GURL("http://www.foobar.com")) + .Title("Title2") + .PublisherName("foobar.com") + .Snippet("Summary 2") + .ImageId("def") + .Build()); + + ClusterBuilder cluster2_builder("Category 2"); + cluster2_builder + .AddSuggestion(SuggestionBuilder(GURL("http://www.barbaz.com")) + .Title("Title3") + .PublisherName("barbaz.com") + .Snippet("Summary 3") + .ImageId("ghi") + .Build()) + .AddSuggestion(SuggestionBuilder(GURL("http://www.cnn.com")) + .Title("Title4") + .PublisherName("cnn.com") + .Snippet("Summary 4") + .ImageId("jkl") + .Build()) + .AddSuggestion(SuggestionBuilder(GURL("http://www.slate.com")) + .Title("Title5") + .PublisherName("slate.com") + .Snippet("Summary 5") + .ImageId("mno") + .Build()); + ClusterBuilder c1_copy = cluster1_builder; + ClusterBuilder c2_copy = cluster2_builder; + + clusters.emplace_back(cluster1_builder.Build()); + clusters.emplace_back(cluster2_builder.Build()); + + clusters_copy.emplace_back(c1_copy.Build()); + clusters_copy.emplace_back(c2_copy.Build()); + + SetFakeResponse(SerializedResponseProto("Peek Text", std::move(clusters))); + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + EXPECT_TRUE(callback.has_run); + ExpectResponsesMatch(std::move(callback), "Peek Text", + std::move(clusters_copy)); + + histogram_tester.ExpectTotalCount("ContextualSuggestions.FetchResponseSizeKB", + 1); } -TEST_F(ContextualSuggestionsFetcherTest, ShouldFetchEmptySuggestionsList) { - InitializeFakeCredentials(); - const std::string kValidEmptyCategoryResponseData = - "{\"categories\" : [{" - " \"id\": 0," - " \"suggestions\": []" - "}]}"; - SetFakeResponse(/*response_data=*/kValidEmptyCategoryResponseData, - net::HTTP_OK, net::URLRequestStatus::SUCCESS); - EXPECT_CALL(mock_suggestions_available_callback(), - Run(Property(&Status::IsSuccess, true), - /*fetched_categories=*/IsEmptySuggestionsList())); - - fetcher().FetchContextualSuggestions( - GURL(kValidURL), - ToSuggestionsAvailableCallback(&mock_suggestions_available_callback())); - - identity_test_env()->WaitForAccessTokenRequestAndRespondWithToken( - "access_token", base::Time::Max()); - - FastForwardUntilNoTasksRemain(); - EXPECT_THAT(fetcher().GetLastStatusForTesting(), Eq("OK")); - EXPECT_THAT(fetcher().GetLastJsonForTesting(), - Eq(kValidEmptyCategoryResponseData)); +TEST_F(ContextualSuggestionsFetcherTest, FlatResponse) { + SetFakeResponse(SerializedResponseProto("Peek Text", DefaultCluster())); + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + EXPECT_TRUE(callback.has_run); + // There's no title for the flat/unclustered response case, since there's no + // PivotCluster to copy it from. So we clear the expected title. + std::vector<Cluster> expected_clusters = DefaultClusters(); + expected_clusters[0].title = ""; + ExpectResponsesMatch(std::move(callback), "Peek Text", + std::move(expected_clusters)); } -TEST_F(ContextualSuggestionsFetcherTest, - ShouldReportErrorForEmptyResponseData) { - InitializeFakeCredentials(); - SetFakeResponse(/*response_data=*/std::string(), net::HTTP_NOT_FOUND, - net::URLRequestStatus::SUCCESS); - EXPECT_CALL(mock_suggestions_available_callback(), - Run(Property(&Status::IsSuccess, false), _)); +TEST_F(ContextualSuggestionsFetcherTest, HtmlEntitiesAreUnescaped) { + ClusterBuilder cluster_builder("Category 1"); + cluster_builder.AddSuggestion(SuggestionBuilder(GURL("http://www.test.com")) + .Title(""foobar"") + .PublisherName("test.com") + .Snippet("'barbaz'") + .ImageId("abc") + .Build()); + ClusterBuilder builder_copy = cluster_builder; + SetFakeResponse( + SerializedResponseProto("Peek Text", cluster_builder.Build())); + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + EXPECT_TRUE(callback.has_run); + std::vector<Cluster> expected_clusters; + expected_clusters.emplace_back(builder_copy.Build()); + // Clear the title since it's a flat response. + expected_clusters[0].title = ""; + // Adjust the expected title and snippet to manually unescape the html + // entities we added. + expected_clusters[0].suggestions[0].title = "\"foobar\""; + expected_clusters[0].suggestions[0].snippet = "\'barbaz\'"; + ExpectResponsesMatch(std::move(callback), "Peek Text", + std::move(expected_clusters)); +} - fetcher().FetchContextualSuggestions( - GURL(kValidURL), - ToSuggestionsAvailableCallback(&mock_suggestions_available_callback())); +TEST_F(ContextualSuggestionsFetcherTest, RequestHeaderSetCorrectly) { + net::HttpRequestHeaders headers; + base::RunLoop interceptor_run_loop; + base::HistogramTester histogram_tester; + + test_factory()->SetInterceptor( + base::BindLambdaForTesting([&](const network::ResourceRequest& request) { + headers = request.headers; + interceptor_run_loop.Quit(); + })); + SetFakeResponse(SerializedResponseProto("Peek Text", DefaultClusters())); + + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + interceptor_run_loop.Run(); + + std::string protobuf_header; + ASSERT_TRUE( + headers.GetHeader("X-Protobuffer-Request-Payload", &protobuf_header)); + + std::string decoded_header_value; + base::Base64Decode(protobuf_header, &decoded_header_value); + GetPivotsRequest request; + ASSERT_TRUE(request.ParseFromString(decoded_header_value)); + + EXPECT_EQ(request.context().localization_context().language_code(), "en"); + EXPECT_EQ(request.query().context()[0].url(), "http://www.article.com/"); + EXPECT_TRUE(request.query().document_params().enabled()); + EXPECT_EQ(request.query().document_params().num(), 10); + EXPECT_TRUE(request.query().document_params().enable_images()); + EXPECT_TRUE(request.query().clustering_params().enabled()); + EXPECT_TRUE(request.query().peek_text_params().enabled()); + + histogram_tester.ExpectTotalCount( + "ContextualSuggestions.FetchRequestProtoSizeKB", 1); +} - identity_test_env()->WaitForAccessTokenRequestAndRespondWithToken( - "access_token", base::Time::Max()); +TEST_F(ContextualSuggestionsFetcherTest, ProtocolError) { + MockClustersCallback callback; + MockMetricsCallback metrics_callback; + base::HistogramTester histogram_tester; + + SetFakeResponse("", net::HTTP_NOT_FOUND); + SendAndAwaitResponse(GURL("http://www.article.com"), &callback, + &metrics_callback); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 0u); + EXPECT_THAT( + histogram_tester.GetAllSamples("ContextualSuggestions.FetchResponseCode"), + ElementsAre(base::Bucket(/*min=*/net::HTTP_NOT_FOUND, /*count=*/1))); + EXPECT_EQ(metrics_callback.events, + std::vector<ContextualSuggestionsEvent>( + {contextual_suggestions::FETCH_ERROR})); +} - FastForwardUntilNoTasksRemain(); +TEST_F(ContextualSuggestionsFetcherTest, ServerUnavailable) { + MockClustersCallback callback; + MockMetricsCallback metrics_callback; + base::HistogramTester histogram_tester; + + SetFakeResponse("", net::HTTP_SERVICE_UNAVAILABLE); + SendAndAwaitResponse(GURL("http://www.article.com"), &callback, + &metrics_callback); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 0u); + EXPECT_THAT( + histogram_tester.GetAllSamples("ContextualSuggestions.FetchResponseCode"), + ElementsAre(base::Bucket(/*min=*/net::HTTP_SERVICE_UNAVAILABLE, + /*count=*/1))); + EXPECT_EQ(metrics_callback.events, + std::vector<ContextualSuggestionsEvent>( + {contextual_suggestions::FETCH_SERVER_BUSY})); } -TEST_F(ContextualSuggestionsFetcherTest, - ShouldReportErrorForInvalidResponseData) { - InitializeFakeCredentials(); - const std::string kInvalidResponseData = "{ \"recos\": []"; - SetFakeResponse(/*response_data=*/kInvalidResponseData, net::HTTP_OK, - net::URLRequestStatus::SUCCESS); - EXPECT_CALL( - mock_suggestions_available_callback(), - Run(Field(&Status::code, StatusCode::TEMPORARY_ERROR), - /*fetched_categories=*/Property( - &ContextualSuggestionsFetcher::OptionalSuggestions::has_value, - false))); - - fetcher().FetchContextualSuggestions( - GURL(kValidURL), - ToSuggestionsAvailableCallback(&mock_suggestions_available_callback())); - - identity_test_env()->WaitForAccessTokenRequestAndRespondWithToken( - "access_token", base::Time::Max()); - - FastForwardUntilNoTasksRemain(); - EXPECT_THAT(fetcher().GetLastStatusForTesting(), - StartsWith("Received invalid JSON (error ")); - EXPECT_THAT(fetcher().GetLastJsonForTesting(), Eq(kInvalidResponseData)); +TEST_F(ContextualSuggestionsFetcherTest, NetworkError) { + MockClustersCallback callback; + MockMetricsCallback metrics_callback; + base::HistogramTester histogram_tester; + + SetFakeResponse( + "", net::HTTP_OK, + network::URLLoaderCompletionStatus(net::ERR_CERT_COMMON_NAME_INVALID)); + SendAndAwaitResponse(GURL("http://www.article.com"), &callback, + &metrics_callback); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 0u); + EXPECT_THAT( + histogram_tester.GetAllSamples("ContextualSuggestions.FetchErrorCode"), + ElementsAre(base::Bucket( + /*min=*/net::ERR_CERT_COMMON_NAME_INVALID, /*count=*/1))); + + EXPECT_EQ(metrics_callback.events, + std::vector<ContextualSuggestionsEvent>( + {contextual_suggestions::FETCH_ERROR})); +} + +TEST_F(ContextualSuggestionsFetcherTest, EmptyResponse) { + MockClustersCallback callback; + MockMetricsCallback metrics_callback; + SetFakeResponse(SerializedResponseProto("", Cluster())); + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback, + &metrics_callback); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 0u); + + EXPECT_EQ(metrics_callback.events, + std::vector<ContextualSuggestionsEvent>( + {contextual_suggestions::FETCH_EMPTY})); +} + +TEST_F(ContextualSuggestionsFetcherTest, ResponseWithUnsetFields) { + GetPivotsResponse response; + Pivots* pivots = response.mutable_pivots(); + pivots->add_item()->mutable_document(); + pivots->add_item(); + + SetFakeResponse(" " + response.SerializeAsString()); + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + std::vector<Cluster> expected_clusters; + expected_clusters.emplace_back( + ClusterBuilder("") + .AddSuggestion(SuggestionBuilder(GURL()).Build()) + .AddSuggestion(SuggestionBuilder(GURL()).Build()) + .Build()); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 1u); + ExpectResponsesMatch(std::move(callback), "", std::move(expected_clusters)); +} + +TEST_F(ContextualSuggestionsFetcherTest, CorruptResponse) { + SetFakeResponse("unparseable proto string"); + MockClustersCallback callback; + SendAndAwaitResponse(GURL("http://www.article.com/"), &callback); + + EXPECT_TRUE(callback.has_run); + EXPECT_EQ(callback.response_clusters.size(), 0u); } } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.cc new file mode 100644 index 00000000000..f3f4b600a1d --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.cc @@ -0,0 +1,110 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" + +#include "base/metrics/histogram_macros.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h" +#include "services/metrics/public/cpp/ukm_recorder.h" + +namespace contextual_suggestions { + +ContextualSuggestionsMetricsReporterProvider:: + ContextualSuggestionsMetricsReporterProvider() = default; + +ContextualSuggestionsMetricsReporterProvider:: + ~ContextualSuggestionsMetricsReporterProvider() = default; + +std::unique_ptr<ContextualSuggestionsMetricsReporter> +ContextualSuggestionsMetricsReporterProvider::CreateMetricsReporter() { + return std::make_unique<ContextualSuggestionsMetricsReporter>(); +} + +ContextualSuggestionsMetricsReporter::ContextualSuggestionsMetricsReporter() + : sheet_peeked_(false), + sheet_opened_(false), + sheet_closed_(false), + any_suggestion_downloaded_(false), + any_suggestion_taken_(false), + ukm_entry_(nullptr) {} + +ContextualSuggestionsMetricsReporter::~ContextualSuggestionsMetricsReporter() { + DCHECK(!ukm_entry_) << "Flush should be called before destruction!"; +} + +void ContextualSuggestionsMetricsReporter::SetupForPage( + ukm::SourceId source_id) { + DCHECK(!ukm_entry_) << "Flush should be called before SetupForPage!"; + DCHECK(source_id != ukm::kInvalidSourceId); + ukm_entry_.reset(new ContextualSuggestionsUkmEntry(source_id)); +} + +void ContextualSuggestionsMetricsReporter::RecordEvent( + ContextualSuggestionsEvent event) { + DCHECK(ukm_entry_) + << "Page not set up. Did you forget to call SetupForPage or Flush?"; + ukm_entry_->RecordEventMetrics(event); + RecordUmaMetrics(event); +} + +void ContextualSuggestionsMetricsReporter::Flush() { + ResetUma(); + if (ukm_entry_) { + ukm_entry_->Flush(); + ukm_entry_.reset(); + } +} + +void ContextualSuggestionsMetricsReporter::RecordUmaMetrics( + ContextualSuggestionsEvent event) { + switch (event) { + case FETCH_DELAYED: + case FETCH_REQUESTED: + case FETCH_ERROR: + case FETCH_SERVER_BUSY: + case FETCH_BELOW_THRESHOLD: + case FETCH_EMPTY: + case FETCH_COMPLETED: + break; + case UI_PEEK_REVERSE_SCROLL: + if (sheet_peeked_) + return; + sheet_peeked_ = true; + break; + case UI_OPENED: + if (sheet_opened_) + return; + sheet_opened_ = true; + break; + case UI_CLOSED: + if (sheet_closed_) + return; + sheet_closed_ = true; + break; + case SUGGESTION_DOWNLOADED: + if (any_suggestion_downloaded_) + return; + any_suggestion_downloaded_ = true; + break; + case SUGGESTION_CLICKED: + if (any_suggestion_taken_) + return; + any_suggestion_taken_ = true; + break; + default: + NOTREACHED() << "Unexpected event, not correctly handled!"; + break; + } + UMA_HISTOGRAM_ENUMERATION("ContextualSuggestions.Events", event); +} + +void ContextualSuggestionsMetricsReporter::ResetUma() { + sheet_peeked_ = false; + sheet_opened_ = false; + sheet_closed_ = false; + any_suggestion_downloaded_ = false; + any_suggestion_taken_ = false; +} + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h new file mode 100644 index 00000000000..8772d05a8ef --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h @@ -0,0 +1,163 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_METRICS_REPORTER_H_ +#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_METRICS_REPORTER_H_ + +#include <memory> + +#include "base/callback.h" +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "services/metrics/public/cpp/ukm_source_id.h" + +namespace contextual_suggestions { + +class ContextualSuggestionsUkmEntry; + +FORWARD_DECLARE_TEST(ContextualSuggestionsMetricsReporterTest, BaseTest); + +// NOTE: because this is used for UMA reporting, these values should not be +// changed or reused; new values should be appended immediately before the +// EVENT_MAX value. Make sure to update the histogram enum +// (ContextualSuggestions.Event in enums.xml) accordingly! +// A Java counterpart will be generated for this enum. +// GENERATED_JAVA_ENUM_PACKAGE: ( +// org.chromium.chrome.browser.contextual_suggestions) +enum ContextualSuggestionsEvent { + // Indicates that this state is not initialized. + // Should never be intentionally recorded, just used as a default value. + UNINITIALIZED = 0, + // Records that fetching suggestions has been delayed on the client side. + FETCH_DELAYED = 1, + // The fetch request has been made but a response has not yet been received. + FETCH_REQUESTED = 2, + // The fetch response has been received, but there was some error. + FETCH_ERROR = 3, + // The fetch response indicates that the server was too busy to return any + // suggestions. + FETCH_SERVER_BUSY = 4, + // The fetch response includes suggestions but they were all below the + // confidence threshold needed to show them to the user. + FETCH_BELOW_THRESHOLD = 5, + // The fetch response has been received and parsed, but there were no + // suggestions. + FETCH_EMPTY = 6, + // The fetch response has been received and there are suggestions to show. + FETCH_COMPLETED = 7, + // The UI was shown in the "peeking bar" state, triggered by a reverse-scroll. + // If new gestures are added to trigger the peeking sheet then a new event + // should be added to this list. + UI_PEEK_REVERSE_SCROLL = 8, + // The UI sheet was opened. + UI_OPENED = 9, + // The UI was closed (via the close box). + UI_CLOSED = 10, + // A suggestion was downloaded. + SUGGESTION_DOWNLOADED = 11, + // A suggestion was taken, either with a click, or opened in a separate tab. + SUGGESTION_CLICKED = 12, + // Special name that marks the maximum value in an Enum used for UMA. + // https://cs.chromium.org/chromium/src/tools/metrics/histograms/README.md. + kMaxValue = SUGGESTION_CLICKED, +}; + +class ContextualSuggestionsMetricsReporter; + +// Class producing |ContextualSuggestionsMetricsReporter| instances. It enables +// classes like |ContextualContentSuggestionService| to produce metrics +// reporters when needed, e.g. creation of service proxy, without knowing how to +// initialize them. +class ContextualSuggestionsMetricsReporterProvider { + public: + ContextualSuggestionsMetricsReporterProvider(); + virtual ~ContextualSuggestionsMetricsReporterProvider(); + + virtual std::unique_ptr<ContextualSuggestionsMetricsReporter> + CreateMetricsReporter(); + + private: + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsMetricsReporterProvider); +}; + +// Tracks various UKM and UMA metrics based on reports of events that take place +// within the Contextual Suggestions component. +// +// For example: +// Java UI -> ContextualSuggestionsBridge#reportEvent( +// /* web_contents */, @ContextualSuggestionsEvent int eventId) -> native +// -> ContextualContentSuggestionsService#reportEvent( +// ukm::GetSourceIdForWebContentsDocument(web_contents), eventId)) -> +// if(!reporter) { +// ContextualSuggestionsMetricsReporter* reporter = +// new ContextualSuggestionsMetricsReporter(); +// } +// ukm::SourceId source_id_for_web_contents; +// reporter->SetupForPage(source_id_for_web_contents); +// reporter->RecordEvent(FETCH_REQUESTED); +// ... +// +// if (!my_results) +// reporter->RecordEvent(FETCH_ERROR); +// else if (my_result.size() == 0) +// reporter->RecordEvent(FETCH_EMPTY); +// else +// reporter->RecordEvent(FETCH_COMPLETED); +// ... +// When the UI shows: +// reporter->RecordEvent(UI_PEEK_SHOWN); +// Make sure data is flushed when leaving the page: +// reporter->Flush(); +class ContextualSuggestionsMetricsReporter { + public: + ContextualSuggestionsMetricsReporter(); + ~ContextualSuggestionsMetricsReporter(); + + // Sets up the page with the given |source_id| for event reporting. + // All subsequent RecordEvent calls will apply to this page. + void SetupForPage(ukm::SourceId source_id); + + // Reports that an event occurred for the current page. + // Some data is not written immediately, but will be written when |Reset| is + // called. + void RecordEvent(ContextualSuggestionsEvent event); + + // Flushes all data staged using |RecordEvent|. + // This is required before a subsequent call to |SetupForPage|, and can be + // called multiple times. + void Flush(); + + private: + FRIEND_TEST_ALL_PREFIXES(ContextualSuggestionsMetricsReporterTest, BaseTest); + + // Records UMA metrics for this event. + void RecordUmaMetrics(ContextualSuggestionsEvent event); + + // Reset UMA metrics. + void ResetUma(); + + // Internal UMA state data. + // Whether the sheet ever peeked. + bool sheet_peeked_; + // Whether the sheet was ever opened. + bool sheet_opened_; + // Whether the sheet was closed. + bool sheet_closed_; + // Whether any suggestion was downloaded. + bool any_suggestion_downloaded_; + // Whether any suggestion was taken. + bool any_suggestion_taken_; + + // The current UKM entry. + std::unique_ptr<ContextualSuggestionsUkmEntry> ukm_entry_; + + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsMetricsReporter); +}; + +using ReportFetchMetricsCallback = base::RepeatingCallback<void( + contextual_suggestions::ContextualSuggestionsEvent event)>; + +} // namespace contextual_suggestions + +#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_METRICS_REPORTER_H_ diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter_unittest.cc new file mode 100644 index 00000000000..ce95774f2d2 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter_unittest.cc @@ -0,0 +1,90 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "base/macros.h" +#include "base/test/histogram_tester.h" +#include "base/test/scoped_task_environment.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h" +#include "components/ukm/test_ukm_recorder.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using ukm::TestUkmRecorder; + +namespace contextual_suggestions { + +namespace { +const char kTestNavigationUrl[] = "https://foo.com"; +} // namespace + +class ContextualSuggestionsMetricsReporterTest : public ::testing::Test { + protected: + ContextualSuggestionsMetricsReporterTest() = default; + + TestUkmRecorder* GetTestUkmRecorder() { return &test_ukm_recorder_; } + + ukm::SourceId GetSourceId(); + + ContextualSuggestionsMetricsReporter& GetReporter() { return reporter_; } + + private: + // The reporter under test. + ContextualSuggestionsMetricsReporter reporter_; + + // Sets up the task scheduling/task-runner environment for each test. + base::test::ScopedTaskEnvironment scoped_task_environment_; + + // Sets itself as the global UkmRecorder on construction. + ukm::TestAutoSetUkmRecorder test_ukm_recorder_; + + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsMetricsReporterTest); +}; + +ukm::SourceId ContextualSuggestionsMetricsReporterTest::GetSourceId() { + ukm::SourceId source_id = ukm::UkmRecorder::GetNewSourceID(); + test_ukm_recorder_.UpdateSourceURL(source_id, GURL(kTestNavigationUrl)); + return source_id; +} + +TEST_F(ContextualSuggestionsMetricsReporterTest, BaseTest) { + base::HistogramTester histogram_tester; + GetReporter().SetupForPage(GetSourceId()); + GetReporter().RecordEvent(FETCH_REQUESTED); + GetReporter().RecordEvent(FETCH_COMPLETED); + GetReporter().RecordEvent(UI_PEEK_REVERSE_SCROLL); + GetReporter().RecordEvent(UI_OPENED); + GetReporter().RecordEvent(SUGGESTION_DOWNLOADED); + GetReporter().RecordEvent(SUGGESTION_CLICKED); + // Flush data to write to UKM. + GetReporter().Flush(); + // Check what we wrote. + TestUkmRecorder* test_ukm_recorder = GetTestUkmRecorder(); + std::vector<const ukm::mojom::UkmEntry*> entry_vector = + test_ukm_recorder->GetEntriesByName(kContextualSuggestionsUkmEntryName); + EXPECT_EQ(1U, entry_vector.size()); + const ukm::mojom::UkmEntry* first_entry = entry_vector[0]; + EXPECT_TRUE(test_ukm_recorder->EntryHasMetric( + first_entry, kContextualSuggestionsFetchMetricName)); + EXPECT_EQ(static_cast<int64_t>(FetchState::COMPLETED), + *(test_ukm_recorder->GetEntryMetric( + first_entry, kContextualSuggestionsFetchMetricName))); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 0, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 1, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 2, 1); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 3, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 4, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 5, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 6, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 7, 1); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 8, 1); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 9, 1); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 10, 0); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 11, 1); + histogram_tester.ExpectBucketCount("ContextualSuggestions.Events", 12, 1); +} + +// TODO(donnd): add more tests, and test UMA data! + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.cc new file mode 100644 index 00000000000..968e333739d --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.cc @@ -0,0 +1,146 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h" + +#include <algorithm> + +#include "base/timer/elapsed_timer.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "services/metrics/public/cpp/metrics_utils.h" +#include "services/metrics/public/cpp/ukm_entry_builder.h" +#include "services/metrics/public/cpp/ukm_recorder.h" + +using ukm::UkmEntryBuilder; + +namespace contextual_suggestions { + +namespace { +// Spacing between buckets for the duration in milliseconds. +// The values recorded will be the min of the bucket in a power of 2 range. +// E.g. 1100 => 1024 since 1024 <= 1100 < 2048. +const double kShowDurationBucketSpacing = 2.0; +const int64_t kMinShowDuration = 1; +} // namespace + +ContextualSuggestionsUkmEntry::ContextualSuggestionsUkmEntry( + ukm::SourceId source_id) + : fetch_state_(FetchState::NOT_STARTED), + trigger_event_(TriggerEvent::NEVER_SHOWN), + closed_from_peek_(false), + was_sheet_opened_(false), + any_suggestion_taken_(false), + any_suggestion_downloaded_(false), + show_duration_exponential_bucket_(0), + show_duration_timer_(nullptr), + source_id_(source_id) {} + +ContextualSuggestionsUkmEntry::~ContextualSuggestionsUkmEntry() {} + +void ContextualSuggestionsUkmEntry::RecordEventMetrics( + ContextualSuggestionsEvent event) { + switch (event) { + case UNINITIALIZED: + NOTREACHED() << "An uninitialized event value was sent!"; + break; + case FETCH_DELAYED: + fetch_state_ = FetchState::DELAYED; + break; + case FETCH_REQUESTED: + fetch_state_ = FetchState::REQUESTED; + break; + case FETCH_ERROR: + fetch_state_ = FetchState::ERROR_RESULT; + break; + case FETCH_SERVER_BUSY: + fetch_state_ = FetchState::SERVER_BUSY; + break; + case FETCH_BELOW_THRESHOLD: + fetch_state_ = FetchState::BELOW_THRESHOLD; + break; + case FETCH_EMPTY: + fetch_state_ = FetchState::EMPTY; + break; + case FETCH_COMPLETED: + fetch_state_ = FetchState::COMPLETED; + break; + case UI_PEEK_REVERSE_SCROLL: + trigger_event_ = TriggerEvent::REVERSE_SCROLL; + StartTimerIfNeeded(); + break; + case UI_OPENED: + was_sheet_opened_ = true; + StartTimerIfNeeded(); + break; + case UI_CLOSED: + if (!was_sheet_opened_) + closed_from_peek_ = true; + StopTimerIfNeeded(); + break; + case SUGGESTION_DOWNLOADED: + any_suggestion_downloaded_ = true; + StopTimerIfNeeded(); + break; + case SUGGESTION_CLICKED: + any_suggestion_taken_ = true; + StopTimerIfNeeded(); + break; + } +} + +void ContextualSuggestionsUkmEntry::Flush() { + if (source_id_ == ukm::kInvalidSourceId) + return; + + if (show_duration_timer_) + StopTimerIfNeeded(); + + // When this variable goes out of scope the builder's destructor will write. + std::unique_ptr<UkmEntryBuilder> builder = + ukm::UkmRecorder::Get()->GetEntryBuilder( + source_id_, kContextualSuggestionsUkmEntryName); + // Keep these writes alphabetical by metric name (matching ukm.xml ordering). + builder->AddMetric(kContextualSuggestionsClosedMetricName, closed_from_peek_); + builder->AddMetric(kContextualSuggestionsDownloadedMetricName, + any_suggestion_downloaded_); + builder->AddMetric(kContextualSuggestionsOpenedMetricName, was_sheet_opened_); + builder->AddMetric(kContextualSuggestionsFetchMetricName, + static_cast<int64_t>(fetch_state_)); + builder->AddMetric(kContextualSuggestionsDurationMetricName, + show_duration_exponential_bucket_); + builder->AddMetric(kContextualSuggestionsTriggerMetricName, + static_cast<int64_t>(trigger_event_)); + builder->AddMetric(kContextualSuggestionsTakenMetricName, + any_suggestion_taken_); + + source_id_ = ukm::kInvalidSourceId; +} + +void ContextualSuggestionsUkmEntry::StartTimerIfNeeded() { + // If the timer is already running, don't restart it. + if (show_duration_timer_) + return; + + show_duration_timer_.reset(new base::ElapsedTimer()); +} + +void ContextualSuggestionsUkmEntry::StopTimerIfNeeded() { + // We should either have a timer or a computed duration from the timer. + DCHECK(show_duration_timer_ || show_duration_exponential_bucket_ > 0); + + // If we've already computed the duration, or there's no timer to stop, then + // there's nothing to do. + if (show_duration_exponential_bucket_ > 0 || !show_duration_timer_) + return; + + show_duration_exponential_bucket_ = ukm::GetExponentialBucketMin( + show_duration_timer_->Elapsed().InMillisecondsRoundedUp(), + kShowDurationBucketSpacing); + // Ensure a positive value. + show_duration_exponential_bucket_ = + std::max(kMinShowDuration, show_duration_exponential_bucket_); + show_duration_timer_.reset(); +} + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h new file mode 100644 index 00000000000..6c596c89768 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h @@ -0,0 +1,99 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_UKM_ENTRY_H_ +#define COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_UKM_ENTRY_H_ + +#include "base/gtest_prod_util.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "services/metrics/public/cpp/ukm_source_id.h" + +namespace base { +class ElapsedTimer; +} + +namespace contextual_suggestions { + +FORWARD_DECLARE_TEST(ContextualSuggestionsUkmEntryTest, BinaryOrderTest); + +// The name of the UKM entry written by this class. +const char kContextualSuggestionsUkmEntryName[] = "ContextualSuggestions"; + +// The names of individual UKM metrics written to our UKM entry. +const char kContextualSuggestionsDownloadedMetricName[] = "AnyDownloaded"; +const char kContextualSuggestionsTakenMetricName[] = "AnySuggestionTaken"; +const char kContextualSuggestionsClosedMetricName[] = "ClosedFromPeek"; +const char kContextualSuggestionsOpenedMetricName[] = "EverOpened"; +const char kContextualSuggestionsFetchMetricName[] = "FetchState"; +const char kContextualSuggestionsDurationMetricName[] = "ShowDurationBucketMin"; +const char kContextualSuggestionsTriggerMetricName[] = "TriggerEvent"; + +// The state of the Fetcher request. +// This value is written to the "FetchState" UKM metric. +enum class FetchState { + NOT_STARTED, + DELAYED, + REQUESTED, + ERROR_RESULT, + SERVER_BUSY, + BELOW_THRESHOLD, + EMPTY, + COMPLETED, +}; + +// The way that the sheet UI was triggered. +// This value is written to the "TriggerEvent" UKM metric. +enum class TriggerEvent { + NEVER_SHOWN, + REVERSE_SCROLL, +}; + +// Writes a single UKM entry that describes the latest state of the event stream +// monitored through |RecordEventMetrics|. +class ContextualSuggestionsUkmEntry { + public: + // Sets up recording of a UKM entry for the given |source_id|. + explicit ContextualSuggestionsUkmEntry(ukm::SourceId source_id); + ~ContextualSuggestionsUkmEntry(); + + // Updates tracked metrics for the given |event|. + void RecordEventMetrics(ContextualSuggestionsEvent event); + + // Writes the latest data tracked into a single UKM entry. + void Flush(); + + private: + FRIEND_TEST_ALL_PREFIXES(ContextualSuggestionsUkmEntryTest, BinaryOrderTest); + + // Starts the elapsed timer if not already started. + void StartTimerIfNeeded(); + + // Stops the elapsed timer if not already stopped. + void StopTimerIfNeeded(); + + // Internal state trackers. + FetchState fetch_state_; + TriggerEvent trigger_event_; + bool closed_from_peek_; + bool was_sheet_opened_; + bool any_suggestion_taken_; + bool any_suggestion_downloaded_; + + // The minimum exponential bucket of the duration in milliseconds that the + // sheet was viewed before taking action. A value of 0 means not yet + // computed. + int64_t show_duration_exponential_bucket_; + + // Simple timer for how long the sheet is showing. + std::unique_ptr<base::ElapsedTimer> show_duration_timer_; + + // The UKM identifier for the current URL / page in use. + ukm::SourceId source_id_; + + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsUkmEntry); +}; + +} // namespace contextual_suggestions + +#endif // COMPONENTS_NTP_SNIPPETS_CONTEXTUAL_CONTEXTUAL_SUGGESTIONS_UKM_ENTRY_H_ diff --git a/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry_unittest.cc b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry_unittest.cc new file mode 100644 index 00000000000..93d78903cf6 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/contextual_suggestions_ukm_entry_unittest.cc @@ -0,0 +1,104 @@ +// Copyright 2018 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/ntp_snippets/contextual/contextual_suggestions_ukm_entry.h" +#include "base/macros.h" +#include "base/test/scoped_task_environment.h" +#include "components/ntp_snippets/contextual/contextual_suggestions_metrics_reporter.h" +#include "components/ukm/test_ukm_recorder.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ukm::TestUkmRecorder; + +namespace contextual_suggestions { + +const int NO_ENTRY = -1; + +class ContextualSuggestionsUkmEntryTest : public ::testing::Test { + protected: + ContextualSuggestionsUkmEntryTest() = default; + + void SetUp() override; + + // The entry under test. + std::unique_ptr<ContextualSuggestionsUkmEntry> ukm_entry_; + + TestUkmRecorder* GetTestUkmRecorder() { return &test_ukm_recorder_; } + + const ukm::mojom::UkmEntry* FirstEntry(); + bool HasEntryMetric(const char* metric_name); + int GetEntryMetric(const char* metric_name); + + private: + // Sets up the task scheduling/task-runner environment for each test. + base::test::ScopedTaskEnvironment scoped_task_environment_; + + // Sets itself as the global UkmRecorder on construction. + ukm::TestAutoSetUkmRecorder test_ukm_recorder_; + + DISALLOW_COPY_AND_ASSIGN(ContextualSuggestionsUkmEntryTest); +}; + +void ContextualSuggestionsUkmEntryTest::SetUp() { + ukm_entry_.reset( + new ContextualSuggestionsUkmEntry(ukm::UkmRecorder::GetNewSourceID())); +} + +const ukm::mojom::UkmEntry* ContextualSuggestionsUkmEntryTest::FirstEntry() { + TestUkmRecorder* recorder = GetTestUkmRecorder(); + std::vector<const ukm::mojom::UkmEntry*> entry_vector = + recorder->GetEntriesByName(kContextualSuggestionsUkmEntryName); + EXPECT_EQ(1U, entry_vector.size()); + return entry_vector[0]; +} + +bool ContextualSuggestionsUkmEntryTest::HasEntryMetric( + const char* metric_name) { + TestUkmRecorder* recorder = GetTestUkmRecorder(); + return recorder->EntryHasMetric(FirstEntry(), metric_name); +} + +int ContextualSuggestionsUkmEntryTest::GetEntryMetric(const char* metric_name) { + TestUkmRecorder* recorder = GetTestUkmRecorder(); + const ukm::mojom::UkmEntry* first_entry = FirstEntry(); + if (!recorder->EntryHasMetric(first_entry, metric_name)) + return NO_ENTRY; + + return (int)*(recorder->GetEntryMetric(first_entry, metric_name)); +} + +TEST_F(ContextualSuggestionsUkmEntryTest, BaseTest) { + ukm_entry_->Flush(); + // Deleting the entry should write default values for everything. + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsDownloadedMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsTakenMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsClosedMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsOpenedMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsFetchMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsDurationMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsTriggerMetricName)); +} + +TEST_F(ContextualSuggestionsUkmEntryTest, ExpectedOperationTest) { + ukm_entry_->RecordEventMetrics(FETCH_DELAYED); + ukm_entry_->RecordEventMetrics(FETCH_REQUESTED); + ukm_entry_->RecordEventMetrics(FETCH_COMPLETED); + ukm_entry_->RecordEventMetrics(UI_PEEK_REVERSE_SCROLL); + ukm_entry_->RecordEventMetrics(UI_OPENED); + ukm_entry_->RecordEventMetrics(SUGGESTION_DOWNLOADED); + ukm_entry_->RecordEventMetrics(SUGGESTION_DOWNLOADED); + ukm_entry_->RecordEventMetrics(SUGGESTION_CLICKED); + ukm_entry_->Flush(); + EXPECT_EQ(1, GetEntryMetric(kContextualSuggestionsDownloadedMetricName)); + EXPECT_EQ(1, GetEntryMetric(kContextualSuggestionsTakenMetricName)); + EXPECT_EQ(0, GetEntryMetric(kContextualSuggestionsClosedMetricName)); + EXPECT_EQ(1, GetEntryMetric(kContextualSuggestionsOpenedMetricName)); + EXPECT_EQ(7, GetEntryMetric(kContextualSuggestionsFetchMetricName)); + EXPECT_LT(0, GetEntryMetric(kContextualSuggestionsDurationMetricName)); + EXPECT_EQ(1, GetEntryMetric(kContextualSuggestionsTriggerMetricName)); +} + +// TODO(donnd): add more tests! + +} // namespace contextual_suggestions diff --git a/chromium/components/ntp_snippets/contextual/proto/chrome_search_api_request_context.proto b/chromium/components/ntp_snippets/contextual/proto/chrome_search_api_request_context.proto new file mode 100644 index 00000000000..7cf84cdae7d --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/proto/chrome_search_api_request_context.proto @@ -0,0 +1,38 @@ +// Copyright 2018 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package contextual_suggestions; + +message SearchApiRequestContext { + optional SearchApiClientContext client_context = 2; + optional LocalizationContext localization_context = 4; + optional CookieRequestContext cookie_context = 3; +} + +message SearchApiClientContext { + optional ClientId client_id = 1; +} + +message ClientId { + enum ClientType { + UNKNOWN_CLIENT_TYPE = 0; + ANDROID_CHROME = 79; + } + optional ClientType client_type = 1; +} + +message LocalizationContext { + optional string language_code = 1; + optional string country_code = 2; + optional string search_domain = 4; + optional string spoken_language = 7; +} + +message CookieRequestContext { + optional string nid = 1; +} diff --git a/chromium/components/ntp_snippets/contextual/proto/get_pivots_request.proto b/chromium/components/ntp_snippets/contextual/proto/get_pivots_request.proto new file mode 100644 index 00000000000..48cb0c572cc --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/proto/get_pivots_request.proto @@ -0,0 +1,86 @@ +// Copyright 2018 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package contextual_suggestions; + +import "chrome_search_api_request_context.proto"; + +message GetPivotsRequest { + // Context common to the search API. + optional SearchApiRequestContext context = 1; + + // Request fields specific to the Pivots request. + optional GetPivotsQuery query = 2; +} + +message GetPivotsQuery { + // A historical chain of the user's past exploration to date, sorted from + // most recent to least recent (reverse chronological order). A context is + // usually a document and some related data (eg. salient terms and/or query + // that generated that doc). + repeated ExploreContext context = 1; + + // Parameters related to pivot documents. + optional PivotDocumentParams document_params = 2; + + // Parameters related to clustering pivot items (documents, queries, + // questions, etc.). + optional PivotClusteringParams clustering_params = 6; + + // Parameters related to peek text. + optional PivotPeekTextParams peek_text_params = 7; +} + +// One piece of historical context in the user's exploration session. +message ExploreContext { + // If specified, the (possibly uncanonicalized) document the user visited. + optional string url = 1; +} + +message PivotDocumentParams { + // If true, compute pivot documents. + optional bool enabled = 1 [default = false]; + + // How many documents should we return? + optional int32 num = 2 [default = 5]; + + // If we have fewer documents than this to return, then don't return any. + optional int32 min_documents = 6 [default = 3]; + + // If true, return images for documents. + optional bool enable_images = 3 [default = false]; + + // When images are returned, what aspect ratio should be used. + enum ImageAspectRatio { + // Allow the server to select. This will typically return a thumbnail in + // the same aspect ratio as the original image. + ASPECT_RATIO_UNSPECIFIED = 0; + // Crop to a square image. + SQUARE = 1; + } + optional ImageAspectRatio image_aspect_ratio = 4 + [default = ASPECT_RATIO_UNSPECIFIED]; +} + +// Parameters for clustering pivot items (documents, queries, questions, etc). +message PivotClusteringParams { + // Whether or not to group PivotItems on similar topics into clusters. + optional bool enabled = 1; + + // Minimum number of clusters we should return. + optional int32 min = 2; + + // Maximum number of clusters we should return. + optional int32 max = 3; +} + +// Parameters related to the peek text. +message PivotPeekTextParams { + // Whether or not to return peek text. + optional bool enabled = 1 [default = false]; +}
\ No newline at end of file diff --git a/chromium/components/ntp_snippets/contextual/proto/get_pivots_response.proto b/chromium/components/ntp_snippets/contextual/proto/get_pivots_response.proto new file mode 100644 index 00000000000..826c8f4f9e7 --- /dev/null +++ b/chromium/components/ntp_snippets/contextual/proto/get_pivots_response.proto @@ -0,0 +1,125 @@ +// Copyright 2018 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. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package contextual_suggestions; + +message GetPivotsResponse { + // The suggested followups. + optional Pivots pivots = 2; +} + +// Container for all explore on content data. +message Pivots { + // Items related to the user's context. + repeated PivotItem item = 1; + + // The conditions under which auto-peek should be enabled. + optional AutoPeekConditions auto_peek_conditions = 3; + + // The text to present to the user in the peek. + optional PeekText peek_text = 4; +} + +// One related response. +message PivotItem { + oneof kind { + PivotDocument document = 1; + PivotCluster cluster = 6; + } +} + +// A document related to the source data. +message PivotDocument { + // A link to the document. + optional Url url = 1; + + // An image that may be shown next to the document. + optional Image image = 2; + + // Title of this document. + optional string title = 3; + + // Snippet summary of this document. + optional string summary = 4; + + // Human-readable version of the source site's name (e.g. "CNN" instead of + // "cnn.com"). + optional string site_name = 5; + + // A favicon image, usually related to the site. + optional Image favicon_image = 6; +} + +// A cluster of responses related to the source data. The cluster optionally has +// a label. +message PivotCluster { + // Label for the cluster. + optional ClusterLabel label = 1; + + // Ranked items that belong to this cluster. + repeated PivotItem item = 2; + + // Number of items to show to the user before they expand. + optional int32 num_items_to_display_pre_expansion = 3 [default = 1]; +} + +// The label for a cluster of followup items. +message ClusterLabel { + // Label to display to the user. + optional string label = 1; +} + +message AutoPeekConditions { + // A measure of confidence that auto-peek should be enabled for this response + // in the range [0, 1]. In general, if the value is 1.0, auto-peek should be + // enabled; if 0.0, it shouldn’t; and intermediate values are left to the + // client to decide. + optional float confidence = 1 [default = 1.0]; + + // The percentage of the page that the user scrolls required for an auto + // peek to occur. Value should be a decimal between 0 and 1.0. + // 1.0 represents the entire page's length. + optional float page_scroll_percentage = 2 [default = 0.0]; + + // The minimum time (seconds) the user spends on the page required for + // auto peek. + optional float minimum_seconds_on_page = 3 [default = 0.0]; + + // The maximum number of auto peeks that we can show for this page. + // If set to 0, we will never auto peek. Service can set this to a very + // large number to effectively have unlimited auto peeks. + optional uint64 maximum_number_of_peeks = 4 [default = 0]; +} + +message PeekText { + // Text to display to the user. + optional string text = 1; +} + +message Url { + optional string raw_url = 1; +} + +message Image { + // The identity of this image. + optional ImageId id = 1; +} + +message ImageId { + // The encrypted document ID of the image, which is a base64-encoded string + // that can be used to generate image URLs of the form: + // + // "http://www.google.com/images?q=tbn:<encrypted doc id>" + // + // For example, the image: + // + // "http://www.google.com/images?q=tbn:e-AB_0zkH08qtM + // + // ... has an encrypted doc ID of "e-AB_0zkH08qtM". + optional string encrypted_docid = 1; +} diff --git a/chromium/components/ntp_snippets/mock_content_suggestions_provider.cc b/chromium/components/ntp_snippets/mock_content_suggestions_provider.cc index cf447b336ba..46dc34f6e74 100644 --- a/chromium/components/ntp_snippets/mock_content_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/mock_content_suggestions_provider.cc @@ -64,6 +64,12 @@ void MockContentSuggestionsProvider::FetchSuggestionImage( FetchSuggestionImageMock(id, callback); } +void MockContentSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& id, + ImageDataFetchedCallback callback) { + FetchSuggestionImageDataMock(id, &callback); +} + void MockContentSuggestionsProvider::FireSuggestionsChanged( Category category, std::vector<ContentSuggestion> suggestions) { diff --git a/chromium/components/ntp_snippets/mock_content_suggestions_provider.h b/chromium/components/ntp_snippets/mock_content_suggestions_provider.h index 22c43d1d660..7cabecf8b97 100644 --- a/chromium/components/ntp_snippets/mock_content_suggestions_provider.h +++ b/chromium/components/ntp_snippets/mock_content_suggestions_provider.h @@ -76,8 +76,12 @@ class MockContentSuggestionsProvider : public ContentSuggestionsProvider { void(const ContentSuggestion::ID& suggestion_id)); void FetchSuggestionImage(const ContentSuggestion::ID& id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& id, + ImageDataFetchedCallback callback) override; MOCK_METHOD2(FetchSuggestionImageMock, void(const ContentSuggestion::ID&, const ImageFetchedCallback&)); + MOCK_METHOD2(FetchSuggestionImageDataMock, + void(const ContentSuggestion::ID&, ImageDataFetchedCallback*)); private: std::vector<Category> provided_categories_; diff --git a/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.cc b/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.cc index 728ca5c95fc..8c64425f1a3 100644 --- a/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.cc @@ -113,6 +113,15 @@ void RecentTabSuggestionsProvider::FetchSuggestionImage( FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); } +void RecentTabSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + // TODO(vitaliii): Fetch proper thumbnail from OfflinePageModel once it's + // available there. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); +} + void RecentTabSuggestionsProvider::Fetch( const Category& category, const std::set<std::string>& known_suggestion_ids, diff --git a/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.h b/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.h index 9aa31ce2a36..2c8c11e3228 100644 --- a/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.h +++ b/chromium/components/ntp_snippets/offline_pages/recent_tab_suggestions_provider.h @@ -44,6 +44,8 @@ class RecentTabSuggestionsProvider : public ContentSuggestionsProvider, void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; diff --git a/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.cc b/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.cc index 8d05d52f526..357a8c74c2e 100644 --- a/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.cc @@ -145,6 +145,13 @@ void PhysicalWebPageSuggestionsProvider::FetchSuggestionImage( raw_data.size()))); } +void PhysicalWebPageSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + // Not implemented for this provider. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); +} void PhysicalWebPageSuggestionsProvider::Fetch( const Category& category, const std::set<std::string>& known_suggestion_ids, diff --git a/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.h b/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.h index 20c00f82fcf..c4c1130ad65 100644 --- a/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.h +++ b/chromium/components/ntp_snippets/physical_web_pages/physical_web_page_suggestions_provider.h @@ -44,6 +44,8 @@ class PhysicalWebPageSuggestionsProvider void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc index b8a12e1d3ac..e4937822070 100644 --- a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.cc @@ -29,7 +29,7 @@ const int kMaxEntries = 3; bool CompareEntries(const ReadingListEntry* lhs, const ReadingListEntry* rhs) { return lhs->UpdateTime() > rhs->UpdateTime(); } -} +} // namespace ReadingListSuggestionsProvider::ReadingListSuggestionsProvider( ContentSuggestionsProvider::Observer* observer, @@ -86,6 +86,13 @@ void ReadingListSuggestionsProvider::FetchSuggestionImage( FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); } +void ReadingListSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); +} + void ReadingListSuggestionsProvider::Fetch( const Category& category, const std::set<std::string>& known_suggestion_ids, diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h index 69c603fa8a0..1a2aeee8f50 100644 --- a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider.h @@ -35,6 +35,8 @@ class ReadingListSuggestionsProvider : public ContentSuggestionsProvider, void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; diff --git a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc index cc6065ecc9c..8c1d72592b2 100644 --- a/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc +++ b/chromium/components/ntp_snippets/reading_list/reading_list_suggestions_provider_unittest.cc @@ -30,11 +30,8 @@ using ::testing::Property; class ReadingListSuggestionsProviderTest : public ::testing::Test { public: ReadingListSuggestionsProviderTest() { - std::unique_ptr<base::SimpleTestClock> clock = - std::make_unique<base::SimpleTestClock>(); - clock_ = clock.get(); model_ = std::make_unique<ReadingListModelImpl>( - /*storage_layer=*/nullptr, /*pref_service=*/nullptr, std::move(clock)); + /*storage_layer=*/nullptr, /*pref_service=*/nullptr, &clock_); } void CreateProvider() { @@ -56,23 +53,23 @@ class ReadingListSuggestionsProviderTest : public ::testing::Test { void AddEntries() { model_->AddEntry(url_unread1_, kTitleUnread1, reading_list::ADDED_VIA_CURRENT_APP); - clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + clock_.Advance(base::TimeDelta::FromMilliseconds(10)); model_->AddEntry(url_unread2_, kTitleUnread2, reading_list::ADDED_VIA_CURRENT_APP); - clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + clock_.Advance(base::TimeDelta::FromMilliseconds(10)); model_->AddEntry(url_read1_, kTitleRead1, reading_list::ADDED_VIA_CURRENT_APP); model_->SetReadStatus(url_read1_, true); - clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + clock_.Advance(base::TimeDelta::FromMilliseconds(10)); model_->AddEntry(url_unread3_, kTitleUnread3, reading_list::ADDED_VIA_CURRENT_APP); - clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + clock_.Advance(base::TimeDelta::FromMilliseconds(10)); model_->AddEntry(url_unread4_, kTitleUnread4, reading_list::ADDED_VIA_CURRENT_APP); } protected: - base::SimpleTestClock* clock_; + base::SimpleTestClock clock_; std::unique_ptr<ReadingListModelImpl> model_; testing::StrictMock<MockContentSuggestionsProviderObserver> observer_; std::unique_ptr<ReadingListSuggestionsProvider> provider_; @@ -117,7 +114,7 @@ TEST_F(ReadingListSuggestionsProviderTest, ReturnsOnlyUnreadSuggestion) { std::string title_read1 = "title_read1"; model_->AddEntry(url_unread1, title_unread1, reading_list::ADDED_VIA_CURRENT_APP); - clock_->Advance(base::TimeDelta::FromMilliseconds(10)); + clock_.Advance(base::TimeDelta::FromMilliseconds(10)); model_->AddEntry(url_read1, title_read1, reading_list::ADDED_VIA_CURRENT_APP); model_->SetReadStatus(url_read1, true); diff --git a/chromium/components/ntp_snippets/remote/cached_image_fetcher.cc b/chromium/components/ntp_snippets/remote/cached_image_fetcher.cc index b9995faa750..eaeda716d4c 100644 --- a/chromium/components/ntp_snippets/remote/cached_image_fetcher.cc +++ b/chromium/components/ntp_snippets/remote/cached_image_fetcher.cc @@ -3,9 +3,10 @@ // found in the LICENSE file. #include "components/ntp_snippets/remote/cached_image_fetcher.h" - #include "base/bind.h" +#include "base/bind_helpers.h" #include "base/location.h" +#include "base/logging.h" #include "components/image_fetcher/core/image_decoder.h" #include "components/image_fetcher/core/image_fetcher.h" #include "components/ntp_snippets/remote/remote_suggestions_database.h" @@ -14,6 +15,31 @@ #include "ui/gfx/image/image.h" namespace ntp_snippets { +namespace { +constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = + net::DefineNetworkTrafficAnnotation("remote_suggestions_provider", R"( + semantics { + sender: "Content Suggestion Thumbnail Fetch" + description: + "Retrieves thumbnails for content suggestions, for display on the " + "New Tab page or Chrome Home." + trigger: + "Triggered when the user looks at a content suggestion (and its " + "thumbnail isn't cached yet)." + data: "None." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: "Currently not available, but in progress: crbug.com/703684" + chrome_policy { + NTPContentSuggestionsEnabled { + policy_options {mode: MANDATORY} + NTPContentSuggestionsEnabled: false + } + } + })"); +} // namespace CachedImageFetcher::CachedImageFetcher( std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher, @@ -26,7 +52,6 @@ CachedImageFetcher::CachedImageFetcher( RequestThrottler::RequestType::CONTENT_SUGGESTION_THUMBNAIL) { // |image_fetcher_| can be null in tests. if (image_fetcher_) { - image_fetcher_->SetImageFetcherDelegate(this); image_fetcher_->SetDataUseServiceName( data_use_measurement::DataUseUserData::NTP_SNIPPETS_THUMBNAILS); } @@ -37,23 +62,13 @@ CachedImageFetcher::~CachedImageFetcher() {} void CachedImageFetcher::FetchSuggestionImage( const ContentSuggestion::ID& suggestion_id, const GURL& url, - ImageFetchedCallback callback) { + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback) { database_->LoadImage( suggestion_id.id_within_category(), - base::Bind(&CachedImageFetcher::OnImageFetchedFromDatabase, - base::Unretained(this), base::Passed(std::move(callback)), - suggestion_id, url)); -} - -// This function gets only called for caching the image data received from the -// network. The actual decoding is done in OnImageDecodedFromDatabase(). -void CachedImageFetcher::OnImageDataFetched( - const std::string& id_within_category, - const std::string& image_data) { - if (image_data.empty()) { - return; - } - database_->SaveImage(id_within_category, image_data); + base::BindOnce(&CachedImageFetcher::OnImageFetchedFromDatabase, + base::Unretained(this), std::move(image_data_callback), + std::move(image_callback), suggestion_id, url)); } void CachedImageFetcher::OnImageDecodingDone( @@ -65,23 +80,32 @@ void CachedImageFetcher::OnImageDecodingDone( } void CachedImageFetcher::OnImageFetchedFromDatabase( - ImageFetchedCallback callback, + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback, const ContentSuggestion::ID& suggestion_id, const GURL& url, std::string data) { // SnippetImageCallback requires by-value. - // The image decoder is null in tests. - if (image_fetcher_->GetImageDecoder() && !data.empty()) { + if (data.empty()) { + // Fetching from the DB failed; start a network fetch. + FetchImageFromNetwork(suggestion_id, url, std::move(image_data_callback), + std::move(image_callback)); + return; + } + + if (image_data_callback) { + std::move(image_data_callback).Run(data); + } + + if (image_callback) { image_fetcher_->GetImageDecoder()->DecodeImage( data, // We're not dealing with multi-frame images. /*desired_image_frame_size=*/gfx::Size(), base::Bind(&CachedImageFetcher::OnImageDecodedFromDatabase, - base::Unretained(this), base::Passed(std::move(callback)), - suggestion_id, url)); - return; + base::Unretained(this), + base::Passed(std::move(image_callback)), suggestion_id, + url)); } - // Fetching from the DB failed; start a network fetch. - FetchImageFromNetwork(suggestion_id, url, std::move(callback)); } void CachedImageFetcher::OnImageDecodedFromDatabase( @@ -95,50 +119,55 @@ void CachedImageFetcher::OnImageDecodedFromDatabase( } // If decoding the image failed, delete the DB entry. database_->DeleteImage(suggestion_id.id_within_category()); - FetchImageFromNetwork(suggestion_id, url, std::move(callback)); + FetchImageFromNetwork(suggestion_id, url, ImageDataFetchedCallback(), + std::move(callback)); } void CachedImageFetcher::FetchImageFromNetwork( const ContentSuggestion::ID& suggestion_id, const GURL& url, - ImageFetchedCallback callback) { + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback) { if (url.is_empty() || !thumbnail_requests_throttler_.DemandQuotaForRequest( /*interactive_request=*/true)) { // Return an empty image. Directly, this is never synchronous with the // original FetchSuggestionImage() call - an asynchronous database query has // happened in the meantime. - std::move(callback).Run(gfx::Image()); + if (image_data_callback) { + std::move(image_data_callback).Run(std::string()); + } + if (image_callback) { + std::move(image_callback).Run(gfx::Image()); + } return; } + // Image decoding callback only set when requested. + image_fetcher::ImageFetcher::ImageFetcherCallback decode_callback; + if (image_callback) { + decode_callback = base::BindOnce(&CachedImageFetcher::OnImageDecodingDone, + base::Unretained(this), + base::Passed(std::move(image_callback))); + } - net::NetworkTrafficAnnotationTag traffic_annotation = - net::DefineNetworkTrafficAnnotation("remote_suggestions_provider", R"( - semantics { - sender: "Content Suggestion Thumbnail Fetch" - description: - "Retrieves thumbnails for content suggestions, for display on the " - "New Tab page or Chrome Home." - trigger: - "Triggered when the user looks at a content suggestion (and its " - "thumbnail isn't cached yet)." - data: "None." - destination: GOOGLE_OWNED_SERVICE - } - policy { - cookies_allowed: NO - setting: "Currently not available, but in progress: crbug.com/703684" - chrome_policy { - NTPContentSuggestionsEnabled { - policy_options {mode: MANDATORY} - NTPContentSuggestionsEnabled: false - } - } - })"); - image_fetcher_->StartOrQueueNetworkRequest( + image_fetcher_->FetchImageAndData( suggestion_id.id_within_category(), url, - base::Bind(&CachedImageFetcher::OnImageDecodingDone, - base::Unretained(this), base::Passed(std::move(callback))), - traffic_annotation); + base::BindOnce(&CachedImageFetcher::SaveImageAndInvokeDataCallback, + base::Unretained(this), suggestion_id.id_within_category(), + std::move(image_data_callback)), + std::move(decode_callback), kTrafficAnnotation); +} + +void CachedImageFetcher::SaveImageAndInvokeDataCallback( + const std::string& id_within_category, + ImageDataFetchedCallback callback, + const std::string& image_data, + const image_fetcher::RequestMetadata& request_metadata) { + if (!image_data.empty()) { + database_->SaveImage(id_within_category, image_data); + } + if (callback) { + std::move(callback).Run(image_data); + } } } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/remote/cached_image_fetcher.h b/chromium/components/ntp_snippets/remote/cached_image_fetcher.h index 51eef498ac9..d1e1c6d55d6 100644 --- a/chromium/components/ntp_snippets/remote/cached_image_fetcher.h +++ b/chromium/components/ntp_snippets/remote/cached_image_fetcher.h @@ -13,7 +13,6 @@ #include "base/callback_forward.h" #include "base/gtest_prod_util.h" #include "base/macros.h" -#include "components/image_fetcher/core/image_fetcher_delegate.h" #include "components/ntp_snippets/callbacks.h" #include "components/ntp_snippets/content_suggestion.h" #include "components/ntp_snippets/remote/request_throttler.h" @@ -35,43 +34,55 @@ class RemoteSuggestionsDatabase; // CachedImageFetcher takes care of fetching images from the network and caching // them in the database. -class CachedImageFetcher : public image_fetcher::ImageFetcherDelegate { +class CachedImageFetcher { public: // |pref_service| and |database| need to outlive the created image fetcher // instance. CachedImageFetcher(std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher, PrefService* pref_service, RemoteSuggestionsDatabase* database); - ~CachedImageFetcher() override; + virtual ~CachedImageFetcher(); // Fetches the image for a suggestion. The fetcher will first issue a lookup // to the underlying cache with a fallback to the network. - virtual void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, - const GURL& image_url, - ImageFetchedCallback callback); + virtual void FetchSuggestionImage( + const ContentSuggestion::ID& suggestion_id, + const GURL& image_url, + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback); private: - // image_fetcher::ImageFetcherDelegate implementation. void OnImageDataFetched(const std::string& id_within_category, - const std::string& image_data) override; + const std::string& image_data, + const image_fetcher::RequestMetadata& metadata); void OnImageDecodingDone(ImageFetchedCallback callback, const std::string& id_within_category, const gfx::Image& image, const image_fetcher::RequestMetadata& metadata); + void OnImageFetchedFromDatabase( - ImageFetchedCallback callback, + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback, const ContentSuggestion::ID& suggestion_id, const GURL& image_url, // SnippetImageCallback requires by-value (not const ref). std::string data); + void OnImageDecodedFromDatabase(ImageFetchedCallback callback, const ContentSuggestion::ID& suggestion_id, const GURL& url, const gfx::Image& image); + void FetchImageFromNetwork(const ContentSuggestion::ID& suggestion_id, const GURL& url, - ImageFetchedCallback callback); + ImageDataFetchedCallback image_data_callback, + ImageFetchedCallback image_callback); + void SaveImageAndInvokeDataCallback( + const std::string& id_within_category, + ImageDataFetchedCallback callback, + const std::string& image_data, + const image_fetcher::RequestMetadata& request_metadata); std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher_; RemoteSuggestionsDatabase* database_; diff --git a/chromium/components/ntp_snippets/remote/cached_image_fetcher_unittest.cc b/chromium/components/ntp_snippets/remote/cached_image_fetcher_unittest.cc index 48e47227776..64dc9e19951 100644 --- a/chromium/components/ntp_snippets/remote/cached_image_fetcher_unittest.cc +++ b/chromium/components/ntp_snippets/remote/cached_image_fetcher_unittest.cc @@ -37,6 +37,9 @@ namespace { const char kImageData[] = "data"; const char kImageURL[] = "http://image.test/test.png"; const char kSnippetID[] = "http://localhost"; +const ContentSuggestion::ID kSuggestionID( + Category::FromKnownCategory(KnownCategories::ARTICLES), + kSnippetID); // Always decodes a valid image for all non-empty input. class FakeImageDecoder : public image_fetcher::ImageDecoder { @@ -45,17 +48,31 @@ class FakeImageDecoder : public image_fetcher::ImageDecoder { const std::string& image_data, const gfx::Size& desired_image_frame_size, const image_fetcher::ImageDecodedCallback& callback) override { + ASSERT_TRUE(enabled_); gfx::Image image; if (!image_data.empty()) { + ASSERT_EQ(kImageData, image_data); image = gfx::test::CreateImage(); } - callback.Run(image); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindRepeating(callback, image)); } + void SetEnabled(bool enabled) { enabled_ = enabled; } + + private: + bool enabled_ = true; }; +enum class TestType { + kImageCallback, + kImageDataCallback, + kBothCallbacks, +}; } // namespace -class CachedImageFetcherTest : public testing::Test { +// This test is parameterized to run all tests in the three configurations: +// both callbacks used, only image_callback used, only image_data_callback used. +class CachedImageFetcherTest : public testing::TestWithParam<TestType> { public: CachedImageFetcherTest() : fake_url_fetcher_factory_(nullptr) { EXPECT_TRUE(database_dir_.CreateUniqueTempDir()); @@ -85,11 +102,36 @@ class CachedImageFetcherTest : public testing::Test { RunUntilIdle(); } - void FetchImage(ImageFetchedCallback callback) { - ContentSuggestion::ID content_suggestion_id( - Category::FromKnownCategory(KnownCategories::ARTICLES), kSnippetID); - cached_image_fetcher_->FetchSuggestionImage( - content_suggestion_id, GURL(kImageURL), std::move(callback)); + void Fetch(std::string expected_image_data, bool expect_image) { + fake_image_decoder()->SetEnabled(GetParam() != + TestType::kImageDataCallback); + base::MockCallback<ImageFetchedCallback> image_callback; + base::MockCallback<ImageDataFetchedCallback> image_data_callback; + switch (GetParam()) { + case TestType::kImageCallback: { + EXPECT_CALL(image_callback, + Run(Property(&gfx::Image::IsEmpty, Eq(!expect_image)))); + cached_image_fetcher()->FetchSuggestionImage( + kSuggestionID, GURL(kImageURL), ImageDataFetchedCallback(), + image_callback.Get()); + + } break; + case TestType::kImageDataCallback: { + EXPECT_CALL(image_data_callback, Run(expected_image_data)); + cached_image_fetcher()->FetchSuggestionImage( + kSuggestionID, GURL(kImageURL), image_data_callback.Get(), + ImageFetchedCallback()); + } break; + case TestType::kBothCallbacks: { + EXPECT_CALL(image_data_callback, Run(expected_image_data)); + EXPECT_CALL(image_callback, + Run(Property(&gfx::Image::IsEmpty, Eq(!expect_image)))); + cached_image_fetcher()->FetchSuggestionImage( + kSuggestionID, GURL(kImageURL), image_data_callback.Get(), + image_callback.Get()); + } break; + } + RunUntilIdle(); } void RunUntilIdle() { scoped_task_environment_.RunUntilIdle(); } @@ -99,6 +141,9 @@ class CachedImageFetcherTest : public testing::Test { net::FakeURLFetcherFactory* fake_url_fetcher_factory() { return &fake_url_fetcher_factory_; } + CachedImageFetcher* cached_image_fetcher() { + return cached_image_fetcher_.get(); + } private: std::unique_ptr<CachedImageFetcher> cached_image_fetcher_; @@ -113,44 +158,44 @@ class CachedImageFetcherTest : public testing::Test { DISALLOW_COPY_AND_ASSIGN(CachedImageFetcherTest); }; -TEST_F(CachedImageFetcherTest, FetchImageFromCache) { +TEST_P(CachedImageFetcherTest, FetchImageFromCache) { // Save the image in the database. database()->SaveImage(kSnippetID, kImageData); RunUntilIdle(); // Do not provide any URL responses and expect that the image is fetched (from // cache). - base::MockCallback<ImageFetchedCallback> mock_image_fetched_callback; - EXPECT_CALL(mock_image_fetched_callback, - Run(Property(&gfx::Image::IsEmpty, Eq(false)))); - FetchImage(mock_image_fetched_callback.Get()); - RunUntilIdle(); + Fetch(kImageData, true); } -TEST_F(CachedImageFetcherTest, FetchImageNotInCache) { +TEST_P(CachedImageFetcherTest, FetchImagePopulatesCache) { // Expect the image to be fetched by URL. - fake_url_fetcher_factory()->SetFakeResponse(GURL(kImageURL), kImageData, - net::HTTP_OK, - net::URLRequestStatus::SUCCESS); - base::MockCallback<ImageFetchedCallback> mock_image_fetched_callback; - EXPECT_CALL(mock_image_fetched_callback, - Run(Property(&gfx::Image::IsEmpty, Eq(false)))); - FetchImage(mock_image_fetched_callback.Get()); - RunUntilIdle(); + { + fake_url_fetcher_factory()->SetFakeResponse(GURL(kImageURL), kImageData, + net::HTTP_OK, + net::URLRequestStatus::SUCCESS); + Fetch(kImageData, true); + } + // Fetch again. The cache should be populated, no network request is needed. + { + fake_url_fetcher_factory()->ClearFakeResponses(); + Fetch(kImageData, true); + } } -TEST_F(CachedImageFetcherTest, FetchNonExistingImage) { +TEST_P(CachedImageFetcherTest, FetchNonExistingImage) { const std::string kErrorResponse = "error-response"; fake_url_fetcher_factory()->SetFakeResponse(GURL(kImageURL), kErrorResponse, net::HTTP_NOT_FOUND, net::URLRequestStatus::FAILED); // Expect an empty image is fetched if the URL cannot be requested. - const std::string kEmptyImageData; - base::MockCallback<ImageFetchedCallback> mock_image_fetched_callback; - EXPECT_CALL(mock_image_fetched_callback, - Run(Property(&gfx::Image::IsEmpty, Eq(true)))); - FetchImage(mock_image_fetched_callback.Get()); - RunUntilIdle(); + Fetch("", false); } +INSTANTIATE_TEST_CASE_P(, + CachedImageFetcherTest, + testing::Values(TestType::kImageCallback, + TestType::kImageDataCallback, + TestType::kBothCallbacks)); + } // namespace ntp_snippets diff --git a/chromium/components/ntp_snippets/remote/json_request_unittest.cc b/chromium/components/ntp_snippets/remote/json_request_unittest.cc index 08af1967adb..24da870e3e1 100644 --- a/chromium/components/ntp_snippets/remote/json_request_unittest.cc +++ b/chromium/components/ntp_snippets/remote/json_request_unittest.cc @@ -65,7 +65,6 @@ class JsonRequestTest : public testing::Test { {ntp_snippets::kArticleSuggestionsFeature.name}), pref_service_(std::make_unique<TestingPrefServiceSimple>()), mock_task_runner_(new base::TestMockTimeTaskRunner()), - clock_(mock_task_runner_->GetMockClock()), request_context_getter_( new net::TestURLRequestContextGetter(mock_task_runner_.get())) { language::UrlLanguageHistogram::RegisterProfilePrefs( @@ -88,7 +87,7 @@ class JsonRequestTest : public testing::Test { JsonRequest::Builder CreateMinimalBuilder() { JsonRequest::Builder builder; builder.SetUrl(GURL("http://valid-url.test")) - .SetClock(clock_.get()) + .SetClock(mock_task_runner_->GetMockClock()) .SetUrlRequestContextGetter(request_context_getter_.get()); return builder; } @@ -97,7 +96,6 @@ class JsonRequestTest : public testing::Test { variations::testing::VariationParamsManager params_manager_; std::unique_ptr<TestingPrefServiceSimple> pref_service_; scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_; - std::unique_ptr<base::Clock> clock_; scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; net::TestURLFetcherFactory fetcher_factory_; diff --git a/chromium/components/ntp_snippets/remote/remote_suggestion.h b/chromium/components/ntp_snippets/remote/remote_suggestion.h index 7aef0b66549..9da686d9d62 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestion.h +++ b/chromium/components/ntp_snippets/remote/remote_suggestion.h @@ -32,7 +32,7 @@ class RemoteSuggestion { public: using PtrVector = std::vector<std::unique_ptr<RemoteSuggestion>>; - enum ContentType { UNKNOWN, VIDEO }; + enum class ContentType { UNKNOWN, VIDEO }; ~RemoteSuggestion(); diff --git a/chromium/components/ntp_snippets/remote/remote_suggestion_builder.cc b/chromium/components/ntp_snippets/remote/remote_suggestion_builder.cc index f552601147b..b9bcf31f997 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestion_builder.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestion_builder.cc @@ -7,7 +7,6 @@ #include <limits> #include <memory> -#include "base/memory/ptr_util.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "components/ntp_snippets/remote/proto/ntp_snippets.pb.h" diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_database.cc b/chromium/components/ntp_snippets/remote/remote_suggestions_database.cc index 7e317ad9002..c8fb4090d7d 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_database.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_database.cc @@ -26,8 +26,7 @@ const char kImageDatabaseUMAClientName[] = "NTPSnippetImages"; const char kSnippetDatabaseFolder[] = "snippets"; const char kImageDatabaseFolder[] = "images"; -const size_t kDatabaseWriteBufferSizeBytes = 512 << 10; -const size_t kDatabaseWriteBufferSizeBytesForLowEndDevice = 128 << 10; +const size_t kDatabaseWriteBufferSizeBytes = 128 << 10; } // namespace namespace ntp_snippets { @@ -59,11 +58,9 @@ RemoteSuggestionsDatabase::RemoteSuggestionsDatabase( weak_ptr_factory_(this) { base::FilePath snippet_dir = database_dir.AppendASCII(kSnippetDatabaseFolder); leveldb_env::Options options = leveldb_proto::CreateSimpleOptions(); - if (base::SysInfo::IsLowEndDevice()) { - options.write_buffer_size = kDatabaseWriteBufferSizeBytesForLowEndDevice; - } else { - options.write_buffer_size = kDatabaseWriteBufferSizeBytes; - } + options.reuse_logs = false; // Consumes less RAM over time. + options.write_buffer_size = kDatabaseWriteBufferSizeBytes; + database_->Init(kDatabaseUMAClientName, snippet_dir, options, base::Bind(&RemoteSuggestionsDatabase::OnDatabaseInited, weak_ptr_factory_.GetWeakPtr())); @@ -91,11 +88,11 @@ void RemoteSuggestionsDatabase::SetErrorCallback( error_callback_ = error_callback; } -void RemoteSuggestionsDatabase::LoadSnippets(const SnippetsCallback& callback) { +void RemoteSuggestionsDatabase::LoadSnippets(SnippetsCallback callback) { if (IsInitialized()) { - LoadSnippetsImpl(callback); + LoadSnippetsImpl(std::move(callback)); } else { - pending_snippets_callbacks_.emplace_back(callback); + pending_snippets_callbacks_.emplace_back(std::move(callback)); } } @@ -135,13 +132,12 @@ void RemoteSuggestionsDatabase::DeleteSnippets( weak_ptr_factory_.GetWeakPtr())); } -void RemoteSuggestionsDatabase::LoadImage( - const std::string& snippet_id, - const SnippetImageCallback& callback) { +void RemoteSuggestionsDatabase::LoadImage(const std::string& snippet_id, + SnippetImageCallback callback) { if (IsInitialized()) { - LoadImageImpl(snippet_id, callback); + LoadImageImpl(snippet_id, std::move(callback)); } else { - pending_image_callbacks_.emplace_back(snippet_id, callback); + pending_image_callbacks_.emplace_back(snippet_id, std::move(callback)); } } @@ -197,7 +193,7 @@ void RemoteSuggestionsDatabase::OnDatabaseInited(bool success) { } void RemoteSuggestionsDatabase::OnDatabaseLoaded( - const SnippetsCallback& callback, + SnippetsCallback callback, bool success, std::unique_ptr<std::vector<SnippetProto>> entries) { if (!success) { @@ -226,7 +222,7 @@ void RemoteSuggestionsDatabase::OnDatabaseLoaded( } } - callback.Run(std::move(snippets)); + std::move(callback).Run(std::move(snippets)); // If any of the snippet protos couldn't be converted to actual snippets, // clean them up now. @@ -256,7 +252,7 @@ void RemoteSuggestionsDatabase::OnImageDatabaseInited(bool success) { } void RemoteSuggestionsDatabase::OnImageDatabaseLoaded( - const SnippetImageCallback& callback, + SnippetImageCallback callback, bool success, std::unique_ptr<SnippetImageProto> entry) { if (!success) { @@ -266,12 +262,12 @@ void RemoteSuggestionsDatabase::OnImageDatabaseLoaded( } if (!entry) { - callback.Run(std::string()); + std::move(callback).Run(std::string()); return; } std::unique_ptr<std::string> data(entry->release_data()); - callback.Run(std::move(*data)); + std::move(callback).Run(std::move(*data)); } void RemoteSuggestionsDatabase::OnImageDatabaseSaved(bool success) { @@ -292,23 +288,22 @@ void RemoteSuggestionsDatabase::OnDatabaseError() { void RemoteSuggestionsDatabase::ProcessPendingLoads() { DCHECK(IsInitialized()); - for (const auto& callback : pending_snippets_callbacks_) { - LoadSnippetsImpl(callback); + for (auto& callback : pending_snippets_callbacks_) { + LoadSnippetsImpl(std::move(callback)); } pending_snippets_callbacks_.clear(); - for (const auto& id_callback : pending_image_callbacks_) { - LoadImageImpl(id_callback.first, id_callback.second); + for (auto& id_callback : pending_image_callbacks_) { + LoadImageImpl(id_callback.first, std::move(id_callback.second)); } pending_image_callbacks_.clear(); } -void RemoteSuggestionsDatabase::LoadSnippetsImpl( - const SnippetsCallback& callback) { +void RemoteSuggestionsDatabase::LoadSnippetsImpl(SnippetsCallback callback) { DCHECK(IsInitialized()); database_->LoadEntries( - base::Bind(&RemoteSuggestionsDatabase::OnDatabaseLoaded, - weak_ptr_factory_.GetWeakPtr(), callback)); + base::BindOnce(&RemoteSuggestionsDatabase::OnDatabaseLoaded, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } void RemoteSuggestionsDatabase::SaveSnippetsImpl( @@ -323,13 +318,13 @@ void RemoteSuggestionsDatabase::SaveSnippetsImpl( weak_ptr_factory_.GetWeakPtr())); } -void RemoteSuggestionsDatabase::LoadImageImpl( - const std::string& snippet_id, - const SnippetImageCallback& callback) { +void RemoteSuggestionsDatabase::LoadImageImpl(const std::string& snippet_id, + SnippetImageCallback callback) { DCHECK(IsInitialized()); image_database_->GetEntry( - snippet_id, base::Bind(&RemoteSuggestionsDatabase::OnImageDatabaseLoaded, - weak_ptr_factory_.GetWeakPtr(), callback)); + snippet_id, + base::BindOnce(&RemoteSuggestionsDatabase::OnImageDatabaseLoaded, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } void RemoteSuggestionsDatabase::DeleteUnreferencedImages( diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_database.h b/chromium/components/ntp_snippets/remote/remote_suggestions_database.h index 6966cf13040..c957ddebd4c 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_database.h +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_database.h @@ -31,8 +31,9 @@ class SnippetProto; // TODO(gaschler): implement a Fake version for testing class RemoteSuggestionsDatabase { public: - using SnippetsCallback = base::Callback<void(RemoteSuggestion::PtrVector)>; - using SnippetImageCallback = base::Callback<void(std::string)>; + using SnippetsCallback = + base::OnceCallback<void(RemoteSuggestion::PtrVector)>; + using SnippetImageCallback = base::OnceCallback<void(std::string)>; // Creates a RemoteSuggestionsDatabase backed by real ProtoDatabaseImpls. RemoteSuggestionsDatabase(const base::FilePath& database_dir); @@ -58,7 +59,7 @@ class RemoteSuggestionsDatabase { void SetErrorCallback(const base::Closure& error_callback); // Loads all snippets from storage and passes them to |callback|. - void LoadSnippets(const SnippetsCallback& callback); + void LoadSnippets(SnippetsCallback callback); // Adds or updates the given snippet. void SaveSnippet(const RemoteSuggestion& snippet); @@ -72,8 +73,7 @@ class RemoteSuggestionsDatabase { // Loads the image data for the snippet with the given ID and passes it to // |callback|. Passes an empty string if not found. - void LoadImage(const std::string& snippet_id, - const SnippetImageCallback& callback); + void LoadImage(const std::string& snippet_id, SnippetImageCallback callback); // Adds or updates the image data for the given snippet ID. void SaveImage(const std::string& snippet_id, const std::string& image_data); @@ -102,14 +102,14 @@ class RemoteSuggestionsDatabase { // Callbacks for ProtoDatabase<SnippetProto> operations. void OnDatabaseInited(bool success); - void OnDatabaseLoaded(const SnippetsCallback& callback, + void OnDatabaseLoaded(SnippetsCallback callback, bool success, std::unique_ptr<std::vector<SnippetProto>> entries); void OnDatabaseSaved(bool success); // Callbacks for ProtoDatabase<SnippetImageProto> operations. void OnImageDatabaseInited(bool success); - void OnImageDatabaseLoaded(const SnippetImageCallback& callback, + void OnImageDatabaseLoaded(SnippetImageCallback callback, bool success, std::unique_ptr<SnippetImageProto> entry); void OnImageDatabaseSaved(bool success); @@ -118,11 +118,11 @@ class RemoteSuggestionsDatabase { void ProcessPendingLoads(); - void LoadSnippetsImpl(const SnippetsCallback& callback); + void LoadSnippetsImpl(SnippetsCallback callback); void SaveSnippetsImpl(std::unique_ptr<KeyEntryVector> entries_to_save); void LoadImageImpl(const std::string& snippet_id, - const SnippetImageCallback& callback); + SnippetImageCallback callback); void DeleteUnreferencedImages( std::unique_ptr<std::set<std::string>> references, bool load_keys_success, diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_fetcher_impl_unittest.cc b/chromium/components/ntp_snippets/remote/remote_suggestions_fetcher_impl_unittest.cc index f748abd2d6c..fe213ab6560 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_fetcher_impl_unittest.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_fetcher_impl_unittest.cc @@ -288,8 +288,7 @@ class RemoteSuggestionsFetcherImplTest : public testing::Test { GetFetchEndpoint(version_info::Channel::STABLE), api_key, user_classifier_.get()); - clock_ = mock_task_runner_->GetMockClock(); - fetcher_->SetClockForTesting(clock_.get()); + fetcher_->SetClockForTesting(mock_task_runner_->GetMockClock()); } void SignIn() { identity_test_env_.MakePrimaryAccountAvailable(kTestEmail); } @@ -348,10 +347,6 @@ class RemoteSuggestionsFetcherImplTest : public testing::Test { identity::IdentityTestEnvironment identity_test_env_; private: - // TODO(tzik): Remove |clock_| after updating GetMockTickClock to own the - // instance. http://crbug.com/789079 - std::unique_ptr<base::Clock> clock_; - test::RemoteSuggestionsTestUtils utils_; variations::testing::VariationParamsManager params_manager_; scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_; diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.cc b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.cc index 0035b61911b..25e7edb4fc2 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.cc @@ -418,8 +418,8 @@ RemoteSuggestionsProviderImpl::RemoteSuggestionsProviderImpl( // database is done loading. database_load_start_ = base::TimeTicks::Now(); database_->LoadSnippets( - base::Bind(&RemoteSuggestionsProviderImpl::OnDatabaseLoaded, - base::Unretained(this))); + base::BindOnce(&RemoteSuggestionsProviderImpl::OnDatabaseLoaded, + base::Unretained(this))); } RemoteSuggestionsProviderImpl::~RemoteSuggestionsProviderImpl() { @@ -442,7 +442,7 @@ void RemoteSuggestionsProviderImpl::ReloadSuggestions() { if (!remote_suggestions_scheduler_->AcquireQuotaForInteractiveFetch()) { return; } - auto callback = base::Bind( + auto callback = base::BindOnce( [](RemoteSuggestionsScheduler* scheduler, Status status_code) { scheduler->OnInteractiveFetchFinished(status_code); }, @@ -1423,25 +1423,46 @@ void RemoteSuggestionsProviderImpl::NukeAllSuggestions() { StoreCategoriesToPrefs(); } +GURL RemoteSuggestionsProviderImpl::GetImageURLToFetch( + const ContentSuggestion::ID& suggestion_id) const { + if (!base::ContainsKey(category_contents_, suggestion_id.category())) { + return GURL(); + } + return FindSuggestionImageUrl(suggestion_id); +} + void RemoteSuggestionsProviderImpl::FetchSuggestionImage( const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) { - if (!base::ContainsKey(category_contents_, suggestion_id.category())) { + GURL image_url = GetImageURLToFetch(suggestion_id); + if (image_url.is_empty()) { + // As we don't know the corresponding suggestion anymore, we don't expect to + // find it in the database (and also can't fetch it remotely). Cut the + // lookup short and return directly. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); return; } - GURL image_url = FindSuggestionImageUrl(suggestion_id); + image_fetcher_.FetchSuggestionImage(suggestion_id, image_url, + ImageDataFetchedCallback(), + std::move(callback)); +} + +void RemoteSuggestionsProviderImpl::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + GURL image_url = GetImageURLToFetch(suggestion_id); if (image_url.is_empty()) { // As we don't know the corresponding suggestion anymore, we don't expect to // find it in the database (and also can't fetch it remotely). Cut the // lookup short and return directly. base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); + FROM_HERE, base::BindOnce(std::move(callback), std::string())); return; } image_fetcher_.FetchSuggestionImage(suggestion_id, image_url, - std::move(callback)); + std::move(callback), + ntp_snippets::ImageFetchedCallback()); } void RemoteSuggestionsProviderImpl:: diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.h b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.h index 46a367e7ead..8a9c830e1eb 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.h +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl.h @@ -102,6 +102,8 @@ class RemoteSuggestionsProviderImpl final : public RemoteSuggestionsProvider { void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; @@ -417,6 +419,8 @@ class RemoteSuggestionsProviderImpl final : public RemoteSuggestionsProvider { void NotifyFetchWithLoadingIndicatorStarted(); void NotifyFetchWithLoadingIndicatorFailedOrTimeouted(); + GURL GetImageURLToFetch(const ContentSuggestion::ID& suggestion_id) const; + State state_; PrefService* pref_service_; diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl_unittest.cc b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl_unittest.cc index 6e55a78f265..8f579741544 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl_unittest.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_provider_impl_unittest.cc @@ -31,7 +31,7 @@ #include "base/timer/timer.h" #include "components/image_fetcher/core/image_decoder.h" #include "components/image_fetcher/core/image_fetcher.h" -#include "components/image_fetcher/core/image_fetcher_delegate.h" +#include "components/image_fetcher/core/mock_image_fetcher.h" #include "components/image_fetcher/core/request_metadata.h" #include "components/ntp_snippets/breaking_news/breaking_news_listener.h" #include "components/ntp_snippets/category.h" @@ -69,7 +69,7 @@ using base::TestMockTimeTaskRunner; using image_fetcher::ImageFetcher; -using image_fetcher::ImageFetcherDelegate; +using image_fetcher::MockImageFetcher; using ntp_snippets::test::FetchedCategoryBuilder; using ntp_snippets::test::RemoteSuggestionBuilder; using testing::_; @@ -162,22 +162,16 @@ std::unique_ptr<RemoteSuggestion> CreateTestRemoteSuggestion( return RemoteSuggestion::CreateFromProto(snippet_proto); } -using ServeImageCallback = base::Callback<void( - const std::string&, - base::Callback<void(const std::string&, - const gfx::Image&, - const image_fetcher::RequestMetadata&)>)>; - void ServeOneByOneImage( - image_fetcher::ImageFetcherDelegate* notify, const std::string& id, - base::Callback<void(const std::string&, - const gfx::Image&, - const image_fetcher::RequestMetadata&)> callback) { + image_fetcher::ImageFetcher::ImageDataFetcherCallback* image_data_callback, + image_fetcher::ImageFetcher::ImageFetcherCallback* callback) { + std::move(*image_data_callback) + .Run("1-by-1-image-data", image_fetcher::RequestMetadata()); base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::Bind(callback, id, gfx::test::CreateImage(1, 1), - image_fetcher::RequestMetadata())); - notify->OnImageDataFetched(id, "1-by-1-image-data"); + FROM_HERE, + base::BindOnce(std::move(*callback), id, gfx::test::CreateImage(1, 1), + image_fetcher::RequestMetadata())); } gfx::Image FetchImage(RemoteSuggestionsProviderImpl* provider, @@ -196,21 +190,6 @@ gfx::Image FetchImage(RemoteSuggestionsProviderImpl* provider, return result; } -class MockImageFetcher : public ImageFetcher { - public: - MOCK_METHOD1(SetImageFetcherDelegate, void(ImageFetcherDelegate*)); - MOCK_METHOD1(SetDataUseServiceName, void(DataUseServiceName)); - MOCK_METHOD1(SetImageDownloadLimit, - void(base::Optional<int64_t> max_download_bytes)); - MOCK_METHOD1(SetDesiredImageFrameSize, void(const gfx::Size&)); - MOCK_METHOD4(StartOrQueueNetworkRequest, - void(const std::string&, - const GURL&, - const ImageFetcherCallback&, - const net::NetworkTrafficAnnotationTag&)); - MOCK_METHOD0(GetImageDecoder, image_fetcher::ImageDecoder*()); -}; - class FakeImageDecoder : public image_fetcher::ImageDecoder { public: FakeImageDecoder() {} @@ -344,7 +323,6 @@ class RemoteSuggestionsProviderImplTest : public ::testing::Test { RemoteSuggestionsProviderImpl::RegisterProfilePrefs( utils_.pref_service()->registry()); RequestThrottler::RegisterProfilePrefs(utils_.pref_service()->registry()); - tick_clock_ = timer_mock_task_runner_->GetMockTickClock(); EXPECT_TRUE(database_dir_.CreateUniqueTempDir()); } @@ -413,7 +391,6 @@ class RemoteSuggestionsProviderImplTest : public ::testing::Test { auto image_fetcher = std::make_unique<NiceMock<MockImageFetcher>>(); image_fetcher_ = image_fetcher.get(); - EXPECT_CALL(*image_fetcher, SetImageFetcherDelegate(_)); ON_CALL(*image_fetcher, GetImageDecoder()) .WillByDefault(Return(&image_decoder_)); EXPECT_FALSE(observer_); @@ -422,8 +399,8 @@ class RemoteSuggestionsProviderImplTest : public ::testing::Test { std::make_unique<RemoteSuggestionsDatabase>(database_dir_.GetPath()); database_ = database.get(); - auto fetch_timeout_timer = - std::make_unique<base::OneShotTimer>(tick_clock_.get()); + auto fetch_timeout_timer = std::make_unique<base::OneShotTimer>( + timer_mock_task_runner_->GetMockTickClock()); fetch_timeout_timer->SetTaskRunner(timer_mock_task_runner_); return std::make_unique<RemoteSuggestionsProviderImpl>( @@ -719,11 +696,6 @@ class RemoteSuggestionsProviderImplTest : public ::testing::Test { RemoteSuggestionsDatabase* database_; Logger debug_logger_; - - // TODO(tzik): Remove |mock_tick_clock_| after updating GetMockTickClock to - // own the instance. http://crbug.com/789079 - std::unique_ptr<base::TickClock> tick_clock_; - scoped_refptr<TestMockTimeTaskRunner> timer_mock_task_runner_; DISALLOW_COPY_AND_ASSIGN(RemoteSuggestionsProviderImplTest); @@ -1348,10 +1320,10 @@ TEST_F(RemoteSuggestionsProviderImplTest, ElementsAre(Pointee(Property(&RemoteSuggestion::id, "id")))); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); - ServeImageCallback serve_one_by_one_image_callback = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>( + auto serve_one_by_one_image_callback = + base::BindRepeating(&ServeOneByOneImage); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( Invoke(CreateFunctor(serve_one_by_one_image_callback)))); gfx::Image image = FetchImage(provider.get(), MakeArticleID("id")); @@ -1420,10 +1392,10 @@ TEST_F(RemoteSuggestionsProviderImplTest, Status::Success(), std::move(fetched_categories)); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); - ServeImageCallback serve_one_by_one_image_callback = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>( + auto serve_one_by_one_image_callback = + base::BindRepeating(&ServeOneByOneImage); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( Invoke(CreateFunctor(serve_one_by_one_image_callback)))); gfx::Image image = FetchImage(provider.get(), MakeArticleID("id")); @@ -1590,11 +1562,10 @@ TEST_F(RemoteSuggestionsProviderImplTest, Status::Success(), std::move(fetched_categories)); // Make sure images of both batches are available. This is to sanity check our // assumptions for the test are right. - ServeImageCallback cb = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) .Times(2) - .WillRepeatedly(WithArgs<0, 2>(Invoke(CreateFunctor(cb)))); + .WillRepeatedly(WithArgs<0, 2, 3>( + Invoke(CreateFunctor(base::BindRepeating(&ServeOneByOneImage))))); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); gfx::Image image = FetchImage(provider.get(), MakeArticleID("http://id-1")); ASSERT_FALSE(image.IsEmpty()); @@ -1756,10 +1727,9 @@ TEST_F(RemoteSuggestionsProviderImplTest, Dismiss) { ASSERT_THAT(provider->GetSuggestionsForTesting(articles_category()), SizeIs(1)); // Load the image to store it in the database. - ServeImageCallback cb = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>(Invoke(CreateFunctor(cb)))); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( + Invoke(CreateFunctor(base::BindRepeating(&ServeOneByOneImage))))); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); gfx::Image image = FetchImage(provider.get(), MakeArticleID("http://site.com")); @@ -1883,10 +1853,9 @@ TEST_F(RemoteSuggestionsProviderImplTest, RemoveExpiredDismissedContent) { // Load the image to store it in the database. // TODO(tschumann): Introduce some abstraction to nicely work with image // fetching expectations. - ServeImageCallback cb = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>(Invoke(CreateFunctor(cb)))); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( + Invoke(CreateFunctor(base::BindRepeating(&ServeOneByOneImage))))); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); gfx::Image image = FetchImage(provider.get(), MakeArticleID("http://first/")); EXPECT_FALSE(image.IsEmpty()); @@ -2148,12 +2117,11 @@ TEST_F(RemoteSuggestionsProviderImplTest, ImageReturnedWithTheSameId) { gfx::Image image; MockFunction<void(const gfx::Image&)> image_fetched; - ServeImageCallback cb = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); { InSequence s; - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>(Invoke(CreateFunctor(cb)))); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( + Invoke(CreateFunctor(base::BindRepeating(&ServeOneByOneImage))))); EXPECT_CALL(image_fetched, Call(_)).WillOnce(SaveArg<0>(&image)); } @@ -2286,11 +2254,10 @@ TEST_F(RemoteSuggestionsProviderImplTest, ShouldClearOrphanedImagesOnRestart) { .Build()); FetchTheseSuggestions(provider.get(), /*interactive_request=*/true, Status::Success(), std::move(fetched_categories)); - ServeImageCallback cb = - base::Bind(&ServeOneByOneImage, &provider->GetImageFetcherForTesting()); - EXPECT_CALL(*image_fetcher(), StartOrQueueNetworkRequest(_, _, _, _)) - .WillOnce(WithArgs<0, 2>(Invoke(CreateFunctor(cb)))); + EXPECT_CALL(*image_fetcher(), FetchImageAndData_(_, _, _, _, _)) + .WillOnce(WithArgs<0, 2, 3>( + Invoke(CreateFunctor(base::BindRepeating(&ServeOneByOneImage))))); image_decoder()->SetDecodedImage(gfx::test::CreateImage(1, 1)); gfx::Image image = FetchImage(provider.get(), MakeArticleID(kSuggestionUrl)); diff --git a/chromium/components/ntp_snippets/remote/remote_suggestions_scheduler_impl_unittest.cc b/chromium/components/ntp_snippets/remote/remote_suggestions_scheduler_impl_unittest.cc index 0dd76c852bb..1c079450e1b 100644 --- a/chromium/components/ntp_snippets/remote/remote_suggestions_scheduler_impl_unittest.cc +++ b/chromium/components/ntp_snippets/remote/remote_suggestions_scheduler_impl_unittest.cc @@ -145,6 +145,14 @@ class MockRemoteSuggestionsProvider : public RemoteSuggestionsProvider { } MOCK_METHOD2(FetchSuggestionImage, void(const ContentSuggestion::ID&, ImageFetchedCallback*)); + + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override { + FetchSuggestionImageData(suggestion_id, &callback); + } + MOCK_METHOD2(FetchSuggestionImageData, + void(const ContentSuggestion::ID&, ImageDataFetchedCallback*)); + void GetDismissedSuggestionsForDebugging( Category category, DismissedSuggestionsCallback callback) override { diff --git a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.cc b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.cc index dbdb6013c70..82a3f34bd88 100644 --- a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.cc +++ b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.cc @@ -225,6 +225,13 @@ void ForeignSessionsSuggestionsProvider::FetchSuggestionImage( FROM_HERE, base::BindOnce(std::move(callback), gfx::Image())); } +void ForeignSessionsSuggestionsProvider::FetchSuggestionImageData( + const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::string())); +} + void ForeignSessionsSuggestionsProvider::Fetch( const Category& category, const std::set<std::string>& known_suggestion_ids, diff --git a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.h b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.h index 0399a8b11fb..0e86c2e86e7 100644 --- a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.h +++ b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.h @@ -56,6 +56,8 @@ class ForeignSessionsSuggestionsProvider : public ContentSuggestionsProvider { void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override; void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id, ImageFetchedCallback callback) override; + void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id, + ImageDataFetchedCallback callback) override; void Fetch(const Category& category, const std::set<std::string>& known_suggestion_ids, FetchDoneCallback callback) override; diff --git a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc index aad3fdb7b50..586aba8087f 100644 --- a/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc +++ b/chromium/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc @@ -50,11 +50,12 @@ const char kUrl11[] = "http://www.fake11.com/"; const char kTitle[] = "title is ignored"; SessionWindow* GetOrCreateWindow(SyncedSession* session, int window_id) { - if (session->windows.find(window_id) == session->windows.end()) { - session->windows[window_id] = std::make_unique<SyncedSessionWindow>(); + SessionID id = SessionID::FromSerializedValue(window_id); + if (session->windows.find(id) == session->windows.end()) { + session->windows[id] = std::make_unique<SyncedSessionWindow>(); } - return &session->windows[window_id]->wrapped_window; + return &session->windows[id]->wrapped_window; } void AddTabToSession(SyncedSession* session, diff --git a/chromium/components/ntp_snippets/sessions/tab_delegate_sync_adapter_unittest.cc b/chromium/components/ntp_snippets/sessions/tab_delegate_sync_adapter_unittest.cc index bcdf618f9db..cf6fe741f09 100644 --- a/chromium/components/ntp_snippets/sessions/tab_delegate_sync_adapter_unittest.cc +++ b/chromium/components/ntp_snippets/sessions/tab_delegate_sync_adapter_unittest.cc @@ -41,9 +41,7 @@ class MockOpenTabsUIDelegate : public OpenTabsUIDelegate { scoped_refptr<RefCountedMemory>*)); MOCK_METHOD1(GetAllForeignSessions, bool(std::vector<const SyncedSession*>*)); MOCK_METHOD3(GetForeignTab, - bool(const std::string&, - SessionID::id_type, - const SessionTab**)); + bool(const std::string&, SessionID, const SessionTab**)); MOCK_METHOD1(DeleteForeignSession, void(const std::string&)); MOCK_METHOD2(GetForeignSession, bool(const std::string&, std::vector<const SessionWindow*>*)); |