| // Copyright 2016 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/arc/arc_session_manager.h" |
| |
| #include <utility> |
| |
| #include "ash/common/shelf/shelf_delegate.h" |
| #include "ash/common/wm_shell.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string16.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/chromeos/arc/arc_auth_context.h" |
| #include "chrome/browser/chromeos/arc/arc_auth_notification.h" |
| #include "chrome/browser/chromeos/arc/arc_optin_uma.h" |
| #include "chrome/browser/chromeos/arc/arc_support_host.h" |
| #include "chrome/browser/chromeos/arc/arc_util.h" |
| #include "chrome/browser/chromeos/arc/optin/arc_terms_of_service_default_negotiator.h" |
| #include "chrome/browser/chromeos/arc/optin/arc_terms_of_service_oobe_negotiator.h" |
| #include "chrome/browser/chromeos/arc/policy/arc_android_management_checker.h" |
| #include "chrome/browser/chromeos/arc/policy/arc_policy_util.h" |
| #include "chrome/browser/chromeos/login/ui/login_display_host.h" |
| #include "chrome/browser/lifetime/application_lifetime.h" |
| #include "chrome/browser/policy/profile_policy_connector.h" |
| #include "chrome/browser/policy/profile_policy_connector_factory.h" |
| #include "chrome/browser/prefs/pref_service_syncable_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/app_list/arc/arc_app_launcher.h" |
| #include "chrome/browser/ui/app_list/arc/arc_app_utils.h" |
| #include "chrome/browser/ui/ash/multi_user/multi_user_util.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chromeos/chromeos_switches.h" |
| #include "chromeos/cryptohome/cryptohome_parameters.h" |
| #include "chromeos/dbus/dbus_thread_manager.h" |
| #include "chromeos/dbus/session_manager_client.h" |
| #include "components/arc/arc_bridge_service.h" |
| #include "components/arc/arc_session_runner.h" |
| #include "components/arc/arc_util.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/sync_preferences/pref_service_syncable.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "extensions/browser/extension_prefs.h" |
| |
| namespace arc { |
| |
| namespace { |
| |
| // Weak pointer. This class is owned by ArcServiceManager. |
| ArcSessionManager* g_arc_session_manager = nullptr; |
| |
| // Skip creating UI in unit tests |
| bool g_disable_ui_for_testing = false; |
| |
| // Use specified ash::ShelfDelegate for unit tests. |
| ash::ShelfDelegate* g_shelf_delegate_for_testing = nullptr; |
| |
| // The Android management check is disabled by default, it's used only for |
| // testing. |
| bool g_enable_check_android_management_for_testing = false; |
| |
| // Maximum amount of time we'll wait for ARC to finish booting up. Once this |
| // timeout expires, keep ARC running in case the user wants to file feedback, |
| // but present the UI to try again. |
| constexpr base::TimeDelta kArcSignInTimeout = base::TimeDelta::FromMinutes(5); |
| |
| ash::ShelfDelegate* GetShelfDelegate() { |
| if (g_shelf_delegate_for_testing) |
| return g_shelf_delegate_for_testing; |
| if (ash::WmShell::HasInstance()) |
| return ash::WmShell::Get()->shelf_delegate(); |
| return nullptr; |
| } |
| |
| } // namespace |
| |
| ArcSessionManager::ArcSessionManager( |
| std::unique_ptr<ArcSessionRunner> arc_session_runner) |
| : arc_session_runner_(std::move(arc_session_runner)), |
| attempt_user_exit_callback_(base::Bind(chrome::AttemptUserExit)), |
| weak_ptr_factory_(this) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!g_arc_session_manager); |
| g_arc_session_manager = this; |
| arc_session_runner_->AddObserver(this); |
| } |
| |
| ArcSessionManager::~ArcSessionManager() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| Shutdown(); |
| arc_session_runner_->RemoveObserver(this); |
| |
| DCHECK_EQ(this, g_arc_session_manager); |
| g_arc_session_manager = nullptr; |
| } |
| |
| // static |
| ArcSessionManager* ArcSessionManager::Get() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return g_arc_session_manager; |
| } |
| |
| // static |
| void ArcSessionManager::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| // TODO(dspaid): Implement a mechanism to allow this to sync on first boot |
| // only. |
| registry->RegisterBooleanPref(prefs::kArcDataRemoveRequested, false); |
| registry->RegisterBooleanPref(prefs::kArcEnabled, false); |
| registry->RegisterBooleanPref(prefs::kArcSignedIn, false); |
| registry->RegisterBooleanPref(prefs::kArcTermsAccepted, false); |
| registry->RegisterBooleanPref(prefs::kArcBackupRestoreEnabled, true); |
| registry->RegisterBooleanPref(prefs::kArcLocationServiceEnabled, true); |
| } |
| |
| // static |
| bool ArcSessionManager::IsOobeOptInActive() { |
| // ARC OOBE OptIn is optional for now. Test if it exists and login host is |
| // active. |
| if (!user_manager::UserManager::Get()->IsCurrentUserNew()) |
| return false; |
| if (!base::CommandLine::ForCurrentProcess()->HasSwitch( |
| chromeos::switches::kEnableArcOOBEOptIn)) |
| return false; |
| if (!chromeos::LoginDisplayHost::default_host()) |
| return false; |
| return true; |
| } |
| |
| // static |
| void ArcSessionManager::DisableUIForTesting() { |
| g_disable_ui_for_testing = true; |
| } |
| |
| // static |
| void ArcSessionManager::SetShelfDelegateForTesting( |
| ash::ShelfDelegate* shelf_delegate) { |
| g_shelf_delegate_for_testing = shelf_delegate; |
| } |
| |
| // static |
| void ArcSessionManager::EnableCheckAndroidManagementForTesting() { |
| g_enable_check_android_management_for_testing = true; |
| } |
| |
| void ArcSessionManager::OnSessionReady() { |
| for (auto& observer : arc_session_observer_list_) |
| observer.OnSessionReady(); |
| } |
| |
| void ArcSessionManager::OnSessionStopped(StopReason reason) { |
| // TODO(crbug.com/625923): Use |reason| to report more detailed errors. |
| if (arc_sign_in_timer_.IsRunning()) |
| OnProvisioningFinished(ProvisioningResult::ARC_STOPPED); |
| |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcDataRemoveRequested)) { |
| // This should be always true, but just in case as this is looked at |
| // inside RemoveArcData() at first. |
| VLOG(1) << "ARC had previously requested to remove user data."; |
| DCHECK(arc_session_runner_->IsStopped()); |
| RemoveArcData(); |
| } else { |
| // To support special "Stop and enable ARC" procedure for enterprise, |
| // here call MaybeReenableArc() asyncronously. |
| // TODO(hidehiko): Restructure the code. crbug.com/665316 |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&ArcSessionManager::MaybeReenableArc, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| for (auto& observer : arc_session_observer_list_) |
| observer.OnSessionStopped(reason); |
| } |
| |
| void ArcSessionManager::RemoveArcData() { |
| // Ignore redundant data removal request. |
| if (state() == State::REMOVING_DATA_DIR) |
| return; |
| |
| // OnArcDataRemoved resets this flag. |
| profile_->GetPrefs()->SetBoolean(prefs::kArcDataRemoveRequested, true); |
| |
| if (!arc_session_runner_->IsStopped()) { |
| // Just set a flag. On session stopped, this will be re-called, |
| // then session manager should remove the data. |
| return; |
| } |
| |
| VLOG(1) << "Starting ARC data removal"; |
| SetState(State::REMOVING_DATA_DIR); |
| chromeos::DBusThreadManager::Get()->GetSessionManagerClient()->RemoveArcData( |
| cryptohome::Identification( |
| multi_user_util::GetAccountIdFromProfile(profile_)), |
| base::Bind(&ArcSessionManager::OnArcDataRemoved, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ArcSessionManager::OnArcDataRemoved(bool success) { |
| if (success) |
| VLOG(1) << "ARC data removal successful"; |
| else |
| LOG(ERROR) << "Request for ARC user data removal failed."; |
| |
| // TODO(khmel): Browser tests may shutdown profile by itself. Update browser |
| // tests and remove this check. |
| if (state() == State::NOT_INITIALIZED) |
| return; |
| |
| for (auto& observer : observer_list_) |
| observer.OnArcDataRemoved(); |
| |
| profile_->GetPrefs()->SetBoolean(prefs::kArcDataRemoveRequested, false); |
| DCHECK_EQ(state(), State::REMOVING_DATA_DIR); |
| SetState(State::STOPPED); |
| |
| MaybeReenableArc(); |
| } |
| |
| void ArcSessionManager::MaybeReenableArc() { |
| // Here check if |reenable_arc_| is marked or not. |
| // The only case this happens should be in the special case for enterprise |
| // "on managed lost" case. In that case, OnSessionStopped() should trigger |
| // the RemoveArcData(), then this. |
| if (!reenable_arc_ || !IsArcEnabled()) |
| return; |
| |
| // Restart ARC anyway. Let the enterprise reporting instance decide whether |
| // the ARC user data wipe is still required or not. |
| reenable_arc_ = false; |
| VLOG(1) << "Reenable ARC"; |
| EnableArc(); |
| } |
| |
| void ArcSessionManager::OnProvisioningFinished(ProvisioningResult result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // If the Mojo message to notify finishing the provisioning is already sent |
| // from the container, it will be processed even after requesting to stop the |
| // container. Ignore all |result|s arriving while ARC is disabled, in order to |
| // avoid popping up an error message triggered below. This code intentionally |
| // does not support the case of reenabling. |
| if (!IsArcEnabled()) { |
| LOG(WARNING) << "Provisioning result received after Arc was disabled. " |
| << "Ignoring result " << static_cast<int>(result) << "."; |
| return; |
| } |
| |
| // Due asynchronous nature of stopping the ARC instance, |
| // OnProvisioningFinished may arrive after setting the |State::STOPPED| state |
| // and |State::Active| is not guaranteed to be set here. |
| // prefs::kArcDataRemoveRequested also can be active for now. |
| |
| if (provisioning_reported_) { |
| // We don't expect ProvisioningResult::SUCCESS is reported twice or reported |
| // after an error. |
| DCHECK_NE(result, ProvisioningResult::SUCCESS); |
| // TODO(khmel): Consider changing LOG to NOTREACHED once we guaranty that |
| // no double message can happen in production. |
| LOG(WARNING) << "Provisioning result was already reported. Ignoring " |
| << "additional result " << static_cast<int>(result) << "."; |
| return; |
| } |
| provisioning_reported_ = true; |
| |
| if (result == ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR) { |
| if (IsArcKioskMode()) { |
| VLOG(1) << "Robot account auth code fetching error"; |
| // Log out the user. All the cleanup will be done in Shutdown() method. |
| // The callback is not called because auth code is empty. |
| attempt_user_exit_callback_.Run(); |
| return; |
| } |
| |
| // For backwards compatibility, use NETWORK_ERROR for |
| // CHROME_SERVER_COMMUNICATION_ERROR case. |
| UpdateOptInCancelUMA(OptInCancelReason::NETWORK_ERROR); |
| } else if (!sign_in_start_time_.is_null()) { |
| arc_sign_in_timer_.Stop(); |
| |
| UpdateProvisioningTiming(base::Time::Now() - sign_in_start_time_, |
| result == ProvisioningResult::SUCCESS, |
| policy_util::IsAccountManaged(profile_)); |
| UpdateProvisioningResultUMA(result, |
| policy_util::IsAccountManaged(profile_)); |
| if (result != ProvisioningResult::SUCCESS) |
| UpdateOptInCancelUMA(OptInCancelReason::CLOUD_PROVISION_FLOW_FAIL); |
| } |
| |
| if (result == ProvisioningResult::SUCCESS) { |
| if (support_host_) |
| support_host_->Close(); |
| |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcSignedIn)) |
| return; |
| |
| profile_->GetPrefs()->SetBoolean(prefs::kArcSignedIn, true); |
| // Don't show Play Store app for ARC Kiosk because the only one UI in kiosk |
| // mode must be the kiosk app and device is not needed for opt-in. |
| if (!IsArcOptInVerificationDisabled() && !IsArcKioskMode()) { |
| playstore_launcher_.reset( |
| new ArcAppLauncher(profile_, kPlayStoreAppId, true)); |
| } |
| |
| for (auto& observer : observer_list_) |
| observer.OnArcInitialStart(); |
| return; |
| } |
| |
| ArcSupportHost::Error error; |
| VLOG(1) << "ARC provisioning failed: " << result << "."; |
| switch (result) { |
| case ProvisioningResult::GMS_NETWORK_ERROR: |
| error = ArcSupportHost::Error::SIGN_IN_NETWORK_ERROR; |
| break; |
| case ProvisioningResult::GMS_SERVICE_UNAVAILABLE: |
| case ProvisioningResult::GMS_SIGN_IN_FAILED: |
| case ProvisioningResult::GMS_SIGN_IN_TIMEOUT: |
| case ProvisioningResult::GMS_SIGN_IN_INTERNAL_ERROR: |
| error = ArcSupportHost::Error::SIGN_IN_SERVICE_UNAVAILABLE_ERROR; |
| break; |
| case ProvisioningResult::GMS_BAD_AUTHENTICATION: |
| error = ArcSupportHost::Error::SIGN_IN_BAD_AUTHENTICATION_ERROR; |
| break; |
| case ProvisioningResult::DEVICE_CHECK_IN_FAILED: |
| case ProvisioningResult::DEVICE_CHECK_IN_TIMEOUT: |
| case ProvisioningResult::DEVICE_CHECK_IN_INTERNAL_ERROR: |
| error = ArcSupportHost::Error::SIGN_IN_GMS_NOT_AVAILABLE_ERROR; |
| break; |
| case ProvisioningResult::CLOUD_PROVISION_FLOW_FAILED: |
| case ProvisioningResult::CLOUD_PROVISION_FLOW_TIMEOUT: |
| case ProvisioningResult::CLOUD_PROVISION_FLOW_INTERNAL_ERROR: |
| error = ArcSupportHost::Error::SIGN_IN_CLOUD_PROVISION_FLOW_FAIL_ERROR; |
| break; |
| case ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR: |
| error = ArcSupportHost::Error::SERVER_COMMUNICATION_ERROR; |
| break; |
| default: |
| error = ArcSupportHost::Error::SIGN_IN_UNKNOWN_ERROR; |
| break; |
| } |
| |
| if (result == ProvisioningResult::ARC_STOPPED || |
| result == ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR) { |
| if (profile_->GetPrefs()->HasPrefPath(prefs::kArcSignedIn)) |
| profile_->GetPrefs()->SetBoolean(prefs::kArcSignedIn, false); |
| ShutdownSession(); |
| if (support_host_) |
| support_host_->ShowError(error, false); |
| return; |
| } |
| |
| if (result == ProvisioningResult::CLOUD_PROVISION_FLOW_FAILED || |
| result == ProvisioningResult::CLOUD_PROVISION_FLOW_TIMEOUT || |
| result == ProvisioningResult::CLOUD_PROVISION_FLOW_INTERNAL_ERROR || |
| // OVERALL_SIGN_IN_TIMEOUT might be an indication that ARC believes it is |
| // fully setup, but Chrome does not. |
| result == ProvisioningResult::OVERALL_SIGN_IN_TIMEOUT || |
| // Just to be safe, remove data if we don't know the cause. |
| result == ProvisioningResult::UNKNOWN_ERROR) { |
| VLOG(1) << "ARC provisioning failed permanently. Removing user data"; |
| RemoveArcData(); |
| } |
| |
| // We'll delay shutting down the ARC instance in this case to allow people |
| // to send feedback. |
| if (support_host_) |
| support_host_->ShowError(error, true /* = show send feedback button */); |
| } |
| |
| void ArcSessionManager::SetState(State state) { |
| state_ = state; |
| } |
| |
| bool ArcSessionManager::IsAllowed() const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return profile_ != nullptr; |
| } |
| |
| void ArcSessionManager::OnPrimaryUserProfilePrepared(Profile* profile) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile && profile != profile_); |
| |
| Shutdown(); |
| |
| if (!IsArcAllowedForProfile(profile)) |
| return; |
| |
| // TODO(khmel): Move this to IsArcAllowedForProfile. |
| if (policy_util::IsArcDisabledForEnterprise() && |
| policy_util::IsAccountManaged(profile)) { |
| VLOG(2) << "Enterprise users are not supported in ARC."; |
| return; |
| } |
| |
| profile_ = profile; |
| |
| // Create the support host at initialization. Note that, practically, |
| // ARC support Chrome app is rarely used (only opt-in and re-auth flow). |
| // So, it may be better to initialize it lazily. |
| // TODO(hidehiko): Revisit to think about lazy initialization. |
| // |
| // Don't show UI for ARC Kiosk because the only one UI in kiosk mode must |
| // be the kiosk app. In case of error the UI will be useless as well, because |
| // in typical use case there will be no one nearby the kiosk device, who can |
| // do some action to solve the problem be means of UI. |
| if (!g_disable_ui_for_testing && !IsArcOptInVerificationDisabled() && |
| !IsArcKioskMode()) { |
| DCHECK(!support_host_); |
| support_host_ = base::MakeUnique<ArcSupportHost>(profile_); |
| support_host_->AddObserver(this); |
| } |
| |
| DCHECK_EQ(State::NOT_INITIALIZED, state_); |
| SetState(State::STOPPED); |
| |
| context_.reset(new ArcAuthContext(profile_)); |
| |
| if (!g_disable_ui_for_testing || |
| g_enable_check_android_management_for_testing) { |
| ArcAndroidManagementChecker::StartClient(); |
| } |
| pref_change_registrar_.Init(profile_->GetPrefs()); |
| pref_change_registrar_.Add( |
| prefs::kArcEnabled, |
| base::Bind(&ArcSessionManager::OnOptInPreferenceChanged, |
| weak_ptr_factory_.GetWeakPtr())); |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcEnabled)) { |
| // Don't start ARC if there is a pending request to remove the data. Restart |
| // ARC once data removal finishes. |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcDataRemoveRequested)) { |
| reenable_arc_ = true; |
| VLOG(1) << "ARC previously requested to remove data."; |
| RemoveArcData(); |
| } else { |
| OnOptInPreferenceChanged(); |
| } |
| } else { |
| VLOG(1) << "ARC disabled on profile. Removing data."; |
| RemoveArcData(); |
| PrefServiceSyncableFromProfile(profile_)->AddObserver(this); |
| OnIsSyncingChanged(); |
| } |
| } |
| |
| void ArcSessionManager::OnIsSyncingChanged() { |
| sync_preferences::PrefServiceSyncable* const pref_service_syncable = |
| PrefServiceSyncableFromProfile(profile_); |
| if (!pref_service_syncable->IsSyncing()) |
| return; |
| |
| pref_service_syncable->RemoveObserver(this); |
| if (!g_disable_ui_for_testing && |
| !base::CommandLine::ForCurrentProcess()->HasSwitch( |
| chromeos::switches::kEnableArcOOBEOptIn) && |
| profile_->IsNewProfile() && |
| !profile_->GetPrefs()->HasPrefPath(prefs::kArcEnabled)) { |
| ArcAuthNotification::Show(profile_); |
| } |
| } |
| |
| void ArcSessionManager::Shutdown() { |
| if (!g_disable_ui_for_testing) |
| ArcAuthNotification::Hide(); |
| |
| ShutdownSession(); |
| if (support_host_) { |
| support_host_->Close(); |
| support_host_->RemoveObserver(this); |
| support_host_.reset(); |
| } |
| if (profile_) { |
| sync_preferences::PrefServiceSyncable* pref_service_syncable = |
| PrefServiceSyncableFromProfile(profile_); |
| pref_service_syncable->RemoveObserver(this); |
| } |
| pref_change_registrar_.RemoveAll(); |
| context_.reset(); |
| profile_ = nullptr; |
| SetState(State::NOT_INITIALIZED); |
| } |
| |
| void ArcSessionManager::StopArc() { |
| if (state_ != State::STOPPED) { |
| profile_->GetPrefs()->SetBoolean(prefs::kArcSignedIn, false); |
| profile_->GetPrefs()->SetBoolean(prefs::kArcTermsAccepted, false); |
| } |
| ShutdownSession(); |
| if (support_host_) |
| support_host_->Close(); |
| } |
| |
| void ArcSessionManager::OnOptInPreferenceChanged() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| |
| const bool arc_enabled = IsArcEnabled(); |
| if (!IsArcManaged()) { |
| // Update UMA only for non-Managed cases. |
| UpdateOptInActionUMA(arc_enabled ? OptInActionType::OPTED_IN |
| : OptInActionType::OPTED_OUT); |
| |
| if (!arc_enabled) { |
| // Remove the pinned Play Store icon launcher in Shelf. |
| // This is only for non-Managed cases. In managed cases, it is expected |
| // to be "disabled" rather than "removed", so keep it here. |
| ash::ShelfDelegate* shelf_delegate = GetShelfDelegate(); |
| if (shelf_delegate) |
| shelf_delegate->UnpinAppWithID(ArcSupportHost::kHostAppId); |
| } |
| } |
| |
| for (auto& observer : observer_list_) |
| observer.OnArcOptInChanged(arc_enabled); |
| |
| // Hide auth notification if it was opened before and arc.enabled pref was |
| // explicitly set to true or false. |
| if (!g_disable_ui_for_testing && |
| profile_->GetPrefs()->HasPrefPath(prefs::kArcEnabled)) { |
| ArcAuthNotification::Hide(); |
| } |
| |
| if (!arc_enabled) { |
| // Reset any pending request to re-enable ARC. |
| VLOG(1) << "ARC opt-out. Removing user data."; |
| reenable_arc_ = false; |
| StopArc(); |
| RemoveArcData(); |
| return; |
| } |
| |
| if (state_ == State::ACTIVE) |
| return; |
| |
| if (state_ == State::REMOVING_DATA_DIR) { |
| // Data removal request is in progress. Set flag to re-enable Arc once it is |
| // finished. |
| reenable_arc_ = true; |
| return; |
| } |
| |
| if (support_host_) |
| support_host_->SetArcManaged(IsArcManaged()); |
| |
| // For ARC Kiosk we skip ToS because it is very likely that near the device |
| // there will be no one who is eligible to accept them. |
| // TODO(poromov): Move to more Kiosk dedicated set-up phase. |
| if (IsArcKioskMode()) |
| profile_->GetPrefs()->SetBoolean(prefs::kArcTermsAccepted, true); |
| |
| // If it is marked that sign in has been successfully done, then directly |
| // start ARC. |
| // For testing, and for Kisok mode, we also skip ToS negotiation procedure. |
| // For backward compatibility, this check needs to be prior to the |
| // kArcTermsAccepted check below. |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcSignedIn) || |
| IsArcOptInVerificationDisabled() || IsArcKioskMode()) { |
| StartArc(); |
| |
| // Skip Android management check for testing. |
| // We also skip if Android management check for Kiosk mode, |
| // because there are no managed human users for Kiosk exist. |
| if (IsArcOptInVerificationDisabled() || IsArcKioskMode() || |
| (g_disable_ui_for_testing && |
| !g_enable_check_android_management_for_testing)) { |
| return; |
| } |
| |
| // Check Android management in parallel. |
| // Note: Because the callback may be called in synchronous way (i.e. called |
| // on the same stack), StartCheck() needs to be called *after* StartArc(). |
| // Otherwise, DisableArc() which may be called in |
| // OnBackgroundAndroidManagementChecked() could be ignored. |
| android_management_checker_ = base::MakeUnique<ArcAndroidManagementChecker>( |
| profile_, context_->token_service(), context_->account_id(), |
| true /* retry_on_error */); |
| android_management_checker_->StartCheck( |
| base::Bind(&ArcSessionManager::OnBackgroundAndroidManagementChecked, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| // If it is marked that the Terms of service is accepted already, |
| // just skip the negotiation with user, and start Android management |
| // check directly. |
| // This happens, e.g., when; |
| // 1) User accepted the Terms of service on OOBE flow. |
| // 2) User accepted the Terms of service on Opt-in flow, but logged out |
| // before ARC sign in procedure was done. Then, logs in again. |
| if (profile_->GetPrefs()->GetBoolean(prefs::kArcTermsAccepted)) { |
| // Don't show UI for this progress if it was not shown. |
| if (support_host_->ui_page() != ArcSupportHost::UIPage::NO_PAGE) |
| support_host_->ShowArcLoading(); |
| StartArcAndroidManagementCheck(); |
| return; |
| } |
| |
| // Need user's explicit Terms Of Service agreement. Prevent race condition |
| // when ARC can be enabled before profile is synced. In last case |
| // OnOptInPreferenceChanged is called twice. |
| // TODO(crbug.com/687185): Remove the condition. |
| if (state_ != State::SHOWING_TERMS_OF_SERVICE) |
| StartTermsOfServiceNegotiation(); |
| } |
| |
| void ArcSessionManager::ShutdownSession() { |
| arc_sign_in_timer_.Stop(); |
| playstore_launcher_.reset(); |
| terms_of_service_negotiator_.reset(); |
| android_management_checker_.reset(); |
| arc_session_runner_->RequestStop(); |
| // TODO(hidehiko): The ARC instance's stopping is asynchronous, so it might |
| // still be running when we return from this function. Do not set the |
| // STOPPED state immediately here. |
| if (state_ != State::NOT_INITIALIZED && state_ != State::REMOVING_DATA_DIR) |
| SetState(State::STOPPED); |
| for (auto& observer : observer_list_) |
| observer.OnArcBridgeShutdown(); |
| } |
| |
| void ArcSessionManager::AddObserver(Observer* observer) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| observer_list_.AddObserver(observer); |
| } |
| |
| void ArcSessionManager::RemoveObserver(Observer* observer) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| void ArcSessionManager::AddSessionObserver(ArcSessionObserver* observer) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| arc_session_observer_list_.AddObserver(observer); |
| } |
| |
| void ArcSessionManager::RemoveSessionObserver(ArcSessionObserver* observer) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| arc_session_observer_list_.RemoveObserver(observer); |
| } |
| |
| bool ArcSessionManager::IsSessionRunning() const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return arc_session_runner_->IsRunning(); |
| } |
| |
| bool ArcSessionManager::IsSessionStopped() const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return arc_session_runner_->IsStopped(); |
| } |
| |
| // This is the special method to support enterprise mojo API. |
| // TODO(hidehiko): Remove this. |
| void ArcSessionManager::StopAndEnableArc() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!arc_session_runner_->IsStopped()); |
| reenable_arc_ = true; |
| StopArc(); |
| } |
| |
| void ArcSessionManager::StartArc() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Arc must be started only if no pending data removal request exists. |
| DCHECK(!profile_->GetPrefs()->GetBoolean(prefs::kArcDataRemoveRequested)); |
| |
| arc_start_time_ = base::Time::Now(); |
| |
| provisioning_reported_ = false; |
| |
| arc_session_runner_->RequestStart(); |
| SetState(State::ACTIVE); |
| } |
| |
| void ArcSessionManager::OnArcSignInTimeout() { |
| LOG(ERROR) << "Timed out waiting for first sign in."; |
| OnProvisioningFinished(ProvisioningResult::OVERALL_SIGN_IN_TIMEOUT); |
| } |
| |
| void ArcSessionManager::CancelAuthCode() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (state_ == State::NOT_INITIALIZED) { |
| NOTREACHED(); |
| return; |
| } |
| |
| // If ARC failed to boot normally, stop ARC. Similarly, if the current page is |
| // LSO, closing the window should stop ARC since the user activity chooses to |
| // not sign in. In any other case, ARC is booting normally and the instance |
| // should not be stopped. |
| if ((state_ != State::SHOWING_TERMS_OF_SERVICE && |
| state_ != State::CHECKING_ANDROID_MANAGEMENT) && |
| (!support_host_ || |
| (support_host_->ui_page() != ArcSupportHost::UIPage::ERROR && |
| support_host_->ui_page() != ArcSupportHost::UIPage::LSO))) { |
| return; |
| } |
| |
| // Update UMA with user cancel only if error is not currently shown. |
| if (support_host_ && |
| support_host_->ui_page() != ArcSupportHost::UIPage::NO_PAGE && |
| support_host_->ui_page() != ArcSupportHost::UIPage::ERROR) { |
| UpdateOptInCancelUMA(OptInCancelReason::USER_CANCEL); |
| } |
| |
| StopArc(); |
| |
| if (IsArcManaged()) |
| return; |
| |
| DisableArc(); |
| } |
| |
| bool ArcSessionManager::IsArcManaged() const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| return profile_->GetPrefs()->IsManagedPreference(prefs::kArcEnabled); |
| } |
| |
| bool ArcSessionManager::IsArcEnabled() const { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!IsAllowed()) |
| return false; |
| |
| DCHECK(profile_); |
| return profile_->GetPrefs()->GetBoolean(prefs::kArcEnabled); |
| } |
| |
| void ArcSessionManager::EnableArc() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| |
| if (IsArcEnabled()) { |
| OnOptInPreferenceChanged(); |
| return; |
| } |
| |
| if (!IsArcManaged()) |
| profile_->GetPrefs()->SetBoolean(prefs::kArcEnabled, true); |
| } |
| |
| void ArcSessionManager::DisableArc() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| profile_->GetPrefs()->SetBoolean(prefs::kArcEnabled, false); |
| } |
| |
| void ArcSessionManager::RecordArcState() { |
| // Only record Enabled state if ARC is allowed in the first place, so we do |
| // not split the ARC population by devices that cannot run ARC. |
| if (IsAllowed()) |
| UpdateEnabledStateUMA(IsArcEnabled()); |
| } |
| |
| void ArcSessionManager::StartTermsOfServiceNegotiation() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!terms_of_service_negotiator_); |
| |
| if (!arc_session_runner_->IsStopped()) { |
| // If the user attempts to re-enable ARC while the ARC instance is still |
| // running the user should not be able to continue until the ARC instance |
| // has stopped. |
| if (support_host_) { |
| support_host_->ShowError( |
| ArcSupportHost::Error::SIGN_IN_SERVICE_UNAVAILABLE_ERROR, false); |
| } |
| return; |
| } |
| |
| SetState(State::SHOWING_TERMS_OF_SERVICE); |
| if (IsOobeOptInActive()) { |
| VLOG(1) << "Use OOBE negotiator."; |
| terms_of_service_negotiator_ = |
| base::MakeUnique<ArcTermsOfServiceOobeNegotiator>(); |
| } else if (support_host_) { |
| VLOG(1) << "Use default negotiator."; |
| terms_of_service_negotiator_ = |
| base::MakeUnique<ArcTermsOfServiceDefaultNegotiator>( |
| profile_->GetPrefs(), support_host_.get()); |
| } |
| |
| if (terms_of_service_negotiator_) { |
| terms_of_service_negotiator_->StartNegotiation( |
| base::Bind(&ArcSessionManager::OnTermsOfServiceNegotiated, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void ArcSessionManager::OnTermsOfServiceNegotiated(bool accepted) { |
| DCHECK(terms_of_service_negotiator_); |
| terms_of_service_negotiator_.reset(); |
| |
| if (!accepted) { |
| // To cancel, user needs to close the window. Note that clicking "Cancel" |
| // button effectively just closes the window. |
| CancelAuthCode(); |
| return; |
| } |
| |
| // Terms were accepted. |
| profile_->GetPrefs()->SetBoolean(prefs::kArcTermsAccepted, true); |
| |
| // Don't show UI for this progress if it was not shown. |
| if (support_host_ && |
| support_host_->ui_page() != ArcSupportHost::UIPage::NO_PAGE) |
| support_host_->ShowArcLoading(); |
| StartArcAndroidManagementCheck(); |
| } |
| |
| void ArcSessionManager::StartArcAndroidManagementCheck() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(arc_session_runner_->IsStopped()); |
| DCHECK(state_ == State::SHOWING_TERMS_OF_SERVICE || |
| state_ == State::CHECKING_ANDROID_MANAGEMENT || |
| (state_ == State::STOPPED && |
| profile_->GetPrefs()->GetBoolean(prefs::kArcTermsAccepted))); |
| SetState(State::CHECKING_ANDROID_MANAGEMENT); |
| |
| android_management_checker_.reset(new ArcAndroidManagementChecker( |
| profile_, context_->token_service(), context_->account_id(), |
| false /* retry_on_error */)); |
| android_management_checker_->StartCheck( |
| base::Bind(&ArcSessionManager::OnAndroidManagementChecked, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ArcSessionManager::OnAndroidManagementChecked( |
| policy::AndroidManagementClient::Result result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_EQ(state_, State::CHECKING_ANDROID_MANAGEMENT); |
| |
| switch (result) { |
| case policy::AndroidManagementClient::Result::UNMANAGED: |
| VLOG(1) << "Starting ARC for first sign in."; |
| sign_in_start_time_ = base::Time::Now(); |
| arc_sign_in_timer_.Start( |
| FROM_HERE, kArcSignInTimeout, |
| base::Bind(&ArcSessionManager::OnArcSignInTimeout, |
| weak_ptr_factory_.GetWeakPtr())); |
| StartArc(); |
| break; |
| case policy::AndroidManagementClient::Result::MANAGED: |
| ShutdownSession(); |
| if (support_host_) { |
| support_host_->ShowError( |
| ArcSupportHost::Error::ANDROID_MANAGEMENT_REQUIRED_ERROR, false); |
| } |
| UpdateOptInCancelUMA(OptInCancelReason::ANDROID_MANAGEMENT_REQUIRED); |
| break; |
| case policy::AndroidManagementClient::Result::ERROR: |
| ShutdownSession(); |
| if (support_host_) { |
| support_host_->ShowError( |
| ArcSupportHost::Error::SERVER_COMMUNICATION_ERROR, false); |
| } |
| UpdateOptInCancelUMA(OptInCancelReason::NETWORK_ERROR); |
| break; |
| } |
| } |
| |
| void ArcSessionManager::OnBackgroundAndroidManagementChecked( |
| policy::AndroidManagementClient::Result result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| switch (result) { |
| case policy::AndroidManagementClient::Result::UNMANAGED: |
| // Do nothing. ARC should be started already. |
| break; |
| case policy::AndroidManagementClient::Result::MANAGED: |
| DisableArc(); |
| break; |
| case policy::AndroidManagementClient::Result::ERROR: |
| // This code should not be reached. For background check, |
| // retry_on_error should be set. |
| NOTREACHED(); |
| } |
| } |
| |
| void ArcSessionManager::OnWindowClosed() { |
| DCHECK(support_host_); |
| if (terms_of_service_negotiator_) { |
| // In this case, ArcTermsOfServiceNegotiator should handle the case. |
| // Do nothing. |
| return; |
| } |
| CancelAuthCode(); |
| } |
| |
| void ArcSessionManager::OnTermsAgreed(bool is_metrics_enabled, |
| bool is_backup_and_restore_enabled, |
| bool is_location_service_enabled) { |
| DCHECK(support_host_); |
| DCHECK(terms_of_service_negotiator_); |
| // This should be handled in ArcTermsOfServiceNegotiator. Do nothing here. |
| } |
| |
| void ArcSessionManager::OnRetryClicked() { |
| DCHECK(support_host_); |
| |
| UpdateOptInActionUMA(OptInActionType::RETRY); |
| |
| // TODO(hidehiko): Simplify the retry logic. |
| if (terms_of_service_negotiator_) { |
| // Currently Terms of service is shown. ArcTermsOfServiceNegotiator should |
| // handle this. |
| } else if (!profile_->GetPrefs()->GetBoolean(prefs::kArcTermsAccepted)) { |
| StartTermsOfServiceNegotiation(); |
| } else if (support_host_->ui_page() == ArcSupportHost::UIPage::ERROR && |
| !arc_session_runner_->IsStopped()) { |
| // ERROR_WITH_FEEDBACK is set in OnSignInFailed(). In the case, stopping |
| // ARC was postponed to contain its internal state into the report. |
| // Here, on retry, stop it, then restart. |
| DCHECK_EQ(State::ACTIVE, state_); |
| support_host_->ShowArcLoading(); |
| ShutdownSession(); |
| reenable_arc_ = true; |
| } else if (state_ == State::ACTIVE) { |
| // This case is handled in ArcAuthService. |
| // Do nothing. |
| } else { |
| // Otherwise, we restart ARC. Note: this is the first boot case. |
| // For second or later boot, either ERROR_WITH_FEEDBACK case or ACTIVE |
| // case must hit. |
| support_host_->ShowArcLoading(); |
| StartArcAndroidManagementCheck(); |
| } |
| } |
| |
| void ArcSessionManager::OnSendFeedbackClicked() { |
| DCHECK(support_host_); |
| chrome::OpenFeedbackDialog(nullptr); |
| } |
| |
| void ArcSessionManager::SetArcSessionRunnerForTesting( |
| std::unique_ptr<ArcSessionRunner> arc_session_runner) { |
| DCHECK(arc_session_runner); |
| DCHECK(arc_session_runner_); |
| DCHECK(arc_session_runner_->IsStopped()); |
| arc_session_runner_->RemoveObserver(this); |
| arc_session_runner_ = std::move(arc_session_runner); |
| arc_session_runner_->AddObserver(this); |
| } |
| |
| void ArcSessionManager::SetAttemptUserExitCallbackForTesting( |
| const base::Closure& callback) { |
| DCHECK(!callback.is_null()); |
| attempt_user_exit_callback_ = callback; |
| } |
| |
| std::ostream& operator<<(std::ostream& os, |
| const ArcSessionManager::State& state) { |
| #define MAP_STATE(name) \ |
| case ArcSessionManager::State::name: \ |
| return os << #name |
| |
| switch (state) { |
| MAP_STATE(NOT_INITIALIZED); |
| MAP_STATE(STOPPED); |
| MAP_STATE(SHOWING_TERMS_OF_SERVICE); |
| MAP_STATE(CHECKING_ANDROID_MANAGEMENT); |
| MAP_STATE(REMOVING_DATA_DIR); |
| MAP_STATE(ACTIVE); |
| } |
| |
| #undef MAP_STATE |
| |
| // Some compilers report an error even if all values of an enum-class are |
| // covered exhaustively in a switch statement. |
| NOTREACHED() << "Invalid value " << static_cast<int>(state); |
| return os; |
| } |
| |
| } // namespace arc |