blob: 73272a9ce8cb046960db146645a1e38a30a97e8c [file] [log] [blame]
// 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 <string>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/strings/string16.h"
#include "base/strings/string_split.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/arc/arc_migration_guide_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/auth/arc_auth_service.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/login/demo_mode/demo_resources.h"
#include "chrome/browser/chromeos/login/demo_mode/demo_session.h"
#include "chrome/browser/chromeos/profiles/profile_helper.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/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.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/app_list/arc/arc_fast_app_reinstall_starter.h"
#include "chrome/browser/ui/app_list/arc/arc_pai_starter.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/browser_commands.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/account_id/account_id.h"
#include "components/arc/arc_data_remover.h"
#include "components/arc/arc_features.h"
#include "components/arc/arc_instance_mode.h"
#include "components/arc/arc_prefs.h"
#include "components/arc/arc_session_runner.h"
#include "components/arc/arc_supervision_transition.h"
#include "components/arc/arc_util.h"
#include "components/arc/metrics/arc_metrics_constants.h"
#include "components/arc/metrics/arc_metrics_service.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "content/public/browser/browser_thread.h"
#include "ui/display/types/display_constants.h"
namespace arc {
namespace {
// Weak pointer. This class is owned by ArcServiceManager.
ArcSessionManager* g_arc_session_manager = nullptr;
// Allows the session manager to skip creating UI in unit tests.
bool g_ui_enabled = true;
base::Optional<bool> g_enable_check_android_management_in_tests;
// 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);
// Updates UMA with user cancel only if error is not currently shown.
void MaybeUpdateOptInCancelUMA(const ArcSupportHost* support_host) {
if (!support_host ||
support_host->ui_page() == ArcSupportHost::UIPage::NO_PAGE ||
support_host->ui_page() == ArcSupportHost::UIPage::ERROR) {
return;
}
UpdateOptInCancelUMA(OptInCancelReason::USER_CANCEL);
}
chromeos::SessionManagerClient* GetSessionManagerClient() {
// If the DBusThreadManager or the SessionManagerClient aren't available,
// there isn't much we can do. This should only happen when running tests.
if (!chromeos::DBusThreadManager::IsInitialized() ||
!chromeos::DBusThreadManager::Get() ||
!chromeos::DBusThreadManager::Get()->GetSessionManagerClient())
return nullptr;
return chromeos::DBusThreadManager::Get()->GetSessionManagerClient();
}
// Returns true if launching the Play Store on OptIn succeeded is needed.
// Launch Play Store app, except for the following cases:
// * When Opt-in verification is disabled (for tests);
// * In case ARC is enabled from OOBE.
// * In ARC Kiosk mode, because the only one UI in kiosk mode must be the
// kiosk app and device is not needed for opt-in;
// * In Public Session mode, because Play Store will be hidden from users
// and only apps configured by policy should be installed.
// * When ARC is managed and all OptIn preferences are managed/unused, too,
// because the whole OptIn flow should happen as seamless as possible for
// the user.
// For Active Directory users we always show a page notifying them that they
// have to authenticate with their identity provider (through SAML) to make
// it less weird that a browser window pops up.
// Some tests require the Play Store to be shown and forces this using chromeos
// switch kArcForceShowPlayStoreApp.
bool ShouldLaunchPlayStoreApp(Profile* profile,
bool oobe_or_assistant_wizard_start) {
if (!IsPlayStoreAvailable())
return false;
if (oobe_or_assistant_wizard_start)
return false;
if (ShouldShowOptInForTesting())
return true;
if (IsRobotOrOfflineDemoAccountMode())
return false;
if (IsArcOptInVerificationDisabled())
return false;
if (IsArcPlayStoreEnabledPreferenceManagedForProfile(profile) &&
AreArcAllOptInPreferencesIgnorableForProfile(profile)) {
return false;
}
return true;
}
} // namespace
// This class is used to track statuses on OptIn flow. It is created in case ARC
// is activated, and it needs to OptIn. Once started OptInFlowResult::STARTED is
// recorded via UMA. If it finishes successfully OptInFlowResult::SUCCEEDED is
// recorded. Optional OptInFlowResult::SUCCEEDED_AFTER_RETRY is recorded in this
// case if an error occurred during OptIn flow, and user pressed Retry. In case
// the user cancels OptIn flow before it was completed then
// OptInFlowResult::CANCELED is recorded and if an error occurred optional
// OptInFlowResult::CANCELED_AFTER_ERROR. If a shutdown happens during the OptIn
// nothing is recorded, except initial OptInFlowResult::STARTED.
// OptInFlowResult::STARTED = OptInFlowResult::SUCCEEDED +
// OptInFlowResult::CANCELED + cases happened during the shutdown.
class ArcSessionManager::ScopedOptInFlowTracker {
public:
ScopedOptInFlowTracker() {
UpdateOptInFlowResultUMA(OptInFlowResult::STARTED);
}
~ScopedOptInFlowTracker() {
if (shutdown_)
return;
UpdateOptInFlowResultUMA(success_ ? OptInFlowResult::SUCCEEDED
: OptInFlowResult::CANCELED);
if (error_) {
UpdateOptInFlowResultUMA(success_
? OptInFlowResult::SUCCEEDED_AFTER_RETRY
: OptInFlowResult::CANCELED_AFTER_ERROR);
}
}
// Tracks error occurred during the OptIn flow.
void TrackError() {
DCHECK(!success_ && !shutdown_);
error_ = true;
}
// Tracks that OptIn finished successfully.
void TrackSuccess() {
DCHECK(!success_ && !shutdown_);
success_ = true;
}
// Tracks that OptIn was not completed before shutdown.
void TrackShutdown() {
DCHECK(!success_ && !shutdown_);
shutdown_ = true;
}
private:
bool error_ = false;
bool success_ = false;
bool shutdown_ = false;
DISALLOW_COPY_AND_ASSIGN(ScopedOptInFlowTracker);
};
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);
chromeos::SessionManagerClient* client = GetSessionManagerClient();
if (client)
client->AddObserver(this);
}
ArcSessionManager::~ArcSessionManager() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
chromeos::SessionManagerClient* client = GetSessionManagerClient();
if (client)
client->RemoveObserver(this);
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::SetUiEnabledForTesting(bool enable) {
g_ui_enabled = enable;
}
// static
void ArcSessionManager::EnableCheckAndroidManagementForTesting(bool enable) {
g_enable_check_android_management_in_tests = enable;
}
void ArcSessionManager::OnSessionStopped(ArcStopReason reason,
bool restarting) {
if (restarting) {
DCHECK_EQ(state_, State::ACTIVE);
// If ARC is being restarted, here do nothing, and just wait for its
// next run.
VLOG(1) << "ARC session is stopped, but being restarted: " << reason;
return;
}
DCHECK(state_ == State::ACTIVE || state_ == State::STOPPING) << state_;
state_ = State::STOPPED;
// TODO(crbug.com/625923): Use |reason| to report more detailed errors.
if (arc_sign_in_timer_.IsRunning())
OnProvisioningFinished(ProvisioningResult::ARC_STOPPED);
for (auto& observer : observer_list_)
observer.OnArcSessionStopped(reason);
MaybeStartArcDataRemoval();
}
void ArcSessionManager::OnSessionRestarting() {
for (auto& observer : observer_list_)
observer.OnArcSessionRestarting();
}
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 (!enable_requested_) {
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.
const bool provisioning_successful =
result == ProvisioningResult::SUCCESS ||
result == ProvisioningResult::SUCCESS_ALREADY_PROVISIONED;
if (provisioning_reported_) {
// We don't expect ProvisioningResult::SUCCESS or
// ProvisioningResult::SUCCESS_ALREADY_PROVISIONED to be reported twice or
// reported after an error.
DCHECK(!provisioning_successful);
// 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 " << result << ".";
return;
}
provisioning_reported_ = true;
if (scoped_opt_in_tracker_ && !provisioning_successful)
scoped_opt_in_tracker_->TrackError();
if (result == ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR) {
// TODO(poromov): Consider ARC PublicSession offline mode.
// Currently ARC session will be exited below, while the main user session
// will be kept alive without Android apps.
if (IsRobotOrOfflineDemoAccountMode())
VLOG(1) << "Robot account auth code fetching error";
if (IsArcKioskMode()) {
VLOG(1) << "Exiting kiosk session due to provisioning failure";
// 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_,
provisioning_successful, profile_);
UpdateProvisioningResultUMA(result, profile_);
if (!provisioning_successful)
UpdateOptInCancelUMA(OptInCancelReason::CLOUD_PROVISION_FLOW_FAIL);
}
if (provisioning_successful) {
if (support_host_)
support_host_->Close();
if (scoped_opt_in_tracker_) {
scoped_opt_in_tracker_->TrackSuccess();
scoped_opt_in_tracker_.reset();
}
PrefService* const prefs = profile_->GetPrefs();
if (prefs->GetBoolean(prefs::kArcSignedIn))
return;
prefs->SetBoolean(prefs::kArcSignedIn, true);
if (ShouldLaunchPlayStoreApp(
profile_,
prefs->GetBoolean(prefs::kArcProvisioningInitiatedFromOobe))) {
playstore_launcher_ = std::make_unique<ArcAppLauncher>(
profile_, kPlayStoreAppId,
GetLaunchIntent(kPlayStorePackage, kPlayStoreActivity,
{kInitialStartParam}),
false /* deferred_launch_allowed */, display::kInvalidDisplayId,
arc::UserInteractionType::NOT_USER_INITIATED);
}
prefs->ClearPref(prefs::kArcProvisioningInitiatedFromOobe);
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;
case ProvisioningResult::NO_NETWORK_CONNECTION:
error = ArcSupportHost::Error::NETWORK_UNAVAILABLE_ERROR;
break;
case ProvisioningResult::ARC_DISABLED:
error = ArcSupportHost::Error::ANDROID_MANAGEMENT_REQUIRED_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();
ShowArcSupportHostError(error, true);
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";
RequestArcDataRemoval();
}
// We'll delay shutting down the ARC instance in this case to allow people
// to send feedback.
ShowArcSupportHostError(error, true /* = show send feedback button */);
}
bool ArcSessionManager::IsAllowed() const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return profile_ != nullptr;
}
void ArcSessionManager::SetProfile(Profile* profile) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!profile || !profile_);
DCHECK(!profile || IsArcAllowedForProfile(profile));
profile_ = profile;
}
void ArcSessionManager::Initialize() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
DCHECK_EQ(state_, State::NOT_INITIALIZED);
state_ = State::STOPPED;
// 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_ui_enabled && !IsArcOptInVerificationDisabled() &&
!IsRobotOrOfflineDemoAccountMode()) {
DCHECK(!support_host_);
support_host_ = std::make_unique<ArcSupportHost>(profile_);
support_host_->SetErrorDelegate(this);
}
data_remover_ = std::make_unique<ArcDataRemover>(
profile_->GetPrefs(),
cryptohome::Identification(
multi_user_util::GetAccountIdFromProfile(profile_)));
if (g_enable_check_android_management_in_tests.value_or(g_ui_enabled))
ArcAndroidManagementChecker::StartClient();
// Request removing data if enabled for a regular->child transition.
if (GetSupervisionTransition(profile_) ==
ArcSupervisionTransition::REGULAR_TO_CHILD &&
base::FeatureList::IsEnabled(
kCleanArcDataOnRegularToChildTransitionFeature)) {
LOG(WARNING) << "User transited from regular to child, deleting ARC data";
// Since method below starts removal procedure automatically, return.
RequestArcDataRemoval();
return;
}
// Chrome may be shut down before completing ARC data removal.
// For such a case, start removing the data now, if necessary.
MaybeStartArcDataRemoval();
}
void ArcSessionManager::Shutdown() {
enable_requested_ = false;
ResetArcState();
arc_session_runner_->OnShutdown();
data_remover_.reset();
if (support_host_) {
support_host_->SetErrorDelegate(nullptr);
support_host_->Close();
support_host_.reset();
}
pai_starter_.reset();
fast_app_reinstall_starter_.reset();
profile_ = nullptr;
state_ = State::NOT_INITIALIZED;
if (scoped_opt_in_tracker_) {
scoped_opt_in_tracker_->TrackShutdown();
scoped_opt_in_tracker_.reset();
}
}
void ArcSessionManager::ShutdownSession() {
ResetArcState();
switch (state_) {
case State::NOT_INITIALIZED:
// Ignore in NOT_INITIALIZED case. This is called in initial SetProfile
// invocation.
// TODO(hidehiko): Remove this along with the clean up.
break;
case State::STOPPED:
// Currently, ARC is stopped. Do nothing.
break;
case State::NEGOTIATING_TERMS_OF_SERVICE:
case State::CHECKING_ANDROID_MANAGEMENT:
// We need to kill the mini-container that might be running here.
arc_session_runner_->RequestStop();
// While RequestStop is asynchronous, ArcSessionManager is agnostic to the
// state of the mini-container, so we can set it's state_ to STOPPED
// immediately.
state_ = State::STOPPED;
break;
case State::REMOVING_DATA_DIR:
// When data removing is done, |state_| will be set to STOPPED.
// Do nothing here.
break;
case State::ACTIVE:
// Request to stop the ARC. |state_| will be set to STOPPED eventually.
// Set state before requesting the runner to stop in order to prevent the
// case when |OnSessionStopped| can be called inline and as result
// |state_| might be changed.
state_ = State::STOPPING;
arc_session_runner_->RequestStop();
break;
case State::STOPPING:
// Now ARC is stopping. Do nothing here.
break;
}
}
void ArcSessionManager::ResetArcState() {
arc_sign_in_timer_.Stop();
playstore_launcher_.reset();
terms_of_service_negotiator_.reset();
android_management_checker_.reset();
}
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::NotifyArcPlayStoreEnabledChanged(bool enabled) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
for (auto& observer : observer_list_)
observer.OnArcPlayStoreEnabledChanged(enabled);
}
// This is the special method to support enterprise mojo API.
// TODO(hidehiko): Remove this.
void ArcSessionManager::StopAndEnableArc() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
reenable_arc_ = true;
StopArc();
}
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
// ACTIVE_DIRECTORY_AUTH, closing the window should stop ARC since the user
// chooses to not sign in. In any other case, ARC is booting normally and
// the instance should not be stopped.
if ((state_ != State::NEGOTIATING_TERMS_OF_SERVICE &&
state_ != State::CHECKING_ANDROID_MANAGEMENT) &&
(!support_host_ ||
(support_host_->ui_page() != ArcSupportHost::UIPage::ERROR &&
support_host_->ui_page() !=
ArcSupportHost::UIPage::ACTIVE_DIRECTORY_AUTH))) {
return;
}
MaybeUpdateOptInCancelUMA(support_host_.get());
StopArc();
SetArcPlayStoreEnabledForProfile(profile_, false);
}
void ArcSessionManager::RecordArcState() {
// Only record legacy 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(enable_requested_);
UpdateEnabledStateByUserTypeUMA(enable_requested_, profile_);
ArcMetricsService* service =
ArcMetricsService::GetForBrowserContext(profile_);
service->RecordNativeBridgeUMA();
return;
}
const Profile* profile = ProfileManager::GetPrimaryUserProfile();
// Don't record UMA for the set of cases:
// * No primary profile is set at this moment.
// * Primary profile matches the built-in profile used for signing in or the
// lock screen.
// * Primary profile matches guest session.
// * Primary profile is in incognito mode.
if (!profile || chromeos::ProfileHelper::IsSigninProfile(profile) ||
chromeos::ProfileHelper::IsLockScreenAppProfile(profile) ||
profile->IsOffTheRecord() || profile->IsGuestSession()) {
return;
}
UpdateEnabledStateByUserTypeUMA(enable_requested_, profile);
}
void ArcSessionManager::RequestEnable() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
if (enable_requested_) {
VLOG(1) << "ARC is already enabled. Do nothing.";
return;
}
enable_requested_ = true;
VLOG(1) << "ARC opt-in. Starting ARC session.";
// |directly_started_| flag must be preserved during the internal ARC restart.
// So set it only when ARC is externally requested to start.
directly_started_ = RequestEnableImpl();
}
bool ArcSessionManager::IsPlaystoreLaunchRequestedForTesting() const {
return playstore_launcher_.get();
}
bool ArcSessionManager::RequestEnableImpl() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
DCHECK(enable_requested_);
DCHECK(state_ == State::STOPPED || state_ == State::STOPPING ||
state_ == State::REMOVING_DATA_DIR)
<< state_;
if (state_ != State::STOPPED) {
// If the previous invocation of ARC is still running (but currently being
// stopped) or ARC data removal is in progress, postpone the enabling
// procedure.
reenable_arc_ = true;
return false;
}
PrefService* const prefs = profile_->GetPrefs();
// |prefs::kArcProvisioningInitiatedFromOobe| is used to remember
// |IsArcOobeOptInActive| or |IsArcOptInWizardForAssistantActive| state when
// ARC start request was made initially. |IsArcOobeOptInActive| or
// |IsArcOptInWizardForAssistantActive| will be changed by the time when
// decision to auto-launch the Play Store would be made.
// |IsArcOobeOptInActive| and |IsArcOptInWizardForAssistantActive| are not
// preserved on Chrome restart also and in last case
// |prefs::kArcProvisioningInitiatedFromOobe| is used to remember the state of
// the initial request.
// |prefs::kArcProvisioningInitiatedFromOobe| is reset when provisioning is
// done or ARC is opted out.
if (IsArcOobeOptInActive() || IsArcOptInWizardForAssistantActive())
prefs->SetBoolean(prefs::kArcProvisioningInitiatedFromOobe, true);
// If it is marked that sign in has been successfully done or if Play Store is
// not available, then directly start ARC with skipping Play Store ToS.
// For Kiosk mode, skip ToS because it is very likely that near the device
// there will be no one who is eligible to accept them.
// In Public Session mode ARC should be started silently without user
// interaction. If opt-in verification is disabled, skip negotiation, too.
// This is for testing purpose.
const bool start_arc_directly =
prefs->GetBoolean(prefs::kArcSignedIn) || ShouldArcAlwaysStart() ||
IsRobotOrOfflineDemoAccountMode() || IsArcOptInVerificationDisabled();
// When ARC is blocked because of filesystem compatibility, do not proceed
// to starting ARC nor follow further state transitions.
if (IsArcBlockedDueToIncompatibleFileSystem(profile_)) {
// If the next step was the ToS negotiation, show a notification instead.
// Otherwise, be silent now. Users are notified when clicking ARC app icons.
if (!start_arc_directly && g_ui_enabled)
arc::ShowArcMigrationGuideNotification(profile_);
return false;
}
if (!pai_starter_ && IsPlayStoreAvailable()) {
pai_starter_ =
ArcPaiStarter::CreateIfNeeded(profile_, profile_->GetPrefs());
}
if (!fast_app_reinstall_starter_ && IsPlayStoreAvailable()) {
fast_app_reinstall_starter_ = ArcFastAppReinstallStarter::CreateIfNeeded(
profile_, profile_->GetPrefs());
}
if (start_arc_directly) {
StartArc();
// When in ARC kiosk mode, there's no Chrome tabs to restore. Remove the
// cgroups now.
if (IsArcKioskMode())
SetArcCpuRestriction(false /* do_restrict */);
// Check Android management in parallel.
// Note: StartBackgroundAndroidManagementCheck() may call
// OnBackgroundAndroidManagementChecked() synchronously (or
// asynchronously). In the callback, Google Play Store enabled preference
// can be set to false if managed, and it triggers RequestDisable() via
// ArcPlayStoreEnabledPreferenceHandler.
// Thus, StartArc() should be called so that disabling should work even
// if synchronous call case.
StartBackgroundAndroidManagementCheck();
return true;
}
MaybeStartTermsOfServiceNegotiation();
return false;
}
void ArcSessionManager::RequestDisable() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
if (!enable_requested_) {
VLOG(1) << "ARC is already disabled. "
<< "Killing an instance for login screen (if any).";
arc_session_runner_->RequestStop();
return;
}
directly_started_ = false;
enable_requested_ = false;
scoped_opt_in_tracker_.reset();
pai_starter_.reset();
fast_app_reinstall_starter_.reset();
// Reset any pending request to re-enable ARC.
reenable_arc_ = false;
StopArc();
VLOG(1) << "ARC opt-out. Removing user data.";
RequestArcDataRemoval();
}
void ArcSessionManager::RequestArcDataRemoval() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
DCHECK(data_remover_);
// TODO(hidehiko): DCHECK the previous state. This is called for four cases;
// 1) Supporting managed user initial disabled case (Please see also
// ArcPlayStoreEnabledPreferenceHandler::Start() for details).
// 2) Supporting enterprise triggered data removal.
// 3) One called in OnProvisioningFinished().
// 4) On request disabling.
// After the state machine is fixed, 2) should be replaced by
// RequestDisable() immediately followed by RequestEnable().
// 3) and 4) are internal state transition. So, as for public interface, 1)
// should be the only use case, and the |state_| should be limited to
// STOPPED, then.
// TODO(hidehiko): Think a way to get rid of 1), too.
data_remover_->Schedule();
profile_->GetPrefs()->SetInteger(
prefs::kArcSupervisionTransition,
static_cast<int>(ArcSupervisionTransition::NO_TRANSITION));
// To support 1) case above, maybe start data removal.
if (state_ == State::STOPPED)
MaybeStartArcDataRemoval();
}
void ArcSessionManager::MaybeStartTermsOfServiceNegotiation() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
DCHECK(!terms_of_service_negotiator_);
// In Kiosk and Public Session mode, Terms of Service negotiation should be
// skipped. See also RequestEnableImpl().
DCHECK(!IsRobotOrOfflineDemoAccountMode());
// If opt-in verification is disabled, Terms of Service negotiation should
// be skipped, too. See also RequestEnableImpl().
DCHECK(!IsArcOptInVerificationDisabled());
DCHECK_EQ(state_, State::STOPPED);
state_ = State::NEGOTIATING_TERMS_OF_SERVICE;
// TODO(hidehiko): In kArcSignedIn = true case, this method should never
// be called. Remove the check.
// Conceptually, this is starting ToS negotiation, rather than opt-in flow.
// Move to RequestEnabledImpl.
if (!scoped_opt_in_tracker_ &&
!profile_->GetPrefs()->GetBoolean(prefs::kArcSignedIn)) {
scoped_opt_in_tracker_ = std::make_unique<ScopedOptInFlowTracker>();
}
if (!IsArcTermsOfServiceNegotiationNeeded(profile_)) {
if (IsArcStatsReportingEnabled() &&
!profile_->GetPrefs()->GetBoolean(prefs::kArcTermsAccepted)) {
// Don't enable stats reporting for users who are not shown the reporting
// notice during ARC setup.
profile_->GetPrefs()->SetBoolean(prefs::kArcSkippedReportingNotice, true);
}
// Moves to next state, Android management check, immediately, as if
// Terms of Service negotiation is done successfully.
StartAndroidManagementCheck();
return;
}
if (IsArcOobeOptInActive() || IsArcOptInWizardForAssistantActive()) {
VLOG(1) << "Use OOBE negotiator.";
terms_of_service_negotiator_ =
std::make_unique<ArcTermsOfServiceOobeNegotiator>();
} else if (support_host_) {
VLOG(1) << "Use default negotiator.";
terms_of_service_negotiator_ =
std::make_unique<ArcTermsOfServiceDefaultNegotiator>(
profile_->GetPrefs(), support_host_.get());
}
if (!terms_of_service_negotiator_) {
// The only case reached here is when g_ui_enabled is false so ARC support
// host is not created in SetProfile(), for testing purpose.
DCHECK(!g_ui_enabled) << "Negotiator is not created on production.";
return;
}
// Start the mini-container here to save time starting the container if the
// user decides to opt-in.
arc_session_runner_->RequestStartMiniInstance();
terms_of_service_negotiator_->StartNegotiation(
base::Bind(&ArcSessionManager::OnTermsOfServiceNegotiated,
weak_ptr_factory_.GetWeakPtr()));
}
void ArcSessionManager::OnTermsOfServiceNegotiated(bool accepted) {
DCHECK_EQ(state_, State::NEGOTIATING_TERMS_OF_SERVICE);
DCHECK(profile_);
DCHECK(terms_of_service_negotiator_ || !g_ui_enabled);
terms_of_service_negotiator_.reset();
if (!accepted) {
VLOG(1) << "Terms of services declined";
// User does not accept the Terms of Service. Disable Google Play Store.
MaybeUpdateOptInCancelUMA(support_host_.get());
SetArcPlayStoreEnabledForProfile(profile_, false);
return;
}
// Terms were accepted.
VLOG(1) << "Terms of services accepted";
profile_->GetPrefs()->SetBoolean(prefs::kArcTermsAccepted, true);
StartAndroidManagementCheck();
}
void ArcSessionManager::StartAndroidManagementCheck() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// State::STOPPED appears here in following scenario.
// Initial provisioning finished with state
// ProvisioningResult::ArcStop or
// ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR.
// At this moment |prefs::kArcTermsAccepted| is set to true, once user
// confirmed ToS prior to provisioning flow. Once user presses "Try Again"
// button, OnRetryClicked calls this immediately.
DCHECK(state_ == State::NEGOTIATING_TERMS_OF_SERVICE ||
state_ == State::CHECKING_ANDROID_MANAGEMENT ||
state_ == State::STOPPED)
<< state_;
state_ = State::CHECKING_ANDROID_MANAGEMENT;
// Show loading UI only if ARC support app's window is already shown.
// User may not see any ARC support UI if everything needed is done in
// background. In such a case, showing loading UI here (then closed sometime
// soon later) would look just noisy.
if (support_host_ &&
support_host_->ui_page() != ArcSupportHost::UIPage::NO_PAGE) {
support_host_->ShowArcLoading();
}
for (auto& observer : observer_list_)
observer.OnArcOptInManagementCheckStarted();
if (!g_ui_enabled)
return;
android_management_checker_ = std::make_unique<ArcAndroidManagementChecker>(
profile_, 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);
DCHECK(android_management_checker_);
android_management_checker_.reset();
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();
// Since opt-in is an explicit user (or admin) action, relax the
// cgroups restriction now.
SetArcCpuRestriction(false /* do_restrict */);
break;
case policy::AndroidManagementClient::Result::MANAGED:
ShowArcSupportHostError(
ArcSupportHost::Error::ANDROID_MANAGEMENT_REQUIRED_ERROR, false);
UpdateOptInCancelUMA(OptInCancelReason::ANDROID_MANAGEMENT_REQUIRED);
break;
case policy::AndroidManagementClient::Result::ERROR:
ShowArcSupportHostError(ArcSupportHost::Error::SERVER_COMMUNICATION_ERROR,
true);
UpdateOptInCancelUMA(OptInCancelReason::NETWORK_ERROR);
break;
}
}
void ArcSessionManager::StartBackgroundAndroidManagementCheck() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(state_, State::ACTIVE);
DCHECK(!android_management_checker_);
// Skip Android management check for testing.
// We also skip if Android management check for Kiosk and Public Session mode,
// because there are no managed human users for them exist.
if (IsArcOptInVerificationDisabled() || IsRobotOrOfflineDemoAccountMode() ||
(!g_ui_enabled &&
!g_enable_check_android_management_in_tests.value_or(false))) {
return;
}
android_management_checker_ = std::make_unique<ArcAndroidManagementChecker>(
profile_, true /* retry_on_error */);
android_management_checker_->StartCheck(
base::Bind(&ArcSessionManager::OnBackgroundAndroidManagementChecked,
weak_ptr_factory_.GetWeakPtr()));
}
void ArcSessionManager::OnBackgroundAndroidManagementChecked(
policy::AndroidManagementClient::Result result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(android_management_checker_);
android_management_checker_.reset();
switch (result) {
case policy::AndroidManagementClient::Result::UNMANAGED:
// Do nothing. ARC should be started already.
break;
case policy::AndroidManagementClient::Result::MANAGED:
SetArcPlayStoreEnabledForProfile(profile_, false);
break;
case policy::AndroidManagementClient::Result::ERROR:
// This code should not be reached. For background check,
// retry_on_error should be set.
NOTREACHED();
}
}
void ArcSessionManager::StartArc() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(state_ == State::STOPPED ||
state_ == State::CHECKING_ANDROID_MANAGEMENT)
<< state_;
state_ = State::ACTIVE;
// ARC must be started only if no pending data removal request exists.
DCHECK(!profile_->GetPrefs()->GetBoolean(prefs::kArcDataRemoveRequested));
for (auto& observer : observer_list_)
observer.OnArcStarted();
arc_start_time_ = base::Time::Now();
provisioning_reported_ = false;
std::string locale;
std::string preferred_languages;
GetLocaleAndPreferredLanguages(profile_, &locale, &preferred_languages);
ArcSession::UpgradeParams params;
const chromeos::DemoSession* demo_session = chromeos::DemoSession::Get();
params.is_demo_session = demo_session && demo_session->started();
if (params.is_demo_session) {
DCHECK(demo_session->resources()->loaded());
params.demo_session_apps_path =
demo_session->resources()->GetDemoAppsPath();
}
params.supervision_transition = GetSupervisionTransition(profile_);
params.locale = locale;
// Empty |preferred_languages| is converted to empty array.
params.preferred_languages = base::SplitString(
preferred_languages, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
arc_session_runner_->RequestUpgrade(std::move(params));
}
void ArcSessionManager::StopArc() {
// TODO(hidehiko): This STOPPED guard should be unnecessary. Remove it later.
// |reenable_arc_| may be set in |StopAndEnableArc| in case enterprise
// management state is lost.
if (!reenable_arc_ && state_ != State::STOPPED) {
profile_->GetPrefs()->SetBoolean(prefs::kArcSignedIn, false);
profile_->GetPrefs()->SetBoolean(prefs::kArcPaiStarted, false);
profile_->GetPrefs()->SetBoolean(prefs::kArcTermsAccepted, false);
profile_->GetPrefs()->SetBoolean(prefs::kArcFastAppReinstallStarted, false);
profile_->GetPrefs()->SetBoolean(prefs::kArcProvisioningInitiatedFromOobe,
false);
}
ShutdownSession();
if (support_host_)
support_host_->Close();
}
void ArcSessionManager::MaybeStartArcDataRemoval() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(profile_);
// Data removal cannot run in parallel with ARC session.
// LoginScreen instance does not use data directory, so removing should work.
DCHECK_EQ(state_, State::STOPPED);
state_ = State::REMOVING_DATA_DIR;
data_remover_->Run(base::BindOnce(&ArcSessionManager::OnArcDataRemoved,
weak_ptr_factory_.GetWeakPtr()));
}
void ArcSessionManager::OnArcDataRemoved(base::Optional<bool> result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(state_, State::REMOVING_DATA_DIR);
DCHECK(profile_);
state_ = State::STOPPED;
if (result.has_value()) {
// Remove Play user ID for Active Directory managed devices.
profile_->GetPrefs()->SetString(prefs::kArcActiveDirectoryPlayUserId,
std::string());
// Regardless of whether it is successfully done or not, notify observers.
for (auto& observer : observer_list_)
observer.OnArcDataRemoved();
// Note: Currently, we may re-enable ARC even if data removal fails.
// We may have to avoid it.
}
MaybeReenableArc();
}
void ArcSessionManager::MaybeReenableArc() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(state_, State::STOPPED);
if (!reenable_arc_) {
// Re-enabling is not triggered. Do nothing.
return;
}
DCHECK(enable_requested_);
// 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";
RequestEnableImpl();
}
void ArcSessionManager::OnWindowClosed() {
CancelAuthCode();
}
void ArcSessionManager::OnRetryClicked() {
DCHECK(!g_ui_enabled || support_host_);
DCHECK(!g_ui_enabled ||
support_host_->ui_page() == ArcSupportHost::UIPage::ERROR);
DCHECK(!terms_of_service_negotiator_);
DCHECK(!g_ui_enabled || !support_host_->HasAuthDelegate());
UpdateOptInActionUMA(OptInActionType::RETRY);
if (state_ == State::ACTIVE) {
// 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.
if (support_host_)
support_host_->ShowArcLoading();
// In unit tests ShutdownSession may be executed inline and OnSessionStopped
// is called before |reenable_arc_| is set.
reenable_arc_ = true;
ShutdownSession();
} else {
// Otherwise, we start ARC once it is stopped now. Usually ARC container is
// left active after provisioning failure but in case
// ProvisioningResult::ARC_STOPPED and
// ProvisioningResult::CHROME_SERVER_COMMUNICATION_ERROR failures container
// is stopped.
// At this point ToS is already accepted and
// IsArcTermsOfServiceNegotiationNeeded returns true or ToS needs not to be
// shown at all. However there is an exception when this does not happen in
// case an error page is shown when re-opt-in right after opt-out (this is a
// bug as it should not show an error). When the user click the retry
// button on this error page, we may start ToS negotiation instead of
// recreating the instance.
// TODO(hidehiko): consider removing this case after fixing the bug.
MaybeStartTermsOfServiceNegotiation();
}
}
void ArcSessionManager::OnSendFeedbackClicked() {
DCHECK(support_host_);
chrome::OpenFeedbackDialog(nullptr, chrome::kFeedbackSourceArcApp);
}
void ArcSessionManager::SetArcSessionRunnerForTesting(
std::unique_ptr<ArcSessionRunner> arc_session_runner) {
DCHECK(arc_session_runner);
DCHECK(arc_session_runner_);
arc_session_runner_->RemoveObserver(this);
arc_session_runner_ = std::move(arc_session_runner);
arc_session_runner_->AddObserver(this);
}
ArcSessionRunner* ArcSessionManager::GetArcSessionRunnerForTesting() {
return arc_session_runner_.get();
}
void ArcSessionManager::SetAttemptUserExitCallbackForTesting(
const base::Closure& callback) {
DCHECK(!callback.is_null());
attempt_user_exit_callback_ = callback;
}
void ArcSessionManager::ShowArcSupportHostError(
ArcSupportHost::Error error,
bool should_show_send_feedback) {
if (support_host_)
support_host_->ShowError(error, should_show_send_feedback);
for (auto& observer : observer_list_)
observer.OnArcErrorShowRequested(error);
}
void ArcSessionManager::EmitLoginPromptVisibleCalled() {
// Since 'login-prompt-visible' Upstart signal starts all Upstart jobs the
// instance may depend on such as cras, EmitLoginPromptVisibleCalled() is the
// safe place to start a mini instance.
if (!IsArcAvailable())
return;
arc_session_runner_->RequestStartMiniInstance();
}
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(NEGOTIATING_TERMS_OF_SERVICE);
MAP_STATE(CHECKING_ANDROID_MANAGEMENT);
MAP_STATE(REMOVING_DATA_DIR);
MAP_STATE(ACTIVE);
MAP_STATE(STOPPING);
}
#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