Chromium Code Reviews| Index: chrome/browser/chromeos/device_settings_provider.cc |
| diff --git a/chrome/browser/chromeos/device_settings_provider.cc b/chrome/browser/chromeos/device_settings_provider.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..6875973e2ee095611efcae754bb48eb7ea350bfc |
| --- /dev/null |
| +++ b/chrome/browser/chromeos/device_settings_provider.cc |
| @@ -0,0 +1,560 @@ |
| +// Copyright (c) 2011 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/device_settings_provider.h" |
| + |
| +#include "base/base64.h" |
| +#include "base/bind.h" |
| +#include "base/bind_helpers.h" |
| +#include "base/callback.h" |
| +#include "base/hash_tables.h" |
| +#include "base/logging.h" |
| +#include "base/memory/singleton.h" |
| +#include "base/string_util.h" |
| +#include "base/values.h" |
| +#include "chrome/browser/browser_process.h" |
| +#include "chrome/browser/chromeos/cros/cros_library.h" |
| +#include "chrome/browser/chromeos/cros/network_library.h" |
| +#include "chrome/browser/chromeos/cros_settings.h" |
| +#include "chrome/browser/chromeos/cros_settings_names.h" |
| +#include "chrome/browser/chromeos/login/ownership_service.h" |
| +#include "chrome/browser/chromeos/login/ownership_status_checker.h" |
| +#include "chrome/browser/chromeos/login/signed_settings_cache.h" |
| +#include "chrome/browser/chromeos/login/user_manager.h" |
| +#include "chrome/browser/policy/proto/chrome_device_policy.pb.h" |
| +#include "chrome/browser/policy/proto/device_management_backend.pb.h" |
| +#include "chrome/browser/prefs/pref_service.h" |
| +#include "chrome/browser/prefs/pref_value_map.h" |
| +#include "chrome/browser/prefs/scoped_user_pref_update.h" |
| +#include "chrome/browser/ui/options/options_util.h" |
| +#include "chrome/common/chrome_notification_types.h" |
| +#include "chrome/installer/util/google_update_settings.h" |
| +#include "content/public/browser/browser_thread.h" |
| +#include "content/public/browser/notification_service.h" |
| + |
| +using content::BrowserThread; |
| +using google::protobuf::RepeatedPtrField; |
| + |
| +namespace chromeos { |
| + |
| +namespace { |
| + |
| +const char* kBooleanSettings[] = { |
| + kAccountsPrefAllowNewUser, |
| + kAccountsPrefAllowGuest, |
| + kAccountsPrefShowUserNamesOnSignIn, |
| + kSignedDataRoamingEnabled, |
| + kStatsReportingPref |
| +}; |
| + |
| +const char* kStringSettings[] = { |
| + kDeviceOwner, |
| + kReleaseChannel, |
| + kSettingProxyEverywhere |
| +}; |
| + |
| +const char* kListSettings[] = { |
| + kAccountsPrefUsers |
| +}; |
| + |
| +// Upper bound for number of retries to fetch a signed setting. |
| +static const int kNumRetriesLimit = 9; |
| + |
| +bool IsControlledBooleanSetting(const std::string& pref_path) { |
| + const char** end = kBooleanSettings + arraysize(kBooleanSettings); |
| + return std::find(kBooleanSettings, end, pref_path) != end; |
| +} |
| + |
| +bool IsControlledStringSetting(const std::string& pref_path) { |
| + const char** end = kStringSettings + arraysize(kStringSettings); |
| + return std::find(kStringSettings, end, pref_path) != end; |
| +} |
| + |
| +bool IsControlledListSetting(const std::string& pref_path) { |
| + const char** end = kListSettings + arraysize(kListSettings); |
| + return std::find(kListSettings, end, pref_path) != end; |
| +} |
| + |
| +bool IsControlledSetting(const std::string& pref_path) { |
| + return (IsControlledBooleanSetting(pref_path) || |
| + IsControlledStringSetting(pref_path) || |
| + IsControlledListSetting(pref_path)); |
| +} |
| + |
| +bool HasOldMetricsFile() { |
| + // TODO(pastarmovj): Remove this once migration is not needed anymore. |
| + // If the value is not set we should try to migrate legacy consent file. |
| + // Loading consent file state causes us to do blocking IO on UI thread. |
| + // Temporarily allow it until we fix http://crbug.com/62626 |
| + base::ThreadRestrictions::ScopedAllowIO allow_io; |
| + return GoogleUpdateSettings::GetCollectStatsConsent(); |
| +} |
| + |
| +} // namespace |
| + |
| +DeviceSettingsProvider::DeviceSettingsProvider() |
| + : ownership_service_(OwnershipService::GetSharedInstance()), |
| + migration_helper_(new SignedSettingsMigrationHelper()), |
| + retries_left_(kNumRetriesLimit), |
| + trusted_(false) { |
| + values_cache_.SetBoolean("aa_dummy", true); |
| + // Register for notification when ownership is taken so that we can finalize |
| + // the SignedSettingsCache. |
| + registrar_.Add(this, chrome::NOTIFICATION_OWNER_KEY_FETCH_ATTEMPT_SUCCEEDED, |
| + content::NotificationService::AllSources()); |
| + // Make sure we have at least the cache data immediately. |
| + RetrieveCachedData(); |
| + // Start prefetching preferences. |
| + Reload(); |
| +} |
| + |
| +DeviceSettingsProvider::~DeviceSettingsProvider() { |
| +} |
| + |
| +void DeviceSettingsProvider::Reload() { |
| + // While fetching we can't trust the cache anymore. |
| + trusted_ = false; |
| + OwnershipService::Status ownership_status = |
| + ownership_service_->GetStatus(true); |
| + if (ownership_status == OwnershipService::OWNERSHIP_NONE) { |
| + RetrieveCachedData(); |
| + } else { |
| + // Retrieve the real data. |
| + SignedSettingsHelper::Get()->StartRetrievePolicyOp( |
| + base::Bind(&DeviceSettingsProvider::OnRetrievePolicyCompleted, |
| + base::Unretained(this))); |
| + } |
| +} |
| + |
| +void DeviceSettingsProvider::DoSet(const std::string& path, |
| + const base::Value& in_value) { |
| + OwnershipService::Status ownership_status = |
| + ownership_service_->GetStatus(true); |
| + if (!UserManager::Get()->current_user_is_owner() && |
| + ownership_status != OwnershipService::OWNERSHIP_NONE) { |
| + LOG(WARNING) << "Changing settings from non-owner, setting=" << path; |
| + |
| + // Revert UI change. |
| + CrosSettings::Get()->FireObservers(path.c_str()); |
| + return; |
| + } |
| + |
| + if (IsControlledSetting(path)) |
| + SetInPolicy(path, in_value); |
| + else |
| + NOTREACHED() << "Try to set unhandled cros setting " << path; |
| +} |
| + |
| +void DeviceSettingsProvider::Observe( |
| + int type, |
| + const content::NotificationSource& source, |
| + const content::NotificationDetails& details) { |
| + if (type == chrome::NOTIFICATION_OWNER_KEY_FETCH_ATTEMPT_SUCCEEDED && |
| + UserManager::Get()->current_user_is_owner()) { |
| + // Reload the initial policy blob, apply settings from temp storage, |
| + // and write back the blob. |
| + SignedSettingsHelper::Get()->StartRetrievePolicyOp( |
| + base::Bind(&DeviceSettingsProvider::FinalizeTempStorage, |
| + base::Unretained(this))); |
| + } |
| +} |
| + |
| +void DeviceSettingsProvider::FinalizeTempStorage( |
| + SignedSettings::ReturnCode code, |
| + const em::PolicyFetchResponse& policy) { |
| + if (code != SignedSettings::SUCCESS) { |
| + LOG(ERROR) << "Can't finalize temp store error code:" << code; |
| + return; |
| + } |
| + |
| + policy_.ParseFromString(policy.policy_data()); |
| + UpdateValuesCache(); |
| + trusted_ = true; |
| + signed_settings_cache::Finalize(g_browser_process->local_state(), policy); |
| + Reload(); |
| +} |
| + |
| +const em::PolicyData DeviceSettingsProvider::get_policy() const { |
| + return policy_; |
| +} |
| + |
| +void DeviceSettingsProvider::RetrieveCachedData() { |
| + // If there is no owner yet, this function will pull the policy cache from the |
| + // temp storage and use that instead. |
| + em::PolicyData policy; |
| + if (!signed_settings_cache::Retrieve(&policy, |
| + g_browser_process->local_state())) { |
| + VLOG(1) << "Can't retrieve temp store possibly not created yet."; |
| + // Prepare empty data for the case we don't have temp cache yet. |
| + policy.set_policy_type(kDevicePolicyType); |
| + em::ChromeDeviceSettingsProto pol; |
| + policy.set_policy_value(pol.SerializeAsString()); |
| + } |
| + |
| + policy_ = policy; |
| + UpdateValuesCache(); |
| +} |
| + |
| +void DeviceSettingsProvider::SetInPolicy(const std::string& prop, |
| + const base::Value& value) { |
| + if (prop == kDeviceOwner) { |
| + // Just store it in the memory cache no trusted checks no persisting. |
| + std::string owner; |
| + if (value.GetAsString(&owner)) { |
| + policy_.set_username(owner); |
| + values_cache_.SetValue(prop, value.DeepCopy()); |
| + CrosSettings::Get()->FireObservers(prop.c_str()); |
| + // We can't trust this value anymore until we reload the real username. |
| + trusted_ = false; |
| + } else { |
| + NOTREACHED(); |
| + } |
| + return; |
| + } |
| + |
| + if (!RequestTrustedEntity()) { |
| + // Otherwise we should first reload and apply on top of that. |
| + SignedSettingsHelper::Get()->StartRetrievePolicyOp( |
| + base::Bind(&DeviceSettingsProvider::FinishSetInPolicy, |
| + base::Unretained(this), |
| + prop, base::Owned(value.DeepCopy()))); |
| + return; |
| + } |
| + |
| + trusted_ = false; |
| + em::PolicyData data = get_policy(); |
| + em::ChromeDeviceSettingsProto pol; |
| + pol.ParseFromString(data.policy_value()); |
| + if (prop == kAccountsPrefAllowNewUser) { |
| + em::AllowNewUsersProto* allow = pol.mutable_allow_new_users(); |
| + bool allow_value; |
| + if (value.GetAsBoolean(&allow_value)) |
| + allow->set_allow_new_users(allow_value); |
| + else |
| + NOTREACHED(); |
| + } else if (prop == kAccountsPrefAllowGuest) { |
| + em::GuestModeEnabledProto* guest = pol.mutable_guest_mode_enabled(); |
| + bool guest_value; |
| + if (value.GetAsBoolean(&guest_value)) |
| + guest->set_guest_mode_enabled(guest_value); |
| + else |
| + NOTREACHED(); |
| + } else if (prop == kAccountsPrefShowUserNamesOnSignIn) { |
| + em::ShowUserNamesOnSigninProto* show = pol.mutable_show_user_names(); |
| + bool show_value; |
| + if (value.GetAsBoolean(&show_value)) |
| + show->set_show_user_names(show_value); |
| + else |
| + NOTREACHED(); |
| + } else if (prop == kSignedDataRoamingEnabled) { |
| + em::DataRoamingEnabledProto* roam = pol.mutable_data_roaming_enabled(); |
| + bool roaming_value = false; |
| + if (value.GetAsBoolean(&roaming_value)) |
| + roam->set_data_roaming_enabled(roaming_value); |
| + else |
| + NOTREACHED(); |
| + ApplyRoamingSetting(roaming_value); |
| + } else if (prop == kSettingProxyEverywhere) { |
| + // TODO(cmasone): NOTIMPLEMENTED() once http://crosbug.com/13052 is fixed. |
| + std::string proxy_value; |
| + if (value.GetAsString(&proxy_value)) { |
| + bool success = |
| + pol.mutable_device_proxy_settings()->ParseFromString(proxy_value); |
| + DCHECK(success); |
| + } else { |
| + NOTREACHED(); |
| + } |
| + } else if (prop == kReleaseChannel) { |
| + em::ReleaseChannelProto* release_channel = pol.mutable_release_channel(); |
| + std::string channel_value; |
| + if (value.GetAsString(&channel_value)) |
| + release_channel->set_release_channel(channel_value); |
| + else |
| + NOTREACHED(); |
| + } else if (prop == kStatsReportingPref) { |
| + em::MetricsEnabledProto* metrics = pol.mutable_metrics_enabled(); |
| + bool metrics_value = false; |
| + if (value.GetAsBoolean(&metrics_value)) |
| + metrics->set_metrics_enabled(metrics_value); |
| + else |
| + NOTREACHED(); |
| + ApplyMetricsSetting(false, metrics_value); |
| + } else if (prop == kAccountsPrefUsers) { |
| + em::UserWhitelistProto* whitelist_proto = pol.mutable_user_whitelist(); |
| + whitelist_proto->clear_user_whitelist(); |
| + const base::ListValue& users = static_cast<const base::ListValue&>(value); |
| + for (base::ListValue::const_iterator i = users.begin(); |
| + i != users.end(); ++i) { |
| + std::string email; |
| + if ((*i)->GetAsString(&email)) |
| + whitelist_proto->add_user_whitelist(email.c_str()); |
| + } |
| + } else { |
| + NOTREACHED(); |
| + } |
| + data.set_policy_value(pol.SerializeAsString()); |
| + // Set the cache to the updated value. |
| + policy_ = data; |
| + UpdateValuesCache(); |
| + CrosSettings::Get()->FireObservers(prop.c_str()); |
| + |
| + if (!signed_settings_cache::Store(data, g_browser_process->local_state())) |
| + LOG(ERROR) << "Couldn't store to the temp storage."; |
| + |
| + OwnershipService::Status ownership_status = |
| + ownership_service_->GetStatus(true); |
| + if (ownership_status == OwnershipService::OWNERSHIP_TAKEN) { |
| + em::PolicyFetchResponse policy_envelope; |
| + policy_envelope.set_policy_data(policy_.SerializeAsString()); |
| + SignedSettingsHelper::Get()->StartStorePolicyOp( |
| + policy_envelope, |
| + base::Bind(&DeviceSettingsProvider::OnStorePolicyCompleted, |
| + base::Unretained(this))); |
| + } |
| +} |
| + |
| +void DeviceSettingsProvider::FinishSetInPolicy( |
| + const std::string& prop, |
| + const base::Value* value, |
| + SignedSettings::ReturnCode code, |
| + const em::PolicyFetchResponse& policy) { |
| + if (code != SignedSettings::SUCCESS) { |
| + LOG(ERROR) << "Can't serialize to policy error code: " << code; |
| + Reload(); |
| + return; |
| + } |
| + SetInPolicy(prop, *value); |
| +} |
| + |
| +void DeviceSettingsProvider::UpdateValuesCache() { |
| + const em::PolicyData data = get_policy(); |
| + values_cache_.Clear(); |
| + |
| + if (data.has_username() && !data.has_request_token()) |
| + values_cache_.SetString(kDeviceOwner, data.username()); |
| + |
| + em::ChromeDeviceSettingsProto pol; |
| + pol.ParseFromString(data.policy_value()); |
| + |
| + // For all our boolean settings the following is applicable: |
| + // true is default permissive value and false is safe prohibitive value. |
| + // Exception: kSignedDataRoamingEnabled which has default value of false. |
| + if (pol.has_allow_new_users() && |
| + pol.allow_new_users().has_allow_new_users() && |
| + pol.allow_new_users().allow_new_users()) { |
|
Mattias Nissler (ping if slow)
2011/12/01 11:17:49
As discussed, we might replace these if-else patte
pastarmovj
2011/12/01 15:34:49
Done.
|
| + // New users allowed, user_whitelist() ignored. |
| + values_cache_.SetBoolean(kAccountsPrefAllowNewUser, true); |
| + } else if (!pol.has_user_whitelist()) { |
| + // If we have the allow_new_users bool, and it is true, we honor that above. |
| + // In all other cases (don't have it, have it and it is set to false, etc), |
| + // We will honor the user_whitelist() if it is there and populated. |
| + // Otherwise we default to allowing new users. |
| + values_cache_.SetBoolean(kAccountsPrefAllowNewUser, true); |
| + } else { |
| + values_cache_.SetBoolean(kAccountsPrefAllowNewUser, |
| + pol.user_whitelist().user_whitelist_size() == 0); |
| + } |
| + |
| + if (!pol.has_guest_mode_enabled() || |
| + !pol.guest_mode_enabled().has_guest_mode_enabled()) { |
| + // Default to allowing guests; |
| + values_cache_.SetBoolean(kAccountsPrefAllowGuest, true); |
| + } else { |
| + values_cache_.SetBoolean(kAccountsPrefAllowGuest, |
| + pol.guest_mode_enabled().guest_mode_enabled()); |
| + } |
| + |
| + if (!pol.has_show_user_names() || |
| + !pol.show_user_names().has_show_user_names()) { |
| + // Default to showing pods on the login screen; |
| + values_cache_.SetBoolean(kAccountsPrefShowUserNamesOnSignIn, true); |
| + } else { |
| + values_cache_.SetBoolean(kAccountsPrefShowUserNamesOnSignIn, |
| + pol.show_user_names().show_user_names()); |
| + } |
| + |
| + if (!pol.has_data_roaming_enabled() || |
| + !pol.data_roaming_enabled().has_data_roaming_enabled()) { |
| + // Default to disabling cellular data roaming; |
| + values_cache_.SetBoolean(kSignedDataRoamingEnabled, false); |
| + } else { |
| + values_cache_.SetBoolean(kSignedDataRoamingEnabled, |
| + pol.data_roaming_enabled().data_roaming_enabled()); |
| + } |
| + |
| + // TODO(cmasone): NOTIMPLEMENTED() once http://crosbug.com/13052 is fixed. |
| + std::string serialized; |
| + if (pol.has_device_proxy_settings() && |
| + pol.device_proxy_settings().SerializeToString(&serialized)) { |
| + values_cache_.SetString(kSettingProxyEverywhere, serialized); |
| + } |
| + |
| + if (!pol.has_release_channel() || |
| + !pol.release_channel().has_release_channel()) { |
| + // Default to an invalid channel (will be ignored). |
| + values_cache_.SetString(kReleaseChannel, ""); |
| + } else { |
| + values_cache_.SetString(kReleaseChannel, |
| + pol.release_channel().release_channel()); |
| + } |
| + |
| + if (pol.has_metrics_enabled()) { |
| + values_cache_.SetBoolean(kStatsReportingPref, |
| + pol.metrics_enabled().metrics_enabled()); |
| + } else { |
| + values_cache_.SetBoolean(kStatsReportingPref, HasOldMetricsFile()); |
| + } |
| + |
| + base::ListValue* list = new base::ListValue(); |
| + const em::UserWhitelistProto& whitelist_proto = pol.user_whitelist(); |
| + const RepeatedPtrField<std::string>& whitelist = |
| + whitelist_proto.user_whitelist(); |
| + for (RepeatedPtrField<std::string>::const_iterator it = whitelist.begin(); |
| + it != whitelist.end(); ++it) { |
| + list->Append(base::Value::CreateStringValue(*it)); |
| + } |
| + values_cache_.SetValue(kAccountsPrefUsers, list); |
| +} |
| + |
| +void DeviceSettingsProvider::ApplyMetricsSetting(bool use_file, |
| + bool new_value) const { |
| + // TODO(pastarmovj): Remove this once migration is not needed anymore. |
| + // If the value is not set we should try to migrate legacy consent file. |
| + if (use_file) { |
| + new_value = HasOldMetricsFile(); |
| + // Make sure the values will get eventually written to the policy file. |
| + migration_helper_->AddMigrationValue( |
| + kStatsReportingPref, base::Value::CreateBooleanValue(new_value)); |
| + migration_helper_->MigrateValues(); |
| + LOG(INFO) << "No metrics policy set will revert to checking " |
| + << "consent file which is " |
| + << (new_value ? "on." : "off."); |
| + } |
| + VLOG(1) << "Metrics policy is being set to : " << new_value |
| + << "(use file : " << use_file << ")"; |
| + // TODO(pastarmovj): Remove this once we don't need to regenerate the |
| + // consent file for the GUID anymore. |
| + OptionsUtil::ResolveMetricsReportingEnabled(new_value); |
| +} |
| + |
| +void DeviceSettingsProvider::ApplyRoamingSetting(bool new_value) const { |
| + NetworkLibrary* cros = CrosLibrary::Get()->GetNetworkLibrary(); |
| + const NetworkDevice* cellular = cros->FindCellularDevice(); |
| + if (cellular) { |
| + bool device_value = cellular->data_roaming_allowed(); |
| + if (!device_value && cros->IsCellularAlwaysInRoaming()) { |
| + // If operator requires roaming always enabled, ignore supplied value |
| + // and set data roaming allowed in true always. |
| + cros->SetCellularDataRoamingAllowed(true); |
| + } else if (device_value != new_value) { |
| + cros->SetCellularDataRoamingAllowed(new_value); |
| + } |
| + } |
| +} |
| + |
| +void DeviceSettingsProvider::ApplySideEffects() const { |
| + const em::PolicyData data = get_policy(); |
| + em::ChromeDeviceSettingsProto pol; |
| + pol.ParseFromString(data.policy_value()); |
| + // First migrate metrics settings as needed. |
| + if (pol.has_metrics_enabled()) |
| + ApplyMetricsSetting(false, pol.metrics_enabled().metrics_enabled()); |
| + else |
| + ApplyMetricsSetting(true, false); |
| + // Next set the roaming setting as needed. |
| + ApplyRoamingSetting(pol.has_data_roaming_enabled() ? |
| + pol.data_roaming_enabled().data_roaming_enabled() : false); |
| +} |
| + |
| +const base::Value* DeviceSettingsProvider::Get(const std::string& path) const { |
| + if (IsControlledSetting(path)) { |
| + const base::Value* value; |
| + if (values_cache_.GetValue(path, &value)) |
| + return value; |
| + } else { |
| + NOTREACHED() << "Trying to get non cros setting."; |
| + } |
| + |
| + return NULL; |
| +} |
| + |
| +bool DeviceSettingsProvider::GetTrusted(const std::string& path, |
| + const base::Closure& callback) { |
| + if (!IsControlledSetting(path)) { |
| + NOTREACHED(); |
| + return true; |
| + } |
| + |
| + if (RequestTrustedEntity()) { |
| + return true; |
| + } else { |
| + if (!callback.is_null()) |
| + callbacks_.push_back(callback); |
| + return false; |
| + } |
| +} |
| + |
| +bool DeviceSettingsProvider::HandlesSetting(const std::string& path) const { |
| + return IsControlledSetting(path); |
| +} |
| + |
| +bool DeviceSettingsProvider::RequestTrustedEntity() { |
| + OwnershipService::Status ownership_status = |
| + ownership_service_->GetStatus(true); |
| + if (ownership_status == OwnershipService::OWNERSHIP_NONE) |
| + return true; |
| + return trusted_; |
| +} |
| + |
| +void DeviceSettingsProvider::OnStorePolicyCompleted( |
| + SignedSettings::ReturnCode code) { |
| + // In any case reload the policy cache to now. |
| + if (code != SignedSettings::SUCCESS) |
| + Reload(); |
| + else |
| + trusted_ = true; |
| +} |
| + |
| +void DeviceSettingsProvider::OnRetrievePolicyCompleted( |
| + SignedSettings::ReturnCode code, |
| + const em::PolicyFetchResponse& policy) { |
| + switch (code) { |
| + case SignedSettings::SUCCESS: { |
| + DCHECK(policy.has_policy_data()); |
| + policy_.ParseFromString(policy.policy_data()); |
| + signed_settings_cache::Store(get_policy(), |
| + g_browser_process->local_state()); |
| + UpdateValuesCache(); |
| + trusted_ = true; |
| + for (size_t i = 0; i < callbacks_.size(); ++i) |
| + callbacks_[i].Run(); |
| + callbacks_.clear(); |
| + // TODO(pastarmovj): Make those side effects responsibility of the |
| + // respective subsystems. |
| + ApplySideEffects(); |
| + break; |
| + } |
| + case SignedSettings::NOT_FOUND: |
| + case SignedSettings::KEY_UNAVAILABLE: { |
| + if (ownership_service_->GetStatus(true) != |
| + OwnershipService::OWNERSHIP_TAKEN) { |
| + NOTREACHED() << "No policies present yet, will use the temp storage."; |
| + } |
| + break; |
| + } |
| + case SignedSettings::BAD_SIGNATURE: |
| + case SignedSettings::OPERATION_FAILED: { |
| + LOG(ERROR) << "Failed to retrieve cros policies. Reason:" << code; |
| + if (retries_left_ > 0) { |
| + retries_left_ -= 1; |
| + Reload(); |
| + return; |
| + } |
| + LOG(ERROR) << "No retries left"; |
| + break; |
| + } |
| + } |
| +} |
| + |
| +} // namespace chromeos |