blob: 64f15fd9372facdcba3c84d5a53f0ac81cd4c1db [file] [log] [blame]
// 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.
#import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller.h"
#import <optional>
#import <string>
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/trace_event/trace_event.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/omnibox/browser/autocomplete_classifier.h"
#import "components/omnibox/browser/autocomplete_controller.h"
#import "components/omnibox/browser/autocomplete_input.h"
#import "components/omnibox/browser/autocomplete_match.h"
#import "components/omnibox/browser/autocomplete_result.h"
#import "components/omnibox/browser/clipboard_provider.h"
#import "components/omnibox/browser/history_url_provider.h"
#import "components/omnibox/browser/omnibox_client.h"
#import "components/omnibox/browser/omnibox_popup_selection.h"
#import "components/omnibox/browser/page_classification_functions.h"
#import "components/omnibox/browser/verbatim_match.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "ios/chrome/browser/omnibox/model/autocomplete_controller_observer_bridge.h"
#import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller_debugger_delegate.h"
#import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller_delegate.h"
#import "ios/chrome/browser/omnibox/model/omnibox_metrics_recorder.h"
#import "ios/chrome/browser/omnibox/model/omnibox_text_controller.h"
#import "ios/chrome/browser/omnibox/model/omnibox_text_model.h"
#import "ios/chrome/browser/omnibox/model/suggestions/autocomplete_result_wrapper.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_backed_boolean.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "third_party/omnibox_proto/groups.pb.h"
#import "ui/gfx/image/image.h"
#import "url/gurl.h"
using base::UserMetricsAction;
@interface OmniboxAutocompleteController () <AutocompleteControllerObserver,
BooleanObserver>
/// Redefined as a readwrite
@property(nonatomic, assign, readwrite) BOOL hasSuggestions;
@end
@implementation OmniboxAutocompleteController {
/// Client of the omnibox.
raw_ptr<OmniboxClient> _omniboxClient;
/// Omnibox text model.
raw_ptr<OmniboxTextModel> _omniboxTextModel;
/// The autocomplete controller.
std::unique_ptr<AutocompleteController> _autocompleteController;
/// Autocomplete controller observer.
std::unique_ptr<AutocompleteControllerObserverBridge>
_autocompleteControllerObserverBridge;
/// Pref tracking if the bottom omnibox is enabled.
PrefBackedBoolean* _bottomOmniboxEnabled;
/// Preferred omnibox position, logged in omnibox logs.
metrics::OmniboxEventProto::OmniboxPosition _preferredOmniboxPosition;
}
- (instancetype)initWithOmniboxClient:(OmniboxClient*)omniboxClient
omniboxTextModel:(OmniboxTextModel*)omniboxTextModel {
self = [super init];
if (self) {
_omniboxClient = omniboxClient;
_omniboxTextModel = omniboxTextModel;
_autocompleteController = std::make_unique<AutocompleteController>(
_omniboxClient->CreateAutocompleteProviderClient(),
AutocompleteClassifier::DefaultOmniboxProviders());
_autocompleteControllerObserverBridge =
std::make_unique<AutocompleteControllerObserverBridge>(self);
_autocompleteController->AddObserver(
_autocompleteControllerObserverBridge.get());
_preferredOmniboxPosition = metrics::OmniboxEventProto::UNKNOWN_POSITION;
_bottomOmniboxEnabled = [[PrefBackedBoolean alloc]
initWithPrefService:GetApplicationContext()->GetLocalState()
prefName:prefs::kBottomOmnibox];
[_bottomOmniboxEnabled setObserver:self];
// Initialize to the correct value.
[self booleanDidChange:_bottomOmniboxEnabled];
}
return self;
}
- (void)disconnect {
if (_autocompleteControllerObserverBridge && _autocompleteController) {
_autocompleteController->RemoveObserver(
_autocompleteControllerObserverBridge.get());
_autocompleteControllerObserverBridge.reset();
}
[self.autocompleteResultWrapper disconnect];
[_bottomOmniboxEnabled stop];
[_bottomOmniboxEnabled setObserver:nil];
_bottomOmniboxEnabled = nil;
_autocompleteResultWrapper = nil;
_autocompleteController.reset();
_omniboxTextModel = nullptr;
_omniboxClient = nullptr;
}
- (AutocompleteController*)autocompleteController {
return _autocompleteController.get();
}
- (void)updatePopupSuggestions {
if (_autocompleteController) {
BOOL isFocusing = _autocompleteController->input().focus_type() ==
metrics::OmniboxFocusType::INTERACTION_FOCUS;
self.hasSuggestions = !_autocompleteController->result().empty();
[self.delegate
omniboxAutocompleteControllerDidUpdateSuggestions:self
hasSuggestions:self.hasSuggestions
isFocusing:isFocusing];
[self.debuggerDelegate omniboxAutocompleteController:self
didUpdateWithSuggestionsAvailable:self.hasSuggestions];
}
}
- (void)stopAutocompleteWithClearSuggestions:(BOOL)clearSuggestions {
if (_autocompleteController) {
TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::StopAutocomplete");
_autocompleteController->Stop(clearSuggestions
? AutocompleteStopReason::kClobbered
: AutocompleteStopReason::kInteraction);
}
}
- (void)openSelection:(OmniboxPopupSelection)selection
timestamp:(base::TimeTicks)timestamp
disposition:(WindowOpenDisposition)disposition {
if (!_autocompleteController) {
return;
}
// Intentionally accept input when selection has no line.
// This will usually reach `OpenMatch` indirectly.
if (selection.line >= _autocompleteController->result().size()) {
[self acceptInputWithDisposition:disposition timestamp:timestamp];
return;
}
const AutocompleteMatch& match =
_autocompleteController->result().match_at(selection.line);
// Open the match.
GURL alternate_nav_url = AutocompleteResult::ComputeAlternateNavUrl(
_omniboxTextModel->input, match,
self.autocompleteController->autocomplete_provider_client());
[self openMatch:match
popupSelection:selection
windowOpenDisposition:disposition
alternateNavURL:alternate_nav_url
pastedText:u""
matchSelectionTimestamp:timestamp];
}
- (void)openCurrentSelectionWithDisposition:(WindowOpenDisposition)disposition
timestamp:(base::TimeTicks)timestamp {
[self openSelection:OmniboxPopupSelection(OmniboxPopupSelection::kNoMatch,
OmniboxPopupSelection::NORMAL)
timestamp:timestamp
disposition:disposition];
}
#pragma mark - properties
- (AutocompleteProviderClient*)autocompleteProviderClient {
if (!_autocompleteController) {
return nullptr;
}
return _autocompleteController->autocomplete_provider_client();
}
#pragma mark - AutocompleteControllerObserver
- (void)autocompleteController:(AutocompleteController*)autocompleteController
didUpdateResultChangingDefaultMatch:(BOOL)defaultMatchChanged {
TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::OnResultChanged");
DCHECK(autocompleteController == _autocompleteController.get());
DCHECK(_omniboxClient);
const bool popup_was_open = self.hasSuggestions;
[self updatePopupSuggestions];
if (defaultMatchChanged) {
// The default match has changed, we need to let the text controller
// know about new inline autocomplete text (blue highlight).
if (const AutocompleteMatch* match =
_autocompleteController->result().default_match()) {
// onPopupDataChanged resets text model's `current_match_` early
// on. Therefore, copy match.inline_autocompletion to a temp to preserve
// its value across the entire call.
[self.omniboxTextController
onPopupDataChanged:match->inline_autocompletion
additionalText:match->additional_text
newMatch:*match];
} else {
[self.omniboxTextController onPopupDataChanged:std::u16string()
additionalText:std::u16string()
newMatch:AutocompleteMatch()];
}
}
const bool popup_is_open = self.hasSuggestions;
if (popup_was_open != popup_is_open && _omniboxClient) {
_omniboxClient->OnPopupVisibilityChanged(popup_is_open);
}
if (popup_was_open && !popup_is_open) {
// Closing the popup can change the default suggestion. This usually occurs
// when it's unclear whether the input represents a search or URL; e.g.,
// 'a.com/b c' or when title autocompleting. Clear the additional text to
// avoid suggesting the omnibox contains a URL suggestion when that may no
// longer be the case; i.e. when the default suggestion changed from a URL
// to a search suggestion upon closing the popup.
TRACE_EVENT0("omnibox",
"OmniboxAutocompleteController::ClearAdditionalText");
[self.omniboxTextController setAdditionalText:std::u16string()];
}
// Note: The client outlives `this`, so bind a weak pointer to the callback
// passed in to eliminate the potential for crashes on shutdown.
// `should_preload` is set to `controller->done()` as prerender may only want
// to start preloading a result after all Autocomplete results are ready.
if (_omniboxClient) {
_omniboxClient->OnResultChanged(
autocompleteController->result(), defaultMatchChanged,
/*should_preload=*/autocompleteController->done(),
/*on_bitmap_fetched=*/base::DoNothing());
}
}
#pragma mark - AutocompleteResultWrapperDelegate
- (void)autocompleteResultWrapper:(AutocompleteResultWrapper*)wrapper
didInvalidatePedals:(NSArray<id<AutocompleteSuggestionGroup>>*)
nonPedalSuggestionsGroups {
[self.delegate omniboxAutocompleteController:self
didUpdateSuggestionsGroups:nonPedalSuggestionsGroups];
}
#pragma mark - Boolean Observer
- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean {
if (observableBoolean == _bottomOmniboxEnabled) {
_preferredOmniboxPosition =
_bottomOmniboxEnabled.value
? metrics::OmniboxEventProto::BOTTOM_POSITION
: metrics::OmniboxEventProto::TOP_POSITION;
_autocompleteController->SetSteadyStateOmniboxPosition(
_preferredOmniboxPosition);
}
}
#pragma mark - OmniboxPopup event
- (void)requestSuggestionsWithVisibleSuggestionCount:
(NSUInteger)visibleSuggestionCount {
if (!_autocompleteController) {
return;
}
size_t resultSize = _autocompleteController->result().size();
// If no suggestions are visible, consider all of them visible.
if (visibleSuggestionCount == 0) {
visibleSuggestionCount = resultSize;
}
NSUInteger visibleSuggestions = MIN(visibleSuggestionCount, resultSize);
if (visibleSuggestions > 0) {
// Groups visible suggestions by search vs url. Skip the first suggestion
// because it's the omnibox content.
_autocompleteController->GroupSuggestionsBySearchVsURL(1,
visibleSuggestions);
}
// Groups hidden suggestions by search vs url.
if (visibleSuggestions < resultSize) {
_autocompleteController->GroupSuggestionsBySearchVsURL(visibleSuggestions,
resultSize);
}
[self updateWithSortedResults:_autocompleteController->result()];
}
- (void)selectMatchForOpening:(AutocompleteMatch&)match
withCustomDestinationURL:(GURL)destinationURL
inRow:(NSUInteger)row
openIn:(WindowOpenDisposition)disposition {
const auto matchSelectionTimestamp = base::TimeTicks();
match.destination_url = destinationURL;
[self openMatch:match
popupSelection:OmniboxPopupSelection(OmniboxPopupSelection(row))
windowOpenDisposition:disposition
alternateNavURL:GURL()
pastedText:u""
matchSelectionTimestamp:matchSelectionTimestamp];
}
- (void)selectMatchForOpening:(const AutocompleteMatch&)match
inRow:(NSUInteger)row
openIn:(WindowOpenDisposition)disposition {
const auto matchSelectionTimestamp = base::TimeTicks();
base::RecordAction(UserMetricsAction("MobileOmniboxUse"));
if (match.type == AutocompleteMatchType::CLIPBOARD_URL) {
base::RecordAction(UserMetricsAction("MobileOmniboxClipboardToURL"));
base::UmaHistogramLongTimes100(
"MobileOmnibox.PressedClipboardSuggestionAge",
ClipboardRecentContent::GetInstance()->GetClipboardContentAge());
}
if (!_autocompleteController) {
return;
}
// Sometimes the match provided does not correspond to the autocomplete
// result match specified by `index`. Most Visited Tiles, for example,
// provide ad hoc matches that are not in the result at all.
if (row >= _autocompleteController->result().size() ||
_autocompleteController->result().match_at(row).destination_url !=
match.destination_url) {
[self openCustomMatch:match
disposition:disposition
selectionTimestamp:matchSelectionTimestamp];
return;
}
// Clipboard match handling.
if (match.destination_url.is_empty() &&
AutocompleteMatch::IsClipboardType(match.type)) {
[self openClipboardMatch:match
disposition:disposition
selectionTimestamp:matchSelectionTimestamp];
return;
}
[self openSelection:OmniboxPopupSelection(row)
timestamp:matchSelectionTimestamp
disposition:disposition];
}
- (void)selectMatchForAppending:(const AutocompleteMatch&)match {
// Make a defensive copy of `match.fill_into_edit`, as CopyToOmnibox() will
// trigger a new round of autocomplete and modify `match`.
std::u16string fill_into_edit(match.fill_into_edit);
// If the match is not a URL, append a whitespace to the end of it.
if (AutocompleteMatch::IsSearchType(match.type)) {
fill_into_edit.append(1, ' ');
}
[self.omniboxTextController refineWithText:fill_into_edit];
}
- (void)selectMatchForDeletion:(const AutocompleteMatch&)match {
if (_autocompleteController) {
_autocompleteController->DeleteMatch(match);
}
}
- (void)onScroll {
[self.omniboxTextController onScroll];
}
- (void)onCallAction {
[self.omniboxTextController hideKeyboard];
}
#pragma mark - OmniboxText events
- (void)startAutocompleteWithText:(const std::u16string&)text
cursorPosition:(size_t)cursorPosition
preventInlineAutocomplete:(bool)preventInlineAutocomplete {
if (!_omniboxClient || !_omniboxTextModel) {
return;
}
// Use text_model()->input during the refactoring while the edit model is
// still using it crbug.com/390409559.
_omniboxTextModel->input = AutocompleteInput(
text, cursorPosition,
_omniboxClient->GetPageClassification(/*is_prefetch=*/false),
_omniboxClient->GetSchemeClassifier(),
_omniboxClient->ShouldDefaultTypedNavigationsToHttps(),
_omniboxClient->GetHttpsPortForTesting(),
_omniboxClient->IsUsingFakeHttpsForHttpsUpgradeTesting());
AutocompleteInput& input = _omniboxTextModel->input;
input.set_current_url(_omniboxClient->GetURL());
input.set_current_title(_omniboxClient->GetTitle());
input.set_prevent_inline_autocomplete(preventInlineAutocomplete);
if (std::optional<lens::proto::LensOverlaySuggestInputs> suggestInputs =
_omniboxClient->GetLensOverlaySuggestInputs()) {
input.set_lens_overlay_suggest_inputs(*suggestInputs);
}
[self startAutocompleteWithInput:input];
}
- (void)startZeroSuggestRequestWithText:(const std::u16string&)text
userClobbered:(BOOL)userClobberedPermanentText {
if (!_autocompleteController || !_omniboxClient || !_omniboxTextModel) {
return;
}
// Early exit if a query is already in progress or the popup is already open.
// This is what allows this method to be called multiple times in multiple
// code locations without harm.
if (!_autocompleteController->done() || self.hasSuggestions) {
return;
}
// Early exit if the page has not loaded yet, so we don't annoy users.
if (!_omniboxClient->CurrentPageExists()) {
return;
}
// Early exit if the user already has a navigation or search query in mind.
if (_omniboxTextModel->user_input_in_progress &&
!userClobberedPermanentText) {
return;
}
TRACE_EVENT0("omnibox",
"OmniboxTextController::startZeroSuggestRequestWithClobber");
// Send the textfield contents exactly as-is, as otherwise the verbatim
// match can be wrong. The full page URL is anyways in set_current_url().
// Don't attempt to use https as the default scheme for these requests.
_omniboxTextModel->input = AutocompleteInput(
text, _omniboxClient->GetPageClassification(/*is_prefetch=*/false),
_omniboxClient->GetSchemeClassifier(),
/*should_use_https_as_default_scheme=*/false,
_omniboxClient->GetHttpsPortForTesting(),
_omniboxClient->IsUsingFakeHttpsForHttpsUpgradeTesting());
AutocompleteInput& input = _omniboxTextModel->input;
input.set_current_url(_omniboxClient->GetURL());
input.set_current_title(_omniboxClient->GetTitle());
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
// Set the lens overlay suggest inputs, if available.
if (std::optional<lens::proto::LensOverlaySuggestInputs> suggestInputs =
_omniboxClient->GetLensOverlaySuggestInputs()) {
input.set_lens_overlay_suggest_inputs(*suggestInputs);
}
[self startAutocompleteWithInput:input];
}
- (void)resetSession {
if (_autocompleteController) {
_autocompleteController->ResetSession();
}
}
- (BOOL)findMatchForInput:(const AutocompleteInput&)input
match:(AutocompleteMatch*)match
alternateNavigationURL:(GURL*)alternateNavigationURL {
// If there's a query in progress or the popup is open, pick out the default
// match or selected match, if there is one.
BOOL foundMatch = NO;
if (_autocompleteController &&
(!_autocompleteController->done() || self.hasSuggestions)) {
if (!_autocompleteController->done() &&
_autocompleteController->result().default_match()) {
// The user cannot have manually selected a match, or the query would have
// stopped. So the default match must be the desired selection.
*match = *_autocompleteController->result().default_match();
foundMatch = YES;
if (alternateNavigationURL) {
*alternateNavigationURL = [self computeAlternateNavURLForInput:input
match:*match];
}
}
}
return foundMatch;
}
- (GURL)computeAlternateNavURLForInput:(const AutocompleteInput&)input
match:(const AutocompleteMatch&)match {
if (!_autocompleteController) {
return GURL();
}
AutocompleteProviderClient* providerClient =
_autocompleteController->autocomplete_provider_client();
return AutocompleteResult::ComputeAlternateNavUrl(input, match,
providerClient);
}
- (void)closeOmniboxPopup {
[self stopAutocompleteWithClearSuggestions:YES];
}
- (void)setTextAlignment:(NSTextAlignment)alignment {
[self.delegate omniboxAutocompleteController:self
didUpdateTextAlignment:alignment];
}
- (void)setSemanticContentAttribute:
(UISemanticContentAttribute)semanticContentAttribute {
[self.delegate omniboxAutocompleteController:self
didUpdateSemanticContentAttribute:semanticContentAttribute];
}
- (void)setHasThumbnail:(BOOL)hasThumbnail {
self.autocompleteResultWrapper.hasThumbnail = hasThumbnail;
}
- (void)previewSuggestion:(id<AutocompleteSuggestion>)suggestion
isFirstUpdate:(BOOL)isFirstUpdate {
[self.omniboxTextController previewSuggestion:suggestion
isFirstUpdate:isFirstUpdate];
}
- (const AutocompleteResult*)autocompleteResult {
return _autocompleteController ? &_autocompleteController->result() : nullptr;
}
#pragma mark - Prefetch events
- (void)startZeroSuggestPrefetch {
TRACE_EVENT0("omnibox",
"OmniboxAutocompleteController::StartZeroSuggestPrefetch");
if (!_autocompleteController || !_omniboxClient) {
return;
}
auto page_classification =
_omniboxClient->GetPageClassification(/*is_prefetch=*/true);
GURL currentURL = _omniboxClient->GetURL();
std::u16string text = base::UTF8ToUTF16(currentURL.spec());
if (omnibox::IsNTPPage(page_classification)) {
text.clear();
}
AutocompleteInput input(text, page_classification,
_omniboxClient->GetSchemeClassifier());
input.set_current_url(currentURL);
input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);
_autocompleteController->StartPrefetch(input);
}
- (void)setBackgroundStateForProviders:(BOOL)inBackground {
if (_autocompleteController) {
_autocompleteController->autocomplete_provider_client()
->set_in_background_state(inBackground);
}
}
#pragma mark - Private
/// Wraps the suggestions and send them to the delegate.
- (void)updateWithSortedResults:(const AutocompleteResult&)results {
NSArray<id<AutocompleteSuggestionGroup>>* suggestionGroups =
[self.autocompleteResultWrapper wrapAutocompleteResultInGroups:results];
[self.delegate omniboxAutocompleteController:self
didUpdateSuggestionsGroups:suggestionGroups];
}
/// Starts autocomplete with `input`.
- (void)startAutocompleteWithInput:(const AutocompleteInput&)input {
if (_autocompleteController) {
TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::StartAutocomplete");
_autocompleteController->Start(input);
}
}
#pragma mark Open match
/// Opens a match created outside of autocomplete controller.
- (void)openCustomMatch:(std::optional<AutocompleteMatch>)match
disposition:(WindowOpenDisposition)disposition
selectionTimestamp:(base::TimeTicks)timestamp {
AutocompleteController* autocompleteController = self.autocompleteController;
if (!autocompleteController || !match) {
return;
}
OmniboxPopupSelection selection(
autocompleteController->InjectAdHocMatch(match.value()));
[self openSelection:selection timestamp:timestamp disposition:disposition];
}
/// Asks the browser to load the popup's currently selected item, using the
/// supplied disposition. This may close the popup.
- (void)acceptInputWithDisposition:(WindowOpenDisposition)disposition
timestamp:(base::TimeTicks)timestamp {
// Get the URL and transition type for the selected entry.
GURL alternate_nav_url;
AutocompleteMatch match =
[self.omniboxTextController currentMatch:&alternate_nav_url];
if (!match.destination_url.is_valid()) {
return;
}
if (_omniboxTextModel->paste_state != OmniboxPasteState::kNone &&
match.type == AutocompleteMatchType::URL_WHAT_YOU_TYPED) {
// When the user pasted in a URL and hit enter, score it like a link click
// rather than a normal typed URL, so it doesn't get inline autocompleted
// as aggressively later.
match.transition = ui::PAGE_TRANSITION_LINK;
}
[self openMatch:match
popupSelection:OmniboxPopupSelection(
OmniboxPopupSelection::kNoMatch)
windowOpenDisposition:disposition
alternateNavURL:alternate_nav_url
pastedText:u""
matchSelectionTimestamp:timestamp];
}
/// Asks the browser to load `match` or execute one of its actions
/// according to `selection`.
///
/// openMatch: needs to know the original text that drove this action. If
/// `pastedText` is non-empty, this is a Paste-And-Go/Search action, and
/// that's the relevant input text. Otherwise, the relevant input text is
/// either the user text or the display URL, depending on if user input is
/// in progress.
///
/// `match` is passed by value for two reasons:
/// (1) This function needs to modify `match`, so a const ref isn't
/// appropriate. Callers don't actually care about the modifications, so a
/// pointer isn't required.
/// (2) The passed-in match is, on the caller side, typically coming from data
/// associated with the popup. Since this call can close the popup, that
/// could clear that data, leaving us with a pointer-to-garbage. So at
/// some point someone needs to make a copy of the match anyway, to
/// preserve it past the popup closure.
- (void)openMatch:(AutocompleteMatch)match
popupSelection:(OmniboxPopupSelection)selection
windowOpenDisposition:(WindowOpenDisposition)disposition
alternateNavURL:(const GURL&)alternateNavURL
pastedText:(const std::u16string&)pastedText
matchSelectionTimestamp:(base::TimeTicks)matchSelectionTimestamp {
// If the user is executing an action, this will be non-null and some match
// opening and metrics behavior will be adjusted accordingly.
OmniboxAction* action = nullptr;
if (selection.state == OmniboxPopupSelection::NORMAL &&
match.takeover_action) {
DCHECK(matchSelectionTimestamp != base::TimeTicks());
action = match.takeover_action.get();
} else if (selection.IsAction()) {
DCHECK_LT(selection.action_index, match.actions.size());
action = match.actions[selection.action_index].get();
}
// Invalid URLs such as chrome://history can end up here, but that's okay
// if the user is executing an action instead of navigating to the URL.
if (!match.destination_url.is_valid() && !action) {
return;
}
// NULL_RESULT_MESSAGE matches are informational only and cannot be acted
// upon. Immediately return when attempting to open one.
if (match.type == AutocompleteMatchType::NULL_RESULT_MESSAGE) {
return;
}
// Also switch the window disposition for tab switch actions. The action
// itself will already open with SWITCH_TO_TAB disposition, but the change
// is needed earlier for metrics.
bool isTabSwitchAction =
action && action->ActionId() == OmniboxActionId::TAB_SWITCH;
if (isTabSwitchAction) {
disposition = WindowOpenDisposition::SWITCH_TO_TAB;
}
TRACE_EVENT("omnibox", "OmniboxAutocompleteController::OpenMatch", "match",
match, "disposition", disposition, "altenate_nav_url",
alternateNavURL, "pasted_text", pastedText);
GURL destinationURL = action ? action->getUrl() : match.destination_url;
std::u16string inputText(pastedText);
if (inputText.empty()) {
inputText = _omniboxTextModel->user_input_in_progress
? _omniboxTextModel->user_text
: _omniboxTextModel->url_for_editing;
}
// Save the result of the interaction, but do not record the histogram yet.
_omniboxTextModel->focus_resulted_in_navigation = true;
// Create a dummy AutocompleteInput for use in calling VerbatimMatchForInput()
// to create an alternate navigational match.
AutocompleteInput alternateInput(
inputText, _omniboxClient->GetPageClassification(/*is_prefetch=*/false),
_omniboxClient->GetSchemeClassifier(),
_omniboxClient->ShouldDefaultTypedNavigationsToHttps(), 0, false);
// Somehow we can occasionally get here with no active tab. It's not
// clear why this happens.
alternateInput.set_current_url(_omniboxClient->GetURL());
alternateInput.set_current_title(_omniboxClient->GetTitle());
[self.omniboxMetricsRecorder recordOpenMatch:match
destinationURL:destinationURL
inputText:inputText
popupSelection:selection
windowOpenDisposition:disposition
isAction:action
isPastedText:!pastedText.empty()];
if (action) {
OmniboxAction::ExecutionContext context(
*(self.autocompleteController->autocomplete_provider_client()),
base::BindOnce(&OmniboxClient::OnAutocompleteAccept,
_omniboxClient->AsWeakPtr()),
matchSelectionTimestamp, disposition);
action->Execute(context);
}
if (disposition != WindowOpenDisposition::NEW_BACKGROUND_TAB) {
base::AutoReset<bool> tmp(&_omniboxTextModel->in_revert, true);
[self.omniboxTextController
revertAll]; // Revert the box to its unedited state.
}
if (!action) {
// This block should be the last call in openMatch, because controller_ is
// not guaranteed to exist after client()->OnAutocompleteAccept is called.
if (destinationURL.is_valid()) {
// This calls RevertAll again.
base::AutoReset<bool> tmp(&_omniboxTextModel->in_revert, true);
_omniboxClient->OnAutocompleteAccept(
destinationURL, match.post_content.get(), disposition,
ui::PageTransitionFromInt(match.transition |
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
match.type, matchSelectionTimestamp,
_omniboxTextModel->input.added_default_scheme_to_typed_url(),
_omniboxTextModel->input.typed_url_had_http_scheme() &&
match.type == AutocompleteMatchType::URL_WHAT_YOU_TYPED,
inputText, match,
VerbatimMatchForInput(
self.autocompleteController->history_url_provider(),
self.autocompleteController->autocomplete_provider_client(),
alternateInput, alternateNavURL, false));
}
}
}
#pragma mark Clipboard match handling
/// Creates a match with the clipboard URL and open it.
- (void)openClipboardURL:(std::optional<GURL>)optionalURL
disposition:(WindowOpenDisposition)disposition
timestamp:(base::TimeTicks)timestamp {
if (!optionalURL || !_autocompleteController) {
return;
}
GURL URL = std::move(optionalURL).value();
[self openCustomMatch:_autocompleteController->clipboard_provider()
->NewClipboardURLMatch(URL)
disposition:disposition
selectionTimestamp:timestamp];
}
/// Creates a match with the clipboard text and open it.
- (void)openClipboardText:(std::optional<std::u16string>)optionalText
disposition:(WindowOpenDisposition)disposition
timestamp:(base::TimeTicks)timestamp {
if (!optionalText || !_autocompleteController) {
return;
}
[self openCustomMatch:_autocompleteController->clipboard_provider()
->NewClipboardTextMatch(optionalText.value())
disposition:disposition
selectionTimestamp:timestamp];
}
/// Creates a match with the clipboard image and open it.
- (void)openClipboardImage:(std::optional<gfx::Image>)optionalImage
disposition:(WindowOpenDisposition)disposition
timestamp:(base::TimeTicks)timestamp {
if (!optionalImage || !_autocompleteController) {
return;
}
__weak __typeof(self) weakSelf = self;
_autocompleteController->clipboard_provider()->NewClipboardImageMatch(
optionalImage,
base::BindOnce(
[](OmniboxAutocompleteController* controller,
WindowOpenDisposition disposition, base::TimeTicks timestamp,
std::optional<AutocompleteMatch> optionalMatch) {
[controller openCustomMatch:optionalMatch
disposition:disposition
selectionTimestamp:timestamp];
},
weakSelf, disposition, timestamp));
}
/// Opens a clipboard match. Fetches the content of the clipboard and creates a
/// new match with it.
- (void)openClipboardMatch:(const AutocompleteMatch&)match
disposition:(WindowOpenDisposition)disposition
selectionTimestamp:(base::TimeTicks)timestamp {
__weak __typeof__(self) weakSelf = self;
ClipboardRecentContent* clipboardRecentContent =
ClipboardRecentContent::GetInstance();
CHECK(clipboardRecentContent);
switch (match.type) {
case AutocompleteMatchType::CLIPBOARD_URL: {
clipboardRecentContent->GetRecentURLFromClipboard(base::BindOnce(
[](OmniboxAutocompleteController* controller,
WindowOpenDisposition disposition, base::TimeTicks timestamp,
std::optional<GURL> optionalURL) {
[controller openClipboardURL:optionalURL
disposition:disposition
timestamp:timestamp];
},
weakSelf, disposition, timestamp));
break;
}
case AutocompleteMatchType::CLIPBOARD_TEXT: {
clipboardRecentContent->GetRecentTextFromClipboard(base::BindOnce(
[](OmniboxAutocompleteController* controller,
WindowOpenDisposition disposition, base::TimeTicks timestamp,
std::optional<std::u16string> optionalText) {
[controller openClipboardText:optionalText
disposition:disposition
timestamp:timestamp];
},
weakSelf, disposition, timestamp));
break;
}
case AutocompleteMatchType::CLIPBOARD_IMAGE: {
clipboardRecentContent->GetRecentImageFromClipboard(base::BindOnce(
[](OmniboxAutocompleteController* controller,
WindowOpenDisposition disposition, base::TimeTicks timestamp,
std::optional<gfx::Image> optionalImage) {
[controller openClipboardImage:optionalImage
disposition:disposition
timestamp:timestamp];
},
weakSelf, disposition, timestamp));
break;
}
default:
NOTREACHED() << "Unsupported clipboard match type";
}
}
#pragma mark - Testing
- (void)setAutocompleteController:
(std::unique_ptr<AutocompleteController>)controller {
CHECK(_autocompleteControllerObserverBridge);
// Remove observation on old controller.
if (_autocompleteController) {
_autocompleteController->RemoveObserver(
_autocompleteControllerObserverBridge.get());
}
// Set new controller.
_autocompleteController = std::move(controller);
// Observe new controller.
if (_autocompleteController) {
_autocompleteController->AddObserver(
_autocompleteControllerObserverBridge.get());
// Update the autocomplete controller in the metrics recorder and the text
// controller.
[self.omniboxMetricsRecorder
setAutocompleteController:_autocompleteController.get()];
}
}
@end