chromium/chrome/browser/ash/file_suggest/drive_recent_file_suggestion_provider.cc

// Copyright 2023 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/drive_recent_file_suggestion_provider.h"

#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "base/barrier_closure.h"
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/i18n/time_formatting.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/files/justifications.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/file_suggest/file_suggest_util.h"
#include "chromeos/ash/components/drivefs/drivefs_host.h"
#include "chromeos/ash/components/drivefs/drivefs_util.h"
#include "components/drive/file_errors.h"
#include "components/prefs/pref_service.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"

namespace ash {
namespace {

constexpr char kBaseHistogramName[] = "Ash.Search.FileSuggestions.DriveRecents";

drivefs::mojom::QueryParametersPtr CreateRecentlyModifiedQuery(
    base::TimeDelta max_recency) {
  auto query = drivefs::mojom::QueryParameters::New();
  query->modified_time = base::Time::Now() - max_recency;
  query->modified_time_operator =
      drivefs::mojom::QueryParameters::DateComparisonOperator::kGreaterThan;
  query->viewed_time = base::Time::Now() - max_recency;
  query->viewed_time_operator =
      drivefs::mojom::QueryParameters::DateComparisonOperator::kGreaterThan;
  query->page_size = 15;
  query->query_source =
      drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
  query->sort_direction =
      drivefs::mojom::QueryParameters::SortDirection::kDescending;
  query->sort_field = drivefs::mojom::QueryParameters::SortField::kLastModified;
  return query;
}

drivefs::mojom::QueryParametersPtr CreateRecentlyViewedQuery(
    base::TimeDelta max_recency) {
  auto query = drivefs::mojom::QueryParameters::New();
  query->page_size = 15;
  query->query_source =
      drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
  query->sort_direction =
      drivefs::mojom::QueryParameters::SortDirection::kDescending;
  query->sort_field =
      drivefs::mojom::QueryParameters::SortField::kLastViewedByMe;
  query->viewed_time = base::Time::Now() - max_recency;
  query->viewed_time_operator =
      drivefs::mojom::QueryParameters::DateComparisonOperator::kGreaterThan;
  return query;
}

drivefs::mojom::QueryParametersPtr CreateSharedWithMeQuery() {
  auto query = drivefs::mojom::QueryParameters::New();
  query->page_size = 15;
  query->query_source =
      drivefs::mojom::QueryParameters::QuerySource::kLocalOnly;
  query->shared_with_me = true;
  query->sort_direction =
      drivefs::mojom::QueryParameters::SortDirection::kDescending;
  query->sort_field = drivefs::mojom::QueryParameters::SortField::kSharedWithMe;
  return query;
}

FileSuggestData CreateFileSuggestionWithJustification(
    const base::FilePath& path,
    const std::optional<std::string>& title,
    const std::optional<base::Time>& modified_time,
    const std::optional<base::Time>& viewed_time,
    const std::optional<base::Time>& shared_time,
    const std::optional<std::u16string>& justification_string,
    const std::optional<std::string>& drive_file_id,
    const std::optional<std::string>& icon_url) {
  return FileSuggestData(
      FileSuggestionType::kDriveFile, path, title, justification_string,
      modified_time, viewed_time, shared_time,
      /*new_score=*/std::nullopt, drive_file_id, /*icon_url=*/icon_url);
}

std::optional<std::u16string> CreateSuggestionStatusForNonSharedFile(
    const drivefs::mojom::FileMetadata& file_metadata) {
  const base::Time& modified_time = file_metadata.modification_time;
  const base::Time& viewed_time = file_metadata.last_viewed_by_me_time;
  if (viewed_time > modified_time) {
    return app_list::GetJustificationString(
        FileSuggestionJustificationType::kViewed, viewed_time, "");
  }

  // Last modification was by the user.
  if (file_metadata.modified_by_me_time &&
      !file_metadata.modified_by_me_time->is_null() &&
      file_metadata.modified_by_me_time >= modified_time) {
    return app_list::GetJustificationString(
        FileSuggestionJustificationType::kModifiedByCurrentUser,
        *file_metadata.modified_by_me_time, "");
  }

  const auto& user_info = file_metadata.last_modifying_user;
  return app_list::GetJustificationString(
      FileSuggestionJustificationType::kModified, modified_time,
      user_info ? user_info->display_name : "");
}

std::optional<FileSuggestData> CreateFileSuggestion(
    const base::FilePath& path,
    const drivefs::mojom::FileMetadata& file_metadata,
    base::TimeDelta max_recency) {
  const base::Time& viewed_time = file_metadata.last_viewed_by_me_time;
  const base::Time modified_time =
      file_metadata.modified_by_me_time.value_or(base::Time());
  const std::string& icon_url = file_metadata.custom_icon_url;
  const std::optional<std::string>& title = file_metadata.title;

  // If the file was shared with user, but not yet viewed by the user, surface
  // it as a shared file.
  if (const std::optional<base::Time>& shared_time =
          file_metadata.shared_with_me_time;
      shared_time && !shared_time->is_null() && viewed_time.is_null()) {
    if ((base::Time::Now() - *shared_time).magnitude() > max_recency) {
      return std::nullopt;
    }
    std::string sharing_user_name;
    if (features::IsShowSharingUserInLauncherContinueSectionEnabled()) {
      const auto& user_info = file_metadata.sharing_user;
      sharing_user_name = user_info ? user_info->display_name : "";
    }
    return {CreateFileSuggestionWithJustification(
        path, title, /*modified_time=*/std::nullopt,
        /*viewed_time*/ std::nullopt, *shared_time,
        app_list::GetJustificationString(
            FileSuggestionJustificationType::kShared, *shared_time,
            sharing_user_name),
        file_metadata.item_id, icon_url)};
  }

  if (viewed_time <= file_metadata.modification_time &&
      (base::Time::Now() - file_metadata.modification_time).magnitude() <=
          max_recency) {
    base::UmaHistogramBoolean(
        base::JoinString({kBaseHistogramName, "ModifyingUserMetadataPresent"},
                         "."),
        !!file_metadata.last_modifying_user);
  }

  if ((base::Time::Now() - modified_time).magnitude() > max_recency) {
    if ((base::Time::Now() - viewed_time).magnitude() > max_recency) {
      return std::nullopt;
    }

    return CreateFileSuggestionWithJustification(
        path, title,
        /*modified_time=*/std::nullopt, viewed_time,
        /*shared_time=*/std::nullopt,
        CreateSuggestionStatusForNonSharedFile(file_metadata),
        file_metadata.item_id, icon_url);
  }

  return CreateFileSuggestionWithJustification(
      path, title, modified_time, viewed_time, /*shared_time=*/std::nullopt,
      CreateSuggestionStatusForNonSharedFile(file_metadata),
      file_metadata.item_id, icon_url);
}

}  // namespace

DriveRecentFileSuggestionProvider::DriveRecentFileSuggestionProvider(
    Profile* profile,
    base::RepeatingCallback<void(FileSuggestionType)> notify_update_callback)
    : FileSuggestionProvider(notify_update_callback),
      profile_(profile),
      max_recency_(GetMaxFileSuggestionRecency()) {
  auto* drive_integration_service =
      drive::DriveIntegrationServiceFactory::GetForProfile(profile);
  if (drive_integration_service) {
    drive::DriveIntegrationService::Observer::Observe(
        drive_integration_service);
    auto* drive_fs_host = drive_integration_service->GetDriveFsHost();
    if (drive_fs_host) {
      drivefs::DriveFsHost::Observer::Observe(drive_fs_host);
    }
  }
}

DriveRecentFileSuggestionProvider::~DriveRecentFileSuggestionProvider() =
    default;

void DriveRecentFileSuggestionProvider::GetSuggestFileData(
    GetSuggestFileDataCallback callback) {
  const bool has_active_request =
      !on_drive_results_ready_callback_list_.empty();

  // Return early if there is an active search request. `callback` will run when
  // the active search completes.
  if (has_active_request) {
    // Add `callback` to the waiting list, and return.
    on_drive_results_ready_callback_list_.AddUnsafe(std::move(callback));
    return;
  }

  drive::DriveIntegrationService* drive_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile_);

  // When drive service is disabled or mount fails, send back an empty list of
  // results to avoid further waiting for file suggest data. Check
  // `mount_failed` before checking `IsMounted` to catch the case where not only
  // are we not mounted, but mounting has completely failed.
  if (drive_service &&
      (!drive_service->is_enabled() || drive_service->mount_failed())) {
    std::move(callback).Run(
        /*suggest_results=*/std::vector<FileSuggestData>());
    return;
  }

  // If there is not any available drive service, return early.
  if (!drive_service || !drive_service->IsMounted()) {
    std::move(callback).Run(/*suggest_results=*/std::nullopt);
    return;
  }

  if (can_use_cache_) {
    std::move(callback).Run(GetSuggestionsFromLatestQueryResults());
    return;
  }

  on_drive_results_ready_callback_list_.AddUnsafe(std::move(callback));

  search_start_time_ = base::Time::Now();
  query_result_files_by_path_.clear();

  // Allow using cached results for requests that come in after the current one
  // finishes.
  can_use_cache_ = true;

  base::RepeatingClosure search_callback = base::BarrierClosure(
      3, base::BindOnce(
             &DriveRecentFileSuggestionProvider::OnRecentFilesSearchesCompleted,
             weak_factory_.GetWeakPtr()));
  PerformSearch(SearchType::kLastModified,
                CreateRecentlyModifiedQuery(max_recency_), drive_service,
                search_callback);
  PerformSearch(SearchType::kLastViewed,
                CreateRecentlyViewedQuery(max_recency_), drive_service,
                search_callback);
  PerformSearch(SearchType::kSharedWithUser, CreateSharedWithMeQuery(),
                drive_service, search_callback);
}

// static
std::string DriveRecentFileSuggestionProvider::GetHistogramSuffix(
    SearchType search_type) {
  switch (search_type) {
    case SearchType::kLastViewed:
      return "Viewed";
    case SearchType::kLastModified:
      return "Modified";
    case SearchType::kSharedWithUser:
      return "Shared";
  }
}

void DriveRecentFileSuggestionProvider::OnDriveIntegrationServiceDestroyed() {
  drive::DriveIntegrationService::Observer::Reset();
  drivefs::DriveFsHost::Observer::Reset();

  can_use_cache_ = false;
  query_result_files_by_path_.clear();
  on_drive_results_ready_callback_list_.Notify(
      /*suggest_results=*/std::nullopt);
}

void DriveRecentFileSuggestionProvider::OnFileSystemMounted() {
  if (!drivefs::DriveFsHost::Observer::GetHost()) {
    auto* drive_fs_host = drive::DriveIntegrationService::Observer::GetService()
                              ->GetDriveFsHost();
    if (drive_fs_host) {
      drivefs::DriveFsHost::Observer::Observe(drive_fs_host);
    }
  }
  NotifySuggestionUpdate(FileSuggestionType::kDriveFile);
}

void DriveRecentFileSuggestionProvider::OnHostDestroyed() {
  drivefs::DriveFsHost::Observer::Reset();
}

void DriveRecentFileSuggestionProvider::OnUnmounted() {
  can_use_cache_ = false;
  query_result_files_by_path_.clear();
  on_drive_results_ready_callback_list_.Notify(
      /*suggest_results=*/std::nullopt);

  NotifySuggestionUpdate(FileSuggestionType::kDriveFile);
}

void DriveRecentFileSuggestionProvider::OnFilesChanged(
    const std::vector<drivefs::mojom::FileChange>& changes) {
  can_use_cache_ = false;

  for (const auto& change : changes) {
    if (change.type == drivefs::mojom::FileChange::Type::kDelete) {
      query_result_files_by_path_.erase(change.path);
    }
  }
}

void DriveRecentFileSuggestionProvider::PerformSearch(
    SearchType search_type,
    drivefs::mojom::QueryParametersPtr query,
    drive::DriveIntegrationService* drive_service,
    base::RepeatingClosure callback) {
  drive_service->GetDriveFsHost()->PerformSearch(
      std::move(query),
      base::BindOnce(
          &DriveRecentFileSuggestionProvider::OnSearchRequestComplete,
          weak_factory_.GetWeakPtr(), search_type, std::move(callback)));
}

void DriveRecentFileSuggestionProvider::MaybeUpdateItemSuggestCache(
    base::PassKey<FileSuggestKeyedService>) {}

void DriveRecentFileSuggestionProvider::OnSearchRequestComplete(
    SearchType search_type,
    base::RepeatingClosure callback,
    drive::FileError error,
    std::optional<std::vector<drivefs::mojom::QueryItemPtr>> items) {
  // `error` is an enum, but has negative values, so UmaEnumeration does not
  // work - record it as a count instead.
  base::UmaHistogramCounts100(
      base::JoinString(
          {kBaseHistogramName, "QueryResult", GetHistogramSuffix(search_type)},
          "."),
      std::abs(error));

  if (!drive::IsFileErrorOk(error) &&
      error != drive::FileError::FILE_ERROR_INVALID_OPERATION) {
    can_use_cache_ = false;
  }

  if (drive::IsFileErrorOk(error) && items) {
    base::UmaHistogramTimes(
        base::JoinString({kBaseHistogramName, "DurationOnSuccess",
                          GetHistogramSuffix(search_type)},
                         "."),
        base::Time::Now() - search_start_time_);
    base::UmaHistogramCounts100(
        base::JoinString(
            {kBaseHistogramName, "ItemCount", GetHistogramSuffix(search_type)},
            "."),
        items->size());

    size_t folder_count = 0u;
    for (auto& item : *items) {
      if (item->metadata->type ==
          drivefs::mojom::FileMetadata::Type::kDirectory) {
        ++folder_count;
        continue;
      }
      query_result_files_by_path_.emplace(item->path,
                                          std::move(item->metadata));
    }
    base::UmaHistogramCounts100(
        base::JoinString({kBaseHistogramName, "FolderCount",
                          GetHistogramSuffix(search_type)},
                         "."),
        folder_count);

  } else {
    base::UmaHistogramTimes(
        base::JoinString({kBaseHistogramName, "DurationOnError",
                          GetHistogramSuffix(search_type)},
                         "."),
        base::Time::Now() - search_start_time_);
  }
  callback.Run();
}

std::vector<FileSuggestData>
DriveRecentFileSuggestionProvider::GetSuggestionsFromLatestQueryResults() {
  drive::DriveIntegrationService* const drive_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile_);
  CHECK(drive_service);

  std::vector<FileSuggestData> results;
  for (const auto& item : query_result_files_by_path_) {
    base::FilePath root("/");
    base::FilePath path = drive_service->GetMountPointPath();
    if (!root.AppendRelativePath(item.first, &path)) {
      continue;
    }

    std::optional<FileSuggestData> suggestion =
        CreateFileSuggestion(path, *item.second, max_recency_);
    if (suggestion) {
      results.push_back(*suggestion);
    }
  }

  base::ranges::sort(results, [](const auto& lhs, const auto& rhs) {
    if ((lhs.modified_time || rhs.modified_time) &&
        lhs.modified_time != rhs.modified_time) {
      return lhs.modified_time.value_or(base::Time()) >
             rhs.modified_time.value_or(base::Time());
    }

    if ((lhs.viewed_time || rhs.viewed_time) &&
        lhs.viewed_time != rhs.viewed_time) {
      return lhs.viewed_time.value_or(base::Time()) >
             rhs.viewed_time.value_or(base::Time());
    }

    return lhs.shared_time.value_or(base::Time()) >
           rhs.shared_time.value_or(base::Time());
  });

  return results;
}

void DriveRecentFileSuggestionProvider::OnRecentFilesSearchesCompleted() {
  drive::DriveIntegrationService* const drive_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile_);
  if (!drive_service || !drive_service->IsMounted()) {
    can_use_cache_ = false;
    query_result_files_by_path_.clear();
    on_drive_results_ready_callback_list_.Notify(
        std::vector<FileSuggestData>());
    NotifySuggestionUpdate(FileSuggestionType::kDriveFile);
    return;
  }

  std::vector<FileSuggestData> results = GetSuggestionsFromLatestQueryResults();

  base::UmaHistogramTimes(
      base::JoinString({kBaseHistogramName, "DurationOnSuccess.Total"}, "."),
      base::Time::Now() - search_start_time_);
  base::UmaHistogramCounts100(
      base::JoinString({kBaseHistogramName, "ItemCount.Total"}, "."),
      results.size());

  for (size_t i = 0; i < results.size(); ++i) {
    if (!results[i].modified_time && !results[i].viewed_time &&
        results[i].shared_time) {
      base::UmaHistogramCounts100(
          base::JoinString({kBaseHistogramName, "FirstSharedSuggestionIndex"},
                           "."),
          i);
      break;
    }
  }

  on_drive_results_ready_callback_list_.Notify(results);
  if (can_use_cache_) {
    NotifySuggestionUpdate(FileSuggestionType::kDriveFile);
  }
}

}  // namespace ash