blob: 22136eada1b78f23054e62e54ce270cfb549c864 [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 "chrome/browser/permissions/permissions_ai_handler.h"
#include "base/containers/fixed_flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/optimization_guide/proto/features/permissions_ai.pb.h"
#include "components/permissions/request_type.h"
namespace permissions {
namespace {
using ::optimization_guide::ModelBasedCapabilityKey;
using ::optimization_guide::SessionConfigParams;
using ::optimization_guide::proto::PermissionsAiRequest;
using ::optimization_guide::proto::PermissionsAiResponse;
using ::optimization_guide::proto::PermissionType;
constexpr ModelBasedCapabilityKey kFeatureKey =
ModelBasedCapabilityKey::kPermissionsAi;
constexpr SessionConfigParams kSessionConfigParams = SessionConfigParams{
.execution_mode = SessionConfigParams::ExecutionMode::kOnDeviceOnly,
};
// Currently, the following errors, which are used when a model may have been
// installed but not yet loaded, are treated as waitable.
static constexpr auto kWaitableReasons =
base::MakeFixedFlatSet<optimization_guide::OnDeviceModelEligibilityReason>({
optimization_guide::OnDeviceModelEligibilityReason::
kConfigNotAvailableForFeature,
optimization_guide::OnDeviceModelEligibilityReason::kModelToBeInstalled,
optimization_guide::OnDeviceModelEligibilityReason::
kSafetyModelNotAvailable,
optimization_guide::OnDeviceModelEligibilityReason::
kLanguageDetectionModelNotAvailable,
});
void LogOnDeviceModelDownloadSuccessAndTime(
bool success,
base::TimeTicks model_download_start_time) {
base::UmaHistogramBoolean("Permissions.AIv1.DownloadSuccess", success);
if (success) {
base::UmaHistogramMediumTimes(
"Permissions.AIv1.DownloadTime",
base::TimeTicks::Now() - model_download_start_time);
}
}
void LogOnDeviceModelPreviousSessionFinishedInTime(bool success) {
base::UmaHistogramBoolean("Permissions.AIv1.PreviousSessionFinishedInTime",
success);
}
void LogOnDeviceModelSessionCreationSuccessAndTime(
bool success,
base::TimeTicks session_creation_start_time) {
base::UmaHistogramBoolean("Permissions.AIv1.SessionCreationSuccess", success);
if (success) {
base::UmaHistogramMediumTimes(
"Permissions.AIv1.SessionCreationTime",
base::TimeTicks::Now() - session_creation_start_time);
}
}
void LogOnDeviceModelExecutionSuccessAndTime(
bool success,
base::TimeTicks session_execution_start_time) {
base::UmaHistogramBoolean("Permissions.AIv1.ExecutionSuccess", success);
if (success) {
base::UmaHistogramMediumTimes(
"Permissions.AIv1.ExecutionDuration",
base::TimeTicks::Now() - session_execution_start_time);
}
}
void LogOnDeviceModelExecutionParse(bool success) {
base::UmaHistogramBoolean("Permissions.AIv1.ResponseParseSuccess", success);
}
void LogOnDeviceModelAvailabilityAtInquiryTime(bool success) {
base::UmaHistogramBoolean("Permissions.AIv1.AvailableAtInquiryTime", success);
}
PermissionType GetPermissionType(permissions::RequestType request_type) {
switch (request_type) {
case permissions::RequestType::kNotifications:
return PermissionType::PERMISSION_TYPE_NOTIFICATIONS;
case permissions::RequestType::kGeolocation:
return PermissionType::PERMISSION_TYPE_GEOLOCATION;
default:
return PermissionType::PERMISSION_TYPE_NOT_SPECIFIED;
}
}
} // namespace
PermissionsAiHandler::PermissionsAiHandler(
OptimizationGuideKeyedService* optimization_guide)
: optimization_guide_(optimization_guide) {}
PermissionsAiHandler::~PermissionsAiHandler() {
StopListeningToOnDeviceModelUpdate();
}
void PermissionsAiHandler::StartListeningToOnDeviceModelUpdate() {
if (observing_on_device_model_availability_) {
return;
}
if (!optimization_guide_) {
LogOnDeviceModelDownloadSuccessAndTime(/*success=*/false,
on_device_download_start_time_);
return;
}
observing_on_device_model_availability_ = true;
optimization_guide_->AddOnDeviceModelAvailabilityChangeObserver(kFeatureKey,
this);
session_ =
optimization_guide_->StartSession(kFeatureKey, kSessionConfigParams);
if (session_) {
SetOnDeviceModelAvailable();
} else {
on_device_download_start_time_ = base::TimeTicks::Now();
}
}
void PermissionsAiHandler::StopListeningToOnDeviceModelUpdate() {
if (!observing_on_device_model_availability_ || !optimization_guide_) {
return;
}
observing_on_device_model_availability_ = false;
optimization_guide_->RemoveOnDeviceModelAvailabilityChangeObserver(
kFeatureKey, this);
}
void PermissionsAiHandler::SetOnDeviceModelAvailable() {
LogOnDeviceModelDownloadSuccessAndTime(/*success=*/true,
on_device_download_start_time_);
is_on_device_model_available_ = true;
observing_on_device_model_availability_ = false;
}
void PermissionsAiHandler::OnDeviceModelAvailabilityChanged(
ModelBasedCapabilityKey feature,
optimization_guide::OnDeviceModelEligibilityReason reason) {
if (!observing_on_device_model_availability_ || feature != kFeatureKey) {
return;
}
VLOG(1) << "[PermissionsAIv1] OnDeviceModelAvailability changed to state: "
<< reason;
if (kWaitableReasons.contains(reason)) {
return;
}
if (reason == optimization_guide::OnDeviceModelEligibilityReason::kSuccess) {
SetOnDeviceModelAvailable();
} else {
LogOnDeviceModelDownloadSuccessAndTime(/*success=*/false,
on_device_download_start_time_);
}
}
void PermissionsAiHandler::CreateModelExecutorSession() {
if (!optimization_guide_) {
return;
}
if (is_on_device_model_available_) {
session_ =
optimization_guide_->StartSession(kFeatureKey, kSessionConfigParams);
} else {
StartListeningToOnDeviceModelUpdate();
}
}
void PermissionsAiHandler::OnModelExecutionComplete(
optimization_guide::OptimizationGuideModelStreamingExecutionResult result) {
if (!result.response.has_value()) {
VLOG(1) << "[PermissionsAIv1] OnModelExecutionComplete failed with error: "
<< static_cast<int>(result.response.error().error());
LogOnDeviceModelExecutionSuccessAndTime(/*success=*/false,
session_execution_start_time_);
if (inquire_on_device_model_callback_) {
std::move(inquire_on_device_model_callback_).Run(std::nullopt);
}
return;
}
// This is a non-error response, but it's not completed, yet so we wait till
// it's complete. We will not respond to the callback yet because of this.
if (!result.response->is_complete) {
return;
}
LogOnDeviceModelExecutionSuccessAndTime(/*success=*/true,
session_execution_start_time_);
std::optional<PermissionsAiResponse> permissions_ai_response =
optimization_guide::ParsedAnyMetadata<PermissionsAiResponse>(
result.response->response);
if (!permissions_ai_response.has_value()) {
VLOG(1) << "[PermissionsAIv1] OnModelExecutionComplete failed while "
"parsing the response proto.";
LogOnDeviceModelExecutionParse(/*success=*/false);
if (inquire_on_device_model_callback_) {
std::move(inquire_on_device_model_callback_).Run(std::nullopt);
}
return;
}
LogOnDeviceModelExecutionParse(/*success=*/true);
if (session_) {
session_.reset();
}
if (inquire_on_device_model_callback_) {
std::move(inquire_on_device_model_callback_).Run(permissions_ai_response);
}
}
bool PermissionsAiHandler::IsOnDeviceModelAvailable() {
return is_on_device_model_available_;
}
void PermissionsAiHandler::InquireAiOnDeviceModel(
std::string rendered_text,
permissions::RequestType request_type,
base::OnceCallback<void(std::optional<PermissionsAiResponse>)> callback) {
// TODO(crbug.com/382447738): It can happen that a new inquiry comes before
// the previous finishes its execution. To avoid unexpected behavior return
// `std::nullopt` which means another type of CPSS logic will be executed.
if (session_) {
LogOnDeviceModelPreviousSessionFinishedInTime(/*success=*/false);
std::move(callback).Run(std::nullopt);
return;
} else if (is_on_device_model_available_) {
LogOnDeviceModelPreviousSessionFinishedInTime(/*success=*/true);
}
base::TimeTicks session_creation_start_time = base::TimeTicks::Now();
CreateModelExecutorSession();
LogOnDeviceModelAvailabilityAtInquiryTime(is_on_device_model_available_);
LogOnDeviceModelSessionCreationSuccessAndTime(
/*success=*/session_ != nullptr, session_creation_start_time);
if (!session_) {
std::move(callback).Run(std::nullopt);
return;
}
PermissionsAiRequest request;
request.set_rendered_text(std::move(rendered_text));
request.set_permission_type(GetPermissionType(request_type));
inquire_on_device_model_callback_ = std::move(callback);
session_execution_start_time_ = base::TimeTicks::Now();
session_->ExecuteModel(
request,
base::BindRepeating(&PermissionsAiHandler::OnModelExecutionComplete,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace permissions