blob: a80e91b7e32c2ebbe360f6cb412818c49538ce7e [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/offline_page_suggestions_provider.h"
#include <algorithm>
#include "base/bind.h"
#include "base/guid.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/values.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/offline_pages/client_namespace_constants.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image.h"
using offline_pages::MultipleOfflinePageItemResult;
using offline_pages::OfflinePageModel;
using offline_pages::OfflinePageItem;
namespace ntp_snippets {
namespace {
const int kMaxSuggestionsCount = 5;
struct OrderByMostRecentlyVisited {
bool operator()(const OfflinePageItem* left,
const OfflinePageItem* right) const {
return left->last_access_time > right->last_access_time;
}
};
bool IsRecentTab(const offline_pages::ClientId& client_id) {
return client_id.name_space == offline_pages::kLastNNamespace;
}
bool IsDownload(const offline_pages::ClientId& client_id) {
// TODO(pke): Use kDownloadNamespace once the OfflinePageModel uses that.
// The current logic is taken from DownloadUIAdapter::IsVisibleInUI.
return client_id.name_space == offline_pages::kAsyncNamespace &&
base::IsValidGUID(client_id.id);
}
} // namespace
OfflinePageSuggestionsProvider::OfflinePageSuggestionsProvider(
bool recent_tabs_enabled,
bool downloads_enabled,
bool download_manager_ui_enabled,
ContentSuggestionsProvider::Observer* observer,
CategoryFactory* category_factory,
OfflinePageModel* offline_page_model,
PrefService* pref_service)
: ContentSuggestionsProvider(observer, category_factory),
recent_tabs_status_(recent_tabs_enabled
? CategoryStatus::AVAILABLE_LOADING
: CategoryStatus::NOT_PROVIDED),
downloads_status_(downloads_enabled ? CategoryStatus::AVAILABLE_LOADING
: CategoryStatus::NOT_PROVIDED),
offline_page_model_(offline_page_model),
recent_tabs_category_(
category_factory->FromKnownCategory(KnownCategories::RECENT_TABS)),
downloads_category_(
category_factory->FromKnownCategory(KnownCategories::DOWNLOADS)),
pref_service_(pref_service),
download_manager_ui_enabled_(download_manager_ui_enabled) {
DCHECK(recent_tabs_enabled || downloads_enabled);
if (recent_tabs_enabled) {
observer->OnCategoryStatusChanged(this, recent_tabs_category_,
recent_tabs_status_);
}
if (downloads_enabled) {
observer->OnCategoryStatusChanged(this, downloads_category_,
downloads_status_);
}
offline_page_model_->AddObserver(this);
FetchOfflinePages();
}
OfflinePageSuggestionsProvider::~OfflinePageSuggestionsProvider() {
offline_page_model_->RemoveObserver(this);
}
// static
void OfflinePageSuggestionsProvider::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kDismissedRecentOfflineTabSuggestions);
registry->RegisterListPref(prefs::kDismissedDownloadSuggestions);
}
////////////////////////////////////////////////////////////////////////////////
// Private methods
CategoryStatus OfflinePageSuggestionsProvider::GetCategoryStatus(
Category category) {
if (category == recent_tabs_category_)
return recent_tabs_status_;
if (category == downloads_category_)
return downloads_status_;
NOTREACHED() << "Unknown category " << category.id();
return CategoryStatus::NOT_PROVIDED;
}
CategoryInfo OfflinePageSuggestionsProvider::GetCategoryInfo(
Category category) {
if (category == recent_tabs_category_) {
return CategoryInfo(l10n_util::GetStringUTF16(
IDS_NTP_RECENT_TAB_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::MINIMAL_CARD,
/* has_more_button */ false,
/* show_if_empty */ false);
}
if (category == downloads_category_) {
return CategoryInfo(
l10n_util::GetStringUTF16(IDS_NTP_DOWNLOAD_SUGGESTIONS_SECTION_HEADER),
ContentSuggestionsCardLayout::MINIMAL_CARD,
/* has_more_button */ download_manager_ui_enabled_,
/* show_if_empty */ false);
}
NOTREACHED() << "Unknown category " << category.id();
return CategoryInfo(base::string16(),
ContentSuggestionsCardLayout::MINIMAL_CARD,
/* has_more_button */ false,
/* show_if_empty */ false);
}
void OfflinePageSuggestionsProvider::DismissSuggestion(
const std::string& suggestion_id) {
Category category = GetCategoryFromUniqueID(suggestion_id);
std::string offline_page_id = GetWithinCategoryIDFromUniqueID(suggestion_id);
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs(category);
dismissed_ids.insert(offline_page_id);
StoreDismissedIDsToPrefs(category, dismissed_ids);
}
void OfflinePageSuggestionsProvider::FetchSuggestionImage(
const std::string& suggestion_id,
const ImageFetchedCallback& callback) {
// TODO(pke): Fetch proper thumbnail from OfflinePageModel once it's available
// there.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(callback, suggestion_id, gfx::Image()));
}
void OfflinePageSuggestionsProvider::ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) {
ClearDismissedSuggestionsForDebugging(recent_tabs_category_);
ClearDismissedSuggestionsForDebugging(downloads_category_);
FetchOfflinePages();
}
void OfflinePageSuggestionsProvider::ClearCachedSuggestions(Category category) {
// Ignored.
}
void OfflinePageSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category,
const DismissedSuggestionsCallback& callback) {
offline_page_model_->GetAllPages(
base::Bind(&OfflinePageSuggestionsProvider::
OnOfflinePagesLoadedForDismissedDebugging,
base::Unretained(this), category, callback));
}
void OfflinePageSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
StoreDismissedIDsToPrefs(category, std::set<std::string>());
FetchOfflinePages();
}
void OfflinePageSuggestionsProvider::OfflinePageModelLoaded(
OfflinePageModel* model) {
DCHECK_EQ(offline_page_model_, model);
}
void OfflinePageSuggestionsProvider::OfflinePageModelChanged(
OfflinePageModel* model) {
DCHECK_EQ(offline_page_model_, model);
FetchOfflinePages();
}
void OfflinePageSuggestionsProvider::OfflinePageDeleted(
int64_t offline_id,
const offline_pages::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 (recent_tabs_status_ != CategoryStatus::NOT_PROVIDED &&
IsRecentTab(client_id)) {
InvalidateSuggestion(recent_tabs_category_, offline_id);
} else if (downloads_status_ != CategoryStatus::NOT_PROVIDED &&
IsDownload(client_id)) {
InvalidateSuggestion(downloads_category_, offline_id);
}
}
void OfflinePageSuggestionsProvider::FetchOfflinePages() {
// TODO(pke): When something other than GetAllPages is used here, the
// dismissed IDs cleanup in OnOfflinePagesLoaded needs to be changed to avoid
// suggestions being undismissed accidentally.
offline_page_model_->GetAllPages(
base::Bind(&OfflinePageSuggestionsProvider::OnOfflinePagesLoaded,
base::Unretained(this)));
}
void OfflinePageSuggestionsProvider::OnOfflinePagesLoaded(
const MultipleOfflinePageItemResult& result) {
bool need_recent_tabs = recent_tabs_status_ != CategoryStatus::NOT_PROVIDED;
bool need_downloads = downloads_status_ != CategoryStatus::NOT_PROVIDED;
if (need_recent_tabs)
NotifyStatusChanged(recent_tabs_category_, CategoryStatus::AVAILABLE);
if (need_downloads)
NotifyStatusChanged(downloads_category_, CategoryStatus::AVAILABLE);
std::set<std::string> dismissed_recent_tab_ids =
ReadDismissedIDsFromPrefs(recent_tabs_category_);
std::set<std::string> dismissed_download_ids =
ReadDismissedIDsFromPrefs(downloads_category_);
std::set<std::string> cleaned_recent_tab_ids;
std::set<std::string> cleaned_download_ids;
std::vector<const OfflinePageItem*> recent_tab_items;
std::vector<const OfflinePageItem*> download_items;
for (const OfflinePageItem& item : result) {
std::string offline_page_id = base::IntToString(item.offline_id);
if (need_recent_tabs && IsRecentTab(item.client_id)) {
if (dismissed_recent_tab_ids.count(offline_page_id))
cleaned_recent_tab_ids.insert(offline_page_id);
else
recent_tab_items.push_back(&item);
}
if (need_downloads && IsDownload(item.client_id)) {
if (dismissed_download_ids.count(offline_page_id))
cleaned_download_ids.insert(offline_page_id);
else
download_items.push_back(&item);
}
}
// TODO(pke): Once we have our OfflinePageModel getter and that doesn't do it
// already, filter out duplicate URLs for recent tabs here. Duplicates for
// downloads are fine.
if (need_recent_tabs) {
observer()->OnNewSuggestions(
this, recent_tabs_category_,
GetMostRecentlyVisited(recent_tabs_category_,
std::move(recent_tab_items)));
if (cleaned_recent_tab_ids.size() != dismissed_recent_tab_ids.size())
StoreDismissedIDsToPrefs(recent_tabs_category_, cleaned_recent_tab_ids);
}
if (need_downloads) {
observer()->OnNewSuggestions(
this, downloads_category_,
GetMostRecentlyVisited(downloads_category_, std::move(download_items)));
if (cleaned_download_ids.size() != dismissed_download_ids.size())
StoreDismissedIDsToPrefs(downloads_category_, cleaned_download_ids);
}
}
void OfflinePageSuggestionsProvider::OnOfflinePagesLoadedForDismissedDebugging(
Category category,
const DismissedSuggestionsCallback& callback,
const offline_pages::MultipleOfflinePageItemResult& result) {
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs(category);
std::vector<ContentSuggestion> suggestions;
for (const OfflinePageItem& item : result) {
if (category == recent_tabs_category_ && !IsRecentTab(item.client_id))
continue;
if (category == downloads_category_ && !IsDownload(item.client_id))
continue;
if (!dismissed_ids.count(base::IntToString(item.offline_id)))
continue;
suggestions.push_back(ConvertOfflinePage(category, item));
}
callback.Run(std::move(suggestions));
}
void OfflinePageSuggestionsProvider::NotifyStatusChanged(
Category category,
CategoryStatus new_status) {
if (category == recent_tabs_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, recent_tabs_status_);
if (recent_tabs_status_ == new_status)
return;
recent_tabs_status_ = new_status;
observer()->OnCategoryStatusChanged(this, category, new_status);
} else if (category == downloads_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, downloads_status_);
if (downloads_status_ == new_status)
return;
downloads_status_ = new_status;
observer()->OnCategoryStatusChanged(this, category, new_status);
} else {
NOTREACHED() << "Unknown category " << category.id();
}
}
ContentSuggestion OfflinePageSuggestionsProvider::ConvertOfflinePage(
Category category,
const OfflinePageItem& offline_page) const {
// TODO(pke): Make sure the URL is actually opened as an offline URL.
// Currently, the browser opens the offline URL and then immediately
// redirects to the online URL if the device is online.
ContentSuggestion suggestion(
MakeUniqueID(category, base::IntToString(offline_page.offline_id)),
offline_page.GetOfflineURL());
if (offline_page.title.empty()) {
// TODO(pke): 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()));
return suggestion;
}
std::vector<ContentSuggestion>
OfflinePageSuggestionsProvider::GetMostRecentlyVisited(
Category category,
std::vector<const OfflinePageItem*> offline_page_items) const {
std::sort(offline_page_items.begin(), offline_page_items.end(),
OrderByMostRecentlyVisited());
std::vector<ContentSuggestion> suggestions;
for (const OfflinePageItem* offline_page_item : offline_page_items) {
suggestions.push_back(ConvertOfflinePage(category, *offline_page_item));
if (suggestions.size() == kMaxSuggestionsCount)
break;
}
return suggestions;
}
void OfflinePageSuggestionsProvider::InvalidateSuggestion(Category category,
int64_t offline_id) {
std::string offline_page_id = base::IntToString(offline_id);
observer()->OnSuggestionInvalidated(this, category,
MakeUniqueID(category, offline_page_id));
std::set<std::string> dismissed_ids = ReadDismissedIDsFromPrefs(category);
auto it =
std::find(dismissed_ids.begin(), dismissed_ids.end(), offline_page_id);
if (it != dismissed_ids.end()) {
dismissed_ids.erase(it);
StoreDismissedIDsToPrefs(category, dismissed_ids);
}
}
std::string OfflinePageSuggestionsProvider::GetDismissedPref(
Category category) const {
if (category == recent_tabs_category_)
return prefs::kDismissedRecentOfflineTabSuggestions;
if (category == downloads_category_)
return prefs::kDismissedDownloadSuggestions;
NOTREACHED() << "Unknown category " << category.id();
return std::string();
}
std::set<std::string> OfflinePageSuggestionsProvider::ReadDismissedIDsFromPrefs(
Category category) const {
std::set<std::string> dismissed_ids;
const base::ListValue* list =
pref_service_->GetList(GetDismissedPref(category));
for (const std::unique_ptr<base::Value>& value : *list) {
std::string dismissed_id;
bool success = value->GetAsString(&dismissed_id);
DCHECK(success) << "Failed to parse dismissed offline page ID from prefs";
dismissed_ids.insert(dismissed_id);
}
return dismissed_ids;
}
void OfflinePageSuggestionsProvider::StoreDismissedIDsToPrefs(
Category category,
const std::set<std::string>& dismissed_ids) {
base::ListValue list;
for (const std::string& dismissed_id : dismissed_ids)
list.AppendString(dismissed_id);
pref_service_->Set(GetDismissedPref(category), list);
}
} // namespace ntp_snippets