| // Copyright 2019 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/ash/input_method/assistive_suggester.h" |
| #include <string> |
| |
| #include "ash/clipboard/clipboard_history_controller_impl.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/shell.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/hash/hash.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/strings/string_util.h" |
| #include "chrome/browser/ash/input_method/assistive_prefs.h" |
| #include "chrome/browser/ash/input_method/assistive_suggester_switch.h" |
| #include "chrome/browser/ash/input_method/suggestion_handler_interface.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/ash/services/ime/public/cpp/assistive_suggestions.h" |
| #include "components/exo/wm_helper.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "ui/base/ime/ash/ime_bridge.h" |
| #include "ui/base/ime/ash/input_method_ukm.h" |
| #include "ui/base/ime/ash/text_input_target.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/keycodes/keyboard_codes_posix.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "url/gurl.h" |
| |
| namespace ash::input_method { |
| |
| namespace { |
| |
| using ime::AssistiveSuggestion; |
| using ime::AssistiveSuggestionMode; |
| using ime::AssistiveSuggestionType; |
| using ime::SuggestionsTextContext; |
| |
| constexpr int kModifierKeysMask = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN | |
| ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN | |
| ui::EF_FUNCTION_DOWN | ui::EF_ALTGR_DOWN; |
| |
| const char kMaxTextBeforeCursorLength = 50; |
| |
| constexpr base::TimeDelta kLongpressActivationDelay = base::Milliseconds(500); |
| |
| // TODO(b/217560706): Make this different based on current engine after research |
| // is conducted. |
| constexpr auto kDefaultLongpressEnabledKeys = base::MakeFixedFlatSet<char>( |
| {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', |
| 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', |
| 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', |
| 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}); |
| |
| void RecordAssistiveMatch(AssistiveType type) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Match", type); |
| |
| TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler(); |
| if (!input_context) |
| return; |
| |
| auto sourceId = input_context->GetClientSourceForMetrics(); |
| if (sourceId != ukm::kInvalidSourceId) { |
| RecordUkmAssistiveMatch(sourceId, static_cast<int>(type)); |
| } |
| } |
| |
| void RecordAssistiveDisabled(AssistiveType type) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled", type); |
| } |
| |
| void RecordAssistiveDisabledReasonForEmoji(DisabledReason reason) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled.Emoji", reason); |
| } |
| |
| void RecordAssistiveDisabledReasonForMultiWord(DisabledReason reason) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled.MultiWord", |
| reason); |
| } |
| |
| void RecordAssistiveUserPrefForEmoji(bool value) { |
| base::UmaHistogramBoolean("InputMethod.Assistive.UserPref.Emoji", value); |
| } |
| |
| void RecordAssistiveUserPrefForMultiWord(bool value) { |
| base::UmaHistogramBoolean("InputMethod.Assistive.UserPref.MultiWord", value); |
| } |
| |
| void RecordAssistiveUserPrefForDiacriticsOnLongpress(bool value) { |
| base::UmaHistogramBoolean( |
| "InputMethod.Assistive.UserPref.PhysicalKeyboardDiacriticsOnLongpress", |
| value); |
| } |
| |
| void RecordAssistiveNotAllowed(AssistiveType type) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.NotAllowed", type); |
| } |
| |
| void RecordAssistiveCoverage(AssistiveType type) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Coverage", type); |
| } |
| |
| void RecordAssistiveSuccess(AssistiveType type) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Success", type); |
| } |
| |
| void RecordLongPressDiacriticAutoRepeatSuppressedMetric() { |
| base::UmaHistogramEnumeration( |
| "InputMethod.PhysicalKeyboard.LongpressDiacritics.Action", |
| IMEPKLongpressDiacriticAction::kAutoRepeatSuppressed); |
| } |
| |
| bool IsTopResultMultiWord(const std::vector<AssistiveSuggestion>& suggestions) { |
| if (suggestions.empty()) |
| return false; |
| // There should only ever be one multi word suggestion given if any. |
| return suggestions[0].type == AssistiveSuggestionType::kMultiWord; |
| } |
| |
| void RecordSuggestionsMatch( |
| const std::vector<AssistiveSuggestion>& suggestions) { |
| if (suggestions.empty()) |
| return; |
| |
| auto top_result = suggestions[0]; |
| if (top_result.type != AssistiveSuggestionType::kMultiWord) |
| return; |
| |
| switch (top_result.mode) { |
| case AssistiveSuggestionMode::kCompletion: |
| RecordAssistiveMatch(AssistiveType::kMultiWordCompletion); |
| return; |
| case AssistiveSuggestionMode::kPrediction: |
| RecordAssistiveMatch(AssistiveType::kMultiWordPrediction); |
| return; |
| } |
| } |
| |
| bool IsUsEnglishEngine(const std::string& engine_id) { |
| return engine_id == "xkb:us::eng"; |
| } |
| |
| void RecordTextInputStateMetric(AssistiveTextInputState state) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.MultiWord.InputState", |
| state); |
| } |
| |
| // Returns whether Ctrl+V is pressed with Ctrl+V long-press behavior enabled. |
| bool IsLongpressEnabledControlV(const ui::KeyEvent& event) { |
| if (!features::IsClipboardHistoryLongpressEnabled()) { |
| return false; |
| } |
| |
| return event.key_code() == ui::VKEY_V && |
| (event.flags() & kModifierKeysMask) == ui::EF_CONTROL_DOWN; |
| } |
| |
| // Returns the location to which the clipboard history menu should anchor. When |
| // possible, this anchor is where a clipboard history item would be pasted if |
| // the user made a selection; otherwise, this function returns a point at (0,0). |
| gfx::Rect GetClipboardHistoryMenuAnchor() { |
| TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler(); |
| if (!input_context) { |
| return gfx::Rect(); |
| } |
| |
| ui::TextInputClient* input_client = |
| input_context->GetInputMethod()->GetTextInputClient(); |
| if (!input_client) { |
| return gfx::Rect(); |
| } |
| |
| return input_client->GetCaretBounds(); |
| } |
| |
| void RecordMultiWordTextInputState( |
| PrefService* pref_service, |
| const std::string& engine_id, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (!enabled_suggestions.multi_word_suggestions) { |
| RecordTextInputStateMetric( |
| AssistiveTextInputState::kFeatureBlockedByDenylist); |
| return; |
| } |
| |
| if (!IsUsEnglishEngine(engine_id)) { |
| RecordTextInputStateMetric(AssistiveTextInputState::kUnsupportedLanguage); |
| return; |
| } |
| |
| if (!IsPredictiveWritingPrefEnabled(pref_service, engine_id)) { |
| RecordTextInputStateMetric( |
| AssistiveTextInputState::kFeatureBlockedByPreference); |
| return; |
| } |
| |
| RecordTextInputStateMetric(AssistiveTextInputState::kFeatureEnabled); |
| } |
| |
| } // namespace |
| |
| AssistiveSuggester::AssistiveSuggester( |
| SuggestionHandlerInterface* suggestion_handler, |
| Profile* profile, |
| std::unique_ptr<AssistiveSuggesterSwitch> suggester_switch) |
| : profile_(profile), |
| emoji_suggester_(suggestion_handler, profile), |
| multi_word_suggester_(suggestion_handler, profile), |
| longpress_diacritics_suggester_(suggestion_handler), |
| longpress_control_v_suggester_(suggestion_handler), |
| suggester_switch_(std::move(suggester_switch)), |
| context_(TextInputMethod::InputContext(ui::TEXT_INPUT_TYPE_NONE)) { |
| RecordAssistiveUserPrefForEmoji( |
| profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled)); |
| } |
| |
| AssistiveSuggester::~AssistiveSuggester() = default; |
| |
| bool AssistiveSuggester::IsAssistiveFeatureEnabled() { |
| return IsEmojiSuggestAdditionEnabled() || IsMultiWordSuggestEnabled() || |
| IsEnhancedEmojiSuggestEnabled() || |
| IsDiacriticsOnPhysicalKeyboardLongpressEnabled() || |
| features::IsClipboardHistoryLongpressEnabled(); |
| } |
| |
| void AssistiveSuggester::FetchEnabledSuggestionsFromBrowserContextThen( |
| AssistiveSuggesterSwitch::FetchEnabledSuggestionsCallback callback) { |
| suggester_switch_->FetchEnabledSuggestionsThen(std::move(callback), context_); |
| } |
| |
| bool AssistiveSuggester::IsEmojiSuggestAdditionEnabled() { |
| return profile_->GetPrefs()->GetBoolean( |
| prefs::kEmojiSuggestionEnterpriseAllowed) && |
| profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled); |
| } |
| |
| bool AssistiveSuggester::IsEnhancedEmojiSuggestEnabled() { |
| return IsEmojiSuggestAdditionEnabled() && |
| base::FeatureList::IsEnabled(features::kAssistEmojiEnhanced); |
| } |
| |
| bool AssistiveSuggester::IsMultiWordSuggestEnabled() { |
| return base::FeatureList::IsEnabled(features::kAssistMultiWord) && |
| IsPredictiveWritingPrefEnabled(profile_->GetPrefs(), |
| active_engine_id_); |
| } |
| |
| bool AssistiveSuggester::IsExpandedMultiWordSuggestEnabled() { |
| return IsMultiWordSuggestEnabled() && |
| base::FeatureList::IsEnabled(features::kAssistMultiWordExpanded); |
| } |
| |
| bool AssistiveSuggester::IsDiacriticsOnPhysicalKeyboardLongpressEnabled() { |
| return base::FeatureList::IsEnabled( |
| features::kDiacriticsOnPhysicalKeyboardLongpress) && |
| IsUsEnglishEngine(active_engine_id_) && |
| IsDiacriticsOnLongpressPrefEnabled(profile_->GetPrefs(), |
| active_engine_id_); |
| } |
| |
| DisabledReason AssistiveSuggester::GetDisabledReasonForEmoji( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (!profile_->GetPrefs()->GetBoolean( |
| prefs::kEmojiSuggestionEnterpriseAllowed)) { |
| return DisabledReason::kEnterpriseSettingsOff; |
| } |
| if (!profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled)) { |
| return DisabledReason::kUserSettingsOff; |
| } |
| if (!enabled_suggestions.emoji_suggestions) { |
| return DisabledReason::kUrlOrAppNotAllowed; |
| } |
| return DisabledReason::kNone; |
| } |
| |
| DisabledReason AssistiveSuggester::GetDisabledReasonForMultiWord( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (!base::FeatureList::IsEnabled(features::kAssistMultiWord)) { |
| return DisabledReason::kFeatureFlagOff; |
| } |
| if (!profile_->GetPrefs()->GetBoolean( |
| prefs::kAssistPredictiveWritingEnabled)) { |
| return DisabledReason::kUserSettingsOff; |
| } |
| if (!enabled_suggestions.multi_word_suggestions) { |
| return DisabledReason::kUrlOrAppNotAllowed; |
| } |
| return DisabledReason::kNone; |
| } |
| |
| AssistiveSuggester::AssistiveFeature |
| AssistiveSuggester::GetAssistiveFeatureForType(AssistiveType type) { |
| switch (type) { |
| case AssistiveType::kEmoji: |
| return AssistiveFeature::kEmojiSuggestion; |
| case AssistiveType::kMultiWordCompletion: |
| case AssistiveType::kMultiWordPrediction: |
| return AssistiveFeature::kMultiWordSuggestion; |
| default: |
| // We should only handle Emoji and Multiword related assistive types. |
| // |
| // Any assistive types outside of this should not be processed in this |
| // class, hence we shall DCHECK here if that ever occurs. |
| LOG(DFATAL) << "Unexpected AssistiveType value: " |
| << static_cast<int>(type); |
| return AssistiveFeature::kUnknown; |
| } |
| } |
| |
| bool AssistiveSuggester::IsAssistiveTypeEnabled(AssistiveType type) { |
| switch (GetAssistiveFeatureForType(type)) { |
| case AssistiveFeature::kEmojiSuggestion: |
| return IsEmojiSuggestAdditionEnabled(); |
| case AssistiveFeature::kMultiWordSuggestion: |
| return IsMultiWordSuggestEnabled(); |
| default: |
| LOG(DFATAL) << "Unexpected AssistiveType value: " |
| << static_cast<int>(type); |
| return false; |
| } |
| } |
| |
| bool AssistiveSuggester::IsAssistiveTypeAllowedInBrowserContext( |
| AssistiveType type, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| switch (GetAssistiveFeatureForType(type)) { |
| case AssistiveFeature::kEmojiSuggestion: |
| return enabled_suggestions.emoji_suggestions; |
| case AssistiveFeature::kMultiWordSuggestion: |
| return enabled_suggestions.multi_word_suggestions; |
| default: |
| LOG(DFATAL) << "Unexpected AssistiveType value: " |
| << static_cast<int>(type); |
| return false; |
| } |
| } |
| |
| void AssistiveSuggester::OnFocus(int context_id, |
| const TextInputMethod::InputContext& context) { |
| // Some parts of the code reserve negative/zero context_id for unfocused |
| // context. As a result we should make sure it is not being errornously set to |
| // a negative number, and cause unexpected behaviour. |
| context_ = context; |
| DCHECK(context_id > 0); |
| focused_context_id_ = context_id; |
| emoji_suggester_.OnFocus(context_id); |
| multi_word_suggester_.OnFocus(context_id); |
| longpress_diacritics_suggester_.OnFocus(context_id); |
| longpress_control_v_suggester_.OnFocus(context_id); |
| enabled_suggestions_from_last_onfocus_ = std::nullopt; |
| suggester_switch_->FetchEnabledSuggestionsThen( |
| base::BindOnce(&AssistiveSuggester::HandleEnabledSuggestionsOnFocus, |
| weak_ptr_factory_.GetWeakPtr()), |
| context); |
| } |
| |
| void AssistiveSuggester::HandleEnabledSuggestionsOnFocus( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| enabled_suggestions_from_last_onfocus_ = enabled_suggestions; |
| AssistiveSuggester::RecordTextInputStateMetrics(enabled_suggestions); |
| } |
| |
| void AssistiveSuggester::OnBlur() { |
| focused_context_id_ = std::nullopt; |
| enabled_suggestions_from_last_onfocus_ = std::nullopt; |
| emoji_suggester_.OnBlur(); |
| multi_word_suggester_.OnBlur(); |
| longpress_diacritics_suggester_.OnBlur(); |
| longpress_control_v_suggester_.OnBlur(); |
| } |
| |
| AssistiveSuggesterKeyResult AssistiveSuggester::OnKeyEvent( |
| const ui::KeyEvent& event) { |
| if (!focused_context_id_.has_value()) |
| return AssistiveSuggesterKeyResult::kNotHandled; |
| |
| // Auto repeat resets whenever a key is pressed/released as long as its not a |
| // repeat event. |
| if (!event.is_repeat()) { |
| auto_repeat_suppress_metric_emitted_ = false; |
| } |
| |
| // We only track keydown event because the suggesting action is triggered by |
| // surrounding text change, which is triggered by a keydown event. As a |
| // result, the next key event after suggesting would be a keyup event of the |
| // same key, and that event is meaningless to us. |
| if (IsSuggestionShown() && event.type() == ui::ET_KEY_PRESSED && |
| !event.IsControlDown() && !event.IsAltDown() && !event.IsShiftDown()) { |
| SuggestionStatus status = current_suggester_->HandleKeyEvent(event); |
| switch (status) { |
| case SuggestionStatus::kAccept: |
| // Handle a race condition where the current suggester_ is set to |
| // nullptr by a simultaneous event (such as a key event causing a |
| // onBlur() event). |
| // TODO(b/240534923): Figure out how to record metrics when |
| // current_suggester_ is set to nullptr prematurely by a different |
| // event. |
| if (current_suggester_) { |
| RecordAssistiveSuccess(current_suggester_->GetProposeActionType()); |
| } |
| current_suggester_ = nullptr; |
| return AssistiveSuggesterKeyResult::kHandled; |
| case SuggestionStatus::kDismiss: |
| current_suggester_ = nullptr; |
| return AssistiveSuggesterKeyResult::kHandled; |
| case SuggestionStatus::kBrowsing: |
| return AssistiveSuggesterKeyResult::kHandled; |
| default: |
| break; |
| } |
| } |
| |
| return AssistiveSuggester::HandleLongpressEnabledKeyEvent(event); |
| } |
| |
| AssistiveSuggesterKeyResult AssistiveSuggester::HandleLongpressEnabledKeyEvent( |
| const ui::KeyEvent& event) { |
| const bool is_enabled_diacritic_long_press = |
| IsDiacriticsOnPhysicalKeyboardLongpressEnabled() && |
| enabled_suggestions_from_last_onfocus_ && |
| enabled_suggestions_from_last_onfocus_->diacritic_suggestions && |
| kDefaultLongpressEnabledKeys.contains(event.GetCharacter()); |
| if (!is_enabled_diacritic_long_press && !IsLongpressEnabledControlV(event)) { |
| return AssistiveSuggesterKeyResult::kNotHandled; |
| } |
| |
| // Longpress diacritics behaviour overrides the longpress to repeat key |
| // behaviour for alphabetical keys. |
| if (event.is_repeat()) { |
| // Check for cases where auto-repeat behavior is suppressed for characters |
| // with no available diacritic suggestion. Only emit the metric if |
| // `auto_repeat_suppress_metric_emitted_` is false as the metric should only |
| // be emitted once per Press->Release cycle. |
| if (!auto_repeat_suppress_metric_emitted_ && |
| !longpress_diacritics_suggester_.HasDiacriticSuggestions( |
| event.GetCharacter()) && |
| !IsLongpressEnabledControlV(event)) { |
| auto_repeat_suppress_metric_emitted_ = true; |
| RecordLongPressDiacriticAutoRepeatSuppressedMetric(); |
| } |
| return AssistiveSuggesterKeyResult::kHandled; |
| } |
| |
| // Process longpress keydown event. |
| if (current_longpress_keydown_ == std::nullopt && |
| event.type() == ui::EventType::ET_KEY_PRESSED) { |
| current_longpress_keydown_ = event; |
| |
| if (IsLongpressEnabledControlV(event)) { |
| longpress_control_v_suggester_.CachePastedTextStart(); |
| } |
| |
| longpress_timer_.Start( |
| FROM_HERE, kLongpressActivationDelay, |
| base::BindOnce(&AssistiveSuggester::OnLongpressDetected, |
| weak_ptr_factory_.GetWeakPtr())); |
| return AssistiveSuggesterKeyResult::kNotHandledSuppressAutoRepeat; |
| } |
| |
| // Process longpress interrupted event (key press up before timer callback |
| // fired) |
| if (current_longpress_keydown_.has_value() && |
| event.type() == ui::EventType::ET_KEY_RELEASED && |
| current_longpress_keydown_->code() == event.code()) { |
| current_longpress_keydown_ = std::nullopt; |
| longpress_timer_.Stop(); |
| } |
| return AssistiveSuggesterKeyResult::kNotHandled; |
| } |
| |
| void AssistiveSuggester::OnLongpressDetected() { |
| if (!(current_longpress_keydown_.has_value() || |
| IsLongpressEnabledControlV(current_longpress_keydown_.value()))) { |
| return; |
| } |
| |
| if (IsLongpressEnabledControlV(current_longpress_keydown_.value())) { |
| if (Shell::Get()->clipboard_history_controller()->ShowMenu( |
| GetClipboardHistoryMenuAnchor(), |
| ui::MenuSourceType::MENU_SOURCE_KEYBOARD, |
| crosapi::mojom::ClipboardHistoryControllerShowSource:: |
| kControlVLongpress, |
| base::BindOnce(&AssistiveSuggester::OnClipboardHistoryMenuClosing, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| // Only set `current_suggester_` if the clipboard history menu was shown. |
| current_suggester_ = &longpress_control_v_suggester_; |
| } |
| } else if (longpress_diacritics_suggester_.TrySuggestOnLongpress( |
| current_longpress_keydown_->GetCharacter())) { |
| current_suggester_ = &longpress_diacritics_suggester_; |
| } |
| current_longpress_keydown_ = std::nullopt; |
| } |
| |
| void AssistiveSuggester::OnClipboardHistoryMenuClosing(bool will_paste_item) { |
| DCHECK_EQ(current_suggester_, &longpress_control_v_suggester_); |
| if (will_paste_item) { |
| // Note: The suggestion index is irrelevant for long-pressed Ctrl+V. |
| AcceptSuggestion(/*index=*/-1); |
| } else { |
| DismissSuggestion(); |
| } |
| } |
| |
| void AssistiveSuggester::OnExternalSuggestionsUpdated( |
| const std::vector<AssistiveSuggestion>& suggestions, |
| const std::optional<SuggestionsTextContext>& context) { |
| if (!IsMultiWordSuggestEnabled()) |
| return; |
| |
| suggester_switch_->FetchEnabledSuggestionsThen( |
| base::BindOnce(&AssistiveSuggester::ProcessExternalSuggestions, |
| weak_ptr_factory_.GetWeakPtr(), suggestions, context), |
| context_); |
| } |
| |
| void AssistiveSuggester::ProcessExternalSuggestions( |
| const std::vector<AssistiveSuggestion>& suggestions, |
| const std::optional<SuggestionsTextContext>& context, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| RecordSuggestionsMatch(suggestions); |
| |
| if (!enabled_suggestions.multi_word_suggestions && |
| !IsExpandedMultiWordSuggestEnabled()) { |
| if (IsTopResultMultiWord(suggestions)) |
| RecordAssistiveDisabledReasonForMultiWord( |
| GetDisabledReasonForMultiWord(enabled_suggestions)); |
| return; |
| } |
| |
| if (current_suggester_) { |
| current_suggester_->OnExternalSuggestionsUpdated(suggestions, context); |
| return; |
| } |
| |
| if (IsTopResultMultiWord(suggestions)) { |
| current_suggester_ = &multi_word_suggester_; |
| current_suggester_->OnExternalSuggestionsUpdated(suggestions, context); |
| // The multi word suggester may not show the suggestions we pass to it. The |
| // suggestions received here may be stale and not valid given the current |
| // internal state of the multi word suggester. |
| if (current_suggester_->HasSuggestions()) { |
| RecordAssistiveCoverage(current_suggester_->GetProposeActionType()); |
| } |
| } |
| } |
| |
| void AssistiveSuggester::RecordTextInputStateMetrics( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (base::FeatureList::IsEnabled(features::kAssistMultiWord)) { |
| RecordMultiWordTextInputState(profile_->GetPrefs(), active_engine_id_, |
| enabled_suggestions); |
| } |
| } |
| |
| void AssistiveSuggester::RecordAssistiveMatchMetricsForAssistiveType( |
| AssistiveType type, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| RecordAssistiveMatch(type); |
| if (!IsAssistiveTypeEnabled(type)) { |
| RecordAssistiveDisabled(type); |
| } else if (!IsAssistiveTypeAllowedInBrowserContext(type, |
| enabled_suggestions)) { |
| RecordAssistiveNotAllowed(type); |
| } |
| } |
| |
| void AssistiveSuggester::RecordAssistiveMatchMetrics( |
| const std::u16string& text, |
| const gfx::Range selection_range, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| int len = static_cast<int>(text.length()); |
| const int cursor_pos = selection_range.end(); |
| if (cursor_pos > 0 && cursor_pos <= len && selection_range.is_empty() && |
| (cursor_pos == len || base::IsAsciiWhitespace(text[cursor_pos]))) { |
| int start_pos = std::max(0, cursor_pos - kMaxTextBeforeCursorLength); |
| std::u16string text_before_cursor = |
| text.substr(start_pos, cursor_pos - start_pos); |
| // Emoji suggestion match |
| if (emoji_suggester_.ShouldShowSuggestion(text_before_cursor)) { |
| RecordAssistiveMatchMetricsForAssistiveType(AssistiveType::kEmoji, |
| enabled_suggestions); |
| base::RecordAction( |
| base::UserMetricsAction("InputMethod.Assistive.EmojiSuggested")); |
| RecordAssistiveDisabledReasonForEmoji( |
| GetDisabledReasonForEmoji(enabled_suggestions)); |
| } |
| } |
| } |
| |
| bool AssistiveSuggester::WithinGrammarFragment() { |
| TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler(); |
| if (!input_context) |
| return false; |
| |
| std::optional<ui::GrammarFragment> grammar_fragment_opt = |
| input_context->GetGrammarFragmentAtCursor(); |
| |
| return grammar_fragment_opt != std::nullopt; |
| } |
| |
| void AssistiveSuggester::OnSurroundingTextChanged( |
| const std::u16string& text, |
| const gfx::Range selection_range) { |
| last_surrounding_text_ = text; |
| last_cursor_pos_ = selection_range.end(); |
| suggester_switch_->FetchEnabledSuggestionsThen( |
| base::BindOnce(&AssistiveSuggester::ProcessOnSurroundingTextChanged, |
| weak_ptr_factory_.GetWeakPtr(), text, selection_range), |
| context_); |
| } |
| |
| void AssistiveSuggester::ProcessOnSurroundingTextChanged( |
| const std::u16string& text, |
| const gfx::Range selection_range, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| RecordAssistiveMatchMetrics(text, selection_range, enabled_suggestions); |
| if (!IsAssistiveFeatureEnabled() || !focused_context_id_.has_value()) |
| return; |
| |
| if (IsMultiWordSuggestEnabled() && |
| enabled_suggestions.multi_word_suggestions) { |
| // Only multi word cares about tracking the current state of the text |
| // field |
| multi_word_suggester_.OnSurroundingTextChanged(text, selection_range); |
| } |
| |
| if (WithinGrammarFragment() || |
| !TrySuggestWithSurroundingText(text, selection_range, |
| enabled_suggestions)) { |
| DismissSuggestion(); |
| } |
| } |
| |
| bool AssistiveSuggester::TrySuggestWithSurroundingText( |
| const std::u16string& text, |
| const gfx::Range selection_range, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (IsSuggestionShown()) { |
| return current_suggester_->TrySuggestWithSurroundingText(text, |
| selection_range); |
| } |
| if (IsEmojiSuggestAdditionEnabled() && !IsEnhancedEmojiSuggestEnabled() && |
| enabled_suggestions.emoji_suggestions && |
| emoji_suggester_.TrySuggestWithSurroundingText(text, selection_range)) { |
| current_suggester_ = &emoji_suggester_; |
| RecordAssistiveCoverage(current_suggester_->GetProposeActionType()); |
| return true; |
| } |
| // No suggestions were shown. |
| return false; |
| } |
| |
| void AssistiveSuggester::AcceptSuggestion(size_t index) { |
| if (current_suggester_ && current_suggester_->AcceptSuggestion(index)) { |
| // Handle a race condition where the current suggester_ is set to nullptr by |
| // a simultaneous event (such as a mouse click causing a onBlur() |
| // event). |
| // TODO(b/240534923): Figure out how to record metrics when |
| // current_suggester_ is set to nullptr prematurely by a different event. |
| if (current_suggester_) { |
| RecordAssistiveSuccess(current_suggester_->GetProposeActionType()); |
| current_suggester_ = nullptr; |
| } |
| } |
| } |
| |
| void AssistiveSuggester::DismissSuggestion() { |
| if (current_suggester_) |
| current_suggester_->DismissSuggestion(); |
| current_suggester_ = nullptr; |
| } |
| |
| bool AssistiveSuggester::IsSuggestionShown() { |
| return current_suggester_ != nullptr; |
| } |
| |
| std::vector<ime::AssistiveSuggestion> AssistiveSuggester::GetSuggestions() { |
| if (IsSuggestionShown()) |
| return current_suggester_->GetSuggestions(); |
| return {}; |
| } |
| |
| void AssistiveSuggester::OnActivate(const std::string& engine_id) { |
| active_engine_id_ = engine_id; |
| longpress_diacritics_suggester_.SetEngineId(engine_id); |
| |
| if (base::FeatureList::IsEnabled(features::kAssistMultiWord)) { |
| RecordAssistiveUserPrefForMultiWord( |
| IsPredictiveWritingPrefEnabled(profile_->GetPrefs(), engine_id)); |
| } |
| if (base::FeatureList::IsEnabled( |
| features::kDiacriticsOnPhysicalKeyboardLongpress) && |
| IsUsEnglishEngine(active_engine_id_)) { |
| RecordAssistiveUserPrefForDiacriticsOnLongpress( |
| IsDiacriticsOnLongpressPrefEnabled(profile_->GetPrefs(), engine_id)); |
| } |
| } |
| |
| } // namespace ash::input_method |