chromium/chrome/browser/ash/app_list/search/help_app_provider.cc

// Copyright 2020 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/help_app_provider.h"

#include <memory>

#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_metrics.h"
#include "ash/webui/help_app_ui/help_app_manager.h"
#include "ash/webui/help_app_ui/help_app_manager_factory.h"
#include "ash/webui/help_app_ui/search/search_handler.h"
#include "ash/webui/help_app_ui/url_constants.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "chrome/browser/ash/app_list/vector_icons/vector_icons.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/core/session_manager_observer.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/paint_vector_icon.h"
#include "url/gurl.h"

namespace app_list {
namespace {

constexpr size_t kMinQueryLength = 3u;
constexpr double kMinScore = 0.4;
constexpr size_t kNumRequestedResults = 5u;

// The end result of a list search. Logged once per time a list search finishes.
// Not logged if the search is canceled by a new search starting. Not logged for
// suggestion chips. These values persist to logs. Entries should not be
// renumbered and numeric values should never be reused.
enum class ListSearchResultState {
  // Search finished with no problems.
  kOk = 0,
  // Search canceled because no help app icon.
  kNoHelpAppIcon = 1,
  // Search canceled because the search backend isn't available.
  kSearchHandlerUnavailable = 2,
  kMaxValue = kSearchHandlerUnavailable,
};

// Use this when a list search finishes.
void LogListSearchResultState(ListSearchResultState state) {
  base::UmaHistogramEnumeration(
      "Apps.AppList.HelpAppProvider.ListSearchResultState", state);
}

}  // namespace

HelpAppResult::HelpAppResult(
    const float& relevance,
    Profile* profile,
    const ash::help_app::mojom::SearchResultPtr& result,
    const ui::ImageModel& icon,
    const std::u16string& query)
    : profile_(profile),
      url_path_(result->url_path_with_parameters),
      help_app_content_id_(result->id) {
  DCHECK(profile_);
  set_id(ash::kChromeUIHelpAppURL + url_path_);
  set_relevance(relevance);
  SetTitle(result->title);
  SetCategory(Category::kHelp);
  SetResultType(ResultType::kHelpApp);
  SetDisplayType(DisplayType::kList);
  SetMetricsType(ash::HELP_APP_DEFAULT);
  SetIcon(IconInfo(icon, kAppIconDimension));
  SetDetails(result->main_category);
}

HelpAppResult::~HelpAppResult() = default;

void HelpAppResult::Open(int event_flags) {
  // This is a google-internal histogram. If changing this, also change the
  // corresponding histograms file.
  base::UmaHistogramSparse("Discover.LauncherSearch.ContentLaunched",
                           base::PersistentHash(help_app_content_id_));

  // Note: event_flags is ignored, LaunchSWA doesn't need it.
  // Launch list result.
  ash::SystemAppLaunchParams params;
  params.url = GURL(ash::kChromeUIHelpAppURL + url_path_);
  params.launch_source = apps::LaunchSource::kFromAppListQuery;
  ash::LaunchSystemWebAppAsync(
      profile_, ash::SystemWebAppType::HELP, params,
      std::make_unique<apps::WindowInfo>(display::kDefaultDisplayId));
}

HelpAppProvider::HelpAppProvider(Profile* profile)
    : SearchProvider(SearchCategory::kHelp), profile_(profile) {
  CHECK(profile_);

  auto* session_manager = session_manager::SessionManager::Get();
  if (session_manager->IsUserSessionStartUpTaskCompleted()) {
    // If user session start up task has completed, the initialization can
    // start.
    MaybeInitialize();
  } else {
    // Wait for the user session start up task completion to prioritize
    // resources for them.
    session_manager_observation_.Observe(session_manager);
  }
}

HelpAppProvider::~HelpAppProvider() = default;

void HelpAppProvider::MaybeInitialize(
    ash::help_app::SearchHandler* fake_search_handler) {
  // Ensures that the provider can be initialized once only.
  if (has_initialized) {
    return;
  }
  has_initialized = true;

  // Initialization is happening, so we no longer need to wait for user session
  // start up task completion.
  session_manager_observation_.Reset();

  app_registry_cache_observer_.Observe(
      &apps::AppServiceProxyFactory::GetForProfile(profile_)
           ->AppRegistryCache());
  LoadIcon();

  // TODO(b/261867385): We manually load the icon from the local codebase as
  // the icon load from proxy is flaky. When the flakiness if solved, we can
  // safely remove this.
  icon_ = ui::ImageModel::FromVectorIcon(
      app_list::kHelpAppIcon, SK_ColorTRANSPARENT, kAppIconDimension);

  // Use fake search handler if provided in tests, or get it from
  // `help_app_manager`.
  if (fake_search_handler) {
    search_handler_ = fake_search_handler;
  } else {
    auto* help_app_manager =
        ash::help_app::HelpAppManagerFactory::GetForBrowserContext(profile_);
    CHECK(help_app_manager);
    search_handler_ = help_app_manager->search_handler();
  }

  if (!search_handler_) {
    return;
  }
  search_handler_->OnProfileDirAvailable(profile_->GetPath());
  search_handler_->Observe(
      search_results_observer_receiver_.BindNewPipeAndPassRemote());
}

void HelpAppProvider::Start(const std::u16string& query) {
  if (query.size() < kMinQueryLength) {
    // Do not do a list search for queries that are too short because the
    // results generally aren't meaningful. This isn't worth logging as a list
    // search result case because it happens frequently when entering a new
    // search query.
    return;
  }

  // Stop the search if:
  //  - the search backend isn't available (or the feature is disabled)
  //  - we don't have an icon to display with results.
  if (!search_handler_) {
    LogListSearchResultState(ListSearchResultState::kSearchHandlerUnavailable);
    // If user has started to user launcher search before the user session
    // startup tasks completed, we should honor this user action and
    // initialize the provider. It makes the help app search available
    // earlier.
    MaybeInitialize();
    return;
  } else if (icon_.IsEmpty()) {
    LogListSearchResultState(ListSearchResultState::kNoHelpAppIcon);
    // This prevents a timeout in the test, but it does not change the user
    // experience because the results were already cleared at the start.
    SearchProvider::Results search_results;
    SwapResults(&search_results);
    return;
  }

  // Start a search for list results.
  const base::TimeTicks start_time = base::TimeTicks::Now();
  last_query_ = query;

  // Invalidate weak pointers to cancel existing searches.
  weak_factory_.InvalidateWeakPtrs();
  search_handler_->Search(
      query, kNumRequestedResults,
      base::BindOnce(&HelpAppProvider::OnSearchReturned,
                     weak_factory_.GetWeakPtr(), query, start_time));
}

void HelpAppProvider::StopQuery() {
  last_query_.clear();
  // Invalidate weak pointers to cancel existing searches.
  weak_factory_.InvalidateWeakPtrs();
}

void HelpAppProvider::OnSearchReturned(
    const std::u16string& query,
    const base::TimeTicks& start_time,
    std::vector<ash::help_app::mojom::SearchResultPtr> sorted_results) {
  DCHECK_LE(sorted_results.size(), kNumRequestedResults);

  SearchProvider::Results search_results;
  for (const auto& result : sorted_results) {
    if (result->relevance_score < kMinScore) {
      continue;
    }

    search_results.emplace_back(std::make_unique<HelpAppResult>(
        result->relevance_score, profile_, result, icon_, last_query_));
  }

  base::UmaHistogramTimes("Apps.AppList.HelpAppProvider.QueryTime",
                          base::TimeTicks::Now() - start_time);
  LogListSearchResultState(ListSearchResultState::kOk);
  SwapResults(&search_results);
}

ash::AppListSearchResultType HelpAppProvider::ResultType() const {
  return ash::AppListSearchResultType::kHelpApp;
}

void HelpAppProvider::OnAppUpdate(const apps::AppUpdate& update) {
  if (update.AppId() == web_app::kHelpAppId && update.ReadinessChanged() &&
      update.Readiness() == apps::Readiness::kReady) {
    LoadIcon();
  }
}

void HelpAppProvider::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
}

// If the availability of search results changed, start a new search.
void HelpAppProvider::OnSearchResultAvailabilityChanged() {
  if (last_query_.empty()) {
    return;
  }
  Start(last_query_);
}

void HelpAppProvider::OnUserSessionStartUpTaskCompleted() {
  MaybeInitialize();
}

void HelpAppProvider::OnLoadIcon(apps::IconValuePtr icon_value) {
  if (icon_value && icon_value->icon_type == apps::IconType::kStandard) {
    icon_ = ui::ImageModel::FromImageSkia(icon_value->uncompressed);
  }
}

void HelpAppProvider::LoadIcon() {
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
  proxy->LoadIcon(
      web_app::kHelpAppId, apps::IconType::kStandard,
      ash::SharedAppListConfig::instance().suggestion_chip_icon_dimension(),
      /*allow_placeholder_icon=*/false,
      base::BindOnce(&HelpAppProvider::OnLoadIcon, weak_factory_.GetWeakPtr()));
}

}  // namespace app_list