| // 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/policy/arc_policy_bridge.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/guid.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/logging.h" |
| #include "base/memory/singleton.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/values.h" |
| #include "chrome/browser/chromeos/arc/arc_session_manager.h" |
| #include "chrome/browser/chromeos/profiles/profile_helper.h" |
| #include "chrome/browser/policy/developer_tools_policy_handler.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/common/pref_names.h" |
| #include "chromeos/network/onc/onc_utils.h" |
| #include "components/arc/arc_bridge_service.h" |
| #include "components/arc/arc_browser_context_keyed_service_factory_base.h" |
| #include "components/arc/arc_prefs.h" |
| #include "components/onc/onc_constants.h" |
| #include "components/policy/core/common/policy_map.h" |
| #include "components/policy/core/common/policy_namespace.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/user_manager/user.h" |
| #include "content/public/common/service_manager_connection.h" |
| #include "crypto/sha2.h" |
| #include "services/data_decoder/public/cpp/safe_json_parser.h" |
| |
| namespace arc { |
| |
| namespace { |
| |
| constexpr char kArcCaCerts[] = "caCerts"; |
| constexpr char kPolicyCompliantJson[] = "{ \"policyCompliant\": true }"; |
| |
| // invert_bool_value: If the Chrome policy and the ARC policy with boolean value |
| // have opposite semantics, set this to true so the bool is inverted before |
| // being added. Otherwise, set it to false. |
| void MapBoolToBool(const std::string& arc_policy_name, |
| const std::string& policy_name, |
| const policy::PolicyMap& policy_map, |
| bool invert_bool_value, |
| base::DictionaryValue* filtered_policies) { |
| const base::Value* const policy_value = policy_map.GetValue(policy_name); |
| if (!policy_value) |
| return; |
| if (!policy_value->is_bool()) { |
| NOTREACHED() << "Policy " << policy_name << " is not a boolean."; |
| return; |
| } |
| bool bool_value; |
| policy_value->GetAsBoolean(&bool_value); |
| filtered_policies->SetBoolean(arc_policy_name, |
| bool_value != invert_bool_value); |
| } |
| |
| // int_true: value of Chrome OS policy for which arc policy is set to true. |
| // It is set to false for all other values. |
| void MapIntToBool(const std::string& arc_policy_name, |
| const std::string& policy_name, |
| const policy::PolicyMap& policy_map, |
| int int_true, |
| base::DictionaryValue* filtered_policies) { |
| const base::Value* const policy_value = policy_map.GetValue(policy_name); |
| if (!policy_value) |
| return; |
| if (!policy_value->is_int()) { |
| NOTREACHED() << "Policy " << policy_name << " is not an integer."; |
| return; |
| } |
| int int_value; |
| policy_value->GetAsInteger(&int_value); |
| filtered_policies->SetBoolean(arc_policy_name, int_value == int_true); |
| } |
| |
| // |arc_policy_name| is only set if the |pref_name| pref is managed. |
| // int_true: value of Chrome OS pref for which arc policy is set to true. |
| // It is set to false for all other values. |
| void MapManagedIntPrefToBool(const std::string& arc_policy_name, |
| const std::string& pref_name, |
| const PrefService* profile_prefs, |
| int int_true, |
| base::DictionaryValue* filtered_policies) { |
| if (!profile_prefs->IsManagedPreference(pref_name)) |
| return; |
| |
| int int_value = profile_prefs->GetInteger(pref_name); |
| filtered_policies->SetBoolean(arc_policy_name, int_value == int_true); |
| } |
| |
| // Checks whether |policy_name| is present as an object and has all |fields|, |
| // Sets |arc_policy_name| to true only if the condition above is satisfied. |
| void MapObjectToPresenceBool(const std::string& arc_policy_name, |
| const std::string& policy_name, |
| const policy::PolicyMap& policy_map, |
| base::DictionaryValue* filtered_policies, |
| const std::vector<std::string>& fields) { |
| const base::Value* const policy_value = policy_map.GetValue(policy_name); |
| if (!policy_value) |
| return; |
| const base::DictionaryValue* dict = nullptr; |
| if (!policy_value->GetAsDictionary(&dict)) { |
| NOTREACHED() << "Policy " << policy_name << " is not an object."; |
| return; |
| } |
| for (const auto& field : fields) { |
| if (!dict->HasKey(field)) |
| return; |
| } |
| filtered_policies->SetBoolean(arc_policy_name, true); |
| } |
| |
| void AddOncCaCertsToPolicies(const policy::PolicyMap& policy_map, |
| base::DictionaryValue* filtered_policies) { |
| const base::Value* const policy_value = |
| policy_map.GetValue(policy::key::kArcCertificatesSyncMode); |
| int32_t mode = ArcCertsSyncMode::SYNC_DISABLED; |
| |
| // Old certs should be uninstalled if the sync is disabled or policy is not |
| // set. |
| if (!policy_value || !policy_value->GetAsInteger(&mode) || |
| mode != ArcCertsSyncMode::COPY_CA_CERTS) { |
| return; |
| } |
| |
| // Importing CA certificates from device policy is not allowed. |
| // Import only from user policy. |
| const base::Value* onc_policy_value = |
| policy_map.GetValue(policy::key::kOpenNetworkConfiguration); |
| if (!onc_policy_value) { |
| VLOG(1) << "onc policy is not set."; |
| return; |
| } |
| std::string onc_blob; |
| if (!onc_policy_value->GetAsString(&onc_blob)) { |
| LOG(ERROR) << "Value of onc policy has invalid format."; |
| return; |
| } |
| |
| base::ListValue certificates; |
| { |
| base::ListValue unused_network_configs; |
| base::DictionaryValue unused_global_network_config; |
| if (!chromeos::onc::ParseAndValidateOncForImport( |
| onc_blob, onc::ONCSource::ONC_SOURCE_USER_POLICY, |
| "" /* no passphrase */, &unused_network_configs, |
| &unused_global_network_config, &certificates)) { |
| LOG(ERROR) << "Value of onc policy has invalid format =" << onc_blob; |
| } |
| } |
| |
| std::unique_ptr<base::ListValue> ca_certs( |
| std::make_unique<base::ListValue>()); |
| for (const auto& entry : certificates) { |
| const base::DictionaryValue* certificate = nullptr; |
| if (!entry.GetAsDictionary(&certificate)) { |
| DLOG(FATAL) << "Value of a certificate entry is not a dictionary " |
| << "value."; |
| continue; |
| } |
| |
| std::string cert_type; |
| certificate->GetStringWithoutPathExpansion(::onc::certificate::kType, |
| &cert_type); |
| if (cert_type != ::onc::certificate::kAuthority) |
| continue; |
| |
| const base::ListValue* trust_list = nullptr; |
| if (!certificate->GetListWithoutPathExpansion( |
| ::onc::certificate::kTrustBits, &trust_list)) { |
| continue; |
| } |
| |
| bool web_trust_flag = false; |
| for (const auto& list_val : *trust_list) { |
| std::string trust_type; |
| if (!list_val.GetAsString(&trust_type)) |
| NOTREACHED(); |
| |
| if (trust_type == ::onc::certificate::kWeb) { |
| // "Web" implies that the certificate is to be trusted for SSL |
| // identification. |
| web_trust_flag = true; |
| break; |
| } |
| } |
| if (!web_trust_flag) |
| continue; |
| |
| std::string x509_data; |
| if (!certificate->GetStringWithoutPathExpansion(::onc::certificate::kX509, |
| &x509_data)) { |
| continue; |
| } |
| |
| base::DictionaryValue data; |
| data.SetString("X509", x509_data); |
| ca_certs->Append(data.CreateDeepCopy()); |
| } |
| if (!ca_certs->GetList().empty()) |
| filtered_policies->SetKey("credentialsConfigDisabled", base::Value(true)); |
| filtered_policies->Set(kArcCaCerts, std::move(ca_certs)); |
| } |
| |
| std::string GetFilteredJSONPolicies(const policy::PolicyMap& policy_map, |
| const PrefService* profile_prefs, |
| const std::string& guid, |
| bool is_affiliated) { |
| base::DictionaryValue filtered_policies; |
| // Parse ArcPolicy as JSON string before adding other policies to the |
| // dictionary. |
| const base::Value* const app_policy_value = |
| policy_map.GetValue(policy::key::kArcPolicy); |
| if (app_policy_value) { |
| std::string app_policy_string; |
| app_policy_value->GetAsString(&app_policy_string); |
| std::unique_ptr<base::DictionaryValue> app_policy_dict = |
| base::DictionaryValue::From( |
| base::JSONReader::ReadDeprecated(app_policy_string)); |
| if (app_policy_dict) { |
| // Need a deep copy of all values here instead of doing a swap, because |
| // JSONReader::Read constructs a dictionary whose StringValues are |
| // JSONStringValues which are based on StringPiece instead of string. |
| filtered_policies.MergeDictionary(app_policy_dict.get()); |
| } else { |
| LOG(ERROR) << "Value of ArcPolicy has invalid format: " |
| << app_policy_string; |
| } |
| } |
| |
| // Keep them sorted by the ARC policy names. |
| MapBoolToBool("cameraDisabled", policy::key::kVideoCaptureAllowed, policy_map, |
| /* invert_bool_value */ true, &filtered_policies); |
| // Use the pref for "debuggingFeaturesDisabled" to avoid duplicating the logic |
| // of handling DeveloperToolsDisabled / DeveloperToolsAvailability policies. |
| MapManagedIntPrefToBool( |
| "debuggingFeaturesDisabled", ::prefs::kDevToolsAvailability, |
| profile_prefs, |
| static_cast<int>( |
| policy::DeveloperToolsPolicyHandler::Availability::kDisallowed), |
| &filtered_policies); |
| MapBoolToBool("printingDisabled", policy::key::kPrintingEnabled, policy_map, |
| /* invert_bool_value */ true, &filtered_policies); |
| MapBoolToBool("screenCaptureDisabled", policy::key::kDisableScreenshots, |
| policy_map, false, &filtered_policies); |
| MapIntToBool("shareLocationDisabled", policy::key::kDefaultGeolocationSetting, |
| policy_map, 2 /*BlockGeolocation*/, &filtered_policies); |
| MapBoolToBool("unmuteMicrophoneDisabled", policy::key::kAudioCaptureAllowed, |
| policy_map, /* invert_bool_value */ true, &filtered_policies); |
| MapBoolToBool("mountPhysicalMediaDisabled", |
| policy::key::kExternalStorageDisabled, policy_map, |
| /* invert_bool_value */ false, &filtered_policies); |
| MapObjectToPresenceBool("setWallpaperDisabled", policy::key::kWallpaperImage, |
| policy_map, &filtered_policies, {"url", "hash"}); |
| MapBoolToBool("vpnConfigDisabled", policy::key::kVpnConfigAllowed, policy_map, |
| /* invert_bool_value */ true, &filtered_policies); |
| |
| // Add CA certificates. |
| AddOncCaCertsToPolicies(policy_map, &filtered_policies); |
| |
| if (!is_affiliated) |
| filtered_policies.RemoveKey("apkCacheEnabled"); |
| |
| filtered_policies.SetString("guid", guid); |
| |
| std::string policy_json; |
| JSONStringValueSerializer serializer(&policy_json); |
| serializer.Serialize(filtered_policies); |
| return policy_json; |
| } |
| |
| void OnReportComplianceParseFailure( |
| base::OnceCallback<void(const std::string&)> callback, |
| const std::string& error) { |
| // TODO(poromov@): Report to histogram. |
| DLOG(ERROR) << "Can't parse policy compliance report"; |
| std::move(callback).Run(kPolicyCompliantJson); |
| } |
| |
| void UpdateFirstComplianceSinceSignInTiming( |
| const base::TimeDelta& elapsed_time) { |
| UMA_HISTOGRAM_CUSTOM_TIMES("Arc.FirstComplianceReportTime.SinceSignIn", |
| elapsed_time, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromMinutes(10), 50); |
| } |
| |
| void UpdateFirstComplianceSinceStartupTiming( |
| const base::TimeDelta& elapsed_time) { |
| UMA_HISTOGRAM_CUSTOM_TIMES("Arc.FirstComplianceReportTime.SinceStartup", |
| elapsed_time, base::TimeDelta::FromSeconds(1), |
| base::TimeDelta::FromMinutes(10), 50); |
| } |
| |
| void UpdateComplianceSinceUpdateTiming(const base::TimeDelta& elapsed_time) { |
| UMA_HISTOGRAM_CUSTOM_TIMES("Arc.ComplianceReportSinceUpdateNotificationTime", |
| elapsed_time, |
| base::TimeDelta::FromMilliseconds(100), |
| base::TimeDelta::FromMinutes(10), 50); |
| } |
| |
| // Returns the SHA-256 hash of the JSON dump of the ARC policies, in the textual |
| // hex dump format. Note that no specific JSON normalization is performed, as |
| // the spurious hash mismatches, even if they occur (which is unlikely), would |
| // only result in some UMA metrics not being sent. |
| std::string GetPoliciesHash(const std::string& json_policies) { |
| const std::string hash_bits = crypto::SHA256HashString(json_policies); |
| return base::ToLowerASCII( |
| base::HexEncode(hash_bits.c_str(), hash_bits.length())); |
| } |
| |
| // Singleton factory for ArcPolicyBridge. |
| class ArcPolicyBridgeFactory |
| : public internal::ArcBrowserContextKeyedServiceFactoryBase< |
| ArcPolicyBridge, |
| ArcPolicyBridgeFactory> { |
| public: |
| // Factory name used by ArcBrowserContextKeyedServiceFactoryBase. |
| static constexpr const char* kName = "ArcPolicyBridgeFactory"; |
| |
| static ArcPolicyBridgeFactory* GetInstance() { |
| return base::Singleton<ArcPolicyBridgeFactory>::get(); |
| } |
| |
| private: |
| friend base::DefaultSingletonTraits<ArcPolicyBridgeFactory>; |
| |
| ArcPolicyBridgeFactory() { |
| DependsOn(policy::ProfilePolicyConnectorFactory::GetInstance()); |
| } |
| ~ArcPolicyBridgeFactory() override = default; |
| }; |
| |
| } // namespace |
| |
| // static |
| ArcPolicyBridge* ArcPolicyBridge::GetForBrowserContext( |
| content::BrowserContext* context) { |
| return ArcPolicyBridgeFactory::GetForBrowserContext(context); |
| } |
| |
| // static |
| ArcPolicyBridge* ArcPolicyBridge::GetForBrowserContextForTesting( |
| content::BrowserContext* context) { |
| return ArcPolicyBridgeFactory::GetForBrowserContextForTesting(context); |
| } |
| |
| base::WeakPtr<ArcPolicyBridge> ArcPolicyBridge::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| ArcPolicyBridge::ArcPolicyBridge(content::BrowserContext* context, |
| ArcBridgeService* bridge_service) |
| : ArcPolicyBridge(context, bridge_service, nullptr /* policy_service */) {} |
| |
| ArcPolicyBridge::ArcPolicyBridge(content::BrowserContext* context, |
| ArcBridgeService* bridge_service, |
| policy::PolicyService* policy_service) |
| : context_(context), |
| arc_bridge_service_(bridge_service), |
| policy_service_(policy_service), |
| instance_guid_(base::GenerateGUID()), |
| weak_ptr_factory_(this) { |
| VLOG(2) << "ArcPolicyBridge::ArcPolicyBridge"; |
| arc_bridge_service_->policy()->SetHost(this); |
| arc_bridge_service_->policy()->AddObserver(this); |
| } |
| |
| ArcPolicyBridge::~ArcPolicyBridge() { |
| VLOG(2) << "ArcPolicyBridge::~ArcPolicyBridge"; |
| arc_bridge_service_->policy()->RemoveObserver(this); |
| arc_bridge_service_->policy()->SetHost(nullptr); |
| } |
| |
| const std::string& ArcPolicyBridge::GetInstanceGuidForTesting() { |
| return instance_guid_; |
| } |
| |
| void ArcPolicyBridge::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ArcPolicyBridge::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void ArcPolicyBridge::OverrideIsManagedForTesting(bool is_managed) { |
| is_managed_ = is_managed; |
| } |
| |
| void ArcPolicyBridge::OnConnectionReady() { |
| VLOG(1) << "ArcPolicyBridge::OnConnectionReady"; |
| if (policy_service_ == nullptr) { |
| InitializePolicyService(); |
| } |
| policy_service_->AddObserver(policy::POLICY_DOMAIN_CHROME, this); |
| initial_policies_hash_ = GetPoliciesHash(GetCurrentJSONPolicies()); |
| |
| if (!on_arc_instance_ready_callback_.is_null()) { |
| std::move(on_arc_instance_ready_callback_).Run(); |
| } |
| } |
| |
| void ArcPolicyBridge::OnConnectionClosed() { |
| VLOG(1) << "ArcPolicyBridge::OnConnectionClosed"; |
| policy_service_->RemoveObserver(policy::POLICY_DOMAIN_CHROME, this); |
| policy_service_ = nullptr; |
| initial_policies_hash_.clear(); |
| } |
| |
| void ArcPolicyBridge::GetPolicies(GetPoliciesCallback callback) { |
| VLOG(1) << "ArcPolicyBridge::GetPolicies"; |
| const std::string policy = GetCurrentJSONPolicies(); |
| for (Observer& observer : observers_) { |
| observer.OnPolicySent(policy); |
| } |
| std::move(callback).Run(policy); |
| } |
| |
| void ArcPolicyBridge::ReportCompliance(const std::string& request, |
| ReportComplianceCallback callback) { |
| VLOG(1) << "ArcPolicyBridge::ReportCompliance"; |
| // TODO(crbug.com/730593): Remove AdaptCallbackForRepeating() by updating |
| // the callee interface. |
| auto repeating_callback = |
| base::AdaptCallbackForRepeating(std::move(callback)); |
| data_decoder::SafeJsonParser::Parse( |
| content::ServiceManagerConnection::GetForProcess()->GetConnector(), |
| request, |
| base::Bind(&ArcPolicyBridge::OnReportComplianceParseSuccess, |
| weak_ptr_factory_.GetWeakPtr(), repeating_callback), |
| base::Bind(&OnReportComplianceParseFailure, repeating_callback)); |
| } |
| |
| void ArcPolicyBridge::ReportCloudDpsRequested( |
| base::Time time, |
| const std::vector<std::string>& package_names) { |
| const std::set<std::string> packages_set(package_names.begin(), |
| package_names.end()); |
| for (Observer& observer : observers_) |
| observer.OnCloudDpsRequested(time, packages_set); |
| } |
| |
| void ArcPolicyBridge::ReportCloudDpsSucceeded( |
| base::Time time, |
| const std::vector<std::string>& package_names) { |
| const std::set<std::string> packages_set(package_names.begin(), |
| package_names.end()); |
| for (Observer& observer : observers_) |
| observer.OnCloudDpsSucceeded(time, packages_set); |
| } |
| |
| void ArcPolicyBridge::ReportCloudDpsFailed(base::Time time, |
| const std::string& package_name, |
| mojom::InstallErrorReason reason) { |
| for (Observer& observer : observers_) |
| observer.OnCloudDpsFailed(time, package_name, reason); |
| } |
| |
| void ArcPolicyBridge::OnPolicyUpdated(const policy::PolicyNamespace& ns, |
| const policy::PolicyMap& previous, |
| const policy::PolicyMap& current) { |
| VLOG(1) << "ArcPolicyBridge::OnPolicyUpdated"; |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD(arc_bridge_service_->policy(), |
| OnPolicyUpdated); |
| if (!instance) |
| return; |
| |
| const std::string policies_hash = GetPoliciesHash(GetCurrentJSONPolicies()); |
| if (policies_hash != update_notification_policies_hash_) { |
| update_notification_policies_hash_ = policies_hash; |
| update_notification_time_ = base::Time::Now(); |
| compliance_since_update_timing_reported_ = false; |
| } |
| |
| instance->OnPolicyUpdated(); |
| } |
| |
| void ArcPolicyBridge::OnCommandReceived( |
| const std::string& command, |
| mojom::PolicyInstance::OnCommandReceivedCallback callback) { |
| VLOG(1) << "ArcPolicyBridge::OnCommandReceived"; |
| auto* const instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_bridge_service_->policy(), OnCommandReceived); |
| |
| if (!instance) { |
| VLOG(1) << "ARC not ready yet, will retry remote command once it is ready."; |
| DCHECK(on_arc_instance_ready_callback_.is_null()); |
| |
| // base::Unretained is safe here since this class owns the callback's |
| // lifetime. |
| on_arc_instance_ready_callback_ = |
| base::BindOnce(&ArcPolicyBridge::OnCommandReceived, |
| base::Unretained(this), command, std::move(callback)); |
| |
| return; |
| } |
| |
| instance->OnCommandReceived(command, std::move(callback)); |
| } |
| |
| void ArcPolicyBridge::InitializePolicyService() { |
| auto* profile_policy_connector = |
| policy::ProfilePolicyConnectorFactory::GetForBrowserContext(context_); |
| policy_service_ = profile_policy_connector->policy_service(); |
| is_managed_ = profile_policy_connector->IsManaged(); |
| } |
| |
| std::string ArcPolicyBridge::GetCurrentJSONPolicies() const { |
| if (!is_managed_) |
| return std::string(); |
| const policy::PolicyNamespace policy_namespace(policy::POLICY_DOMAIN_CHROME, |
| std::string()); |
| const policy::PolicyMap& policy_map = |
| policy_service_->GetPolicies(policy_namespace); |
| |
| const Profile* const profile = Profile::FromBrowserContext(context_); |
| const user_manager::User* const user = |
| chromeos::ProfileHelper::Get()->GetUserByProfile(profile); |
| |
| return GetFilteredJSONPolicies(policy_map, profile->GetPrefs(), |
| instance_guid_, user->IsAffiliated()); |
| } |
| |
| void ArcPolicyBridge::OnReportComplianceParseSuccess( |
| base::OnceCallback<void(const std::string&)> callback, |
| std::unique_ptr<base::Value> parsed_json) { |
| // Always returns "compliant". |
| std::move(callback).Run(kPolicyCompliantJson); |
| Profile::FromBrowserContext(context_)->GetPrefs()->SetBoolean( |
| prefs::kArcPolicyComplianceReported, true); |
| |
| const base::DictionaryValue* dict = nullptr; |
| if (parsed_json->GetAsDictionary(&dict)) { |
| UpdateComplianceReportMetrics(dict); |
| for (Observer& observer : observers_) { |
| observer.OnComplianceReportReceived(parsed_json.get()); |
| } |
| } |
| } |
| |
| void ArcPolicyBridge::UpdateComplianceReportMetrics( |
| const base::DictionaryValue* report) { |
| bool is_arc_plus_plus_report_successful = false; |
| report->GetBoolean("isArcPlusPlusReportSuccessful", |
| &is_arc_plus_plus_report_successful); |
| std::string reported_policies_hash; |
| report->GetString("policyHash", &reported_policies_hash); |
| if (!is_arc_plus_plus_report_successful || reported_policies_hash.empty()) |
| return; |
| |
| const base::Time now = base::Time::Now(); |
| ArcSessionManager* const session_manager = ArcSessionManager::Get(); |
| |
| if (reported_policies_hash == initial_policies_hash_ && |
| !first_compliance_timing_reported_) { |
| const base::Time sign_in_start_time = session_manager->sign_in_start_time(); |
| if (!sign_in_start_time.is_null()) { |
| UpdateFirstComplianceSinceSignInTiming(now - sign_in_start_time); |
| } else { |
| UpdateFirstComplianceSinceStartupTiming( |
| now - session_manager->arc_start_time()); |
| } |
| first_compliance_timing_reported_ = true; |
| } |
| |
| if (reported_policies_hash == update_notification_policies_hash_ && |
| !compliance_since_update_timing_reported_) { |
| UpdateComplianceSinceUpdateTiming(now - update_notification_time_); |
| compliance_since_update_timing_reported_ = true; |
| } |
| } |
| |
| } // namespace arc |