blob: 8ecaa37a68e69636a6b60352d56e32fd722cf70d [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/bookmarks/bookmark_suggestions_provider.h"
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/location.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/ntp_snippets/bookmarks/bookmark_last_visit_utils.h"
#include "components/ntp_snippets/category.h"
#include "components/ntp_snippets/content_suggestion.h"
#include "components/ntp_snippets/features.h"
#include "components/strings/grit/components_strings.h"
#include "components/variations/variations_associated_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
namespace ntp_snippets {
namespace {
const int kMaxBookmarks = 10;
const int kMaxBookmarkAgeInDays = 7;
const char* kMaxBookmarksParamName = "bookmarks_max_count";
const char* kMaxBookmarkAgeInDaysParamName = "bookmarks_max_age_in_days";
const char* kConsiderDesktopVisitsParamName =
"bookmarks_consider_desktop_visits";
// Any bookmark created or visited after this time will be considered recent.
// Note that bookmarks can be shown that do not meet this threshold.
base::Time GetThresholdTime() {
return base::Time::Now() -
base::TimeDelta::FromDays(variations::GetVariationParamByFeatureAsInt(
ntp_snippets::kBookmarkSuggestionsFeature,
kMaxBookmarkAgeInDaysParamName, kMaxBookmarkAgeInDays));
}
// The maximum number of suggestions ever provided.
int GetMaxCount() {
return variations::GetVariationParamByFeatureAsInt(
ntp_snippets::kBookmarkSuggestionsFeature, kMaxBookmarksParamName,
kMaxBookmarks);
}
bool AreDesktopVisitsConsidered() {
return variations::GetVariationParamByFeatureAsBool(
ntp_snippets::kBookmarkSuggestionsFeature,
kConsiderDesktopVisitsParamName, true);
}
} // namespace
BookmarkSuggestionsProvider::BookmarkSuggestionsProvider(
ContentSuggestionsProvider::Observer* observer,
bookmarks::BookmarkModel* bookmark_model)
: ContentSuggestionsProvider(observer),
category_status_(CategoryStatus::AVAILABLE_LOADING),
provided_category_(
Category::FromKnownCategory(KnownCategories::BOOKMARKS)),
bookmark_model_(bookmark_model),
fetch_requested_(false),
fetch_in_progress_(false),
end_of_list_last_visit_date_(GetThresholdTime()),
consider_bookmark_visits_from_desktop_(AreDesktopVisitsConsidered()),
weak_ptr_factory_(this) {
observer->OnCategoryStatusChanged(this, provided_category_, category_status_);
bookmark_model_->AddObserver(this);
FetchBookmarks();
}
BookmarkSuggestionsProvider::~BookmarkSuggestionsProvider() {
bookmark_model_->RemoveObserver(this);
}
////////////////////////////////////////////////////////////////////////////////
// Private methods
CategoryStatus BookmarkSuggestionsProvider::GetCategoryStatus(
Category category) {
DCHECK_EQ(category, provided_category_);
return category_status_;
}
CategoryInfo BookmarkSuggestionsProvider::GetCategoryInfo(Category category) {
return CategoryInfo(
l10n_util::GetStringUTF16(IDS_NTP_BOOKMARK_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::MINIMAL_CARD,
ContentSuggestionsAdditionalAction::VIEW_ALL,
/*show_if_empty=*/false,
l10n_util::GetStringUTF16(IDS_NTP_BOOKMARK_SUGGESTIONS_SECTION_EMPTY));
}
void BookmarkSuggestionsProvider::DismissSuggestion(
const ContentSuggestion::ID& suggestion_id) {
DCHECK(bookmark_model_->loaded());
GURL url(suggestion_id.id_within_category());
MarkBookmarksDismissed(bookmark_model_, url);
}
void BookmarkSuggestionsProvider::FetchSuggestionImage(
const ContentSuggestion::ID& suggestion_id,
ImageFetchedCallback callback) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
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,
FetchDoneCallback callback) {
LOG(DFATAL) << "BookmarkSuggestionsProvider has no |Fetch| functionality!";
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback),
Status(StatusCode::PERMANENT_ERROR,
"BookmarkSuggestionsProvider has no |Fetch| functionality!"),
std::vector<ContentSuggestion>()));
}
void BookmarkSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
// To avoid race conditions with the history-removal of the last-visited
// timestamps we also trigger a deletion here. The problem is that we need to
// update the bookmarks data here and otherwise (depending on the order in
// which the code runs) could pick up to-be-deleted data again.
if (bookmark_model_->loaded()) {
RemoveLastVisitedDatesBetween(begin, end, filter, bookmark_model_);
}
ClearDismissedSuggestionsForDebugging(provided_category_);
FetchBookmarks();
}
void BookmarkSuggestionsProvider::ClearCachedSuggestions() {
// Ignored.
}
void BookmarkSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
DismissedSuggestionsCallback callback) {
DCHECK_EQ(category, provided_category_);
std::vector<const BookmarkNode*> bookmarks =
GetDismissedBookmarksForDebugging(bookmark_model_);
std::vector<ContentSuggestion> suggestions;
for (const BookmarkNode* bookmark : bookmarks) {
ConvertBookmark(*bookmark, &suggestions);
}
std::move(callback).Run(std::move(suggestions));
}
void BookmarkSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
DCHECK_EQ(category, provided_category_);
if (!bookmark_model_->loaded()) {
return;
}
MarkAllBookmarksUndismissed(bookmark_model_);
}
void BookmarkSuggestionsProvider::BookmarkModelLoaded(
bookmarks::BookmarkModel* model,
bool ids_reassigned) {
DCHECK_EQ(bookmark_model_, model);
if (fetch_requested_) {
fetch_requested_ = false;
FetchBookmarks();
}
}
void BookmarkSuggestionsProvider::OnWillChangeBookmarkMetaInfo(
BookmarkModel* model,
const BookmarkNode* node) {
// Store the last visit date of the node that is about to change.
if (!GetLastVisitDateForNTPBookmark(*node,
consider_bookmark_visits_from_desktop_,
&node_to_change_last_visit_date_)) {
node_to_change_last_visit_date_ = base::Time::UnixEpoch();
}
}
void BookmarkSuggestionsProvider::BookmarkMetaInfoChanged(
BookmarkModel* model,
const BookmarkNode* node) {
base::Time time;
if (!GetLastVisitDateForNTPBookmark(
*node, consider_bookmark_visits_from_desktop_, &time)) {
// Error in loading the last visit date after the change. This happens when
// the bookmark just got dismissed. We must not update the suggestion in
// such a case.
return;
}
if (time == node_to_change_last_visit_date_ ||
time < end_of_list_last_visit_date_) {
// The last visit date has not changed or the change is irrelevant.
return;
}
// Otherwise, we should update the suggestions.
FetchBookmarks();
}
void BookmarkSuggestionsProvider::BookmarkNodeRemoved(
bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent,
int old_index,
const bookmarks::BookmarkNode* node,
const std::set<GURL>& no_longer_bookmarked) {
base::Time time;
if (GetLastVisitDateForNTPBookmark(
*node, consider_bookmark_visits_from_desktop_, &time) &&
time < end_of_list_last_visit_date_) {
// We know the node is too old to influence the list.
return;
}
// Some node from our list got deleted, we should update the suggestions.
FetchBookmarks();
}
void BookmarkSuggestionsProvider::BookmarkNodeAdded(
bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent,
int index) {
base::Time time;
if (!GetLastVisitDateForNTPBookmark(*parent->GetChild(index),
consider_bookmark_visits_from_desktop_,
&time) ||
time < end_of_list_last_visit_date_) {
// The new node has no last visited info or is too old to get into the list.
return;
}
// Some relevant node got created (e.g. by sync), we should update the list.
FetchBookmarks();
}
void BookmarkSuggestionsProvider::ConvertBookmark(
const BookmarkNode& bookmark,
std::vector<ContentSuggestion>* suggestions) {
base::Time publish_date;
if (!GetLastVisitDateForNTPBookmark(
bookmark, consider_bookmark_visits_from_desktop_, &publish_date)) {
return;
}
ContentSuggestion suggestion(provided_category_, bookmark.url().spec(),
bookmark.url());
suggestion.set_title(bookmark.GetTitle());
suggestion.set_snippet_text(base::string16());
suggestion.set_publish_date(publish_date);
suggestion.set_publisher_name(base::UTF8ToUTF16(bookmark.url().host()));
suggestions->emplace_back(std::move(suggestion));
}
void BookmarkSuggestionsProvider::FetchBookmarksInternal() {
DCHECK(bookmark_model_->loaded());
NotifyStatusChanged(CategoryStatus::AVAILABLE);
base::Time threshold_time = GetThresholdTime();
std::vector<const BookmarkNode*> bookmarks = GetRecentlyVisitedBookmarks(
bookmark_model_, GetMaxCount(), threshold_time,
consider_bookmark_visits_from_desktop_);
std::vector<ContentSuggestion> suggestions;
for (const BookmarkNode* bookmark : bookmarks) {
ConvertBookmark(*bookmark, &suggestions);
}
if (suggestions.empty()) {
end_of_list_last_visit_date_ = threshold_time;
} else {
end_of_list_last_visit_date_ = suggestions.back().publish_date();
}
observer()->OnNewSuggestions(this, provided_category_,
std::move(suggestions));
fetch_in_progress_ = false;
}
void BookmarkSuggestionsProvider::FetchBookmarks() {
if (!bookmark_model_->loaded()) {
fetch_requested_ = true;
return;
}
if (fetch_in_progress_) {
return;
}
// Post an async task (and block further calls before it gets executed) so
// that the bookmarks are fetched only once per a sequence of updates to the
// model. In particular, if the user has plenty of bookmarks for one given
// URL, bookmark_last_visit_updater updates each such bookmark separately.
// Using the async task here, we avoid fetching once per each such bookmark.
fetch_in_progress_ = true;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&BookmarkSuggestionsProvider::FetchBookmarksInternal,
weak_ptr_factory_.GetWeakPtr()));
}
void BookmarkSuggestionsProvider::NotifyStatusChanged(
CategoryStatus new_status) {
if (category_status_ == new_status) {
return;
}
category_status_ = new_status;
observer()->OnCategoryStatusChanged(this, provided_category_, new_status);
}
} // namespace ntp_snippets