blob: d033e7efea30544333697fdb73665fa556b24e14 [file] [log] [blame]
// Copyright 2021 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 "components/accuracy_tips/accuracy_service.h"
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_forward.h"
#include "base/containers/contains.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/accuracy_tips/accuracy_tip_interaction.h"
#include "components/accuracy_tips/accuracy_tip_safe_browsing_client.h"
#include "components/accuracy_tips/accuracy_tip_status.h"
#include "components/accuracy_tips/features.h"
#include "components/accuracy_tips/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/ukm/content/source_url_recorder.h"
#include "components/unified_consent/pref_names.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "url/gurl.h"
namespace accuracy_tips {
// Returns a suffix for accuracy tips histograms.
// Needs to match AccuracyTipInteractions from histogram_suffixes_list.xml.
const std::string GetHistogramSuffix(AccuracyTipInteraction interaction) {
switch (interaction) {
case AccuracyTipInteraction::kNoAction:
return "NoAction";
case AccuracyTipInteraction::kLearnMore:
return "LearnMore";
case AccuracyTipInteraction::kOptOut:
return "OptOut";
case AccuracyTipInteraction::kClosed:
return "Closed";
case AccuracyTipInteraction::kIgnore:
return "Ignore";
case AccuracyTipInteraction::kPermissionRequested:
return "PermissionRequested";
case AccuracyTipInteraction::kDisabledByExperiment:
NOTREACHED(); // We don't need specific histograms for this.
return "";
}
}
// Returns the preference to store feature state in. Uses a different pref if
// the UI is disabled to avoid dark-launch experiments affecting real usage.
const char* GetLastShownPrefName(bool disable_ui) {
return disable_ui ? prefs::kLastAccuracyTipShownDisabledUi
: prefs::kLastAccuracyTipShown;
}
const char* GetPreviousInteractionsPrefName(bool disable_ui) {
return disable_ui ? prefs::kPreviousInteractionsDisabledUi
: prefs::kPreviousInteractions;
}
using AccuracyCheckCallback = AccuracyService::AccuracyCheckCallback;
// static
void AccuracyService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
const bool disable_ui = features::kDisableUi.Get();
registry->RegisterTimePref(GetLastShownPrefName(disable_ui), base::Time());
registry->RegisterListPref(GetPreviousInteractionsPrefName(disable_ui));
}
AccuracyService::AccuracyService(
std::unique_ptr<Delegate> delegate,
PrefService* pref_service,
scoped_refptr<safe_browsing::SafeBrowsingDatabaseManager> sb_database,
history::HistoryService* history_service,
scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
scoped_refptr<base::SequencedTaskRunner> io_task_runner)
: delegate_(std::move(delegate)),
pref_service_(pref_service),
ui_task_runner_(ui_task_runner),
sample_url_(GURL(features::kSampleUrl.Get())),
time_between_prompts_(features::kTimeBetweenPrompts.Get()),
disable_ui_(features::kDisableUi.Get()) {
if (sb_database) {
sb_client_ = base::MakeRefCounted<AccuracyTipSafeBrowsingClient>(
std::move(sb_database), std::move(ui_task_runner),
std::move(io_task_runner));
}
if (history_service) {
history_service_observation_.Observe(history_service);
}
}
AccuracyService::~AccuracyService() = default;
void AccuracyService::Shutdown() {
if (sb_client_) {
sb_client_->Shutdown();
sb_client_ = nullptr;
}
history_service_observation_.Reset();
}
void AccuracyService::CheckAccuracyStatus(const GURL& url,
AccuracyCheckCallback callback) {
DCHECK(ui_task_runner_->RunsTasksInCurrentSequence());
const base::Value* last_interactions =
pref_service_->Get(GetPreviousInteractionsPrefName(disable_ui_));
const base::Value opt_out_value(
static_cast<int>(AccuracyTipInteraction::kOptOut));
if (base::Contains(last_interactions->GetList(), opt_out_value)) {
return std::move(callback).Run(AccuracyTipStatus::kOptOut);
}
base::Time last_shown =
pref_service_->GetTime(GetLastShownPrefName(disable_ui_));
if (clock_->Now() - last_shown < time_between_prompts_) {
return std::move(callback).Run(AccuracyTipStatus::kRateLimited);
}
if (sample_url_.is_valid() && url == sample_url_) {
return OnAccuracyStatusReceived(url, std::move(callback),
AccuracyTipStatus::kShowAccuracyTip);
}
if (!sb_client_) {
return std::move(callback).Run(AccuracyTipStatus::kNone);
}
sb_client_->CheckAccuracyStatus(
url,
base::BindOnce(&AccuracyService::OnAccuracyStatusReceived,
weak_factory_.GetWeakPtr(), url, std::move(callback)));
}
void AccuracyService::OnAccuracyStatusReceived(const GURL& url,
AccuracyCheckCallback callback,
AccuracyTipStatus status) {
if (status == AccuracyTipStatus::kShowAccuracyTip &&
delegate_->IsEngagementHigh(url)) {
return std::move(callback).Run(AccuracyTipStatus::kHighEnagagement);
}
return std::move(callback).Run(status);
}
void AccuracyService::MaybeShowAccuracyTip(content::WebContents* web_contents) {
DCHECK(ui_task_runner_->RunsTasksInCurrentSequence());
pref_service_->SetTime(GetLastShownPrefName(disable_ui_), clock_->Now());
if (disable_ui_) {
return OnAccuracyTipClosed(
base::TimeTicks(), ukm::GetSourceIdForWebContentsDocument(web_contents),
AccuracyTipInteraction::kDisabledByExperiment);
}
bool show_opt_out =
pref_service_->GetList(GetPreviousInteractionsPrefName(disable_ui_))
->GetList()
.size() >= static_cast<size_t>(features::kNumIgnorePrompts.Get());
url_for_last_shown_tip_ = web_contents->GetLastCommittedURL();
web_contents_showing_accuracy_tip_ = web_contents;
delegate_->ShowAccuracyTip(
web_contents, AccuracyTipStatus::kShowAccuracyTip,
/*show_opt_out=*/show_opt_out,
base::BindOnce(&AccuracyService::OnAccuracyTipClosed,
weak_factory_.GetWeakPtr(), base::TimeTicks::Now(),
ukm::GetSourceIdForWebContentsDocument(web_contents)));
for (Observer& observer : observers_)
observer.OnAccuracyTipShown();
}
void AccuracyService::MaybeShowSurvey() {
if (CanShowSurvey()) {
auto* interactions_list =
pref_service_->GetList(GetPreviousInteractionsPrefName(disable_ui_));
const int last_interaction = interactions_list->GetList().back().GetInt();
const bool ukm_enabled = pref_service_->GetBoolean(
unified_consent::prefs::kUrlKeyedAnonymizedDataCollectionEnabled);
std::string url_parameter_for_hats =
ukm_enabled ? url_for_last_shown_tip_.DeprecatedGetOriginAsURL().spec()
: "";
delegate_->ShowSurvey(
{}, {{"Tip shown for URL", url_parameter_for_hats},
{"UI interaction", base::NumberToString(last_interaction)}});
}
}
bool AccuracyService::IsSecureConnection(content::WebContents* web_contents) {
return delegate_->IsSecureConnection(web_contents);
}
void AccuracyService::OnURLsDeleted(
history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) {
if (deletion_info.time_range().IsValid()) {
base::Time last_shown =
pref_service_->GetTime(GetLastShownPrefName(disable_ui_));
if (last_shown >= deletion_info.time_range().begin() &&
last_shown <= deletion_info.time_range().end()) {
url_for_last_shown_tip_ = GURL();
}
} else {
if (deletion_info.deleted_urls_origin_map().count(
url_for_last_shown_tip_.DeprecatedGetOriginAsURL())) {
url_for_last_shown_tip_ = GURL();
}
}
}
void AccuracyService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void AccuracyService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
bool AccuracyService::IsShowingAccuracyTip(content::WebContents* web_contents) {
return web_contents_showing_accuracy_tip_ != nullptr &&
web_contents_showing_accuracy_tip_ == web_contents;
}
void AccuracyService::OnAccuracyTipClosed(base::TimeTicks time_opened,
ukm::SourceId ukm_source_id,
AccuracyTipInteraction interaction) {
DCHECK(ui_task_runner_->RunsTasksInCurrentSequence());
ListPrefUpdate update(pref_service_,
GetPreviousInteractionsPrefName(disable_ui_));
base::Value* interaction_list = update.Get();
interaction_list->Append(static_cast<int>(interaction));
// Record metrics.
base::UmaHistogramEnumeration("Privacy.AccuracyTip.AccuracyTipInteraction",
interaction);
base::UmaHistogramCounts100("Privacy.AccuracyTip.NumDialogsShown",
interaction_list->GetList().size());
ukm::builders::AccuracyTipDialog ukm_builder(ukm_source_id);
ukm_builder.SetInteraction(static_cast<int>(interaction));
if (interaction != AccuracyTipInteraction::kDisabledByExperiment) {
const base::TimeDelta time_open = base::TimeTicks::Now() - time_opened;
base::UmaHistogramMediumTimes("Privacy.AccuracyTip.AccuracyTipTimeOpen",
time_open);
ukm_builder.SetTimeSpent(
ukm::GetExponentialBucketMinForFineUserTiming(time_open.InSeconds()));
const std::string suffix = GetHistogramSuffix(interaction);
base::UmaHistogramCounts100("Privacy.AccuracyTip.NumDialogsShown." + suffix,
interaction_list->GetList().size());
base::UmaHistogramMediumTimes(
"Privacy.AccuracyTip.AccuracyTipTimeOpen." + suffix, time_open);
}
ukm_builder.Record(ukm::UkmRecorder::Get());
web_contents_showing_accuracy_tip_ = nullptr;
for (Observer& observer : observers_)
observer.OnAccuracyTipClosed();
}
bool AccuracyService::CanShowSurvey() {
if (!base::FeatureList::IsEnabled(
accuracy_tips::features::kAccuracyTipsSurveyFeature) ||
disable_ui_) {
return false;
}
base::Time last_shown =
pref_service_->GetTime(GetLastShownPrefName(disable_ui_));
base::TimeDelta last_shown_delta = clock_->Now() - last_shown;
const bool has_required_time_passed =
last_shown_delta >= features::kMinTimeToShowSurvey.Get() &&
last_shown_delta <= features::kMaxTimeToShowSurvey.Get();
if (!has_required_time_passed)
return false;
if (url_for_last_shown_tip_.is_empty() || !url_for_last_shown_tip_.is_valid())
return false;
int interactions_count =
pref_service_->GetList(GetPreviousInteractionsPrefName(disable_ui_))
->GetList()
.size();
return interactions_count >= features::kMinPromptCountRequiredForSurvey.Get();
}
} // namespace accuracy_tips