| // 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.policy_for_404_visits = history::VisitQuery404sPolicy::kExclude404s; |
| 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(); |
| } |