blob: 62fe97cf3baece1340f4e3ae2555b5bb444a3016 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/permissions/embedded_permission_prompt_flow_model.h"
#include "base/memory/raw_ptr.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/permissions_client.h"
#include "content/public/browser/web_contents.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/permissions/android/android_permission_util.h"
#endif
namespace {
using content_settings::SettingSource;
using Variant = permissions::EmbeddedPermissionPromptFlowModel::Variant;
// An upper bound on the maximum number of screens that we can record in
// metrics. Practically speaking the actual number should never be more than 3
// but a higher bound allows us to detect via metrics if this happens in the
// wild.
constexpr int kScreenCounterMaximum = 10;
bool CanGroupVariants(Variant a, Variant b) {
// Ask and PreviouslyDenied are a special case and can be grouped together.
if ((a == Variant::kPreviouslyDenied && b == Variant::kAsk) ||
(a == Variant::kAsk && b == Variant::kPreviouslyDenied)) {
return true;
}
return (a == b);
}
permissions::ElementAnchoredBubbleVariant GetElementAnchoredBubbleVariant(
Variant variant) {
switch (variant) {
case Variant::kUninitialized:
return permissions::ElementAnchoredBubbleVariant::kUninitialized;
case Variant::kAdministratorGranted:
return permissions::ElementAnchoredBubbleVariant::kAdministratorGranted;
case Variant::kPreviouslyGranted:
return permissions::ElementAnchoredBubbleVariant::kPreviouslyGranted;
case Variant::kOsSystemSettings:
return permissions::ElementAnchoredBubbleVariant::kOsSystemSettings;
case Variant::kOsPrompt:
return permissions::ElementAnchoredBubbleVariant::kOsPrompt;
case Variant::kAsk:
return permissions::ElementAnchoredBubbleVariant::kAsk;
case Variant::kPreviouslyDenied:
return permissions::ElementAnchoredBubbleVariant::kPreviouslyDenied;
case Variant::kAdministratorDenied:
return permissions::ElementAnchoredBubbleVariant::kAdministratorDenied;
}
NOTREACHED();
}
} // namespace
namespace permissions {
EmbeddedPermissionPromptFlowModel::EmbeddedPermissionPromptFlowModel(
content::WebContents* web_contents,
PermissionPrompt::Delegate* delegate)
: delegate_(delegate), web_contents_(web_contents) {}
EmbeddedPermissionPromptFlowModel::~EmbeddedPermissionPromptFlowModel() =
default;
EmbeddedPermissionPromptFlowModel::Variant
EmbeddedPermissionPromptFlowModel::DeterminePromptVariant(
ContentSetting setting,
const content_settings::SettingInfo& info,
ContentSettingsType type) {
// If the administrator blocked the permission, there is nothing the user can
// do. Presenting them with a different screen in unproductive.
if (PermissionsClient::Get()->IsPermissionBlockedByDevicePolicy(
web_contents(), setting, info, type)) {
return Variant::kAdministratorDenied;
}
#if BUILDFLAG(IS_ANDROID)
if (!HasSystemPermission(type, web_contents_) &&
!CanRequestSystemPermission(type, web_contents_)) {
return Variant::kOsSystemSettings;
}
if (setting == CONTENT_SETTING_ALLOW &&
!HasSystemPermission(type, web_contents_) &&
CanRequestSystemPermission(type, web_contents_)) {
return Variant::kOsPrompt;
}
#else
// Determine if we can directly show one of the OS views. The "System
// Settings" view is higher priority then all the other remaining options,
// whereas the "OS Prompt" view is only higher priority then the views that
// are associated with a site-level allowed state.
// TODO(crbug.com/40275129): Handle going to Windows settings.
if (PermissionsClient::Get()->IsSystemDenied(type)) {
return Variant::kOsSystemSettings;
}
if (setting == CONTENT_SETTING_ALLOW &&
PermissionsClient::Get()->CanPromptSystemPermission(type)) {
return Variant::kOsPrompt;
}
#endif
if (PermissionsClient::Get()->IsPermissionAllowedByDevicePolicy(
web_contents(), setting, info, type)) {
return Variant::kAdministratorGranted;
}
switch (setting) {
case CONTENT_SETTING_ASK:
return Variant::kAsk;
case CONTENT_SETTING_ALLOW:
return Variant::kPreviouslyGranted;
case CONTENT_SETTING_BLOCK:
return Variant::kPreviouslyDenied;
default:
break;
}
return Variant::kUninitialized;
}
void EmbeddedPermissionPromptFlowModel::PrioritizeAndMergeNewVariant(
EmbeddedPermissionPromptFlowModel::Variant new_variant,
ContentSettingsType new_type) {
// The new variant can be grouped with the already existing one.
if (CanGroupVariants(prompt_variant_, new_variant)) {
prompt_types_.insert(new_type);
prompt_variant_ = std::max(prompt_variant_, new_variant);
return;
}
// The existing variant is higher priority than the new one.
if (prompt_variant_ > new_variant) {
return;
}
// The new variant has higher priority than the existing one.
prompt_types_.clear();
prompt_types_.insert(new_type);
prompt_variant_ = new_variant;
}
void EmbeddedPermissionPromptFlowModel::CalculateCurrentVariant() {
Clear();
auto* map = PermissionsClient::Get()->GetSettingsMap(
web_contents()->GetBrowserContext());
content_settings::SettingInfo info;
for (const auto& request : delegate_->Requests()) {
ContentSettingsType type = request->GetContentSettingsType();
ContentSetting setting =
map->GetContentSetting(delegate_->GetRequestingOrigin(),
delegate_->GetEmbeddingOrigin(), type, &info);
Variant current_request_variant =
DeterminePromptVariant(setting, info, type);
PrioritizeAndMergeNewVariant(current_request_variant, type);
}
const auto& requests = delegate_->Requests();
for (const auto& request : requests) {
if (prompt_types_.contains(request->GetContentSettingsType())) {
requests_.push_back(request->GetWeakPtr());
}
}
}
void EmbeddedPermissionPromptFlowModel::PrecalculateVariantsForMetrics() {
if (prompt_variant() == Variant::kUninitialized) {
return;
}
if (os_prompt_variant_ == Variant::kUninitialized) {
for (const auto& request : delegate_->Requests()) {
const auto& type = request->GetContentSettingsType();
#if BUILDFLAG(IS_ANDROID)
if (!HasSystemPermission(type, web_contents_) &&
CanRequestSystemPermission(type, web_contents_)) {
#else
if (PermissionsClient::Get()->CanPromptSystemPermission(type)) {
#endif
os_prompt_variant_ = Variant::kOsPrompt;
break;
}
}
}
if (os_system_settings_variant_ == Variant::kUninitialized) {
for (const auto& request : delegate_->Requests()) {
const auto& type = request->GetContentSettingsType();
#if BUILDFLAG(IS_ANDROID)
if (!HasSystemPermission(type, web_contents_) &&
!CanRequestSystemPermission(type, web_contents_)) {
#else
if (PermissionsClient::Get()->IsSystemDenied(type)) {
#endif
os_system_settings_variant_ = Variant::kOsSystemSettings;
break;
}
}
}
}
void EmbeddedPermissionPromptFlowModel::RecordOsMetrics(
permissions::OsScreenAction action) {
const auto& requests = delegate_->Requests();
CHECK_GT(requests.size(), 0U);
permissions::OsScreen screen;
switch (prompt_variant()) {
case Variant::kOsPrompt:
screen = permissions::OsScreen::kOsPrompt;
break;
case Variant::kOsSystemSettings:
screen = permissions::OsScreen::kOsSystemSettings;
break;
default:
return;
}
base::TimeDelta time_to_decision =
base::Time::Now() - current_variant_first_display_time_;
permissions::PermissionUmaUtil::RecordElementAnchoredBubbleOsMetrics(
requests, screen, action, time_to_decision);
}
void EmbeddedPermissionPromptFlowModel::RecordPermissionActionUKM(
permissions::ElementAnchoredBubbleAction action) {
// There should never be more than kScreenCounterMaximum screens. If this is
// hit something has gone wrong and we're probably caught in a loop showing
// the same screens over and over.
DCHECK_LE(prompt_screen_counter_for_metrics_, kScreenCounterMaximum);
permissions::PermissionUmaUtil::RecordElementAnchoredPermissionPromptAction(
// This represents all the requests for the entire prompt.
delegate_->Requests(),
// This only contains the requests for the currently active screen, which
// could sometimes be a subset of all requests for the entire prompt.
requests(), action, GetElementAnchoredBubbleVariant(prompt_variant()),
prompt_screen_counter_for_metrics_, delegate_->GetRequestingOrigin(),
delegate_->GetAssociatedWebContents(),
delegate_->GetAssociatedWebContents()->GetBrowserContext());
++prompt_screen_counter_for_metrics_;
}
void EmbeddedPermissionPromptFlowModel::RecordElementAnchoredBubbleVariantUMA(
Variant variant) {
permissions::PermissionUmaUtil::RecordElementAnchoredBubbleVariantUMA(
delegate_->Requests(), GetElementAnchoredBubbleVariant(variant));
}
std::vector<permissions::ElementAnchoredBubbleVariant>
EmbeddedPermissionPromptFlowModel::GetPromptVariants() const {
std::vector<permissions::ElementAnchoredBubbleVariant> variants;
// Current prompt variant when the user takes an action on a site level
// prompt.
if (prompt_variant() != Variant::kUninitialized) {
variants.push_back(GetElementAnchoredBubbleVariant(prompt_variant()));
}
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
if (os_prompt_variant_ != Variant::kUninitialized) {
variants.push_back(GetElementAnchoredBubbleVariant(os_prompt_variant_));
}
if (os_system_settings_variant_ != Variant::kUninitialized) {
variants.push_back(
GetElementAnchoredBubbleVariant(os_system_settings_variant_));
}
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
return variants;
}
void EmbeddedPermissionPromptFlowModel::SetDelegateAction(
DelegateAction action) {
if (action_.has_value()) {
return;
}
action_ = action;
switch (action) {
case DelegateAction::kAllow:
delegate_->Accept();
break;
case DelegateAction::kAllowThisTime:
delegate_->AcceptThisTime();
break;
case DelegateAction::kDeny:
delegate_->Deny();
break;
case DelegateAction::kDismiss:
delegate_->Dismiss();
break;
}
}
} // namespace permissions