// Copyright 2017 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/ash/fileapi/recent_arc_media_source.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "ash/components/arc/mojom/file_system.mojom.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_file_system_instance.h"
#include "base/functional/bind.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/time/time.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_file_system_mounter.h"
#include "chrome/browser/ash/arc/fileapi/arc_file_system_operation_runner.h"
#include "chrome/browser/ash/arc/fileapi/arc_media_view_util.h"
#include "chrome/browser/ash/fileapi/recent_file.h"
#include "chrome/browser/ash/fileapi/recent_source.h"
#include "chrome/browser/ash/fileapi/test/recent_file_matcher.h"
#include "chrome/test/base/testing_profile.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash {
namespace {
std::unique_ptr<KeyedService> CreateFileSystemOperationRunnerForTesting(
content::BrowserContext* context) {
return arc::ArcFileSystemOperationRunner::CreateForTesting(
context, arc::ArcServiceManager::Get()->arc_bridge_service());
}
base::FilePath GetPathForRoot(const std::string& root,
const std::string& name) {
return arc::GetDocumentsProviderMountPath(
arc::kMediaDocumentsProviderAuthority, root)
.Append(name);
}
base::FilePath GetDocumentPath(const std::string& name) {
return GetPathForRoot(arc::kDocumentsRootId, name);
}
base::FilePath GetImagePath(const std::string& name) {
return GetPathForRoot(arc::kImagesRootId, name);
}
base::FilePath GetVideoPath(const std::string& name) {
return GetPathForRoot(arc::kVideosRootId, name);
}
base::Time ModifiedTime(int64_t millis) {
return base::Time::FromMillisecondsSinceUnixEpoch(millis);
}
arc::FakeFileSystemInstance::Document MakeDocument(
const std::string& document_id,
const std::string& parent_document_id,
const std::string& display_name,
const std::string& mime_type,
const base::Time& last_modified) {
return arc::FakeFileSystemInstance::Document(
arc::kMediaDocumentsProviderAuthority, // authority
document_id, // document_id
parent_document_id, // parent_document_id
display_name, // display_name
mime_type, // mime_type
0, // size
last_modified.InMillisecondsSinceUnixEpoch()); // last_modified
}
} // namespace
class RecentArcMediaSourceTest : public testing::Test {
public:
RecentArcMediaSourceTest() = default;
void SetUp() override {
arc_service_manager_ = std::make_unique<arc::ArcServiceManager>();
profile_ = std::make_unique<TestingProfile>();
arc_service_manager_->set_browser_context(profile_.get());
runner_ = static_cast<arc::ArcFileSystemOperationRunner*>(
arc::ArcFileSystemOperationRunner::GetFactory()
->SetTestingFactoryAndUse(
profile_.get(),
base::BindRepeating(
&CreateFileSystemOperationRunnerForTesting)));
// Mount ARC file systems.
arc::ArcFileSystemMounter::GetForBrowserContext(profile_.get());
// Add documents to FakeFileSystemInstance. Note that they are not available
// until EnableFakeFileSystemInstance() is called.
AddDocumentsToFakeFileSystemInstance();
}
void TearDown() override {
arc_service_manager_->arc_bridge_service()->file_system()->CloseInstance(
&fake_file_system_);
arc_service_manager_->set_browser_context(nullptr);
}
protected:
void AddDocumentsToFakeFileSystemInstance() {
auto images_root_doc =
MakeDocument(arc::kImagesRootId, "", "", arc::kAndroidDirectoryMimeType,
base::Time::FromMillisecondsSinceUnixEpoch(1));
auto cat_doc = MakeDocument("cat", arc::kImagesRootId, "cat.png",
"image/png", ModifiedTime(2));
auto dog_doc = MakeDocument("dog", arc::kImagesRootId, "dog.jpg",
"image/jpeg", ModifiedTime(3));
auto fox_doc = MakeDocument("fox", arc::kImagesRootId, "fox.gif",
"image/gif", ModifiedTime(4));
auto elk_doc = MakeDocument("elk", arc::kImagesRootId, "elk.tiff",
"image/tiff", ModifiedTime(5));
auto audio_root_doc =
MakeDocument(arc::kAudioRootId, "", "", arc::kAndroidDirectoryMimeType,
ModifiedTime(1));
auto god_doc = MakeDocument("god", arc::kAudioRootId, "god.mp3",
"audio/mp3", ModifiedTime(6));
auto videos_root_doc =
MakeDocument(arc::kVideosRootId, "", "", arc::kAndroidDirectoryMimeType,
ModifiedTime(1));
auto hot_doc = MakeDocument("hot", arc::kVideosRootId, "hot.mp4",
"video/mp4", ModifiedTime(7));
auto ink_doc = MakeDocument("ink", arc::kVideosRootId, "ink.webm",
"video/webm", ModifiedTime(8));
auto documents_root_doc =
MakeDocument(arc::kDocumentsRootId, "", "",
arc::kAndroidDirectoryMimeType, ModifiedTime(1));
auto word_doc = MakeDocument("word", arc::kDocumentsRootId, "word.doc",
"application/msword", ModifiedTime(9));
auto text_doc = MakeDocument("text", arc::kDocumentsRootId, "text.txt",
"text/plain", ModifiedTime(10));
fake_file_system_.AddDocument(images_root_doc);
fake_file_system_.AddDocument(cat_doc);
fake_file_system_.AddDocument(dog_doc);
fake_file_system_.AddDocument(fox_doc);
fake_file_system_.AddDocument(audio_root_doc);
fake_file_system_.AddDocument(god_doc);
fake_file_system_.AddDocument(videos_root_doc);
fake_file_system_.AddDocument(hot_doc);
fake_file_system_.AddDocument(ink_doc);
fake_file_system_.AddDocument(documents_root_doc);
fake_file_system_.AddDocument(word_doc);
fake_file_system_.AddDocument(text_doc);
fake_file_system_.AddRecentDocument(arc::kImagesRootId, images_root_doc);
fake_file_system_.AddRecentDocument(arc::kImagesRootId, cat_doc);
fake_file_system_.AddRecentDocument(arc::kImagesRootId, dog_doc);
fake_file_system_.AddRecentDocument(arc::kImagesRootId, elk_doc);
fake_file_system_.AddRecentDocument(arc::kAudioRootId, audio_root_doc);
fake_file_system_.AddRecentDocument(arc::kAudioRootId, god_doc);
fake_file_system_.AddRecentDocument(arc::kVideosRootId, videos_root_doc);
fake_file_system_.AddRecentDocument(arc::kVideosRootId, hot_doc);
fake_file_system_.AddRecentDocument(arc::kVideosRootId, ink_doc);
fake_file_system_.AddRecentDocument(arc::kDocumentsRootId,
documents_root_doc);
fake_file_system_.AddRecentDocument(arc::kDocumentsRootId, word_doc);
fake_file_system_.AddRecentDocument(arc::kDocumentsRootId, text_doc);
}
void EnableFakeFileSystemInstance() {
arc_service_manager_->arc_bridge_service()->file_system()->SetInstance(
&fake_file_system_);
arc::WaitForInstanceReady(
arc_service_manager_->arc_bridge_service()->file_system());
}
// Fetches files matching the given query and specified `file_type` from
// the `source_`. If the `root_lag` has value it can be used to cause one
// of the roots (documents, images, videos) to experience an artificial lag.
// If the lag is specified, it must be greater than 100ms.
std::vector<RecentFile> GetRecentFiles(
RecentArcMediaSource* source,
const std::string& query,
RecentSource::FileType file_type = RecentSource::FileType::kAll,
std::optional<std::pair<const char*, base::TimeDelta>> root_lag = {},
size_t max_files = 10) {
std::vector<RecentFile> files;
base::RunLoop run_loop;
base::OneShotTimer timer;
const int32_t call_id = 0;
if (root_lag.has_value()) {
auto [root_id, lag] = root_lag.value();
base::TimeDelta stop_delta = lag - base::Milliseconds(100);
source->SetLagForTesting(lag);
EXPECT_TRUE(stop_delta.is_positive());
timer.Start(FROM_HERE, stop_delta, base::BindLambdaForTesting([&]() {
files = source->Stop(call_id);
run_loop.Quit();
}));
}
source->GetRecentFiles(
RecentSource::Params(
/*file_system_context=*/nullptr, call_id,
/*origin=*/GURL(), query,
/*max_files=*/max_files,
/*cutoff_time=*/base::Time(),
/*end_time=*/base::TimeTicks::Max(),
/*file_type=*/file_type),
base::BindOnce(
[](base::RunLoop* run_loop, std::vector<RecentFile>* out_files,
std::vector<RecentFile> files) {
*out_files = std::move(files);
run_loop->Quit();
},
&run_loop, &files));
run_loop.Run();
return files;
}
void EnableDefer() { runner_->SetShouldDefer(true); }
// NOTE: In local run real time was used. However, that causes the test to run
// for over 6s. With MOCK_TIME, this is reduced to 3s with no difference to
// test outcomes (UmaStats taking the longest).
content::BrowserTaskEnvironment task_environment_{
content::BrowserTaskEnvironment::REAL_IO_THREAD,
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
arc::FakeFileSystemInstance fake_file_system_;
// Use the same initialization/destruction order as
// `ChromeBrowserMainPartsAsh`.
std::unique_ptr<arc::ArcServiceManager> arc_service_manager_;
std::unique_ptr<TestingProfile> profile_;
raw_ptr<arc::ArcFileSystemOperationRunner> runner_;
};
TEST_F(RecentArcMediaSourceTest, Normal) {
EnableFakeFileSystemInstance();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> doc_files = GetRecentFiles(doc_source.get(), "");
ASSERT_EQ(2u, doc_files.size());
EXPECT_THAT(doc_files[0],
IsRecentFile(GetDocumentPath("text.txt"), ModifiedTime(10)));
EXPECT_THAT(doc_files[1],
IsRecentFile(GetDocumentPath("word.doc"), ModifiedTime(9)));
auto video_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kVideosRootId);
std::vector<RecentFile> video_files = GetRecentFiles(video_source.get(), "");
ASSERT_EQ(2u, video_files.size());
EXPECT_THAT(video_files[0],
IsRecentFile(GetVideoPath("ink.webm"), ModifiedTime(8)));
EXPECT_THAT(video_files[1],
IsRecentFile(GetVideoPath("hot.mp4"), ModifiedTime(7)));
auto image_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kImagesRootId);
std::vector<RecentFile> image_files = GetRecentFiles(image_source.get(), "");
EXPECT_THAT(image_files[0],
IsRecentFile(GetImagePath("dog.jpg"), ModifiedTime(3)));
EXPECT_THAT(image_files[1],
IsRecentFile(GetImagePath("cat.png"), ModifiedTime(2)));
doc_files = GetRecentFiles(doc_source.get(), "text");
ASSERT_EQ(1u, doc_files.size());
EXPECT_THAT(doc_files[0],
IsRecentFile(GetDocumentPath("text.txt"), ModifiedTime(10)));
ASSERT_EQ(0u, GetRecentFiles(video_source.get(), "text").size());
ASSERT_EQ(0u, GetRecentFiles(image_source.get(), "text").size());
}
TEST_F(RecentArcMediaSourceTest, ArcNotAvailable) {
// By not enabling fake file system instance we make Arc unavailable.
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> files = GetRecentFiles(doc_source.get(), "");
EXPECT_EQ(0u, files.size());
files = GetRecentFiles(doc_source.get(), "hot");
EXPECT_EQ(0u, files.size());
}
TEST_F(RecentArcMediaSourceTest, Deferred) {
EnableFakeFileSystemInstance();
EnableDefer();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> files = GetRecentFiles(doc_source.get(), "");
EXPECT_EQ(0u, files.size());
files = GetRecentFiles(doc_source.get(), "word");
EXPECT_EQ(0u, files.size());
}
TEST_F(RecentArcMediaSourceTest, GetAudioFiles) {
EnableFakeFileSystemInstance();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> files =
GetRecentFiles(doc_source.get(), "", RecentSource::FileType::kAudio);
// Query for recently-modified audio files should be ignored, since
// MediaDocumentsProvider doesn't support queryRecentDocuments for audio.
ASSERT_EQ(0u, files.size());
}
TEST_F(RecentArcMediaSourceTest, GetImageFiles) {
EnableFakeFileSystemInstance();
auto image_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kImagesRootId);
std::vector<RecentFile> files =
GetRecentFiles(image_source.get(), "", RecentSource::FileType::kImage);
ASSERT_EQ(2u, files.size());
EXPECT_THAT(files[0], IsRecentFile(GetImagePath("dog.jpg"), ModifiedTime(3)));
EXPECT_THAT(files[1], IsRecentFile(GetImagePath("cat.png"), ModifiedTime(2)));
}
TEST_F(RecentArcMediaSourceTest, GetVideoFiles) {
EnableFakeFileSystemInstance();
auto video_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kVideosRootId);
std::vector<RecentFile> files =
GetRecentFiles(video_source.get(), "", RecentSource::FileType::kVideo);
ASSERT_EQ(2u, files.size());
EXPECT_THAT(files[0],
IsRecentFile(GetVideoPath("ink.webm"), ModifiedTime(8)));
EXPECT_THAT(files[1], IsRecentFile(GetVideoPath("hot.mp4"), ModifiedTime(7)));
}
TEST_F(RecentArcMediaSourceTest, GetDocumentFiles) {
EnableFakeFileSystemInstance();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> files =
GetRecentFiles(doc_source.get(), "", RecentSource::FileType::kDocument);
ASSERT_EQ(2u, files.size());
EXPECT_THAT(files[0],
IsRecentFile(GetDocumentPath("text.txt"), ModifiedTime(10)));
EXPECT_THAT(files[1],
IsRecentFile(GetDocumentPath("word.doc"), ModifiedTime(9)));
files = GetRecentFiles(doc_source.get(), "word",
RecentSource::FileType::kDocument);
ASSERT_EQ(1u, files.size());
EXPECT_THAT(files[0],
IsRecentFile(GetDocumentPath("word.doc"), ModifiedTime(9)));
files = GetRecentFiles(doc_source.get(), "no-match",
RecentSource::FileType::kDocument);
ASSERT_EQ(0u, files.size());
}
TEST_F(RecentArcMediaSourceTest, LaggyDocuments) {
EnableFakeFileSystemInstance();
// Find all recent files containing 'd' in their name.
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> files_no_lag = GetRecentFiles(doc_source.get(), "d");
ASSERT_EQ(1u, files_no_lag.size());
EXPECT_THAT(files_no_lag[0],
IsRecentFile(GetDocumentPath("word.doc"), ModifiedTime(9)));
// Now search again; but with an artificial lag for documents. Expect that
// word.doc is no longer found.
std::vector<RecentFile> files = GetRecentFiles(
doc_source.get(), "d", RecentSource::FileType::kAll,
std::make_pair(arc::kDocumentsRootId, base::Milliseconds(500)));
ASSERT_EQ(0u, files.size());
}
TEST_F(RecentArcMediaSourceTest, OverlappingLaggySearches) {
EnableFakeFileSystemInstance();
// The number of times laggy, overlapping search is repeated.
constexpr int32_t reps = 10;
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
std::vector<RecentFile> results[reps];
base::OneShotTimer timers[reps];
// Prepare timers; timers are stopping searches at 250ms + 100ms * call_id.
// Whenever a source is stopped, the code just collects its partial results
// for later analysis.
for (int32_t call_id = 0; call_id < reps; ++call_id) {
base::TimeDelta stop_delta = base::Milliseconds(250 + 100 * call_id);
base::TimeDelta lag_delta = base::Milliseconds(500 + 100 * call_id);
doc_source->SetLagForTesting(lag_delta);
timers[call_id].Start(
FROM_HERE, stop_delta,
base::BindOnce(
[](const int32_t my_call_id, std::vector<RecentFile>* out_files,
RecentArcMediaSource* source) {
*out_files = source->Stop(my_call_id);
},
call_id, &results[call_id], doc_source.get()));
doc_source->GetRecentFiles(RecentSource::Params(
/*file_system_context=*/nullptr,
/*call_id=*/call_id,
/*origin=*/GURL(),
/*query=*/"d",
/*max_files=*/10,
/*cutoff_time=*/base::Time(),
/*end_time=*/base::TimeTicks::Max(),
/*file_type=*/RecentSource::FileType::kAll),
base::BindOnce(
[](std::vector<RecentFile>* out_files,
std::vector<RecentFile> files) {
*out_files = std::move(files);
},
&results[call_id]));
}
// Last call; wait for the results; these results are not interrupted and take
// longer than any of the request requested above.
doc_source->SetLagForTesting(base::Milliseconds(500 + 100 * reps));
base::RunLoop run_loop;
std::vector<RecentFile> final_result;
doc_source->GetRecentFiles(
RecentSource::Params(
/*file_system_context=*/nullptr,
/*call_id=*/reps,
/*origin=*/GURL(),
/*query=*/"d",
/*max_files=*/10,
/*cutoff_time=*/base::Time(),
/*end_time=*/base::TimeTicks::Max(),
/*file_type=*/RecentSource::FileType::kAll),
base::BindOnce(
[](base::RunLoop* run_loop, std::vector<RecentFile>* out_files,
std::vector<RecentFile> files) {
*out_files = std::move(files);
run_loop->Quit();
},
&run_loop, &final_result));
run_loop.Run();
ASSERT_EQ(1u, final_result.size());
EXPECT_THAT(final_result[0],
IsRecentFile(GetDocumentPath("word.doc"), ModifiedTime(9)));
for (int32_t call_id = 0; call_id < reps; ++call_id) {
ASSERT_EQ(0u, results[call_id].size());
}
}
TEST_F(RecentArcMediaSourceTest, UmaStats) {
EnableFakeFileSystemInstance();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
base::HistogramTester histogram_tester;
GetRecentFiles(doc_source.get(), "");
histogram_tester.ExpectTotalCount(RecentArcMediaSource::kLoadHistogramName,
1);
}
TEST_F(RecentArcMediaSourceTest, UmaStats_Deferred) {
EnableFakeFileSystemInstance();
EnableDefer();
auto doc_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kDocumentsRootId);
base::HistogramTester histogram_tester;
GetRecentFiles(doc_source.get(), "");
histogram_tester.ExpectTotalCount(RecentArcMediaSource::kLoadHistogramName,
0);
}
TEST_F(RecentArcMediaSourceTest, MaxFiles) {
EnableFakeFileSystemInstance();
// Maximum one image can be returned per query, regardless of matched numbers.
auto image_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kImagesRootId);
std::vector<RecentFile> files = GetRecentFiles(
image_source.get(), "", RecentSource::FileType::kImage, {}, 1);
ASSERT_EQ(1u, files.size());
EXPECT_THAT(files[0], IsRecentFile(GetImagePath("dog.jpg"), ModifiedTime(3)));
}
TEST_F(RecentArcMediaSourceTest, CallStopLate) {
EnableFakeFileSystemInstance();
// Maximum one image can be returned per query, relardless of matched numbers.
auto image_source = std::make_unique<RecentArcMediaSource>(
profile_.get(), arc::kImagesRootId);
std::vector<RecentFile> files =
GetRecentFiles(image_source.get(), "", RecentSource::FileType::kImage);
ASSERT_EQ(2u, files.size());
EXPECT_THAT(files[0], IsRecentFile(GetImagePath("dog.jpg"), ModifiedTime(3)));
EXPECT_THAT(files[1], IsRecentFile(GetImagePath("cat.png"), ModifiedTime(2)));
std::vector<RecentFile> stop_files = image_source->Stop(0);
ASSERT_EQ(0u, stop_files.size());
}
} // namespace ash