blob: 4f20f4adf89706528f8ad8d4d3c91d4b03275067 [file] [log] [blame]
// 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.
#include "chrome/browser/ash/login/quickstart_controller.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/bluetooth_config_service.h"
#include "base/check.h"
#include "base/logging.h"
#include "chrome/browser/ash/login/demo_mode/demo_setup_controller.h"
#include "chrome/browser/ash/login/oobe_quick_start/connectivity/target_device_connection_broker.h"
#include "chrome/browser/ash/login/oobe_quick_start/oobe_quick_start_pref_names.h"
#include "chrome/browser/ash/login/oobe_quick_start/target_device_bootstrap_controller.h"
#include "chrome/browser/ash/login/oobe_screen.h"
#include "chrome/browser/ash/login/wizard_context.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/login/login_display_host.h"
#include "chrome/browser/ui/webui/ash/login/add_child_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/consumer_update_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/error_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/gaia_info_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/gaia_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/network_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/online_login_utils.h"
#include "chrome/browser/ui/webui/ash/login/quick_start_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/update_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/user_creation_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/welcome_screen_handler.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_type_pattern.h"
#include "chromeos/ash/components/quick_start/logging.h"
#include "chromeos/ash/components/quick_start/quick_start_metrics.h"
#include "chromeos/ash/components/quick_start/types.h"
#include "components/account_id/account_id.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/session_manager_types.h"
#include "components/user_manager/user_type.h"
namespace ash::quick_start {
namespace {
using bluetooth_config::mojom::BluetoothDevicePropertiesPtr;
using bluetooth_config::mojom::BluetoothSystemState;
std::string GetBluetoothStateString(BluetoothSystemState system_state) {
switch (system_state) {
case BluetoothSystemState::kDisabled:
return "Bluetooth is turned off.";
case BluetoothSystemState::kDisabling:
return "Bluetooth is in the process of turning off.";
case BluetoothSystemState::kEnabled:
return "Bluetooth is turned on.";
case BluetoothSystemState::kEnabling:
return "Bluetooth is in the process of turning on.";
case BluetoothSystemState::kUnavailable:
return "Device does not have access to Bluetooth.";
default:
return "Unknown bluetooth state!";
}
}
std::optional<QuickStartController::EntryPoint> EntryPointFromScreen(
OobeScreenId screen) {
if (screen.name == WelcomeScreenHandler::kScreenId.name) {
return QuickStartController::EntryPoint::WELCOME_SCREEN;
} else if (screen.name == NetworkScreenHandler::kScreenId.name) {
return QuickStartController::EntryPoint::NETWORK_SCREEN;
} else if (screen.name == GaiaInfoScreenHandler::kScreenId.name) {
return QuickStartController::EntryPoint::GAIA_INFO_SCREEN;
} else if (screen.name == GaiaScreenHandler::kScreenId.name) {
return QuickStartController::EntryPoint::GAIA_SCREEN;
}
return std::nullopt;
}
QuickStartMetrics::ScreenName ScreenNameFromOobeScreenId(
std::optional<OobeScreenId> screen_id) {
if (!screen_id.has_value()) {
return QuickStartMetrics::ScreenName::kNone;
} else if (screen_id == WelcomeView::kScreenId) {
return QuickStartMetrics::ScreenName::kWelcomeScreen;
} else if (screen_id == NetworkScreenView::kScreenId) {
return QuickStartMetrics::ScreenName::kNetworkScreen;
} else if (screen_id == GaiaInfoScreenView::kScreenId) {
return QuickStartMetrics::ScreenName::kGaiaInfoScreen;
} else if (screen_id == GaiaView::kScreenId) {
return QuickStartMetrics::ScreenName::kGaiaScreen;
} else if (screen_id == UpdateView::kScreenId) {
return QuickStartMetrics::ScreenName::
kCheckingForUpdateAndDeterminingDeviceConfiguration;
} else if (screen_id == UserCreationView::kScreenId) {
return QuickStartMetrics::ScreenName::kChooseChromebookSetup;
} else if (screen_id == ConsumerUpdateScreenView::kScreenId) {
return QuickStartMetrics::ScreenName::kConsumerUpdate;
} else if (screen_id == AddChildScreenView::kScreenId) {
return QuickStartMetrics::ScreenName::kAddChild;
} else {
return QuickStartMetrics::ScreenName::kOther;
}
}
QuickStartMetrics::ScreenName ScreenNameFromUiState(
std::optional<QuickStartController::UiState> ui_state,
QuickStartController::ControllerState controller_state) {
if (!ui_state.has_value()) {
return QuickStartMetrics::ScreenName::kNone;
}
switch (ui_state.value()) {
case QuickStartController::UiState::SHOWING_QR:
[[fallthrough]];
case QuickStartController::UiState::SHOWING_PIN:
return QuickStartMetrics::ScreenName::kQSSetUpWithAndroidPhone;
case QuickStartController::UiState::CONNECTING_TO_WIFI:
return QuickStartMetrics::ScreenName::kQSConnectingToWifi;
case QuickStartController::UiState::WIFI_CREDENTIALS_RECEIVED:
return QuickStartMetrics::ScreenName::kQSWifiCredentialsReceived;
case QuickStartController::UiState::CONFIRM_GOOGLE_ACCOUNT:
return QuickStartMetrics::ScreenName::kQSSelectGoogleAccount;
case QuickStartController::UiState::SIGNING_IN:
return QuickStartMetrics::ScreenName::kQSGettingGoogleAccountInfo;
case QuickStartController::UiState::CREATING_ACCOUNT:
return QuickStartMetrics::ScreenName::kQSCreatingAccount;
case QuickStartController::UiState::SETUP_COMPLETE:
return QuickStartMetrics::ScreenName::kQSComplete;
case QuickStartController::UiState::FALLBACK_URL_FLOW:
return QuickStartMetrics::ScreenName::kQSFallbackURL;
case QuickStartController::UiState::CONNECTING_TO_PHONE:
return QuickStartMetrics::ScreenName::kQSConnectingToPhone;
case QuickStartController::UiState::EXIT_SCREEN:
[[fallthrough]];
case QuickStartController::UiState::SHOWING_BLUETOOTH_DIALOG:
[[fallthrough]];
default:
return QuickStartMetrics::ScreenName::kNone;
}
}
bool IsConnectedToWiFi() {
NetworkStateHandler* nsh = NetworkHandler::Get()->network_state_handler();
return nsh->ConnectedNetworkByType(NetworkTypePattern::WiFi()) != nullptr;
}
TargetDeviceBootstrapController::ConnectionClosedReason
ConnectionClosedReasonFromAbortFlowReason(
QuickStartController::AbortFlowReason reason) {
switch (reason) {
case QuickStartController::AbortFlowReason::USER_CLICKED_CANCEL:
[[fallthrough]];
case QuickStartController::AbortFlowReason::USER_CLICKED_BACK:
[[fallthrough]];
case QuickStartController::AbortFlowReason::SIGNIN_SCHOOL:
[[fallthrough]];
case QuickStartController::AbortFlowReason::ADD_CHILD:
[[fallthrough]];
case QuickStartController::AbortFlowReason::ENTERPRISE_ENROLLMENT:
return TargetDeviceBootstrapController::ConnectionClosedReason::
kUserAborted;
case QuickStartController::AbortFlowReason::ERROR:
return TargetDeviceBootstrapController::ConnectionClosedReason::
kUnknownError;
}
}
} // namespace
QuickStartController::QuickStartController() {
metrics_ = std::make_unique<QuickStartMetrics>();
if (g_browser_process->local_state()->GetBoolean(
prefs::kShouldResumeQuickStartAfterReboot)) {
QS_LOG(INFO) << "This session should resume Quick Start after a reboot.";
should_resume_quick_start_after_update_ = true;
// Clear pref right away to prevent bad state in case of crash.
g_browser_process->local_state()->ClearPref(
prefs::kShouldResumeQuickStartAfterReboot);
}
// Main feature flag
if (!features::IsCrossDeviceFeatureSuiteAllowed()) {
if (should_resume_quick_start_after_update_) {
ForceEnableQuickStart();
}
return;
}
// QuickStart may not be available on the login screen.
if (session_manager::SessionManager::Get()->session_state() !=
session_manager::SessionState::OOBE &&
!features::IsOobeQuickStartOnLoginScreenEnabled()) {
return;
}
// A guest session state is SessionState::OOBE if there are no other users
// added. Quick Start is not available in this case.
if (ProfileManager::GetActiveUserProfile()->IsGuestSession()) {
return;
}
InitTargetDeviceBootstrapController();
StartObservingBluetoothState();
}
QuickStartController::~QuickStartController() {
if (bootstrap_controller_) {
bootstrap_controller_->RemoveObserver(this);
}
}
void QuickStartController::AttachFrontend(
QuickStartController::UiDelegate* delegate) {
CHECK(ui_delegates_.empty()) << "Only one UI delegate shall be attached!";
ui_delegates_.AddObserver(delegate);
}
void QuickStartController::DetachFrontend(
QuickStartController::UiDelegate* delegate) {
ui_delegates_.RemoveObserver(delegate);
}
void QuickStartController::MaybeRecordQuickStartScreenOpened(
QuickStartController::UiState new_ui) {
QuickStartMetrics::ScreenName screen_name =
ScreenNameFromUiState(new_ui, controller_state_);
if (screen_name != QuickStartMetrics::ScreenName::kNone) {
metrics_->QuickStartMetrics::RecordScreenOpened(screen_name);
}
}
void QuickStartController::MaybeRecordQuickStartScreenAdvanced(
std::optional<QuickStartController::UiState> closed_ui) {
QuickStartMetrics::ScreenName screen_name =
ScreenNameFromUiState(closed_ui, controller_state_);
if (screen_name != QuickStartMetrics::ScreenName::kNone) {
metrics_->RecordScreenClosed(
screen_name, QuickStartMetrics::ScreenClosedReason::kAdvancedInFlow);
}
}
void QuickStartController::UpdateUiState(UiState ui_state) {
QS_LOG(INFO) << "Updating UI state to " << ui_state;
if (is_transitioning_to_quick_start_screen_) {
is_transitioning_to_quick_start_screen_ = false;
QuickStartMetrics::ScreenName previous_screen_name =
ScreenNameFromOobeScreenId(previous_screen_);
metrics_->RecordScreenClosed(
previous_screen_name,
QuickStartMetrics::ScreenClosedReason::kAdvancedInFlow);
} else {
MaybeRecordQuickStartScreenAdvanced(ui_state_);
}
ui_state_ = ui_state;
MaybeRecordQuickStartScreenOpened(ui_state);
CHECK(!ui_delegates_.empty()) << "ui_delegates_ is empty";
for (auto& delegate : ui_delegates_) {
delegate.OnUiUpdateRequested(ui_state_.value());
}
}
void QuickStartController::ForceEnableQuickStart() {
if (bootstrap_controller_) {
return;
}
InitTargetDeviceBootstrapController();
StartObservingBluetoothState();
}
void QuickStartController::DetermineEntryPointVisibility(
EntryPointButtonVisibilityCallback callback) {
// Bootstrap controller is only instantiated when the feature is enabled (also
// via the keyboard shortcut. See |ForceEnableQuickStart|.)
if (!bootstrap_controller_) {
std::move(callback).Run(/*visible=*/false);
return;
}
// QuickStart should not be enabled for Demo mode or OS Install flows
if (DemoSetupController::IsOobeDemoSetupFlowInProgress() ||
ash::switches::IsOsInstallAllowed()) {
std::move(callback).Run(/*visible=*/false);
return;
}
// If the flow is ongoing, entry points are hidden.
if (IsSetupOngoing()) {
std::move(callback).Run(/*visible=*/false);
return;
}
bootstrap_controller_->GetFeatureSupportStatusAsync(base::BindRepeating(
&QuickStartController::OnGetQuickStartFeatureSupportStatus,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void QuickStartController::AbortFlow(AbortFlowReason reason) {
CHECK(bootstrap_controller_) << "Missing bootstrap_controller_";
QuickStartMetrics::RecordAbortFlowReason(reason);
// Screen is closed when flow aborts on these screens.
if (current_screen_ == QuickStartScreenHandler::kScreenId ||
current_screen_ == NetworkScreenHandler::kScreenId) {
QuickStartMetrics::ScreenName current_screen_name =
current_screen_ == QuickStartScreenHandler::kScreenId
? ScreenNameFromUiState(ui_state_, controller_state_)
: ScreenNameFromOobeScreenId(current_screen_.value());
metrics_->RecordScreenClosed(
current_screen_name,
QuickStartMetrics::MapAbortFlowReasonToScreenClosedReason(reason));
}
// If user proceeds with school, enterprise, or unicorn setup, allow source
// device to gracefully close connection and show "setup complete" UI.
constexpr AbortFlowReason kUnsupportedUserTypes[] = {
AbortFlowReason::ENTERPRISE_ENROLLMENT, AbortFlowReason::SIGNIN_SCHOOL,
AbortFlowReason::ADD_CHILD};
if (base::Contains(kUnsupportedUserTypes, reason)) {
QS_LOG(INFO) << "Aborting flow due to unsupported user type: " << reason;
bootstrap_controller_->OnSetupComplete();
return;
}
bootstrap_controller_->CloseOpenConnections(
ConnectionClosedReasonFromAbortFlowReason(reason));
bootstrap_controller_->StopAdvertising();
bootstrap_controller_->Cleanup();
ResetState();
// Triggers a screen exit if there is a UiDelegate driving the UI.
if (!ui_delegates_.empty()) {
CHECK(current_screen_ == QuickStartScreenHandler::kScreenId ||
current_screen_ == NetworkScreenHandler::kScreenId)
<< "Unexpected current_screen_.";
ui_delegates_.begin()->OnUiUpdateRequested(UiState::EXIT_SCREEN);
}
}
QuickStartController::EntryPoint QuickStartController::GetExitPoint() {
return exit_point_.value();
}
void QuickStartController::PrepareForUpdate(bool is_forced) {
QuickStartMetrics::RecordUpdateStarted(is_forced);
bootstrap_controller_->PrepareForUpdate();
}
void QuickStartController::ResumeSessionAfterCancelledUpdate() {
QuickStartMetrics::RecordConsumerUpdateCancelled();
LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_setup_ongoing = true;
controller_state_ = ControllerState::WAITING_TO_RESUME_AFTER_UPDATE;
}
void QuickStartController::RecordFlowFinished() {
// State has already been reset when SETUP_COMPLETE UI is shown.
// We still want to record how long user viewed this final UI.
metrics_->RecordScreenClosed(
QuickStartMetrics::ScreenName::kQSComplete,
QuickStartMetrics::ScreenClosedReason::kSetupComplete);
}
void QuickStartController::InitTargetDeviceBootstrapController() {
CHECK(LoginDisplayHost::default_host()) << "Missing LoginDisplayHost";
CHECK(!bootstrap_controller_) << "Expected to not have bootstrap_controller_";
if (should_resume_quick_start_after_update_) {
LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_setup_ongoing = true;
controller_state_ = ControllerState::WAITING_TO_RESUME_AFTER_UPDATE;
}
StartObservingScreenTransitions();
LoginDisplayHost::default_host()->GetWizardContext()->quick_start_enabled =
true;
bootstrap_controller_ =
LoginDisplayHost::default_host()->GetQuickStartBootstrapController();
// Start observing and determine the discoverable name.
bootstrap_controller_->AddObserver(this);
}
void QuickStartController::OnGetQuickStartFeatureSupportStatus(
EntryPointButtonVisibilityCallback set_button_visibility_callback,
TargetDeviceConnectionBroker::FeatureSupportStatus status) {
// Maybe prevent a delayed repeated call from TargetDeviceConnectionBroker by
// re-checking that the flow is not ongoing.
const bool visible =
!IsSetupOngoing() &&
status == TargetDeviceConnectionBroker::FeatureSupportStatus::kSupported;
// Make the entry point button visible when supported, otherwise keep hidden.
std::move(set_button_visibility_callback).Run(visible);
}
void QuickStartController::OnStatusChanged(
const TargetDeviceBootstrapController::Status& status) {
using Step = TargetDeviceBootstrapController::Step;
using ErrorCode = TargetDeviceBootstrapController::ErrorCode;
switch (status.step) {
case Step::ADVERTISING_WITH_QR_CODE:
controller_state_ = ControllerState::ADVERTISING;
CHECK(absl::holds_alternative<QRCode>(status.payload))
<< "Missing expected QR Code data";
qr_code_ = absl::get<QRCode>(status.payload);
UpdateUiState(UiState::SHOWING_QR);
return;
case Step::ADVERTISING_WITHOUT_QR_CODE:
UpdateUiState(UiState::CONNECTING_TO_PHONE);
return;
case Step::PIN_VERIFICATION:
CHECK(absl::holds_alternative<PinString>(status.payload))
<< "Missing expected PIN string";
pin_ = *absl::get<PinString>(status.payload);
CHECK_EQ(pin_.value().length(), 4UL);
UpdateUiState(UiState::SHOWING_PIN);
return;
case Step::CONNECTED:
controller_state_ = ControllerState::CONNECTED;
OnPhoneConnectionEstablished();
return;
case Step::REQUESTING_WIFI_CREDENTIALS:
CHECK(did_request_wifi_credentials_) << "Unrequested WiFi credentials!";
UpdateUiState(UiState::CONNECTING_TO_WIFI);
return;
case Step::WIFI_CREDENTIALS_RECEIVED:
CHECK(did_request_wifi_credentials_) << "Unrequested WiFi credentials!";
CHECK(absl::holds_alternative<mojom::WifiCredentials>(status.payload))
<< "Missing expected WifiCredentials";
LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_wifi_credentials =
absl::get<mojom::WifiCredentials>(status.payload);
ABSL_FALLTHROUGH_INTENDED;
case Step::EMPTY_WIFI_CREDENTIALS_RECEIVED:
CHECK(did_request_wifi_credentials_) << "Unrequested WiFi credentials!";
UpdateUiState(UiState::WIFI_CREDENTIALS_RECEIVED);
return;
case Step::REQUESTING_GOOGLE_ACCOUNT_INFO:
CHECK(did_request_account_info_) << "Unrequested account info received!";
return;
case Step::GOOGLE_ACCOUNT_INFO_RECEIVED:
CHECK(did_request_account_info_) << "Unrequested account info received!";
CHECK(absl::holds_alternative<EmailString>(status.payload))
<< "Missing expected EmailString";
// If there aren't any accounts on the phone, the flow is aborted.
if (absl::get<EmailString>(status.payload)->empty()) {
QS_LOG(ERROR) << "No account on Android phone. No email received.";
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/false, /*failure_reason=*/QuickStartMetrics::
GaiaTransferResultFailureReason::kNoAccountOnPhone);
AbortFlow(AbortFlowReason::ERROR);
return;
}
// Populate the 'UserInfo' that is shown on the UI and start the transfer.
user_info_.email = *absl::get<EmailString>(status.payload);
UpdateUiState(UiState::SIGNING_IN);
did_request_account_transfer_ = true;
bootstrap_controller_->AttemptGoogleAccountTransfer();
return;
case Step::TRANSFERRING_GOOGLE_ACCOUNT_DETAILS:
CHECK(did_request_account_transfer_) << "Unrequested account transfer!";
// Intermediate state. Nothing to do.
if (controller_state_ != ControllerState::CONNECTED) {
QS_LOG(ERROR) << "Expected controller_state_ to be CONNECTED. Actual "
"controller_state_: "
<< controller_state_;
AbortFlow(AbortFlowReason::ERROR);
}
return;
case Step::TRANSFERRED_GOOGLE_ACCOUNT_DETAILS:
CHECK(did_request_account_transfer_) << "Unrequested account transfer!";
if (controller_state_ != ControllerState::CONNECTED) {
QS_LOG(ERROR) << "Expected controller_state_ to be CONNECTED. Actual "
"controller_state_: "
<< controller_state_;
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/false, /*failure_reason=*/QuickStartMetrics::
GaiaTransferResultFailureReason::kConnectionLost);
AbortFlow(AbortFlowReason::ERROR);
return;
}
if (absl::holds_alternative<
TargetDeviceBootstrapController::GaiaCredentials>(
status.payload)) {
const TargetDeviceBootstrapController::GaiaCredentials gaia_creds =
absl::get<TargetDeviceBootstrapController::GaiaCredentials>(
status.payload);
if (!gaia_creds.auth_code.empty()) {
QS_LOG(INFO) << "Successfully received an OAuth authorization code.";
OnOAuthTokenReceived(gaia_creds);
} else {
QS_LOG(INFO) << "QuickStart flow will continue via fallback URL";
CHECK(!gaia_creds.fallback_url_path->empty())
<< "Fallback URL path empty";
fallback_url_ = gaia_creds.fallback_url_path.value();
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/false, /*failure_reason=*/QuickStartMetrics::
GaiaTransferResultFailureReason::kFallbackURLRequired);
controller_state_ = ControllerState::FALLBACK_URL_FLOW_ON_GAIA_SCREEN;
UpdateUiState(UiState::FALLBACK_URL_FLOW);
}
} else {
CHECK(absl::holds_alternative<ErrorCode>(status.payload))
<< "Missing expected ErrorCode";
QS_LOG(ERROR) << "Error receiving FIDO assertion. Error Code = "
<< static_cast<int>(absl::get<ErrorCode>(status.payload));
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/false, /*failure_reason=*/QuickStartMetrics::
GaiaTransferResultFailureReason::kErrorReceivingFIDOAssertion);
// TODO(b:286873060) - Implement retry mechanism/graceful exit.
NOTIMPLEMENTED();
}
return;
case Step::NONE:
// Indicates we've stopped advertising and are not connected to the source
// device. No action required.
return;
case Step::ERROR:
if (absl::holds_alternative<ErrorCode>(status.payload)) {
QS_LOG(ERROR) << absl::get<ErrorCode>(status.payload);
} else {
QS_LOG(ERROR) << "Missing ErrorCode.";
}
AbortFlow(AbortFlowReason::ERROR);
return;
case Step::FLOW_ABORTED:
return;
case Step::SETUP_COMPLETE:
ResetState();
return;
}
}
void QuickStartController::OnCurrentScreenChanged(OobeScreenId previous_screen,
OobeScreenId current_screen) {
current_screen_ = current_screen;
previous_screen_ = previous_screen;
QS_LOG(INFO) << "Current screen changed from " << previous_screen << " to "
<< current_screen;
if (current_screen_ == QuickStartScreenHandler::kScreenId) {
// Just switched into the quick start screen. The ScreenOpened and
// ScreenClosed metrics are recorded from UpdateUiState() in this case.
is_transitioning_to_quick_start_screen_ = true;
HandleTransitionToQuickStartScreen();
} else if (IsSetupOngoing()) {
QuickStartMetrics::ScreenName previous_screen_name =
previous_screen == QuickStartScreenHandler::kScreenId
? ScreenNameFromUiState(ui_state_, controller_state_)
: ScreenNameFromOobeScreenId(previous_screen);
metrics_->RecordScreenClosed(
previous_screen_name,
QuickStartMetrics::ScreenClosedReason::kAdvancedInFlow);
metrics_->RecordScreenOpened(ScreenNameFromOobeScreenId(current_screen));
// Detect when the user leaves the Gaia screen during the fallback flow.
if (controller_state_ ==
ControllerState::FALLBACK_URL_FLOW_ON_GAIA_SCREEN &&
previous_screen_ == GaiaScreenHandler::kScreenId) {
QS_LOG(INFO) << "Gaia screen dismissed while handling fallback URL flow.";
if (current_screen_ == ErrorScreenHandler::kScreenId) {
AbortFlow(AbortFlowReason::ERROR);
} else {
AbortFlow(AbortFlowReason::USER_CLICKED_BACK);
}
}
}
}
void QuickStartController::OnDestroyingOobeUI() {
observation_.Reset();
}
void QuickStartController::OnOAuthTokenReceived(
TargetDeviceBootstrapController::GaiaCredentials gaia_creds) {
gaia_creds_ = gaia_creds;
if (gaia_creds_.gaia_id.empty()) {
QS_LOG(ERROR) << "Obfuscated Gaia ID missing!";
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/false, /*failure_reason=*/QuickStartMetrics::
GaiaTransferResultFailureReason::kObfuscatedGaiaIdMissing);
AbortFlow(AbortFlowReason::ERROR);
return;
}
FinishAccountCreation();
}
void QuickStartController::StartObservingScreenTransitions() {
CHECK(LoginDisplayHost::default_host()) << "Missing LoginDisplayHost";
CHECK(LoginDisplayHost::default_host()->GetOobeUI()) << "Missing Oobe UI";
// Do not observe transitions when the OOBE overlay debugger is enabled since
// the debugger 'forces' the screen for each state and this breaks the logic.
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kShowOobeDevOverlay)) {
return;
}
observation_.Observe(LoginDisplayHost::default_host()->GetOobeUI());
}
void QuickStartController::HandleTransitionToQuickStartScreen() {
CHECK(current_screen_ == QuickStartScreenHandler::kScreenId)
<< "Unexpected current_screen_";
// No ongoing setup. Entering the screen via entry point.
if (!IsSetupOngoing()) {
// Initially there is no UI step. TargetDeviceBootstrapController
// then determines whether a loading spinner (for the PIN case),
// or the QR code will be shown. If bluetooth is not turned on, a dialog
// is shown asking the user for their permission first.
CHECK(!ui_state_.has_value()) << "Found UI state without ongoing setup!";
// Keep track of where the flow originated.
CHECK(!entry_point_.has_value()) << "Entry point without ongoing setup";
const auto entry_point = EntryPointFromScreen(previous_screen_.value());
CHECK(entry_point.has_value()) << "Unknown entry point!";
exit_point_ = entry_point_ = entry_point;
QuickStartMetrics::RecordEntryPoint(entry_point.value());
// Set the QuickStart flow as ongoing for the rest of the system.
LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_setup_ongoing = true;
if (IsBluetoothDisabled()) {
controller_state_ = ControllerState::WAITING_FOR_BLUETOOTH_PERMISSION;
UpdateUiState(UiState::SHOWING_BLUETOOTH_DIALOG);
return;
}
StartAdvertising();
} else if (controller_state_ ==
ControllerState::WAITING_TO_RESUME_AFTER_UPDATE) {
exit_point_ = EntryPoint::GAIA_INFO_SCREEN;
QuickStartMetrics::RecordEntryPoint(EntryPoint::AUTO_RESUME_AFTER_UPDATE);
// It's possible the local state still needs to be cleared if an update was
// initiated but cancelled. We can't check/clear the state immediately upon
// cancelling the update since it's possible it happens before the target
// device persists this pref to local state.
if (g_browser_process->local_state()->GetBoolean(
prefs::kShouldResumeQuickStartAfterReboot)) {
g_browser_process->local_state()->ClearPref(
prefs::kShouldResumeQuickStartAfterReboot);
}
if (IsBluetoothDisabled()) {
controller_state_ = ControllerState::WAITING_FOR_BLUETOOTH_PERMISSION;
UpdateUiState(UiState::SHOWING_BLUETOOTH_DIALOG);
return;
}
StartAdvertising();
} else {
// If the setup has finished, transitioning to QuickStart should
// show the last step of the flow.
if (controller_state_ == ControllerState::SETUP_COMPLETE) {
UpdateUiState(UiState::SETUP_COMPLETE);
SavePhoneInstanceID();
bootstrap_controller_->OnSetupComplete();
QuickStartMetrics::RecordSetupComplete();
return;
}
// The flow must be resuming after reaching the GaiaInfoScreen or
// GaiaScreen. Note the the GaiaInfoScreen/GaiaScreen is technically never
// shown when it switches to QuickStart, so |previous_screen_| is one of the
// many screens that may have appeared up to this point.
// TODO(b:283965994) - Improve the resume logic.
// OOBE flow cannot go back after enrollment checks, update exit point.
exit_point_ = QuickStartController::EntryPoint::GAIA_INFO_SCREEN;
if (controller_state_ != ControllerState::CONNECTED) {
QS_LOG(ERROR) << "Expected controller_state_ to be CONNECTED. Actual "
"controller_state_: "
<< controller_state_;
AbortFlow(AbortFlowReason::ERROR);
return;
}
CHECK(LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_setup_ongoing)
<< "Expected quick_start_setup_ongoing";
StartAccountTransfer();
}
}
void QuickStartController::StartAccountTransfer() {
UpdateUiState(UiState::CONFIRM_GOOGLE_ACCOUNT);
QuickStartMetrics::RecordGaiaTransferStarted();
did_request_account_info_ = true;
bootstrap_controller_->RequestGoogleAccountInfo();
}
void QuickStartController::OnPhoneConnectionEstablished() {
bootstrap_controller_->StopAdvertising();
// If cancelling the flow would end on the welcome or network screen,
// we are still early in the OOBE flow. Transfer WiFi creds if not already
// connected.
if (exit_point_ == EntryPoint::WELCOME_SCREEN ||
exit_point_ == EntryPoint::NETWORK_SCREEN) {
if (IsConnectedToWiFi()) {
// This will cause the QuickStartScreen to exit and the NetworkScreen
// will be shown next.
UpdateUiState(UiState::WIFI_CREDENTIALS_RECEIVED);
} else {
did_request_wifi_credentials_ = true;
bootstrap_controller_->AttemptWifiCredentialTransfer();
}
} else {
// We are after the 'Gaia Info' screen. Transfer credentials.
StartAccountTransfer();
}
}
void QuickStartController::SavePhoneInstanceID() {
CHECK(bootstrap_controller_) << "Missing bootstrap_controller_";
std::string phone_instance_id = bootstrap_controller_->GetPhoneInstanceId();
if (phone_instance_id.empty()) {
return;
}
QS_LOG(INFO) << "Adding Phone Instance ID to Wizard Object for Unified "
"Setup UI enhancements. quick_start_phone_instance_id: "
<< phone_instance_id;
LoginDisplayHost::default_host()
->GetWizardContext()
->quick_start_phone_instance_id = phone_instance_id;
}
void QuickStartController::FinishAccountCreation() {
CHECK(!gaia_creds_.email.empty()) << "Missing Gaia email";
CHECK(!gaia_creds_.gaia_id.empty()) << "Missing Gaia ID";
CHECK(!gaia_creds_.auth_code.empty()) << "Missing Gaia auth code";
UpdateUiState(UiState::CREATING_ACCOUNT);
controller_state_ = ControllerState::SETUP_COMPLETE;
QuickStartMetrics::RecordGaiaTransferResult(
/*succeeded=*/true, /*failure_reason=*/std::nullopt);
const AccountId account_id = AccountId::FromNonCanonicalEmail(
gaia_creds_.email, gaia_creds_.gaia_id, AccountType::GOOGLE);
// The user type is known to be regular. The unicorn flow transitions to the
// Gaia screen and uses its own mechanism for account creation.
std::unique_ptr<UserContext> user_context =
login::BuildUserContextForGaiaSignIn(
/*user_type=*/user_manager::UserType::kRegular,
/*account_id=*/account_id,
/*using_saml=*/false,
/*using_saml_api=*/false,
/*password=*/"",
/*password_attributes=*/SamlPasswordAttributes(),
/*sync_trusted_vault_keys=*/std::nullopt,
/*challenge_response_key=*/std::nullopt);
user_context->SetAuthCode(gaia_creds_.auth_code);
if (LoginDisplayHost::default_host()) {
LoginDisplayHost::default_host()->CompleteLogin(*user_context);
}
}
void QuickStartController::ResetState() {
entry_point_.reset();
fallback_url_.reset();
qr_code_.reset();
pin_.reset();
user_info_ = UserInfo();
gaia_creds_ = TargetDeviceBootstrapController::GaiaCredentials();
wifi_name_.reset();
controller_state_ = ControllerState::NOT_ACTIVE;
ui_state_.reset();
auto* wizard_context = LoginDisplayHost::default_host()->GetWizardContext();
wizard_context->quick_start_setup_ongoing = false;
wizard_context->quick_start_wifi_credentials.reset();
did_request_wifi_credentials_ = false;
did_request_account_info_ = false;
did_request_account_transfer_ = false;
// Don't cleanup |bootstrap_controller_| state here, since it may be waiting
// for source device to gracefully drop connection.
}
/******************* Bluetooth dialog related functions *******************/
void QuickStartController::StartObservingBluetoothState() {
GetBluetoothConfigService(
cros_bluetooth_config_remote_.BindNewPipeAndPassReceiver());
cros_bluetooth_config_remote_->ObserveSystemProperties(
cros_system_properties_observer_receiver_.BindNewPipeAndPassRemote());
}
void QuickStartController::OnPropertiesUpdated(
bluetooth_config::mojom::BluetoothSystemPropertiesPtr properties) {
if (bluetooth_system_state_ == properties->system_state) {
return;
}
bluetooth_system_state_ = properties->system_state;
if (!IsSetupOngoing()) {
return;
}
QS_LOG(INFO) << "New Bluetooth state: "
<< GetBluetoothStateString(bluetooth_system_state_);
if (controller_state_ == ControllerState::WAITING_FOR_BLUETOOTH_PERMISSION ||
controller_state_ == ControllerState::WAITING_FOR_BLUETOOTH_ACTIVATION) {
if (bluetooth_system_state_ == BluetoothSystemState::kEnabled) {
StartAdvertising();
}
}
}
bool QuickStartController::IsBluetoothDisabled() {
return bluetooth_system_state_ == BluetoothSystemState::kDisabled;
}
bool QuickStartController::WillRequestWiFi() {
return !IsConnectedToWiFi();
}
void QuickStartController::OnFallbackUrlFlowSuccess() {
if (controller_state_ == ControllerState::FALLBACK_URL_FLOW_ON_GAIA_SCREEN) {
SavePhoneInstanceID();
controller_state_ = ControllerState::SETUP_COMPLETE;
}
}
void QuickStartController::OnBluetoothPermissionGranted() {
if (controller_state_ != ControllerState::WAITING_FOR_BLUETOOTH_PERMISSION) {
return;
}
controller_state_ = ControllerState::WAITING_FOR_BLUETOOTH_ACTIVATION;
if (IsBluetoothDisabled()) {
CHECK(cros_bluetooth_config_remote_)
<< "Missing cros_bluetooth_config_remote_";
cros_bluetooth_config_remote_->SetBluetoothEnabledWithoutPersistence();
// Advertising will start once we are notified that bluetooth is enabled.
}
}
void QuickStartController::StartAdvertising() {
QS_LOG(INFO) << "ControllerState::INITIALIZING requesting advertising.";
controller_state_ = ControllerState::INITIALIZING;
bootstrap_controller_->StartAdvertisingAndMaybeGetQRCode();
}
std::ostream& operator<<(std::ostream& stream,
const QuickStartController::UiState& ui_state) {
switch (ui_state) {
case QuickStartController::UiDelegate::UiState::SHOWING_BLUETOOTH_DIALOG:
stream << "[showing Bluetooth dialog]";
break;
case QuickStartController::UiDelegate::UiState::CONNECTING_TO_PHONE:
stream << "[connecting to phone]";
break;
case QuickStartController::UiDelegate::UiState::SHOWING_QR:
stream << "[showing QR]";
break;
case QuickStartController::UiDelegate::UiState::SHOWING_PIN:
stream << "[showing PIN]";
break;
case QuickStartController::UiDelegate::UiState::CONNECTING_TO_WIFI:
stream << "[connecting to WiFi]";
break;
case QuickStartController::UiDelegate::UiState::WIFI_CREDENTIALS_RECEIVED:
stream << "[WiFi credentials received]";
break;
case QuickStartController::UiDelegate::UiState::CONFIRM_GOOGLE_ACCOUNT:
stream << "[confirm Google account]";
break;
case QuickStartController::UiDelegate::UiState::SIGNING_IN:
stream << "[signing in]";
break;
case QuickStartController::UiDelegate::UiState::CREATING_ACCOUNT:
stream << "[creating account]";
break;
case QuickStartController::UiDelegate::UiState::FALLBACK_URL_FLOW:
stream << "[fallback URL flow]";
break;
case QuickStartController::UiDelegate::UiState::SETUP_COMPLETE:
stream << "[setup complete]";
break;
case QuickStartController::UiDelegate::UiState::EXIT_SCREEN:
stream << "[exit screen]";
break;
}
return stream;
}
std::ostream& operator<<(
std::ostream& stream,
const QuickStartController::AbortFlowReason& abort_flow_reason) {
switch (abort_flow_reason) {
case QuickStartController::AbortFlowReason::USER_CLICKED_BACK:
stream << "[user clicked back]";
break;
case QuickStartController::AbortFlowReason::USER_CLICKED_CANCEL:
stream << "[user clicked cancel]";
break;
case QuickStartController::AbortFlowReason::SIGNIN_SCHOOL:
stream << "[signin school]";
break;
case QuickStartController::AbortFlowReason::ENTERPRISE_ENROLLMENT:
stream << "[enterprise enrollment]";
break;
case QuickStartController::AbortFlowReason::ERROR:
stream << "[error]";
break;
case QuickStartController::AbortFlowReason::ADD_CHILD:
stream << "[add child]";
break;
}
return stream;
}
std::ostream& operator<<(
std::ostream& stream,
const QuickStartController::ControllerState& controller_state) {
switch (controller_state) {
case QuickStartController::ControllerState::NOT_ACTIVE:
stream << "[not active]";
break;
case QuickStartController::ControllerState::
WAITING_FOR_BLUETOOTH_PERMISSION:
stream << "[waiting for bluetooth permission]";
break;
case QuickStartController::ControllerState::
WAITING_FOR_BLUETOOTH_ACTIVATION:
stream << "[waiting for bluetooth activation]";
break;
case QuickStartController::ControllerState::WAITING_TO_RESUME_AFTER_UPDATE:
stream << "[waiting to resume after update]";
break;
case QuickStartController::ControllerState::INITIALIZING:
stream << "[initializing]";
break;
case QuickStartController::ControllerState::ADVERTISING:
stream << "[advertising]";
break;
case QuickStartController::ControllerState::CONNECTED:
stream << "[connected]";
break;
case QuickStartController::ControllerState::
CONTINUING_AFTER_ENROLLMENT_CHECKS:
stream << "[continuing after enrollment checks]";
break;
case QuickStartController::ControllerState::
FALLBACK_URL_FLOW_ON_GAIA_SCREEN:
stream << "[fallback URL flow on Gaia screen]";
break;
case QuickStartController::ControllerState::SETUP_COMPLETE:
stream << "[setup complete]";
break;
}
return stream;
}
} // namespace ash::quick_start