| // 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 |