blob: 39eedefefd6aae31b593fadbbe916fee898ddc0d [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/ntp_tiles/most_visited_sites.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/user_metrics.h"
#include "base/notimplemented.h"
#include "base/observer_list.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/ntp_tiles/constants.h"
#include "components/ntp_tiles/features.h"
#include "components/ntp_tiles/icon_cacher.h"
#include "components/ntp_tiles/metrics.h"
#include "components/ntp_tiles/pref_names.h"
#include "components/ntp_tiles/switches.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/search/ntp_features.h"
#include "components/supervised_user/core/common/buildflags.h"
#include "components/webapps/common/constants.h"
#include "extensions/buildflags/buildflags.h"
#include "third_party/re2/src/re2/re2.h"
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
#include "components/supervised_user/core/browser/family_link_user_capabilities.h"
#include "components/supervised_user/core/browser/supervised_user_service.h"
#endif
#if BUILDFLAG(ENABLE_EXTENSIONS)
// GN doesn't understand conditional includes, so we need nogncheck here.
#include "extensions/common/constants.h" // nogncheck
#endif
using history::TopSites;
namespace ntp_tiles {
namespace {
// URL host prefixes. Hosts with these prefixes often redirect to each other, or
// have the same content.
// Popular sites are excluded if the user has visited a page whose host only
// differs by one of these prefixes. Even if the URL does not point to the exact
// same page, the user will have a personalized suggestion that is more likely
// to be of use for them.
// A cleaner way could be checking the history for redirects but this requires
// the page to be visited on the device.
const char* kKnownGenericPagePrefixes[] = {
"m.", "mobile.", // Common prefixes among popular sites.
"edition.", // Used among news papers (CNN, Independent, ...)
"www.", // Usually no-www domains redirect to www or vice-versa.
// The following entry MUST REMAIN LAST as it is prefix of every string!
""}; // The no-www domain matches domains on same level .
// Determine whether we need any tiles from PopularSites to fill up a grid of
// |num_tiles| tiles.
bool NeedPopularSites(const PrefService* prefs, int num_tiles) {
return prefs->GetInteger(prefs::kNumPersonalTiles) < num_tiles;
}
bool HasHomeTile(const NTPTilesVector& tiles) {
for (const auto& tile : tiles) {
if (tile.source == TileSource::HOMEPAGE) {
return true;
}
}
return false;
}
std::string StripFirstGenericPrefix(const std::string& host) {
for (const char* prefix : kKnownGenericPagePrefixes) {
if (base::StartsWith(host, prefix, base::CompareCase::INSENSITIVE_ASCII)) {
return std::string(
base::TrimString(host, prefix, base::TrimPositions::TRIM_LEADING));
}
}
return host;
}
// Generate a short title for Most Visited items before they're converted to
// custom links.
std::u16string GenerateShortTitle(const std::u16string& title) {
// Empty title only happened in the unittests.
if (title.empty()) {
return std::u16string();
}
// Match "anything- anything" where "-" is one of the delimiters shown in the
// following examples of intended matches: "Front - Back", "Front | Back",
// "Front: Back", "Front; Back"
const std::string regex = "(.*?)[-|:;]+\\s(.*)";
std::string utf8_short_title_front;
std::string utf8_short_title_back;
std::string utf8_title = base::UTF16ToUTF8(title);
std::u16string short_title_front;
std::u16string short_title_back;
std::u16string short_title;
if (!re2::RE2::FullMatch(utf8_title, regex, &utf8_short_title_front,
&utf8_short_title_back)) {
// If FullMatch() returns false, we don't have a split title, so return full
// title. Tests expect trimmed title.
return std::u16string(
base::TrimWhitespace(title, base::TrimPositions::TRIM_ALL));
}
if (!utf8_short_title_front.empty()) {
short_title_front = base::UTF8ToUTF16(utf8_short_title_front);
short_title = short_title_front;
}
if (!utf8_short_title_back.empty()) {
short_title_back = base::UTF8ToUTF16(utf8_short_title_back);
}
if (short_title_front != short_title_back) {
int words_in_front =
SplitString(short_title_front, base::kWhitespaceASCIIAs16,
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)
.size();
int words_in_back =
SplitString(short_title_back, base::kWhitespaceASCIIAs16,
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)
.size();
if (words_in_front >= 3 && words_in_back >= 1 && words_in_back <= 3) {
short_title = short_title_back;
}
}
base::TrimWhitespace(short_title, base::TrimPositions::TRIM_ALL,
&short_title);
return short_title;
}
} // namespace
/******** CustomLinksCache ********/
CustomLinksCache::CustomLinksCache() = default;
CustomLinksCache::~CustomLinksCache() = default;
void CustomLinksCache::PushBack(const NTPTile& tile) {
list_.push_back(tile);
url_set_.insert(tile.url);
}
void CustomLinksCache::Clear() {
list_.clear();
url_set_.clear();
}
bool CustomLinksCache::HasUrl(const GURL& url) const {
return url_set_.count(url) != 0;
}
const NTPTilesVector& CustomLinksCache::GetList() const {
return list_;
}
/******** MostVisitedSites ********/
MostVisitedSites::MostVisitedSites(
PrefService* prefs,
signin::IdentityManager* identity_manager,
supervised_user::SupervisedUserService* supervised_user_service,
scoped_refptr<history::TopSites> top_sites,
std::unique_ptr<PopularSites> popular_sites,
std::unique_ptr<CustomLinksManager> custom_links_manager,
std::unique_ptr<EnterpriseShortcutsManager> enterprise_shortcuts_manager,
std::unique_ptr<IconCacher> icon_cacher,
bool is_default_chrome_app_migrated)
: prefs_(prefs),
identity_manager_(identity_manager),
supervised_user_service_(supervised_user_service),
top_sites_(top_sites),
popular_sites_(std::move(popular_sites)),
custom_links_manager_(std::move(custom_links_manager)),
enterprise_shortcuts_manager_(std::move(enterprise_shortcuts_manager)),
icon_cacher_(std::move(icon_cacher)),
is_default_chrome_app_migrated_(is_default_chrome_app_migrated),
max_num_sites_(0u),
is_observing_(false) {
DCHECK(prefs_);
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
if (supervised_user_service_) {
supervised_user_service_observation_.Observe(supervised_user_service_);
}
#endif
}
MostVisitedSites::~MostVisitedSites() {
observers_.Clear();
}
// static
bool MostVisitedSites::IsHostOrMobilePageKnown(
const std::set<std::string>& hosts_to_skip,
const std::string& host) {
std::string no_prefix_host = StripFirstGenericPrefix(host);
for (const char* prefix : kKnownGenericPagePrefixes) {
if (hosts_to_skip.count(prefix + no_prefix_host) ||
hosts_to_skip.count(prefix + host)) {
return true;
}
}
return false;
}
bool MostVisitedSites::DoesSourceExist(TileSource source) const {
switch (source) {
case TileSource::TOP_SITES:
return top_sites_ != nullptr;
case TileSource::POPULAR_BAKED_IN:
case TileSource::POPULAR:
return popular_sites_ != nullptr;
case TileSource::HOMEPAGE:
return homepage_client_ != nullptr;
case TileSource::ALLOWLIST:
return supervised_user_service_ != nullptr;
case TileSource::CUSTOM_LINKS:
return custom_links_manager_ != nullptr;
case TileSource::ENTERPRISE_SHORTCUTS:
return enterprise_shortcuts_manager_ != nullptr;
}
NOTREACHED();
}
void MostVisitedSites::SetHomepageClient(
std::unique_ptr<HomepageClient> client) {
DCHECK(client);
homepage_client_ = std::move(client);
}
void MostVisitedSites::AddMostVisitedURLsObserver(Observer* observer,
size_t max_num_sites) {
observers_.AddObserver(observer);
// All observer must provide the same |max_num_sites| value.
DCHECK(max_num_sites_ == 0u || max_num_sites_ == max_num_sites);
max_num_sites_ = max_num_sites;
// Starts observing the following sources when the first observer is added.
if (!is_observing_) {
is_observing_ = true;
if (popular_sites_ && NeedPopularSites(prefs_, GetMaxNumSites())) {
popular_sites_->MaybeStartFetch(
false, base::BindOnce(&MostVisitedSites::OnPopularSitesDownloaded,
base::Unretained(this)));
}
if (top_sites_) {
// Register as TopSitesObserver so that we can update ourselves when the
// TopSites changes.
top_sites_observation_.Observe(top_sites_.get());
}
if (custom_links_manager_) {
custom_links_subscription_ =
custom_links_manager_->RegisterCallbackForOnChanged(
base::BindRepeating(&MostVisitedSites::OnCustomLinksChanged,
base::Unretained(this)));
}
if (enterprise_shortcuts_manager_) {
enterprise_shortcuts_subscription_ =
enterprise_shortcuts_manager_->RegisterCallbackForOnChanged(
base::BindRepeating(
&MostVisitedSites::OnEnterpriseShortcutsChanged,
base::Unretained(this)));
}
}
// Immediately build the current set of tiles, getting suggestions from
// TopSites.
BuildCurrentTiles(/* is_user_triggered= */ false);
// Also start a request for fresh suggestions.
Refresh();
}
void MostVisitedSites::RemoveMostVisitedURLsObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void MostVisitedSites::Refresh() {
if (top_sites_) {
// TopSites updates itself after a delay. To ensure up-to-date results,
// force an update now.
top_sites_->SyncWithHistory();
}
}
void MostVisitedSites::RefreshTiles() {
BuildCurrentTiles(/* is_user_triggered= */ false);
}
void MostVisitedSites::InitializeCustomLinks() {
if (!custom_links_manager_ || !IsCustomLinksEnabled()) {
return;
}
if (IsTopSitesEnabled()) {
// Custom Tiles can mix with other tiles: Initialize as empty list.
if (custom_links_manager_->Initialize(NTPTilesVector())) {
custom_links_action_count_ = 0;
}
} else {
// Custom Tiles are stand-alone: Require other tiles to exist, and
// if so, convert them to Custom Tiles. Do not include enterprise shortcuts
// since Custom Tiles should only store personal shortcuts.
if (current_tiles_.has_value()) {
NTPTilesVector personal_tiles;
for (const auto& tile : current_tiles_.value()) {
if (tile.source != TileSource::ENTERPRISE_SHORTCUTS) {
personal_tiles.push_back(tile);
}
}
if (custom_links_manager_->Initialize(personal_tiles)) {
custom_links_action_count_ = 0;
}
}
}
}
void MostVisitedSites::UninitializeCustomLinks() {
if (!custom_links_manager_ || !IsCustomLinksEnabled()) {
return;
}
custom_links_action_count_ = -1;
custom_links_manager_->Uninitialize();
BuildCurrentTiles(/* is_user_triggered= */ true);
}
bool MostVisitedSites::IsCustomLinksInitialized() const {
return custom_links_manager_ && IsCustomLinksEnabled() &&
custom_links_manager_->IsInitialized();
}
void MostVisitedSites::EnableTileTypes(
const MostVisitedSites::EnableTileTypesOptions& options) {
// Mixing of personal types is only supported on Android.
#if !BUILDFLAG(IS_ANDROID)
if (options.enable_top_sites && options.enable_custom_links) {
NOTIMPLEMENTED();
}
#endif
if (enabled_tile_types_ != options) {
enabled_tile_types_ = options;
BuildCurrentTiles(/* is_user_triggered= */ true);
}
}
bool MostVisitedSites::IsTopSitesEnabled() const {
return enabled_tile_types_.enable_top_sites;
}
bool MostVisitedSites::IsCustomLinksEnabled() const {
return enabled_tile_types_.enable_custom_links;
}
bool MostVisitedSites::IsEnterpriseShortcutsEnabled() const {
return enabled_tile_types_.enable_enterprise_shortcuts;
}
void MostVisitedSites::SetShortcutsVisible(bool visible) {
if (is_shortcuts_visible_ != visible) {
is_shortcuts_visible_ = visible;
BuildCurrentTiles(/* is_user_triggered= */ true);
}
}
bool MostVisitedSites::IsShortcutsVisible() const {
return is_shortcuts_visible_;
}
bool MostVisitedSites::AddCustomLinkTo(const GURL& url,
const std::u16string& title,
size_t pos) {
return ApplyCustomLinksAction(base::BindOnce(
&CustomLinksManager::AddLinkTo,
base::Unretained(custom_links_manager_.get()), url, title, pos));
}
bool MostVisitedSites::AddCustomLink(const GURL& url,
const std::u16string& title) {
return ApplyCustomLinksAction(base::BindOnce(
&CustomLinksManager::AddLink,
base::Unretained(custom_links_manager_.get()), url, title));
}
bool MostVisitedSites::UpdateCustomLink(const GURL& url,
const GURL& new_url,
const std::u16string& new_title) {
return ApplyCustomLinksAction(base::BindOnce(
&CustomLinksManager::UpdateLink,
base::Unretained(custom_links_manager_.get()), url, new_url, new_title));
}
bool MostVisitedSites::ReorderCustomLink(const GURL& url, size_t new_pos) {
return ApplyCustomLinksAction(base::BindOnce(
&CustomLinksManager::ReorderLink,
base::Unretained(custom_links_manager_.get()), url, new_pos));
}
bool MostVisitedSites::DeleteCustomLink(const GURL& url) {
return ApplyCustomLinksAction(
base::BindOnce(&CustomLinksManager::DeleteLink,
base::Unretained(custom_links_manager_.get()), url));
}
bool MostVisitedSites::HasCustomLink(const GURL& url) {
if (!custom_links_manager_ || !IsCustomLinksEnabled()) {
return false;
}
return custom_links_cache_.HasUrl(url);
}
void MostVisitedSites::UndoCustomLinkAction() {
if (!custom_links_manager_ || !IsCustomLinksEnabled()) {
return;
}
// If this is undoing the first action after initialization, uninitialize
// custom links.
if (custom_links_action_count_-- == 1) {
UninitializeCustomLinks();
} else if (custom_links_manager_->UndoAction()) {
BuildCurrentTiles(/* is_user_triggered= */ true);
}
}
size_t MostVisitedSites::GetCustomLinkNum() {
return custom_links_manager_->GetLinks().size();
}
void MostVisitedSites::RestoreEnterpriseShortcutsDefaults() {
if (!enterprise_shortcuts_manager_ || !IsEnterpriseShortcutsEnabled()) {
return;
}
enterprise_shortcuts_manager_->RestorePolicyLinks();
BuildCurrentTiles(/* is_user_triggered= */ true);
}
bool MostVisitedSites::UpdateEnterpriseShortcut(const GURL& url,
const std::u16string& title) {
return ApplyEnterpriseShortcutsAction(base::BindOnce(
&EnterpriseShortcutsManager::UpdateLink,
base::Unretained(enterprise_shortcuts_manager_.get()), url, title));
}
bool MostVisitedSites::ReorderEnterpriseShortcut(const GURL& url,
size_t new_pos) {
return ApplyEnterpriseShortcutsAction(base::BindOnce(
&EnterpriseShortcutsManager::ReorderLink,
base::Unretained(enterprise_shortcuts_manager_.get()), url, new_pos));
}
bool MostVisitedSites::DeleteEnterpriseShortcut(const GURL& url) {
return ApplyEnterpriseShortcutsAction(base::BindOnce(
&EnterpriseShortcutsManager::DeleteLink,
base::Unretained(enterprise_shortcuts_manager_.get()), url));
}
bool MostVisitedSites::UndoEnterpriseShortcutAction() {
return ApplyEnterpriseShortcutsAction(
base::BindOnce(&EnterpriseShortcutsManager::UndoAction,
base::Unretained(enterprise_shortcuts_manager_.get())));
}
void MostVisitedSites::AddOrRemoveBlockedUrl(const GURL& url, bool add_url) {
if (add_url) {
base::RecordAction(base::UserMetricsAction("Suggestions.Site.Removed"));
} else {
base::RecordAction(
base::UserMetricsAction("Suggestions.Site.RemovalUndone"));
}
if (top_sites_) {
if (add_url) {
top_sites_->AddBlockedUrl(url);
} else {
top_sites_->RemoveBlockedUrl(url);
}
}
}
void MostVisitedSites::ClearBlockedUrls() {
if (top_sites_) {
top_sites_->ClearBlockedUrls();
}
}
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
void MostVisitedSites::OnURLFilterChanged() {
BuildCurrentTiles(/* is_user_triggered= */ false);
}
#endif
double MostVisitedSites::GetSuggestionScore(const GURL& url) const {
if (current_tiles_.has_value()) {
for (const auto& tile : *current_tiles_) {
if (tile.url == url) {
return tile.score;
}
}
}
return kInvalidSuggestionScore;
}
// static
void MostVisitedSites::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterIntegerPref(prefs::kNumPersonalTiles, 0);
registry->RegisterBooleanPref(prefs::kMostVisitedHomeModuleEnabled, true);
}
// static
void MostVisitedSites::ResetProfilePrefs(PrefService* prefs) {
prefs->SetInteger(prefs::kNumPersonalTiles, 0);
}
size_t MostVisitedSites::GetMaxNumSites() const {
#if BUILDFLAG(IS_ANDROID)
// The "Add new" button (for custom tiles) is not a Tile; don't include.
return max_num_sites_;
#else
return max_num_sites_ +
((custom_links_manager_ && IsCustomLinksEnabled()) ? 1 : 0);
#endif
}
void MostVisitedSites::InitiateTopSitesQuery(bool is_user_triggered) {
if (!top_sites_) {
return;
}
if (top_sites_weak_ptr_factory_.HasWeakPtrs()) {
return; // Ongoing query.
}
top_sites_->GetMostVisitedURLs(base::BindOnce(
&MostVisitedSites::OnMostVisitedURLsAvailable,
top_sites_weak_ptr_factory_.GetWeakPtr(), is_user_triggered));
}
void MostVisitedSites::OnMostVisitedURLsAvailable(
bool is_user_triggered,
const history::MostVisitedURLList& visited_list) {
// Ignore the event if top sites should not be queried.
if (!ShouldQueryTopSites()) {
return;
}
NTPTilesVector tiles;
size_t num_tiles = std::min(visited_list.size(), GetMaxNumSites());
for (size_t i = 0; i < num_tiles; ++i) {
const history::MostVisitedURL& visited = visited_list[i];
if (visited.url.is_empty()) {
break; // This is the signal that there are no more real visited sites.
}
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
if (supervised_user_service_ &&
supervised_user_service_->IsBlockedURL(visited.url)) {
continue;
}
#endif
NTPTile tile;
tile.title = custom_links_manager_ ? GenerateShortTitle(visited.title)
: visited.title;
tile.url = visited.url;
tile.source = TileSource::TOP_SITES;
// MostVisitedURL.title is either the title or the URL which is treated
// exactly as the title. Differentiating here is not worth the overhead.
tile.title_source = TileTitleSource::TITLE_TAG;
tile.visit_count = visited.visit_count;
tile.last_visit_time = visited.last_visit_time;
tile.score = visited.score;
// TODO(crbug.com/41349031): Populate |data_generation_time| here in order
// to log UMA metrics of age.
tiles.push_back(std::move(tile));
}
InitiateNotificationForNewTiles(is_user_triggered, std::move(tiles));
}
void MostVisitedSites::BuildCurrentTiles(bool is_user_triggered) {
ReloadCustomLinksCache();
if (ShouldQueryTopSites()) {
InitiateTopSitesQuery(is_user_triggered);
} else {
SaveTilesAndNotify(is_user_triggered, NTPTilesVector(),
std::map<SectionType, NTPTilesVector>());
}
}
std::map<SectionType, NTPTilesVector>
MostVisitedSites::CreatePopularSitesSections(
const std::set<std::string>& used_hosts,
size_t num_actual_tiles) {
std::map<SectionType, NTPTilesVector> sections = {
std::make_pair(SectionType::PERSONALIZED, NTPTilesVector())};
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
// For child accounts popular sites tiles will not be added.
if (identity_manager_ &&
supervised_user::IsPrimaryAccountSubjectToParentalControls(
identity_manager_) == signin::Tribool::kTrue) {
return sections;
}
#endif
if (!popular_sites_) {
return sections;
}
const std::set<std::string> no_hosts;
for (const auto& section_type_and_sites : popular_sites()->sections()) {
SectionType type = section_type_and_sites.first;
const PopularSites::SitesVector& sites = section_type_and_sites.second;
if (type == SectionType::PERSONALIZED) {
size_t num_required_tiles = GetMaxNumSites() - num_actual_tiles;
sections[type] =
CreatePopularSitesTiles(/*popular_sites=*/sites,
/*hosts_to_skip=*/used_hosts,
/*num_max_tiles=*/num_required_tiles);
} else {
sections[type] =
CreatePopularSitesTiles(/*popular_sites=*/sites,
/*hosts_to_skip=*/no_hosts,
/*num_max_tiles=*/GetMaxNumSites());
}
}
return sections;
}
NTPTilesVector MostVisitedSites::CreatePopularSitesTiles(
const PopularSites::SitesVector& sites_vector,
const std::set<std::string>& hosts_to_skip,
size_t num_max_tiles) {
// Collect non-blocked popular suggestions, skipping those already present
// in the personal suggestions.
NTPTilesVector popular_sites_tiles;
for (const PopularSites::Site& popular_site : sites_vector) {
if (popular_sites_tiles.size() >= num_max_tiles) {
break;
}
// Skip blocked sites.
if (top_sites_ && top_sites_->IsBlocked(popular_site.url)) {
continue;
}
const std::string& host = popular_site.url.GetHost();
if (IsHostOrMobilePageKnown(hosts_to_skip, host)) {
continue;
}
NTPTile tile;
tile.title = popular_site.title;
tile.url = GURL(popular_site.url);
tile.title_source = popular_site.title_source;
tile.source = popular_site.baked_in ? TileSource::POPULAR_BAKED_IN
: TileSource::POPULAR;
popular_sites_tiles.push_back(std::move(tile));
icon_cacher_->StartFetchPopularSites(
popular_site,
base::BindOnce(&MostVisitedSites::OnIconMadeAvailable,
base::Unretained(this), popular_site.url),
base::BindOnce(&MostVisitedSites::OnIconMadeAvailable,
base::Unretained(this), popular_site.url));
}
return popular_sites_tiles;
}
NTPTilesVector MostVisitedSites::GetEnterpriseShortcutTiles() {
CHECK(enterprise_shortcuts_manager_);
const std::vector<EnterpriseShortcut>& shortcuts =
enterprise_shortcuts_manager_->GetLinks();
NTPTilesVector new_tiles;
for (EnterpriseShortcut shortcut : shortcuts) {
// Skip shortcut if it was hidden by the user.
if (shortcut.is_hidden_by_user) {
continue;
}
NTPTile tile;
tile.title = shortcut.title;
tile.url = shortcut.url;
tile.source = TileSource::ENTERPRISE_SHORTCUTS;
#if !BUILDFLAG(IS_ANDROID)
tile.allow_user_edit = shortcut.allow_user_edit;
tile.allow_user_delete = shortcut.allow_user_delete;
#endif // !BUILDFLAG(IS_ANDROID)
new_tiles.push_back(std::move(tile));
}
return new_tiles;
}
void MostVisitedSites::OnHomepageTitleDetermined(
bool is_user_triggered,
NTPTilesVector tiles,
const std::optional<std::u16string>& title) {
if (!title.has_value()) {
return; // If there is no title, the most recent tile was already sent out.
}
MergeMostVisitedTiles(is_user_triggered,
InsertHomeTile(std::move(tiles), title.value()));
}
NTPTilesVector MostVisitedSites::InsertHomeTile(
NTPTilesVector tiles,
const std::u16string& title) const {
DCHECK(homepage_client_);
DCHECK_GT(GetMaxNumSites(), 0u);
const GURL& homepage_url = homepage_client_->GetHomepageUrl();
NTPTilesVector new_tiles;
bool homepage_tile_added = false;
for (auto& tile : tiles) {
if (new_tiles.size() >= GetMaxNumSites()) {
break;
}
// If there's a tile has the same host name with homepage, insert the tile
// to the first position of the list. This is also a deduplication.
if (tile.url.GetHost() == homepage_url.GetHost() && !homepage_tile_added) {
tile.source = TileSource::HOMEPAGE;
homepage_tile_added = true;
new_tiles.insert(new_tiles.begin(), std::move(tile));
continue;
}
new_tiles.push_back(std::move(tile));
}
if (!homepage_tile_added) {
// Make room for the homepage tile.
if (new_tiles.size() >= GetMaxNumSites()) {
new_tiles.pop_back();
}
NTPTile homepage_tile;
homepage_tile.url = homepage_url;
homepage_tile.title = title;
homepage_tile.source = TileSource::HOMEPAGE;
homepage_tile.title_source = TileTitleSource::TITLE_TAG;
// Always insert |homepage_tile| to the front of |new_tiles| to ensure it's
// the first tile.
new_tiles.insert(new_tiles.begin(), std::move(homepage_tile));
}
return new_tiles;
}
// Ensures |custom_links_manager_| is initialized (exits on failure), then runs
// |custom_links_action|. On success, builds the tileset. On failure, if this is
// the first custom link action then uninitialize custom links.
bool MostVisitedSites::ApplyCustomLinksAction(
base::OnceCallback<bool()> custom_links_action) {
if (!custom_links_manager_ || !IsCustomLinksEnabled()) {
return false;
}
bool is_first_action = !custom_links_manager_->IsInitialized();
// Initialize custom links if they have not been initialized yet.
InitializeCustomLinks();
bool success = std::move(custom_links_action).Run();
if (success) {
if (custom_links_action_count_ != -1) {
custom_links_action_count_++;
}
BuildCurrentTiles(/* is_user_triggered= */ true);
} else if (is_first_action) {
// We don't want to keep custom links initialized if the first action after
// initialization failed.
UninitializeCustomLinks();
}
return success;
}
bool MostVisitedSites::ApplyEnterpriseShortcutsAction(
base::OnceCallback<bool()> enterprise_shortcuts_action) {
if (!enterprise_shortcuts_manager_ || !IsEnterpriseShortcutsEnabled()) {
return false;
}
bool success = std::move(enterprise_shortcuts_action).Run();
if (success) {
BuildCurrentTiles(/* is_user_triggered= */ true);
}
return success;
}
void MostVisitedSites::OnCustomLinksChanged() {
DCHECK(custom_links_manager_);
BuildCurrentTiles(/* is_user_triggered= */ true);
}
void MostVisitedSites::OnEnterpriseShortcutsChanged() {
DCHECK(enterprise_shortcuts_manager_);
BuildCurrentTiles(/* is_user_triggered= */ true);
}
void MostVisitedSites::ReloadCustomLinksCache() {
custom_links_cache_.Clear();
if (!IsCustomLinksInitialized() || !IsCustomLinksEnabled()) {
return;
}
DCHECK(custom_links_manager_);
const std::vector<CustomLinksManager::Link>& links =
custom_links_manager_->GetLinks();
// The maximum number of custom links that can be shown is independent of the
// maximum number of Most Visited sites that can be shown.
size_t num_tiles = std::min(links.size(), kMaxNumCustomLinks);
for (size_t i = 0; i < num_tiles; ++i) {
const CustomLinksManager::Link& link = links.at(i);
#if BUILDFLAG(ENABLE_SUPERVISED_USERS)
if (supervised_user_service_ &&
supervised_user_service_->IsBlockedURL(link.url)) {
continue;
}
#endif
NTPTile tile;
tile.title = link.title;
tile.url = link.url;
tile.source = TileSource::CUSTOM_LINKS;
tile.from_most_visited = link.is_most_visited;
custom_links_cache_.PushBack(tile);
}
}
void MostVisitedSites::InitiateNotificationForNewTiles(
bool is_user_triggered,
NTPTilesVector new_tiles) {
if (ShouldAddHomeTile() && !HasHomeTile(new_tiles)) {
homepage_client_->QueryHomepageTitle(
base::BindOnce(&MostVisitedSites::OnHomepageTitleDetermined,
base::Unretained(this), is_user_triggered, new_tiles));
GURL homepage_url = homepage_client_->GetHomepageUrl();
icon_cacher_->StartFetchMostLikely(
homepage_url,
base::BindRepeating(&MostVisitedSites::OnIconMadeAvailable,
base::Unretained(this), homepage_url));
// Don't wait for the homepage title from history but immediately serve a
// copy of new tiles.
new_tiles = InsertHomeTile(std::move(new_tiles), std::u16string());
}
MergeMostVisitedTiles(is_user_triggered, std::move(new_tiles));
}
void MostVisitedSites::MergeMostVisitedTiles(bool is_user_triggered,
NTPTilesVector personal_tiles) {
std::set<std::string> used_hosts;
size_t num_actual_tiles = 0;
AddToHostsAndTotalCount(personal_tiles, &used_hosts, &num_actual_tiles);
std::map<SectionType, NTPTilesVector> sections =
CreatePopularSitesSections(used_hosts, num_actual_tiles);
AddToHostsAndTotalCount(sections[SectionType::PERSONALIZED], &used_hosts,
&num_actual_tiles);
NTPTilesVector new_tiles =
MergeTiles(std::move(personal_tiles),
std::move(sections[SectionType::PERSONALIZED]));
SaveTilesAndNotify(is_user_triggered, std::move(new_tiles),
std::move(sections));
}
NTPTilesVector MostVisitedSites::ImposeCustomLinks(NTPTilesVector tiles) {
NTPTilesVector out_tiles(custom_links_cache_.GetList());
// Insert |tiles| if there are aren't enough non-custom links.
size_t max_num_tiles_without_custom = GetMaxNumSites();
if (out_tiles.size() < max_num_tiles_without_custom) {
// Exclude |tiles| elements with |url| found in |custom_links_cache_|.
std::copy_if(tiles.begin(), tiles.end(), std::back_inserter(out_tiles),
[&](const NTPTile& tile) -> bool {
return !custom_links_cache_.HasUrl(tile.url);
});
// Note that |out_tiles| truncation only happens under the "if" clause.
// So if |out_tiles| started with more than GetMaxNumSites() custom links,
// then no truncation takes place.
out_tiles.resize(std::min(out_tiles.size(), max_num_tiles_without_custom));
}
return out_tiles;
}
NTPTilesVector MostVisitedSites::ImposeEnterpriseShortcuts(
NTPTilesVector tiles) {
if (!IsEnterpriseShortcutsEnabled()) {
return tiles;
}
NTPTilesVector out_tiles = GetEnterpriseShortcutTiles();
// Insert |tiles| after enterprise shortcuts.
out_tiles.insert(out_tiles.end(), std::make_move_iterator(tiles.begin()),
std::make_move_iterator(tiles.end()));
return out_tiles;
}
void MostVisitedSites::SaveTilesAndNotify(
bool is_user_triggered,
NTPTilesVector new_tiles,
std::map<SectionType, NTPTilesVector> sections) {
// TODO(crbug.com/40802205):
// Remove this after preinstalled apps are migrated.
NTPTilesVector fixed_tiles = is_default_chrome_app_migrated_
? RemoveInvalidPreinstallApps(new_tiles)
: new_tiles;
if (fixed_tiles.size() != new_tiles.size()) {
metrics::RecordsMigratedDefaultAppDeleted(TileType::kTopSites);
}
fixed_tiles = ImposeCustomLinks(std::move(fixed_tiles));
fixed_tiles = ImposeEnterpriseShortcuts(std::move(fixed_tiles));
if (!current_tiles_.has_value() || (*current_tiles_ != fixed_tiles)) {
current_tiles_.emplace(std::move(fixed_tiles));
int num_personal_tiles = 0;
for (const auto& tile : *current_tiles_) {
if (tile.source != TileSource::POPULAR &&
tile.source != TileSource::POPULAR_BAKED_IN &&
tile.source != TileSource::ENTERPRISE_SHORTCUTS) {
num_personal_tiles++;
}
}
prefs_->SetInteger(prefs::kNumPersonalTiles, num_personal_tiles);
}
if (observers_.empty()) {
return;
}
sections[SectionType::PERSONALIZED] = *current_tiles_;
for (auto& observer : observers_) {
observer.OnURLsAvailable(is_user_triggered, sections);
}
}
// static
bool MostVisitedSites::IsNtpTileFromPreinstalledApp(GURL url) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
return url.is_valid() && url.SchemeIs(extensions::kExtensionScheme) &&
extension_misc::IsPreinstalledAppId(url.GetHost());
#else
return false;
#endif
}
// static
bool MostVisitedSites::WasNtpAppMigratedToWebApp(PrefService* prefs, GURL url) {
const base::Value::List& migrated_apps =
prefs->GetList(webapps::kWebAppsMigratedPreinstalledApps);
for (const auto& val : migrated_apps) {
if (val.is_string() && val.GetString() == url.GetHost()) {
return true;
}
}
return false;
}
NTPTilesVector MostVisitedSites::RemoveInvalidPreinstallApps(
NTPTilesVector new_tiles) {
std::erase_if(new_tiles, [this](const NTPTile& ntp_tile) {
return MostVisitedSites::IsNtpTileFromPreinstalledApp(ntp_tile.url) &&
MostVisitedSites::WasNtpAppMigratedToWebApp(prefs_, ntp_tile.url);
});
return new_tiles;
}
NTPTilesVector MostVisitedSites::MergeTiles(NTPTilesVector personal_tiles,
NTPTilesVector popular_tiles) {
NTPTilesVector merged_tiles;
std::move(personal_tiles.begin(), personal_tiles.end(),
std::back_inserter(merged_tiles));
std::move(popular_tiles.begin(), popular_tiles.end(),
std::back_inserter(merged_tiles));
return merged_tiles;
}
void MostVisitedSites::OnPopularSitesDownloaded(bool success) {
if (!success) {
LOG(WARNING) << "Download of popular sites failed";
return;
}
for (const auto& section : popular_sites_->sections()) {
for (const PopularSites::Site& site : section.second) {
// Ignore callback; these icons will be seen on the *next* NTP.
icon_cacher_->StartFetchPopularSites(site, base::NullCallback(),
base::NullCallback());
}
}
}
void MostVisitedSites::OnIconMadeAvailable(const GURL& site_url) {
for (auto& observer : observers_) {
observer.OnIconMadeAvailable(site_url);
}
}
void MostVisitedSites::TopSitesLoaded(TopSites* top_sites) {}
void MostVisitedSites::TopSitesChanged(TopSites* top_sites,
ChangeReason change_reason) {
if (ShouldQueryTopSites()) {
// Call InitiateTopSitesQuery() instead of BuildCurrentTiles() to skip
// unneeded |custom_links_cache_| update.
bool is_user_triggered =
(change_reason == TopSitesObserver::ChangeReason::BLOCKED_URLS);
InitiateTopSitesQuery(is_user_triggered);
}
}
bool MostVisitedSites::ShouldAddHomeTile() const {
return GetMaxNumSites() > 0u &&
homepage_client_ && // No platform-specific implementation - no tile.
homepage_client_->IsHomepageTileEnabled() &&
!homepage_client_->GetHomepageUrl().is_empty() &&
!(top_sites_ &&
top_sites_->IsBlocked(homepage_client_->GetHomepageUrl()));
}
bool MostVisitedSites::ShouldQueryTopSites() const {
return IsTopSitesEnabled() ||
(IsCustomLinksEnabled() && !IsCustomLinksInitialized());
}
void MostVisitedSites::AddToHostsAndTotalCount(const NTPTilesVector& new_tiles,
std::set<std::string>* hosts,
size_t* total_tile_count) const {
for (const auto& tile : new_tiles) {
hosts->insert(tile.url.GetHost());
}
*total_tile_count += new_tiles.size();
DCHECK_LE(*total_tile_count, GetMaxNumSites());
}
} // namespace ntp_tiles