blob: 23d7d045e79d564260fb750c402e59600900a2f1 [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/assistive_suggester.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/window_properties.h"
#include "base/feature_list.h"
#include "base/hash/hash.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "components/exo/wm_helper.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "url/gurl.h"
namespace chromeos {
namespace {
const char kMaxTextBeforeCursorLength = 50;
const char* kAllowedDomainsForPersonalInfoSuggester[] = {
"discord.com", "messenger.com", "web.whatsapp.com",
"web.skype.com", "duo.google.com", "hangouts.google.com",
"chat.google.com", "messages.google.com", "web.telegram.org",
"voice.google.com",
};
const char* kAllowedDomainsForEmojiSuggester[] = {
"discord.com", "messenger.com", "web.whatsapp.com",
"web.skype.com", "duo.google.com", "hangouts.google.com",
"chat.google.com", "messages.google.com", "web.telegram.org",
"voice.google.com",
};
const char* kTestUrls[] = {
"e14s-test",
"simple_textarea.html",
"test_page.html",
};
// For some internal websites, we do not want to reveal their urls in plain
// text. See map between url and hash code in
// https://docs.google.com/spreadsheets/d/1VELTWiHrUTEyX4HQI5PL_jDVFreM-lRhThVOurUuOk4/edit#gid=0
const uint32_t kHashedInternalUrls[] = {
1845308025U,
153302869U,
};
// For ARC++ apps, use arc package name. For system apps, use app ID.
const char* kAllowedAppsForPersonalInfoSuggester[] = {
"com.discord",
"com.facebook.orca",
"com.whatsapp",
"com.skype.raider",
"com.google.android.apps.tachyon",
"com.google.android.talk",
"org.telegram.messenger",
"com.enflick.android.TextNow",
"com.facebook.mlite",
"com.viber.voip",
"com.skype.m2",
"com.imo.android.imoim",
"com.google.android.apps.googlevoice",
"com.playstation.mobilemessenger",
"kik.android",
"com.link.messages.sms",
"jp.naver.line.android",
"com.skype.m2",
"co.happybits.marcopolo",
"com.imo.android.imous",
"mmfbcljfglbokpmkimbfghdkjmjhdgbg", // System text
};
// For ARC++ apps, use arc package name. For system apps, use app ID.
const char* kAllowedAppsForEmojiSuggester[] = {
"com.discord",
"com.facebook.orca",
"com.whatsapp",
"com.skype.raider",
"com.google.android.apps.tachyon",
"com.google.android.talk",
"org.telegram.messenger",
"com.enflick.android.TextNow",
"com.facebook.mlite",
"com.viber.voip",
"com.skype.m2",
"com.imo.android.imoim",
"com.google.android.apps.googlevoice",
"com.playstation.mobilemessenger",
"kik.android",
"com.link.messages.sms",
"jp.naver.line.android",
"com.skype.m2",
"co.happybits.marcopolo",
"com.imo.android.imous",
"mmfbcljfglbokpmkimbfghdkjmjhdgbg", // System text
};
void RecordAssistiveMatch(AssistiveType type) {
base::UmaHistogramEnumeration("InputMethod.Assistive.Match", 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 RecordAssistiveUserPrefForPersonalInfo(bool value) {
base::UmaHistogramBoolean("InputMethod.Assistive.UserPref.PersonalInfo",
value);
}
void RecordAssistiveUserPrefForEmoji(bool value) {
base::UmaHistogramBoolean("InputMethod.Assistive.UserPref.Emoji", 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 IsTestUrl(GURL url) {
std::string filename = url.ExtractFileName();
for (const char* test_url : kTestUrls) {
if (base::CompareCaseInsensitiveASCII(filename, test_url) == 0) {
return true;
}
}
return false;
}
bool IsInternalWebsite(GURL url) {
std::string host = url.host();
for (const size_t hash_code : kHashedInternalUrls) {
if (hash_code == base::PersistentHash(host)) {
return true;
}
}
return false;
}
template <size_t N>
bool IsAllowedUrl(const char* (&allowedDomains)[N]) {
Browser* browser = chrome::FindLastActive();
if (browser && browser->window() && browser->window()->IsActive() &&
browser->tab_strip_model() &&
browser->tab_strip_model()->GetActiveWebContents()) {
GURL url = browser->tab_strip_model()
->GetActiveWebContents()
->GetLastCommittedURL();
if (IsTestUrl(url) || IsInternalWebsite(url))
return true;
for (size_t i = 0; i < N; i++) {
if (url.DomainIs(allowedDomains[i])) {
return true;
}
}
}
return false;
}
template <size_t N>
bool IsAllowedApp(const char* (&allowedApps)[N]) {
// WMHelper is not available in Chrome on Linux.
if (!exo::WMHelper::HasInstance())
return false;
auto* wm_helper = exo::WMHelper::GetInstance();
auto* window = wm_helper ? wm_helper->GetActiveWindow() : nullptr;
if (!window)
return false;
// TODO(crbug/1094113): improve to cover more scenarios such as chat heads.
const std::string* arc_package_name =
window->GetProperty(ash::kArcPackageNameKey);
if (arc_package_name && std::find(allowedApps, allowedApps + N,
*arc_package_name) != allowedApps + N) {
return true;
}
const std::string* app_id = window->GetProperty(ash::kAppIDKey);
if (app_id &&
std::find(allowedApps, allowedApps + N, *app_id) != allowedApps + N) {
return true;
}
return false;
}
bool IsAllowedUrlOrAppForPersonalInfoSuggestion() {
return IsAllowedUrl(kAllowedDomainsForPersonalInfoSuggester) ||
IsAllowedApp(kAllowedAppsForPersonalInfoSuggester);
}
bool IsAllowedUrlOrAppForEmojiSuggestion() {
return IsAllowedUrl(kAllowedDomainsForEmojiSuggester) ||
IsAllowedApp(kAllowedAppsForEmojiSuggester);
}
} // namespace
AssistiveSuggester::AssistiveSuggester(InputMethodEngine* engine,
Profile* profile)
: profile_(profile),
personal_info_suggester_(engine, profile),
emoji_suggester_(engine, profile) {
RecordAssistiveUserPrefForPersonalInfo(
profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled));
RecordAssistiveUserPrefForEmoji(
profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled));
}
bool AssistiveSuggester::IsAssistiveFeatureEnabled() {
return IsAssistPersonalInfoEnabled() || IsEmojiSuggestAdditionEnabled();
}
bool AssistiveSuggester::IsAssistPersonalInfoEnabled() {
return base::FeatureList::IsEnabled(
chromeos::features::kAssistPersonalInfo) &&
profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled);
}
bool AssistiveSuggester::IsEmojiSuggestAdditionEnabled() {
return base::FeatureList::IsEnabled(
chromeos::features::kEmojiSuggestAddition) &&
profile_->GetPrefs()->GetBoolean(
prefs::kEmojiSuggestionEnterpriseAllowed) &&
profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled);
}
DisabledReason AssistiveSuggester::GetDisabledReasonForPersonalInfo() {
if (!base::FeatureList::IsEnabled(chromeos::features::kAssistPersonalInfo)) {
return DisabledReason::kFeatureFlagOff;
}
if (!profile_->GetPrefs()->GetBoolean(prefs::kAssistPersonalInfoEnabled)) {
return DisabledReason::kUserSettingsOff;
}
if (!IsAllowedUrlOrAppForPersonalInfoSuggestion()) {
return DisabledReason::kUrlOrAppNotAllowed;
}
return DisabledReason::kNone;
}
DisabledReason AssistiveSuggester::GetDisabledReasonForEmoji() {
if (!base::FeatureList::IsEnabled(
chromeos::features::kEmojiSuggestAddition)) {
return DisabledReason::kFeatureFlagOff;
}
if (!profile_->GetPrefs()->GetBoolean(
prefs::kEmojiSuggestionEnterpriseAllowed)) {
return DisabledReason::kEnterpriseSettingsOff;
}
if (!profile_->GetPrefs()->GetBoolean(prefs::kEmojiSuggestionEnabled)) {
return DisabledReason::kUserSettingsOff;
}
if (!IsAllowedUrlOrAppForEmojiSuggestion()) {
return DisabledReason::kUrlOrAppNotAllowed;
}
return DisabledReason::kNone;
}
bool AssistiveSuggester::IsActionEnabled(AssistiveType action) {
switch (action) {
case AssistiveType::kPersonalEmail:
case AssistiveType::kPersonalAddress:
case AssistiveType::kPersonalPhoneNumber:
case AssistiveType::kPersonalName:
case AssistiveType::kPersonalNumber:
case AssistiveType::kPersonalFirstName:
case AssistiveType::kPersonalLastName:
// TODO: Use value from settings when crbug/1068457 is done.
return IsAssistPersonalInfoEnabled();
break;
case AssistiveType::kEmoji:
return IsEmojiSuggestAdditionEnabled();
default:
break;
}
return false;
}
void AssistiveSuggester::OnFocus(int context_id) {
context_id_ = context_id;
personal_info_suggester_.OnFocus(context_id_);
emoji_suggester_.OnFocus(context_id_);
}
void AssistiveSuggester::OnBlur() {
context_id_ = -1;
personal_info_suggester_.OnBlur();
emoji_suggester_.OnBlur();
}
bool AssistiveSuggester::OnKeyEvent(const ui::KeyEvent& event) {
if (context_id_ == -1)
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) {
SuggestionStatus status = current_suggester_->HandleKeyEvent(event);
switch (status) {
case SuggestionStatus::kAccept:
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 false;
}
void AssistiveSuggester::RecordAssistiveMatchMetricsForAction(
AssistiveType action) {
RecordAssistiveMatch(action);
if (!IsActionEnabled(action)) {
RecordAssistiveDisabled(action);
} else if (!IsAllowedUrlOrAppForEmojiSuggestion()) {
RecordAssistiveNotAllowed(action);
}
}
void AssistiveSuggester::RecordAssistiveMatchMetrics(const std::u16string& text,
int cursor_pos,
int anchor_pos) {
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 action =
ProposePersonalInfoAssistiveAction(text_before_cursor);
if (action != AssistiveType::kGenericAction) {
RecordAssistiveMatchMetricsForAction(action);
RecordAssistiveDisabledReasonForPersonalInfo(
GetDisabledReasonForPersonalInfo());
// Emoji suggestion match
} else if (emoji_suggester_.ShouldShowSuggestion(text_before_cursor)) {
RecordAssistiveMatchMetricsForAction(AssistiveType::kEmoji);
base::RecordAction(
base::UserMetricsAction("InputMethod.Assistive.EmojiSuggested"));
RecordAssistiveDisabledReasonForEmoji(GetDisabledReasonForEmoji());
}
}
}
bool AssistiveSuggester::OnSurroundingTextChanged(const std::u16string& text,
int cursor_pos,
int anchor_pos) {
if (context_id_ == -1)
return false;
if (!Suggest(text, cursor_pos, anchor_pos)) {
DismissSuggestion();
}
return IsSuggestionShown();
}
bool AssistiveSuggester::Suggest(const std::u16string& text,
int cursor_pos,
int anchor_pos) {
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])) &&
(base::IsAsciiWhitespace(text[cursor_pos - 1]) || IsSuggestionShown())) {
// |text| could be very long, we get at most |kMaxTextBeforeCursorLength|
// characters before cursor.
int start_pos = std::max(0, cursor_pos - kMaxTextBeforeCursorLength);
std::u16string text_before_cursor =
text.substr(start_pos, cursor_pos - start_pos);
if (IsSuggestionShown()) {
return current_suggester_->Suggest(text_before_cursor);
}
if (IsAssistPersonalInfoEnabled() &&
IsAllowedUrlOrAppForPersonalInfoSuggestion() &&
personal_info_suggester_.Suggest(text_before_cursor)) {
current_suggester_ = &personal_info_suggester_;
if (personal_info_suggester_.IsFirstShown()) {
RecordAssistiveCoverage(current_suggester_->GetProposeActionType());
}
return true;
} else if (IsEmojiSuggestAdditionEnabled() &&
IsAllowedUrlOrAppForEmojiSuggestion() &&
emoji_suggester_.Suggest(text_before_cursor)) {
current_suggester_ = &emoji_suggester_;
RecordAssistiveCoverage(current_suggester_->GetProposeActionType());
return true;
}
}
return false;
}
void AssistiveSuggester::AcceptSuggestion(size_t index) {
if (current_suggester_ && current_suggester_->AcceptSuggestion(index)) {
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;
}
} // namespace chromeos