blob: b8382c0caa002e07aad20dee6439435cafb3e5eb [file] [log] [blame]
// 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/physical_web_pages/physical_web_page_suggestions_provider.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/grit/components_scaled_resources.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/ntp_snippets/pref_util.h"
#include "components/physical_web/data_source/physical_web_data_source.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "url/gurl.h"
namespace ntp_snippets {
namespace {
const size_t kMaxSuggestionsCount = 10;
std::string GetPageId(const physical_web::Metadata& page_metadata) {
return page_metadata.resolved_url.spec();
}
bool CompareByDistance(const physical_web::Metadata& left,
const physical_web::Metadata& right) {
// When there is no estimate, the value is <= 0, so we implicitly treat it as
// infinity.
bool is_left_estimated = left.distance_estimate > 0;
bool is_right_estimated = right.distance_estimate > 0;
if (is_left_estimated != is_right_estimated)
return is_left_estimated;
return left.distance_estimate < right.distance_estimate;
}
void FilterOutByGroupId(physical_web::MetadataList& page_metadata_list) {
// |std::unique| only removes duplicates that immediately follow each other.
// Thus, first, we have to sort by group_id and distance and only then remove
// duplicates.
std::sort(page_metadata_list.begin(), page_metadata_list.end(),
[](const physical_web::Metadata& left,
const physical_web::Metadata& right) {
if (left.group_id != right.group_id) {
return left.group_id < right.group_id;
}
// We want closest pages first, so in case of same group_id we
// sort by distance.
return CompareByDistance(left, right);
});
// Each empty group_id must be treated as unique, so we do not apply
// std::unique to them at all.
auto nonempty_group_id_begin =
std::find_if(page_metadata_list.begin(), page_metadata_list.end(),
[](const physical_web::Metadata& page) {
return !page.group_id.empty();
});
auto new_end = std::unique(nonempty_group_id_begin, page_metadata_list.end(),
[](const physical_web::Metadata& left,
const physical_web::Metadata& right) {
return left.group_id == right.group_id;
});
page_metadata_list.erase(new_end, page_metadata_list.end());
}
} // namespace
PhysicalWebPageSuggestionsProvider::PhysicalWebPageSuggestionsProvider(
ContentSuggestionsProvider::Observer* observer,
physical_web::PhysicalWebDataSource* physical_web_data_source,
PrefService* pref_service)
: ContentSuggestionsProvider(observer),
category_status_(CategoryStatus::AVAILABLE),
provided_category_(
Category::FromKnownCategory(KnownCategories::PHYSICAL_WEB_PAGES)),
physical_web_data_source_(physical_web_data_source),
pref_service_(pref_service) {
observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
physical_web_data_source_->RegisterListener(
this, physical_web::BACKGROUND_INTERMITTENT);
// TODO(vitaliii): Rewrite initial fetch once crbug.com/667754 is resolved.
FetchPhysicalWebPages();
}
PhysicalWebPageSuggestionsProvider::~PhysicalWebPageSuggestionsProvider() {
physical_web_data_source_->UnregisterListener(this);
}
CategoryStatus PhysicalWebPageSuggestionsProvider::GetCategoryStatus(
Category category) {
return category_status_;
}
CategoryInfo PhysicalWebPageSuggestionsProvider::GetCategoryInfo(
Category category) {
return CategoryInfo(l10n_util::GetStringUTF16(
IDS_NTP_PHYSICAL_WEB_PAGE_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::FULL_CARD,
ContentSuggestionsAdditionalAction::NONE,
/*show_if_empty=*/false,
l10n_util::GetStringUTF16(
IDS_NTP_PHYSICAL_WEB_PAGE_SUGGESTIONS_SECTION_EMPTY));
}
void PhysicalWebPageSuggestionsProvider::DismissSuggestion(
const ContentSuggestion::ID& suggestion_id) {
DCHECK_EQ(provided_category_, suggestion_id.category());
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs();
dismissed_ids.insert(suggestion_id.id_within_category());
StoreDismissedIDsToPrefs(dismissed_ids);
}
void PhysicalWebPageSuggestionsProvider::FetchSuggestionImage(
const ContentSuggestion::ID& suggestion_id,
const ImageFetchedCallback& callback) {
ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
base::StringPiece raw_data = resource_bundle.GetRawDataResourceForScale(
IDR_PHYSICAL_WEB_LOGO_WITH_PADDING, resource_bundle.GetMaxScaleFactor());
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(callback,
gfx::Image::CreateFrom1xPNGBytes(
reinterpret_cast<const unsigned char*>(raw_data.data()),
raw_data.size())));
}
void PhysicalWebPageSuggestionsProvider::Fetch(
const Category& category,
const std::set<std::string>& known_suggestion_ids,
const FetchDoneCallback& callback) {
DCHECK_EQ(category, provided_category_);
std::vector<ContentSuggestion> suggestions =
GetMostRecentPhysicalWebPagesWithFilter(kMaxSuggestionsCount,
known_suggestion_ids);
AppendToShownScannedUrls(suggestions);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(callback, Status::Success(),
base::Passed(std::move(suggestions))));
}
void PhysicalWebPageSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
ClearDismissedSuggestionsForDebugging(provided_category_);
}
void PhysicalWebPageSuggestionsProvider::ClearCachedSuggestions(
Category category) {
// Ignored
}
void PhysicalWebPageSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
const DismissedSuggestionsCallback& callback) {
DCHECK_EQ(provided_category_, category);
std::unique_ptr<physical_web::MetadataList> page_metadata_list =
physical_web_data_source_->GetMetadataList();
const std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs();
std::vector<ContentSuggestion> suggestions;
for (const auto& page_metadata : *page_metadata_list) {
if (dismissed_ids.count(GetPageId(page_metadata))) {
suggestions.push_back(ConvertPhysicalWebPage(page_metadata));
}
}
callback.Run(std::move(suggestions));
}
void PhysicalWebPageSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
DCHECK_EQ(provided_category_, category);
StoreDismissedIDsToPrefs(std::set<std::string>());
FetchPhysicalWebPages();
}
// static
void PhysicalWebPageSuggestionsProvider::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kDismissedPhysicalWebPageSuggestions);
}
////////////////////////////////////////////////////////////////////////////////
// Private methods
void PhysicalWebPageSuggestionsProvider::NotifyStatusChanged(
CategoryStatus new_status) {
if (category_status_ == new_status) {
return;
}
category_status_ = new_status;
observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
}
void PhysicalWebPageSuggestionsProvider::FetchPhysicalWebPages() {
DCHECK_EQ(CategoryStatus::AVAILABLE, category_status_);
std::vector<ContentSuggestion> suggestions =
GetMostRecentPhysicalWebPagesWithFilter(
kMaxSuggestionsCount,
/*excluded_ids=*/std::set<std::string>());
shown_resolved_urls_by_scanned_url_.clear();
AppendToShownScannedUrls(suggestions);
observer()->OnNewSuggestions(this, provided_category_,
std::move(suggestions));
}
std::vector<ContentSuggestion>
PhysicalWebPageSuggestionsProvider::GetMostRecentPhysicalWebPagesWithFilter(
int max_count,
const std::set<std::string>& excluded_ids) {
std::unique_ptr<physical_web::MetadataList> page_metadata_list =
physical_web_data_source_->GetMetadataList();
// These is to filter out dismissed suggestions and at the same time prune the
// dismissed IDs list removing nonavailable pages (this is needed since some
// OnLost() calls may have been missed).
const std::set<std::string> old_dismissed_ids = ReadDismissedIDsFromPrefs();
std::set<std::string> new_dismissed_ids;
physical_web::MetadataList filtered_metadata_list;
for (const auto& page_metadata : *page_metadata_list) {
const std::string page_id = GetPageId(page_metadata);
if (!excluded_ids.count(page_id) && !old_dismissed_ids.count(page_id)) {
filtered_metadata_list.push_back(page_metadata);
}
if (old_dismissed_ids.count(page_id)) {
new_dismissed_ids.insert(page_id);
}
}
if (old_dismissed_ids.size() != new_dismissed_ids.size()) {
StoreDismissedIDsToPrefs(new_dismissed_ids);
}
FilterOutByGroupId(filtered_metadata_list);
std::sort(filtered_metadata_list.begin(), filtered_metadata_list.end(),
CompareByDistance);
std::vector<ContentSuggestion> suggestions;
for (const auto& page_metadata : filtered_metadata_list) {
if (static_cast<int>(suggestions.size()) == max_count) {
break;
}
suggestions.push_back(ConvertPhysicalWebPage(page_metadata));
}
return suggestions;
}
ContentSuggestion PhysicalWebPageSuggestionsProvider::ConvertPhysicalWebPage(
const physical_web::Metadata& page) const {
ContentSuggestion suggestion(provided_category_, GetPageId(page),
page.resolved_url);
DCHECK(base::IsStringUTF8(page.title));
suggestion.set_title(base::UTF8ToUTF16(page.title));
suggestion.set_publisher_name(base::UTF8ToUTF16(page.resolved_url.host()));
DCHECK(base::IsStringUTF8(page.description));
suggestion.set_snippet_text(base::UTF8ToUTF16(page.description));
return suggestion;
}
// PhysicalWebListener implementation.
void PhysicalWebPageSuggestionsProvider::OnFound(const GURL& url) {
FetchPhysicalWebPages();
}
void PhysicalWebPageSuggestionsProvider::OnLost(const GURL& url) {
auto it = shown_resolved_urls_by_scanned_url_.find(url);
if (it == shown_resolved_urls_by_scanned_url_.end()) {
// The notification is propagated further in case the suggestion is shown on
// old NTPs (created before last |shown_resolved_urls_by_scanned_url_|
// update).
// TODO(vitaliii): Use |resolved_url| here when it is available. Currently
// there is no way to find out |resolved_url|, which corresponds to this
// |scanned_url| (the metadata has been already removed from the Physical
// Web list). We use |scanned_url| (it may be the same as |resolved_url|,
// otherwise nothing happens), however, we should use the latter once it is
// provided (e.g. as an argument).
InvalidateSuggestion(url.spec());
return;
}
// This is not a reference, because the multimap pair will be removed below.
const GURL lost_resolved_url = it->second;
shown_resolved_urls_by_scanned_url_.erase(it);
if (std::find_if(shown_resolved_urls_by_scanned_url_.begin(),
shown_resolved_urls_by_scanned_url_.end(),
[lost_resolved_url](const std::pair<GURL, GURL>& pair) {
return lost_resolved_url == pair.second;
}) == shown_resolved_urls_by_scanned_url_.end()) {
// There are no more beacons for this URL.
InvalidateSuggestion(lost_resolved_url.spec());
}
}
void PhysicalWebPageSuggestionsProvider::OnDistanceChanged(
const GURL& url,
double distance_estimate) {
FetchPhysicalWebPages();
}
void PhysicalWebPageSuggestionsProvider::InvalidateSuggestion(
const std::string& page_id) {
observer()->OnSuggestionInvalidated(
this, ContentSuggestion::ID(provided_category_, page_id));
// Remove |page_id| from dismissed suggestions, if present.
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs();
auto it = dismissed_ids.find(page_id);
if (it != dismissed_ids.end()) {
dismissed_ids.erase(it);
StoreDismissedIDsToPrefs(dismissed_ids);
}
}
void PhysicalWebPageSuggestionsProvider::AppendToShownScannedUrls(
const std::vector<ContentSuggestion>& suggestions) {
std::unique_ptr<physical_web::MetadataList> page_metadata_list =
physical_web_data_source_->GetMetadataList();
for (const auto& page_metadata : *page_metadata_list) {
if (std::find_if(suggestions.begin(), suggestions.end(),
[page_metadata](const ContentSuggestion& suggestion) {
return suggestion.url() == page_metadata.resolved_url;
}) != suggestions.end()) {
shown_resolved_urls_by_scanned_url_.insert(std::make_pair(
page_metadata.scanned_url, page_metadata.resolved_url));
}
}
}
std::set<std::string>
PhysicalWebPageSuggestionsProvider::ReadDismissedIDsFromPrefs() const {
return prefs::ReadDismissedIDsFromPrefs(
*pref_service_, prefs::kDismissedPhysicalWebPageSuggestions);
}
void PhysicalWebPageSuggestionsProvider::StoreDismissedIDsToPrefs(
const std::set<std::string>& dismissed_ids) {
prefs::StoreDismissedIDsToPrefs(pref_service_,
prefs::kDismissedPhysicalWebPageSuggestions,
dismissed_ids);
}
} // namespace ntp_snippets