chromium/ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_util.mm

// 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/ios_tab_group_sync_util.h"

#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/saved_tab_group_tab.h"
#import "components/saved_tab_groups/tab_group_sync_delegate.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/tab_group_sync_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/profile/profile_ios.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/public/features/features.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"

using tab_groups::SavedTabGroupTab;

namespace tab_groups {
namespace utils {

LocalTabGroupInfo GetLocalTabGroupInfo(
    BrowserList* browser_list,
    const tab_groups::SavedTabGroup& saved_tab_group) {
  if (!saved_tab_group.local_group_id().has_value() ||
      saved_tab_group.saved_tabs().size() == 0) {
    return LocalTabGroupInfo{};
  }

  return GetLocalTabGroupInfo(browser_list,
                              saved_tab_group.local_group_id().value());
}

LocalTabGroupInfo GetLocalTabGroupInfo(
    BrowserList* browser_list,
    const tab_groups::LocalTabGroupID& tab_group_id) {
  for (Browser* browser :
       browser_list->BrowsersOfType(BrowserList::BrowserType::kRegular)) {
    WebStateList* web_state_list = browser->GetWebStateList();
    for (const TabGroup* group : web_state_list->GetGroups()) {
      if (group->tab_group_id() == tab_group_id) {
        return LocalTabGroupInfo{
            .tab_group = group,
            .web_state_list = web_state_list,
            .browser = browser,
        };
      }
    }
  }
  return LocalTabGroupInfo{};
}

LocalTabInfo GetLocalTabInfo(BrowserList* browser_list,
                             web::WebStateID web_state_identifier) {
  for (Browser* browser :
       browser_list->BrowsersOfType(BrowserList::BrowserType::kRegular)) {
    WebStateList* web_state_list = browser->GetWebStateList();
    LocalTabInfo info = GetLocalTabInfo(web_state_list, web_state_identifier);
    if (info.tab_group) {
      return info;
    }
  }
  return LocalTabInfo{};
}

LocalTabInfo GetLocalTabInfo(WebStateList* web_state_list,
                             web::WebStateID web_state_identifier) {
  for (int index = 0; index < web_state_list->count(); ++index) {
    web::WebState* web_state = web_state_list->GetWebStateAt(index);
    if (web_state_identifier == web_state->GetUniqueIdentifier()) {
      const TabGroup* group = web_state_list->GetGroupOfWebStateAt(index);
      int index_in_group = group ? index - group->range().range_begin()
                                 : WebStateList::kInvalidIndex;
      return LocalTabInfo{
          .tab_group = group,
          .index_in_group = index_in_group,
      };
    }
  }
  return LocalTabInfo{};
}

void CloseTabGroupLocally(const TabGroup* tab_group,
                          WebStateList* web_state_list,
                          TabGroupSyncService* sync_service) {
  // `sync_service` is nullptr in incognito.
  if (sync_service && sync_service->GetGroup(tab_group->tab_group_id())) {
    sync_service->RemoveLocalTabGroupMapping(tab_group->tab_group_id());
  }
  CloseAllWebStatesInGroup(*web_state_list, tab_group,
                           WebStateList::CLOSE_USER_ACTION);
}

// Moves tab group across browsers.
void MoveTabGroupAcrossBrowsers(const TabGroup* source_tab_group,
                                Browser* source_browser,
                                Browser* destination_browser,
                                int destination_tab_group_index) {
  // Get and lock `source_web_state_list` and `destination_web_state_list`.
  WebStateList* source_web_state_list = source_browser->GetWebStateList();
  WebStateList* destination_web_state_list =
      destination_browser->GetWebStateList();
  auto source_lock = source_web_state_list->StartBatchOperation();
  auto destination_lock = destination_web_state_list->StartBatchOperation();

  int source_web_state_start_index = source_tab_group->range().range_begin();
  int tab_count = source_tab_group->range().count();
  CHECK(tab_count > 0);

  // Create the `TabGroupVisualData` for the new group.
  const tab_groups::TabGroupVisualData destination_visual_data(
      source_tab_group->visual_data());

  // Duplicate the `TabGroupId` for the new group.
  const tab_groups::TabGroupId destination_local_id =
      source_tab_group->tab_group_id();

  // Move tabs to the new browser.
  int moved_tab_count = 0;
  size_t source_group_count =
      source_browser->GetWebStateList()->GetGroups().size();
  for (int destination_index_offset = 0; destination_index_offset < tab_count;
       destination_index_offset++) {
    if (!source_web_state_list->ContainsIndex(source_web_state_start_index)) {
      // `source_web_state_start_index` should have been a valid index at all
      // times during the loop.
      base::debug::DumpWithoutCrashing();
      break;
    }
    if (source_web_state_list->GetGroupOfWebStateAt(
            source_web_state_start_index) != source_tab_group) {
      // The group of the tab to move does not match.
      base::debug::DumpWithoutCrashing();
      break;
    }
    MoveTabFromBrowserToBrowser(
        source_browser, source_web_state_start_index, destination_browser,
        destination_tab_group_index + destination_index_offset);
    moved_tab_count++;
  }

  // Create the new group.
  const TabGroup* destination_tab_group =
      destination_browser->GetWebStateList()->CreateGroup(
          TabGroupRange(destination_tab_group_index, moved_tab_count).AsSet(),
          destination_visual_data, destination_local_id);
  CHECK(destination_browser->GetWebStateList()->ContainsGroup(
      destination_tab_group));
  // Check that the source browser has one less group.
  CHECK_EQ(source_group_count,
           source_browser->GetWebStateList()->GetGroups().size() + 1,
           base::NotFatalUntil::M128);
}

void MoveTabGroupToBrowser(const TabGroup* source_tab_group,
                           Browser* destination_browser,
                           int destination_tab_group_index) {
  ChromeBrowserState* browser_state = destination_browser->GetBrowserState();
  BrowserList* browser_list =
      BrowserListFactory::GetForBrowserState(browser_state);
  const BrowserList::BrowserType browser_types =
      browser_state->IsOffTheRecord() ? BrowserList::BrowserType::kIncognito
                                      : BrowserList::BrowserType::kRegular;
  std::set<Browser*> browsers = browser_list->BrowsersOfType(browser_types);

  // Retrieve the `source_browser`.
  Browser* source_browser;
  for (Browser* browser : browsers) {
    WebStateList* web_state_list = browser->GetWebStateList();
    if (web_state_list->ContainsGroup(source_tab_group)) {
      source_browser = browser;
      break;
    }
  }

  if (!source_browser) {
    DUMP_WILL_BE_NOTREACHED()
        << "Either the 'source_tab_group' is incorrect, or the user is "
           "attempting to move a tab group across profiles (incognito <-> "
           "regular)";
    return;
  }

  if (source_browser == destination_browser) {
    // This is a reorder operation within the same WebStateList.
    destination_browser->GetWebStateList()->MoveGroup(
        source_tab_group, destination_tab_group_index);
    return;
  }

  if (!IsTabGroupSyncEnabled()) {
    MoveTabGroupAcrossBrowsers(source_tab_group, source_browser,
                               destination_browser,
                               destination_tab_group_index);
    return;
  }

  // Lock tab group sync service observer.
  CHECK_EQ(source_browser->GetBrowserState(),
           destination_browser->GetBrowserState());
  auto* sync_service =
      tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
          source_browser->GetBrowserState());
  auto lock = sync_service->CreateScopedLocalObserverPauser();

  MoveTabGroupAcrossBrowsers(source_tab_group, source_browser,
                             destination_browser, destination_tab_group_index);
}

bool ShouldUpdateHistory(web::NavigationContext* navigation_context) {
  web::WebState* web_state = navigation_context->GetWebState();

  if (!web_state || web_state->GetBrowserState()->IsOffTheRecord()) {
    return false;
  }

  // Failed navigations and 404 errors are not saved to history.
  if (navigation_context->GetError()) {
    return false;
  }

  if (navigation_context->GetResponseHeaders() &&
      navigation_context->GetResponseHeaders()->response_code() == 404) {
    return false;
  }

  if (!navigation_context->HasCommitted() ||
      !web_state->GetNavigationManager()->GetLastCommittedItem()) {
    // Navigation was replaced or aborted.
    return false;
  }

  web::NavigationItem* last_committed_item =
      web_state->GetNavigationManager()->GetLastCommittedItem();

  // Back/forward navigations do not update history.
  const ui::PageTransition transition =
      last_committed_item->GetTransitionType();
  if (transition & ui::PAGE_TRANSITION_FORWARD_BACK) {
    return false;
  }

  return true;
}

bool IsSaveableNavigation(web::NavigationContext* navigation_context) {
  // Please keep this in sync with TabGroupSyncUtils::IsSaveableNavigation as a
  // best effort.

  ui::PageTransition page_transition = navigation_context->GetPageTransition();

  // TODO(crbug.com/359726089): Check all methods other than GET.
  if (navigation_context->IsPost()) {
    return false;
  }
  if (!ui::IsValidPageTransitionType(page_transition)) {
    return false;
  }
  if (ui::PageTransitionIsRedirect(page_transition)) {
    return false;
  }

  if (!ui::PageTransitionIsMainFrame(page_transition)) {
    return false;
  }

  if (!navigation_context->HasCommitted()) {
    return false;
  }

  if (!ShouldUpdateHistory(navigation_context)) {
    return false;
  }

  // For renderer initiated navigation, in most cases these navigations will be
  // auto triggered on restoration. So there is no need to save them.
  if (navigation_context->IsRendererInitiated() &&
      !navigation_context->HasUserGesture()) {
    return false;
  }

  return IsURLValidForSavedTabGroups(navigation_context->GetUrl());
}

}  // namespace utils
}  // namespace tab_groups