blob: 96aeef9e5049646c2793490f891387f495f66278 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/permissions/contextual_notification_permission_ui_selector.h"
#include <utility>
#include "base/bind.h"
#include "base/check_op.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/time/default_clock.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/permissions/quiet_notification_permission_ui_config.h"
#include "chrome/browser/permissions/quiet_notification_permission_ui_state.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/common/chrome_features.h"
#include "components/permissions/permission_request.h"
#include "components/permissions/request_type.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
namespace {
using QuietUiReason = ContextualNotificationPermissionUiSelector::QuietUiReason;
using WarningReason = ContextualNotificationPermissionUiSelector::WarningReason;
using Decision = ContextualNotificationPermissionUiSelector::Decision;
// Records a histogram sample for NotificationUserExperienceQuality.
void RecordNotificationUserExperienceQuality(
CrowdDenyPreloadData::SiteReputation::NotificationUserExperienceQuality
value) {
// Cannot use either base::UmaHistogramEnumeration() overload here because
// ARRAYSIZE is defined as MAX+1 but also as type `int`.
base::UmaHistogramExactLinear(
"Permissions.CrowdDeny.PreloadData.NotificationUxQuality",
static_cast<int>(value),
CrowdDenyPreloadData::SiteReputation::
NotificationUserExperienceQuality_ARRAYSIZE);
}
// Records a histogram sample for the |warning_only| bit.
void RecordWarningOnlyState(bool value) {
base::UmaHistogramBoolean("Permissions.CrowdDeny.PreloadData.WarningOnly",
value);
}
// Attempts to decide which UI to use based on preloaded site reputation data,
// or returns absl::nullopt if not possible. |site_reputation| can be nullptr.
absl::optional<Decision> GetDecisionBasedOnSiteReputation(
const CrowdDenyPreloadData::SiteReputation* site_reputation) {
using Config = QuietNotificationPermissionUiConfig;
if (!site_reputation) {
RecordNotificationUserExperienceQuality(
CrowdDenyPreloadData::SiteReputation::UNKNOWN);
return absl::nullopt;
}
RecordNotificationUserExperienceQuality(
site_reputation->notification_ux_quality());
RecordWarningOnlyState(site_reputation->warning_only());
switch (site_reputation->notification_ux_quality()) {
case CrowdDenyPreloadData::SiteReputation::ACCEPTABLE: {
return Decision::UseNormalUiAndShowNoWarning();
}
case CrowdDenyPreloadData::SiteReputation::UNSOLICITED_PROMPTS: {
if (site_reputation->warning_only())
return Decision::UseNormalUiAndShowNoWarning();
if (!Config::IsCrowdDenyTriggeringEnabled())
return absl::nullopt;
return Decision(QuietUiReason::kTriggeredByCrowdDeny,
Decision::ShowNoWarning());
}
case CrowdDenyPreloadData::SiteReputation::ABUSIVE_PROMPTS: {
if (site_reputation->warning_only()) {
if (!Config::IsAbusiveRequestWarningEnabled())
return Decision::UseNormalUiAndShowNoWarning();
return Decision(Decision::UseNormalUi(),
WarningReason::kAbusiveRequests);
}
if (!Config::IsAbusiveRequestBlockingEnabled())
return absl::nullopt;
return Decision(QuietUiReason::kTriggeredDueToAbusiveRequests,
Decision::ShowNoWarning());
}
case CrowdDenyPreloadData::SiteReputation::ABUSIVE_CONTENT: {
if (site_reputation->warning_only()) {
if (!Config::IsAbusiveContentTriggeredRequestWarningEnabled())
return Decision::UseNormalUiAndShowNoWarning();
return Decision(Decision::UseNormalUi(),
WarningReason::kAbusiveContent);
}
if (!Config::IsAbusiveContentTriggeredRequestBlockingEnabled())
return absl::nullopt;
return Decision(QuietUiReason::kTriggeredDueToAbusiveContent,
Decision::ShowNoWarning());
}
case CrowdDenyPreloadData::SiteReputation::DISRUPTIVE_BEHAVIOR: {
DCHECK(!site_reputation->warning_only());
if (!Config::IsDisruptiveBehaviorRequestBlockingEnabled())
return absl::nullopt;
return Decision(QuietUiReason::kTriggeredDueToDisruptiveBehavior,
Decision::ShowNoWarning());
}
case CrowdDenyPreloadData::SiteReputation::UNKNOWN: {
return absl::nullopt;
}
}
NOTREACHED();
return absl::nullopt;
}
// Roll the dice to decide whether to use the normal UI even when the preload
// data indicates that quiet UI should be used. This creates a control group of
// normal UI prompt impressions, which facilitates comparing acceptance rates,
// better calibrating server-side logic, and detecting when the notification
// experience on the site has improved.
bool ShouldHoldBackQuietUI(QuietUiReason quiet_ui_reason) {
const double kHoldbackChance =
QuietNotificationPermissionUiConfig::GetCrowdDenyHoldBackChance();
// There is no hold-back when the quiet UI is shown due to abusive permission
// request UX, as those verdicts are not calculated based on acceptance rates.
if (quiet_ui_reason != QuietUiReason::kTriggeredByCrowdDeny)
return false;
// Avoid rolling a dice if the chance is 0.
const bool result = kHoldbackChance && base::RandDouble() < kHoldbackChance;
base::UmaHistogramBoolean("Permissions.CrowdDeny.DidHoldbackQuietUi", result);
return result;
}
} // namespace
ContextualNotificationPermissionUiSelector::
ContextualNotificationPermissionUiSelector() = default;
void ContextualNotificationPermissionUiSelector::SelectUiToUse(
permissions::PermissionRequest* request,
DecisionMadeCallback callback) {
callback_ = std::move(callback);
DCHECK(callback_);
if (!base::FeatureList::IsEnabled(features::kQuietNotificationPrompts)) {
Notify(Decision::UseNormalUiAndShowNoWarning());
return;
}
// Even if the quiet UI is enabled on all sites, the crowd deny, abuse and
// disruption trigger conditions must be evaluated first, so that the
// corresponding, less prominent UI and the strings are shown on blocklisted
// origins.
EvaluatePerSiteTriggers(url::Origin::Create(request->requesting_origin()));
}
void ContextualNotificationPermissionUiSelector::Cancel() {
// The computation either finishes synchronously above, or is waiting on the
// Safe Browsing check.
safe_browsing_request_.reset();
}
bool ContextualNotificationPermissionUiSelector::IsPermissionRequestSupported(
permissions::RequestType request_type) {
return request_type == permissions::RequestType::kNotifications;
}
ContextualNotificationPermissionUiSelector::
~ContextualNotificationPermissionUiSelector() = default;
void ContextualNotificationPermissionUiSelector::EvaluatePerSiteTriggers(
const url::Origin& origin) {
CrowdDenyPreloadData::GetInstance()->GetReputationDataForSiteAsync(
origin,
base::BindOnce(
&ContextualNotificationPermissionUiSelector::OnSiteReputationReady,
weak_factory_.GetWeakPtr(), origin));
}
void ContextualNotificationPermissionUiSelector::OnSiteReputationReady(
const url::Origin& origin,
const CrowdDenyPreloadData::SiteReputation* reputation) {
absl::optional<Decision> decision =
GetDecisionBasedOnSiteReputation(reputation);
// If the PreloadData suggests this is an unacceptable site, ping Safe
// Browsing to verify; but do not ping if it is not warranted.
if (!decision || (!decision->quiet_ui_reason && !decision->warning_reason)) {
Notify(Decision::UseNormalUiAndShowNoWarning());
return;
}
DCHECK(!safe_browsing_request_);
DCHECK(g_browser_process->safe_browsing_service());
// It is fine to use base::Unretained() here, as |safe_browsing_request_|
// guarantees not to fire the callback after its destruction.
safe_browsing_request_.emplace(
g_browser_process->safe_browsing_service()->database_manager(),
base::DefaultClock::GetInstance(), origin,
base::BindOnce(&ContextualNotificationPermissionUiSelector::
OnSafeBrowsingVerdictReceived,
base::Unretained(this), *decision));
}
void ContextualNotificationPermissionUiSelector::OnSafeBrowsingVerdictReceived(
Decision candidate_decision,
CrowdDenySafeBrowsingRequest::Verdict verdict) {
DCHECK(safe_browsing_request_);
DCHECK(callback_);
safe_browsing_request_.reset();
switch (verdict) {
case CrowdDenySafeBrowsingRequest::Verdict::kAcceptable:
Notify(Decision::UseNormalUiAndShowNoWarning());
break;
case CrowdDenySafeBrowsingRequest::Verdict::kUnacceptable:
if (candidate_decision.quiet_ui_reason &&
ShouldHoldBackQuietUI(*(candidate_decision.quiet_ui_reason))) {
candidate_decision.quiet_ui_reason.reset();
}
Notify(candidate_decision);
break;
}
}
void ContextualNotificationPermissionUiSelector::Notify(
const Decision& decision) {
std::move(callback_).Run(decision);
}
// static