0

specialized_features: Add ShellDelegate::SendSpecializedFeatureFeedback

…and migrate existing Lobster usages of it over to it. This method will
later be used by Scanner.
Migrating Editor to this new method is currently unnecessary until it is
moved out of //c/b/a/input_method, see b/384383652 for more details.

To avoid introducing a behavioural change, an explicit account ID is
passed into `SendSpecializedFeatureFeedback`. The existing Lobster usage
now passes in the account ID from the profile keyed service, but this
could be migrated over to pure Ash code with
`UserManager::Get()->GetActiveUser()->GetAccountId()` when the client is
created (from the active profile).

As unit tests in //chromeos/ash/components/specialized_features cannot
use the real `feedback::FeedbackUploaderChrome` dependency from Chrome,
this CL keeps around //c/b/a/lobster/lobster_feedback_unittest.cc even
though its associated implementation has been removed.

Bug: b:367882164
Change-Id: I6a6a210040ef70992a38769c1f7563e3d232be10
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6151774
Commit-Queue: Michael Cui <mlcui@google.com>
Reviewed-by: Hidehiko Abe <hidehiko@chromium.org>
Reviewed-by: Chuong Ho <hdchuong@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1407135}
This commit is contained in:
Michael Cui
2025-01-15 22:20:06 -08:00
committed by Chromium LUCI CQ
parent 5449508391
commit b16cc99002
21 changed files with 268 additions and 138 deletions

@ -3493,6 +3493,7 @@ component("ash") {
"//components/endpoint_fetcher",
"//components/favicon/core",
"//components/favicon_base:favicon_base",
"//components/feedback",
"//components/fullscreen_control",
"//components/global_media_controls",
"//components/history/core/browser",
@ -4730,6 +4731,7 @@ test("ash_unittests") {
"//components/favicon/core/test:test_support",
"//components/feature_engagement/public",
"//components/feature_engagement/test:test_support",
"//components/feedback",
"//components/global_media_controls",
"//components/global_media_controls:test_support",
"//components/history/core/browser",

@ -13,6 +13,7 @@ include_rules = [
"+components/desks_storage",
"+components/discardable_memory/public",
"+components/discardable_memory/service/discardable_shared_memory_manager.h",
"+components/feedback",
"+components/fullscreen_control",
"+components/global_media_controls",
"+components/language/core/browser/pref_names.h",

@ -17,12 +17,15 @@
#include "ash/public/cpp/lobster/lobster_client.h"
#include "ash/public/cpp/lobster/lobster_image_candidate.h"
#include "ash/public/cpp/lobster/lobster_metrics_state_enums.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/i18n/file_util_icu.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "components/feedback/feedback_constants.h"
namespace ash {
@ -259,8 +262,11 @@ bool LobsterSessionImpl::SubmitFeedback(int candidate_id,
// TODO: b/362403784 - add the proper version.
std::string feedback_description = BuildFeedbackDescription(
candidate->query, /*model_version=*/"dummy_version", description);
return client_->SubmitFeedback(std::move(feedback_description),
candidate->image_bytes);
return Shell::Get()->shell_delegate()->SendSpecializedFeatureFeedback(
client_->GetAccountId(), feedback::kLobsterFeedbackProductId,
std::move(feedback_description), std::move(candidate->image_bytes),
/*image_mime_type=*/std::nullopt);
}
void LobsterSessionImpl::OnRequestCandidates(RequestCandidatesCallback callback,

@ -4,20 +4,28 @@
#include "ash/lobster/lobster_session_impl.h"
#include <memory>
#include <optional>
#include <string_view>
#include <utility>
#include "ash/lobster/lobster_candidate_store.h"
#include "ash/public/cpp/lobster/lobster_client.h"
#include "ash/public/cpp/lobster/lobster_enums.h"
#include "ash/public/cpp/lobster/lobster_metrics_state_enums.h"
#include "ash/public/cpp/lobster/lobster_session.h"
#include "ash/public/cpp/lobster/lobster_system_state.h"
#include "ash/test/ash_test_base.h"
#include "ash/test_shell_delegate.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/account_id/account_id.h"
#include "components/feedback/feedback_constants.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ime/ash/input_method_ash.h"
@ -26,6 +34,10 @@
namespace ash {
namespace {
using ::testing::_;
using ::testing::Eq;
using ::testing::Optional;
LobsterCandidateStore GetDummyLobsterCandidateStore() {
LobsterCandidateStore store;
store.Cache({.id = 0,
@ -42,7 +54,9 @@ LobsterCandidateStore GetDummyLobsterCandidateStore() {
class MockLobsterClient : public LobsterClient {
public:
MockLobsterClient() {}
MockLobsterClient() {
ON_CALL(*this, GetAccountId).WillByDefault(&EmptyAccountId);
}
MockLobsterClient(const MockLobsterClient&) = delete;
MockLobsterClient& operator=(const MockLobsterClient&) = delete;
@ -63,10 +77,6 @@ class MockLobsterClient : public LobsterClient {
const std::string& query,
InflateCandidateCallback),
(override));
MOCK_METHOD(bool,
SubmitFeedback,
(std::string description, const std::string& image_bytes),
(override));
MOCK_METHOD(void,
QueueInsertion,
(const std::string& image_bytes,
@ -80,6 +90,7 @@ class MockLobsterClient : public LobsterClient {
(override));
MOCK_METHOD(void, ShowUI, (), (override));
MOCK_METHOD(void, CloseUI, (), (override));
MOCK_METHOD(const AccountId&, GetAccountId, (), (override));
};
class LobsterSessionImplTest : public AshTestBase {
@ -90,7 +101,10 @@ class LobsterSessionImplTest : public AshTestBase {
~LobsterSessionImplTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
auto shell_delegate = std::make_unique<TestShellDelegate>();
shell_delegate->SetSendSpecializedFeatureFeedbackCallback(
mock_send_specialized_feature_feedback_.Get());
AshTestBase::SetUp(std::move(shell_delegate));
ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
}
@ -100,10 +114,18 @@ class LobsterSessionImplTest : public AshTestBase {
ui::InputMethod& ime() { return ime_; }
base::MockCallback<TestShellDelegate::SendSpecializedFeatureFeedbackCallback>&
mock_send_specialized_feature_feedback() {
return mock_send_specialized_feature_feedback_;
}
private:
InputMethodAsh ime_{nullptr};
base::ScopedTempDir scoped_temp_dir_;
base::HistogramTester histogram_tester_;
base::MockCallback<TestShellDelegate::SendSpecializedFeatureFeedbackCallback>
mock_send_specialized_feature_feedback_;
};
TEST_F(LobsterSessionImplTest, RequestCandidatesWithThreeResults) {
@ -241,27 +263,56 @@ TEST_F(LobsterSessionImplTest, CanPreviewFeedbackForACandidateIfItIsInCache) {
EXPECT_EQ(future.Get()->fields, expected_feedback_preview_fields);
}
TEST_F(LobsterSessionImplTest,
CanNotSubmitFeedbackForACandiateIfItIsNotCached) {
auto lobster_client = std::make_unique<MockLobsterClient>();
TEST_F(LobsterSessionImplTest, SubmitFeedbackUsesClientAccountId) {
LobsterCandidateStore store = GetDummyLobsterCandidateStore();
auto lobster_client = std::make_unique<MockLobsterClient>();
AccountId fake_account_id =
AccountId::FromUserEmailGaiaId("user@test.com", GaiaId("fakegaia"));
EXPECT_CALL(*lobster_client, GetAccountId)
.WillOnce(testing::ReturnRef(fake_account_id));
ON_CALL(*lobster_client,
SubmitFeedback(/*description=*/"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/"a1b2c3"))
.WillByDefault(testing::Return(true));
ON_CALL(*lobster_client,
SubmitFeedback(/*description=*/"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/"d4e5f6"))
.WillByDefault(testing::Return(true));
EXPECT_CALL(mock_send_specialized_feature_feedback(),
Run(fake_account_id, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("a1b2c3")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillOnce(testing::Return(true));
LobsterSessionImpl session(std::move(lobster_client), store,
LobsterEntryPoint::kQuickInsert);
EXPECT_TRUE(session.SubmitFeedback(/*candidate_id=*/0,
/*description=*/"Awesome raspberry"));
}
TEST_F(LobsterSessionImplTest,
CanNotSubmitFeedbackForACandiateIfItIsNotCached) {
LobsterCandidateStore store = GetDummyLobsterCandidateStore();
ON_CALL(mock_send_specialized_feature_feedback(),
Run(_, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("a1b2c3")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillByDefault(testing::Return(true));
ON_CALL(mock_send_specialized_feature_feedback(),
Run(_, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("d4e5f6")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillByDefault(testing::Return(true));
LobsterSessionImpl session(std::make_unique<MockLobsterClient>(), store,
LobsterEntryPoint::kQuickInsert);
EXPECT_FALSE(session.SubmitFeedback(/*candidate_id*/ 2,
/*description=*/"Awesome raspberry"));
}
@ -269,42 +320,47 @@ TEST_F(LobsterSessionImplTest,
TEST_F(LobsterSessionImplTest,
CanNotSubmitFeedbackForACandiateIfSubmissionFails) {
LobsterCandidateStore store = GetDummyLobsterCandidateStore();
auto lobster_client = std::make_unique<MockLobsterClient>();
ON_CALL(*lobster_client,
SubmitFeedback(/*description=*/"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/"a1b2c3"))
ON_CALL(mock_send_specialized_feature_feedback(),
Run(_, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("a1b2c3")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillByDefault(testing::Return(false));
LobsterSessionImpl session(std::move(lobster_client), store,
LobsterSessionImpl session(std::make_unique<MockLobsterClient>(), store,
LobsterEntryPoint::kQuickInsert);
EXPECT_FALSE(session.SubmitFeedback(/*candidate_id*/ 0,
/*description=*/"Awesome raspberry"));
}
TEST_F(LobsterSessionImplTest, CanSubmitFeedbackForACandiateIfItIsInCache) {
auto lobster_client = std::make_unique<MockLobsterClient>();
LobsterCandidateStore store = GetDummyLobsterCandidateStore();
EXPECT_CALL(
*lobster_client,
SubmitFeedback(/*description=*/"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/"a1b2c3"))
EXPECT_CALL(mock_send_specialized_feature_feedback(),
Run(_, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("a1b2c3")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillOnce(testing::Return(true));
EXPECT_CALL(
*lobster_client,
SubmitFeedback(/*description=*/"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/"d4e5f6"))
EXPECT_CALL(mock_send_specialized_feature_feedback(),
Run(_, feedback::kLobsterFeedbackProductId,
/*description=*/
"model_input: a nice raspberry\n"
"model_version: dummy_version\n"
"user_description: Awesome raspberry",
/*image_bytes=*/Optional(Eq("d4e5f6")),
/*image_mime_type=*/Eq(std::nullopt)))
.WillOnce(testing::Return(true));
LobsterSessionImpl session(std::move(lobster_client), store,
LobsterSessionImpl session(std::make_unique<MockLobsterClient>(), store,
LobsterEntryPoint::kQuickInsert);
EXPECT_TRUE(session.SubmitFeedback(/*candidate_id*/ 0,
/*description=*/"Awesome raspberry"));

@ -14,6 +14,8 @@
#include "ash/public/cpp/lobster/lobster_system_state.h"
#include "base/functional/callback.h"
class AccountId;
namespace ash {
class ASH_PUBLIC_EXPORT LobsterClient {
@ -31,13 +33,16 @@ class ASH_PUBLIC_EXPORT LobsterClient {
InflateCandidateCallback) = 0;
virtual void QueueInsertion(const std::string& image_bytes,
StatusCallback insert_status_callback) = 0;
virtual bool SubmitFeedback(std::string description,
const std::string& image_bytes) = 0;
virtual void LoadUI(std::optional<std::string> query,
LobsterMode mode,
const gfx::Rect& caret_bounds) = 0;
virtual void ShowUI() = 0;
virtual void CloseUI() = 0;
// Returns the account ID associated with this client.
// As the class is created by getting the active user profile, this is
// equivalent to getting the active user's account ID when this class was
// created.
virtual const AccountId& GetAccountId() = 0;
};
} // namespace ash

@ -6,6 +6,8 @@
#define ASH_SHELL_DELEGATE_H_
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "ash/ash_export.h"
@ -23,6 +25,7 @@
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"
class AccountId;
namespace aura {
class Window;
}
@ -204,6 +207,18 @@ class ASH_EXPORT ShellDelegate {
const std::string& description_template,
const std::string& category_tag) = 0;
// Uploads feedback about a specialized feature after redacting the given
// description using the given account ID.
// Returns false if there is no feedback uploader for the given account ID.
// See //chromeos/ash/components/specialized_features/feedback.h for more
// details.
virtual bool SendSpecializedFeatureFeedback(
const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type) = 0;
// Calls browser service to open the profile manager.
virtual void OpenProfileManager() = 0;

@ -5,7 +5,9 @@
#include "ash/test_shell_delegate.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "ash/accelerators/test_accelerator_prefs_delegate.h"
#include "ash/accessibility/default_accessibility_delegate.h"
@ -214,6 +216,19 @@ void TestShellDelegate::OpenFeedbackDialog(
++open_feedback_dialog_call_count_;
}
bool TestShellDelegate::SendSpecializedFeatureFeedback(
const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type) {
return send_specialized_feature_feedback_callback_
? send_specialized_feature_feedback_callback_.Run(
account_id, product_id, std::move(description),
std::move(image), std::move(image_mime_type))
: true;
}
const GURL& TestShellDelegate::GetLastCommittedURLForWindowIfAny(
aura::Window* window) {
return last_committed_url_;

@ -6,7 +6,9 @@
#define ASH_TEST_SHELL_DELEGATE_H_
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "ash/public/cpp/tab_strip_delegate.h"
#include "ash/shell_delegate.h"
@ -51,6 +53,23 @@ class TestShellDelegate : public ShellDelegate {
user_education_delegate_factory_ = std::move(factory);
}
// Allows tests to override and mock `SendSpecializedFeatureFeedback`.
// For example:
// base::MockCallback<
// TestShellDelegate::SendSpecializedFeatureFeedbackCallback> cb;
// EXPECT_CALL(cb, Run(_, kProductId, _, _, _));
// shell_delegate.SetSendSpecializedFeatureFeedbackCallback(cb.Get());
using SendSpecializedFeatureFeedbackCallback =
base::RepeatingCallback<bool(const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type)>;
void SetSendSpecializedFeatureFeedbackCallback(
SendSpecializedFeatureFeedbackCallback cb) {
send_specialized_feature_feedback_callback_ = std::move(cb);
}
// Overridden from ShellDelegate:
bool CanShowWindowForUser(const aura::Window* window) const override;
std::unique_ptr<CaptureModeDelegate> CreateCaptureModeDelegate()
@ -113,6 +132,12 @@ class TestShellDelegate : public ShellDelegate {
void OpenFeedbackDialog(FeedbackSource source,
const std::string& description_template,
const std::string& category_tag) override;
bool SendSpecializedFeatureFeedback(
const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type) override;
void OpenProfileManager() override {}
void SetLastCommittedURLForWindow(const GURL& url);
version_info::Channel GetChannel() override;
@ -147,6 +172,8 @@ class TestShellDelegate : public ShellDelegate {
MultiDeviceSetupBinder multidevice_setup_binder_;
UserEducationDelegateFactory user_education_delegate_factory_;
SendSpecializedFeatureFeedbackCallback
send_specialized_feature_feedback_callback_;
scoped_refptr<network::TestSharedURLLoaderFactory> url_loader_factory_;

@ -113,6 +113,8 @@ void EditorSystemActuator::CloseUI() {
}
void EditorSystemActuator::SubmitFeedback(const std::string& description) {
// TODO: b/384383652 - Use `ShellDelegate::SendSpecializedFeatureFeedback`
// after this is moved out of //chrome.
SendEditorFeedback(profile_, description);
system_->Announce(
l10n_util::GetStringUTF16(IDS_EDITOR_ANNOUNCEMENT_TEXT_FOR_FEEDBACK));

@ -21,8 +21,6 @@ static_library("lobster") {
"lobster_client_impl.cc",
"lobster_client_impl.h",
"lobster_event_sink.h",
"lobster_feedback.cc",
"lobster_feedback.h",
"lobster_insertion.cc",
"lobster_insertion.h",
"lobster_service.cc",
@ -40,12 +38,10 @@ static_library("lobster") {
"//ash/public/cpp",
"//base",
"//chrome/browser:browser_process",
"//chrome/browser/feedback",
"//chrome/browser/profiles:profile",
"//chrome/browser/resources/chromeos/mako:resources",
"//chrome/browser/ui/webui/ash/lobster:lobster",
"//chromeos/ash/components/specialized_features",
"//components/feedback",
"//chromeos/ash/components/browser_context_helper",
"//components/keyed_service/core",
"//components/manta",
"//components/variations/service",
@ -96,6 +92,7 @@ source_set("unit_tests") {
"//chrome/browser/ash/lobster/mock:test_support",
"//chrome/browser/feedback",
"//chrome/test:test_support",
"//chromeos/ash/components/specialized_features",
"//components/feedback/proto",
"//services/data_decoder/public/cpp:test_support",
"//services/network:network_service",

@ -11,6 +11,7 @@
#include "ash/public/cpp/lobster/lobster_system_state.h"
#include "chrome/browser/ash/lobster/lobster_service.h"
#include "chrome/browser/ash/lobster/lobster_system_state_provider.h"
#include "components/account_id/account_id.h"
LobsterClientImpl::LobsterClientImpl(LobsterService* service)
: service_(service) {}
@ -39,11 +40,6 @@ void LobsterClientImpl::InflateCandidate(
service_->InflateCandidate(seed, query, std::move(callback));
}
bool LobsterClientImpl::SubmitFeedback(std::string description,
const std::string& image_bytes) {
return service_->SubmitFeedback(std::move(description), image_bytes);
}
void LobsterClientImpl::LoadUI(std::optional<std::string> query,
ash::LobsterMode mode,
const gfx::Rect& caret_bounds) {
@ -62,3 +58,7 @@ void LobsterClientImpl::QueueInsertion(const std::string& image_bytes,
StatusCallback insert_status_callback) {
service_->QueueInsertion(image_bytes, std::move(insert_status_callback));
}
const AccountId& LobsterClientImpl::GetAccountId() {
return service_->GetAccountId();
}

@ -33,8 +33,6 @@ class LobsterClientImpl : public ash::LobsterClient {
void InflateCandidate(uint32_t seed,
const std::string& query,
ash::InflateCandidateCallback callback) override;
bool SubmitFeedback(std::string description,
const std::string& image_bytes) override;
void QueueInsertion(const std::string& image_bytes,
StatusCallback insert_status_callback) override;
void LoadUI(std::optional<std::string> query,
@ -42,6 +40,7 @@ class LobsterClientImpl : public ash::LobsterClient {
const gfx::Rect& caret_bounds) override;
void ShowUI() override;
void CloseUI() override;
const AccountId& GetAccountId() override;
private:
// Not owned by this class

@ -1,34 +0,0 @@
// 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 "chrome/browser/ash/lobster/lobster_feedback.h"
#include <string>
#include <string_view>
#include <utility>
#include "chrome/browser/feedback/feedback_uploader_chrome.h" // IWYU pragma: keep - Required for pointer cast.
#include "chrome/browser/feedback/feedback_uploader_factory_chrome.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/specialized_features/feedback.h"
#include "components/feedback/feedback_constants.h"
bool SendLobsterFeedback(Profile* profile,
std::string description,
std::string_view image_bytes) {
// NOTE: `FeedbackUploaderFactoryChrome` (in //chrome/browser/feedback/)
// returns different instances to `FeedbackUploaderFactory` (in
// //components/feedback/content). The correct instance should be obtained
// from `FeedbackUploaderFactoryChrome`.
feedback::FeedbackUploader* uploader =
feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(profile);
if (!uploader) {
return false;
}
specialized_features::SendFeedback(
*uploader, feedback::kLobsterFeedbackProductId, std::move(description),
std::string(image_bytes));
return true;
}

@ -1,19 +0,0 @@
// 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.
#ifndef CHROME_BROWSER_ASH_LOBSTER_LOBSTER_FEEDBACK_H_
#define CHROME_BROWSER_ASH_LOBSTER_LOBSTER_FEEDBACK_H_
#include <string>
#include <string_view>
class Profile;
// Returns true if the feedback is successfully queued for submission.
// Returns false otherwise (e.g. the feedback mechanism is disabled).
bool SendLobsterFeedback(Profile* profile,
std::string description,
std::string_view image_bytes);
#endif // CHROME_BROWSER_ASH_LOBSTER_LOBSTER_FEEDBACK_H_

@ -2,18 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/lobster/lobster_feedback.h"
#include "base/test/gtest_util.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/scoped_chromeos_version_info.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/feedback/feedback_uploader_chrome.h"
#include "chrome/browser/feedback/feedback_uploader_factory_chrome.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/channel_info.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/specialized_features/feedback.h"
#include "components/feedback/feedback_constants.h"
#include "components/feedback/feedback_uploader.h"
#include "components/feedback/proto/extension.pb.h"
@ -25,6 +25,9 @@
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
// Integration tests for `specialized_features::SendFeedback` with real Chrome
// dependencies.
namespace {
class MockUploader : public feedback::FeedbackUploader {
@ -92,13 +95,16 @@ TEST(LobsterFeedback, SendFeedbackDoesNotSendEmail) {
base::test::TestFuture<userfeedback::ExtensionSubmit> on_report_sent_future;
std::unique_ptr<TestingProfile> profile = CreateTestingProfile(
"test@email.com", on_report_sent_future.GetRepeatingCallback());
feedback::FeedbackUploaderChrome* uploader =
feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(
profile.get());
EXPECT_TRUE(
SendLobsterFeedback(profile.get(),
/*description=*/
"visit https://www.whatismyip.com/, log in using "
"test@email.com and try entering 111.222.333.444",
/*image_bytes=*/"a1b2c3"));
specialized_features::SendFeedback(
*uploader, feedback::kLobsterFeedbackProductId,
/*description=*/
"visit https://www.whatismyip.com/, log in using "
"test@email.com and try entering 111.222.333.444",
/*image=*/"a1b2c3");
auto feedback_data =
on_report_sent_future.Get<userfeedback::ExtensionSubmit>();
@ -110,13 +116,16 @@ TEST(LobsterFeedback, SendFeedbackRedactsDescription) {
base::test::TestFuture<userfeedback::ExtensionSubmit> on_report_sent_future;
std::unique_ptr<TestingProfile> profile = CreateTestingProfile(
"test@email.com", on_report_sent_future.GetRepeatingCallback());
feedback::FeedbackUploaderChrome* uploader =
feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(
profile.get());
EXPECT_TRUE(
SendLobsterFeedback(profile.get(),
/*description=*/
"visit https://www.whatismyip.com/ log in using "
"test@email.com and try entering 111.222.333.444",
/*image_bytes=*/"a1b2c3"));
specialized_features::SendFeedback(
*uploader, feedback::kLobsterFeedbackProductId,
/*description=*/
"visit https://www.whatismyip.com/ log in using "
"test@email.com and try entering 111.222.333.444",
/*image=*/"a1b2c3");
auto feedback_data =
on_report_sent_future.Get<userfeedback::ExtensionSubmit>();
@ -132,10 +141,14 @@ TEST(LobsterFeedback, SendFeedbackOnlyContainsNecessaryInformation) {
base::test::TestFuture<userfeedback::ExtensionSubmit> on_report_sent_future;
std::unique_ptr<TestingProfile> profile = CreateTestingProfile(
"test@google.com", on_report_sent_future.GetRepeatingCallback());
feedback::FeedbackUploaderChrome* uploader =
feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(
profile.get());
EXPECT_TRUE(SendLobsterFeedback(profile.get(),
/*description=*/"some dummy description",
/*image_bytes=*/"a1b2c3"));
specialized_features::SendFeedback(*uploader,
feedback::kLobsterFeedbackProductId,
/*description=*/"some dummy description",
/*image=*/"a1b2c3");
auto feedback_data =
on_report_sent_future.Get<userfeedback::ExtensionSubmit>();

@ -9,18 +9,23 @@
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/lobster/lobster_session.h"
#include "base/check_deref.h"
#include "base/command_line.h"
#include "base/hash/sha1.h"
#include "chrome/browser/ash/lobster/image_fetcher.h"
#include "chrome/browser/ash/lobster/lobster_candidate_id_generator.h"
#include "chrome/browser/ash/lobster/lobster_feedback.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/browser_context_helper/annotated_account_id.h"
#include "components/manta/snapper_provider.h"
LobsterService::LobsterService(
std::unique_ptr<manta::SnapperProvider> snapper_provider,
Profile* profile)
: profile_(profile),
// `LobsterService` is only created for regular profiles as specified in
// the `LobsterServiceProvider` constructor, so the below call should
// always return a non-null pointer.
account_id_(CHECK_DEREF(ash::AnnotatedAccountId::Get(profile))),
image_provider_(std::move(snapper_provider)),
image_fetcher_(image_provider_.get(), &candidate_id_generator_),
resizer_(&image_fetcher_),
@ -59,12 +64,6 @@ void LobsterService::QueueInsertion(const std::string& image_bytes,
image_bytes, std::move(insert_status_callback));
}
bool LobsterService::SubmitFeedback(std::string description,
const std::string& image_bytes) {
return SendLobsterFeedback(profile_, std::move(description),
/*image_bytes=*/image_bytes);
}
void LobsterService::LoadUI(std::optional<std::string> query,
ash::LobsterMode mode,
const gfx::Rect& caret_bounds) {

@ -17,6 +17,7 @@
#include "chrome/browser/ash/lobster/lobster_event_sink.h"
#include "chrome/browser/ash/lobster/lobster_insertion.h"
#include "chrome/browser/ash/lobster/lobster_system_state_provider.h"
#include "components/account_id/account_id.h"
#include "components/keyed_service/core/keyed_service.h"
namespace manta {
@ -48,8 +49,6 @@ class LobsterService : public KeyedService, public LobsterEventSink {
void QueueInsertion(const std::string& image_bytes,
StatusCallback insert_status_callback);
bool SubmitFeedback(std::string description, const std::string& image_bytes);
void LoadUI(std::optional<std::string> query,
ash::LobsterMode mode,
const gfx::Rect& caret_bounds);
@ -58,12 +57,15 @@ class LobsterService : public KeyedService, public LobsterEventSink {
void CloseUI();
const AccountId& GetAccountId() const { return account_id_; }
// Relevant input events
void OnFocus(int context_id) override;
private:
// Not owned by this class
raw_ptr<Profile> profile_;
AccountId account_id_;
raw_ptr<ash::LobsterSession> active_session_;
LobsterCandidateIdGenerator candidate_id_generator_;

@ -30,6 +30,7 @@ static_library("shell_delegate") {
"//chrome/browser/ash/multidevice_setup",
"//chrome/browser/ash/profiles",
"//chrome/browser/ash/scanner",
"//chrome/browser/feedback",
"//chrome/browser/profiles:profile",
"//chrome/browser/ui/ash/accelerator",
"//chrome/browser/ui/ash/accessibility",
@ -49,6 +50,7 @@ static_library("shell_delegate") {
"//chrome/common",
"//chromeos/ash/components/audio",
"//chromeos/ash/components/browser_context_helper",
"//chromeos/ash/components/specialized_features",
"//chromeos/ash/services/multidevice_setup",
"//components/ui_devtools",
"//components/user_manager",

@ -24,6 +24,7 @@ include_rules = [
"+chrome/browser/ash/scanner",
"+chrome/browser/browser_process.h",
"+chrome/browser/browser_process_platform_part_ash.h",
"+chrome/browser/feedback",
"+chrome/browser/nearby_sharing",
"+chrome/browser/profiles",
"+chrome/browser/sessions",

@ -5,6 +5,8 @@
#include "chrome/browser/ui/ash/shell_delegate/chrome_shell_delegate.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "ash/accelerators/accelerator_prefs_delegate.h"
@ -42,6 +44,8 @@
#include "chrome/browser/ash/scanner/chrome_scanner_delegate.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part_ash.h"
#include "chrome/browser/feedback/feedback_uploader_chrome.h"
#include "chrome/browser/feedback/feedback_uploader_factory_chrome.h"
#include "chrome/browser/nearby_sharing/nearby_share_delegate_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
@ -78,6 +82,7 @@
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/audio/system_sounds_delegate_impl.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/specialized_features/feedback.h"
#include "chromeos/ash/services/multidevice_setup/multidevice_setup_service.h"
#include "components/ui_devtools/devtools_server.h"
#include "components/user_manager/user_manager.h"
@ -439,6 +444,36 @@ void ChromeShellDelegate::OpenFeedbackDialog(
description_template, category_tag);
}
bool ChromeShellDelegate::SendSpecializedFeatureFeedback(
const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type) {
// NOTE: `FeedbackUploaderFactoryChrome` (in //chrome/browser/feedback/)
// returns different instances to `FeedbackUploaderFactory` (in
// //components/feedback/content). The correct instance should be obtained
// from `FeedbackUploaderFactoryChrome`.
content::BrowserContext* browser_context =
ash::BrowserContextHelper::Get()->GetBrowserContextByAccountId(
account_id);
if (!browser_context) {
return false;
}
feedback::FeedbackUploaderChrome* uploader =
feedback::FeedbackUploaderFactoryChrome::GetForBrowserContext(
browser_context);
if (!uploader) {
return false;
}
specialized_features::SendFeedback(*uploader, product_id,
std::move(description), std::move(image),
std::move(image_mime_type));
return true;
}
void ChromeShellDelegate::OpenProfileManager() {
NOTREACHED();
}

@ -88,6 +88,12 @@ class ChromeShellDelegate : public ash::ShellDelegate {
void OpenFeedbackDialog(ShellDelegate::FeedbackSource source,
const std::string& description_template,
const std::string& category_tag) override;
bool SendSpecializedFeatureFeedback(
const AccountId& account_id,
int product_id,
std::string description,
std::optional<std::string> image,
std::optional<std::string> image_mime_type) override;
void OpenProfileManager() override;
static void SetDisableLoggingRedirectForTesting(bool value);
static void ResetDisableLoggingRedirectForTesting();