blob: d57fcc33f3fb611ee67060eb99acc305542a4c06 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/input_method/personal_info_suggester.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autofill/personal_data_manager_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/extensions/input_method_api.h"
#include "chrome/browser/chromeos/input_method/ui/suggestion_details.h"
#include "chrome/browser/extensions/api/input_ime/input_ime_api.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/autofill/core/browser/ui/label_formatter_utils.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/strings/grit/components_strings.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/events/keycodes/dom/dom_code.h"
namespace chromeos {
namespace {
const size_t kMaxConfirmedTextLength = 10;
const char kSingleSubjectRegex[] = "my ";
const char kSingleOrPluralSubjectRegex[] = "(my|our) ";
const char kTriggersRegex[] = "( is:?|:) $";
const char kEmailRegex[] = "email";
const char kNameRegex[] = "(full )?name";
const char kAddressRegex[] =
"((mailing|postal|shipping|home|delivery|physical|current|billing|correct) "
")?address";
const char kPhoneNumberRegex[] =
"(((phone|mobile|telephone) )?number|phone|telephone)";
const char kFirstNameRegex[] = "first name";
const char kLastNameRegex[] = "last name";
const char kShowPersonalInfoSuggestionMessage[] =
"Personal info suggested. Press down arrow to access; escape to ignore.";
const char kDismissPersonalInfoSuggestionMessage[] = "Suggestion dismissed.";
const char kAcceptPersonalInfoSuggestionMessage[] = "Suggestion inserted.";
// The current personal information would only provide one suggestion, so there
// could be only two possible UI: 1. only one suggestion, 2. one suggestion and
// one learn more button, and the suggestion is always before the learn more
// button. So suggestion could be 1 of 1 or 1 of 2 depending on whether the
// learn more button is displayed, but learn more button can only be 2 of 2.
const char kSuggestionMessageTemplate[] =
"Suggestion %s. Button. Menu item 1 of %d. Press enter to insert; escape "
"to dismiss.";
const char kLearnMoreMessage[] =
"Learn more about suggestions. Link. Menu item 2 of 2. Press enter to "
"activate; escape to dismiss.";
const int kNoneHighlighted = -1;
constexpr base::TimeDelta kTtsShowDelay =
base::TimeDelta::FromMilliseconds(1200);
const std::vector<autofill::ServerFieldType>& GetHomeAddressTypes() {
static base::NoDestructor<std::vector<autofill::ServerFieldType>>
homeAddressTypes{
{autofill::ServerFieldType::ADDRESS_HOME_LINE1,
autofill::ServerFieldType::ADDRESS_HOME_LINE2,
autofill::ServerFieldType::ADDRESS_HOME_LINE3,
autofill::ServerFieldType::ADDRESS_HOME_STREET_ADDRESS,
autofill::ServerFieldType::ADDRESS_HOME_DEPENDENT_LOCALITY,
autofill::ServerFieldType::ADDRESS_HOME_CITY,
autofill::ServerFieldType::ADDRESS_HOME_STATE,
autofill::ServerFieldType::ADDRESS_HOME_ZIP,
autofill::ServerFieldType::ADDRESS_HOME_SORTING_CODE,
autofill::ServerFieldType::ADDRESS_HOME_COUNTRY}};
return *homeAddressTypes;
}
void RecordTimeToAccept(base::TimeDelta delta) {
base::UmaHistogramTimes("InputMethod.Assistive.TimeToAccept.PersonalInfo",
delta);
}
void RecordTimeToDismiss(base::TimeDelta delta) {
base::UmaHistogramTimes("InputMethod.Assistive.TimeToDismiss.PersonalInfo",
delta);
}
void RecordAssistiveInsufficientData(AssistiveType type) {
base::UmaHistogramEnumeration("InputMethod.Assistive.InsufficientData", type);
}
} // namespace
AssistiveType ProposePersonalInfoAssistiveAction(const std::u16string& text) {
std::string lower_case_utf8_text =
base::ToLowerASCII(base::UTF16ToUTF8(text));
if (!(RE2::FullMatch(lower_case_utf8_text, ".* $"))) {
return AssistiveType::kGenericAction;
}
if (base::FeatureList::IsEnabled(
chromeos::features::kAssistPersonalInfoAddress)) {
if (RE2::FullMatch(
lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleOrPluralSubjectRegex,
kAddressRegex, kTriggersRegex))) {
return AssistiveType::kPersonalAddress;
}
}
if (base::FeatureList::IsEnabled(
chromeos::features::kAssistPersonalInfoEmail)) {
if (RE2::FullMatch(lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleSubjectRegex,
kEmailRegex, kTriggersRegex))) {
return AssistiveType::kPersonalEmail;
}
}
if (base::FeatureList::IsEnabled(
chromeos::features::kAssistPersonalInfoName)) {
if (RE2::FullMatch(lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleSubjectRegex,
kNameRegex, kTriggersRegex))) {
return AssistiveType::kPersonalName;
}
if (RE2::FullMatch(lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleSubjectRegex,
kFirstNameRegex, kTriggersRegex))) {
return AssistiveType::kPersonalFirstName;
}
if (RE2::FullMatch(lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleSubjectRegex,
kLastNameRegex, kTriggersRegex))) {
return AssistiveType::kPersonalLastName;
}
}
if (base::FeatureList::IsEnabled(
chromeos::features::kAssistPersonalInfoPhoneNumber)) {
if (RE2::FullMatch(lower_case_utf8_text,
base::StringPrintf(".*%s%s%s", kSingleSubjectRegex,
kPhoneNumberRegex, kTriggersRegex))) {
return AssistiveType::kPersonalPhoneNumber;
}
}
return AssistiveType::kGenericAction;
}
PersonalInfoSuggester::PersonalInfoSuggester(
SuggestionHandlerInterface* suggestion_handler,
Profile* profile,
autofill::PersonalDataManager* personal_data_manager,
std::unique_ptr<TtsHandler> tts_handler)
: suggestion_handler_(suggestion_handler),
profile_(profile),
personal_data_manager_(
personal_data_manager
? personal_data_manager
: autofill::PersonalDataManagerFactory::GetForProfile(profile)),
tts_handler_(tts_handler ? std::move(tts_handler)
: std::make_unique<TtsHandler>(profile)),
highlighted_index_(kNoneHighlighted) {
suggestion_button_.id = ui::ime::ButtonId::kSuggestion;
suggestion_button_.window_type =
ui::ime::AssistiveWindowType::kPersonalInfoSuggestion;
suggestion_button_.index = 0;
settings_button_.id = ui::ime::ButtonId::kSmartInputsSettingLink;
settings_button_.announce_string = kLearnMoreMessage;
settings_button_.window_type =
ui::ime::AssistiveWindowType::kPersonalInfoSuggestion;
}
PersonalInfoSuggester::~PersonalInfoSuggester() = default;
void PersonalInfoSuggester::OnFocus(int context_id) {
context_id_ = context_id;
}
void PersonalInfoSuggester::OnBlur() {
context_id_ = -1;
}
SuggestionStatus PersonalInfoSuggester::HandleKeyEvent(
const ui::KeyEvent& event) {
if (!suggestion_shown_)
return SuggestionStatus::kNotHandled;
if (event.code() == ui::DomCode::ESCAPE) {
DismissSuggestion();
return SuggestionStatus::kDismiss;
}
if (highlighted_index_ == kNoneHighlighted && buttons_.size() > 0) {
if (event.code() == ui::DomCode::ARROW_DOWN ||
event.code() == ui::DomCode::ARROW_UP) {
highlighted_index_ =
event.code() == ui::DomCode::ARROW_DOWN ? 0 : buttons_.size() - 1;
SetButtonHighlighted(buttons_[highlighted_index_], true);
return SuggestionStatus::kBrowsing;
}
} else {
if (event.code() == ui::DomCode::ENTER) {
switch (buttons_[highlighted_index_].id) {
case ui::ime::ButtonId::kSuggestion:
AcceptSuggestion();
return SuggestionStatus::kAccept;
case ui::ime::ButtonId::kSmartInputsSettingLink:
suggestion_handler_->ClickButton(buttons_[highlighted_index_]);
return SuggestionStatus::kOpenSettings;
default:
break;
}
} else if (event.code() == ui::DomCode::ARROW_UP ||
event.code() == ui::DomCode::ARROW_DOWN) {
SetButtonHighlighted(buttons_[highlighted_index_], false);
if (event.code() == ui::DomCode::ARROW_UP) {
highlighted_index_ =
(highlighted_index_ + buttons_.size() - 1) % buttons_.size();
} else {
highlighted_index_ = (highlighted_index_ + 1) % buttons_.size();
}
SetButtonHighlighted(buttons_[highlighted_index_], true);
return SuggestionStatus::kBrowsing;
}
}
return SuggestionStatus::kNotHandled;
}
bool PersonalInfoSuggester::Suggest(const std::u16string& text) {
if (suggestion_shown_) {
size_t text_length = text.length();
bool matched = false;
for (size_t offset = 0;
offset < suggestion_.length() && offset < text_length &&
offset < kMaxConfirmedTextLength;
offset++) {
std::u16string text_before = text.substr(0, text_length - offset);
std::u16string confirmed_text = text.substr(text_length - offset);
if (base::StartsWith(suggestion_, confirmed_text,
base::CompareCase::INSENSITIVE_ASCII) &&
suggestion_ == GetSuggestion(text_before)) {
matched = true;
ShowSuggestion(suggestion_, offset);
break;
}
}
return matched;
} else {
suggestion_ = GetSuggestion(text);
if (suggestion_.empty()) {
if (proposed_action_type_ != AssistiveType::kGenericAction)
RecordAssistiveInsufficientData(proposed_action_type_);
} else {
ShowSuggestion(suggestion_, 0);
}
return suggestion_shown_;
}
}
std::u16string PersonalInfoSuggester::GetSuggestion(
const std::u16string& text) {
proposed_action_type_ = ProposePersonalInfoAssistiveAction(text);
if (proposed_action_type_ == AssistiveType::kGenericAction)
return base::EmptyString16();
if (proposed_action_type_ == AssistiveType::kPersonalEmail) {
return profile_ ? base::UTF8ToUTF16(profile_->GetProfileUserName())
: base::EmptyString16();
}
if (!personal_data_manager_)
return base::EmptyString16();
auto autofill_profiles = personal_data_manager_->GetProfilesToSuggest();
if (autofill_profiles.empty())
return base::EmptyString16();
// Currently, we are just picking the first candidate, will improve the
// strategy in the future.
auto* profile = autofill_profiles[0];
std::u16string suggestion;
const std::string app_locale = g_browser_process->GetApplicationLocale();
switch (proposed_action_type_) {
case AssistiveType::kPersonalName:
suggestion = profile->GetRawInfo(autofill::ServerFieldType::NAME_FULL);
break;
case AssistiveType::kPersonalAddress:
suggestion = autofill::GetLabelNationalAddress(GetHomeAddressTypes(),
*profile, app_locale);
break;
case AssistiveType::kPersonalPhoneNumber:
suggestion = profile->GetRawInfo(
autofill::ServerFieldType::PHONE_HOME_WHOLE_NUMBER);
break;
case AssistiveType::kPersonalFirstName:
suggestion = profile->GetRawInfo(autofill::ServerFieldType::NAME_FIRST);
break;
case AssistiveType::kPersonalLastName:
suggestion = profile->GetRawInfo(autofill::ServerFieldType::NAME_LAST);
break;
default:
NOTREACHED();
break;
}
return suggestion;
}
void PersonalInfoSuggester::ShowSuggestion(const std::u16string& text,
const size_t confirmed_length) {
if (ChromeKeyboardControllerClient::Get()->is_keyboard_visible()) {
const std::vector<std::string> args{base::UTF16ToUTF8(text)};
suggestion_handler_->OnSuggestionsChanged(args);
return;
}
if (highlighted_index_ != kNoneHighlighted) {
SetButtonHighlighted(buttons_[highlighted_index_], false);
highlighted_index_ = kNoneHighlighted;
}
std::string error;
bool show_annotation =
GetPrefValue(kPersonalInfoSuggesterAcceptanceCount) < kMaxAcceptanceCount;
ui::ime::SuggestionDetails details;
details.text = text;
details.confirmed_length = confirmed_length;
details.show_annotation = show_annotation;
details.show_setting_link =
GetPrefValue(kPersonalInfoSuggesterAcceptanceCount) == 0 &&
GetPrefValue(kPersonalInfoSuggesterShowSettingCount) <
kMaxShowSettingCount;
suggestion_handler_->SetSuggestion(context_id_, details, &error);
if (!error.empty()) {
LOG(ERROR) << "Fail to show suggestion. " << error;
}
suggestion_button_.announce_string = base::StringPrintf(
kSuggestionMessageTemplate, base::UTF16ToUTF8(text).c_str(),
details.show_setting_link ? 2 : 1);
buttons_.clear();
buttons_.push_back(suggestion_button_);
if (details.show_setting_link)
buttons_.push_back(settings_button_);
if (suggestion_shown_) {
first_shown_ = false;
} else {
session_start_ = base::TimeTicks::Now();
first_shown_ = true;
IncrementPrefValueTilCapped(kPersonalInfoSuggesterShowSettingCount,
kMaxShowSettingCount);
tts_handler_->Announce(
// TODO(jiwan): Add translation to other languages when we support
// more than English.
kShowPersonalInfoSuggestionMessage, kTtsShowDelay);
}
suggestion_shown_ = true;
}
int PersonalInfoSuggester::GetPrefValue(const std::string& pref_name) {
DictionaryPrefUpdate update(profile_->GetPrefs(),
prefs::kAssistiveInputFeatureSettings);
auto value = update->FindIntKey(pref_name);
if (!value.has_value()) {
update->SetIntKey(pref_name, 0);
return 0;
}
return *value;
}
void PersonalInfoSuggester::IncrementPrefValueTilCapped(
const std::string& pref_name,
int max_value) {
int value = GetPrefValue(pref_name);
if (value < max_value) {
DictionaryPrefUpdate update(profile_->GetPrefs(),
prefs::kAssistiveInputFeatureSettings);
update->SetIntKey(pref_name, value + 1);
}
}
AssistiveType PersonalInfoSuggester::GetProposeActionType() {
return proposed_action_type_;
}
bool PersonalInfoSuggester::AcceptSuggestion(size_t index) {
std::string error;
suggestion_handler_->AcceptSuggestion(context_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to accept suggestion. " << error;
return false;
}
RecordTimeToAccept(base::TimeTicks::Now() - session_start_);
IncrementPrefValueTilCapped(kPersonalInfoSuggesterAcceptanceCount,
kMaxAcceptanceCount);
suggestion_shown_ = false;
tts_handler_->Announce(kAcceptPersonalInfoSuggestionMessage);
return true;
}
void PersonalInfoSuggester::DismissSuggestion() {
std::string error;
suggestion_handler_->DismissSuggestion(context_id_, &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to dismiss suggestion. " << error;
return;
}
suggestion_shown_ = false;
RecordTimeToDismiss(base::TimeTicks::Now() - session_start_);
tts_handler_->Announce(kDismissPersonalInfoSuggestionMessage);
}
void PersonalInfoSuggester::SetButtonHighlighted(
const ui::ime::AssistiveWindowButton& button,
bool highlighted) {
std::string error;
suggestion_handler_->SetButtonHighlighted(context_id_, button, highlighted,
&error);
if (!error.empty()) {
LOG(ERROR) << "Failed to set button highlighted. " << error;
}
}
} // namespace chromeos