// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/input_method/editor_switch.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/containers/extend.h"
#include "base/containers/fixed_flat_set.h"
#include "base/json/json_reader.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/input_method/editor_consent_enums.h"
#include "chrome/browser/ash/input_method/input_methods_by_language.h"
#include "chrome/browser/ash/input_method/url_utils.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/manta/manta_service_factory.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/language/core/common/locale_util.h"
#include "components/manta/manta_service.h"
#include "extensions/common/constants.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "net/base/network_change_notifier.h"
#include "ui/base/ime/text_input_type.h"
namespace ash::input_method {
namespace {
const char* kWorkspaceDomainsWithPathDenylist[][2] = {
{"calendar.google", ""}, {"docs.google", ""}, {"drive.google", ""},
{"keep.google", ""}, {"mail.google", "/chat"}, {"mail.google", "/mail"},
{"meet.google", ""}, {"script.google", ""}, {"sites.google", ""},
};
constexpr int kTextLengthMaxLimit = 10000;
constexpr char kExperimentName[] = "OrcaEnabled";
constexpr char kImeAllowlistLabel[] = "ime_allowlist";
std::vector<std::string> AllowedInputMethods() {
std::vector<std::string> input_methods = EnglishInputMethods();
if (base::FeatureList::IsEnabled(features::kOrcaAfrikaans)) {
base::Extend(input_methods, AfrikaansInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaDanish)) {
base::Extend(input_methods, DanishInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaDutch)) {
base::Extend(input_methods, DutchInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaFinnish)) {
base::Extend(input_methods, FinnishInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaFrench)) {
base::Extend(input_methods, FrenchInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaGerman)) {
base::Extend(input_methods, GermanInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaItalian)) {
base::Extend(input_methods, ItalianInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaJapanese)) {
base::Extend(input_methods, JapaneseInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaNorwegian)) {
base::Extend(input_methods, NorwegianInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaPolish)) {
base::Extend(input_methods, PolishInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaPortugese)) {
base::Extend(input_methods, PortugeseInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaSpanish)) {
base::Extend(input_methods, SpanishInputMethods());
}
if (base::FeatureList::IsEnabled(features::kOrcaSwedish)) {
base::Extend(input_methods, SwedishInputMethods());
}
return input_methods;
}
manta::FeatureSupportStatus FetchOrcaAccountCapabilityFromMantaService(
Profile* profile) {
if (manta::MantaService* service =
manta::MantaServiceFactory::GetForProfile(profile)) {
return service->SupportsOrca();
}
return manta::FeatureSupportStatus::kUnknown;
}
bool IsCountryAllowed(std::string_view country_code) {
constexpr auto kCountryAllowlist = base::MakeFixedFlatSet<std::string_view>({
"au", "be", "ca", "ch", "cz", "de", "dk", "es", "fi",
"fr", "gb", "ie", "in", "it", "jp", "lu", "mx", "no",
"nz", "nl", "pl", "pt", "se", "us", "za",
});
return kCountryAllowlist.contains(country_code);
}
bool IsInputTypeAllowed(ui::TextInputType type) {
constexpr auto kTextInputTypeAllowlist =
base::MakeFixedFlatSet<ui::TextInputType>(
{ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE, ui::TEXT_INPUT_TYPE_TEXT,
ui::TEXT_INPUT_TYPE_TEXT_AREA});
return kTextInputTypeAllowlist.contains(type);
}
bool IsInputMethodEngineAllowed(const std::vector<std::string>& allowlist,
std::string_view engine_id) {
for (auto& ime : allowlist) {
if (engine_id == ime) {
return true;
}
}
return false;
}
bool IsAppTypeAllowed(chromeos::AppType app_type) {
if (base::FeatureList::IsEnabled(features::kOrcaArc) &&
app_type == chromeos::AppType::ARC_APP) {
return true;
}
constexpr auto kAppTypeDenylist = base::MakeFixedFlatSet<chromeos::AppType>({
chromeos::AppType::ARC_APP,
chromeos::AppType::CROSTINI_APP,
});
return !kAppTypeDenylist.contains(app_type);
}
bool IsTriggerableFromConsentStatus(ConsentStatus consent_status) {
return consent_status == ConsentStatus::kApproved ||
consent_status == ConsentStatus::kPending ||
consent_status == ConsentStatus::kUnset;
}
bool IsUrlAllowed(GURL url) {
if (base::FeatureList::IsEnabled(features::kOrcaOnWorkspace)) {
return true;
}
for (auto& denied_domain_with_path : kWorkspaceDomainsWithPathDenylist) {
if (IsSubDomainWithPathPrefix(url, denied_domain_with_path[0],
denied_domain_with_path[1])) {
return false;
}
}
return true;
}
bool IsAppAllowed(std::string_view app_id) {
constexpr auto kNonWorkspaceAppIdDenylist =
base::MakeFixedFlatSet<std::string_view>({
extension_misc::kFilesManagerAppId,
file_manager::kFileManagerSwaAppId,
});
if (kNonWorkspaceAppIdDenylist.contains(app_id)) {
return false;
}
constexpr auto kWorkspaceAppIdDenylist =
base::MakeFixedFlatSet<std::string_view>({
extension_misc::kGmailAppId,
extension_misc::kCalendarAppId,
extension_misc::kGoogleDocsAppId,
extension_misc::kGoogleSlidesAppId,
extension_misc::kGoogleSheetsAppId,
extension_misc::kGoogleDriveAppId,
extension_misc::kGoogleKeepAppId,
extension_misc::kGoogleMeetPwaAppId,
extension_misc::kGoogleDocsPwaAppId,
extension_misc::kGoogleSheetsPwaAppId,
// App ids in demo mode
extension_misc::kCalendarDemoAppId,
extension_misc::kGoogleDocsDemoAppId,
extension_misc::kGoogleSheetsDemoAppId,
extension_misc::kGoogleSlidesDemoAppId,
web_app::kGmailAppId,
web_app::kGoogleChatAppId,
web_app::kGoogleMeetAppId,
web_app::kGoogleDocsAppId,
web_app::kGoogleSlidesAppId,
web_app::kGoogleSheetsAppId,
web_app::kGoogleDriveAppId,
web_app::kGoogleKeepAppId,
web_app::kGoogleCalendarAppId,
});
return base::FeatureList::IsEnabled(features::kOrcaOnWorkspace) ||
!kWorkspaceAppIdDenylist.contains(app_id);
}
bool IsTriggerableFromTextLength(int text_length) {
return text_length <= kTextLengthMaxLimit;
}
std::vector<std::string> GetAllowedInputMethodEngines() {
std::vector<std::string> allowed_imes = AllowedInputMethods();
// Loads allowed imes from field trials
if (auto parsed = base::JSONReader::Read(
base::GetFieldTrialParamValue(kExperimentName, kImeAllowlistLabel));
parsed.has_value() && parsed->is_list()) {
for (const auto& item : parsed->GetList()) {
if (item.is_string()) {
allowed_imes.push_back(item.GetString());
}
}
}
return allowed_imes;
}
} // namespace
bool IsAllowedForUseInDemoMode(std::string_view country_code) {
return base::FeatureList::IsEnabled(chromeos::features::kOrca) &&
base::FeatureList::IsEnabled(
chromeos::features::kFeatureManagementOrca) &&
IsCountryAllowed(country_code);
}
bool IsAllowedForUseInNonDemoMode(Profile* profile,
std::string_view country_code) {
if (!base::FeatureList::IsEnabled(chromeos::features::kOrca) ||
!base::FeatureList::IsEnabled(
chromeos::features::kFeatureManagementOrca) ||
!IsCountryAllowed(country_code) ||
(base::FeatureList::IsEnabled(
ash::features::kOrcaUseAccountCapabilities) &&
FetchOrcaAccountCapabilityFromMantaService(profile) !=
manta::FeatureSupportStatus::kSupported)) {
return false;
}
// Allow the feature traits to be visible (at the minimum in settings) in
// either one scenario: (1) The feature is not driven by any policy. (2) The
// feature is driven by a policy, and we allow the policy to take effect by
// the feature flag value.
return !profile->GetPrefs()->IsManagedPreference(prefs::kOrcaEnabled) ||
base::FeatureList::IsEnabled(features::kOrcaForManagedUsers);
}
bool IsSystemInEnglishLanguage() {
return g_browser_process != nullptr &&
language::ExtractBaseLanguage(
g_browser_process->GetApplicationLocale()) == "en";
}
EditorSwitch::EditorSwitch(Observer* observer,
Profile* profile,
EditorContext* context)
: observer_(observer),
profile_(profile),
context_(context),
ime_allowlist_(GetAllowedInputMethodEngines()),
last_known_editor_mode_(GetEditorMode()) {}
EditorSwitch::~EditorSwitch() = default;
// TODO: b:362381487 - Rename this method as now this method no longer includes
// the check for policy value.
bool EditorSwitch::IsAllowedForUse() const {
if (base::FeatureList::IsEnabled(chromeos::features::kOrcaDogfood)) {
return true;
}
if (profile_ == nullptr) {
return false;
}
if (chromeos::IsKioskSession()) {
return false;
}
return base::FeatureList::IsEnabled(ash::features::kOrcaSupportDemoMode) &&
ash::DemoSession::IsDeviceInDemoMode()
? IsAllowedForUseInDemoMode(context_->active_country_code())
: IsAllowedForUseInNonDemoMode(profile_,
context_->active_country_code());
}
bool EditorSwitch::IsFeedbackEnabled() const {
if (profile_ == nullptr) {
return false;
}
// If unmanaged, allow Feedback.
if (!profile_->GetPrefs()->IsManagedPreference(prefs::kOrcaFeedbackEnabled)) {
return true;
}
// If managed, check the enablement value.
return profile_->GetPrefs()->GetBoolean(prefs::kOrcaFeedbackEnabled);
}
EditorOpportunityMode EditorSwitch::GetEditorOpportunityMode() const {
if (!IsAllowedForUse()) {
return EditorOpportunityMode::kNotAllowedForUse;
}
if (IsInputTypeAllowed(context_->input_type())) {
return context_->selected_text_length() > 0
? EditorOpportunityMode::kRewrite
: EditorOpportunityMode::kWrite;
}
return EditorOpportunityMode::kInvalidInput;
}
std::vector<EditorBlockedReason> EditorSwitch::GetBlockedReasons() const {
std::vector<EditorBlockedReason> blocked_reasons;
if (base::FeatureList::IsEnabled(chromeos::features::kOrca)) {
if (!IsCountryAllowed(context_->active_country_code())) {
blocked_reasons.push_back(
EditorBlockedReason::kBlockedByUnsupportedRegion);
}
if (profile_->GetPrefs()->IsManagedPreference(prefs::kOrcaEnabled) &&
!profile_->GetPrefs()->GetBoolean(prefs::kOrcaEnabled)) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByPolicy);
}
if (base::FeatureList::IsEnabled(
ash::features::kOrcaUseAccountCapabilities)) {
switch (FetchOrcaAccountCapabilityFromMantaService(profile_)) {
case manta::FeatureSupportStatus::kUnsupported:
blocked_reasons.push_back(
EditorBlockedReason::kBlockedByUnsupportedCapability);
break;
case manta::FeatureSupportStatus::kUnknown:
blocked_reasons.push_back(
EditorBlockedReason::kBlockedByUnknownCapability);
break;
case manta::FeatureSupportStatus::kSupported:
break;
}
}
}
if (!IsTriggerableFromConsentStatus(GetConsentStatusFromInteger(
profile_->GetPrefs()->GetInteger(prefs::kOrcaConsentStatus)))) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByConsent);
}
if (!profile_->GetPrefs()->GetBoolean(prefs::kOrcaEnabled)) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedBySetting);
}
if (!IsTriggerableFromTextLength(context_->selected_text_length())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByTextLength);
}
if (!IsUrlAllowed(context_->active_url())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByUrl);
}
if (!IsAppAllowed(context_->app_id())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByApp);
}
if (!IsAppTypeAllowed(context_->app_type())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByAppType);
}
if (!IsInputMethodEngineAllowed(ime_allowlist_,
context_->active_engine_id())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByInputMethod);
}
if (!IsInputTypeAllowed(context_->input_type())) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByInputType);
}
if (context_->InTabletMode()) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByInvalidFormFactor);
}
if (net::NetworkChangeNotifier::IsOffline()) {
blocked_reasons.push_back(EditorBlockedReason::kBlockedByNetworkStatus);
}
return blocked_reasons;
}
bool EditorSwitch::CanBeTriggered() const {
if (profile_ == nullptr) {
return false;
}
ConsentStatus current_consent_status = GetConsentStatusFromInteger(
profile_->GetPrefs()->GetInteger(prefs::kOrcaConsentStatus));
return IsAllowedForUse() &&
IsInputMethodEngineAllowed(ime_allowlist_,
context_->active_engine_id()) &&
IsInputTypeAllowed(context_->input_type()) &&
IsAppTypeAllowed(context_->app_type()) &&
IsTriggerableFromConsentStatus(current_consent_status) &&
IsUrlAllowed(context_->active_url()) &&
IsAppAllowed(context_->app_id()) &&
!net::NetworkChangeNotifier::IsOffline() &&
!context_->InTabletMode() &&
// user pref value
profile_->GetPrefs()->GetBoolean(prefs::kOrcaEnabled) &&
context_->selected_text_length() <= kTextLengthMaxLimit &&
(!base::FeatureList::IsEnabled(features::kOrcaOnlyInEnglishLocales) ||
IsSystemInEnglishLanguage());
}
EditorMode EditorSwitch::GetEditorMode() const {
if (!IsAllowedForUse()) {
return EditorMode::kHardBlocked;
}
if (!CanBeTriggered()) {
return EditorMode::kSoftBlocked;
}
ConsentStatus current_consent_status = GetConsentStatusFromInteger(
profile_->GetPrefs()->GetInteger(prefs::kOrcaConsentStatus));
if (current_consent_status == ConsentStatus::kPending ||
current_consent_status == ConsentStatus::kUnset) {
return EditorMode::kConsentNeeded;
} else if (context_->selected_text_length() > 0) {
return EditorMode::kRewrite;
} else {
return EditorMode::kWrite;
}
}
void EditorSwitch::OnContextUpdated() {
EditorMode current_mode = GetEditorMode();
if (current_mode != last_known_editor_mode_) {
observer_->OnEditorModeChanged(current_mode);
}
last_known_editor_mode_ = current_mode;
}
} // namespace ash::input_method