blob: 390f3d72fa144f031222622bf59130014f4a754f [file] [log] [blame] [edit]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/scanner/scanner_controller.h"
#include <cstddef>
#include <functional>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/scanner/scanner_delegate.h"
#include "ash/public/cpp/scanner/scanner_feedback_info.h"
#include "ash/public/cpp/scanner/scanner_profile_scoped_delegate.h"
#include "ash/public/cpp/system/toast_data.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/scanner/scanner_action_handler.h"
#include "ash/scanner/scanner_action_view_model.h"
#include "ash/scanner/scanner_command_delegate_impl.h"
#include "ash/scanner/scanner_enterprise_policy.h"
#include "ash/scanner/scanner_feedback.h"
#include "ash/scanner/scanner_metrics.h"
#include "ash/scanner/scanner_session.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/screen_pinning_controller.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_view_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromeos/ash/components/specialized_features/feature_access_checker.h"
#include "components/account_id/account_id.h"
#include "components/feedback/feedback_constants.h"
#include "components/manta/proto/scanner.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/public/cpp/notifier_id.h"
namespace ash {
namespace {
using enum ScannerFeatureUserState;
constexpr char kScannerActionNotificationId[] = "scanner_action_notification";
constexpr char kScannerNotifierId[] = "ash.scanner";
constexpr char kScannerActionSuccessToastId[] = "scanner_action_success";
constexpr char kScannerActionFailureToastId[] = "scanner_action_failure";
constexpr size_t kUserFacingStringDepthLimit = 20;
constexpr size_t kUserFacingStringOutputLimit =
std::numeric_limits<size_t>::max();
std::u16string GetTitleForActionProgressNotification(
manta::proto::ScannerAction::ActionCase action_case) {
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_ADDING_EVENT);
case manta::proto::ScannerAction::kNewContact:
case manta::proto::ScannerAction::kNewGoogleDoc:
case manta::proto::ScannerAction::kNewGoogleSheet:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_CREATING);
case manta::proto::ScannerAction::kCopyToClipboard:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_PROGRESS_TITLE_COPYING);
case manta::proto::ScannerAction::ACTION_NOT_SET:
NOTREACHED();
}
}
std::u16string GetDisplaySourceForActionProgressNotification(
manta::proto::ScannerAction::ActionCase action_case) {
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
return l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ACTION_NEW_EVENT_SOURCE);
case manta::proto::ScannerAction::kNewContact:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_NEW_CONTACT_SOURCE);
case manta::proto::ScannerAction::kNewGoogleDoc:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_NEW_GOOGLE_DOC_SOURCE);
case manta::proto::ScannerAction::kNewGoogleSheet:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_NEW_GOOGLE_SHEET_SOURCE);
case manta::proto::ScannerAction::kCopyToClipboard:
return l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ACTION_COPY_TEXT_SOURCE);
case manta::proto::ScannerAction::ACTION_NOT_SET:
NOTREACHED();
}
}
std::u16string GetToastMessageForActionSuccess(
manta::proto::ScannerAction::ActionCase action_case) {
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_EVENT);
case manta::proto::ScannerAction::kNewContact:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_CONTACT);
case manta::proto::ScannerAction::kNewGoogleDoc:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_DOC);
case manta::proto::ScannerAction::kNewGoogleSheet:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_CREATE_SHEET);
case manta::proto::ScannerAction::kCopyToClipboard:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_SUCCESS_TOAST_COPY_TEXT_AND_FORMAT);
case manta::proto::ScannerAction::ACTION_NOT_SET:
NOTREACHED();
}
}
std::u16string GetToastMessageForActionFailure(
manta::proto::ScannerAction::ActionCase action_case) {
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_EVENT);
case manta::proto::ScannerAction::kNewContact:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_CONTACT);
case manta::proto::ScannerAction::kNewGoogleDoc:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_DOC);
case manta::proto::ScannerAction::kNewGoogleSheet:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_CREATE_SHEET);
case manta::proto::ScannerAction::kCopyToClipboard:
return l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_FAILURE_TOAST_COPY_TEXT_AND_FORMAT);
case manta::proto::ScannerAction::ACTION_NOT_SET:
NOTREACHED();
}
}
// Shows an action progress notification. The new notification will remove the
// previous action notification if there was one.
void ShowActionProgressNotification(
const ScannerActionViewModel& scanner_action) {
message_center::RichNotificationData optional_fields;
// Show an infinite loading progress bar.
optional_fields.progress = -1;
optional_fields.never_timeout = true;
optional_fields.pinned = true;
auto* message_center = message_center::MessageCenter::Get();
message_center->RemoveNotification(kScannerActionNotificationId,
/*by_user=*/false);
manta::proto::ScannerAction::ActionCase action_case =
scanner_action.GetActionCase();
std::unique_ptr<message_center::Notification> notification =
CreateSystemNotificationPtr(
message_center::NOTIFICATION_TYPE_PROGRESS,
kScannerActionNotificationId,
/*title=*/GetTitleForActionProgressNotification(action_case),
/*message=*/u"",
/*display_source=*/
GetDisplaySourceForActionProgressNotification(action_case), GURL(),
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kScannerNotifierId, NotificationCatalogName::kScannerAction),
optional_fields, /*delegate=*/nullptr, scanner_action.GetIcon(),
message_center::SystemNotificationWarningLevel::NORMAL);
notification->SetSystemPriority();
message_center->AddNotification(std::move(notification));
}
void RecordExecutePopulatedActionTimer(
manta::proto::ScannerAction::ActionCase action_case,
base::TimeTicks execute_start_time) {
// TODO(b/363101363): Add tests.
std::string_view variant_name;
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
variant_name = kScannerFeatureTimerExecutePopulatedNewCalendarEventAction;
break;
case manta::proto::ScannerAction::kNewContact:
variant_name = kScannerFeatureTimerExecutePopulatedNewContactAction;
break;
case manta::proto::ScannerAction::kNewGoogleDoc:
variant_name = kScannerFeatureTimerExecutePopulatedNewGoogleDocAction;
break;
case manta::proto::ScannerAction::kNewGoogleSheet:
variant_name = kScannerFeatureTimerExecutePopulatedNewGoogleSheetAction;
break;
case manta::proto::ScannerAction::kCopyToClipboard:
variant_name =
kScannerFeatureTimerExecutePopulatedNewCopyToClipboardAction;
break;
case manta::proto::ScannerAction::ACTION_NOT_SET:
break;
}
if (variant_name.empty()) {
return;
}
base::UmaHistogramMediumTimes(variant_name,
base::TimeTicks::Now() - execute_start_time);
}
void RecordPopulateActionTimer(
manta::proto::ScannerAction::ActionCase action_case,
base::TimeTicks request_start_time) {
// TODO(b/363101363): Add tests.
std::string_view variant_name;
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
variant_name = kScannerFeatureTimerPopulateNewCalendarEventAction;
break;
case manta::proto::ScannerAction::kNewContact:
variant_name = kScannerFeatureTimerPopulateNewContactAction;
break;
case manta::proto::ScannerAction::kNewGoogleDoc:
variant_name = kScannerFeatureTimerPopulateNewGoogleDocAction;
break;
case manta::proto::ScannerAction::kNewGoogleSheet:
variant_name = kScannerFeatureTimerPopulateNewGoogleSheetAction;
break;
case manta::proto::ScannerAction::kCopyToClipboard:
variant_name = kScannerFeatureTimerPopulateNewCopyToClipboardAction;
break;
case manta::proto::ScannerAction::ACTION_NOT_SET:
break;
}
if (variant_name.empty()) {
return;
}
base::UmaHistogramMediumTimes(variant_name,
base::TimeTicks::Now() - request_start_time);
}
void RecordPopulateActionFailure(
manta::proto::ScannerAction::ActionCase action_case) {
// TODO(b/363101363): Add tests.
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
RecordScannerFeatureUserState(kNewCalendarEventActionPopulationFailed);
return;
case manta::proto::ScannerAction::kNewContact:
RecordScannerFeatureUserState(kNewContactActionPopulationFailed);
return;
case manta::proto::ScannerAction::kNewGoogleDoc:
RecordScannerFeatureUserState(kNewGoogleDocActionPopulationFailed);
return;
case manta::proto::ScannerAction::kNewGoogleSheet:
RecordScannerFeatureUserState(kNewGoogleSheetActionPopulationFailed);
return;
case manta::proto::ScannerAction::kCopyToClipboard:
RecordScannerFeatureUserState(kCopyToClipboardActionPopulationFailed);
return;
case manta::proto::ScannerAction::ACTION_NOT_SET:
return;
}
}
void RecordActionExecutionAndRun(
manta::proto::ScannerAction::ActionCase action_case,
base::TimeTicks execute_start_time,
ScannerCommandCallback action_finished_callback,
bool success) {
// TODO(b/363101363): Add tests.
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
RecordScannerFeatureUserState(
success ? kNewCalendarEventActionFinishedSuccessfully
: kNewCalendarEventPopulatedActionExecutionFailed);
break;
case manta::proto::ScannerAction::kNewContact:
RecordScannerFeatureUserState(
success ? kNewContactActionFinishedSuccessfully
: kNewContactPopulatedActionExecutionFailed);
break;
case manta::proto::ScannerAction::kNewGoogleDoc:
RecordScannerFeatureUserState(
success ? kNewGoogleDocActionFinishedSuccessfully
: kNewGoogleDocPopulatedActionExecutionFailed);
break;
case manta::proto::ScannerAction::kNewGoogleSheet:
RecordScannerFeatureUserState(
success ? kNewGoogleSheetActionFinishedSuccessfully
: kNewGoogleSheetPopulatedActionExecutionFailed);
break;
case manta::proto::ScannerAction::kCopyToClipboard:
RecordScannerFeatureUserState(
success ? kCopyToClipboardActionFinishedSuccessfully
: kCopyToClipboardPopulatedActionExecutionFailed);
break;
case manta::proto::ScannerAction::ACTION_NOT_SET:
break;
}
RecordExecutePopulatedActionTimer(action_case, execute_start_time);
std::move(action_finished_callback).Run(success);
}
// Executes the populated action, if it exists, calling
// `action_finished_callback` with the result of the execution.
void ExecutePopulatedAction(
manta::proto::ScannerAction::ActionCase action_case,
base::TimeTicks request_start_time,
base::WeakPtr<ScannerCommandDelegate> delegate,
base::OnceCallback<void(manta::proto::ScannerAction populated_action,
bool success)> action_finished_callback,
manta::proto::ScannerAction populated_action) {
RecordPopulateActionTimer(action_case, request_start_time);
if (populated_action.action_case() ==
manta::proto::ScannerAction::ACTION_NOT_SET) {
RecordPopulateActionFailure(action_case);
std::move(action_finished_callback).Run(std::move(populated_action), false);
return;
}
ScannerCommandCallback record_metrics_callback = base::BindOnce(
&RecordActionExecutionAndRun, action_case, base::TimeTicks::Now(),
base::BindOnce(std::move(action_finished_callback), populated_action));
HandleScannerCommand(std::move(delegate),
ScannerActionToCommand(std::move(populated_action)),
std::move(record_metrics_callback));
}
void OnFeedbackFormSendButtonClicked(const AccountId& account_id,
base::Value::Dict action_dict,
ScannerFeedbackInfo feedback_info,
const std::string& user_description) {
RecordScannerFeatureUserState(ScannerFeatureUserState::kFeedbackSent);
std::optional<std::string> pretty_printed_action = base::WriteJsonWithOptions(
action_dict, base::JsonOptions::OPTIONS_PRETTY_PRINT);
// JSON serialisation should always succeed as the depth of the Dict is fixed,
// and no binary values should appear in the Dict.
CHECK(pretty_printed_action.has_value());
// Work around limitations with `feedback::RedactionTool` by prepending two
// spaces and appending a new line to any data to be redacted.
std::string description =
base::StrCat({"details: ", *pretty_printed_action,
"\nuser_description: ", user_description, "\n"});
Shell::Get()->shell_delegate()->SendSpecializedFeatureFeedback(
account_id, feedback::kScannerFeedbackProductId, std::move(description),
std::string(base::as_string_view(*feedback_info.screenshot)),
/*image_mime_type=*/"image/jpeg");
}
void SetStringIfPresent(const base::Value::Dict* dict,
const std::string& key,
auto* field) {
if (const std::string* value = dict->FindString(key)) {
*field = *value;
}
}
manta::proto::ScannerAction ScannerActionFromValue(
const base::Value::Dict& dict) {
manta::proto::ScannerAction action;
// The input dictionary dict is expected to contain exactly one of the
// following top-level keys, representing the type of action to perform.
if (const base::Value::Dict* new_event = dict.FindDict("new_event")) {
auto* event = action.mutable_new_event();
SetStringIfPresent(new_event, "title", event->mutable_title());
SetStringIfPresent(new_event, "dates", event->mutable_dates());
SetStringIfPresent(new_event, "description", event->mutable_description());
SetStringIfPresent(new_event, "location", event->mutable_location());
} else if (const base::Value::Dict* copy_action =
dict.FindDict("copy_to_clipboard")) {
auto* clipboard = action.mutable_copy_to_clipboard();
SetStringIfPresent(copy_action, "plain_text",
clipboard->mutable_plain_text());
SetStringIfPresent(copy_action, "html_text",
clipboard->mutable_html_text());
} else {
LOG(ERROR) << "Unknown scanner action type in mock response: " << dict;
}
return action;
}
std::unique_ptr<manta::proto::ScannerOutput> CreateMockScannerOutput(
const std::vector<std::string> mock_responses) {
auto mock_output = std::make_unique<manta::proto::ScannerOutput>();
manta::proto::ScannerObject* object = mock_output->add_objects();
for (const std::string& json_string : mock_responses) {
std::optional<base::Value> parsed_json =
base::JSONReader::Read(json_string, base::JSON_ALLOW_TRAILING_COMMAS);
if (!parsed_json.has_value() || !parsed_json->is_dict()) {
LOG(ERROR) << "Invalid json string: " << json_string;
continue;
}
base::Value::Dict& action_dict = parsed_json->GetDict();
manta::proto::ScannerAction action = ScannerActionFromValue(action_dict);
if (action.action_case() != manta::proto::ScannerAction::ACTION_NOT_SET) {
*object->add_actions() = std::move(action);
}
}
return mock_output;
}
} // namespace
ScannerController::ScannerController(
std::unique_ptr<ScannerDelegate> delegate,
SessionControllerImpl& session_controller,
const ScreenPinningController* screen_pinning_controller)
: delegate_(std::move(delegate)),
session_controller_(session_controller),
screen_pinning_controller_(screen_pinning_controller) {
if (screen_pinning_controller_ == nullptr) {
CHECK_IS_TEST();
}
}
ScannerController::~ScannerController() = default;
// static
void ScannerController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kScannerEnabled, true);
registry->RegisterIntegerPref(
prefs::kScannerEnterprisePolicyAllowed,
static_cast<int>(ScannerEnterprisePolicy::kAllowedWithModelImprovement));
registry->RegisterBooleanPref(
prefs::kScannerEntryPointDisclaimerAckSmartActionsButton, false);
registry->RegisterBooleanPref(
prefs::kScannerEntryPointDisclaimerAckSunfishSession, false);
}
// static
bool ScannerController::CanShowUiForShell() {
if (!Shell::HasInstance()) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToNoShellInstance);
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
ScannerController* controller = Shell::Get()->scanner_controller();
if (!controller) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::
kCanShowUiReturnedFalseDueToNoControllerOnShell);
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
return controller->CanShowUi();
}
void ScannerController::OnActiveUserSessionChanged(
const AccountId& account_id) {
scanner_session_ = nullptr;
command_delegate_ = nullptr;
}
bool ScannerController::CanShowUi() {
if (screen_pinning_controller_ == nullptr) {
CHECK_IS_TEST();
} else if (screen_pinning_controller_->IsPinned()) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToPinnedMode);
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
// Check enterprise policy.
const AccountId& account_id = session_controller_->GetActiveAccountId();
PrefService* prefs =
session_controller_->GetUserPrefServiceForUser(account_id);
// We assume a default value of 1 (allowed without model improvement) if the
// value is invalid, or the pref service isn't valid.
if (prefs != nullptr &&
prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
static_cast<int>(ScannerEnterprisePolicy::kDisallowed)) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToEnterprisePolicy);
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
ScannerProfileScopedDelegate* profile_scoped_delegate =
delegate_->GetProfileScopedDelegate();
if (profile_scoped_delegate == nullptr) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::
kCanShowUiReturnedFalseDueToNoProfileScopedDelegate);
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
specialized_features::FeatureAccessFailureSet checks =
profile_scoped_delegate->CheckFeatureAccess();
bool consent_accepted = true;
bool show_ui = true;
for (specialized_features::FeatureAccessFailure failure : checks) {
switch (failure) {
case specialized_features::FeatureAccessFailure::kConsentNotAccepted:
consent_accepted = false;
break;
case specialized_features::FeatureAccessFailure::kDisabledInSettings:
RecordScannerFeatureUserState(
ScannerFeatureUserState::
kCanShowUiReturnedFalseDueToSettingsToggle);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::kFeatureFlagDisabled:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToFeatureFlag);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::
kFeatureManagementCheckFailed:
RecordScannerFeatureUserState(
ScannerFeatureUserState::
kCanShowUiReturnedFalseDueToFeatureManagement);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::kSecretKeyCheckFailed:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToSecretKey);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::
kAccountCapabilitiesCheckFailed:
RecordScannerFeatureUserState(
ScannerFeatureUserState::
kCanShowUiReturnedFalseDueToAccountCapabilities);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::kCountryCheckFailed:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToCountry);
show_ui = false;
break;
case specialized_features::FeatureAccessFailure::
kDisabledInKioskModeCheckFailed:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalseDueToKioskMode);
show_ui = false;
break;
}
}
if (!show_ui) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedFalse);
return false;
}
if (!consent_accepted) {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedTrueWithoutConsent);
} else {
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCanShowUiReturnedTrueWithConsent);
}
return true;
}
bool ScannerController::CanShowFeatureSettingsToggle() {
if (screen_pinning_controller_ == nullptr) {
CHECK_IS_TEST();
} else if (screen_pinning_controller_->IsPinned()) {
return false;
}
// Intentionally ignore enterprise policy here, as we still want to show the
// settings toggle (as disabled).
ScannerProfileScopedDelegate* profile_scoped_delegate =
delegate_->GetProfileScopedDelegate();
if (profile_scoped_delegate == nullptr) {
return false;
}
specialized_features::FeatureAccessFailureSet checks =
profile_scoped_delegate->CheckFeatureAccess();
// Show settings toggle even if the setting is disabled or consent not
// accepted.
// Hence we ignore these checks if they have failed.
checks.Remove(
specialized_features::FeatureAccessFailure::kDisabledInSettings);
checks.Remove(
specialized_features::FeatureAccessFailure::kConsentNotAccepted);
return checks.empty();
}
bool ScannerController::CanStartSession() {
if (screen_pinning_controller_ == nullptr) {
CHECK_IS_TEST();
} else if (screen_pinning_controller_->IsPinned()) {
return false;
}
// Check enterprise policy.
const AccountId& account_id = session_controller_->GetActiveAccountId();
PrefService* prefs =
session_controller_->GetUserPrefServiceForUser(account_id);
// We assume a default value of 1 (allowed without model improvement) if the
// value is invalid, or the pref service isn't valid.
if (prefs != nullptr &&
prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
static_cast<int>(ScannerEnterprisePolicy::kDisallowed)) {
return false;
}
ScannerProfileScopedDelegate* profile_scoped_delegate =
delegate_->GetProfileScopedDelegate();
if (profile_scoped_delegate == nullptr) {
return false;
}
if (!profile_scoped_delegate->CheckFeatureAccess().empty()) {
return false;
}
return true;
}
ScannerSession* ScannerController::StartNewSession() {
// Reset the current session if there is one. We do this here to ensure that
// the old session is destroyed before attempting to create the new session
// (to avoid subtle issues from having simultaneously existing sessions).
scanner_session_ = nullptr;
if (CanStartSession()) {
scanner_session_ =
std::make_unique<ScannerSession>(delegate_->GetProfileScopedDelegate());
}
return scanner_session_.get();
}
bool ScannerController::FetchActionsForImage(
scoped_refptr<base::RefCountedMemory> jpeg_bytes,
ScannerSession::FetchActionsCallback callback) {
if (!scanner_session_) {
std::move(callback).Run({});
return false;
}
if (!mock_scanner_responses_for_testing_.empty()) {
scanner_session_->SetMockScannerOutput(CreateMockScannerOutput(
std::move(mock_scanner_responses_for_testing_)));
mock_scanner_responses_for_testing_.clear();
}
scanner_session_->FetchActionsForImage(jpeg_bytes, std::move(callback));
return true;
}
void ScannerController::OnSessionUIClosed() {
scanner_session_ = nullptr;
}
void ScannerController::ExecuteAction(
const ScannerActionViewModel& scanner_action) {
if (!scanner_session_) {
return;
}
if (!mock_scanner_responses_for_testing_.empty()) {
scanner_session_->SetMockScannerOutput(CreateMockScannerOutput(
std::move(mock_scanner_responses_for_testing_)));
mock_scanner_responses_for_testing_.clear();
}
// Keep the existing `command_delegate_` if there is one, to allow commands
// from previous sessions to continue in the background if needed.
if (!command_delegate_) {
command_delegate_ = std::make_unique<ScannerCommandDelegateImpl>(
delegate_->GetProfileScopedDelegate());
}
const manta::proto::ScannerAction::ActionCase action_case =
scanner_action.GetActionCase();
scanner_session_->PopulateAction(
scanner_action.downscaled_jpeg_bytes(),
scanner_action.unpopulated_action(),
base::BindOnce(&ExecutePopulatedAction, action_case,
base::TimeTicks::Now(), command_delegate_->GetWeakPtr(),
base::BindOnce(&ScannerController::OnActionFinished,
weak_ptr_factory_.GetWeakPtr(), action_case,
scanner_action.downscaled_jpeg_bytes())));
ShowActionProgressNotification(scanner_action);
}
void ScannerController::OpenFeedbackDialog(
const AccountId& account_id,
manta::proto::ScannerAction action,
scoped_refptr<base::RefCountedMemory> screenshot) {
RecordScannerFeatureUserState(ScannerFeatureUserState::kFeedbackFormOpened);
base::Value::Dict action_dict = ScannerActionToDict(std::move(action));
std::optional<std::string> user_facing_string = ValueToUserFacingString(
action_dict, kUserFacingStringDepthLimit, kUserFacingStringOutputLimit);
// `user_facing_string` can only be nullopt if:
// - `ScannerActionToDict` output a binary value, which is impossible,
// - `ScannerActionToDict` output a more-than-twenty nested value, which is
// impossible (all returned values are at most three-nested)
// - the excessively large output limit is hit, which should be impossible.
CHECK(user_facing_string.has_value());
delegate_->OpenFeedbackDialog(
account_id,
ScannerFeedbackInfo(std::move(*user_facing_string),
std::move(screenshot)),
base::BindOnce(&OnFeedbackFormSendButtonClicked, account_id,
std::move(action_dict)));
}
void ScannerController::SetOnActionFinishedForTesting(
OnActionFinishedCallback callback) {
on_action_finished_for_testing_ = std::move(callback);
}
bool ScannerController::HasActiveSessionForTesting() const {
return !!scanner_session_;
}
void ScannerController::OnActionFinished(
manta::proto::ScannerAction::ActionCase action_case,
scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes,
manta::proto::ScannerAction populated_action,
bool success) {
// Remove the action progress notification.
message_center::MessageCenter::Get()->RemoveNotification(
kScannerActionNotificationId,
/*by_user=*/false);
if (success) {
if (features::IsScannerFeedbackToastEnabled()) {
ToastData toast_data(kScannerActionSuccessToastId,
ToastCatalogName::kScannerActionSuccess,
GetToastMessageForActionSuccess(action_case));
// TODO: crbug.com/367882164 - Pass in the account ID to this method to
// ensure that the feedback form is shown for the same account that
// performed the action.
const AccountId& account_id = session_controller_->GetActiveAccountId();
PrefService* prefs =
session_controller_->GetUserPrefServiceForUser(account_id);
if (prefs &&
prefs->GetInteger(prefs::kScannerEnterprisePolicyAllowed) ==
static_cast<int>(
ScannerEnterprisePolicy::kAllowedWithModelImprovement)) {
toast_data.button_type = ToastData::ButtonType::kIconButton;
toast_data.button_text = l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ACTION_TOAST_FEEDBACK_ICON_ACCESSIBLE_NAME);
toast_data.button_icon = &kFeedbackIcon;
// TODO: crbug.com/259100049 - Change this to be `BindOnce` once
// `ToastData::button_callback` is migrated to be a `OnceClosure`.
toast_data.button_callback = base::BindRepeating(
&ScannerController::OpenFeedbackDialog,
weak_ptr_factory_.GetWeakPtr(), account_id,
std::move(populated_action), std::move(downscaled_jpeg_bytes));
}
ToastManager::Get()->Show(std::move(toast_data));
} else if (action_case == manta::proto::ScannerAction::kCopyToClipboard) {
// If feedback is disabled, only show a success toast for the copy to
// clipboard action.
ToastData toast_data(kScannerActionSuccessToastId,
ToastCatalogName::kScannerActionSuccess,
GetToastMessageForActionSuccess(action_case));
ToastManager::Get()->Show(std::move(toast_data));
}
} else {
ToastManager::Get()->Show(ToastData(
kScannerActionFailureToastId, ToastCatalogName::kScannerActionFailure,
GetToastMessageForActionFailure(action_case)));
}
if (!on_action_finished_for_testing_.is_null()) {
CHECK_IS_TEST();
std::move(on_action_finished_for_testing_).Run(success);
}
}
void ScannerController::SetScannerResponsesForTesting(
std::vector<std::string> responses) {
mock_scanner_responses_for_testing_ = std::move(responses);
}
} // namespace ash