blob: a121968747d5d4f8dff9eb7d6fe829812541c488 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <optional>
#include <unordered_set>
#include <vector>
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/supports_user_data.h"
#include "base/uuid.h"
#include "chrome/browser/apps/app_service/web_contents_app_id_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_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_list_observer.h"
#include "chrome/browser/ui/browser_live_tab_context.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_keyed_service.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_service_factory.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.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 "chrome/browser/ui/ui_features.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "components/saved_tab_groups/features.h"
#include "components/saved_tab_groups/saved_tab_group.h"
#include "components/saved_tab_groups/saved_tab_group_tab.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/sessions/core/live_tab_context.h"
#include "components/sessions/core/serialized_navigation_entry.h"
#include "components/sessions/core/session_id.h"
#include "components/sessions/core/tab_restore_service.h"
#include "components/sessions/core/tab_restore_service_observer.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "url/gurl.h"
namespace chrome {
namespace {
const char kBrowserTabRestorerKey[] = "BrowserTabRestorer";
// BrowserTabRestorer is responsible for restoring a tab when the
// sessions::TabRestoreService finishes loading. A TabRestoreService is
// associated with a
// single Browser and deletes itself if the Browser is destroyed.
// BrowserTabRestorer is installed on the Profile (by way of user data), only
// one instance is created per profile at a time.
class BrowserTabRestorer : public sessions::TabRestoreServiceObserver,
public BrowserListObserver,
public base::SupportsUserData::Data {
public:
BrowserTabRestorer(const BrowserTabRestorer&) = delete;
BrowserTabRestorer& operator=(const BrowserTabRestorer&) = delete;
~BrowserTabRestorer() override;
static void CreateIfNecessary(Browser* browser);
private:
explicit BrowserTabRestorer(Browser* browser);
// TabRestoreServiceObserver:
void TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) override;
void TabRestoreServiceLoaded(sessions::TabRestoreService* service) override;
// BrowserListObserver:
void OnBrowserRemoved(Browser* browser) override;
raw_ptr<Browser> browser_;
raw_ptr<sessions::TabRestoreService> tab_restore_service_;
};
BrowserTabRestorer::~BrowserTabRestorer() {
tab_restore_service_->RemoveObserver(this);
BrowserList::RemoveObserver(this);
}
// static
void BrowserTabRestorer::CreateIfNecessary(Browser* browser) {
DCHECK(browser);
if (browser->profile()->GetUserData(kBrowserTabRestorerKey))
return; // Only allow one restore for a given profile at a time.
// BrowserTabRestorer is deleted at the appropriate time.
new BrowserTabRestorer(browser);
}
BrowserTabRestorer::BrowserTabRestorer(Browser* browser)
: browser_(browser),
tab_restore_service_(
TabRestoreServiceFactory::GetForProfile(browser->profile())) {
DCHECK(tab_restore_service_);
DCHECK(!tab_restore_service_->IsLoaded());
tab_restore_service_->AddObserver(this);
BrowserList::AddObserver(this);
browser_->profile()->SetUserData(kBrowserTabRestorerKey,
base::WrapUnique(this));
tab_restore_service_->LoadTabsFromLastSession();
}
void BrowserTabRestorer::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {}
void BrowserTabRestorer::TabRestoreServiceLoaded(
sessions::TabRestoreService* service) {
RestoreTab(browser_);
// This deletes us.
browser_->profile()->SetUserData(kBrowserTabRestorerKey, nullptr);
}
void BrowserTabRestorer::OnBrowserRemoved(Browser* browser) {
// This deletes us.
browser_->profile()->SetUserData(kBrowserTabRestorerKey, nullptr);
}
std::unordered_set<std::string> GetUrlsInSavedTabGroup(
tab_groups::SavedTabGroupKeyedService& saved_tab_group_service,
const base::Uuid& saved_id) {
const tab_groups::SavedTabGroup* const saved_group =
saved_tab_group_service.model()->Get(saved_id);
CHECK(saved_group);
std::unordered_set<std::string> saved_urls;
for (const tab_groups::SavedTabGroupTab& saved_tab :
saved_group->saved_tabs()) {
if (!saved_urls.contains(saved_tab.url().spec())) {
saved_urls.emplace(saved_tab.url().spec());
}
}
return saved_urls;
}
content::WebContents* OpenTabWithNavigationStack(
Browser* browser,
const sessions::TabRestoreService::Tab& restored_tab) {
const sessions::SerializedNavigationEntry& entry =
restored_tab.navigations.at(restored_tab.normalized_navigation_index());
const GURL tab_url = entry.virtual_url();
content::WebContents* created_contents = created_contents =
tab_groups::SavedTabGroupUtils::OpenTabInBrowser(
tab_url, browser, browser->profile(),
WindowOpenDisposition::NEW_BACKGROUND_TAB);
std::vector<std::unique_ptr<content::NavigationEntry>> entries =
sessions::ContentSerializedNavigationBuilder::ToNavigationEntries(
restored_tab.navigations, browser->profile());
created_contents->GetController().Restore(
restored_tab.normalized_navigation_index(),
content::RestoreType::kRestored, &entries);
CHECK_EQ(0u, entries.size());
if (restored_tab.pinned) {
browser->tab_strip_model()->SetTabPinned(
browser->tab_strip_model()->GetIndexOfWebContents(created_contents),
/*pinned=*/true);
}
return created_contents;
}
// Adds a restored tab to the saved group if its URL does not exist in the
// group.
void AddMissingTabToGroup(
Browser* const browser,
tab_groups::SavedTabGroupKeyedService& saved_tab_group_service,
const base::Uuid& saved_id,
const sessions::TabRestoreService::Tab& restored_tab,
std::unordered_set<std::string>* const saved_urls) {
const tab_groups::SavedTabGroup* const saved_group =
saved_tab_group_service.model()->Get(saved_id);
CHECK(saved_group);
const sessions::SerializedNavigationEntry& entry =
restored_tab.navigations.at(restored_tab.normalized_navigation_index());
const GURL tab_url = entry.virtual_url();
if (!saved_urls->contains(tab_url.spec())) {
// Restore the tab with is navigation stack.
content::WebContents* created_contents =
OpenTabWithNavigationStack(browser, restored_tab);
// Add the tab to the correct group.
int index =
browser->tab_strip_model()->GetIndexOfWebContents(created_contents);
browser->tab_strip_model()->AddToGroupForRestore(
{index}, saved_group->local_group_id().value());
saved_urls->emplace(tab_url.spec());
}
}
void UpdateGroupVisualData(const tab_groups::TabGroupId& group_id,
const tab_groups::TabGroupVisualData& visual_data) {
TabGroup* const tab_group =
tab_groups::SavedTabGroupUtils::GetTabGroupWithId(group_id);
CHECK(tab_group);
tab_group->SetVisualData(visual_data);
}
void OpenSavedTabGroupAndAddRestoredTabs(
Browser* browser,
const sessions::TabRestoreService::Group& group,
tab_groups::SavedTabGroupKeyedService& saved_tab_group_service) {
// service, saved id, group
std::optional<tab_groups::TabGroupId> new_group_id =
saved_tab_group_service.OpenSavedTabGroupInBrowser(
browser, group.saved_group_id.value());
CHECK(new_group_id.has_value());
// It could be the case that the current state of the saved group has
// deviated from what is represented in TabRestoreService. Make sure any
// tabs that are not in the saved group are added to it.
std::unordered_set<std::string> urls_in_saved_group = GetUrlsInSavedTabGroup(
saved_tab_group_service, group.saved_group_id.value());
for (const std::unique_ptr<sessions::TabRestoreService::Tab>& grouped_tab :
group.tabs) {
AddMissingTabToGroup(browser, saved_tab_group_service,
group.saved_group_id.value(), *grouped_tab.get(),
&urls_in_saved_group);
}
UpdateGroupVisualData(new_group_id.value(), group.visual_data);
}
void OpenTabGroup(sessions::TabRestoreService& tab_restore_service,
const sessions::TabRestoreService::Group& group,
Browser* browser) {
tab_groups::SavedTabGroupKeyedService* saved_tab_group_service =
tab_groups::SavedTabGroupServiceFactory::GetForProfile(
browser->profile());
CHECK(saved_tab_group_service);
const std::optional<base::Uuid>& saved_id = group.saved_group_id;
const bool is_group_saved =
saved_id.has_value() &&
saved_tab_group_service->model()->Contains(saved_id.value());
if (!is_group_saved) {
// Copy these values so they are not overwritten when we remove the entry
// from TabRestoreService .
tab_groups::TabGroupId group_id = group.group_id;
tab_groups::TabGroupVisualData visual_data = group.visual_data;
// If the group is not saved, restore it normally, and save it.
tab_restore_service.RestoreMostRecentEntry(browser->live_tab_context());
saved_tab_group_service->SaveGroup(group_id);
// Update the color and title of the group appropriately.
UpdateGroupVisualData(group_id, visual_data);
return;
}
const SessionID& session_id = group.id;
OpenSavedTabGroupAndAddRestoredTabs(browser, group, *saved_tab_group_service);
// Clean up TabRestoreService.
tab_restore_service.RemoveEntryById(session_id);
}
void OpenTab(sessions::TabRestoreService& tab_restore_service,
const sessions::TabRestoreService::Tab& tab,
Browser* browser) {
tab_groups::SavedTabGroupKeyedService* saved_tab_group_service =
tab_groups::SavedTabGroupServiceFactory::GetForProfile(
browser->profile());
CHECK(saved_tab_group_service);
// This value is copied here since it is used throughout this function.
std::optional<tab_groups::TabGroupId> group_id = tab.group;
const bool is_group_saved =
group_id.has_value() && tab.saved_group_id.has_value() &&
saved_tab_group_service->model()->Contains(tab.saved_group_id.value());
if (!is_group_saved) {
// Copy these values so they are not overwritten when we make calls to the
// TabRestoreService that will update its list of entries.
std::optional<base::Uuid> saved_id = tab.saved_group_id;
std::optional<tab_groups::TabGroupVisualData> visual_data =
tab.group_visual_data;
// If the tab is not in a group or has not been saved restore it normally.
tab_restore_service.RestoreMostRecentEntry(browser->live_tab_context());
if (group_id.has_value() &&
!saved_tab_group_service->model()->Contains(group_id.value())) {
// Save the group if it isn't already saved.
saved_tab_group_service->SaveGroup(group_id.value());
}
// Update the color and title of the group appropriately.
if (visual_data.has_value()) {
UpdateGroupVisualData(group_id.value(), visual_data.value());
}
return;
}
const SessionID& session_id = tab.id;
const std::optional<base::Uuid>& saved_id = tab.saved_group_id;
const std::optional<tab_groups::TabGroupVisualData>& visual_data =
tab.group_visual_data;
const tab_groups::SavedTabGroup* const saved_group =
saved_tab_group_service->model()->Get(saved_id.value());
if (saved_group->local_group_id().has_value()) {
// If the group is open already, restore the tab normally.
tab_restore_service.RestoreMostRecentEntry(browser->live_tab_context());
// Move the tab into the correct group. This happens in cases where the
// original group id was regenerated (such as when calling
// SavedTabGroupKeyedService::OpenSavedTabGroupInBrowser).
int index = browser->tab_strip_model()->active_index();
browser->tab_strip_model()->AddToExistingGroup(
{index}, saved_group->local_group_id().value(), /*add_to_end=*/true);
return;
}
std::optional<tab_groups::TabGroupId> new_group_id =
saved_tab_group_service->OpenSavedTabGroupInBrowser(browser,
saved_id.value());
CHECK(new_group_id.has_value());
// It could be the case that the current state of the saved group has deviated
// from what is represented in TabRestoreService. Make sure any tabs that are
// not in the saved group are added to it.
std::unordered_set<std::string> urls_in_saved_group =
GetUrlsInSavedTabGroup(*saved_tab_group_service, saved_id.value());
AddMissingTabToGroup(browser, *saved_tab_group_service, saved_id.value(), tab,
&urls_in_saved_group);
if (visual_data.has_value()) {
UpdateGroupVisualData(new_group_id.value(), visual_data.value());
}
// Clean up TabRestoreService.
tab_restore_service.RemoveEntryById(session_id);
}
// |app_name| can could be for an app that has been uninstalled. In that
// case we don't want to open an app window. Note that |app_name| is also used
// for other types of windows like dev tools and we always want to open an
// app window in those cases.
bool ShouldCreateAppWindowForAppName(Profile* profile,
const std::string& app_name) {
if (app_name.empty()) {
return false;
}
// Only need to check that the app is installed if |app_name| is for a
// platform app or web app. (|app_name| could also be for a devtools window.)
const std::string app_id = web_app::GetAppIdFromApplicationName(app_name);
if (app_id.empty()) {
return true;
}
return apps::IsInstalledApp(profile, app_id);
}
Browser* CreateBrowserWindow(
Profile* profile,
const sessions::TabRestoreService::Window& window) {
std::unique_ptr<Browser::CreateParams> create_params;
if (ShouldCreateAppWindowForAppName(profile, window.app_name)) {
// Only trusted app popup windows should ever be restored.
if (window.type == sessions::SessionWindow::TYPE_APP_POPUP) {
create_params = std::make_unique<Browser::CreateParams>(
Browser::CreateParams::CreateForAppPopup(
window.app_name, /*trusted_source=*/true, window.bounds, profile,
/*user_gesture=*/true));
} else {
create_params = std::make_unique<Browser::CreateParams>(
Browser::CreateParams::CreateForApp(
window.app_name, /*trusted_source=*/true, window.bounds, profile,
/*user_gesture=*/true));
}
} else {
create_params = std::make_unique<Browser::CreateParams>(
Browser::CreateParams(profile, true));
create_params->initial_bounds = window.bounds;
}
create_params->initial_show_state = window.show_state;
create_params->initial_workspace = window.workspace;
create_params->user_title = window.user_title;
return Browser::Create(*create_params.get());
}
void RecreateAndSaveTabGroup(
Browser* browser,
const sessions::TabRestoreService::Group& group,
tab_groups::SavedTabGroupKeyedService& saved_tab_group_service) {
// If the group is not saved:
// 0. Generate a new tab group id to avoid conflicts.
// 1. Open all of the tabs in the new browser window.
// 2. Add all of the tabs to a new tab group using |new_id|.
// 3. Save the group.
tab_groups::TabGroupId new_id = tab_groups::TabGroupId::GenerateNew();
std::vector<int> indices_of_tabs;
for (const std::unique_ptr<sessions::TabRestoreService::Tab>& tab :
group.tabs) {
content::WebContents* opened_tab =
OpenTabWithNavigationStack(browser, *tab.get());
int index = browser->tab_strip_model()->GetIndexOfWebContents(opened_tab);
indices_of_tabs.emplace_back(index);
}
browser->tab_strip_model()->AddToGroupForRestore(indices_of_tabs, new_id);
saved_tab_group_service.SaveGroup(new_id);
UpdateGroupVisualData(new_id, group.visual_data);
}
void OpenWindow(sessions::TabRestoreService& tab_restore_service,
const sessions::TabRestoreService::Window& window,
Browser* browser) {
tab_groups::SavedTabGroupKeyedService* saved_tab_group_service =
tab_groups::SavedTabGroupServiceFactory::GetForProfile(
browser->profile());
CHECK(saved_tab_group_service);
std::unordered_set<std::string> seen_groups;
// This should only be created when we actually need to open a new window.
Browser* new_browser = nullptr;
for (const std::unique_ptr<sessions::TabRestoreService::Tab>& tab :
window.tabs) {
if (!tab->group.has_value()) {
if (!new_browser) {
// Create a new browser window.
new_browser = CreateBrowserWindow(browser->profile(), window);
}
OpenTabWithNavigationStack(new_browser, *tab.get());
continue;
}
// Skip this group if we have already processed it.
if (seen_groups.contains(tab->group.value().ToString())) {
continue;
}
// Process all of the tabs in this group.
seen_groups.emplace(tab->group.value().ToString());
const std::unique_ptr<sessions::TabRestoreService::Group>& group =
window.groups.at(tab->group.value());
const tab_groups::SavedTabGroup* saved_group =
group->saved_group_id.has_value()
? saved_tab_group_service->model()->Get(
group->saved_group_id.value())
: nullptr;
if ((!saved_group || !saved_group->local_group_id().has_value()) &&
!new_browser) {
// Create a new browser window if we haven't already if:
// 1. The group was not saved and should be opened in the new window.
// 2. The group is not open and should be reopened in the new window.
new_browser = CreateBrowserWindow(browser->profile(), window);
}
if (!saved_group) {
// If the group is not saved:
// 1. Restore normally it
// 2. Save it.
RecreateAndSaveTabGroup(new_browser, *group.get(),
*saved_tab_group_service);
} else {
// If the group is saved:
// 1. Find the browser the group should go in.
// 2. Open the group in the browser.
// 3. Add tabs from restore to the saved group if they do not exist.
Browser* groups_browser =
saved_group->local_group_id().has_value()
? tab_groups::SavedTabGroupUtils::GetBrowserWithTabGroupId(
saved_group->local_group_id().value())
: new_browser;
CHECK(groups_browser);
OpenSavedTabGroupAndAddRestoredTabs(groups_browser, *group.get(),
*saved_tab_group_service);
groups_browser->window()->Show();
}
}
if (new_browser) {
new_browser->window()->Show();
}
tab_restore_service.RemoveEntryById(window.id);
}
} // namespace
void RestoreTab(Browser* browser) {
base::RecordAction(base::UserMetricsAction("RestoreTab"));
sessions::TabRestoreService* service =
TabRestoreServiceFactory::GetForProfile(browser->profile());
if (!service) {
return;
}
if (service->IsLoaded()) {
if (!tab_groups::IsTabGroupsSaveV2Enabled() || service->entries().empty()) {
// Restore normally.
service->RestoreMostRecentEntry(browser->live_tab_context());
return;
}
const std::unique_ptr<sessions::TabRestoreService::Entry>&
most_recent_entry = service->entries().front();
switch (most_recent_entry->type) {
case sessions::TabRestoreService::TAB: {
OpenTab(*service,
static_cast<sessions::TabRestoreService::Tab&>(
*most_recent_entry.get()),
browser);
return;
}
case sessions::TabRestoreService::WINDOW: {
OpenWindow(*service,
static_cast<sessions::TabRestoreService::Window&>(
*most_recent_entry.get()),
browser);
return;
}
case sessions::TabRestoreService::GROUP: {
OpenTabGroup(*service,
static_cast<sessions::TabRestoreService::Group&>(
*most_recent_entry.get()),
browser);
return;
}
}
NOTREACHED();
}
BrowserTabRestorer::CreateIfNecessary(browser);
}
} // namespace chrome