| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/startup/focus/match_candidate.h" |
| |
| #include "base/strings/string_util.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/browser/ui/startup/focus/selector.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/web_applications/web_app_browser_controller.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace focus { |
| |
| namespace { |
| |
| // Canonicalizes URLs for consistent matching by normalizing format. |
| GURL CanonicalizeUrl(const GURL& url) { |
| if (!url.is_valid()) { |
| return url; |
| } |
| |
| std::string spec = url.spec(); |
| |
| // For root paths (just domain), ensure consistent trailing slash. |
| if (url.path().length() <= 1) { |
| // Root path: ensure it ends with / |
| if (!base::EndsWith(spec, "/", base::CompareCase::SENSITIVE)) { |
| spec += "/"; |
| } |
| } else { |
| // Non-root path: remove trailing slash. |
| if (base::EndsWith(spec, "/", base::CompareCase::SENSITIVE)) { |
| spec = spec.substr(0, spec.length() - 1); |
| } |
| } |
| |
| return GURL(spec); |
| } |
| |
| // Attempts to match an app window by its manifest ID against the selector. |
| // Returns the manifest ID spec if matched, std::nullopt otherwise. |
| std::optional<std::string> MatchAppByManifestId( |
| BrowserWindowInterface& browser_window, |
| const Selector& selector, |
| const GURL& canonicalized_selector_url, |
| web_app::WebAppRegistrar* registrar) { |
| web_app::AppBrowserController* app_controller = |
| browser_window.GetAppBrowserController(); |
| // Ensure this is specifically a WebAppBrowserController, not just any |
| // AppBrowserController. |
| web_app::WebAppBrowserController* web_app_controller = |
| app_controller ? app_controller->AsWebAppBrowserController() : nullptr; |
| if (!web_app_controller) { |
| return std::nullopt; |
| } |
| |
| const webapps::AppId& browser_app_id = web_app_controller->app_id(); |
| const web_app::WebApp* web_app = registrar->GetAppById(browser_app_id); |
| if (!web_app) { |
| return std::nullopt; |
| } |
| |
| const webapps::ManifestId& manifest_id = web_app->manifest_id(); |
| GURL canonicalized_manifest_id = CanonicalizeUrl(manifest_id); |
| |
| bool is_match = false; |
| if (selector.type == SelectorType::kUrlExact) { |
| is_match = (canonicalized_manifest_id == canonicalized_selector_url); |
| } else if (selector.type == SelectorType::kUrlPrefix) { |
| std::string manifest_id_string = canonicalized_manifest_id.spec(); |
| std::string selector_url_string = canonicalized_selector_url.spec(); |
| is_match = base::StartsWith(manifest_id_string, selector_url_string, |
| base::CompareCase::SENSITIVE); |
| } |
| |
| if (is_match) { |
| return manifest_id.spec(); |
| } |
| |
| return std::nullopt; |
| } |
| |
| } // namespace |
| |
| MatchCandidate::MatchCandidate(BrowserWindowInterface& browser_window, |
| int tab_index, |
| content::WebContents& web_contents, |
| base::Time last_active_time, |
| const std::string& matched_url, |
| const std::optional<std::string>& app_id) |
| : browser_window(browser_window), |
| tab_index(tab_index), |
| web_contents(web_contents), |
| last_active_time(last_active_time), |
| matched_url(matched_url), |
| app_id(app_id) {} |
| |
| MatchCandidate::MatchCandidate(MatchCandidate&& other) noexcept = default; |
| |
| MatchCandidate& MatchCandidate::operator=(MatchCandidate&& other) noexcept = |
| default; |
| |
| MatchCandidate::~MatchCandidate() = default; |
| |
| bool MatchCandidate::operator<(const MatchCandidate& other) const { |
| // For MRU sorting: more recent items should come first in the sorted array. |
| // std::sort uses operator< to determine order: if a < b, then a comes before. |
| // So we want more recent (larger time) items to be "less than" older items. |
| |
| // First priority: Compare tab last active times. |
| if (last_active_time != other.last_active_time) { |
| return last_active_time > |
| other.last_active_time; // More recent = "less than" = comes first |
| } |
| |
| // Second priority: App windows come before regular tabs (if times are equal). |
| if (app_id.has_value() && !other.app_id.has_value()) { |
| return true; // App window comes first. |
| } |
| if (!app_id.has_value() && other.app_id.has_value()) { |
| return false; // Regular tab comes after app window. |
| } |
| |
| // Third priority: Among regular tabs with same times, use tab index as |
| // tiebreaker. |
| if (!app_id.has_value() && !other.app_id.has_value()) { |
| return tab_index < other.tab_index; // Lower tab index comes first. |
| } |
| |
| return false; // Equal items |
| } |
| |
| std::optional<MatchCandidate> MatchTab(const Selector& selector, |
| BrowserWindowInterface& browser_window, |
| int tab_index, |
| content::WebContents& web_contents, |
| web_app::WebAppRegistrar* registrar) { |
| const GURL& tab_url = web_contents.GetLastCommittedURL(); |
| GURL canonicalized_tab_url = CanonicalizeUrl(tab_url); |
| GURL canonicalized_selector_url = CanonicalizeUrl(selector.url); |
| |
| bool is_match = false; |
| std::optional<std::string> matched_app_id; |
| |
| // Check if this is an app window and try matching against manifest ID ONLY. |
| // Apps should only match by their manifest ID to allow disambiguation |
| // between apps and regular tabs with similar URLs. |
| if (browser_window.GetType() == BrowserWindowInterface::TYPE_APP && |
| registrar) { |
| matched_app_id = MatchAppByManifestId( |
| browser_window, selector, canonicalized_selector_url, registrar); |
| // For app windows, return early - don't match by tab URL. |
| if (!matched_app_id.has_value()) { |
| return std::nullopt; |
| } |
| is_match = true; |
| } else { |
| // For regular browser windows, match by tab URL |
| if (selector.type == SelectorType::kUrlExact) { |
| is_match = (canonicalized_tab_url == canonicalized_selector_url); |
| } else if (selector.type == SelectorType::kUrlPrefix) { |
| std::string tab_url_string = canonicalized_tab_url.spec(); |
| std::string selector_url_string = canonicalized_selector_url.spec(); |
| is_match = base::StartsWith(tab_url_string, selector_url_string, |
| base::CompareCase::SENSITIVE); |
| } |
| } |
| |
| if (!is_match) { |
| return std::nullopt; |
| } |
| |
| base::Time last_active_time = web_contents.GetLastActiveTime(); |
| return MatchCandidate(browser_window, tab_index, web_contents, |
| last_active_time, tab_url.spec(), matched_app_id); |
| } |
| |
| } // namespace focus |