| // Copyright 2022 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/file_suggest/local_file_suggestion_provider.h" | 
 |  | 
 | #include <algorithm> | 
 | #include <optional> | 
 | #include <vector> | 
 |  | 
 | #include "base/files/file.h" | 
 | #include "base/files/file_path.h" | 
 | #include "base/files/file_util.h" | 
 | #include "base/functional/bind.h" | 
 | #include "base/time/time.h" | 
 | #include "chrome/browser/ash/app_list/search/files/justifications.h" | 
 | #include "chrome/browser/ash/app_list/search/ranking/util.h" | 
 | #include "chrome/browser/ash/app_list/search/util/mrfu_cache.h" | 
 | #include "chrome/browser/ash/file_manager/file_tasks_notifier_factory.h" | 
 | #include "chrome/browser/ash/file_manager/path_util.h" | 
 | #include "chrome/browser/ash/file_manager/trash_common_util.h" | 
 | #include "chrome/browser/ash/file_suggest/file_suggest_util.h" | 
 | #include "chrome/browser/ash/file_suggest/file_suggestion_provider.h" | 
 | #include "chrome/browser/profiles/profile.h" | 
 |  | 
 | namespace ash { | 
 |  | 
 | namespace { | 
 | constexpr base::TimeDelta kSaveDelay = base::Seconds(3); | 
 | constexpr base::TimeDelta kSuggestionNotificationDebounce = | 
 |     base::Milliseconds(100); | 
 |  | 
 | // Given the output of MrfuCache::GetAll, partition files into valid and invalid | 
 | // files. Valid files are files that: | 
 | // - Exist on-disk | 
 | // - Have been modified in the last |max_last_modified_time| days | 
 | std::pair<std::vector<LocalFileSuggestionProvider::LocalFileData>, | 
 |           std::vector<base::FilePath>> | 
 | ValidateFiles(const std::vector<std::pair<std::string, float>>& ranker_results, | 
 |               const base::TimeDelta& max_last_modified_time, | 
 |               std::vector<base::FilePath> trash_paths) { | 
 |   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, | 
 |                                                 base::BlockingType::MAY_BLOCK); | 
 |  | 
 |   std::vector<LocalFileSuggestionProvider::LocalFileData> valid_results; | 
 |   std::vector<base::FilePath> invalid_results; | 
 |   const base::Time now = base::Time::Now(); | 
 |   for (const auto& path_score : ranker_results) { | 
 |     // We use FilePath::FromUTF8Unsafe to decode the filepath string. As per its | 
 |     // documentation, this is a safe use of the function because | 
 |     // LocalFileSuggestionProvider is only used on ChromeOS, for which filepaths | 
 |     // are UTF8. | 
 |     const auto& path = base::FilePath::FromUTF8Unsafe(path_score.first); | 
 |  | 
 |     // Exclude any paths that are parented at an enabled trash location. | 
 |     if (std::ranges::any_of(trash_paths, | 
 |                             [&path](const base::FilePath& trash_path) { | 
 |                               return trash_path.IsParent(path); | 
 |                             })) { | 
 |       invalid_results.emplace_back(path); | 
 |       continue; | 
 |     } | 
 |  | 
 |     base::File::Info info; | 
 |     if (base::PathExists(path) && base::GetFileInfo(path, &info) && | 
 |         (now - info.last_modified <= max_last_modified_time)) { | 
 |       valid_results.emplace_back(LocalFileSuggestionProvider::LocalFileData{ | 
 |           path_score.second, path, info}); | 
 |     } else { | 
 |       invalid_results.emplace_back(path); | 
 |     } | 
 |   } | 
 |   return {valid_results, invalid_results}; | 
 | } | 
 |  | 
 | }  // anonymous namespace | 
 |  | 
 | LocalFileSuggestionProvider::LocalFileSuggestionProvider( | 
 |     Profile* profile, | 
 |     base::RepeatingCallback<void(FileSuggestionType)> notify_update_callback) | 
 |     : FileSuggestionProvider(notify_update_callback), | 
 |       profile_(profile), | 
 |       max_last_modified_time_(GetMaxFileSuggestionRecency()) { | 
 |   DCHECK(profile_); | 
 |  | 
 |   task_runner_ = base::ThreadPool::CreateSequencedTaskRunner( | 
 |       {base::TaskPriority::USER_BLOCKING, base::MayBlock(), | 
 |        base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}); | 
 |  | 
 |   auto* notifier = | 
 |       file_manager::file_tasks::FileTasksNotifierFactory::GetForProfile( | 
 |           profile); | 
 |  | 
 |   if (notifier) { | 
 |     file_tasks_observer_.Observe(notifier); | 
 |  | 
 |     app_list::MrfuCache::Params params; | 
 |     // 5 consecutive clicks to get a new file to a score of 0.8, and 10 clicks | 
 |     // on other files to reduce its score by half. | 
 |     params.half_life = 10.0f; | 
 |     params.boost_factor = 5.0f; | 
 |     app_list::MrfuCache::Proto proto( | 
 |         app_list::RankerStateDirectory(profile).AppendASCII( | 
 |             "zero_state_local_files.pb"), | 
 |         kSaveDelay); | 
 |  | 
 |     // `proto` is owned by `files_ranker_` which is a class member so it is safe | 
 |     // to call `RegisterOnInitUnsafe()`. | 
 |     proto.RegisterOnInitUnsafe( | 
 |         base::BindOnce(&LocalFileSuggestionProvider::OnProtoInitialized, | 
 |                        base::Unretained(this))); | 
 |  | 
 |     files_ranker_ = | 
 |         std::make_unique<app_list::MrfuCache>(std::move(proto), params); | 
 |   } | 
 | } | 
 |  | 
 | LocalFileSuggestionProvider::~LocalFileSuggestionProvider() = default; | 
 |  | 
 | bool LocalFileSuggestionProvider::IsInitialized() const { | 
 |   return files_ranker_ && files_ranker_->initialized(); | 
 | } | 
 |  | 
 | void LocalFileSuggestionProvider::GetSuggestFileData( | 
 |     GetSuggestFileDataCallback callback) { | 
 |   if (!files_ranker_ || !files_ranker_->initialized()) { | 
 |     std::move(callback).Run(std::nullopt); | 
 |     return; | 
 |   } | 
 |  | 
 |   if (!on_validation_complete_callback_list_.empty()) { | 
 |     on_validation_complete_callback_list_.AddUnsafe(std::move(callback)); | 
 |     return; | 
 |   } | 
 |  | 
 |   on_validation_complete_callback_list_.AddUnsafe(std::move(callback)); | 
 |  | 
 |   // Generate the trash paths on the first get suggestion of file data. This is | 
 |   // to enable unit tests to mock out the trash paths appropriately. | 
 |   if (trash_paths_.empty()) { | 
 |     auto enabled_trash_locations = | 
 |         file_manager::trash::GenerateEnabledTrashLocationsForProfile(profile_); | 
 |     for (const auto& it : enabled_trash_locations) { | 
 |       trash_paths_.emplace_back( | 
 |           it.first.Append(it.second.relative_folder_path)); | 
 |     } | 
 |   } | 
 |  | 
 |   task_runner_->PostTaskAndReplyWithResult( | 
 |       FROM_HERE, | 
 |       base::BindOnce(&ValidateFiles, files_ranker_->GetAll(), | 
 |                      max_last_modified_time_, | 
 |                      (file_manager::trash::IsTrashEnabledForProfile(profile_) | 
 |                           ? trash_paths_ | 
 |                           : std::vector<base::FilePath>())), | 
 |       base::BindOnce(&LocalFileSuggestionProvider::OnValidationComplete, | 
 |                      weak_factory_.GetWeakPtr())); | 
 | } | 
 |  | 
 | void LocalFileSuggestionProvider::MaybeUpdateItemSuggestCache( | 
 |     base::PassKey<FileSuggestKeyedService>) { | 
 |   NOTREACHED(); | 
 | } | 
 |  | 
 | void LocalFileSuggestionProvider::OnFilesOpened( | 
 |     const std::vector<FileOpenEvent>& file_opens) { | 
 |   if (!files_ranker_) { | 
 |     return; | 
 |   } | 
 |  | 
 |   const auto& profile_path = profile_->GetPath(); | 
 |   for (const auto& file_open : file_opens) { | 
 |     // Filter out file opens if: | 
 |     // 1. The open event is not a kLaunch or a kOpen. | 
 |     if (file_open.open_type != FileTasksObserver::OpenType::kLaunch && | 
 |         file_open.open_type != FileTasksObserver::OpenType::kOpen) { | 
 |       continue; | 
 |     } | 
 |  | 
 |     // 2. The open relates to a Drive file, which is handled by another | 
 |     // provider. Filter this out by checking if the file resides in the user's | 
 |     // cryptohome. | 
 |     if (!profile_path.IsParent(file_open.path) && | 
 |         !file_manager::util::GetMyFilesFolderForProfile(profile_).IsParent( | 
 |             file_open.path) && | 
 |         !file_manager::util::GetDownloadsFolderForProfile(profile_).IsParent( | 
 |             file_open.path)) { | 
 |       continue; | 
 |     } | 
 |  | 
 |     files_ranker_->Use(file_open.path.value()); | 
 |   } | 
 |  | 
 |   if (!queued_notification_.IsRunning()) { | 
 |     queued_notification_.Start( | 
 |         FROM_HERE, kSuggestionNotificationDebounce, | 
 |         base::BindOnce(&LocalFileSuggestionProvider::NotifySuggestionUpdate, | 
 |                        weak_factory_.GetWeakPtr(), | 
 |                        FileSuggestionType::kLocalFile)); | 
 |   } | 
 | } | 
 |  | 
 | void LocalFileSuggestionProvider::OnProtoInitialized() { | 
 |   NotifySuggestionUpdate(FileSuggestionType::kLocalFile); | 
 | } | 
 |  | 
 | void LocalFileSuggestionProvider::OnValidationComplete( | 
 |     std::pair<std::vector<LocalFileData>, std::vector<base::FilePath>> | 
 |         results) { | 
 |   // Delete invalid results from the ranker. | 
 |   for (const base::FilePath& path : results.second) { | 
 |     files_ranker_->Delete(path.value()); | 
 |   } | 
 |  | 
 |   std::vector<FileSuggestData> final_results; | 
 |   for (auto& result : results.first) { | 
 |     std::optional<std::u16string> justification_string; | 
 |     if (result.info.last_accessed > result.info.last_modified) { | 
 |       justification_string = app_list::GetJustificationString( | 
 |           FileSuggestionJustificationType::kViewed, result.info.last_accessed, | 
 |           /*user_name=*/""); | 
 |     } else { | 
 |       justification_string = app_list::GetJustificationString( | 
 |           FileSuggestionJustificationType::kModifiedByCurrentUser, | 
 |           result.info.last_modified, | 
 |           /*user_name=*/""); | 
 |     } | 
 |  | 
 |     final_results.emplace_back(FileSuggestionType::kLocalFile, result.path, | 
 |                                /*title=*/std::nullopt, justification_string, | 
 |                                /*modified_time=*/result.info.last_modified, | 
 |                                /*viewed_time=*/result.info.last_accessed, | 
 |                                /*shared_time=*/std::nullopt, result.score, | 
 |                                /*drive_file_id=*/std::nullopt, | 
 |                                /*icon_url=*/std::nullopt); | 
 |   } | 
 |  | 
 |   // Sort valid results high-to-low by score. | 
 |   std::sort(final_results.begin(), final_results.end(), | 
 |             [](const auto& a, const auto& b) { | 
 |               return a.score.value() > b.score.value(); | 
 |             }); | 
 |  | 
 |   on_validation_complete_callback_list_.Notify(final_results); | 
 |   DCHECK(on_validation_complete_callback_list_.empty()); | 
 | } | 
 |  | 
 | }  // namespace ash |