blob: 52e8ac76a65609ac4c55f656214645b32ea8b357 [file] [log] [blame]
// 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_session.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "ash/public/cpp/scanner/scanner_profile_scoped_delegate.h"
#include "ash/scanner/scanner_action_view_model.h"
#include "ash/scanner/scanner_controller.h"
#include "ash/scanner/scanner_metrics.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/functional/callback.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/time/time.h"
#include "components/manta/manta_status.h"
#include "components/manta/proto/scanner.pb.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/codec/jpeg_codec.h"
namespace ash {
namespace {
// The maximum number of pixels in either width or height of an image to be
// processed by scanner.
// We chose a conservative number based on typical laptop screen sizes in case
// that is required by the backend.
constexpr int64_t kMaxEdgePixels = 2300;
// The quality attribute (out of 100) for the JPEG compression algorithm used.
constexpr int kJpegQuality = 90;
// Downscales so that the width and height are both less than kMaxEdgePixels and
// retains the ratio.
scoped_refptr<base::RefCountedMemory> DownscaleImageIfNeeded(
scoped_refptr<base::RefCountedMemory> original_bytes) {
if (original_bytes == nullptr) {
return nullptr;
}
SkBitmap img = gfx::JPEGCodec::Decode(*original_bytes);
int width = img.width();
int height = img.height();
if (width <= 0 || height <= 0 ||
(width <= kMaxEdgePixels && height <= kMaxEdgePixels) ||
(width * height <= kMaxEdgePixels * kMaxEdgePixels)) {
return original_bytes;
}
if (width > height) {
height = kMaxEdgePixels * height / width;
width = kMaxEdgePixels;
} else {
width = kMaxEdgePixels * width / height;
height = kMaxEdgePixels;
}
// If the height and width is 0 given that we only try to resize when the
// total pixels are less than 2300x2300, it will only be possible if it was
// 5 million pixels in one edge and then 1 pixel in the other.
//
// This is clearly an edge case so we will return nullptr to stop processing
// this image.
if (height == 0 || width == 0) {
return nullptr;
}
std::optional<std::vector<uint8_t>> shrunk_jpeg_bytes =
gfx::JPEGCodec::Encode(
skia::ImageOperations::Resize(img, skia::ImageOperations::RESIZE_BEST,
width, height),
kJpegQuality);
if (shrunk_jpeg_bytes.has_value()) {
return base::MakeRefCounted<base::RefCountedBytes>(
std::move(*shrunk_jpeg_bytes));
}
// Fallback.
return original_bytes;
}
// Runs the callback with the populated proto from the response of a call to
// `FetchActionDetailsForImage`.
void OnActionPopulated(ScannerSession::PopulateActionCallback callback,
std::unique_ptr<manta::proto::ScannerOutput> output,
manta::MantaStatus status) {
if (output == nullptr) {
// TODO(b/363100868): Handle error case
std::move(callback).Run(manta::proto::ScannerAction());
return;
}
if (output->objects_size() != 1) {
std::move(callback).Run(manta::proto::ScannerAction());
return;
}
manta::proto::ScannerObject& object = *output->mutable_objects(0);
if (object.actions_size() != 1) {
std::move(callback).Run(manta::proto::ScannerAction());
return;
}
manta::proto::ScannerAction& action = *object.mutable_actions(0);
std::move(callback).Run(std::move(action));
}
void RecordDetectedAction(manta::proto::ScannerAction::ActionCase action_case) {
switch (action_case) {
case manta::proto::ScannerAction::kNewEvent:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kNewCalendarEventActionDetected);
return;
case manta::proto::ScannerAction::kNewContact:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kNewContactActionDetected);
return;
case manta::proto::ScannerAction::kNewGoogleDoc:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kNewGoogleDocActionDetected);
return;
case manta::proto::ScannerAction::kNewGoogleSheet:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kNewGoogleSheetActionDetected);
return;
case manta::proto::ScannerAction::kCopyToClipboard:
RecordScannerFeatureUserState(
ScannerFeatureUserState::kCopyToClipboardActionDetected);
return;
case manta::proto::ScannerAction::ACTION_NOT_SET:
return;
}
// This should never be reached, as `action_case()` should always return a
// valid enum value. If the oneof field is set to something which is not
// recognised by this client, that is indistinguishable from an unknown field,
// and the above case should be `ACTION_NOT_SET`.
NOTREACHED();
}
void RecordAllDetectedActions(const manta::proto::ScannerOutput& output) {
bool action_found = false;
for (const manta::proto::ScannerObject& object : output.objects()) {
for (const manta::proto::ScannerAction& action : object.actions()) {
if (action.action_case() != manta::proto::ScannerAction::ACTION_NOT_SET) {
action_found = true;
RecordDetectedAction(action.action_case());
}
}
}
if (!action_found) {
RecordScannerFeatureUserState(ScannerFeatureUserState::kNoActionsDetected);
}
}
} // namespace
ScannerSession::ScannerSession(ScannerProfileScopedDelegate* delegate)
: delegate_(delegate) {}
ScannerSession::~ScannerSession() = default;
void ScannerSession::FetchActionsForImage(
scoped_refptr<base::RefCountedMemory> jpeg_bytes,
FetchActionsCallback callback) {
scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes =
DownscaleImageIfNeeded(std::move(jpeg_bytes));
if (mock_scanner_output_) {
OnActionsReturned(downscaled_jpeg_bytes, base::TimeTicks::Now(),
std::move(callback), std::move(mock_scanner_output_),
manta::MantaStatus(manta::MantaStatusCode::kOk));
return;
}
delegate_->FetchActionsForImage(
downscaled_jpeg_bytes,
base::BindOnce(&ScannerSession::OnActionsReturned,
weak_ptr_factory_.GetWeakPtr(), downscaled_jpeg_bytes,
base::TimeTicks::Now(), std::move(callback)));
}
void ScannerSession::SetMockScannerOutput(
std::unique_ptr<manta::proto::ScannerOutput> mock_output) {
mock_scanner_output_ = std::move(mock_output);
}
void ScannerSession::OnActionsReturned(
scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes,
base::TimeTicks request_start_time,
FetchActionsCallback callback,
std::unique_ptr<manta::proto::ScannerOutput> output,
manta::MantaStatus status) {
base::UmaHistogramMediumTimes(kScannerFeatureTimerFetchActionsForImage,
base::TimeTicks::Now() - request_start_time);
if (status.status_code == manta::MantaStatusCode::kUnsupportedLanguage) {
std::move(callback).Run(base::unexpected(FetchError{
.error_message = l10n_util::GetStringUTF16(
IDS_ASH_SCANNER_ERROR_UNSUPPORTED_LANGUAGE),
.can_try_again = false,
}));
return;
}
if (output == nullptr) {
std::move(callback).Run(base::unexpected(FetchError{
.error_message =
l10n_util::GetStringUTF16(IDS_ASH_SCANNER_ERROR_GENERIC),
.can_try_again = true,
}));
return;
}
RecordAllDetectedActions(*output);
if (output->objects_size() == 0) {
std::move(callback).Run({});
return;
}
std::vector<ScannerActionViewModel> action_view_models;
for (manta::proto::ScannerAction& proto_action :
*output->mutable_objects(0)->mutable_actions()) {
if (proto_action.action_case() !=
manta::proto::ScannerAction::ACTION_NOT_SET) {
action_view_models.emplace_back(std::move(proto_action),
downscaled_jpeg_bytes);
}
}
std::move(callback).Run(std::move(action_view_models));
}
void ScannerSession::PopulateAction(
scoped_refptr<base::RefCountedMemory> downscaled_jpeg_bytes,
manta::proto::ScannerAction unpopulated_action,
PopulateActionCallback callback) {
if (mock_scanner_output_) {
OnActionPopulated(std::move(callback), std::move(mock_scanner_output_),
{manta::MantaStatusCode::kOk});
return;
}
delegate_->FetchActionDetailsForImage(
std::move(downscaled_jpeg_bytes), std::move(unpopulated_action),
base::BindOnce(&OnActionPopulated, std::move(callback)));
}
} // namespace ash