blob: f27892f36ac7236aa297cfa4efb0b9702dc8c488 [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/offline_pages/recent_tab_suggestions_provider.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/ntp_snippets/features.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/ntp_snippets/pref_util.h"
#include "components/offline_pages/core/client_policy_controller.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_page_model_query.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/variations/variations_associated_data.h"
#include "grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
using offline_pages::ClientId;
using offline_pages::OfflinePageItem;
using offline_pages::OfflinePageModelQuery;
using offline_pages::OfflinePageModelQueryBuilder;
namespace ntp_snippets {
namespace {
const int kDefaultMaxSuggestionsCount = 5;
const char* kMaxSuggestionsCountParamName = "recent_tabs_max_count";
int GetMaxSuggestionsCount() {
return variations::GetVariationParamByFeatureAsInt(
kRecentOfflineTabSuggestionsFeature, kMaxSuggestionsCountParamName,
kDefaultMaxSuggestionsCount);
}
struct OrderOfflinePagesByMostRecentlyCreatedFirst {
bool operator()(const OfflinePageItem* left,
const OfflinePageItem* right) const {
return left->creation_time > right->creation_time;
}
};
struct OrderOfflinePagesByUrlAndThenMostRecentlyCreatedFirst {
bool operator()(const OfflinePageItem* left,
const OfflinePageItem* right) const {
if (left->url != right->url) {
return left->url < right->url;
}
return left->creation_time > right->creation_time;
}
};
std::unique_ptr<OfflinePageModelQuery> BuildRecentTabsQuery(
offline_pages::OfflinePageModel* model) {
OfflinePageModelQueryBuilder builder;
builder.RequireShownAsRecentlyVisitedSite(
OfflinePageModelQuery::Requirement::INCLUDE_MATCHING);
return builder.Build(model->GetPolicyController());
}
} // namespace
RecentTabSuggestionsProvider::RecentTabSuggestionsProvider(
ContentSuggestionsProvider::Observer* observer,
CategoryFactory* category_factory,
offline_pages::OfflinePageModel* offline_page_model,
PrefService* pref_service)
: ContentSuggestionsProvider(observer, category_factory),
category_status_(CategoryStatus::AVAILABLE_LOADING),
provided_category_(
category_factory->FromKnownCategory(KnownCategories::RECENT_TABS)),
offline_page_model_(offline_page_model),
pref_service_(pref_service),
weak_ptr_factory_(this) {
observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
offline_page_model_->AddObserver(this);
FetchRecentTabs();
}
RecentTabSuggestionsProvider::~RecentTabSuggestionsProvider() {
offline_page_model_->RemoveObserver(this);
}
CategoryStatus RecentTabSuggestionsProvider::GetCategoryStatus(
Category category) {
if (category == provided_category_) {
return category_status_;
}
NOTREACHED() << "Unknown category " << category.id();
return CategoryStatus::NOT_PROVIDED;
}
CategoryInfo RecentTabSuggestionsProvider::GetCategoryInfo(Category category) {
DCHECK_EQ(provided_category_, category);
return CategoryInfo(
l10n_util::GetStringUTF16(IDS_NTP_RECENT_TAB_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::MINIMAL_CARD,
/*has_more_action=*/false,
/*has_reload_action=*/false,
/*has_view_all_action=*/false,
/*show_if_empty=*/false,
l10n_util::GetStringUTF16(IDS_NTP_SUGGESTIONS_SECTION_EMPTY));
// TODO(vitaliii): Replace IDS_NTP_SUGGESTIONS_SECTION_EMPTY with a
// category-specific string.
}
void RecentTabSuggestionsProvider::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 RecentTabSuggestionsProvider::FetchSuggestionImage(
const ContentSuggestion::ID& suggestion_id,
const ImageFetchedCallback& callback) {
// TODO(vitaliii): Fetch proper thumbnail from OfflinePageModel once it's
// available there.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(callback, gfx::Image()));
}
void RecentTabSuggestionsProvider::Fetch(
const Category& category,
const std::set<std::string>& known_suggestion_ids,
const FetchDoneCallback& callback) {
LOG(DFATAL) << "RecentTabSuggestionsProvider has no |Fetch| functionality!";
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(
callback,
Status(StatusCode::PERMANENT_ERROR,
"RecentTabSuggestionsProvider has no |Fetch| functionality!"),
base::Passed(std::vector<ContentSuggestion>())));
}
void RecentTabSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
ClearDismissedSuggestionsForDebugging(provided_category_);
FetchRecentTabs();
}
void RecentTabSuggestionsProvider::ClearCachedSuggestions(Category category) {
// Ignored.
}
void RecentTabSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
const DismissedSuggestionsCallback& callback) {
DCHECK_EQ(provided_category_, category);
// Offline pages which are not related to recent tabs are also queried here,
// so that they can be returned if they happen to be dismissed (e.g. due to a
// bug).
OfflinePageModelQueryBuilder query_builder;
offline_page_model_->GetPagesMatchingQuery(
query_builder.Build(offline_page_model_->GetPolicyController()),
base::Bind(&RecentTabSuggestionsProvider::
GetPagesMatchingQueryCallbackForGetDismissedSuggestions,
weak_ptr_factory_.GetWeakPtr(), callback));
}
void RecentTabSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
DCHECK_EQ(provided_category_, category);
StoreDismissedIDsToPrefs(std::set<std::string>());
FetchRecentTabs();
}
// static
void RecentTabSuggestionsProvider::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kDismissedRecentOfflineTabSuggestions);
}
////////////////////////////////////////////////////////////////////////////////
// Private methods
void RecentTabSuggestionsProvider::
GetPagesMatchingQueryCallbackForGetDismissedSuggestions(
const DismissedSuggestionsCallback& callback,
const std::vector<OfflinePageItem>& offline_pages) const {
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs();
std::vector<ContentSuggestion> suggestions;
for (const OfflinePageItem& item : offline_pages) {
if (!dismissed_ids.count(base::IntToString(item.offline_id))) {
continue;
}
suggestions.push_back(ConvertOfflinePage(item));
}
callback.Run(std::move(suggestions));
}
void RecentTabSuggestionsProvider::OfflinePageModelLoaded(
offline_pages::OfflinePageModel* model) {}
void RecentTabSuggestionsProvider::OfflinePageAdded(
offline_pages::OfflinePageModel* model,
const offline_pages::OfflinePageItem& added_page) {
DCHECK_EQ(offline_page_model_, model);
FetchRecentTabs();
}
void RecentTabSuggestionsProvider::
GetPagesMatchingQueryCallbackForFetchRecentTabs(
const std::vector<OfflinePageItem>& offline_pages) {
NotifyStatusChanged(CategoryStatus::AVAILABLE);
std::set<std::string> old_dismissed_ids = ReadDismissedIDsFromPrefs();
std::set<std::string> new_dismissed_ids;
std::vector<const OfflinePageItem*> recent_tab_items;
for (const OfflinePageItem& item : offline_pages) {
std::string offline_page_id = base::IntToString(item.offline_id);
if (old_dismissed_ids.count(offline_page_id)) {
new_dismissed_ids.insert(offline_page_id);
} else {
recent_tab_items.push_back(&item);
}
}
observer()->OnNewSuggestions(
this, provided_category_,
GetMostRecentlyCreatedWithoutDuplicates(std::move(recent_tab_items)));
if (new_dismissed_ids.size() != old_dismissed_ids.size()) {
StoreDismissedIDsToPrefs(new_dismissed_ids);
}
}
void RecentTabSuggestionsProvider::OfflinePageDeleted(
int64_t offline_id,
const ClientId& client_id) {
// Because we never switch to NOT_PROVIDED dynamically, there can be no open
// UI containing an invalidated suggestion unless the status is something
// other than NOT_PROVIDED, so only notify invalidation in that case.
if (category_status_ != CategoryStatus::NOT_PROVIDED) {
InvalidateSuggestion(offline_id);
}
}
void RecentTabSuggestionsProvider::FetchRecentTabs() {
offline_page_model_->GetPagesMatchingQuery(
BuildRecentTabsQuery(offline_page_model_),
base::Bind(&RecentTabSuggestionsProvider::
GetPagesMatchingQueryCallbackForFetchRecentTabs,
weak_ptr_factory_.GetWeakPtr()));
}
void RecentTabSuggestionsProvider::NotifyStatusChanged(
CategoryStatus new_status) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, category_status_);
if (category_status_ == new_status) {
return;
}
category_status_ = new_status;
observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
}
ContentSuggestion RecentTabSuggestionsProvider::ConvertOfflinePage(
const OfflinePageItem& offline_page) const {
// TODO(vitaliii): Make sure the URL is opened in the existing tab.
ContentSuggestion suggestion(provided_category_,
base::IntToString(offline_page.offline_id),
offline_page.url);
if (offline_page.title.empty()) {
// TODO(vitaliii): Remove this fallback once the OfflinePageModel provides
// titles for all (relevant) OfflinePageItems.
suggestion.set_title(base::UTF8ToUTF16(offline_page.url.spec()));
} else {
suggestion.set_title(offline_page.title);
}
suggestion.set_publish_date(offline_page.creation_time);
suggestion.set_publisher_name(base::UTF8ToUTF16(offline_page.url.host()));
auto extra = base::MakeUnique<RecentTabSuggestionExtra>();
extra->tab_id = offline_page.client_id.id;
extra->offline_page_id = offline_page.offline_id;
suggestion.set_recent_tab_suggestion_extra(std::move(extra));
return suggestion;
}
std::vector<ContentSuggestion>
RecentTabSuggestionsProvider::GetMostRecentlyCreatedWithoutDuplicates(
std::vector<const OfflinePageItem*> offline_page_items) const {
// |std::unique| only removes duplicates that immediately follow each other.
// Thus, first, we have to sort by URL and creation time and only then remove
// duplicates and sort the remaining items by creation time.
std::sort(offline_page_items.begin(), offline_page_items.end(),
OrderOfflinePagesByUrlAndThenMostRecentlyCreatedFirst());
std::vector<const OfflinePageItem*>::iterator new_end = std::unique(
offline_page_items.begin(), offline_page_items.end(),
[](const OfflinePageItem* left, const OfflinePageItem* right) {
return left->url == right->url;
});
offline_page_items.erase(new_end, offline_page_items.end());
std::sort(offline_page_items.begin(), offline_page_items.end(),
OrderOfflinePagesByMostRecentlyCreatedFirst());
std::vector<ContentSuggestion> suggestions;
for (const OfflinePageItem* offline_page_item : offline_page_items) {
suggestions.push_back(ConvertOfflinePage(*offline_page_item));
if (static_cast<int>(suggestions.size()) == GetMaxSuggestionsCount()) {
break;
}
}
return suggestions;
}
void RecentTabSuggestionsProvider::InvalidateSuggestion(int64_t offline_id) {
std::string offline_page_id = base::IntToString(offline_id);
observer()->OnSuggestionInvalidated(
this, ContentSuggestion::ID(provided_category_, offline_page_id));
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs();
auto it = dismissed_ids.find(offline_page_id);
if (it != dismissed_ids.end()) {
dismissed_ids.erase(it);
StoreDismissedIDsToPrefs(dismissed_ids);
}
}
std::set<std::string> RecentTabSuggestionsProvider::ReadDismissedIDsFromPrefs()
const {
return prefs::ReadDismissedIDsFromPrefs(
*pref_service_, prefs::kDismissedRecentOfflineTabSuggestions);
}
void RecentTabSuggestionsProvider::StoreDismissedIDsToPrefs(
const std::set<std::string>& dismissed_ids) {
prefs::StoreDismissedIDsToPrefs(pref_service_,
prefs::kDismissedRecentOfflineTabSuggestions,
dismissed_ids);
}
} // namespace ntp_snippets