// 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.
#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/";
// The path of the MyFiles directory inside Android. The UUID "0000....2019" is
// defined in ash/components/arc/volume_mounter/arc_volume_mounter_bridge.cc.
// TODO(crbug.com/929031): Move MyFiles constants to a common place.
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;
}
// Tidies up the vector of files by sorting them and limiting their number to
// the specified maximum.
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;
}
} // namespace
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 ARC file system operations will be deferred, return immediately without
// recording UMA metrics.
//
// TODO(nya): Return files progressively rather than simply giving up.
// Also, it is wrong to assume all following operations will not be deferred
// just because this function returned true. However, in practice, it is rare
// ArcFileSystemOperationRunner's deferring state switches from disabled to
// enabled (one such case is when ARC container crashes).
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())) {
// Return immediately without results when this root's id does not match the
// requested file type.
OnComplete(params.call_id());
return;
}
auto* runner =
arc::ArcFileSystemOperationRunner::GetForBrowserContext(profile_);
if (!runner) {
// This happens when ARC is not allowed in this profile.
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;
}
// Initialize |document_id_to_file_| with recent document IDs returned.
if (maybe_documents.has_value()) {
const std::u16string q16 = base::UTF8ToUTF16(context->params.query());
for (const auto& document : maybe_documents.value()) {
// Exclude media files under Downloads or MyFiles directory since they are
// covered by RecentDiskSource.
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;
}
// We have several recent documents, so start searching their real paths.
ScanDirectory(call_id, base::FilePath());
}
void RecentArcMediaSource::ScanDirectory(const int32_t call_id,
const base::FilePath& path) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// If context was cleared while we were scanning directories, just abandon
// this effort.
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) {
// We already checked ARC is allowed for this profile (indirectly), so
// this should never happen.
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) {
// Media roots should always exist.
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);
// If callback was cleared while we were scanning directories just abandon
// this effort.
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;
}
// Update |document_id_to_file_|.
// We keep the lexicographically smallest URL to stabilize the results when
// there are multiple files with the same document ID.
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) {
// Here we assume that the call to stop came just after we returned the
// results in the OnComplete method.
return {};
}
size_t max_files = context->params.max_files();
// We do not call the callback, so just clean it up.
context->callback.Reset();
// Copy the files we collected so far.
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) {
// If we cannot find the context that means the Stop method has been called.
// Just return immediately.
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 ARC is not allowed the user, |runner| is nullptr.
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));
}
} // namespace ash