#include "chrome/browser/ash/fileapi/recent_arc_media_source.h"
#include <algorithm>
#include <iterator>
#include <utility>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_root.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_root_map.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_media_view_util.h"
#include "chrome/browser/ash/fileapi/recent_file.h"
#include "chrome/common/extensions/api/file_manager_private.h"
#include "content/public/browser/browser_thread.h"
#include "storage/browser/file_system/external_mount_points.h"
using content::BrowserThread;
namespace ash {
namespace {
namespace fmp = extensions::api::file_manager_private;
const char kAndroidDownloadDirPrefix[] = "/storage/emulated/0/Download/";
const char kAndroidMyFilesDirPrefix[] =
"/storage/0000000000000000000000000000CAFEF00D2019/";
base::FilePath GetRelativeMountPath(const std::string& root_id) {
base::FilePath mount_path = arc::GetDocumentsProviderMountPath(
arc::kMediaDocumentsProviderAuthority, root_id);
base::FilePath relative_mount_path;
base::FilePath(arc::kDocumentsProviderMountPointPath)
.AppendRelativePath(mount_path, &relative_mount_path);
return relative_mount_path;
}
bool IsInsideDownloadsOrMyFiles(const std::string& path) {
if (base::StartsWith(path, kAndroidDownloadDirPrefix,
base::CompareCase::SENSITIVE)) {
return true;
}
if (base::StartsWith(path, kAndroidMyFilesDirPrefix,
base::CompareCase::SENSITIVE)) {
return true;
}
return false;
}
std::vector<RecentFile> ExtractFoundFiles(
const std::map<std::string, std::optional<RecentFile>>&
document_id_to_file) {
std::vector<RecentFile> files;
for (const auto& entry : document_id_to_file) {
const std::optional<RecentFile>& file = entry.second;
if (file.has_value()) {
files.emplace_back(file.value());
}
}
return files;
}
std::vector<RecentFile> PrepareResponse(std::vector<RecentFile>&& files,
size_t max_files) {
std::sort(files.begin(), files.end(), RecentFileComparator());
if (files.size() > max_files) {
files.resize(max_files);
}
return files;
}
}
RecentArcMediaSource::CallContext::CallContext(const Params& params,
GetRecentFilesCallback callback)
: params(params),
callback(std::move(callback)),
build_start_time(base::TimeTicks::Now()) {}
RecentArcMediaSource::CallContext::CallContext(CallContext&& context)
: params(context.params),
callback(std::move(context.callback)),
build_start_time(std::move(context.build_start_time)) {}
RecentArcMediaSource::CallContext::~CallContext() = default;
const char RecentArcMediaSource::kLoadHistogramName[] =
"FileBrowser.Recent.LoadArcMedia";
RecentArcMediaSource::RecentArcMediaSource(Profile* profile,
const std::string& root_id)
: RecentSource(fmp::VolumeType::kMediaView),
profile_(profile),
root_id_(root_id),
relative_mount_path_(GetRelativeMountPath(root_id)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
RecentArcMediaSource::~RecentArcMediaSource() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
bool RecentArcMediaSource::MatchesFileType(FileType file_type) const {
switch (file_type) {
case FileType::kAll:
return true;
case FileType::kImage:
return root_id_ == arc::kImagesRootId;
case FileType::kVideo:
return root_id_ == arc::kVideosRootId;
case FileType::kDocument:
return root_id_ == arc::kDocumentsRootId;
case FileType::kAudio:
return root_id_ == arc::kAudioRootId;
default:
LOG(FATAL) << "Unhandled file_type: " << static_cast<int>(file_type);
}
}
void RecentArcMediaSource::GetRecentFiles(const Params& params,
GetRecentFilesCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(context_map_.Lookup(params.call_id()) == nullptr);
if (!WillArcFileSystemOperationsRunImmediately()) {
std::move(callback).Run({});
return;
}
auto context = std::make_unique<CallContext>(params, std::move(callback));
context_map_.AddWithID(std::move(context), params.call_id());
if (!MatchesFileType(params.file_type())) {
OnComplete(params.call_id());
return;
}
auto* runner =
arc::ArcFileSystemOperationRunner::GetForBrowserContext(profile_);
if (!runner) {
OnComplete(params.call_id());
return;
}
runner->GetRecentDocuments(
arc::kMediaDocumentsProviderAuthority, root_id_,
base::BindOnce(&RecentArcMediaSource::OnRunnerDone,
weak_ptr_factory_.GetWeakPtr(), params.call_id()));
}
void RecentArcMediaSource::OnRunnerDone(
const int32_t call_id,
std::optional<std::vector<arc::mojom::DocumentPtr>> maybe_documents) {
if (!lag_.is_positive()) {
OnGotRecentDocuments(call_id, std::move(maybe_documents));
return;
}
if (!timer_) {
timer_ = std::make_unique<base::OneShotTimer>();
}
timer_->Start(FROM_HERE, lag_,
base::BindOnce(&RecentArcMediaSource::OnGotRecentDocuments,
weak_ptr_factory_.GetWeakPtr(), call_id,
std::move(maybe_documents)));
}
void RecentArcMediaSource::OnGotRecentDocuments(
const int32_t call_id,
std::optional<std::vector<arc::mojom::DocumentPtr>> maybe_documents) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CallContext* context = context_map_.Lookup(call_id);
if (context == nullptr) {
return;
}
if (maybe_documents.has_value()) {
const std::u16string q16 = base::UTF8ToUTF16(context->params.query());
for (const auto& document : maybe_documents.value()) {
if (document->android_file_system_path.has_value() &&
IsInsideDownloadsOrMyFiles(
document->android_file_system_path.value())) {
continue;
}
if (!FileNameMatches(base::UTF8ToUTF16(document->display_name), q16)) {
continue;
}
context->document_id_to_file.emplace(document->document_id, std::nullopt);
}
}
if (context->document_id_to_file.empty()) {
OnComplete(call_id);
return;
}
ScanDirectory(call_id, base::FilePath());
}
void RecentArcMediaSource::ScanDirectory(const int32_t call_id,
const base::FilePath& path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CallContext* context = context_map_.Lookup(call_id);
if (context == nullptr) {
return;
}
++context->num_inflight_readdirs;
auto* root_map =
arc::ArcDocumentsProviderRootMap::GetForBrowserContext(profile_);
if (!root_map) {
LOG(ERROR) << "ArcDocumentsProviderRootMap is not available";
OnDirectoryRead(call_id, path, base::File::FILE_ERROR_FAILED, {});
return;
}
auto* root =
root_map->Lookup(arc::kMediaDocumentsProviderAuthority, root_id_);
if (!root) {
LOG(ERROR) << "ArcDocumentsProviderRoot is missing";
OnDirectoryRead(call_id, path, base::File::FILE_ERROR_NOT_FOUND, {});
return;
}
root->ReadDirectory(
path, base::BindOnce(&RecentArcMediaSource::OnDirectoryRead,
weak_ptr_factory_.GetWeakPtr(), call_id, path));
}
void RecentArcMediaSource::OnDirectoryRead(
const int32_t call_id,
const base::FilePath& path,
base::File::Error result,
std::vector<arc::ArcDocumentsProviderRoot::ThinFileInfo> files) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CallContext* context = context_map_.Lookup(call_id);
if (context == nullptr) {
return;
}
for (const auto& file : files) {
base::FilePath subpath = path.Append(file.name);
if (file.is_directory) {
if (!context->params.IsLate()) {
ScanDirectory(call_id, subpath);
}
continue;
}
auto doc_it = context->document_id_to_file.find(file.document_id);
if (doc_it == context->document_id_to_file.end()) {
continue;
}
auto url = BuildDocumentsProviderUrl(context->params, subpath);
std::optional<RecentFile>& entry = doc_it->second;
if (!entry.has_value() ||
storage::FileSystemURL::Comparator()(url, entry.value().url())) {
entry = RecentFile(url, file.last_modified);
}
}
--context->num_inflight_readdirs;
DCHECK_LE(0, context->num_inflight_readdirs);
if (context->num_inflight_readdirs == 0) {
OnComplete(call_id);
}
}
std::vector<RecentFile> RecentArcMediaSource::Stop(const int32_t call_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CallContext* context = context_map_.Lookup(call_id);
if (context == nullptr) {
return {};
}
size_t max_files = context->params.max_files();
context->callback.Reset();
std::vector<RecentFile> files =
ExtractFoundFiles(context->document_id_to_file);
context_map_.Remove(call_id);
return PrepareResponse(std::move(files), max_files);
}
void RecentArcMediaSource::OnComplete(const int32_t call_id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CallContext* context = context_map_.Lookup(call_id);
if (context == nullptr) {
return;
}
UMA_HISTOGRAM_TIMES(kLoadHistogramName,
base::TimeTicks::Now() - context->build_start_time);
DCHECK_EQ(0, context->num_inflight_readdirs);
DCHECK(!context->callback.is_null());
std::vector<RecentFile> files =
ExtractFoundFiles(context->document_id_to_file);
std::move(context->callback)
.Run(PrepareResponse(std::move(files), context->params.max_files()));
context_map_.Remove(call_id);
}
bool RecentArcMediaSource::WillArcFileSystemOperationsRunImmediately() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
auto* runner =
arc::ArcFileSystemOperationRunner::GetForBrowserContext(profile_);
if (!runner) {
return false;
}
return !runner->WillDefer();
}
void RecentArcMediaSource::SetLagForTesting(const base::TimeDelta& lag) {
lag_ = lag;
}
storage::FileSystemURL RecentArcMediaSource::BuildDocumentsProviderUrl(
const Params& params,
const base::FilePath& path) const {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
storage::ExternalMountPoints* mount_points =
storage::ExternalMountPoints::GetSystemInstance();
DCHECK(mount_points);
return mount_points->CreateExternalFileSystemURL(
blink::StorageKey::CreateFirstParty(url::Origin::Create(params.origin())),
arc::kDocumentsProviderMountPointName, relative_mount_path_.Append(path));
}
}