// 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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ui/ash/projector/pending_screencast_manager.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/webui/projector_app/projector_app_client.h"
#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
#include "ash/webui/projector_app/test/mock_xhr_sender.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/drivefs_test_support.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/ui/user_adding_screen.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/projector/projector_app_client_impl.h"
#include "chrome/browser/ui/ash/projector/projector_drivefs_provider.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "content/public/test/browser_test.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/abseil-cpp/absl/utility/utility.h"
namespace ash {
namespace {
constexpr char kTestScreencastPath[] = "/root/test_screencast";
constexpr char kTestScreencastName[] = "test_screencast";
constexpr char kTestMediaFile[] = "test_screencast.webm";
constexpr char kTestMetadataFile[] = "test_screencast.projector";
constexpr char kDefaultMetadataFilePath[] =
"/root/test_screencast/test_screencast.projector";
constexpr char kProjectorPendingScreencastBatchIOTaskDurationHistogramName[] =
"Ash.Projector.PendingScreencastBatchIOTaskDuration";
constexpr char kProjectorPendingScreencastChangeIntervalHistogramName[] =
"Ash.Projector.PendingScreencastChangeInterval";
// The test media file is 0.7 mb.
constexpr int64_t kTestMediaFileBytes = 700 * 1024;
// The test metadata file is 0.1 mb.
constexpr int64_t kTestMetadataFileBytes = 100 * 1024;
} // namespace
// Used to keep track of the count of OnScreencastsPendingStatusChanged call in
// PendingScreencastMangerBrowserTest.
class ScreencastsPendingStatusChangedObserver
: public ProjectorAppClient::Observer {
public:
ScreencastsPendingStatusChangedObserver() {
app_client_observation_.Observe(ProjectorAppClient::Get());
}
~ScreencastsPendingStatusChangedObserver() override = default;
int screencast_update_count() const { return screencast_update_count_; }
// ProjectorAppClient::Observer:
void OnScreencastsPendingStatusChanged(
const PendingScreencastContainerSet& screencast_set) override {
screencast_update_count_++;
}
void OnNewScreencastPreconditionChanged(
const NewScreencastPrecondition& precondition) override {}
void OnSodaProgress(int combined_progress) override {}
void OnSodaError() override {}
void OnSodaInstalled() override {}
private:
int screencast_update_count_ = 0;
base::ScopedObservation<ProjectorAppClient, ProjectorAppClient::Observer>
app_client_observation_{this};
};
class PendingScreencastMangerBrowserTest : public InProcessBrowserTest {
public:
PendingScreencastMangerBrowserTest() {
scoped_feature_list_.InitWithFeatures(
{features::kProjectorUpdateIndexableText}, {});
}
PendingScreencastMangerBrowserTest(
const PendingScreencastMangerBrowserTest&) = delete;
PendingScreencastMangerBrowserTest& operator=(
const PendingScreencastMangerBrowserTest&) = delete;
~PendingScreencastMangerBrowserTest() override = default;
bool SetUpUserDataDirectory() override {
return drive::SetUpUserDataDirectoryForDriveFsTest();
}
void SetUpInProcessBrowserTestFixture() override {
create_drive_integration_service_ = base::BindRepeating(
&PendingScreencastMangerBrowserTest::CreateDriveIntegrationService,
base::Unretained(this));
service_factory_for_test_ = std::make_unique<
drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>(
&create_drive_integration_service_);
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
status_waiter_ =
std::make_unique<ScreencastsPendingStatusChangedObserver>();
}
void TearDownOnMainThread() override {
status_waiter_.reset();
InProcessBrowserTest::TearDownOnMainThread();
}
protected:
virtual drive::DriveIntegrationService* CreateDriveIntegrationService(
Profile* profile) {
// Ignore non-user profile.
if (!ProfileHelper::IsUserProfile(profile)) {
return nullptr;
}
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath mount_path = profile->GetPath().Append("drivefs");
fake_drivefs_helper_ =
std::make_unique<drive::FakeDriveFsHelper>(profile, mount_path);
auto* integration_service = new drive::DriveIntegrationService(
profile, std::string(), mount_path,
fake_drivefs_helper_->CreateFakeDriveFsListenerFactory());
return integration_service;
}
drivefs::FakeDriveFs* GetFakeDriveFs() {
return &fake_drivefs_helper_->fake_drivefs();
}
// Creates file under the Drive relative `file_path` whose size is
// `total_bytes`.
void CreateFileInDriveFsFolder(const std::string& file_path,
int64_t total_bytes) {
CreateFileInDriveFsFolder(file_path, std::string(total_bytes, 'a'));
}
// Creates file under the Drive relative `file_path` and write `file_content`
// to the file.
void CreateFileInDriveFsFolder(const std::string& file_path,
const std::string& file_content) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::FilePath relative_file_path(file_path);
base::FilePath folder_path =
GetDriveFsAbsolutePath(relative_file_path.DirName().value());
// base::CreateDirectory returns 'true' on successful creation, or if the
// directory already exists.
EXPECT_TRUE(base::CreateDirectory(folder_path));
base::File file(folder_path.Append(relative_file_path.BaseName()),
base::File::FLAG_CREATE | base::File::FLAG_WRITE);
EXPECT_EQ(static_cast<int>(file_content.size()),
file.Write(/*offset=*/0, file_content.data(),
/*size=*/file_content.size()));
EXPECT_TRUE(file.IsValid());
file.Close();
}
// Create a file for given `file_path`, which is a relative file path of
// drivefs. Write `total_bytes` to this file. Create a drivefs syncing event
// for this file with `transferred_bytes` transferred and add this event to
// `syncing_status`.
void CreateFileAndTransferItemEvent(
const std::string& file_path,
int64_t total_bytes,
int64_t transferred_bytes,
drivefs::mojom::SyncingStatus& syncing_status) {
CreateFileInDriveFsFolder(file_path, total_bytes);
AddTransferItemEvent(syncing_status, file_path, total_bytes,
transferred_bytes);
}
base::Time GetFileCreatedTime(const std::string& relative_file_path) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::File::Info info;
return base::GetFileInfo(GetDriveFsAbsolutePath(relative_file_path), &info)
? info.creation_time
: base::Time();
}
base::FilePath GetDriveFsAbsolutePath(const std::string& relative_path) {
base::ScopedAllowBlockingForTesting allow_blocking;
drive::DriveIntegrationService* service =
drive::DriveIntegrationServiceFactory::FindForProfile(
browser()->profile());
EXPECT_TRUE(service->IsMounted());
EXPECT_TRUE(base::PathExists(service->GetMountPointPath()));
base::FilePath root("/");
base::FilePath absolute_path(service->GetMountPointPath());
root.AppendRelativePath(base::FilePath(relative_path), &absolute_path);
return absolute_path;
}
void AddTransferItemEvent(drivefs::mojom::SyncingStatus& syncing_status,
const std::string& path,
int64_t total_bytes,
int64_t transferred_bytes) {
syncing_status.item_events.emplace_back(
std::in_place, /*stable_id=*/1, /*group_id=*/1, path,
total_bytes == transferred_bytes
? drivefs::mojom::ItemEvent::State::kCompleted
: drivefs::mojom::ItemEvent::State::kInProgress,
/*bytes_transferred=*/transferred_bytes,
/*bytes_to_transfer=*/total_bytes,
drivefs::mojom::ItemEventReason::kTransfer);
}
// Notifies `pending_screencast_manager_` that a file with `file_path` and
// `total_size` with an uploading event and a completed event.
void MockSyncFileCompleted(const std::string& file_path,
const int64_t total_size) {
drivefs::mojom::SyncingStatus syncing_status;
// Notifies with uploading event:
AddTransferItemEvent(syncing_status, file_path,
/*total_bytes=*/0,
/*transferred_bytes=*/total_size);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
syncing_status.item_events.clear();
// Notifies with completed event:
AddTransferItemEvent(syncing_status, file_path,
/*total_bytes=*/total_size,
/*transferred_bytes=*/total_size);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
}
void TestGetFileIdFailed() {
// Sets get file id callback:
base::RunLoop run_loop;
pending_screencast_manager()->SetOnGetFileIdCallbackForTest(
base::BindLambdaForTesting([&](const base::FilePath& local_file_path,
const std::string& file_id) {
EXPECT_EQ(GetDriveFsAbsolutePath(kDefaultMetadataFilePath),
local_file_path);
EXPECT_EQ(std::string(), file_id);
run_loop.Quit();
}));
// Mocks a metadata file finishes upload:
MockSyncFileCompleted(kDefaultMetadataFilePath, kTestMetadataFileBytes);
run_loop.Run();
}
void ExpectEmptyRequestBodyForProjectorFileContent(
const std::string& file_content) {
CreateFileInDriveFsFolder(kDefaultMetadataFilePath, file_content);
drivefs::FakeMetadata metadata;
metadata.path = base::FilePath(kDefaultMetadataFilePath);
metadata.mime_type = "text/plain";
metadata.original_name = kTestMetadataFile;
metadata.doc_id = "abc123";
metadata.alternate_url = "https://drive.google.com/open?id=fileId";
GetFakeDriveFs()->SetMetadata(std::move(metadata));
// Sets get file id callback:
base::RunLoop run_loop;
pending_screencast_manager()->SetOnGetRequestBodyCallbackForTest(
base::BindLambdaForTesting(
[&](const std::string& file_id, const std::string& request_body) {
EXPECT_EQ(std::string(), request_body);
EXPECT_EQ("fileId", file_id);
run_loop.Quit();
}));
// Mocks a metadata file finishes upload:
MockSyncFileCompleted(kDefaultMetadataFilePath, kTestMetadataFileBytes);
run_loop.Run();
}
void VerifyNotificationCount(size_t size) {
base::RunLoop run_loop;
NotificationDisplayServiceFactory::GetForProfile(browser()->profile())
->GetDisplayed(base::BindLambdaForTesting(
[&run_loop, &size](std::set<std::string> displayed_notification_ids,
bool supports_synchronization) {
EXPECT_EQ(size, displayed_notification_ids.size());
run_loop.Quit();
}));
run_loop.Run();
}
// Simulates syncing event by FakeDriveFs delegate.
void SimulateSyncingEvent(
const drivefs::mojom::SyncingStatus& syncing_status) {
auto& drivefs_delegate = GetFakeDriveFs()->delegate();
drivefs_delegate->OnSyncingStatusUpdate(syncing_status.Clone());
drivefs_delegate.FlushForTesting();
}
void WaitForPendingStatusUpdateToBeFinished() {
// Ensures
// PendingScreencastManager::ProcessAndGenerateNewScreencasts finishes on
// blocking task runner.
WaitBlockingTaskRunnerFinish();
// Ensures
// PendingScreencastManager::OnProcessAndGenerateNewScreencastsFinished
// finishes on blocking task runner.
WaitBlockingTaskRunnerFinish();
}
PendingScreencastManager* pending_screencast_manager() {
ProjectorAppClientImpl* app_client =
static_cast<ProjectorAppClientImpl*>(ash::ProjectorAppClient::Get());
return app_client->get_pending_screencast_manager_for_test();
}
ScreencastsPendingStatusChangedObserver* status_waiter() {
return status_waiter_.get();
}
base::HistogramTester histogram_tester_;
base::test::ScopedFeatureList scoped_feature_list_;
private:
void WaitBlockingTaskRunnerFinish() {
base::RunLoop run_loop;
pending_screencast_manager()->blocking_task_runner()->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
drive::DriveIntegrationServiceFactory::FactoryCallback
create_drive_integration_service_;
std::unique_ptr<drive::DriveIntegrationServiceFactory::ScopedFactoryForTest>
service_factory_for_test_;
std::unique_ptr<drive::FakeDriveFsHelper> fake_drivefs_helper_;
std::unique_ptr<ScreencastsPendingStatusChangedObserver> status_waiter_;
};
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest, ValidScreencast) {
const std::string media_file =
base::StrCat({kTestScreencastPath, "/", kTestMediaFile});
drivefs::mojom::SyncingStatus syncing_status;
{
// Create a valid pending screencast.
CreateFileAndTransferItemEvent(media_file,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0, syncing_status);
}
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(1, status_waiter()->screencast_update_count());
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastChangeIntervalHistogramName,
/*count=*/0);
const base::TimeTicks last_pending_screencast_change_tick =
pending_screencast_manager()->last_pending_screencast_change_tick();
EXPECT_NE(base::TimeTicks(), last_pending_screencast_change_tick);
const PendingScreencastContainerSet pending_screencasts =
pending_screencast_manager()->GetPendingScreencasts();
EXPECT_EQ(pending_screencasts.size(), 1u);
ash::PendingScreencastContainer ps = *(pending_screencasts.begin());
EXPECT_EQ(ps.container_dir(), base::FilePath(kTestScreencastPath));
EXPECT_EQ(ps.pending_screencast().name, kTestScreencastName);
EXPECT_EQ(ps.pending_screencast().created_time,
GetFileCreatedTime(media_file)
.InMillisecondsFSinceUnixEpochIgnoringNull());
// Tests PendingScreencastChangeCallback won't be invoked if pending
// screencast status doesn't change.
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(1, status_waiter()->screencast_update_count());
// Expects no report since PendingScreencastChangeCallback wasn't invoked.
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastChangeIntervalHistogramName,
/*count=*/0);
EXPECT_EQ(
last_pending_screencast_change_tick,
pending_screencast_manager()->last_pending_screencast_change_tick());
// Tests PendingScreencastChangeCallback will be invoked if pending
// screencast status changes.
syncing_status.item_events.clear();
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(2, status_waiter()->screencast_update_count());
// Since pending screencast set is empty, the last pending screencast change
// tick is reset to null:
EXPECT_EQ(
base::TimeTicks(),
pending_screencast_manager()->last_pending_screencast_change_tick());
const base::TimeDelta elapsed_time =
base::TimeTicks::Now() - last_pending_screencast_change_tick;
auto change_interval_samples = histogram_tester_.GetAllSamples(
kProjectorPendingScreencastChangeIntervalHistogramName);
// Expects only 1 sample.
EXPECT_EQ(1u, change_interval_samples.size());
// Expects the sample only have 1 count.
EXPECT_EQ(1, change_interval_samples.front().count);
// Since the end of `elapsed_time` is gotten from "base::TimeTicks::Now()"
// after PendingScreencastChangeCallback gets invoked. Expects `elapsed_time`
// is greater than the sample.
EXPECT_GT(elapsed_time.InMicroseconds(), change_interval_samples.front().min);
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/3);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest, InvalidScreencasts) {
const std::string media_only_path = "/root/media_only/example.webm";
const std::string metadata_only_path =
"/root/metadata_only/example.projector";
const std::string avi = "/root/non_screencast_files/example.avi";
const std::string mov = "/root/non_screencast_files/example.mov";
const std::string mp4 = "/root/non_screencast_files/example.mp4";
drivefs::mojom::SyncingStatus syncing_status;
{
// Create an invalid screencast that only has webm medida file.
CreateFileAndTransferItemEvent(media_only_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
// Create an invalid screencast that only has metadata file.
CreateFileAndTransferItemEvent(metadata_only_path,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0, syncing_status);
// Create an invalid screencast that does not have webm media and metadata
// files but have other media files.
CreateFileAndTransferItemEvent(avi, /*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(mov, /*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(mp4, /*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
}
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(0, status_waiter()->screencast_update_count());
EXPECT_TRUE(pending_screencast_manager()->GetPendingScreencasts().empty());
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/1);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
IgnoreCompletedEvent) {
const std::string media_file =
base::StrCat({kTestScreencastPath, "/", kTestMediaFile});
drivefs::mojom::SyncingStatus syncing_status;
{
// Create a valid uploaded screencast.
CreateFileAndTransferItemEvent(media_file,
/*total_bytes=*/kTestMediaFileBytes,
kTestMediaFileBytes, syncing_status);
CreateFileAndTransferItemEvent(kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
kTestMetadataFileBytes, syncing_status);
}
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(0, status_waiter()->screencast_update_count());
EXPECT_TRUE(pending_screencast_manager()->GetPendingScreencasts().empty());
// There is no IO task for complete events.
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/0);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
MultipleValidAndInvalidScreencasts) {
drivefs::mojom::SyncingStatus syncing_status;
size_t num_of_screencasts = 10;
{
// Create multiple valid pending screencasts.
for (size_t i = 0; i < num_of_screencasts; ++i) {
const std::string test_screencast_path =
base::StrCat({kTestScreencastPath, base::NumberToString(i)});
const std::string media =
base::StrCat({test_screencast_path, "/", kTestMediaFile});
const std::string metadata =
base::StrCat({test_screencast_path, "/", kTestMetadataFile});
CreateFileAndTransferItemEvent(media, /*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(metadata,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0, syncing_status);
}
// Tests with a invalid screencast does not have metadata file.
const std::string no_metadata_screencast = "/root/no_metadata/example.webm";
CreateFileAndTransferItemEvent(no_metadata_screencast,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
// Tests with a invalid screencast does not have media file.
const std::string no_media_screencast = "/root/no_media/example.projector";
CreateFileAndTransferItemEvent(no_media_screencast,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
// Tests with a non-screencast file.
const std::string non_screencast = "/root/non_screencast/example.txt";
CreateFileAndTransferItemEvent(non_screencast, /*total_bytes=*/100,
/*transferred_bytes=*/0, syncing_status);
}
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(1, status_waiter()->screencast_update_count());
const PendingScreencastContainerSet pending_screencasts =
pending_screencast_manager()->GetPendingScreencasts();
int64_t total_size = kTestMediaFileBytes + kTestMetadataFileBytes;
// Only valid screencasts could be processed.
EXPECT_EQ(pending_screencasts.size(), num_of_screencasts);
for (size_t i = 0; i < num_of_screencasts; ++i) {
const std::string container_dir =
base::StrCat({kTestScreencastPath, base::NumberToString(i)});
const std::string name =
base::StrCat({kTestScreencastName, base::NumberToString(i)});
ash::PendingScreencastContainer ps{base::FilePath(container_dir), name,
total_size, 0};
EXPECT_TRUE(pending_screencasts.find(ps) != pending_screencasts.end());
}
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/1);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest, UploadProgress) {
const std::string media_file_path =
base::StrCat({kTestScreencastPath, "/", kTestMediaFile});
drivefs::mojom::SyncingStatus syncing_status;
{
// Create a valid pending screencast.
CreateFileAndTransferItemEvent(media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0, syncing_status);
}
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(1, status_waiter()->screencast_update_count());
const PendingScreencastContainerSet pending_screencasts_1 =
pending_screencast_manager()->GetPendingScreencasts();
EXPECT_EQ(pending_screencasts_1.size(), 1u);
ash::PendingScreencastContainer ps = *(pending_screencasts_1.begin());
const int total_size = kTestMediaFileBytes + kTestMetadataFileBytes;
EXPECT_EQ(total_size, ps.total_size_in_bytes());
EXPECT_EQ(0, ps.bytes_transferred());
// Tests the metadata file finished transferred.
// PendingScreencastChangeCallback won't be invoked if the difference is less
// than kPendingScreencastDiffThresholdInBytes.
syncing_status.item_events.clear();
int64_t media_transferred_1_bytes = 1;
int64_t metadata_transferred_bytes = kTestMetadataFileBytes;
AddTransferItemEvent(syncing_status, media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/media_transferred_1_bytes);
// Create a completed transferred event for metadata.
AddTransferItemEvent(syncing_status, kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/metadata_transferred_bytes);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(1, status_waiter()->screencast_update_count());
const PendingScreencastContainerSet pending_screencasts_2 =
pending_screencast_manager()->GetPendingScreencasts();
ps = *(pending_screencasts_2.begin());
// The screencast status unchanged.
EXPECT_EQ(total_size, ps.total_size_in_bytes());
EXPECT_EQ(0, ps.bytes_transferred());
syncing_status.item_events.clear();
AddTransferItemEvent(syncing_status, media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/kTestMediaFileBytes - 1);
// Create a completed transferred event for metadata.
AddTransferItemEvent(syncing_status, kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/metadata_transferred_bytes);
// Tests PendingScreencastChangeCallback will be invoked if the difference of
// transferred bytes is greater than kPendingScreencastDiffThresholdInBytes.
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(2, status_waiter()->screencast_update_count());
const PendingScreencastContainerSet pending_screencasts_3 =
pending_screencast_manager()->GetPendingScreencasts();
ps = *(pending_screencasts_3.begin());
// The screencast status changed.
EXPECT_EQ(total_size, ps.total_size_in_bytes());
// TODO(b/209854146) After fix b/209854146, the `ps.bytes_transferred` is
// `total_size -1`.
EXPECT_EQ(kTestMediaFileBytes - 1, ps.bytes_transferred());
syncing_status.item_events.clear();
// Create completed transferred events for both files.
AddTransferItemEvent(syncing_status, media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/kTestMediaFileBytes);
AddTransferItemEvent(syncing_status, kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/kTestMetadataFileBytes);
// Tests PendingScreencastChangeCallback will be invoked when all files
// finished transferred.
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
EXPECT_EQ(3, status_waiter()->screencast_update_count());
EXPECT_TRUE(pending_screencast_manager()->GetPendingScreencasts().empty());
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/4);
}
// Test the comparison of pending screencast in a std::set.
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
PendingScreencastContainerSet) {
// The `name` and `total_size_in_bytes` of screencast will not be compare in a
// set.
const base::FilePath container_dir_a = base::FilePath("/root/a");
const std::string screencast_a_name = "a";
const int64_t screencast_a_total_bytes = 2 * 1024 * 1024;
ash::PendingScreencastContainer screencast_a_1_byte_transferred{
container_dir_a, screencast_a_name, screencast_a_total_bytes,
/*bytes_transferred=*/1};
ash::PendingScreencastContainer screencast_a_1kb_transferred{
container_dir_a, screencast_a_name, screencast_a_total_bytes,
/*bytes_transferred=*/1024};
ash::PendingScreencastContainer screencast_a_700kb_transferred{
container_dir_a, screencast_a_name, screencast_a_total_bytes,
/*bytes_transferred=*/700 * 1024};
const base::FilePath container_dir_b = base::FilePath("/root/b");
const std::string screencast_b_name = "b";
const int64_t screencast_b_total_bytes = 2 * 1024 * 1024;
ash::PendingScreencastContainer screencast_b_1_byte_transferred{
container_dir_b, screencast_b_name, screencast_b_total_bytes,
/*bytes_transferred=*/1};
ash::PendingScreencastContainer screencast_b_1kb_transferred{
container_dir_b, screencast_b_name, screencast_b_total_bytes,
/*bytes_transferred=*/1024};
ash::PendingScreencastContainer screencast_b_700kb_transferred{
container_dir_b, screencast_b_name, screencast_b_total_bytes,
/*bytes_transferred=*/700 * 1024};
PendingScreencastContainerSet set1{screencast_a_1_byte_transferred,
screencast_b_1_byte_transferred};
PendingScreencastContainerSet set2{screencast_a_1_byte_transferred,
screencast_b_1_byte_transferred};
PendingScreencastContainerSet set3{screencast_a_1kb_transferred,
screencast_b_1_byte_transferred};
PendingScreencastContainerSet set4{screencast_a_700kb_transferred,
screencast_b_1_byte_transferred};
PendingScreencastContainerSet set5{screencast_a_1_byte_transferred,
screencast_a_700kb_transferred};
PendingScreencastContainerSet set6{screencast_a_700kb_transferred,
screencast_a_1_byte_transferred};
PendingScreencastContainerSet set7{screencast_a_1_byte_transferred,
screencast_a_1kb_transferred};
EXPECT_EQ(set1, set2);
EXPECT_EQ(set1, set3);
EXPECT_NE(set1, set4);
EXPECT_NE(set1, set5);
EXPECT_EQ(set5, set6);
EXPECT_EQ(2u, set5.size());
EXPECT_EQ(2u, set7.size());
}
// Test a screencast failed to upload will remain a "fail to upload" error state
// until it get successfully uploaded.
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
DriveOutOfSpaceError) {
const std::string media_file_path =
base::StrCat({kTestScreencastPath, "/", kTestMediaFile});
drivefs::mojom::SyncingStatus syncing_status;
// Create a valid pending screencast.
CreateFileAndTransferItemEvent(media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0, syncing_status);
CreateFileAndTransferItemEvent(kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0, syncing_status);
// Mock DriveFs sends an out of space error for media file.
drivefs::mojom::DriveError error{
drivefs::mojom::DriveError::Type::kCantUploadStorageFull,
base::FilePath(media_file_path)};
pending_screencast_manager()->OnError(error);
// Even there's DriveError, DriveFs will keep trying to sync both metadata and
// media file.
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
// Verify we have a fail status screencast.
const PendingScreencastContainerSet pending_screencasts =
pending_screencast_manager()->GetPendingScreencasts();
EXPECT_EQ(1u, pending_screencasts.size());
ash::PendingScreencastContainer ps = *(pending_screencasts.begin());
EXPECT_TRUE(ps.pending_screencast().upload_failed);
// Mock both metadata and media file get uploaded.
syncing_status.item_events.clear();
// Create completed transferred events for both files.
AddTransferItemEvent(syncing_status, media_file_path,
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/kTestMediaFileBytes);
AddTransferItemEvent(syncing_status, kDefaultMetadataFilePath,
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/kTestMetadataFileBytes);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
// Expect the screencast get removed from pending screencasts set .
EXPECT_TRUE(pending_screencast_manager()->GetPendingScreencasts().empty());
histogram_tester_.ExpectTotalCount(
kProjectorPendingScreencastBatchIOTaskDurationHistogramName,
/*count=*/2);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
UpdateIndexableTextSuccess) {
// Prepares a ".projector" file and it's metadata:
const std::string kProjectorFileContent =
R"({
"captionLanguage": "en",
"captions": [
{"endOffset": 400, "startOffset": 200, "editState": 1},
{"endOffset": 1260, "hypothesisParts": [], "startOffset": 760,
"text": "metadata file."},
{"endOffset": 2300, "hypothesisParts": [], "startOffset": 2000,
"text": "another sentence."}
],
"tableOfContent":[]})";
CreateFileInDriveFsFolder(kDefaultMetadataFilePath, kProjectorFileContent);
drivefs::FakeMetadata metadata;
metadata.path = base::FilePath(kDefaultMetadataFilePath);
metadata.mime_type = "text/plain";
metadata.original_name = kTestMetadataFile;
metadata.doc_id = "abc123";
metadata.alternate_url = "https://drive.google.com/open?id=fileId";
GetFakeDriveFs()->SetMetadata(std::move(metadata));
// Sets get file id callback:
base::RunLoop run_loop;
network::TestURLLoaderFactory test_url_loader_factory;
pending_screencast_manager()->SetProjectorXhrSenderForTest(
std::make_unique<MockXhrSender>(
base::BindLambdaForTesting(
[&](const GURL& url, projector::mojom::RequestType method,
const std::optional<std::string>& request_body) {
EXPECT_EQ(
"{\"contentHints\":{\"indexableText\":\" metadata file. "
"another sentence.\"}}",
*request_body);
EXPECT_EQ(projector::mojom::RequestType::kPatch, method);
EXPECT_EQ(
GURL("https://www.googleapis.com/drive/v3/files/fileId"),
url);
run_loop.Quit();
}),
&test_url_loader_factory));
// Mocks a metadata file finishes upload:
MockSyncFileCompleted(kDefaultMetadataFilePath, kTestMetadataFileBytes);
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
UpdateIndexableTextFailByEmptyFileId) {
// Does not create ".projector", which leads to drive::FILE_ERROR_NOT_FOUND.
TestGetFileIdFailed();
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
UpdateIndexableTextFailByEmptyAlternateUrl) {
CreateFileInDriveFsFolder(kDefaultMetadataFilePath, kTestMetadataFileBytes);
// Sets empty alternate url in metadata, which could happen when metadata is
// not fully populated.
drivefs::FakeMetadata metadata;
metadata.path = base::FilePath(kDefaultMetadataFilePath);
metadata.mime_type = "text/plain";
metadata.original_name = kTestMetadataFile;
metadata.doc_id = "abc123";
GetFakeDriveFs()->SetMetadata(std::move(metadata));
TestGetFileIdFailed();
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
UpdateIndexableTextFailByInCorrectAlternateUrl) {
CreateFileInDriveFsFolder(kDefaultMetadataFilePath, kTestMetadataFileBytes);
// Sets incorrect alternate url in metadata.
drivefs::FakeMetadata metadata;
metadata.path = base::FilePath(kDefaultMetadataFilePath);
metadata.mime_type = "text/plain";
metadata.original_name = kTestMetadataFile;
metadata.doc_id = "abc123";
metadata.alternate_url = "alternate_url";
GetFakeDriveFs()->SetMetadata(std::move(metadata));
TestGetFileIdFailed();
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
MalformedProjectorFileNoCaption) {
// Prepares a ".projector" file with no captions.
const std::string kProjectorFileContentNoCaption =
"{\"captionLanguage\":\"en\",\"tableOfContent\":[]}";
ExpectEmptyRequestBodyForProjectorFileContent(kProjectorFileContentNoCaption);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
MalformedProjectorFileNotJson) {
// Prepares a ".projector" file with no captions.
const std::string kProjectorFileContentNotJson =
"{\"captionLanguage\":\"en\",\"captions\":[{\"endOffset\":1260,"
"\"hypothesisParts\":[],\"startOffset\":760,\"text\":\"metadata "
"file.\"}],\"tableOfContent\":[]";
ExpectEmptyRequestBodyForProjectorFileContent(kProjectorFileContentNotJson);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
ProjectorFileEmptyCaption) {
// Prepares a ".projector" file and it's metadata:
const std::string kProjectorFileContentEmptyCaption =
"{\"captionLanguage\":\"en\",\"captions\":[],\"tableOfContent\":[]}";
ExpectEmptyRequestBodyForProjectorFileContent(
kProjectorFileContentEmptyCaption);
}
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerBrowserTest,
SuppressDriveNotification) {
auto* app_client = ProjectorAppClient::Get();
app_client->NotifyAppUIActive(true);
base::FilePath container_folder = base::FilePath(kTestScreencastPath);
const base::FilePath media_file = container_folder.Append(kTestMediaFile);
const base::FilePath metadata_file =
container_folder.Append(kTestMetadataFile);
const base::FilePath thumbnail =
container_folder.Append(kScreencastDefaultThumbnailFileName);
const base::FilePath drivefs_mounted_point =
ProjectorDriveFsProvider::GetDriveFsMountPointPath();
app_client->ToggleFileSyncingNotificationForPaths(
{GetDriveFsAbsolutePath(media_file.value()),
GetDriveFsAbsolutePath(metadata_file.value()),
GetDriveFsAbsolutePath(thumbnail.value())},
true);
drivefs::mojom::SyncingStatus syncing_status;
AddTransferItemEvent(syncing_status, media_file.value(),
/*total_bytes=*/kTestMediaFileBytes,
/*transferred_bytes=*/0);
AddTransferItemEvent(syncing_status, metadata_file.value(),
/*total_bytes=*/kTestMetadataFileBytes,
/*transferred_bytes=*/0);
AddTransferItemEvent(syncing_status, thumbnail.value(),
/*total_bytes=*/300,
/*transferred_bytes=*/0);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
VerifyNotificationCount(0);
// When app is closed, the notification shows up:
app_client->NotifyAppUIActive(false);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
VerifyNotificationCount(0);
// When app is open, the notification gets suppressed again:
app_client->NotifyAppUIActive(true);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
VerifyNotificationCount(0);
// While app is open, the Drive notification shows up for non-projector file.
CreateFileAndTransferItemEvent("unrelated file",
/*total_bytes=*/100,
/*transferred_bytes=*/0, syncing_status);
SimulateSyncingEvent(syncing_status);
WaitForPendingStatusUpdateToBeFinished();
VerifyNotificationCount(0);
}
class PendingScreencastMangerMultiProfileTest : public LoginManagerTest {
public:
PendingScreencastMangerMultiProfileTest() : LoginManagerTest() {
login_mixin_.AppendRegularUsers(2);
account_id1_ = login_mixin_.users()[0].account_id;
account_id2_ = login_mixin_.users()[1].account_id;
}
void SetUpOnMainThread() override {
LoginManagerTest::SetUpOnMainThread();
pending_screencast_manager_ =
std::make_unique<PendingScreencastManager>(base::BindLambdaForTesting(
[&](const PendingScreencastContainerSet& set) {
base::DoNothing();
}));
}
void TearDownOnMainThread() override {
pending_screencast_manager_.reset();
LoginManagerTest::TearDownOnMainThread();
}
protected:
AccountId account_id1_;
AccountId account_id2_;
ash::LoginManagerMixin login_mixin_{&mixin_host_};
std::unique_ptr<PendingScreencastManager> pending_screencast_manager_;
};
IN_PROC_BROWSER_TEST_F(PendingScreencastMangerMultiProfileTest,
SwitchActiveUser) {
LoginUser(account_id1_);
// Verify DriveFsHost observation is observing user 1's DriveFsHost.
Profile* profile1 = ProfileHelper::Get()->GetProfileByAccountId(account_id1_);
drive::DriveIntegrationService* service_for_account1 =
drive::DriveIntegrationServiceFactory::FindForProfile(profile1);
EXPECT_EQ(pending_screencast_manager_->GetHost(),
service_for_account1->GetDriveFsHost());
// Add user 2.
ash::UserAddingScreen::Get()->Start();
AddUser(account_id2_);
// Verify DriveFsHost observation is observing user 2's DriveFsHost.
Profile* profile2 = ProfileHelper::Get()->GetProfileByAccountId(account_id2_);
drive::DriveIntegrationService* service_for_account2 =
drive::DriveIntegrationServiceFactory::FindForProfile(profile2);
EXPECT_EQ(pending_screencast_manager_->GetHost(),
service_for_account2->GetDriveFsHost());
// Switch back to user1.
user_manager::UserManager::Get()->SwitchActiveUser(account_id1_);
// Verify DriveFsHost observation is observing user 1's DriveFsHost.
EXPECT_EQ(pending_screencast_manager_->GetHost(),
service_for_account1->GetDriveFsHost());
}
} // namespace ash