blob: 01c441dbc7178f7ec751c1bc787491bbe8ac84c3 [file] [log] [blame]
// Copyright 2020 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/tab_search/tab_search_page_handler.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/api/tab_groups/tab_groups_util.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_live_tab_context.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/webui/util/image_util.h"
#include "chrome/common/webui_url_constants.h"
#include "ui/base/l10n/time_format.h"
namespace {
constexpr base::TimeDelta kTabsChangeDelay =
base::TimeDelta::FromMilliseconds(50);
std::string GetLastActiveElapsedText(
const base::TimeTicks& last_active_time_ticks) {
const base::TimeDelta elapsed =
base::TimeTicks::Now() - last_active_time_ticks;
return base::UTF16ToUTF8(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, elapsed));
}
std::string GetLastActiveElapsedText(const base::Time& last_active_time) {
const base::TimeDelta elapsed = base::Time::Now() - last_active_time;
return base::UTF16ToUTF8(ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, elapsed));
}
// If a recently closed tab is associated to a group that is no longer
// open we create a TabGroup entry with the required fields to support
// rendering the tab's associated group information in the UI.
void CreateTabGroupIfNotPresent(
sessions::TabRestoreService::Tab* tab,
std::set<tab_groups::TabGroupId>& tab_group_ids,
std::vector<tab_search::mojom::TabGroupPtr>& tab_groups) {
if (tab->group.has_value() &&
!base::Contains(tab_group_ids, tab->group.value())) {
tab_groups::TabGroupId tab_group_id = tab->group.value();
const tab_groups::TabGroupVisualData* tab_group_visual_data =
&tab->group_visual_data.value();
auto tab_group = tab_search::mojom::TabGroup::New();
tab_group->id = tab_group_id.token();
tab_group->color = tab_group_visual_data->color();
tab_group->title = base::UTF16ToUTF8(tab_group_visual_data->title());
tab_group_ids.insert(tab_group_id);
tab_groups.push_back(std::move(tab_group));
}
}
} // namespace
TabSearchPageHandler::TabSearchPageHandler(
mojo::PendingReceiver<tab_search::mojom::PageHandler> receiver,
mojo::PendingRemote<tab_search::mojom::Page> page,
content::WebUI* web_ui,
ui::MojoBubbleWebUIController* webui_controller)
: receiver_(this, std::move(receiver)),
page_(std::move(page)),
web_ui_(web_ui),
webui_controller_(webui_controller),
debounce_timer_(std::make_unique<base::RetainingOneShotTimer>(
FROM_HERE,
kTabsChangeDelay,
base::BindRepeating(&TabSearchPageHandler::NotifyTabsChanged,
base::Unretained(this)))) {
Observe(web_ui_->GetWebContents());
browser_tab_strip_tracker_.Init();
}
TabSearchPageHandler::~TabSearchPageHandler() {
base::UmaHistogramCounts1000("Tabs.TabSearch.NumTabsClosedPerInstance",
num_tabs_closed_);
base::UmaHistogramEnumeration("Tabs.TabSearch.CloseAction",
called_switch_to_tab_
? TabSearchCloseAction::kTabSwitch
: TabSearchCloseAction::kNoAction);
}
void TabSearchPageHandler::CloseTab(int32_t tab_id) {
absl::optional<TabDetails> optional_details = GetTabDetails(tab_id);
if (!optional_details)
return;
++num_tabs_closed_;
// CloseTab() can target the WebContents hosting Tab Search if the Tab Search
// WebUI is open in a chrome browser tab rather than its bubble. In this case
// CloseWebContentsAt() closes the WebContents hosting this
// TabSearchPageHandler object, causing it to be immediately destroyed. Ensure
// that no further actions are performed following the call to
// CloseWebContentsAt(). See (https://crbug.com/1175507).
auto* tab_strip_model = optional_details->tab_strip_model;
const int tab_index = optional_details->index;
tab_strip_model->CloseWebContentsAt(
tab_index, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
// Do not add code past this point.
}
void TabSearchPageHandler::GetProfileData(GetProfileDataCallback callback) {
TRACE_EVENT0("browser", "TabSearchPageHandler:GetProfileTabs");
auto profile_tabs = CreateProfileData();
// On first run record the number of windows and tabs open for the given
// profile.
if (!sent_initial_payload_) {
sent_initial_payload_ = true;
int tab_count = 0;
for (const auto& window : profile_tabs->windows)
tab_count += window->tabs.size();
base::UmaHistogramCounts100("Tabs.TabSearch.NumWindowsOnOpen",
profile_tabs->windows.size());
base::UmaHistogramCounts10000("Tabs.TabSearch.NumTabsOnOpen", tab_count);
}
std::move(callback).Run(std::move(profile_tabs));
}
absl::optional<TabSearchPageHandler::TabDetails>
TabSearchPageHandler::GetTabDetails(int32_t tab_id) {
for (auto* browser : *BrowserList::GetInstance()) {
if (!ShouldTrackBrowser(browser)) {
continue;
}
TabStripModel* tab_strip_model = browser->tab_strip_model();
for (int index = 0; index < tab_strip_model->count(); ++index) {
content::WebContents* contents = tab_strip_model->GetWebContentsAt(index);
if (extensions::ExtensionTabUtil::GetTabId(contents) == tab_id) {
return TabDetails(browser, tab_strip_model, index);
}
}
}
return absl::nullopt;
}
void TabSearchPageHandler::SwitchToTab(
tab_search::mojom::SwitchToTabInfoPtr switch_to_tab_info) {
absl::optional<TabDetails> optional_details =
GetTabDetails(switch_to_tab_info->tab_id);
if (!optional_details)
return;
called_switch_to_tab_ = true;
const TabDetails& details = optional_details.value();
details.tab_strip_model->ActivateTabAt(details.index);
details.browser->window()->Activate();
}
void TabSearchPageHandler::OpenRecentlyClosedEntry(int32_t session_id) {
sessions::TabRestoreService* tab_restore_service =
TabRestoreServiceFactory::GetForProfile(Profile::FromWebUI(web_ui_));
if (!tab_restore_service)
return;
Browser* active_browser = chrome::FindLastActive();
if (!active_browser)
return;
tab_restore_service->RestoreEntryById(
BrowserLiveTabContext::FindContextForWebContents(
active_browser->tab_strip_model()->GetActiveWebContents()),
SessionID::FromSerializedValue(session_id),
WindowOpenDisposition::NEW_FOREGROUND_TAB);
}
void TabSearchPageHandler::ShowUI() {
auto embedder = webui_controller_->embedder();
if (embedder)
embedder->ShowUI();
}
tab_search::mojom::ProfileDataPtr TabSearchPageHandler::CreateProfileData() {
auto profile_data = tab_search::mojom::ProfileData::New();
Browser* active_browser = chrome::FindLastActive();
if (!active_browser)
return profile_data;
std::set<DedupKey> tab_dedup_keys;
std::set<tab_groups::TabGroupId> tab_group_ids;
for (auto* browser : *BrowserList::GetInstance()) {
if (!ShouldTrackBrowser(browser))
continue;
TabStripModel* tab_strip_model = browser->tab_strip_model();
auto window = tab_search::mojom::Window::New();
window->active = (browser == active_browser);
window->height = browser->window()->GetContentsSize().height();
for (int i = 0; i < tab_strip_model->count(); ++i) {
auto* web_contents = tab_strip_model->GetWebContentsAt(i);
// A Tab can potentially be in a state where it has no committed entries
// during loading and thus has no title/URL. Skip any such pending tabs.
// These tabs will be added to the list later on once loading has
// finished (crbug.com/1197526).
if (!web_contents->GetController().GetLastCommittedEntry())
continue;
tab_search::mojom::TabPtr tab = GetTab(tab_strip_model, web_contents, i);
tab_dedup_keys.insert(DedupKey(tab->url, tab->group_id));
window->tabs.push_back(std::move(tab));
}
profile_data->windows.push_back(std::move(window));
for (auto tab_group_id : tab_strip_model->group_model()->ListTabGroups()) {
const tab_groups::TabGroupVisualData* tab_group_visual_data =
tab_strip_model->group_model()
->GetTabGroup(tab_group_id)
->visual_data();
auto tab_group = tab_search::mojom::TabGroup::New();
tab_group->id = tab_group_id.token();
tab_group->title = base::UTF16ToUTF8(tab_group_visual_data->title());
tab_group->color = tab_group_visual_data->color();
tab_group_ids.insert(tab_group_id);
profile_data->tab_groups.push_back(std::move(tab_group));
}
}
AddRecentlyClosedEntries(profile_data->recently_closed_tabs,
profile_data->recently_closed_tab_groups,
tab_group_ids, profile_data->tab_groups,
tab_dedup_keys);
DCHECK(features::kTabSearchRecentlyClosedTabCountThreshold.Get() >= 0);
return profile_data;
}
void TabSearchPageHandler::AddRecentlyClosedEntries(
std::vector<tab_search::mojom::RecentlyClosedTabPtr>& recently_closed_tabs,
std::vector<tab_search::mojom::RecentlyClosedTabGroupPtr>&
recently_closed_tab_groups,
std::set<tab_groups::TabGroupId>& tab_group_ids,
std::vector<tab_search::mojom::TabGroupPtr>& tab_groups,
std::set<DedupKey>& tab_dedup_keys) {
sessions::TabRestoreService* tab_restore_service =
TabRestoreServiceFactory::GetForProfile(Profile::FromWebUI(web_ui_));
if (tab_restore_service) {
const int kRecentlyClosedTabCountThreshold = static_cast<size_t>(
features::kTabSearchRecentlyClosedTabCountThreshold.Get());
int recently_closed_tab_count = 0;
// The minimum number of desired recently closed items (tab or group) to be
// shown in the 'Recently Closed' section of the UI.
const int kMinRecentlyClosedItemDisplayCount = static_cast<size_t>(
features::kTabSearchRecentlyClosedDefaultItemDisplayCount.Get());
int recently_closed_item_count = 0;
// Attempt to add as many recently closed items as necessary to support the
// default item display count. On reaching this minimum, keep adding
// items until we have reached or exceeded a tab count threshold value.
// Ignore any entries that match URLs that are currently open.
for (auto& entry : tab_restore_service->entries()) {
if (recently_closed_item_count >= kMinRecentlyClosedItemDisplayCount &&
recently_closed_tab_count >= kRecentlyClosedTabCountThreshold) {
return;
}
if (entry->type == sessions::TabRestoreService::Type::WINDOW) {
sessions::TabRestoreService::Window* window =
static_cast<sessions::TabRestoreService::Window*>(entry.get());
for (auto& window_tab : window->tabs) {
sessions::TabRestoreService::Tab* tab =
static_cast<sessions::TabRestoreService::Tab*>(window_tab.get());
if (AddRecentlyClosedTab(tab, recently_closed_tabs, tab_dedup_keys,
tab_group_ids, tab_groups)) {
recently_closed_tab_count += 1;
recently_closed_item_count += 1;
}
if (recently_closed_item_count >=
kMinRecentlyClosedItemDisplayCount &&
recently_closed_tab_count >= kRecentlyClosedTabCountThreshold) {
return;
}
}
} else if (entry->type == sessions::TabRestoreService::Type::TAB) {
sessions::TabRestoreService::Tab* tab =
static_cast<sessions::TabRestoreService::Tab*>(entry.get());
// If a recently closed tab is associated to a group that is no longer
// open we create TabGroup entry with the required fields to support
// rendering the tab's associated group information in the UI.
if (tab->group.has_value() &&
!base::Contains(tab_group_ids, tab->group.value())) {
tab_groups::TabGroupId tab_group_id = tab->group.value();
const tab_groups::TabGroupVisualData* tab_group_visual_data =
&tab->group_visual_data.value();
auto tab_group = tab_search::mojom::TabGroup::New();
tab_group->id = tab_group_id.token();
tab_group->color = tab_group_visual_data->color();
tab_group->title = base::UTF16ToUTF8(tab_group_visual_data->title());
tab_group_ids.insert(tab_group_id);
tab_groups.push_back(std::move(tab_group));
}
if (AddRecentlyClosedTab(tab, recently_closed_tabs, tab_dedup_keys,
tab_group_ids, tab_groups)) {
recently_closed_tab_count += 1;
recently_closed_item_count += 1;
}
} else if (entry->type == sessions::TabRestoreService::Type::GROUP) {
sessions::TabRestoreService::Group* group =
static_cast<sessions::TabRestoreService::Group*>(entry.get());
const tab_groups::TabGroupVisualData* tab_group_visual_data =
&group->visual_data;
auto recently_closed_tab_group =
tab_search::mojom::RecentlyClosedTabGroup::New();
recently_closed_tab_group->session_id = entry->id.id();
recently_closed_tab_group->id = group->group_id.token();
recently_closed_tab_group->color = tab_group_visual_data->color();
recently_closed_tab_group->title =
base::UTF16ToUTF8(tab_group_visual_data->title());
recently_closed_tab_group->tab_count = group->tabs.size();
recently_closed_tab_group->last_active_time = entry->timestamp;
recently_closed_tab_group->last_active_elapsed_text =
GetLastActiveElapsedText(entry->timestamp);
for (auto& tab : group->tabs) {
if (AddRecentlyClosedTab(tab.get(), recently_closed_tabs,
tab_dedup_keys, tab_group_ids, tab_groups)) {
recently_closed_tab_count += 1;
}
}
recently_closed_tab_groups.push_back(
std::move(recently_closed_tab_group));
// Restored recently closed tab groups map to a single display item.
recently_closed_item_count += 1;
}
}
}
}
bool TabSearchPageHandler::AddRecentlyClosedTab(
sessions::TabRestoreService::Tab* tab,
std::vector<tab_search::mojom::RecentlyClosedTabPtr>& recently_closed_tabs,
std::set<DedupKey>& tab_dedup_keys,
std::set<tab_groups::TabGroupId>& tab_group_ids,
std::vector<tab_search::mojom::TabGroupPtr>& tab_groups) {
if (tab->navigations.size() == 0)
return false;
tab_search::mojom::RecentlyClosedTabPtr recently_closed_tab =
GetRecentlyClosedTab(tab);
DedupKey dedup_id(recently_closed_tab->url, recently_closed_tab->group_id);
// Ignore NTP entries, duplicate entries and and tabs with empty URLs.
if (base::Contains(tab_dedup_keys, dedup_id) ||
recently_closed_tab->url == GURL(chrome::kChromeUINewTabPageURL) ||
recently_closed_tab->url.empty()) {
return false;
}
tab_dedup_keys.insert(dedup_id);
if (tab->group.has_value()) {
recently_closed_tab->group_id = tab->group.value().token();
CreateTabGroupIfNotPresent(tab, tab_group_ids, tab_groups);
}
recently_closed_tabs.push_back(std::move(recently_closed_tab));
return true;
}
tab_search::mojom::TabPtr TabSearchPageHandler::GetTab(
TabStripModel* tab_strip_model,
content::WebContents* contents,
int index) {
auto tab_data = tab_search::mojom::Tab::New();
tab_data->active = tab_strip_model->active_index() == index;
tab_data->tab_id = extensions::ExtensionTabUtil::GetTabId(contents);
tab_data->index = index;
const absl::optional<tab_groups::TabGroupId> group_id =
tab_strip_model->GetTabGroupForTab(index);
if (group_id.has_value()) {
tab_data->group_id = group_id.value().token();
}
TabRendererData tab_renderer_data =
TabRendererData::FromTabInModel(tab_strip_model, index);
tab_data->pinned = tab_renderer_data.pinned;
tab_data->title = base::UTF16ToUTF8(tab_renderer_data.title);
tab_data->url = tab_renderer_data.last_committed_url.is_empty()
? tab_renderer_data.visible_url.spec()
: tab_renderer_data.last_committed_url.spec();
if (tab_renderer_data.favicon.isNull()) {
tab_data->is_default_favicon = true;
} else {
tab_data->favicon_url = webui::EncodePNGAndMakeDataURI(
tab_renderer_data.favicon, web_ui_->GetDeviceScaleFactor());
tab_data->is_default_favicon =
tab_renderer_data.favicon.BackedBySameObjectAs(
favicon::GetDefaultFavicon().AsImageSkia());
}
tab_data->show_icon = tab_renderer_data.show_icon;
const base::TimeTicks last_active_time_ticks = contents->GetLastActiveTime();
tab_data->last_active_time_ticks = last_active_time_ticks;
tab_data->last_active_elapsed_text =
GetLastActiveElapsedText(last_active_time_ticks);
return tab_data;
}
tab_search::mojom::RecentlyClosedTabPtr
TabSearchPageHandler::GetRecentlyClosedTab(
sessions::TabRestoreService::Tab* tab) {
auto recently_closed_tab = tab_search::mojom::RecentlyClosedTab::New();
DCHECK(tab->navigations.size() > 0);
sessions::SerializedNavigationEntry& entry =
tab->navigations[tab->current_navigation_index];
recently_closed_tab->tab_id = tab->id.id();
recently_closed_tab->url = entry.virtual_url().spec();
recently_closed_tab->title = entry.title().empty()
? recently_closed_tab->url
: base::UTF16ToUTF8(entry.title());
const base::Time last_active_time = entry.timestamp();
recently_closed_tab->last_active_time = last_active_time;
recently_closed_tab->last_active_elapsed_text =
GetLastActiveElapsedText(last_active_time);
if (tab->group.has_value()) {
recently_closed_tab->group_id = tab->group.value().token();
}
return recently_closed_tab;
}
void TabSearchPageHandler::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (webui_hidden_ ||
browser_tab_strip_tracker_.is_processing_initial_browsers()) {
return;
}
if (change.type() == TabStripModelChange::kRemoved) {
std::vector<int> tab_ids;
for (auto& content_with_index : change.GetRemove()->contents) {
tab_ids.push_back(
extensions::ExtensionTabUtil::GetTabId(content_with_index.contents));
}
page_->TabsRemoved(tab_ids);
return;
}
ScheduleDebounce();
}
void TabSearchPageHandler::TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) {
if (webui_hidden_)
return;
// TODO(crbug.com/1112496): Support more values for TabChangeType and filter
// out the changes we are not interested in.
if (change_type != TabChangeType::kAll)
return;
Browser* browser = chrome::FindBrowserWithWebContents(contents);
if (!browser)
return;
TRACE_EVENT0("browser", "TabSearchPageHandler:TabChangedAt");
page_->TabUpdated(GetTab(browser->tab_strip_model(), contents, index));
}
void TabSearchPageHandler::ScheduleDebounce() {
if (!debounce_timer_->IsRunning())
debounce_timer_->Reset();
}
void TabSearchPageHandler::NotifyTabsChanged() {
page_->TabsChanged(CreateProfileData());
debounce_timer_->Stop();
}
bool TabSearchPageHandler::ShouldTrackBrowser(Browser* browser) {
return browser->profile() == Profile::FromWebUI(web_ui_) &&
browser->type() == Browser::Type::TYPE_NORMAL;
}
void TabSearchPageHandler::OnVisibilityChanged(content::Visibility visibility) {
webui_hidden_ = visibility == content::Visibility::HIDDEN;
}
void TabSearchPageHandler::SetTimerForTesting(
std::unique_ptr<base::RetainingOneShotTimer> timer) {
debounce_timer_ = std::move(timer);
debounce_timer_->Start(
FROM_HERE, kTabsChangeDelay,
base::BindRepeating(&TabSearchPageHandler::NotifyTabsChanged,
base::Unretained(this)));
}