// Copyright 2021 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/app_list/search/files/drive_search_provider.h"
#include <cmath>
#include <optional>
#include <string>
#include <utility>
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "chrome/browser/ash/app_list/search/common/string_util.h"
#include "chrome/browser/ash/app_list/search/files/file_result.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/drivefs/drivefs_search_query.h"
#include "components/drive/file_errors.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/strings/grit/ui_chromeos_strings.h"
#include "url/gurl.h"
namespace app_list {
namespace {
using ::ash::string_matching::TokenizedString;
constexpr char kDriveSearchSchema[] = "drive_search://";
constexpr int kMaxResults = 50;
constexpr size_t kMinQuerySizeForSharedFiles = 5u;
constexpr double kRelevanceThreshold = 0.79;
// Outcome of a call to DriveSearchProvider::Start. These values persist
// to logs. Entries should not be renumbered and numeric values should never be
// reused.
enum class Status {
kOk = 0,
kDriveUnavailable = 1,
kFileError = 2,
kMaxValue = kFileError,
};
void LogStatus(Status status) {
base::UmaHistogramEnumeration("Apps.AppList.DriveSearchProvider.Status",
status);
}
} // namespace
DriveSearchProvider::DriveSearchProvider(Profile* profile,
bool should_filter_shared_files,
bool should_filter_directories)
: SearchProvider(SearchCategory::kFiles),
should_filter_shared_files_(should_filter_shared_files),
should_filter_directories_(should_filter_directories),
profile_(profile),
drive_service_(
drive::DriveIntegrationServiceFactory::GetForProfile(profile)) {
DCHECK(profile_);
DCHECK(drive_service_);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
DriveSearchProvider::~DriveSearchProvider() = default;
void DriveSearchProvider::SetQuerySource(
drivefs::mojom::QueryParameters::QuerySource query_source) {
query_source_ = query_source;
}
ash::AppListSearchResultType DriveSearchProvider::ResultType() const {
return ash::AppListSearchResultType::kDriveSearch;
}
void DriveSearchProvider::Start(const std::u16string& query) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
query_start_time_ = base::TimeTicks::Now();
weak_factory_.InvalidateWeakPtrs();
if (!drive_service_ || !drive_service_->is_enabled()) {
Results empty_results;
SwapResults(&empty_results);
LogStatus(Status::kDriveUnavailable);
return;
}
last_query_ = query;
last_tokenized_query_.emplace(query, TokenizedString::Mode::kWords);
drivefs_search_query_ = drive_service_->CreateSearchQueryByFileName(
base::UTF16ToUTF8(query), kMaxResults,
drivefs::mojom::QueryParameters::SortField::kLastModified,
drivefs::mojom::QueryParameters::SortDirection::kDescending,
query_source_);
if (drivefs_search_query_ == nullptr) {
Results empty_results;
SwapResults(&empty_results);
LogStatus(Status::kDriveUnavailable);
return;
}
drivefs_search_query_->GetNextPage(
base::BindOnce(&DriveSearchProvider::OnSearchDriveByFileName,
weak_factory_.GetWeakPtr()));
}
void DriveSearchProvider::StopQuery() {
weak_factory_.InvalidateWeakPtrs();
last_query_.clear();
last_tokenized_query_.reset();
drivefs_search_query_.reset();
}
void DriveSearchProvider::OnSearchDriveByFileName(
drive::FileError error,
std::optional<std::vector<drivefs::mojom::QueryItemPtr>> items) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!drive::IsFileErrorOk(error) || !items.has_value()) {
Results empty_results;
SwapResults(&empty_results);
LogStatus(Status::kFileError);
return;
}
// Filter out shared files if it was not disabled in the ctor, and the query
// length is below a threshold.
if (should_filter_shared_files_ &&
last_query_.size() < kMinQuerySizeForSharedFiles) {
std::vector<drivefs::mojom::QueryItemPtr> filtered_items;
for (auto& item : *items) {
if (!item->metadata->shared) {
filtered_items.push_back(std::move(item));
}
}
items = std::move(filtered_items);
}
if (!drive_service_->IsMounted()) {
return;
}
base::UmaHistogramTimes("Apps.AppList.DriveSearchProvider.DriveFSLatency",
base::TimeTicks::Now() - query_start_time_);
results_returned_time_ = base::TimeTicks::Now();
base::FilePath mount_path = drive_service_->GetMountPointPath();
SearchProvider::Results results;
for (const auto& item : *items) {
// Strip leading separators so that the path can be reparented.
const auto& path = item->path;
DCHECK(!path.value().empty());
const base::FilePath relative_path =
!path.value().empty() && base::FilePath::IsSeparator(path.value()[0])
? base::FilePath(path.value().substr(1))
: path;
// Reparent the file path into the user's DriveFS mount.
DCHECK(!relative_path.IsAbsolute());
base::FilePath reparented_path = mount_path.Append(relative_path.value());
std::optional<base::Time> last_accessed;
// DriveFS surfaces atime in the virtual vilesystem as
// `last_viewed_by_me_time` (http://go/lkrez), which does not account for
// "last viewed by other users".
// We can use `last_viewed_by_me_time` from the metadata response directly
// as it should be the same (http://go/yufhv).
// This avoids needing to perform I/O to stat the file.
if (base::Time viewed_time = item->metadata->last_viewed_by_me_time;
!viewed_time.is_null()) {
last_accessed = viewed_time;
}
double relevance = FileResult::CalculateRelevance(
last_tokenized_query_, reparented_path, last_accessed);
if (search_features::IsLauncherFuzzyMatchAcrossProvidersEnabled() &&
relevance < kRelevanceThreshold) {
continue;
}
std::unique_ptr<FileResult> result;
GURL url(item->metadata->alternate_url);
if (item->metadata->type ==
drivefs::mojom::FileMetadata::Type::kDirectory) {
// TODO: b/357740941 - Move this filtering to the DriveFS query.
if (should_filter_directories_) {
continue;
}
const auto type = item->metadata->shared
? FileResult::Type::kSharedDirectory
: FileResult::Type::kDirectory;
result = MakeResult(reparented_path, relevance, type, url,
item->metadata->item_id, item->metadata->title);
} else {
result = MakeResult(reparented_path, relevance, FileResult::Type::kFile,
url, item->metadata->item_id, item->metadata->title);
}
results.push_back(std::move(result));
}
SwapResults(&results);
LogStatus(Status::kOk);
base::UmaHistogramTimes("Apps.AppList.DriveSearchProvider.FileInfoLatency",
base::TimeTicks::Now() - results_returned_time_);
base::UmaHistogramTimes("Apps.AppList.DriveSearchProvider.Latency",
base::TimeTicks::Now() - query_start_time_);
}
std::unique_ptr<FileResult> DriveSearchProvider::MakeResult(
const base::FilePath& reparented_path,
double relevance,
FileResult::Type type,
const GURL& url,
const std::optional<std::string>& id,
const std::optional<std::string>& title) {
// Add "Google Drive" as details.
std::u16string details =
l10n_util::GetStringUTF16(IDS_FILE_BROWSER_DRIVE_DIRECTORY_LABEL);
auto result = std::make_unique<FileResult>(
/*id=*/kDriveSearchSchema + reparented_path.value(), reparented_path,
details, ash::AppListSearchResultType::kDriveSearch,
ash::SearchResultDisplayType::kList, relevance, last_query_, type,
profile_, /*thumbnail_loader=*/nullptr);
if (id.has_value()) {
result->set_drive_id(id);
} else {
result->set_drive_id(GetDriveId(url));
}
if (title.has_value()) {
result->SetTitle(base::UTF8ToUTF16(*title));
}
result->set_url(url);
return result;
}
} // namespace app_list