blob: f1b7c4df06bd0357ba92adcd12e7aa8b8e6c5328 [file] [log] [blame]
// Copyright 2021 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 "chrome/browser/ui/webui/memories/memories_handler.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/history_clusters/memories_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/history_clusters/core/memories_features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "url/gurl.h"
#if !defined(CHROME_BRANDED)
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/time_formatting.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/tabs/tab_group.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/url_and_title.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/search_engines/template_url_service.h"
#include "ui/base/l10n/time_format.h"
namespace {
GURL GetRandomlySizedThumbnailUrl() {
const std::vector<int> dimensions = {150, 160, 170, 180, 190, 200};
auto random_dimension = [&dimensions]() {
return dimensions[rand() % dimensions.size()];
};
return GURL(base::StringPrintf("https://via.placeholder.com/%dX%d",
random_dimension(), random_dimension()));
}
} // namespace
#endif
MemoriesHandler::MemoriesHandler(
mojo::PendingReceiver<history_clusters::mojom::PageHandler>
pending_page_handler,
Profile* profile,
content::WebContents* web_contents)
: profile_(profile),
web_contents_(web_contents),
page_handler_(this, std::move(pending_page_handler)) {
DCHECK(profile_);
DCHECK(web_contents_);
auto* memory_service = MemoriesServiceFactory::GetForBrowserContext(profile_);
DCHECK(memory_service);
service_observation_.Observe(memory_service);
}
MemoriesHandler::~MemoriesHandler() = default;
void MemoriesHandler::SetPage(
mojo::PendingRemote<history_clusters::mojom::Page> pending_page) {
page_.Bind(std::move(pending_page));
}
void MemoriesHandler::QueryMemories(
history_clusters::mojom::QueryParamsPtr query_params) {
auto result_mojom = history_clusters::mojom::MemoriesResult::New();
result_mojom->title = query_params->query;
result_mojom->thumbnail_url = GetRandomlySizedThumbnailUrl();
if (!query_params->recency_threshold.has_value()) {
// The default value for the recency threshold should be the present time.
query_params->recency_threshold = base::Time::Now();
} else {
// Continuation queries have a value for the recency threshold. Mark the
// result as such.
result_mojom->is_continuation = true;
}
auto result_callback =
base::BindOnce(&MemoriesHandler::OnMemoriesQueryResult,
weak_ptr_factory_.GetWeakPtr(), std::move(result_mojom));
if (history_clusters::RemoteModelEndpointForDebugging().is_valid()) {
auto* memory_service =
MemoriesServiceFactory::GetForBrowserContext(profile_);
memory_service->QueryMemories(std::move(query_params),
std::move(result_callback));
} else {
#if defined(CHROME_BRANDED)
page_->OnMemoriesQueryResult(
history_clusters::mojom::MemoriesResult::New());
#else
// Cancel pending queries, if any.
history_task_tracker_.TryCancelAll();
QueryHistoryService(std::move(query_params), {},
std::move(result_callback));
#endif
}
}
void MemoriesHandler::OnMemoriesDebugMessage(const std::string& message) {
// Ignore messages if all the debug flags are off.
if (!base::FeatureList::IsEnabled(history_clusters::kDebug) &&
!history_clusters::RemoteModelEndpointForDebugging().is_valid()) {
return;
}
if (content::RenderFrameHost* rfh = web_contents_->GetMainFrame()) {
rfh->AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kInfo, message);
}
}
void MemoriesHandler::OnMemoriesQueryResult(
history_clusters::mojom::MemoriesResultPtr result_mojom,
history_clusters::mojom::QueryParamsPtr continuation_query_params,
std::vector<history_clusters::mojom::MemoryPtr> memory_mojoms) {
result_mojom->continuation_query_params =
std::move(continuation_query_params);
result_mojom->memories = std::move(memory_mojoms);
page_->OnMemoriesQueryResult(std::move(result_mojom));
}
#if !defined(CHROME_BRANDED)
void MemoriesHandler::QueryHistoryService(
history_clusters::mojom::QueryParamsPtr query_params,
std::vector<history_clusters::mojom::MemoryPtr> memory_mojoms,
MemoriesQueryResultsCallback callback) {
const size_t max_count =
query_params->max_count ? query_params->max_count : -1;
if (memory_mojoms.size() == max_count) {
// Enough Memories have been created. Run the callback with those Memories
// along with the continuation query params.
std::move(callback).Run(std::move(query_params), std::move(memory_mojoms));
return;
}
history::HistoryService* history_service =
HistoryServiceFactory::GetForProfile(profile_,
ServiceAccessType::EXPLICIT_ACCESS);
history::QueryOptions query_options;
query_options.duplicate_policy = history::QueryOptions::KEEP_ALL_DUPLICATES;
query_options.end_time =
query_params->recency_threshold.value_or(base::Time::Now());
// Make sure to look back far enough to find some visits.
query_options.begin_time =
query_options.end_time.LocalMidnight() - base::TimeDelta::FromDays(14);
std::u16string query = base::UTF8ToUTF16(query_params->query);
history_service->QueryHistory(
query, query_options,
base::BindOnce(&MemoriesHandler::OnHistoryQueryResults,
weak_ptr_factory_.GetWeakPtr(), std::move(query_params),
std::move(memory_mojoms), std::move(callback)),
&history_task_tracker_);
}
void MemoriesHandler::OnHistoryQueryResults(
history_clusters::mojom::QueryParamsPtr query_params,
std::vector<history_clusters::mojom::MemoryPtr> memory_mojoms,
MemoriesQueryResultsCallback callback,
history::QueryResults results) {
if (results.empty()) {
// No more results to create Memories from. Run the callback with the
// Memories created so far along with the continuation query params.
std::move(callback).Run(std::move(query_params), std::move(memory_mojoms));
return;
}
auto memory_mojom = history_clusters::mojom::Memory::New();
memory_mojom->id = base::UnguessableToken::Create();
const TemplateURLService* template_url_service =
TemplateURLServiceFactory::GetForProfile(profile_);
const TemplateURL* default_search_provider =
template_url_service->GetDefaultSearchProvider();
const SearchTermsData& search_terms_data =
template_url_service->search_terms_data();
// Keep track of the visited URLs and their titles in this memory.
std::set<std::pair<GURL, std::u16string>> visited_urls;
for (const auto& result : results) {
// Last visit time of the Memory is the visit time of most recently visited
// URL in the Memory. Collect all the visits in that day into the Memory.
if (memory_mojom->last_visit_time.is_null()) {
memory_mojom->last_visit_time = result.visit_time();
} else if (memory_mojom->last_visit_time.LocalMidnight() >
result.visit_time()) {
break;
}
visited_urls.insert(std::make_pair(result.url(), result.title()));
// Check if the URL is a valid search URL.
std::u16string search_terms;
bool is_valid_search_url =
default_search_provider &&
default_search_provider->ExtractSearchTermsFromURL(
result.url(), search_terms_data, &search_terms) &&
!search_terms.empty();
if (is_valid_search_url) {
// If the URL is a valid search URL, try to create a related search query.
const std::u16string& search_query =
base::i18n::ToLower(base::CollapseWhitespace(search_terms, false));
const std::string search_query_utf8 = base::UTF16ToUTF8(search_query);
// Skip duplicate search queries.
if (base::Contains(memory_mojom->related_searches, search_query_utf8,
[](const auto& search_query_ptr) {
return search_query_ptr->query;
})) {
continue;
}
TemplateURLRef::SearchTermsArgs search_terms_args(search_query);
const TemplateURLRef& search_url_ref = default_search_provider->url_ref();
const std::string& search_url = search_url_ref.ReplaceSearchTerms(
search_terms_args, search_terms_data);
auto search_query_mojom = history_clusters::mojom::SearchQuery::New();
search_query_mojom->query = search_query_utf8;
search_query_mojom->url = GURL(search_url);
memory_mojom->related_searches.push_back(std::move(search_query_mojom));
} else { // !is_valid_search_url
// If the URL is not a search URL, try to add the visit to the top visits.
auto visit = history_clusters::mojom::Visit::New();
// TOOD(mahmadi): URLResult does not contain visit_id.
visit->url = result.url();
visit->page_title = base::UTF16ToUTF8(result.title());
visit->thumbnail_url = GetRandomlySizedThumbnailUrl();
visit->time = result.visit_time();
visit->relative_date = base::UTF16ToUTF8(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT,
base::Time::Now() - visit->time));
visit->time_of_day =
base::UTF16ToUTF8(base::TimeFormatTimeOfDay(visit->time));
std::function<void(std::vector<history_clusters::mojom::VisitPtr>&, bool)>
add_visit;
add_visit = [&visit, &add_visit](auto& visits, bool are_top_visits) {
// Count |visit| toward duplicate visits if the same URL is seen before.
auto duplicate_visit_it = std::find_if(
visits.begin(), visits.end(), [&visit](const auto& visit_ptr) {
return visit_ptr->url == visit->url;
});
if (duplicate_visit_it != visits.end()) {
(*duplicate_visit_it)->num_duplicate_visits++;
return;
}
// For the top visits, if the domain name is seen before, add |visit| to
// the related visits of the respective top visit recursively.
if (are_top_visits) {
auto related_visit_it = std::find_if(
visits.begin(), visits.end(), [&visit](const auto& visit_ptr) {
return visit_ptr->url.host() == visit->url.host();
});
if (related_visit_it != visits.end()) {
add_visit((*related_visit_it)->related_visits, false);
return;
}
}
// Otherwise, simply add |visit| to the list of visits.
visits.push_back(std::move(visit));
};
add_visit(memory_mojom->top_visits, true);
}
}
// Add related tab groups (tab groups containing any of the visited URLs).
Browser* browser = chrome::FindBrowserWithWebContents(web_contents_);
if (browser) {
const TabStripModel* tab_strip_model = browser->tab_strip_model();
const TabGroupModel* group_model = tab_strip_model->group_model();
for (const auto& group_id : group_model->ListTabGroups()) {
const TabGroup* tab_group = group_model->GetTabGroup(group_id);
bool tab_group_has_url_in_memory = false;
gfx::Range tabs = tab_group->ListTabs();
for (uint32_t index = tabs.start(); index < tabs.end(); ++index) {
content::WebContents* web_contents =
tab_strip_model->GetWebContentsAt(index);
const GURL& url = web_contents->GetLastCommittedURL();
if (base::Contains(visited_urls, url,
[](const auto& pair) { return pair.first; })) {
tab_group_has_url_in_memory = true;
break;
}
}
if (tab_group_has_url_in_memory) {
auto tab_group_mojom = history_clusters::mojom::TabGroup::New();
tab_group_mojom->id = tab_group->id().token();
tab_group_mojom->title =
base::UTF16ToUTF8(tab_group->visual_data()->title());
for (uint32_t index = tabs.start(); index < tabs.end(); ++index) {
content::WebContents* web_contents =
tab_strip_model->GetWebContentsAt(index);
auto webpage = history_clusters::mojom::WebPage::New();
webpage->url = web_contents->GetLastCommittedURL();
webpage->title = base::UTF16ToUTF8(web_contents->GetTitle());
webpage->thumbnail_url = GetRandomlySizedThumbnailUrl();
tab_group_mojom->pages.push_back(std::move(webpage));
}
memory_mojom->related_tab_groups.push_back(std::move(tab_group_mojom));
}
}
}
// Add related bookmarks (bookmarked URLs among the visited URLs).
bookmarks::BookmarkModel* model =
BookmarkModelFactory::GetForBrowserContext(profile_);
if (model && model->loaded()) {
std::vector<bookmarks::UrlAndTitle> bookmarks;
model->GetBookmarks(&bookmarks);
for (const auto& bookmark : bookmarks) {
auto matching_visited_url_it = std::find_if(
visited_urls.begin(), visited_urls.end(),
[&bookmark](const auto& pair) { return pair.first == bookmark.url; });
if (matching_visited_url_it != visited_urls.end()) {
auto webpage = history_clusters::mojom::WebPage::New();
webpage->url = matching_visited_url_it->first;
webpage->title = base::UTF16ToUTF8(matching_visited_url_it->second);
webpage->thumbnail_url = GetRandomlySizedThumbnailUrl();
memory_mojom->bookmarks.push_back(std::move(webpage));
}
}
}
// Continue to extract Memories. Set the recency threshold to 11:59:59pm of
// the day before the Memory's |last_visit_time|.
query_params->recency_threshold =
memory_mojom->last_visit_time.LocalMidnight() -
base::TimeDelta::FromSeconds(1);
memory_mojoms.push_back(std::move(memory_mojom));
QueryHistoryService(std::move(query_params), std::move(memory_mojoms),
std::move(callback));
}
#endif