chromium/ios/chrome/browser/tabs_search/model/tabs_search_service.mm

// Copyright 2021 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/tabs_search/model/tabs_search_service.h"

#import <Foundation/Foundation.h>

#import "base/i18n/break_iterator.h"
#import "base/i18n/string_search.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/sessions/core/tab_restore_service.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/sync_sessions/session_sync_service.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/history/model/history_utils.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/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/synced_sessions/model/distant_session.h"
#import "ios/chrome/browser/synced_sessions/model/distant_tab.h"
#import "ios/chrome/browser/synced_sessions/model/synced_sessions.h"
#import "ios/chrome/browser/tabs/model/tab_title_util.h"
#import "ios/web/public/web_state.h"

using base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents;

TabsSearchService::TabsSearchService(
    bool is_off_the_record,
    BrowserList* browser_list,
    signin::IdentityManager* identity_manager,
    syncer::SyncService* sync_service,
    sessions::TabRestoreService* restore_service,
    sync_sessions::SessionSyncService* session_sync_service,
    history::HistoryService* history_service,
    WebHistoryServiceGetter web_history_service_getter)
    : is_off_the_record_(is_off_the_record),
      browser_list_(browser_list),
      identity_manager_(identity_manager),
      sync_service_(sync_service),
      restore_service_(restore_service),
      session_sync_service_(session_sync_service),
      history_service_(history_service),
      web_history_service_getter_(web_history_service_getter) {
  DCHECK(browser_list_);

  // Those services are only used if not off-the-record, so allow them to
  // be null when off-the-record.
  if (!is_off_the_record_) {
    DCHECK(identity_manager_);
    DCHECK(sync_service_);
    DCHECK(session_sync_service_);
    DCHECK(restore_service_);
    DCHECK(history_service_);
    DCHECK(!web_history_service_getter_.is_null());
  }
}

TabsSearchService::TabsSearchBrowserResults::TabsSearchBrowserResults(
    Browser* browser,
    const std::vector<web::WebState*> web_states,
    const std::vector<const TabGroup*> tab_groups)
    : browser(browser), web_states(web_states), tab_groups(tab_groups) {}

TabsSearchService::TabsSearchBrowserResults::~TabsSearchBrowserResults() =
    default;

TabsSearchService::TabsSearchBrowserResults::TabsSearchBrowserResults(
    const TabsSearchBrowserResults&) = default;

TabsSearchService::~TabsSearchService() = default;

void TabsSearchService::Search(
    const std::u16string& term,
    base::OnceCallback<void(std::vector<TabsSearchBrowserResults>)>
        completion) {
  const BrowserList::BrowserType browser_types =
      is_off_the_record_ ? BrowserList::BrowserType::kIncognito
                         : BrowserList::BrowserType::kRegularAndInactive;
  std::set<Browser*> browsers = browser_list_->BrowsersOfType(browser_types);
  SearchWithinBrowsers(browsers, term, std::move(completion));
}

void TabsSearchService::SearchRecentlyClosed(
    const std::u16string& term,
    base::OnceCallback<void(std::vector<RecentlyClosedItemPair>)> completion) {
  DCHECK(!is_off_the_record_);
  FixedPatternStringSearchIgnoringCaseAndAccents query_search(term);

  std::vector<RecentlyClosedItemPair> results;
  for (const auto& entry : restore_service_->entries()) {
    DCHECK(entry);

    // Only TAB type is handled.
    // TODO(crbug.com/40676931) : Support WINDOW restoration under multi-window.
    DCHECK_EQ(sessions::tab_restore::Type::TAB, entry->type);
    const sessions::tab_restore::Tab* tab =
        static_cast<const sessions::tab_restore::Tab*>(entry.get());
    const sessions::SerializedNavigationEntry& navigationEntry =
        tab->navigations[tab->current_navigation_index];

    if (query_search.Search(navigationEntry.title(), /*match_index=*/nullptr,
                            /*match_length=*/nullptr) ||
        query_search.Search(
            base::UTF8ToUTF16(navigationEntry.virtual_url().spec()),
            /*match_index=*/nullptr, nullptr)) {
      RecentlyClosedItemPair matching_item = {entry->id, navigationEntry};
      results.push_back(matching_item);
    }
  }

  std::move(completion).Run(results);
}

void TabsSearchService::SearchRemoteTabs(
    const std::u16string& term,
    base::OnceCallback<void(std::unique_ptr<synced_sessions::SyncedSessions>,
                            std::vector<synced_sessions::DistantTabsSet>)>
        completion) {
  DCHECK(!is_off_the_record_);
  std::vector<synced_sessions::DistantTabsSet> results;

  if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    // There must be a primary account for synced sessions to be available.
    std::move(completion).Run(nullptr, results);
    return;
  }

  FixedPatternStringSearchIgnoringCaseAndAccents query_search(term);
  auto synced_sessions =
      std::make_unique<synced_sessions::SyncedSessions>(session_sync_service_);

  for (size_t s = 0; s < synced_sessions->GetSessionCount(); s++) {
    const synced_sessions::DistantSession* session =
        synced_sessions->GetSession(s);

    synced_sessions::DistantTabsSet distant_tabs;
    distant_tabs.session_tag = session->tag;

    std::vector<synced_sessions::DistantTab*> tabs;
    for (auto&& distant_tab : session->tabs) {
      if (query_search.Search(distant_tab->title, /*match_index=*/nullptr,
                              /*match_length=*/nullptr) ||
          query_search.Search(
              base::UTF8ToUTF16(distant_tab->virtual_url.spec()),
              /*match_index=*/nullptr, nullptr)) {
        tabs.push_back(distant_tab.get());
      }
    }
    distant_tabs.filtered_tabs = tabs;

    if (tabs.size() > 0) {
      results.push_back(distant_tabs);
    }
  }

  std::move(completion).Run(std::move(synced_sessions), results);
}

void TabsSearchService::SearchHistory(
    const std::u16string& term,
    base::OnceCallback<void(size_t result_count)> completion) {
  DCHECK(!is_off_the_record_);
  DCHECK(completion);

  if (!browsing_history_service_) {
    history_driver_ = std::make_unique<IOSBrowsingHistoryDriver>(
        web_history_service_getter_, this);

    browsing_history_service_ =
        std::make_unique<history::BrowsingHistoryService>(
            history_driver_.get(), history_service_.get(), sync_service_.get());
  }

  ongoing_history_search_term_ = term;
  history_search_callback_ = std::move(completion);

  history::QueryOptions options;
  options.duplicate_policy = history::QueryOptions::REMOVE_ALL_DUPLICATES;
  options.matching_algorithm =
      query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH;
  browsing_history_service_->QueryHistory(term, options);
}

void TabsSearchService::Shutdown() {
  // BrowsingHistoryService registers a SyncServiceObserver with the
  // SyncService. Destroy it during shutdown to ensure it is removed
  // before the SyncService destruction.
  browsing_history_service_.reset();

  // The driver has reference to WebHistoryServiceFactory and thus
  // needs to be destroyed during the shutdown too.
  history_driver_.reset();
}

#pragma mark - Private

void TabsSearchService::SearchWithinBrowsers(
    const std::set<Browser*>& browsers,
    const std::u16string& term,
    base::OnceCallback<void(std::vector<TabsSearchBrowserResults>)>
        completion) {
  FixedPatternStringSearchIgnoringCaseAndAccents query_search(term);

  std::vector<TabsSearchBrowserResults> results;

  for (Browser* browser : browsers) {
    std::vector<web::WebState*> matching_web_states;
    WebStateList* webStateList = browser->GetWebStateList();
    for (int index = 0; index < webStateList->count(); ++index) {
      web::WebState* web_state = webStateList->GetWebStateAt(index);
      auto title = base::SysNSStringToUTF16(tab_util::GetTabTitle(web_state));
      auto url_string = base::UTF8ToUTF16(web_state->GetVisibleURL().spec());
      if (query_search.Search(title, /*match_index=*/nullptr,
                              /*match_length=*/nullptr) ||
          query_search.Search(url_string, /*match_index=*/nullptr,
                              /*match_length=*/nullptr)) {
        matching_web_states.push_back(web_state);
      }
    }

    std::vector<const TabGroup*> matching_tab_groups;
    for (const TabGroup* group : webStateList->GetGroups()) {
      std::u16string group_title = group->visual_data().title();
      if (query_search.Search(group_title, /*match_index=*/nullptr,
                              /*match_length=*/nullptr)) {
        matching_tab_groups.push_back(group);
      }
    }

    if (!matching_web_states.empty() || !matching_tab_groups.empty()) {
      TabsSearchBrowserResults browser_results(browser, matching_web_states,
                                               matching_tab_groups);
      results.push_back(browser_results);
    }
  }

  std::move(completion).Run(results);
}

#pragma mark history::BrowsingHistoryDriver

void TabsSearchService::HistoryQueryCompleted(
    const std::vector<history::BrowsingHistoryService::HistoryEntry>& results,
    const history::BrowsingHistoryService::QueryResultsInfo& query_results_info,
    base::OnceClosure continuation_closure) {
  if (!history_search_callback_) {
    return;
  }

  if (query_results_info.search_text != ongoing_history_search_term_) {
    // This is an old search, ignore results.
    return;
  }

  std::move(history_search_callback_).Run(results.size());

  ongoing_history_search_term_ = std::u16string();
}