| // 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/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/feature_list.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_suggester_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/profiles/profile_manager.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/autofill/core/browser/personal_data_manager.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 "url/gurl.h" |
| |
| namespace ash { |
| namespace input_method { |
| |
| namespace { |
| |
| using ime::AssistiveSuggestion; |
| using ime::AssistiveSuggestionMode; |
| using ime::AssistiveSuggestionType; |
| |
| 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 RecordAssistiveDisabledReasonForPersonalInfo(DisabledReason reason) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled.PersonalInfo", |
| reason); |
| } |
| |
| void RecordAssistiveDisabledReasonForEmoji(DisabledReason reason) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled.Emoji", reason); |
| } |
| |
| void RecordAssistiveDisabledReasonForMultiWord(DisabledReason reason) { |
| base::UmaHistogramEnumeration("InputMethod.Assistive.Disabled.MultiWord", |
| reason); |
| } |
| |
| void RecordAssistiveUserPrefForPersonalInfo(bool value) { |
| base::UmaHistogramBoolean("InputMethod.Assistive.UserPref.PersonalInfo", |
| value); |
| } |
| |
| 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); |
| } |
| |
| 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); |
| } |
| |
| 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, |
| autofill::PersonalDataManager* personal_data_manager_for_testing) |
| : profile_(profile), |
| personal_info_suggester_(suggestion_handler, |
| profile, |
| personal_data_manager_for_testing), |
| emoji_suggester_(suggestion_handler, profile), |
| multi_word_suggester_(suggestion_handler, profile), |
| longpress_diacritics_suggester_(suggestion_handler), |
| suggester_switch_(std::move(suggester_switch)) { |
| RecordAssistiveUserPrefForPersonalInfo( |
| profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled)); |
| RecordAssistiveUserPrefForEmoji( |
| profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled)); |
| } |
| |
| AssistiveSuggester::~AssistiveSuggester() = default; |
| |
| bool AssistiveSuggester::IsAssistiveFeatureEnabled() { |
| return IsAssistPersonalInfoEnabled() || IsEmojiSuggestAdditionEnabled() || |
| IsMultiWordSuggestEnabled() || IsEnhancedEmojiSuggestEnabled() || |
| IsDiacriticsOnPhysicalKeyboardLongpressEnabled(); |
| } |
| |
| void AssistiveSuggester::FetchEnabledSuggestionsFromBrowserContextThen( |
| AssistiveSuggesterSwitch::FetchEnabledSuggestionsCallback callback) { |
| suggester_switch_->FetchEnabledSuggestionsThen(std::move(callback)); |
| } |
| |
| bool AssistiveSuggester::IsAssistPersonalInfoEnabled() { |
| return base::FeatureList::IsEnabled(features::kAssistPersonalInfo) && |
| profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled); |
| } |
| |
| 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 features::IsAssistiveMultiWordEnabled() && |
| 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::GetDisabledReasonForPersonalInfo( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (!base::FeatureList::IsEnabled(features::kAssistPersonalInfo)) { |
| return DisabledReason::kFeatureFlagOff; |
| } |
| if (!profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled)) { |
| return DisabledReason::kUserSettingsOff; |
| } |
| if (!enabled_suggestions.personal_info_suggestions) { |
| return DisabledReason::kUrlOrAppNotAllowed; |
| } |
| return DisabledReason::kNone; |
| } |
| |
| 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 (!features::IsAssistiveMultiWordEnabled()) { |
| 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::kPersonalEmail: |
| case AssistiveType::kPersonalAddress: |
| case AssistiveType::kPersonalPhoneNumber: |
| case AssistiveType::kPersonalName: |
| case AssistiveType::kPersonalNumber: |
| case AssistiveType::kPersonalFirstName: |
| case AssistiveType::kPersonalLastName: |
| return AssistiveFeature::kPersonalInfoSuggestion; |
| case AssistiveType::kEmoji: |
| return AssistiveFeature::kEmojiSuggestion; |
| case AssistiveType::kMultiWordCompletion: |
| case AssistiveType::kMultiWordPrediction: |
| return AssistiveFeature::kMultiWordSuggestion; |
| default: |
| // We should only handle Personal Info, 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::kPersonalInfoSuggestion: |
| // TODO: Use value from settings when crbug/1068457 is done. |
| return IsAssistPersonalInfoEnabled(); |
| 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::kPersonalInfoSuggestion: |
| return enabled_suggestions.personal_info_suggestions; |
| 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) { |
| // 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. |
| DCHECK(context_id > 0); |
| focused_context_id_ = context_id; |
| personal_info_suggester_.OnFocus(context_id); |
| emoji_suggester_.OnFocus(context_id); |
| multi_word_suggester_.OnFocus(context_id); |
| longpress_diacritics_suggester_.OnFocus(context_id); |
| enabled_suggestions_from_last_onfocus_ = absl::nullopt; |
| suggester_switch_->FetchEnabledSuggestionsThen( |
| base::BindOnce(&AssistiveSuggester::HandleEnabledSuggestionsOnFocus, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| 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_ = absl::nullopt; |
| enabled_suggestions_from_last_onfocus_ = absl::nullopt; |
| personal_info_suggester_.OnBlur(); |
| emoji_suggester_.OnBlur(); |
| multi_word_suggester_.OnBlur(); |
| longpress_diacritics_suggester_.OnBlur(); |
| } |
| |
| bool AssistiveSuggester::OnKeyEvent(const ui::KeyEvent& event) { |
| if (!focused_context_id_.has_value()) |
| return 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 true; |
| case SuggestionStatus::kDismiss: |
| current_suggester_ = nullptr; |
| return true; |
| case SuggestionStatus::kBrowsing: |
| return true; |
| default: |
| break; |
| } |
| } |
| |
| return AssistiveSuggester::HandleLongpressEnabledKeyEvent(event); |
| } |
| |
| bool AssistiveSuggester::HandleLongpressEnabledKeyEvent( |
| const ui::KeyEvent& event) { |
| if (!IsDiacriticsOnPhysicalKeyboardLongpressEnabled() || |
| !enabled_suggestions_from_last_onfocus_ || |
| !enabled_suggestions_from_last_onfocus_->diacritic_suggestions || |
| !kDefaultLongpressEnabledKeys.contains(event.GetCharacter())) { |
| return false; |
| } |
| |
| // Longpress diacritics behaviour overrides the longpress to repeat key |
| // behaviour for alphabetical keys. |
| if (event.is_repeat() && |
| kDefaultLongpressEnabledKeys.contains(event.GetCharacter())) { |
| return true; // Do not propagate this event. |
| } |
| |
| // Process longpress keydown event. |
| if (current_longpress_keydown_ == absl::nullopt && |
| event.type() == ui::EventType::ET_KEY_PRESSED) { |
| current_longpress_keydown_ = event; |
| longpress_timer_.Start( |
| FROM_HERE, kLongpressActivationDelay, |
| base::BindOnce(&AssistiveSuggester::OnLongpressDetected, |
| weak_ptr_factory_.GetWeakPtr())); |
| return false; |
| } |
| |
| // 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_ = absl::nullopt; |
| longpress_timer_.Stop(); |
| } |
| return false; |
| } |
| |
| void AssistiveSuggester::OnLongpressDetected() { |
| if (!current_longpress_keydown_.has_value() || |
| // If the following conditions are met, its possible the text box does not |
| // support IME operations. |
| last_surrounding_text_.empty() || last_cursor_pos_ <= 0 || |
| static_cast<int>(last_surrounding_text_.length()) < last_cursor_pos_ || |
| last_surrounding_text_[last_cursor_pos_ - 1] != |
| current_longpress_keydown_->GetCharacter()) { |
| return; |
| } |
| if (longpress_diacritics_suggester_.TrySuggestOnLongpress( |
| current_longpress_keydown_->GetCharacter())) { |
| current_suggester_ = &longpress_diacritics_suggester_; |
| } |
| current_longpress_keydown_ = absl::nullopt; |
| } |
| |
| void AssistiveSuggester::OnExternalSuggestionsUpdated( |
| const std::vector<AssistiveSuggestion>& suggestions) { |
| if (!IsMultiWordSuggestEnabled()) |
| return; |
| |
| suggester_switch_->FetchEnabledSuggestionsThen( |
| base::BindOnce(&AssistiveSuggester::ProcessExternalSuggestions, |
| weak_ptr_factory_.GetWeakPtr(), suggestions)); |
| } |
| |
| void AssistiveSuggester::ProcessExternalSuggestions( |
| const std::vector<AssistiveSuggestion>& suggestions, |
| 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); |
| return; |
| } |
| |
| if (IsTopResultMultiWord(suggestions)) { |
| current_suggester_ = &multi_word_suggester_; |
| current_suggester_->OnExternalSuggestionsUpdated(suggestions); |
| RecordAssistiveCoverage(current_suggester_->GetProposeActionType()); |
| } |
| } |
| |
| void AssistiveSuggester::RecordTextInputStateMetrics( |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (features::IsAssistiveMultiWordEnabled()) { |
| 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, |
| int cursor_pos, |
| int anchor_pos, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| int len = static_cast<int>(text.length()); |
| if (cursor_pos > 0 && cursor_pos <= len && cursor_pos == anchor_pos && |
| (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); |
| // Personal info suggestion match |
| AssistiveType type = ProposePersonalInfoAssistiveAction(text_before_cursor); |
| if (type != AssistiveType::kGenericAction) { |
| RecordAssistiveMatchMetricsForAssistiveType(type, enabled_suggestions); |
| RecordAssistiveDisabledReasonForPersonalInfo( |
| GetDisabledReasonForPersonalInfo(enabled_suggestions)); |
| // Emoji suggestion match |
| } else 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(int cursor_pos, int anchor_pos) { |
| TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler(); |
| if (!input_context) |
| return false; |
| |
| absl::optional<ui::GrammarFragment> grammar_fragment_opt = |
| input_context->GetGrammarFragmentAtCursor(); |
| |
| return grammar_fragment_opt != absl::nullopt; |
| } |
| |
| void AssistiveSuggester::OnSurroundingTextChanged(const std::u16string& text, |
| int cursor_pos, |
| int anchor_pos) { |
| last_surrounding_text_ = text; |
| last_cursor_pos_ = cursor_pos; |
| suggester_switch_->FetchEnabledSuggestionsThen(base::BindOnce( |
| &AssistiveSuggester::ProcessOnSurroundingTextChanged, |
| weak_ptr_factory_.GetWeakPtr(), text, cursor_pos, anchor_pos)); |
| } |
| |
| void AssistiveSuggester::ProcessOnSurroundingTextChanged( |
| const std::u16string& text, |
| int cursor_pos, |
| int anchor_pos, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| RecordAssistiveMatchMetrics(text, cursor_pos, anchor_pos, |
| 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, cursor_pos, |
| anchor_pos); |
| } |
| |
| if (WithinGrammarFragment(cursor_pos, anchor_pos) || |
| !TrySuggestWithSurroundingText(text, cursor_pos, anchor_pos, |
| enabled_suggestions)) { |
| DismissSuggestion(); |
| } |
| } |
| |
| bool AssistiveSuggester::TrySuggestWithSurroundingText( |
| const std::u16string& text, |
| int cursor_pos, |
| int anchor_pos, |
| const AssistiveSuggesterSwitch::EnabledSuggestions& enabled_suggestions) { |
| if (IsSuggestionShown()) { |
| return current_suggester_->TrySuggestWithSurroundingText(text, cursor_pos, |
| anchor_pos); |
| } |
| if (IsAssistPersonalInfoEnabled() && |
| enabled_suggestions.personal_info_suggestions && |
| personal_info_suggester_.TrySuggestWithSurroundingText(text, cursor_pos, |
| anchor_pos)) { |
| current_suggester_ = &personal_info_suggester_; |
| if (personal_info_suggester_.IsFirstShown()) { |
| RecordAssistiveCoverage(current_suggester_->GetProposeActionType()); |
| } |
| return true; |
| } |
| if (IsEmojiSuggestAdditionEnabled() && !IsEnhancedEmojiSuggestEnabled() && |
| enabled_suggestions.emoji_suggestions && |
| emoji_suggester_.TrySuggestWithSurroundingText(text, cursor_pos, |
| anchor_pos)) { |
| 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 (features::IsAssistiveMultiWordEnabled()) { |
| RecordAssistiveUserPrefForMultiWord( |
| IsPredictiveWritingPrefEnabled(profile_->GetPrefs(), engine_id)); |
| } |
| if (base::FeatureList::IsEnabled( |
| features::kDiacriticsOnPhysicalKeyboardLongpress) && |
| IsUsEnglishEngine(active_engine_id_)) { |
| RecordAssistiveUserPrefForDiacriticsOnLongpress( |
| IsDiacriticsOnLongpressPrefEnabled(profile_->GetPrefs(), engine_id)); |
| } |
| } |
| |
| } // namespace input_method |
| } // namespace ash |