| // 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_model.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/ash/arc/fileapi/arc_media_view_util.h" |
| #include "chrome/browser/ash/file_manager/path_util.h" |
| #include "chrome/browser/ash/file_manager/volume_manager.h" |
| #include "chrome/browser/ash/fileapi/recent_arc_media_source.h" |
| #include "chrome/browser/ash/fileapi/recent_disk_source.h" |
| #include "chrome/browser/ash/fileapi/recent_drive_source.h" |
| #include "chrome/browser/ash/fileapi/recent_file.h" |
| #include "chrome/browser/ash/fileapi/recent_model_factory.h" |
| #include "chrome/common/extensions/api/file_manager_private.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| |
| using content::BrowserThread; |
| |
| namespace ash { |
| |
| namespace { |
| |
| namespace fmp = extensions::api::file_manager_private; |
| |
| // Helper method that transfers files that qualify, based on the cut-off time |
| // to the accumulator. Used either when a recent source completes the work or |
| // when it is stopped. |
| void TransferFiles(const std::vector<RecentFile>& found_files, |
| const base::Time& cutoff_time, |
| FileAccumulator* accumulator) { |
| for (const auto& file : found_files) { |
| if (file.last_modified() >= cutoff_time) { |
| accumulator->Add(file); |
| } |
| } |
| } |
| |
| // Recent file cache will be cleared this duration after it is built. |
| // Note: Do not make this value large. When cache is used, cut-off criteria is |
| // not strictly honored. |
| constexpr base::TimeDelta kCacheExpiration = base::Seconds(10); |
| |
| std::vector<std::unique_ptr<RecentSource>> CreateDefaultSources( |
| Profile* profile) { |
| std::vector<std::unique_ptr<RecentSource>> sources; |
| |
| // ARC sources. |
| sources.emplace_back( |
| std::make_unique<RecentArcMediaSource>(profile, arc::kImagesRootId)); |
| sources.emplace_back( |
| std::make_unique<RecentArcMediaSource>(profile, arc::kVideosRootId)); |
| sources.emplace_back( |
| std::make_unique<RecentArcMediaSource>(profile, arc::kDocumentsRootId)); |
| // Android's MediaDocumentsProvider.queryRecentDocuments() doesn't support |
| // audio files, http://b/175155820. Therefore no arc::kAudioRootId source. |
| |
| // Crostini. |
| sources.emplace_back(std::make_unique<RecentDiskSource>( |
| fmp::VolumeType::kCrostini, |
| file_manager::util::GetCrostiniMountPointName(profile), |
| /*ignore_dotfiles=*/true, /*max_depth=*/4, |
| "FileBrowser.Recent.LoadCrostini")); |
| |
| // Downloads / MyFiles. |
| sources.emplace_back(std::make_unique<RecentDiskSource>( |
| fmp::VolumeType::kDownloads, |
| file_manager::util::GetDownloadsMountPointName(profile), |
| /*ignore_dotfiles=*/true, /*unlimited max_depth=*/0, |
| "FileBrowser.Recent.LoadDownloads")); |
| sources.emplace_back(std::make_unique<RecentDriveSource>(profile)); |
| |
| // File System Providers. |
| file_manager::VolumeManager* volume_manager = |
| file_manager::VolumeManager::Get(profile); |
| for (const base::WeakPtr<file_manager::Volume> volume : |
| volume_manager->GetVolumeList()) { |
| if (!volume || volume->type() != file_manager::VOLUME_TYPE_PROVIDED || |
| volume->file_system_type() == file_manager::util::kFuseBox) { |
| // Provided volume types are served via two file system types: fusebox |
| // (requires ChromeOS' /usr/bin/fusebox daemon process to be running) and |
| // non-fusebox. The Files app runs in ash and could use either. Using both |
| // would return duplicate results. We therefore filter out the fusebox |
| // file system type. |
| continue; |
| } |
| sources.emplace_back(std::make_unique<RecentDiskSource>( |
| fmp::VolumeType::kProvided, |
| volume->mount_path().BaseName().AsUTF8Unsafe(), |
| /*ignore_dot_files=*/true, /*max_depth=*/0, |
| "FileBrowser.Recent.LoadFileSystemProvider")); |
| } |
| |
| return sources; |
| } |
| |
| } // namespace |
| |
| RecentModelOptions::RecentModelOptions() = default; |
| |
| RecentModelOptions::~RecentModelOptions() = default; |
| |
| RecentModel::CallContext::CallContext(const SearchCriteria& criteria, |
| GetRecentFilesCallback callback) |
| : search_criteria(criteria), |
| callback(std::move(callback)), |
| build_start_time(base::TimeTicks::Now()), |
| accumulator(criteria.max_files) {} |
| |
| RecentModel::CallContext::CallContext(CallContext&& context) |
| : search_criteria(context.search_criteria), |
| callback(std::move(context.callback)), |
| build_start_time(context.build_start_time), |
| accumulator(std::move(context.accumulator)), |
| active_sources(std::move(context.active_sources)) {} |
| |
| RecentModel::CallContext::~CallContext() = default; |
| |
| // static |
| std::unique_ptr<RecentModel> RecentModel::CreateForTest( |
| std::vector<std::unique_ptr<RecentSource>> sources) { |
| return base::WrapUnique(new RecentModel(std::move(sources))); |
| } |
| |
| RecentModel::RecentModel(Profile* profile) |
| : RecentModel(CreateDefaultSources(profile)) {} |
| |
| RecentModel::RecentModel(std::vector<std::unique_ptr<RecentSource>> sources) |
| : sources_(std::move(sources)) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| } |
| |
| RecentModel::~RecentModel() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(sources_.empty()); |
| } |
| |
| void RecentModel::GetRecentFiles( |
| storage::FileSystemContext* file_system_context, |
| const GURL& origin, |
| const std::string& query, |
| const RecentModelOptions& options, |
| GetRecentFilesCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| const int32_t this_call_id = ++call_id_; |
| SearchCriteria search_criteria = { |
| .query = query, |
| .now_delta = options.now_delta, |
| .max_files = options.max_files, |
| .file_type = options.file_type, |
| }; |
| |
| /** |
| * Use cache only if: |
| * * cache has value. |
| * * invalidate_cache = false. |
| * * cached file type matches the query |
| * file type. Otherwise clear cache if |
| * it has values. |
| */ |
| if (cached_files_.has_value()) { |
| if (!options.invalidate_cache && |
| cached_search_criteria_ == search_criteria) { |
| std::move(callback).Run(cached_files_.value()); |
| return; |
| } |
| cached_files_.reset(); |
| } |
| |
| auto context = |
| std::make_unique<CallContext>(search_criteria, std::move(callback)); |
| // The source list should never be empty, as this means somebody wants recent |
| // files from without specifying even a single source. |
| DCHECK(!options.source_specs.empty()); |
| std::set<fmp::VolumeType> volume_filter; |
| for (const RecentSourceSpec& restriction : options.source_specs) { |
| volume_filter.emplace(restriction.volume_type); |
| } |
| |
| // filtered_sources is a copy of active_sources. However, as active_sources |
| // is modified, we need to create a copy that is not going to be altered |
| // while we are iterating over it. |
| std::vector<RecentSource*> filtered_sources; |
| filtered_sources.reserve(sources_.size()); |
| for (const auto& source : sources_) { |
| auto it = volume_filter.find(source->volume_type()); |
| if (it != volume_filter.end()) { |
| context->active_sources.insert(source.get()); |
| filtered_sources.emplace_back(source.get()); |
| } |
| } |
| context_map_.AddWithID(std::move(context), this_call_id); |
| |
| if (filtered_sources.empty()) { |
| OnSearchCompleted(this_call_id); |
| return; |
| } |
| |
| // cutoff_time is the oldest modified time for a file to be considered |
| // recent. |
| base::Time cutoff_time = base::Time::Now() - options.now_delta; |
| |
| if (!options.scan_timeout.is_inf()) { |
| auto timer = std::make_unique<base::DeadlineTimer>(); |
| base::DeadlineTimer* timer_ptr = timer.get(); |
| deadline_map_.AddWithID(std::move(timer), this_call_id); |
| timer_ptr->Start(FROM_HERE, base::TimeTicks::Now() + options.scan_timeout, |
| base::BindOnce(&RecentModel::OnScanTimeout, |
| weak_ptr_factory_.GetWeakPtr(), cutoff_time, |
| this_call_id)); |
| } |
| |
| // If there is no scan timeout we set the end_time, i.e., the time by which |
| // the scan is supposed to be done, to maximum possible time. In the current |
| // code base that is about year 292,471. |
| base::TimeTicks end_time = |
| options.scan_timeout.is_inf() |
| ? base::TimeTicks::Max() |
| : base::TimeTicks::Now() + options.scan_timeout; |
| |
| const RecentSource::Params params(file_system_context, this_call_id, origin, |
| query, options.max_files, cutoff_time, |
| end_time, options.file_type); |
| for (const auto& source : filtered_sources) { |
| source->GetRecentFiles( |
| params, base::BindOnce(&RecentModel::OnGotRecentFiles, |
| weak_ptr_factory_.GetWeakPtr(), source, |
| cutoff_time, this_call_id)); |
| } |
| } |
| |
| void RecentModel::OnScanTimeout(const base::Time& cutoff_time, |
| const int32_t call_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallContext* context = context_map_.Lookup(call_id); |
| if (context == nullptr) { |
| return; |
| } |
| |
| for (RecentSource* source : context->active_sources) { |
| TransferFiles(source->Stop(call_id), cutoff_time, &context->accumulator); |
| } |
| context->active_sources.clear(); |
| OnSearchCompleted(call_id); |
| } |
| |
| void RecentModel::Shutdown() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| context_map_.Clear(); |
| deadline_map_.Clear(); |
| // Some RecentSource implementations have references to other |
| // KeyedServices, so we destruct them here. |
| sources_.clear(); |
| } |
| |
| void RecentModel::OnGotRecentFiles(RecentSource* source, |
| const base::Time& cutoff_time, |
| const int32_t call_id, |
| std::vector<RecentFile> files) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallContext* context = context_map_.Lookup(call_id); |
| if (context == nullptr) { |
| return; |
| } |
| TransferFiles(files, cutoff_time, &context->accumulator); |
| context->active_sources.erase(source); |
| |
| if (context->active_sources.empty()) { |
| OnSearchCompleted(call_id); |
| } |
| } |
| |
| void RecentModel::OnSearchCompleted(const int32_t call_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CallContext* context = context_map_.Lookup(call_id); |
| if (context == nullptr) { |
| return; |
| } |
| deadline_map_.Remove(call_id); |
| |
| DCHECK(context->active_sources.empty()); |
| DCHECK(!context->callback.is_null()); |
| DCHECK(!context->build_start_time.is_null()); |
| |
| cached_files_ = context->accumulator.Get(); |
| cached_search_criteria_ = context->search_criteria; |
| |
| DCHECK(cached_files_.has_value()); |
| |
| UMA_HISTOGRAM_TIMES(kLoadHistogramName, |
| base::TimeTicks::Now() - context->build_start_time); |
| |
| // Starts a timer to clear cache. |
| cache_clear_timer_.Start( |
| FROM_HERE, kCacheExpiration, |
| base::BindOnce(&RecentModel::ClearCache, weak_ptr_factory_.GetWeakPtr())); |
| |
| std::move(context->callback).Run(context->accumulator.Get()); |
| context_map_.Remove(call_id); |
| } |
| |
| void RecentModel::ClearCache() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| cached_files_.reset(); |
| } |
| |
| } // namespace ash |