// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_local_update_observer.h"
#import <memory>
#import <optional>
#import "base/check.h"
#import "base/strings/sys_string_conversions.h"
#import "components/saved_tab_groups/tab_group_sync_service.h"
#import "components/saved_tab_groups/types.h"
#import "components/saved_tab_groups/utils.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_util.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer.h"
using tab_groups::utils::GetLocalTabInfo;
using tab_groups::utils::LocalTabInfo;
namespace tab_groups {
TabGroupLocalUpdateObserver::TabGroupLocalUpdateObserver(
BrowserList* browser_list,
TabGroupSyncService* sync_service)
: sync_service_(sync_service),
browser_list_(browser_list) {
browser_list_observation_.Observe(browser_list);
CHECK(browser_list_->BrowsersOfType(BrowserList::BrowserType::kRegular)
.empty());
}
TabGroupLocalUpdateObserver::~TabGroupLocalUpdateObserver() = default;
#pragma mark - Public
void TabGroupLocalUpdateObserver::IgnoreNavigationForWebState(
web::WebState* web_state) {
ignored_web_state_identifiers_.insert(web_state->GetUniqueIdentifier());
}
#pragma mark - BrowserListObserver
void TabGroupLocalUpdateObserver::OnBrowserAdded(
const BrowserList* browser_list,
Browser* browser) {
if (browser->type() != Browser::Type::kRegular) {
return;
}
StartObservingBrowser(browser);
}
void TabGroupLocalUpdateObserver::OnBrowserRemoved(
const BrowserList* browser_list,
Browser* browser) {
if (browser->type() != Browser::Type::kRegular) {
return;
}
StopObservingWebStateList(browser->GetWebStateList());
}
void TabGroupLocalUpdateObserver::OnBrowserListShutdown(
BrowserList* browser_list) {
browser_list_observation_.Reset();
session_restoration_service_observation_.Reset();
}
#pragma mark - WebStateListObserver
void TabGroupLocalUpdateObserver::WebStateListDidChange(
WebStateList* web_state_list,
const WebStateListChange& change,
const WebStateListStatus& status) {
switch (change.type()) {
case WebStateListChange::Type::kStatusOnly: {
const WebStateListChangeStatusOnly& status_only =
change.As<WebStateListChangeStatusOnly>();
const TabGroup* old_group = status_only.old_group();
const TabGroup* new_group = status_only.new_group();
if (old_group != new_group) {
// There is a change of group.
if (old_group) {
// Remove the tab from the synced `old_group`.
RemoveLocalWebStateFromSyncedGroup(status_only.web_state(),
old_group);
}
if (new_group) {
// Insert the tab into the synced `new_group`.
AddLocalWebStateToSyncedGroup(status_only.web_state(),
web_state_list);
}
}
} break;
case WebStateListChange::Type::kDetach: {
const WebStateListChangeDetach& detach =
change.As<WebStateListChangeDetach>();
const TabGroup* detach_group = detach.group();
if (detach_group) {
// Remove the tab from the `detach_group`.
RemoveLocalWebStateFromSyncedGroup(detach.detached_web_state(),
detach_group);
}
} break;
case WebStateListChange::Type::kReplace: {
const WebStateListChangeReplace& replace =
change.As<WebStateListChangeReplace>();
const TabGroup* group =
web_state_list->GetGroupOfWebStateAt(replace.index());
if (group) {
AddLocalWebStateToSyncedGroup(replace.inserted_web_state(),
web_state_list);
RemoveLocalWebStateFromSyncedGroup(replace.replaced_web_state(), group);
}
} break;
case WebStateListChange::Type::kInsert: {
const WebStateListChangeInsert& insert =
change.As<WebStateListChangeInsert>();
if (insert.group()) {
// Insert the tab into the synced `new_group`.
AddLocalWebStateToSyncedGroup(insert.inserted_web_state(),
web_state_list);
}
} break;
case WebStateListChange::Type::kGroupCreate: {
const WebStateListChangeGroupCreate& group_create =
change.As<WebStateListChangeGroupCreate>();
CreateSyncedGroup(web_state_list, group_create.created_group());
break;
}
case WebStateListChange::Type::kGroupVisualDataUpdate: {
const WebStateListChangeGroupVisualDataUpdate& visual_data =
change.As<WebStateListChangeGroupVisualDataUpdate>();
UpdateVisualDataSyncedGroup(visual_data.updated_group());
break;
}
case WebStateListChange::Type::kMove: {
const WebStateListChangeMove& move = change.As<WebStateListChangeMove>();
const TabGroup* old_group = move.old_group();
const TabGroup* new_group = move.new_group();
if (old_group != new_group) {
if (old_group) {
// Remove the tab from the synced `old_group`.
RemoveLocalWebStateFromSyncedGroup(move.moved_web_state(), old_group);
}
if (new_group) {
// Insert the tab into the synced `new_group`.
AddLocalWebStateToSyncedGroup(move.moved_web_state(), web_state_list);
}
} else if (old_group) {
// Move the tab in the group.
MoveLocalWebStateToSyncedGroup(move.moved_web_state(), web_state_list);
}
break;
}
case WebStateListChange::Type::kGroupDelete: {
const WebStateListChangeGroupDelete& delete_group =
change.As<WebStateListChangeGroupDelete>();
DeleteSyncedGroup(delete_group.deleted_group());
break;
}
case WebStateListChange::Type::kGroupMove:
break;
}
web::WebState* web_state = status.new_active_web_state;
if (status.active_web_state_change() && web_state) {
const TabGroup* tab_group =
web_state_list->GetGroupOfWebStateAt(web_state_list->active_index());
if (tab_group) {
sync_service_->OnTabSelected(
tab_group->tab_group_id(),
web_state->GetUniqueIdentifier().identifier());
}
}
}
void TabGroupLocalUpdateObserver::WebStateListDestroyed(
WebStateList* web_state_list) {
StopObservingWebStateList(web_state_list);
}
#pragma mark - WebStateObserver
void TabGroupLocalUpdateObserver::TitleWasSet(web::WebState* web_state) {
if (sync_update_paused_) {
return;
}
// Updates before the first navigation should be ignored.
web::WebStateID identifier = web_state->GetUniqueIdentifier();
if (ignored_web_state_identifiers_.contains(identifier)) {
return;
}
UpdateLocalWebStateInSyncedGroup(web_state);
}
void TabGroupLocalUpdateObserver::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (sync_update_paused_) {
return;
}
// The first navigation after a sync update should be ignored.
web::WebStateID identifier = web_state->GetUniqueIdentifier();
if (ignored_web_state_identifiers_.contains(identifier)) {
ignored_web_state_identifiers_.erase(identifier);
return;
}
if (!utils::IsSaveableNavigation(navigation_context)) {
return;
}
UpdateLocalWebStateInSyncedGroup(web_state);
}
void TabGroupLocalUpdateObserver::WebStateDestroyed(web::WebState* web_state) {
StopObservingWebState(web_state);
}
#pragma mark - SessionRestorationObserver
void TabGroupLocalUpdateObserver::WillStartSessionRestoration(
Browser* browser) {
SetSyncUpdatePaused(true);
}
void TabGroupLocalUpdateObserver::SessionRestorationFinished(
Browser* browser,
const std::vector<web::WebState*>& restored_web_states) {
SetSyncUpdatePaused(false);
}
#pragma mark - Private
void TabGroupLocalUpdateObserver::SetSyncUpdatePaused(bool paused) {
sync_update_paused_ += paused ? 1 : -1;
CHECK_GE(sync_update_paused_, 0);
}
void TabGroupLocalUpdateObserver::StartObservingBrowser(Browser* browser) {
// Observer should be set once the session restoration service has started.
// TODO(crbug.com/350885825): Directly inject the SessionRestorationService to
// this class when it's no longer necessary for MigrateSessionStorageFormat to
// instantiate it.
if (!session_restoration_service_observation_.IsObserving()) {
session_restoration_service_observation_.Observe(
SessionRestorationServiceFactory::GetForBrowserState(
browser->GetBrowserState()));
}
WebStateList* web_state_list = browser->GetWebStateList();
web_state_list_observation_.AddObservation(web_state_list);
for (const TabGroup* group : web_state_list->GetGroups()) {
for (int index : group->range()) {
web_state_observation_.AddObservation(
web_state_list->GetWebStateAt(index));
}
}
}
void TabGroupLocalUpdateObserver::StopObservingWebStateList(
WebStateList* web_state_list) {
web_state_list_observation_.RemoveObservation(web_state_list);
}
void TabGroupLocalUpdateObserver::StartObservingWebState(
web::WebState* web_state) {
web_state_observation_.AddObservation(web_state);
}
void TabGroupLocalUpdateObserver::StopObservingWebState(
web::WebState* web_state) {
// Try to remove the `web_state` identifier from
// `ignored_web_state_identifiers_`.
ignored_web_state_identifiers_.erase(web_state->GetUniqueIdentifier());
web_state_observation_.RemoveObservation(web_state);
}
void TabGroupLocalUpdateObserver::UpdateLocalWebStateInSyncedGroup(
web::WebState* web_state) {
CHECK(!sync_update_paused_);
LocalTabInfo tab_info =
utils::GetLocalTabInfo(browser_list_, web_state->GetUniqueIdentifier());
GURL url = web_state->GetVisibleURL();
std::u16string title = web_state->GetTitle();
if (!IsURLValidForSavedTabGroups(url)) {
url = GetDefaultUrlAndTitle().first;
title = GetDefaultUrlAndTitle().second;
}
SavedTabGroupTabBuilder tab_builder;
tab_builder.SetURL(url);
tab_builder.SetTitle(title);
sync_service_->UpdateTab(tab_info.tab_group->tab_group_id(),
web_state->GetUniqueIdentifier().identifier(),
std::move(tab_builder));
}
void TabGroupLocalUpdateObserver::AddLocalWebStateToSyncedGroup(
web::WebState* web_state,
WebStateList* web_state_list) {
StartObservingWebState(web_state);
if (sync_update_paused_) {
// Early return after starting observing new tabs.
return;
}
LocalTabInfo tab_info =
web_state_list ? utils::GetLocalTabInfo(web_state_list,
web_state->GetUniqueIdentifier())
: utils::GetLocalTabInfo(browser_list_,
web_state->GetUniqueIdentifier());
utils::GetLocalTabInfo(browser_list_, web_state->GetUniqueIdentifier());
sync_service_->AddTab(tab_info.tab_group->tab_group_id(),
web_state->GetUniqueIdentifier().identifier(),
web_state->GetTitle(), web_state->GetVisibleURL(),
std::make_optional(tab_info.index_in_group));
}
void TabGroupLocalUpdateObserver::MoveLocalWebStateToSyncedGroup(
web::WebState* web_state,
WebStateList* web_state_list) {
if (sync_update_paused_) {
return;
}
LocalTabInfo tab_info =
utils::GetLocalTabInfo(web_state_list, web_state->GetUniqueIdentifier());
sync_service_->MoveTab(tab_info.tab_group->tab_group_id(),
web_state->GetUniqueIdentifier().identifier(),
tab_info.index_in_group);
}
void TabGroupLocalUpdateObserver::RemoveLocalWebStateFromSyncedGroup(
web::WebState* web_state,
const TabGroup* tab_group) {
StopObservingWebState(web_state);
if (sync_update_paused_) {
// Early return after stoping observing new tabs.
return;
}
if (!sync_service_->GetGroup(tab_group->tab_group_id())) {
// The group has been closed locally.
return;
}
sync_service_->RemoveTab(tab_group->tab_group_id(),
web_state->GetUniqueIdentifier().identifier());
}
void TabGroupLocalUpdateObserver::CreateSyncedGroup(
WebStateList* web_state_list,
const TabGroup* tab_group) {
LocalTabGroupID local_id = tab_group->tab_group_id();
if (sync_service_->GetGroup(local_id)) {
// The group already exists.
return;
}
// Generate and id for the synced tab group.
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
// Create a vector of `saved_tabs` based on local tabs.
std::vector<SavedTabGroupTab> saved_tabs;
for (int index = 0; index < tab_group->range().count(); ++index) {
int web_state_index = tab_group->range().range_begin() + index;
web::WebState* web_state = web_state_list->GetWebStateAt(web_state_index);
// Start observing the `web_state`.
StartObservingWebState(web_state);
SavedTabGroupTab saved_tab(web_state->GetVisibleURL(),
web_state->GetTitle(), saved_tab_group_id,
std::make_optional(index), std::nullopt,
web_state->GetUniqueIdentifier().identifier());
saved_tabs.push_back(saved_tab);
}
if (sync_update_paused_) {
// Early return after starting observing new tabs.
return;
}
SavedTabGroup saved_group(base::SysNSStringToUTF16(tab_group->GetRawTitle()),
tab_group->visual_data().color(), saved_tabs,
std::nullopt, saved_tab_group_id,
tab_group->tab_group_id());
sync_service_->AddGroup(saved_group);
}
void TabGroupLocalUpdateObserver::UpdateVisualDataSyncedGroup(
const TabGroup* tab_group) {
if (sync_update_paused_) {
return;
}
sync_service_->UpdateVisualData(tab_group->tab_group_id(),
&tab_group->visual_data());
}
void TabGroupLocalUpdateObserver::DeleteSyncedGroup(const TabGroup* tab_group) {
if (sync_update_paused_ ||
!sync_service_->GetGroup(tab_group->tab_group_id())) {
// The group has been closed locally.
return;
}
sync_service_->RemoveGroup(tab_group->tab_group_id());
}
} // namespace tab_groups