blob: 0c153735bee2a199a1d5cf08bd9c0b2de1880202 [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;
}
};
} // 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),
dismissed_recent_tab_ids_(ReadDismissedIDsFromPrefs(
prefs::kDismissedRecentOfflineTabSuggestions)),
dismissed_download_ids_(
ReadDismissedIDsFromPrefs(prefs::kDismissedDownloadSuggestions)),
download_manager_ui_enabled_(download_manager_ui_enabled) {
DCHECK(recent_tabs_enabled || downloads_enabled);
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
std::vector<Category> OfflinePageSuggestionsProvider::GetProvidedCategories() {
std::vector<Category> provided_categories;
if (recent_tabs_status_ != CategoryStatus::NOT_PROVIDED)
provided_categories.push_back(recent_tabs_category_);
if (downloads_status_ != CategoryStatus::NOT_PROVIDED)
provided_categories.push_back(downloads_category_);
return provided_categories;
}
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);
}
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_);
}
NOTREACHED() << "Unknown category " << category.id();
return CategoryInfo(base::string16(),
ContentSuggestionsCardLayout::MINIMAL_CARD,
/* has_more_button */ false);
}
void OfflinePageSuggestionsProvider::DismissSuggestion(
const std::string& suggestion_id) {
Category category = GetCategoryFromUniqueID(suggestion_id);
std::string offline_page_id = GetWithinCategoryIDFromUniqueID(suggestion_id);
if (category == recent_tabs_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, recent_tabs_status_);
dismissed_recent_tab_ids_.insert(offline_page_id);
StoreDismissedIDsToPrefs(prefs::kDismissedRecentOfflineTabSuggestions,
dismissed_recent_tab_ids_);
} else if (category == downloads_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, downloads_status_);
dismissed_download_ids_.insert(offline_page_id);
StoreDismissedIDsToPrefs(prefs::kDismissedDownloadSuggestions,
dismissed_download_ids_);
} else {
NOTREACHED() << "Unknown category " << category.id();
}
}
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::ClearCachedSuggestionsForDebugging(
Category category) {
// Ignored.
}
std::vector<ContentSuggestion>
OfflinePageSuggestionsProvider::GetDismissedSuggestionsForDebugging(
Category category) {
// TODO(pke): Make GetDismissedSuggestionsForDebugging asynchronous so this
// can return proper values.
std::vector<ContentSuggestion> suggestions;
const std::set<std::string>* dismissed_ids = nullptr;
if (category == recent_tabs_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, recent_tabs_status_);
dismissed_ids = &dismissed_recent_tab_ids_;
} else if (category == downloads_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, downloads_status_);
dismissed_ids = &dismissed_download_ids_;
} else {
NOTREACHED() << "Unknown category " << category.id();
return suggestions;
}
for (const std::string& dismissed_id : *dismissed_ids) {
ContentSuggestion suggestion(
MakeUniqueID(category, dismissed_id),
GURL("http://dismissed-offline-page-" + dismissed_id));
suggestion.set_title(base::UTF8ToUTF16("Title not available"));
suggestions.push_back(std::move(suggestion));
}
return suggestions;
}
void OfflinePageSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
Category category) {
if (category == recent_tabs_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, recent_tabs_status_);
dismissed_recent_tab_ids_.clear();
StoreDismissedIDsToPrefs(prefs::kDismissedRecentOfflineTabSuggestions,
dismissed_recent_tab_ids_);
} else if (category == downloads_category_) {
DCHECK_NE(CategoryStatus::NOT_PROVIDED, downloads_status_);
dismissed_download_ids_.clear();
StoreDismissedIDsToPrefs(prefs::kDismissedDownloadSuggestions,
dismissed_download_ids_);
} else {
NOTREACHED() << "Unknown category " << category.id();
}
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) {
// TODO(pke): Implement, suggestion has to be removed from UI immediately.
}
void OfflinePageSuggestionsProvider::FetchOfflinePages() {
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::vector<const OfflinePageItem*> recent_tab_items;
std::vector<const OfflinePageItem*> download_items;
for (const OfflinePageItem& item : result) {
if (need_recent_tabs &&
item.client_id.name_space == offline_pages::kLastNNamespace &&
!dismissed_recent_tab_ids_.count(base::IntToString(item.offline_id))) {
recent_tab_items.push_back(&item);
}
// TODO(pke): Use kDownloadNamespace once the OfflinePageModel uses that.
// The current logic is taken from DownloadUIAdapter::IsVisibleInUI.
if (need_downloads &&
item.client_id.name_space == offline_pages::kAsyncNamespace &&
base::IsValidGUID(item.client_id.id) &&
!dismissed_download_ids_.count(base::IntToString(item.offline_id))) {
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 (need_downloads) {
observer()->OnNewSuggestions(
this, downloads_category_,
GetMostRecentlyVisited(downloads_category_, std::move(download_items)));
}
}
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;
}
std::set<std::string> OfflinePageSuggestionsProvider::ReadDismissedIDsFromPrefs(
const std::string& pref_name) const {
std::set<std::string> dismissed_ids;
const base::ListValue* list = pref_service_->GetList(pref_name);
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(
const std::string& pref_name,
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(pref_name, list);
}
} // namespace ntp_snippets