blob: 3ec16496a77216f021f5550c6846756bb0597a2f [file] [log] [blame]
// Copyright 2020 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/prediction_based_permission_ui_selector.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/time/default_clock.h"
#include "chrome/browser/permissions/permission_actions_history_factory.h"
#include "chrome/browser/permissions/prediction_service_factory.h"
#include "chrome/browser/permissions/prediction_service_request.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/optimization_guide/machine_learning_tflite_buildflags.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_actions_history.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/permission_util.h"
#include "components/permissions/prediction_service/prediction_common.h"
#include "components/permissions/prediction_service/prediction_service.h"
#include "components/permissions/prediction_service/prediction_service_messages.pb.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
#include "chrome/browser/permissions/prediction_model_handler_provider_factory.h"
#include "components/permissions/prediction_service/prediction_model_handler.h"
#include "components/permissions/prediction_service/prediction_model_handler_provider.h"
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
namespace {
using QuietUiReason = PredictionBasedPermissionUiSelector::QuietUiReason;
using Decision = PredictionBasedPermissionUiSelector::Decision;
using PredictionSource = PredictionBasedPermissionUiSelector::PredictionSource;
constexpr auto VeryUnlikely = permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_VERY_UNLIKELY;
// The data we consider can only be at most 28 days old to match the data that
// the ML model is built on.
constexpr base::TimeDelta kPermissionActionCutoffAge = base::Days(28);
// Only send requests if there are at least 4 action in the user's history for
// the particular permission type.
constexpr size_t kRequestedPermissionMinimumHistoricalActions = 4;
absl::optional<
permissions::PermissionPrediction_Likelihood_DiscretizedLikelihood>
ParsePredictionServiceMockLikelihood(const std::string& value) {
if (value == "very-unlikely") {
return permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_VERY_UNLIKELY;
} else if (value == "unlikely") {
return permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_UNLIKELY;
} else if (value == "neutral") {
return permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_NEUTRAL;
} else if (value == "likely") {
return permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_LIKELY;
} else if (value == "very-likely") {
return permissions::
PermissionPrediction_Likelihood_DiscretizedLikelihood_VERY_LIKELY;
}
return absl::nullopt;
}
bool ShouldPredictionTriggerQuietUi(
permissions::PermissionUmaUtil::PredictionGrantLikelihood likelihood) {
return likelihood == VeryUnlikely;
}
} // namespace
PredictionBasedPermissionUiSelector::PredictionBasedPermissionUiSelector(
Profile* profile)
: profile_(profile) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kPredictionServiceMockLikelihood)) {
auto mock_likelihood = ParsePredictionServiceMockLikelihood(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kPredictionServiceMockLikelihood));
if (mock_likelihood.has_value())
set_likelihood_override(mock_likelihood.value());
}
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
if (base::FeatureList::IsEnabled(
permissions::features::kPermissionOnDeviceNotificationPredictions) ||
base::FeatureList::IsEnabled(
permissions::features::kPermissionOnDeviceGeolocationPredictions)) {
PredictionModelHandlerProviderFactory::GetForBrowserContext(profile);
}
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
}
PredictionBasedPermissionUiSelector::~PredictionBasedPermissionUiSelector() =
default;
void PredictionBasedPermissionUiSelector::SelectUiToUse(
permissions::PermissionRequest* request,
DecisionMadeCallback callback) {
VLOG(1) << "[CPSS] Selector activated";
callback_ = std::move(callback);
last_request_grant_likelihood_ = absl::nullopt;
was_decision_held_back_ = absl::nullopt;
const PredictionSource prediction_source =
GetPredictionTypeToUse(request->request_type());
if (prediction_source == PredictionSource::USE_NONE) {
VLOG(1) << "[CPSS] Configuration does not allow CPSS requests";
std::move(callback_).Run(Decision::UseNormalUiAndShowNoWarning());
return;
}
auto features = BuildPredictionRequestFeatures(request);
if (features.requested_permission_counts.total() <
kRequestedPermissionMinimumHistoricalActions) {
VLOG(1) << "[CPSS] Historic prompt count ("
<< features.requested_permission_counts.total()
<< ") is smaller than threshold ("
<< kRequestedPermissionMinimumHistoricalActions << ")";
std::move(callback_).Run(Decision::UseNormalUiAndShowNoWarning());
return;
}
if (likelihood_override_for_testing_.has_value()) {
VLOG(1) << "[CPSS] Using likelihood override value that was provided via "
"command line";
if (ShouldPredictionTriggerQuietUi(
likelihood_override_for_testing_.value())) {
std::move(callback_).Run(
Decision(QuietUiReason::kServicePredictedVeryUnlikelyGrant,
Decision::ShowNoWarning()));
} else {
std::move(callback_).Run(Decision::UseNormalUiAndShowNoWarning());
}
return;
}
DCHECK(!request_);
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
if (prediction_source == PredictionSource::USE_ANY ||
prediction_source == PredictionSource::USE_ONDEVICE) {
permissions::PredictionModelHandlerProvider*
prediction_model_handler_provider =
PredictionModelHandlerProviderFactory::GetForBrowserContext(
profile_);
permissions::PredictionModelHandler* prediction_model_handler = nullptr;
if (prediction_model_handler_provider) {
prediction_model_handler =
prediction_model_handler_provider->GetPredictionModelHandler(
request->request_type());
}
if (prediction_model_handler &&
prediction_model_handler->ModelAvailable()) {
VLOG(1) << "[CPSS] Using locally available model";
permissions::PermissionUmaUtil::RecordPermissionPredictionSource(
permissions::PermissionPredictionSource::ON_DEVICE);
auto proto_request = GetPredictionRequestProto(features);
prediction_model_handler->ExecuteModelWithMetadata(
base::BindOnce(
&PredictionBasedPermissionUiSelector::LookupResponseReceived,
weak_ptr_factory_.GetWeakPtr(), /*is_on_device=*/true,
request->request_type(),
/*lookup_succesful=*/true, /*response_from_cache=*/false),
std::move(proto_request));
return;
} else if (prediction_source == PredictionSource::USE_ONDEVICE) {
VLOG(1) << "[CPSS] Model is not available and cannot fall back to server "
"side execution";
std::move(callback_).Run(Decision::UseNormalUiAndShowNoWarning());
return;
}
}
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
if (prediction_source == PredictionSource::USE_ANY ||
prediction_source == PredictionSource::USE_SERVER_SIDE) {
permissions::PredictionService* service =
PredictionServiceFactory::GetForProfile(profile_);
VLOG(1) << "[CPSS] Starting prediction service request";
permissions::PermissionUmaUtil::RecordPermissionPredictionSource(
permissions::PermissionPredictionSource::SERVER_SIDE);
request_ = std::make_unique<PredictionServiceRequest>(
service, features,
base::BindOnce(
&PredictionBasedPermissionUiSelector::LookupResponseReceived,
base::Unretained(this), /*is_on_device=*/false,
request->request_type()));
return;
}
NOTREACHED();
}
void PredictionBasedPermissionUiSelector::Cancel() {
request_.reset();
callback_.Reset();
}
bool PredictionBasedPermissionUiSelector::IsPermissionRequestSupported(
permissions::RequestType request_type) {
return request_type == permissions::RequestType::kNotifications ||
request_type == permissions::RequestType::kGeolocation;
}
absl::optional<permissions::PermissionUmaUtil::PredictionGrantLikelihood>
PredictionBasedPermissionUiSelector::PredictedGrantLikelihoodForUKM() {
return last_request_grant_likelihood_;
}
absl::optional<bool>
PredictionBasedPermissionUiSelector::WasSelectorDecisionHeldback() {
return was_decision_held_back_;
}
permissions::PredictionRequestFeatures
PredictionBasedPermissionUiSelector::BuildPredictionRequestFeatures(
permissions::PermissionRequest* request) {
permissions::PredictionRequestFeatures features;
features.gesture = request->GetGestureType();
features.type = request->request_type();
base::Time cutoff = base::Time::Now() - kPermissionActionCutoffAge;
permissions::PermissionActionsHistory* action_history =
PermissionActionsHistoryFactory::GetForProfile(profile_);
auto actions = action_history->GetHistory(
cutoff, request->request_type(),
permissions::PermissionActionsHistory::EntryFilter::WANT_ALL_PROMPTS);
permissions::PermissionActionsHistory::FillInActionCounts(
&features.requested_permission_counts, actions);
actions = action_history->GetHistory(
cutoff,
permissions::PermissionActionsHistory::EntryFilter::WANT_ALL_PROMPTS);
permissions::PermissionActionsHistory::FillInActionCounts(
&features.all_permission_counts, actions);
return features;
}
void PredictionBasedPermissionUiSelector::LookupResponseReceived(
bool is_on_device,
permissions::RequestType request_type,
bool lookup_succesful,
bool response_from_cache,
const absl::optional<permissions::GeneratePredictionsResponse>& response) {
request_.reset();
if (!callback_) {
VLOG(1) << "[CPSS] Prediction service response ignored as the request is "
"canceled";
return;
}
if (!lookup_succesful || !response || response->prediction_size() == 0) {
VLOG(1) << "[CPSS] Prediction service request failed";
std::move(callback_).Run(Decision::UseNormalUiAndShowNoWarning());
return;
}
last_request_grant_likelihood_ =
response->prediction(0).grant_likelihood().discretized_likelihood();
if (ShouldHoldBack(is_on_device, request_type)) {
VLOG(1) << "[CPSS] Prediction service decision held back";
was_decision_held_back_ = true;
std::move(callback_).Run(
Decision(Decision::UseNormalUi(), Decision::ShowNoWarning()));
return;
}
was_decision_held_back_ = false;
VLOG(1)
<< "[CPSS] Prediction service request succeeded and received likelihood: "
<< last_request_grant_likelihood_.value();
if (ShouldPredictionTriggerQuietUi(last_request_grant_likelihood_.value())) {
std::move(callback_).Run(Decision(
is_on_device ? QuietUiReason::kOnDevicePredictedVeryUnlikelyGrant
: QuietUiReason::kServicePredictedVeryUnlikelyGrant,
Decision::ShowNoWarning()));
return;
}
std::move(callback_).Run(
Decision(Decision::UseNormalUi(), Decision::ShowNoWarning()));
}
bool PredictionBasedPermissionUiSelector::ShouldHoldBack(
bool is_on_device,
permissions::RequestType request_type) {
// Different holdback threshold for the different experiments.
const double on_device_geolocation_holdback_threshold =
permissions::feature_params::
kPermissionOnDeviceGeolocationPredictionsHoldbackChance.Get();
const double on_device_notification_holdback_threshold =
permissions::feature_params::
kPermissionOnDeviceNotificationPredictionsHoldbackChance.Get();
const double server_side_notification_holdback_threshold =
features::kPermissionPredictionsHoldbackChance.Get();
const double server_side_geolocation_holdback_threshold =
features::kPermissionGeolocationPredictionsHoldbackChance.Get();
// Holdback probability for this request.
const double holdback_chance = base::RandDouble();
bool should_holdback = false;
if (is_on_device) {
if (request_type == permissions::RequestType::kNotifications) {
should_holdback =
holdback_chance < on_device_notification_holdback_threshold;
} else if (request_type == permissions::RequestType::kGeolocation) {
should_holdback =
holdback_chance < on_device_geolocation_holdback_threshold;
} else {
NOTREACHED();
}
} else {
if (request_type == permissions::RequestType::kNotifications) {
should_holdback =
holdback_chance < server_side_notification_holdback_threshold;
} else if (request_type == permissions::RequestType::kGeolocation) {
should_holdback =
holdback_chance < server_side_geolocation_holdback_threshold;
} else {
NOTREACHED();
}
}
permissions::PermissionUmaUtil::RecordPermissionPredictionServiceHoldback(
request_type, is_on_device, should_holdback);
return should_holdback;
}
PredictionSource PredictionBasedPermissionUiSelector::GetPredictionTypeToUse(
permissions::RequestType request_type) {
if (!safe_browsing::IsSafeBrowsingEnabled(*(profile_->GetPrefs()))) {
return PredictionSource::USE_NONE;
}
bool is_server_side_prediction_enabled = false;
bool is_ondevice_prediction_enabled = false;
bool is_tflite_available = false;
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
is_tflite_available = true;
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
// Notification supports both flavours of the quiet prompt
if (request_type == permissions::RequestType::kNotifications &&
(base::FeatureList::IsEnabled(features::kQuietNotificationPrompts) ||
base::FeatureList::IsEnabled(
permissions::features::kPermissionQuietChip))) {
is_server_side_prediction_enabled =
base::FeatureList::IsEnabled(features::kPermissionPredictions);
is_ondevice_prediction_enabled =
is_tflite_available &&
base::FeatureList::IsEnabled(
permissions::features::kPermissionOnDeviceNotificationPredictions);
}
// Geolocation supports only the quiet chip ui
if (request_type == permissions::RequestType::kGeolocation &&
base::FeatureList::IsEnabled(
permissions::features::kPermissionQuietChip)) {
is_server_side_prediction_enabled = base::FeatureList::IsEnabled(
features::kPermissionGeolocationPredictions);
is_ondevice_prediction_enabled =
is_tflite_available &&
base::FeatureList::IsEnabled(
permissions::features::kPermissionOnDeviceGeolocationPredictions);
}
if (is_server_side_prediction_enabled && is_ondevice_prediction_enabled) {
return PredictionSource::USE_ANY;
} else if (is_server_side_prediction_enabled) {
return PredictionSource::USE_SERVER_SIDE;
} else if (is_ondevice_prediction_enabled) {
return PredictionSource::USE_ONDEVICE;
} else {
return PredictionSource::USE_NONE;
}
}