blob: ecea1511b23140e8df95a1565962495e60cc7a78 [file] [log] [blame]
// Copyright 2020 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/emoji_suggester.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/files/file_util.h"
#include "base/i18n/number_formatting.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/services/ime/constants.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/dom/dom_code.h"
namespace chromeos {
namespace {
constexpr char kEmojiSuggesterShowSettingCount[] =
"emoji_suggester.show_setting_count";
const int kMaxCandidateSize = 5;
const char kSpaceChar = ' ';
constexpr char kTrimLeadingChars[] = "(";
constexpr char kEmojiMapFilePathTemplateName[] = "/emoji/emoji-map%s.csv";
const int kMaxSuggestionIndex = 31;
const int kMaxSuggestionSize = kMaxSuggestionIndex + 1;
const int kNoneHighlighted = -1;
std::string ReadEmojiDataFromFile() {
if (!base::DirectoryExists(base::FilePath(ime::kBundledInputMethodsDirPath)))
return base::EmptyString();
std::string emoji_data;
base::FilePath::StringType path(ime::kBundledInputMethodsDirPath);
std::string value = base::GetFieldTrialParamValueByFeature(
chromeos::features::kEmojiSuggestAddition, "map");
std::string file_path =
base::StringPrintf(kEmojiMapFilePathTemplateName, value.c_str());
path.append(FILE_PATH_LITERAL(file_path));
if (!base::ReadFileToString(base::FilePath(path), &emoji_data))
LOG(WARNING) << "Emoji map file missing.";
return emoji_data;
}
std::vector<std::string> SplitString(const std::string& str,
const std::string& delimiter) {
return base::SplitString(str, delimiter, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
std::string GetLastWord(const std::string& str) {
// We only suggest if last char is a white space so search for last word from
// second last char.
DCHECK_EQ(kSpaceChar, str.back());
size_t last_pos_to_search = str.length() - 2;
const auto space_before_last_word = str.find_last_of(" ", last_pos_to_search);
// If not found, return the entire string up to the last position to search
// else return the last word.
const std::string last_word =
space_before_last_word == std::string::npos
? str.substr(0, last_pos_to_search + 1)
: str.substr(space_before_last_word + 1,
last_pos_to_search - space_before_last_word);
// Remove any leading special characters
return base::ToLowerASCII(
base::TrimString(last_word, kTrimLeadingChars, base::TRIM_LEADING));
}
void RecordTimeToAccept(base::TimeDelta delta) {
UMA_HISTOGRAM_MEDIUM_TIMES("InputMethod.Assistive.TimeToAccept.Emoji", delta);
}
void RecordTimeToDismiss(base::TimeDelta delta) {
UMA_HISTOGRAM_MEDIUM_TIMES("InputMethod.Assistive.TimeToDismiss.Emoji",
delta);
}
} // namespace
EmojiSuggester::EmojiSuggester(SuggestionHandlerInterface* suggestion_handler,
Profile* profile)
: suggestion_handler_(suggestion_handler),
profile_(profile),
highlighted_index_(kNoneHighlighted) {
LoadEmojiMap();
properties_.type = ui::ime::AssistiveWindowType::kEmojiSuggestion;
suggestion_button_.id = ui::ime::ButtonId::kSuggestion;
suggestion_button_.window_type =
ui::ime::AssistiveWindowType::kEmojiSuggestion;
learn_more_button_.id = ui::ime::ButtonId::kLearnMore;
learn_more_button_.announce_string = l10n_util::GetStringUTF8(IDS_LEARN_MORE);
learn_more_button_.window_type =
ui::ime::AssistiveWindowType::kEmojiSuggestion;
}
EmojiSuggester::~EmojiSuggester() = default;
void EmojiSuggester::LoadEmojiMap() {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&ReadEmojiDataFromFile),
base::BindOnce(&EmojiSuggester::OnEmojiDataLoaded,
weak_factory_.GetWeakPtr()));
}
void EmojiSuggester::LoadEmojiMapForTesting(const std::string& emoji_data) {
OnEmojiDataLoaded(emoji_data);
}
void EmojiSuggester::OnEmojiDataLoaded(const std::string& emoji_data) {
// Split data into lines.
for (const auto& line : SplitString(emoji_data, "\n")) {
// Get a word and a string of emojis from the line.
const auto comma_pos = line.find_first_of(",");
DCHECK(comma_pos != std::string::npos);
std::string word = line.substr(0, comma_pos);
std::u16string emojis = base::UTF8ToUTF16(line.substr(comma_pos + 1));
// Build emoji_map_ from splitting the string of emojis.
emoji_map_[word] =
base::SplitString(emojis, base::UTF8ToUTF16(";"), base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
// TODO(crbug/1093179): Implement arrow to indicate more emojis available.
// Only loads 5 emojis for now until arrow is implemented.
if (emoji_map_[word].size() > kMaxCandidateSize)
emoji_map_[word].resize(kMaxCandidateSize);
DCHECK_LE(static_cast<int>(emoji_map_[word].size()), kMaxSuggestionSize);
}
}
void EmojiSuggester::RecordAcceptanceIndex(int index) {
base::UmaHistogramExactLinear(
"InputMethod.Assistive.EmojiSuggestAddition.AcceptanceIndex", index,
kMaxSuggestionIndex);
}
void EmojiSuggester::OnFocus(int context_id) {
context_id_ = context_id;
}
void EmojiSuggester::OnBlur() {
context_id_ = -1;
}
SuggestionStatus EmojiSuggester::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(highlighted_index_);
return SuggestionStatus::kAccept;
case ui::ime::ButtonId::kLearnMore:
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 EmojiSuggester::ShouldShowSuggestion(const std::u16string& text) {
if (text[text.length() - 1] != kSpaceChar)
return false;
std::string last_word =
base::ToLowerASCII(GetLastWord(base::UTF16ToUTF8(text)));
if (!last_word.empty() && emoji_map_.count(last_word)) {
return true;
}
return false;
}
bool EmojiSuggester::Suggest(const std::u16string& text) {
if (emoji_map_.empty() || text[text.length() - 1] != kSpaceChar)
return false;
std::string last_word =
base::ToLowerASCII(GetLastWord(base::UTF16ToUTF8(text)));
if (!last_word.empty() && emoji_map_.count(last_word)) {
ShowSuggestion(last_word);
return true;
}
return false;
}
void EmojiSuggester::ShowSuggestion(const std::string& text) {
if (ChromeKeyboardControllerClient::Get()->is_keyboard_visible())
return;
highlighted_index_ = kNoneHighlighted;
std::string error;
// TODO(crbug/1099495): Move suggestion_show_ after checking for error and fix
// tests.
suggestion_shown_ = true;
candidates_ = emoji_map_.at(text);
properties_.visible = true;
properties_.candidates = candidates_;
properties_.announce_string =
l10n_util::GetStringUTF8(IDS_SUGGESTION_EMOJI_SUGGESTED);
properties_.show_setting_link =
GetPrefValue(kEmojiSuggesterShowSettingCount) <
kEmojiSuggesterShowSettingMaxCount;
IncrementPrefValueTilCapped(kEmojiSuggesterShowSettingCount,
kEmojiSuggesterShowSettingMaxCount);
ShowSuggestionWindow();
session_start_ = base::TimeTicks::Now();
buttons_.clear();
for (size_t i = 0; i < candidates_.size(); i++) {
suggestion_button_.index = i;
suggestion_button_.announce_string = l10n_util::GetStringFUTF8(
IDS_SUGGESTION_EMOJI_CHOSEN, candidates_[i], base::FormatNumber(i + 1),
base::FormatNumber(candidates_.size()));
buttons_.push_back(suggestion_button_);
}
if (properties_.show_setting_link) {
buttons_.push_back(learn_more_button_);
}
}
void EmojiSuggester::ShowSuggestionWindow() {
std::string error;
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties_,
&error);
if (!error.empty()) {
LOG(ERROR) << "Fail to show suggestion. " << error;
}
}
bool EmojiSuggester::AcceptSuggestion(size_t index) {
if (index < 0 || index >= candidates_.size())
return false;
std::string error;
suggestion_handler_->AcceptSuggestionCandidate(context_id_,
candidates_[index], &error);
if (!error.empty()) {
LOG(ERROR) << "Failed to accept suggestion. " << error;
return false;
}
RecordTimeToAccept(base::TimeTicks::Now() - session_start_);
suggestion_shown_ = false;
RecordAcceptanceIndex(index);
return true;
}
void EmojiSuggester::DismissSuggestion() {
std::string error;
properties_.visible = false;
properties_.announce_string =
l10n_util::GetStringUTF8(IDS_SUGGESTION_DISMISSED);
suggestion_handler_->SetAssistiveWindowProperties(context_id_, properties_,
&error);
if (!error.empty()) {
LOG(ERROR) << "Failed to dismiss suggestion. " << error;
return;
}
suggestion_shown_ = false;
RecordTimeToDismiss(base::TimeTicks::Now() - session_start_);
}
void EmojiSuggester::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;
}
}
int EmojiSuggester::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 EmojiSuggester::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 EmojiSuggester::GetProposeActionType() {
return AssistiveType::kEmoji;
}
bool EmojiSuggester::GetSuggestionShownForTesting() const {
return suggestion_shown_;
}
size_t EmojiSuggester::GetCandidatesSizeForTesting() const {
return candidates_.size();
}
} // namespace chromeos