blob: 7d377cc0a12cb8e00cee8f3d6eadbd7e8b219f32 [file] [log] [blame]
// Copyright 2021 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/projector/projector_controller_impl.h"
#include <initializer_list>
#include <memory>
#include <string>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/projector/model/projector_session_impl.h"
#include "ash/projector/projector_metadata_controller.h"
#include "ash/projector/projector_metrics.h"
#include "ash/projector/test/mock_projector_metadata_controller.h"
#include "ash/projector/test/mock_projector_ui_controller.h"
#include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
#include "ash/public/cpp/projector/speech_recognition_availability.h"
#include "ash/public/cpp/test/mock_projector_client.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/safe_base_name.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "chromeos/ash/components/dbus/audio/audio_node.h"
#include "chromeos/ash/components/dbus/audio/fake_cras_audio_client.h"
#include "media/mojo/mojom/speech_recognition_result.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image_unittest_util.h"
namespace ash {
namespace {
using testing::_;
using testing::ElementsAre;
struct AudioNodeInfo {
bool is_input;
uint64_t id;
const char* const device_name;
const char* const type;
const char* const name;
};
constexpr char kProjectorCreationFlowHistogramName[] =
"Ash.Projector.CreationFlow.ClamshellMode";
constexpr char kProjectorTranscriptsCountHistogramName[] =
"Ash.Projector.TranscriptsCount.ClamshellMode";
constexpr char kSpeechRecognitionEndStateOnDevice[] =
"Ash.Projector.SpeechRecognitionEndState.OnDevice";
constexpr char kSpeechRecognitionEndStateServerBased[] =
"Ash.Projector.SpeechRecognitionEndState.ServerBased";
constexpr char kMetadataFileName[] = "MyScreencast";
constexpr char kProjectorExtension[] = "projector";
constexpr char kProjectorV2Extension[] = "screencast";
void NotifyControllerForFinalSpeechResult(ProjectorControllerImpl* controller) {
media::SpeechRecognitionResult result;
result.transcription = "transcript text 1";
result.is_final = true;
result.timing_information = media::TimingInformation();
result.timing_information->audio_start_time = base::Milliseconds(0);
result.timing_information->audio_end_time = base::Milliseconds(3000);
std::vector<media::HypothesisParts> hypothesis_parts;
std::string hypothesis_text[3] = {"transcript", "text", "1"};
int hypothesis_time[3] = {1000, 2000, 2500};
for (int i = 0; i < 3; i++) {
hypothesis_parts.emplace_back(
std::vector<std::string>({hypothesis_text[i]}),
base::Milliseconds(hypothesis_time[i]));
}
result.timing_information->hypothesis_parts = std::move(hypothesis_parts);
controller->OnTranscription(result);
}
void NotifyControllerForPartialSpeechResult(
ProjectorControllerImpl* controller) {
controller->OnTranscription(
media::SpeechRecognitionResult("transcript partial text 1", false));
}
class ProjectorMetadataControllerForTest : public ProjectorMetadataController {
public:
ProjectorMetadataControllerForTest() = default;
ProjectorMetadataControllerForTest(
const ProjectorMetadataControllerForTest&) = delete;
ProjectorMetadataControllerForTest& operator=(
const ProjectorMetadataControllerForTest&) = delete;
~ProjectorMetadataControllerForTest() override = default;
void SetRunLoopQuitClosure(base::RepeatingClosure closure) {
quit_closure_ = base::BindOnce(closure);
}
protected:
// ProjectorMetadataController:
void OnSaveFileResult(const base::FilePath& path,
size_t transcripts_count,
bool success) override {
ProjectorMetadataController::OnSaveFileResult(path, transcripts_count,
success);
std::move(quit_closure_).Run();
}
private:
base::OnceClosure quit_closure_;
};
} // namespace
class ProjectorControllerTest : public AshTestBase {
public:
ProjectorControllerTest()
: AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
ProjectorControllerTest(const ProjectorControllerTest&) = delete;
ProjectorControllerTest& operator=(const ProjectorControllerTest&) = delete;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
controller_ =
static_cast<ProjectorControllerImpl*>(ProjectorController::Get());
auto mock_ui_controller =
std::make_unique<MockProjectorUiController>(controller_);
mock_ui_controller_ = mock_ui_controller.get();
controller_->SetProjectorUiControllerForTest(std::move(mock_ui_controller));
auto mock_metadata_controller =
std::make_unique<MockProjectorMetadataController>();
mock_metadata_controller_ = mock_metadata_controller.get();
controller_->SetProjectorMetadataControllerForTest(
std::move(mock_metadata_controller));
SpeechRecognitionAvailability availability;
availability.on_device_availability =
OnDeviceRecognitionAvailability::kAvailable;
ON_CALL(mock_client_, GetSpeechRecognitionAvailability)
.WillByDefault(testing::Return(availability));
controller_->SetClient(&mock_client_);
}
void InitializeRealMetadataController() {
std::unique_ptr<ProjectorMetadataController> metadata_controller =
std::make_unique<ProjectorMetadataControllerForTest>();
metadata_controller_ = static_cast<ProjectorMetadataControllerForTest*>(
metadata_controller.get());
controller_->SetProjectorMetadataControllerForTest(
std::move(metadata_controller));
}
protected:
void InitFakeMic(bool mic_present) {
if (!mic_present) {
CrasAudioHandler::Get()->SetActiveInputNodes({});
return;
}
const AudioNodeInfo kInternalMic[] = {
{true, 55555, "Fake Mic", "INTERNAL_MIC", "Internal Mic"}};
const AudioNode audio_node =
AudioNode(kInternalMic->is_input, kInternalMic->id,
/*has_v2_stable_device_id=*/false, kInternalMic->id,
/*stable_device_id_v2=*/0, kInternalMic->device_name,
kInternalMic->type, kInternalMic->name, /*active=*/false,
/*plugged_time=*/0, /*max_supported_channels=*/1,
/*audio_effect=*/1, /*number_of_volume_steps=*/25);
FakeCrasAudioClient::Get()->SetAudioNodesForTesting({audio_node});
CrasAudioHandler::Get()->SetActiveInputNodes({kInternalMic->id});
}
raw_ptr<MockProjectorUiController, DanglingUntriaged> mock_ui_controller_ =
nullptr;
raw_ptr<MockProjectorMetadataController, DanglingUntriaged>
mock_metadata_controller_ = nullptr;
raw_ptr<ProjectorMetadataControllerForTest, DanglingUntriaged>
metadata_controller_;
raw_ptr<ProjectorControllerImpl, DanglingUntriaged> controller_;
MockProjectorClient mock_client_;
base::HistogramTester histogram_tester_;
base::ScopedTempDir temp_dir_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(ProjectorControllerTest, OnTranscription) {
// Verify that |RecordTranscription| in |ProjectorMetadataController| is
// called to record the transcript.
EXPECT_CALL(*mock_metadata_controller_, RecordTranscription(_)).Times(1);
NotifyControllerForFinalSpeechResult(controller_);
}
TEST_F(ProjectorControllerTest, OnTranscriptionPartialResult) {
// Verify that |RecordTranscription| in |ProjectorMetadataController| is not
// called since it is not a final result.
EXPECT_CALL(*mock_metadata_controller_, RecordTranscription(_)).Times(0);
NotifyControllerForPartialSpeechResult(controller_);
}
TEST_F(ProjectorControllerTest, OnAudioNodesChanged) {
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(true));
InitFakeMic(/*mic_present=*/true);
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kEnabled,
{NewScreencastPreconditionReason::kEnabledBySoda})));
controller_->OnAudioNodesChanged();
InitFakeMic(/*mic_present=*/false);
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kDisabled,
{NewScreencastPreconditionReason::kNoMic})));
controller_->OnAudioNodesChanged();
}
TEST_F(ProjectorControllerTest, OnSpeechRecognitionAvailabilityChanged) {
SpeechRecognitionAvailability availability;
// Soda is not available.
availability.use_on_device = true;
availability.on_device_availability =
OnDeviceRecognitionAvailability::kSodaNotAvailable;
ON_CALL(mock_client_, GetSpeechRecognitionAvailability)
.WillByDefault(testing::Return(availability));
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(true));
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kDisabled,
{NewScreencastPreconditionReason::
kOnDeviceSpeechRecognitionNotSupported})));
controller_->OnSpeechRecognitionAvailabilityChanged();
// Soda is available.
availability.on_device_availability =
OnDeviceRecognitionAvailability::kAvailable;
ON_CALL(mock_client_, GetSpeechRecognitionAvailability)
.WillByDefault(testing::Return(availability));
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kEnabled,
{NewScreencastPreconditionReason::kEnabledBySoda})));
controller_->OnSpeechRecognitionAvailabilityChanged();
// Server based available.
availability.use_on_device = false;
availability.server_based_availability =
ServerBasedRecognitionAvailability::kAvailable;
ON_CALL(mock_client_, GetSpeechRecognitionAvailability)
.WillByDefault(testing::Return(availability));
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kEnabled,
{NewScreencastPreconditionReason::
kEnabledByServerSideSpeechRecognition})));
controller_->OnSpeechRecognitionAvailabilityChanged();
}
TEST_F(ProjectorControllerTest, EnableAnnotatorTool) {
// Verify that |OnMarkerPressed| in |ProjectorUiController| is called.
EXPECT_CALL(*mock_ui_controller_, EnableAnnotatorTool());
controller_->EnableAnnotatorTool();
}
TEST_F(ProjectorControllerTest, SetAnnotatorTool) {
AnnotatorTool tool;
// Verify that |SetAnnotatorTool| in |ProjectorUiController| is called.
EXPECT_CALL(*mock_ui_controller_, SetAnnotatorTool(tool));
controller_->SetAnnotatorTool(tool);
}
TEST_F(ProjectorControllerTest, RecordingStarted) {
EXPECT_CALL(mock_client_, StartSpeechRecognition());
EXPECT_CALL(*mock_metadata_controller_, OnRecordingStarted());
auto* root = Shell::GetPrimaryRootWindow();
EXPECT_CALL(*mock_ui_controller_, ShowAnnotationTray(root)).Times(1);
controller_->projector_session()->Start(
base::SafeBaseName::Create("projector_data").value());
histogram_tester_.ExpectUniqueSample(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kSessionStarted,
/*expected_bucket_count=*/1);
controller_->OnRecordingStarted(root);
histogram_tester_.ExpectBucketCount(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kRecordingStarted,
/*expected_count=*/1);
}
TEST_F(ProjectorControllerTest, RecordingEnded) {
base::FilePath screencast_container_path;
ASSERT_TRUE(mock_client_.GetBaseStoragePath(&screencast_container_path));
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(true));
// Verify that |HideAnnotationTray| in |ProjectorUiController| is
// called.
EXPECT_CALL(*mock_ui_controller_, HideAnnotationTray()).Times(1);
EXPECT_CALL(mock_client_, OpenProjectorApp()).Times(0);
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kDisabled,
{NewScreencastPreconditionReason::kInProjectorSession})));
controller_->projector_session()->Start(
base::SafeBaseName::Create("projector_data").value());
histogram_tester_.ExpectUniqueSample(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kSessionStarted,
/*expected_bucket_count=*/1);
controller_->OnRecordingStarted(Shell::GetPrimaryRootWindow());
histogram_tester_.ExpectBucketCount(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kRecordingStarted,
/*expected_count=*/1);
base::RunLoop runLoop;
controller_->CreateScreencastContainerFolder(base::BindLambdaForTesting(
[&](const base::FilePath& screencast_file_path_no_extension) {
// Expects screencast files name equals to it's parent folder name:
EXPECT_EQ(screencast_file_path_no_extension.BaseName(),
screencast_file_path_no_extension.DirName().BaseName());
EXPECT_CALL(
mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kEnabled,
{NewScreencastPreconditionReason::kEnabledBySoda})))
.Times(0);
EXPECT_CALL(mock_client_, StopSpeechRecognition())
.WillOnce(testing::Invoke([&]() {
controller_->OnSpeechRecognitionStopped(/*forced=*/false);
}));
EXPECT_CALL(*mock_metadata_controller_, SaveMetadata(_)).Times(0);
controller_->OnRecordingEnded();
runLoop.Quit();
}));
runLoop.Run();
histogram_tester_.ExpectBucketCount(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kRecordingEnded, /*expected_count=*/1);
histogram_tester_.ExpectTotalCount(kProjectorCreationFlowHistogramName,
/*expected_count=*/3);
}
enum class RecognitionEndLatency {
// The speech recognition has ended even before recording
// has wrapped up dlp check.
kImmediate,
// The speech recognition ends after recording has wrapped up dlp check
// but fore the restricted timeout.
kDelayed,
// The speech recognition doesn't end and it causes a time out.
kDelayedCausingTimeout
};
class ProjectorOnDlpRestrictionCheckedAtVideoEndTest
: public ::testing::WithParamInterface<
::testing::tuple<RecognitionEndLatency, bool>>,
public ProjectorControllerTest {
public:
ProjectorOnDlpRestrictionCheckedAtVideoEndTest() = default;
ProjectorOnDlpRestrictionCheckedAtVideoEndTest(
const ProjectorOnDlpRestrictionCheckedAtVideoEndTest&) = delete;
ProjectorOnDlpRestrictionCheckedAtVideoEndTest& operator=(
const ProjectorOnDlpRestrictionCheckedAtVideoEndTest&) = delete;
~ProjectorOnDlpRestrictionCheckedAtVideoEndTest() override = default;
};
TEST_P(ProjectorOnDlpRestrictionCheckedAtVideoEndTest, WrapUpRecordingOnce) {
// TODO(b/321064048): Clean up tests when ProjectorV2 is fully launched.
scoped_feature_list_.InitAndDisableFeature(ash::features::kProjectorV2);
bool wrap_up_by_speech_stopped;
bool transcript_end_timed_out;
switch (std::get<0>(GetParam())) {
case RecognitionEndLatency::kImmediate:
wrap_up_by_speech_stopped = false;
transcript_end_timed_out = false;
break;
case RecognitionEndLatency::kDelayed:
wrap_up_by_speech_stopped = true;
transcript_end_timed_out = false;
break;
case RecognitionEndLatency::kDelayedCausingTimeout:
wrap_up_by_speech_stopped = true;
transcript_end_timed_out = true;
break;
}
bool user_deleted_video_file = std::get<1>(GetParam());
base::FilePath screencast_container_path;
ASSERT_TRUE(mock_client_.GetBaseStoragePath(&screencast_container_path));
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(true));
EXPECT_CALL(mock_client_, OpenProjectorApp());
EXPECT_CALL(mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kDisabled,
{NewScreencastPreconditionReason::kInProjectorSession})));
// Advance clock to 20:02:10 Jan 2nd, 2021.
base::Time start_time;
EXPECT_TRUE(base::Time::FromString("2 Jan 2021 20:02:10", &start_time));
base::TimeDelta forward_by = start_time - base::Time::Now();
task_environment()->AdvanceClock(forward_by);
controller_->projector_session()->Start(
base::SafeBaseName::Create("projector_data").value());
histogram_tester_.ExpectUniqueSample(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kSessionStarted,
/*expected_bucket_count=*/1);
controller_->OnRecordingStarted(Shell::GetPrimaryRootWindow());
histogram_tester_.ExpectBucketCount(
kProjectorCreationFlowHistogramName,
/*sample=*/ProjectorCreationFlow::kRecordingStarted,
/*expected_count=*/1);
base::RunLoop runLoop;
controller_->CreateScreencastContainerFolder(base::BindLambdaForTesting(
[&](const base::FilePath& screencast_file_path_no_extension) {
EXPECT_CALL(
mock_client_,
OnNewScreencastPreconditionChanged(NewScreencastPrecondition(
NewScreencastPreconditionState::kEnabled,
{NewScreencastPreconditionReason::kEnabledBySoda})));
const std::string expected_screencast_name =
"Screencast 2021-01-02 20.02.10";
const base::FilePath expected_path =
screencast_container_path.Append("root")
.Append("projector_data")
// Screencast container folder.
.Append(expected_screencast_name)
// Screencast file name without extension.
.Append(expected_screencast_name);
controller_->OnRecordingEnded();
if (!user_deleted_video_file) {
// Verify that |SaveMetadata| in |ProjectorMetadataController| is
// called with the expected path.
EXPECT_EQ(screencast_file_path_no_extension, expected_path);
// Verify that save metadata only triggered once. The path will not
// change as the clock advances.
task_environment()->AdvanceClock(base::Minutes(1));
int expected_count = wrap_up_by_speech_stopped ? 2 : 1;
EXPECT_CALL(*mock_metadata_controller_, SaveMetadata(expected_path))
.Times(expected_count);
// Verify that thumbnail file is saved.
controller_->SetOnFileSavedCallbackForTest(base::BindLambdaForTesting(
[&](const base::FilePath& path, bool success) {
EXPECT_TRUE(success);
EXPECT_TRUE(base::PathExists(path));
}));
} else {
// Verify that save metadata is not triggered.
EXPECT_CALL(*mock_metadata_controller_, SaveMetadata(_)).Times(0);
// Expects notification gets resumed if recording deleted.
const std::vector<base::FilePath> screencast_files = {
expected_path.AddExtension(kProjectorMetadataFileExtension),
expected_path.AddExtension(kProjectorMediaFileExtension),
expected_path.DirName().Append(
kScreencastDefaultThumbnailFileName)};
EXPECT_CALL(mock_client_, ToggleFileSyncingNotificationForPaths(
screencast_files, /*suppress=*/false));
// Verify that Projector Folder is cleaned up.
controller_->SetOnPathDeletedCallbackForTest(
base::BindLambdaForTesting(
[&](const base::FilePath& path, bool success) {
EXPECT_TRUE(success);
EXPECT_FALSE(base::PathExists(path));
}));
}
auto image = gfx::test::CreateImageSkia(10, 10);
if (wrap_up_by_speech_stopped) {
controller_->OnVideoFileFinalized(
/*user_deleted_video_file=*/user_deleted_video_file,
/*thumbnail=*/image);
if (!transcript_end_timed_out) {
controller_->OnSpeechRecognitionStopped(/*forced=*/false);
} else {
EXPECT_CALL(mock_client_, ForceEndSpeechRecognition())
.Times(1)
.WillOnce(testing::Invoke([&]() {
controller_->OnSpeechRecognitionStopped(/*forced=*/true);
}));
// Simulate that the timer has fired.
EXPECT_TRUE(controller_->get_timer_for_testing()->IsRunning());
controller_->get_timer_for_testing()->FireNow();
}
} else {
controller_->OnSpeechRecognitionStopped(/*forced=*/false);
controller_->OnVideoFileFinalized(
/*user_deleted_video_file=*/user_deleted_video_file,
/*thumbnail=*/image);
}
runLoop.Quit();
}));
runLoop.Run();
histogram_tester_.ExpectTotalCount(kProjectorCreationFlowHistogramName,
/*expected_count=*/4);
}
INSTANTIATE_TEST_SUITE_P(
WrapUpRecordingOnce,
ProjectorOnDlpRestrictionCheckedAtVideoEndTest,
::testing::Combine(
::testing::ValuesIn({RecognitionEndLatency::kImmediate,
RecognitionEndLatency::kDelayed,
RecognitionEndLatency::kDelayedCausingTimeout}),
::testing::Bool()));
TEST_F(ProjectorControllerTest, NoTranscriptsTest) {
// TODO(b/321064048): Clean up tests when ProjectorV2 is fully launched.
scoped_feature_list_.InitAndDisableFeature(ash::features::kProjectorV2);
InitializeRealMetadataController();
metadata_controller_->OnRecordingStarted();
base::RunLoop run_loop;
metadata_controller_->SetRunLoopQuitClosure(run_loop.QuitClosure());
// Simulate ending the recording and saving the metadata file.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath metadata_file(temp_dir_.GetPath().Append(kMetadataFileName));
metadata_controller_->SaveMetadata(metadata_file);
run_loop.Run();
histogram_tester_.ExpectUniqueSample(kProjectorTranscriptsCountHistogramName,
/*sample=*/0, /*count=*/1);
// Verify the written metadata file size is between 0-100 bytes. Change this
// limit as needed if you make significant changes to the metadata file.
base::File file(metadata_file.AddExtension(kProjectorExtension),
base::File::FLAG_OPEN | base::File::FLAG_READ);
EXPECT_GT(file.GetLength(), 0);
EXPECT_LT(file.GetLength(), 100);
}
TEST_F(ProjectorControllerTest, TranscriptsTest) {
// TODO(b/321064048): Clean up tests when ProjectorV2 is fully launched.
scoped_feature_list_.InitAndDisableFeature(ash::features::kProjectorV2);
InitializeRealMetadataController();
metadata_controller_->OnRecordingStarted();
base::RunLoop run_loop;
metadata_controller_->SetRunLoopQuitClosure(run_loop.QuitClosure());
// Simulate adding some transcripts.
NotifyControllerForFinalSpeechResult(controller_);
NotifyControllerForFinalSpeechResult(controller_);
// Simulate ending the recording and saving the metadata file.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath metadata_file(temp_dir_.GetPath().Append(kMetadataFileName));
metadata_controller_->SaveMetadata(metadata_file);
run_loop.Run();
histogram_tester_.ExpectUniqueSample(kProjectorTranscriptsCountHistogramName,
/*sample=*/2, /*count=*/1);
// Verify the written metadata file size is between 400-500 bytes. This file
// should be larger than the one in the NoTranscriptsTest above. Change this
// limit as needed if you make significant changes to the metadata file.
base::File file(metadata_file.AddExtension(kProjectorExtension),
base::File::FLAG_OPEN | base::File::FLAG_READ);
EXPECT_GT(file.GetLength(), 400);
EXPECT_LT(file.GetLength(), 500);
}
TEST_F(ProjectorControllerTest, V2TranscriptsTest) {
InitializeRealMetadataController();
metadata_controller_->OnRecordingStarted();
base::RunLoop run_loop;
metadata_controller_->SetRunLoopQuitClosure(run_loop.QuitClosure());
// Simulate adding some transcripts.
NotifyControllerForFinalSpeechResult(controller_);
NotifyControllerForFinalSpeechResult(controller_);
// Simulate ending the recording and saving the metadata file.
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath metadata_file(temp_dir_.GetPath().Append(kMetadataFileName));
metadata_controller_->SaveMetadata(metadata_file);
run_loop.Run();
histogram_tester_.ExpectUniqueSample(kProjectorTranscriptsCountHistogramName,
/*sample=*/2, /*count=*/1);
// Verify the written metadata file size is between 400-500 bytes. This file
// should be larger than the one in the NoTranscriptsTest above. Change this
// limit as needed if you make significant changes to the metadata file.
base::File file(metadata_file.AddExtension(kProjectorV2Extension),
base::File::FLAG_OPEN | base::File::FLAG_READ);
EXPECT_GT(file.GetLength(), 400);
EXPECT_LT(file.GetLength(), 500);
}
TEST_F(ProjectorControllerTest, OnDriveMountFailed) {
ON_CALL(mock_client_, IsDriveFsMountFailed())
.WillByDefault(testing::Return(true));
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(false));
EXPECT_EQ(NewScreencastPrecondition(
NewScreencastPreconditionState::kDisabled,
{NewScreencastPreconditionReason::kDriveFsMountFailed}),
controller_->GetNewScreencastPrecondition());
}
TEST_F(ProjectorControllerTest, SuppressDriveNotification) {
// TODO(b/321064048): Clean up tests when ProjectorV2 is fully launched.
scoped_feature_list_.InitAndDisableFeature(ash::features::kProjectorV2);
ON_CALL(mock_client_, IsDriveFsMounted())
.WillByDefault(testing::Return(true));
base::FilePath mounted_path;
ASSERT_TRUE(mock_client_.GetBaseStoragePath(&mounted_path));
// The screencast name, which is used to form the screencast folder/files
// paths, is generated on projector session starts
auto* projector_session = controller_->projector_session();
projector_session->Start(
base::SafeBaseName::Create("projector_data").value());
const base::FilePath expect_container_path =
mounted_path.Append("root")
.Append(projector_session->storage_dir())
.Append(projector_session->screencast_name());
const base::FilePath expected_path_with_no_extension =
expect_container_path.Append(projector_session->screencast_name());
const std::vector<base::FilePath> screencast_files = {
expected_path_with_no_extension.AddExtension(
kProjectorMetadataFileExtension),
expected_path_with_no_extension.AddExtension(
kProjectorMediaFileExtension),
expect_container_path.Append(kScreencastDefaultThumbnailFileName)};
// Expects notification gets suppressed when creating screencast folder.
EXPECT_CALL(mock_client_, ToggleFileSyncingNotificationForPaths(
screencast_files, /*suppress=*/true))
.Times(1);
base::RunLoop run_loop;
controller_->CreateScreencastContainerFolder(base::BindLambdaForTesting(
[&](const base::FilePath& screencast_file_path_no_extension) {
EXPECT_EQ(expected_path_with_no_extension,
screencast_file_path_no_extension);
// Expects notification gets resumed if recording is aborted.
EXPECT_CALL(mock_client_, ToggleFileSyncingNotificationForPaths(
screencast_files, /*suppress=*/false))
.Times(1);
// Simulates starting abort called by capture mode.
controller_->OnRecordingStartAborted();
run_loop.Quit();
}));
run_loop.Run();
}
// Used to test SpeechRecognitionEndState metric for both on-device and
// server based speech recognition.
class ProjectorSpeechRecognitionEndTest
: public ::testing::WithParamInterface<bool>,
public ProjectorControllerTest {
public:
ProjectorSpeechRecognitionEndTest() = default;
ProjectorSpeechRecognitionEndTest(const ProjectorSpeechRecognitionEndTest&) =
delete;
ProjectorSpeechRecognitionEndTest& operator=(
const ProjectorSpeechRecognitionEndTest&) = delete;
~ProjectorSpeechRecognitionEndTest() override = default;
};
TEST_P(ProjectorSpeechRecognitionEndTest, SpeechRecognitionEndMetric) {
SpeechRecognitionAvailability availability;
availability.on_device_availability =
OnDeviceRecognitionAvailability::kAvailable;
availability.server_based_availability =
ServerBasedRecognitionAvailability::kAvailable;
availability.use_on_device = GetParam();
ON_CALL(mock_client_, GetSpeechRecognitionAvailability)
.WillByDefault(testing::Return(availability));
const std::string histogram_name =
availability.use_on_device ? kSpeechRecognitionEndStateOnDevice
: kSpeechRecognitionEndStateServerBased;
auto* projector_session = controller_->projector_session();
projector_session->Start(
base::SafeBaseName::Create("projector_data").value());
auto* root = Shell::GetPrimaryRootWindow();
// Tests speech recognition encountering an error during session.
controller_->OnRecordingStarted(root);
controller_->OnTranscriptionError();
histogram_tester_.ExpectBucketCount(
histogram_name,
SpeechRecognitionEndState::kSpeechRecognitionEnounteredError,
/*expected_count=*/1);
// Tests speech recognition successfully stopping.
ON_CALL(mock_client_, StopSpeechRecognition)
.WillByDefault(testing::Invoke([&]() {
controller_->OnSpeechRecognitionStopped(/*forced=*/false);
}));
controller_->OnRecordingStarted(root);
controller_->OnRecordingEnded();
histogram_tester_.ExpectBucketCount(
histogram_name,
SpeechRecognitionEndState::kSpeechRecognitionSuccessfullyStopped,
/*expected_count=*/1);
// Tests speech recognition forced stopped.
ON_CALL(mock_client_, StopSpeechRecognition).WillByDefault(testing::Return());
EXPECT_CALL(mock_client_, ForceEndSpeechRecognition())
.Times(1)
.WillOnce(testing::Invoke(
[&]() { controller_->OnSpeechRecognitionStopped(/*forced=*/true); }));
controller_->OnRecordingStarted(root);
controller_->OnRecordingEnded();
controller_->get_timer_for_testing()->FireNow();
histogram_tester_.ExpectBucketCount(
histogram_name,
SpeechRecognitionEndState::kSpeechRecognitionForcedStopped,
/*expected_count=*/1);
// Tests speech recognition encountering error while stopping.
controller_->OnRecordingStarted(root);
controller_->OnRecordingEnded();
controller_->OnTranscriptionError();
histogram_tester_.ExpectBucketCount(
histogram_name,
SpeechRecognitionEndState::
kSpeechRecognitionEncounteredErrorWhileStopping,
/*expected_count=*/1);
}
INSTANTIATE_TEST_SUITE_P(SpeechRecognitionEndMetric,
ProjectorSpeechRecognitionEndTest,
/*use_on_device=*/::testing::Bool());
} // namespace ash