blob: ed383e5ca4f5b48b73707814a2077ecadcd0ef8e [file] [log] [blame]
// 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/reading_list/reading_list_suggestions_provider.h"
#include <algorithm>
#include <memory>
#include <vector>
#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/ntp_snippets/category.h"
#include "components/reading_list/core/reading_list_entry.h"
#include "components/reading_list/core/reading_list_model.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
namespace ntp_snippets {
namespace {
// Max number of entries to return.
const int kMaxEntries = 3;
bool CompareEntries(const ReadingListEntry* lhs, const ReadingListEntry* rhs) {
return lhs->UpdateTime() > rhs->UpdateTime();
}
} // namespace
ReadingListSuggestionsProvider::ReadingListSuggestionsProvider(
ContentSuggestionsProvider::Observer* observer,
ReadingListModel* reading_list_model)
: ContentSuggestionsProvider(observer),
category_status_(CategoryStatus::AVAILABLE_LOADING),
provided_category_(
Category::FromKnownCategory(KnownCategories::READING_LIST)),
reading_list_model_(reading_list_model),
scoped_observer_(this) {
observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
// If the ReadingListModel is loaded, this will trigger a call to
// ReadingListModelLoaded. Keep it as last instruction.
scoped_observer_.Add(reading_list_model_);
}
ReadingListSuggestionsProvider::~ReadingListSuggestionsProvider() {}
CategoryStatus ReadingListSuggestionsProvider::GetCategoryStatus(
Category category) {
DCHECK_EQ(category, provided_category_);
return category_status_;
}
CategoryInfo ReadingListSuggestionsProvider::GetCategoryInfo(
Category category) {
DCHECK_EQ(category, provided_category_);
return CategoryInfo(l10n_util::GetStringUTF16(
IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::FULL_CARD,
ContentSuggestionsAdditionalAction::VIEW_ALL,
/*show_if_empty=*/false,
l10n_util::GetStringUTF16(
IDS_NTP_READING_LIST_SUGGESTIONS_SECTION_EMPTY));
}
void ReadingListSuggestionsProvider::DismissSuggestion(
const ContentSuggestion::ID& suggestion_id) {
if (!reading_list_model_) {
return;
}
DCHECK(reading_list_model_->loaded());
GURL url(suggestion_id.id_within_category());
SetDismissedState(url, true);
}
void ReadingListSuggestionsProvider::FetchSuggestionImage(
const ContentSuggestion::ID& suggestion_id,
ImageFetchedCallback callback) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
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,
FetchDoneCallback callback) {
LOG(DFATAL) << "ReadingListSuggestionsProvider has no |Fetch| functionality!";
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
Status(StatusCode::PERMANENT_ERROR,
"ReadingListSuggestionsProvider has no |Fetch| "
"functionality!"),
std::vector<ContentSuggestion>()));
}
void ReadingListSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
// Ignored, Reading List does not depend on history.
}
void ReadingListSuggestionsProvider::ClearCachedSuggestions() {
// Ignored.
}
void ReadingListSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
DismissedSuggestionsCallback callback) {
if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) {
std::move(callback).Run(std::vector<ContentSuggestion>());
return;
}
DCHECK(reading_list_model_->loaded());
std::vector<const ReadingListEntry*> entries;
for (const GURL& url : reading_list_model_->Keys()) {
const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url);
if (entry->ContentSuggestionsExtra()->dismissed) {
entries.emplace_back(entry);
}
}
std::sort(entries.begin(), entries.end(), CompareEntries);
std::vector<ContentSuggestion> suggestions;
for (const ReadingListEntry* entry : entries) {
suggestions.emplace_back(ConvertEntry(entry));
}
std::move(callback).Run(std::move(suggestions));
}
void ReadingListSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
for (const auto& url : reading_list_model_->Keys()) {
SetDismissedState(url, false);
}
}
void ReadingListSuggestionsProvider::ReadingListModelLoaded(
const ReadingListModel* model) {
DCHECK(model == reading_list_model_);
FetchReadingListInternal();
}
void ReadingListSuggestionsProvider::ReadingListModelBeingDeleted(
const ReadingListModel* model) {
DCHECK(model == reading_list_model_);
scoped_observer_.Remove(reading_list_model_);
reading_list_model_ = nullptr;
}
void ReadingListSuggestionsProvider::ReadingListDidApplyChanges(
ReadingListModel* model) {
DCHECK(model == reading_list_model_);
if (model->IsPerformingBatchUpdates())
return;
FetchReadingListInternal();
}
void ReadingListSuggestionsProvider::ReadingListModelCompletedBatchUpdates(
const ReadingListModel* model) {
DCHECK(model == reading_list_model_);
FetchReadingListInternal();
}
void ReadingListSuggestionsProvider::FetchReadingListInternal() {
if (!reading_list_model_ || reading_list_model_->IsPerformingBatchUpdates()) {
return;
}
DCHECK(reading_list_model_->loaded());
std::vector<const ReadingListEntry*> entries;
for (const GURL& url : reading_list_model_->Keys()) {
const ReadingListEntry* entry = reading_list_model_->GetEntryByURL(url);
if (!entry->IsRead() && !entry->ContentSuggestionsExtra()->dismissed) {
entries.emplace_back(entry);
}
}
if (entries.size() > kMaxEntries) {
// Get the |kMaxEntries| most recent entries.
std::partial_sort(entries.begin(), entries.begin() + kMaxEntries,
entries.end(), CompareEntries);
entries.resize(kMaxEntries);
} else {
std::sort(entries.begin(), entries.end(), CompareEntries);
}
std::vector<ContentSuggestion> suggestions;
for (const ReadingListEntry* entry : entries) {
suggestions.emplace_back(ConvertEntry(entry));
}
NotifyStatusChanged(CategoryStatus::AVAILABLE);
observer()->OnNewSuggestions(this, provided_category_,
std::move(suggestions));
}
ContentSuggestion ReadingListSuggestionsProvider::ConvertEntry(
const ReadingListEntry* entry) {
ContentSuggestion suggestion(provided_category_, entry->URL().spec(),
entry->URL());
if (!entry->Title().empty()) {
suggestion.set_title(base::UTF8ToUTF16(entry->Title()));
} else {
suggestion.set_title(url_formatter::FormatUrl(entry->URL()));
}
suggestion.set_publisher_name(
url_formatter::FormatUrl(entry->URL().GetOrigin()));
int64_t entry_time = entry->DistillationTime();
if (entry_time == 0) {
entry_time = entry->CreationTime();
}
suggestion.set_publish_date(
base::Time::FromDoubleT(entry_time / base::Time::kMicrosecondsPerSecond));
auto extra = std::make_unique<ReadingListSuggestionExtra>();
extra->favicon_page_url =
entry->DistilledURL().is_valid() ? entry->DistilledURL() : entry->URL();
suggestion.set_reading_list_suggestion_extra(std::move(extra));
return suggestion;
}
void ReadingListSuggestionsProvider::NotifyStatusChanged(
CategoryStatus new_status) {
if (category_status_ == new_status) {
return;
}
category_status_ = new_status;
observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
}
void ReadingListSuggestionsProvider::SetDismissedState(const GURL& url,
bool dismissed) {
reading_list::ContentSuggestionsExtra extra;
extra.dismissed = dismissed;
reading_list_model_->SetContentSuggestionsExtra(url, extra);
}
} // namespace ntp_snippets