| // Copyright 2025 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/lobster/lobster_system_state_provider_impl.h" |
| |
| #include <array> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/constants/generative_ai_country_restrictions.h" |
| #include "ash/public/cpp/lobster/lobster_enums.h" |
| #include "ash/public/cpp/lobster/lobster_system_state.h" |
| #include "ash/public/cpp/lobster/lobster_text_input_context.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chromeos/ash/components/editor_menu/public/cpp/editor_consent_status.h" |
| #include "chromeos/ash/components/specialized_features/feature_access_checker.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "net/base/network_change_notifier.h" |
| #include "ui/base/ime/ash/extension_ime_util.h" |
| #include "ui/base/ime/ash/input_method_manager.h" |
| #include "ui/display/screen.h" |
| #include "ui/display/tablet_state.h" |
| |
| namespace { |
| |
| ash::LobsterConsentStatus GetConsentStatusFromInteger(int status_value) { |
| switch (status_value) { |
| case base::to_underlying( |
| chromeos::editor_menu::EditorConsentStatus::kUnset): |
| case base::to_underlying( |
| chromeos::editor_menu::EditorConsentStatus::kPending): |
| return ash::LobsterConsentStatus::kUnset; |
| case base::to_underlying( |
| chromeos::editor_menu::EditorConsentStatus::kApproved): |
| return ash::LobsterConsentStatus::kApproved; |
| case base::to_underlying( |
| chromeos::editor_menu::EditorConsentStatus::kDeclined): |
| return ash::LobsterConsentStatus::kDeclined; |
| default: |
| LOG(ERROR) << "Invalid consent status: " << status_value; |
| // For any of the invalid states, treat the consent status as unset. |
| return ash::LobsterConsentStatus::kUnset; |
| } |
| } |
| |
| std::string GetCurrentImeEngineId() { |
| ash::input_method::InputMethodManager* input_method_manager = |
| ash::input_method::InputMethodManager::Get(); |
| if (input_method_manager == nullptr || |
| input_method_manager->GetActiveIMEState() == nullptr) { |
| return ""; |
| } |
| return ash::extension_ime_util::GetComponentIDByInputMethodID( |
| input_method_manager->GetActiveIMEState()->GetCurrentInputMethod().id()); |
| } |
| |
| bool IsInputTypeAllowed(ui::TextInputType type) { |
| static 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 IsImeAllowed(std::string_view current_ime_id) { |
| static constexpr auto kImeAllowlist = |
| base::MakeFixedFlatSet<std::string_view>({ |
| "xkb:ca:eng:eng", // Canada |
| "xkb:gb::eng", // UK |
| "xkb:gb:extd:eng", // UK Extended |
| "xkb:gb:dvorak:eng", // UK Dvorak |
| "xkb:in::eng", // India |
| "xkb:pk::eng", // Pakistan |
| "xkb:us:altgr-intl:eng", // US Extended |
| "xkb:us:colemak:eng", // US Colemak |
| "xkb:us:dvorak:eng", // US Dvorak |
| "xkb:us:dvp:eng", // US Programmer Dvorak |
| "xkb:us:intl_pc:eng", // US Intl (PC) |
| "xkb:us:intl:eng", // US Intl |
| "xkb:us:workman-intl:eng", // US Workman Intl |
| "xkb:us:workman:eng", // US Workman |
| "xkb:us::eng", // US |
| "xkb:za:gb:eng" // South Africa |
| }); |
| |
| return kImeAllowlist.contains(current_ime_id); |
| } |
| |
| specialized_features::FeatureAccessConfig CreateFeatureAccessConfig() { |
| specialized_features::FeatureAccessConfig config; |
| config.disabled_in_kiosk_mode = true; |
| |
| // Dogfood devices ignore all other checks. |
| if (base::FeatureList::IsEnabled(ash::features::kLobsterDogfood)) { |
| return config; |
| } |
| |
| config.feature_flag = &ash::features::kLobster; |
| config.feature_management_flag = &ash::features::kFeatureManagementLobster; |
| config.capability_callback = |
| base::BindRepeating([](AccountCapabilities capabilities) { |
| return capabilities.can_use_manta_service(); |
| }); |
| config.country_codes = ash::GetGenerativeAiCountryAllowlist(); |
| return config; |
| } |
| |
| } // namespace |
| |
| LobsterSystemStateProviderImpl::LobsterSystemStateProviderImpl( |
| PrefService* pref, |
| signin::IdentityManager* identity_manager, |
| bool is_in_demo_mode) |
| : pref_(pref), |
| access_checker_(CreateFeatureAccessConfig(), |
| pref_, |
| identity_manager, |
| /*variations_service_callback=*/base::BindRepeating([]() { |
| return g_browser_process->variations_service(); |
| })), |
| is_in_demo_mode_(is_in_demo_mode) {} |
| |
| LobsterSystemStateProviderImpl::~LobsterSystemStateProviderImpl() = default; |
| |
| ash::LobsterSystemState LobsterSystemStateProviderImpl::GetSystemState( |
| const ash::LobsterTextInputContext& text_input_context) { |
| ash::LobsterSystemState system_state(ash::LobsterStatus::kEnabled, |
| /*failed_checks=*/{}); |
| |
| specialized_features::FeatureAccessFailureSet access_checker_failure_set = |
| access_checker_.Check(); |
| |
| // Performs feature flag check |
| if (access_checker_failure_set.Has( |
| specialized_features::FeatureAccessFailure::kFeatureFlagDisabled)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kInvalidFeatureFlags); |
| } |
| |
| // Performs a hardware check |
| if (access_checker_failure_set.Has( |
| specialized_features::FeatureAccessFailure:: |
| kFeatureManagementCheckFailed)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kUnsupportedHardware); |
| } |
| |
| // Performs a location check |
| if (access_checker_failure_set.Has( |
| specialized_features::FeatureAccessFailure::kCountryCheckFailed)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put(ash::LobsterSystemCheck::kInvalidRegion); |
| } |
| |
| // TODO: b:406915099 - Migrate demo mode check into the shared feature checker module. |
| // Performs account capabilities check in non-demo mode only |
| if (!is_in_demo_mode_ && access_checker_failure_set.Has( |
| specialized_features::FeatureAccessFailure:: |
| kAccountCapabilitiesCheckFailed)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kInvalidAccountCapabilities); |
| } |
| |
| // Performs a kiosk mode check |
| if (access_checker_failure_set.Has( |
| specialized_features::FeatureAccessFailure:: |
| kDisabledInKioskModeCheckFailed)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kUnsupportedInKioskMode); |
| } |
| |
| if (!IsInputTypeAllowed(text_input_context.text_input_type)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put(ash::LobsterSystemCheck::kInvalidInputField); |
| } |
| |
| if (!ash::features::IsLobsterEnabledForManagedUsers() && |
| pref_->IsManagedPreference( |
| ash::prefs::kLobsterEnterprisePolicySettings)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kForcedDisabledOnManagedUsers); |
| } |
| |
| if (pref_->GetInteger(ash::prefs::kLobsterEnterprisePolicySettings) == |
| base::to_underlying(ash::LobsterEnterprisePolicyValue::kDisabled)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put(ash::LobsterSystemCheck::kUnsupportedPolicy); |
| } |
| |
| if (!pref_->GetBoolean(ash::prefs::kLobsterEnabled)) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put(ash::LobsterSystemCheck::kSettingsOff); |
| } |
| |
| // Performs a network check |
| if (net::NetworkChangeNotifier::IsOffline()) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kNoInternetConnection); |
| } |
| |
| // Performs a tablet mode check |
| if (is_in_tablet_mode_) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kUnsupportedFormFactor); |
| } |
| |
| // Performs an IME check |
| if (ash::features::IsLobsterDisabledByInvalidIME() && |
| !IsImeAllowed(GetCurrentImeEngineId())) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| system_state.failed_checks.Put( |
| ash::LobsterSystemCheck::kInvalidInputMethod); |
| } |
| |
| ash::LobsterConsentStatus consent_status = GetConsentStatusFromInteger( |
| pref_->GetInteger(ash::prefs::kOrcaConsentStatus)); |
| |
| if (consent_status == ash::LobsterConsentStatus::kDeclined) { |
| system_state.failed_checks.Put(ash::LobsterSystemCheck::kInvalidConsent); |
| } |
| |
| if (!system_state.failed_checks.empty()) { |
| system_state.status = ash::LobsterStatus::kBlocked; |
| } else if (consent_status == ash::LobsterConsentStatus::kUnset) { |
| system_state.status = ash::LobsterStatus::kConsentNeeded; |
| } else { |
| system_state.status = ash::LobsterStatus::kEnabled; |
| } |
| |
| return system_state; |
| } |
| |
| void LobsterSystemStateProviderImpl::OnDisplayTabletStateChanged( |
| display::TabletState state) { |
| is_in_tablet_mode_ = (state == display::TabletState::kInTabletMode); |
| } |