chromium/chrome/browser/ash/app_list/search/os_settings_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/os_settings_provider.h"

#include <algorithm>
#include <memory>
#include <string>

#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_macros.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/vector_icons/vector_icons.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/webui/ash/settings/search/hierarchy.h"
#include "chrome/browser/ui/webui/ash/settings/search/search_handler.h"
#include "chrome/browser/ui/webui/ash/settings/services/settings_manager/os_settings_manager.h"
#include "chrome/browser/ui/webui/ash/settings/services/settings_manager/os_settings_manager_factory.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "components/services/app_service/public/cpp/app_registry_cache.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/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"

namespace app_list {
namespace {

using SettingsResultPtr = ::ash::settings::mojom::SearchResultPtr;
using SettingsResultType = ::ash::settings::mojom::SearchResultType;
using Setting = chromeos::settings::mojom::Setting;
using Subpage = chromeos::settings::mojom::Subpage;
using Section = chromeos::settings::mojom::Section;

constexpr char kOsSettingsResultPrefix[] = "os-settings://";

constexpr size_t kNumRequestedResults = 5u;

// Various states of the OsSettingsProvider. These values persist to logs.
// Entries should not be renumbered and numeric values should never be reused.
enum class Status {
  kOk = 0,
  // No longer used.
  // kAppServiceUnavailable = 1,
  kNoSettingsIcon = 2,
  kSearchHandlerUnavailable = 3,
  kHierarchyEmpty = 4,
  kNoHierarchy = 5,
  kSettingsAppNotReady = 6,
  kNoAppServiceProxy = 7,
  kMaxValue = kNoAppServiceProxy,
};

void LogStatus(Status status) {
  UMA_HISTOGRAM_ENUMERATION("Apps.AppList.OsSettingsProvider.Error", status);
}

// Various icon-related states at different branches of the OsSettingsProvider.
// These values persist to logs. Entries should not be renumbered and numeric
// values should never be reused.
//
// TODO(b/261867385) this histogram is to investigate the bug that settings
// search results may not appear in launcher search due to the lack of icon. It
// can be removed once the associated bug is resolved.
enum class IconLoadStatus {
  // Construction
  kNoAppServiceProxy = 0,
  kBindOnLoadIconFromConstructor = 1,
  // On App Update
  kBindOnLoadIconFromOnAppUpdate = 2,
  kReadinessUnknown = 3,
  kIconKeyNotChanged = 4,
  // On Load Icon (from Constructor)
  kOkFromConstructor = 5,
  kNoValueFromConstructor = 6,
  kNotStandardFromConstructor = 7,
  // On Load Icon (from OnAppUpdate)
  kOkFromOnAppUpdate = 8,
  kNoValueFromOnAppUpdate = 9,
  kNotStandardFromOnAppUpdate = 10,
  // On App Registry Cache Will Be Destroyed
  kIconExistOnDestroyed = 11,
  kIconNotExistOnDestroyed = 12,
  // On App Update
  kOnAppUpdateGetCalled = 13,
  kMaxValue = kOnAppUpdateGetCalled,
};

void LogIconLoadStatus(IconLoadStatus icon_load_status) {
  UMA_HISTOGRAM_ENUMERATION("Apps.AppList.OsSettingsProvider.IconLoadStatus",
                            icon_load_status);
}

bool ContainsBetterAncestor(Subpage subpage,
                            const double score,
                            const ash::settings::Hierarchy* hierarchy,
                            const base::flat_map<Subpage, double>& subpages,
                            const base::flat_map<Section, double>& sections) {
  // Returns whether or not a higher-scoring ancestor subpage or section of
  // |subpage| is present within |subpages| or |sections|.
  const auto& metadata = hierarchy->GetSubpageMetadata(subpage);

  // Check parent subpage if one exists.
  if (metadata.parent_subpage) {
    const auto it = subpages.find(metadata.parent_subpage);
    if ((it != subpages.end() && it->second >= score) ||
        ContainsBetterAncestor(metadata.parent_subpage.value(), score,
                               hierarchy, subpages, sections)) {
      return true;
    }
  }

  // Check section.
  const auto it = sections.find(metadata.section);
  return it != sections.end() && it->second >= score;
}

bool ContainsBetterAncestor(Setting setting,
                            const double score,
                            const ash::settings::Hierarchy* hierarchy,
                            const base::flat_map<Subpage, double>& subpages,
                            const base::flat_map<Section, double>& sections) {
  // Returns whether or not a higher-scoring ancestor subpage or section of
  // |setting| is present within |subpages| or |sections|.
  const auto& metadata = hierarchy->GetSettingMetadata(setting);

  // Check primary subpage only. Alternate subpages aren't used enough for the
  // check to be worthwhile.
  if (metadata.primary.subpage) {
    const auto parent_subpage = metadata.primary.subpage.value();
    const auto it = subpages.find(parent_subpage);
    if ((it != subpages.end() && it->second >= score) ||
        ContainsBetterAncestor(parent_subpage, score, hierarchy, subpages,
                               sections)) {
      return true;
    }
  }

  // Check section.
  const auto it = sections.find(metadata.primary.section);
  return it != sections.end() && it->second >= score;
}

}  // namespace

OsSettingsResult::OsSettingsResult(Profile* profile,
                                   const SettingsResultPtr& result,
                                   const double relevance_score,
                                   const ui::ImageModel& icon,
                                   const std::u16string& query)
    : profile_(profile), url_path_(result->url_path_with_parameters) {
  set_id(kOsSettingsResultPrefix + url_path_);
  SetCategory(Category::kSettings);
  set_relevance(relevance_score);
  SetTitle(result->canonical_text);
  SetResultType(ResultType::kOsSettings);
  SetDisplayType(DisplayType::kList);
  SetMetricsType(ash::OS_SETTINGS);
  SetIcon(IconInfo(icon, kAppIconDimension));

  // If the result is not a top-level section, set the display text with
  // information about the result's 'parent' category. This is the last element
  // of |result->settings_page_hierarchy|, which is localized and ready for
  // display. Some subpages have the same name as their section (namely,
  // bluetooth), in which case we should leave the details blank.
  const auto& hierarchy = result->settings_page_hierarchy;
  if (hierarchy.empty()) {
    LogStatus(Status::kHierarchyEmpty);
  } else if (result->type != SettingsResultType::kSection) {
    SetDetails(hierarchy.back());
  }

  // Manually build the accessible name for the search result, in a way that
  // parallels the regular accessible names set by
  // SearchResultBaseView::ComputeAccessibleName.
  std::u16string accessible_name = title();
  if (!details().empty()) {
    accessible_name += u", ";
    accessible_name += details();
  }
  accessible_name += u", ";
  // The first element in the settings hierarchy is always the top-level
  // localized name of the Settings app.
  accessible_name += hierarchy[0];
  SetAccessibleName(accessible_name);
}

OsSettingsResult::~OsSettingsResult() = default;

void OsSettingsResult::Open(int event_flags) {
  chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile_,
                                                               url_path_);
}

OsSettingsProvider::OsSettingsProvider(Profile* profile)
    : SearchProvider(SearchCategory::kSettings), 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);
  }
}

OsSettingsProvider::~OsSettingsProvider() = default;

void OsSettingsProvider::MaybeInitialize(
    ash::settings::SearchHandler* fake_search_handler,
    const ash::settings::Hierarchy* fake_hierarchy) {
  // 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();

  // Use fake search handler and hierarchy if provided in tests, or get it from
  // `os_settings_manager`.
  if (fake_search_handler) {
    search_handler_ = fake_search_handler;
    hierarchy_ = fake_hierarchy;
  } else {
    auto* os_settings_manager =
        ash::settings::OsSettingsManagerFactory::GetForProfile(profile_);
    auto* app_service_proxy =
        apps::AppServiceProxyFactory::GetForProfile(profile_);
    if (!os_settings_manager || !app_service_proxy) {
      return;
    }
    search_handler_ = os_settings_manager->search_handler();
    hierarchy_ = os_settings_manager->hierarchy();
  }

  // `search_handler_` can be nullptr in the case that the new OS settings
  // search chrome flag is disabled. If it is, we should effectively disable the
  // search provider.
  if (!search_handler_) {
    LogStatus(Status::kSearchHandlerUnavailable);
    return;
  }

  if (!hierarchy_) {
    LogStatus(Status::kNoHierarchy);
  }

  search_handler_->Observe(
      search_results_observer_receiver_.BindNewPipeAndPassRemote());

  // 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::kOsSettingsIcon, SK_ColorTRANSPARENT, kAppIconDimension);

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

  // TODO(b/261867385): `LoadIcon()` from constructor is removed as it never
  // succeeds and the icon is only updated from "OnAppUpdate()" according to
  // the UMA metrics. We can either remove this comments if this issue is
  // confirmed, or revert the remove if this issue is solved.
  LogIconLoadStatus(IconLoadStatus::kBindOnLoadIconFromConstructor);
}

void OsSettingsProvider::Start(const std::u16string& query) {
  // Disable the provider if:
  //  - the search backend isn't available
  //  - the settings app isn't ready
  //  - we don't have an icon to display with results.
  if (!search_handler_) {
    // 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 os setting search available
    // earlier.
    MaybeInitialize();
    return;
  } else if (icon_.IsEmpty()) {
    LogStatus(Status::kNoSettingsIcon);
    return;
  }

  // Do not return results for queries that are too short, as the results
  // generally aren't meaningful. Note this provider never provides zero-state
  // results.
  if (query.size() < min_query_length_) {
    return;
  }

  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,
      ash::settings::mojom::ParentResultBehavior::kDoNotIncludeParentResults,
      base::BindOnce(&OsSettingsProvider::OnSearchReturned,
                     weak_factory_.GetWeakPtr(), query, start_time));
}

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

ash::AppListSearchResultType OsSettingsProvider::ResultType() const {
  return ash::AppListSearchResultType::kOsSettings;
}

void OsSettingsProvider::OnAppUpdate(const apps::AppUpdate& update) {
  if (update.AppId() != web_app::kOsSettingsAppId) {
    return;
  }

  LogIconLoadStatus(IconLoadStatus::kOnAppUpdateGetCalled);

  // TODO(crbug.com/40125676): We previously disabled this search provider until
  // the app service signalled that the settings app is ready. But this signal
  // is flaky, so sometimes search provider was permanently disabled. Once the
  // signal is reliable, we should re-add the check.

  // Request the Settings app icon when either the readiness or the icon has
  // changed.
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile_);
  if (update.ReadinessChanged() || update.IconKeyChanged()) {
    proxy->LoadIcon(web_app::kOsSettingsAppId, apps::IconType::kStandard,
                    kAppIconDimension,
                    /*allow_placeholder_icon=*/false,
                    base::BindOnce(&OsSettingsProvider::OnLoadIcon,
                                   weak_factory_.GetWeakPtr(),
                                   /*is_from_constructor=*/false));
    LogIconLoadStatus(IconLoadStatus::kBindOnLoadIconFromOnAppUpdate);
  } else {
    if (!update.ReadinessChanged()) {
      LogIconLoadStatus(IconLoadStatus::kReadinessUnknown);
    }
    if (!update.IconKeyChanged()) {
      LogIconLoadStatus(IconLoadStatus::kIconKeyNotChanged);
    }
  }
}

void OsSettingsProvider::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
  if (icon_.IsEmpty()) {
    LogIconLoadStatus(IconLoadStatus::kIconNotExistOnDestroyed);
  } else {
    LogIconLoadStatus(IconLoadStatus::kIconExistOnDestroyed);
  }
}

void OsSettingsProvider::OnSearchResultsChanged() {
  if (last_query_.empty()) {
    return;
  }

  Start(last_query_);
}

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

void OsSettingsProvider::OnSearchReturned(
    const std::u16string& query,
    const base::TimeTicks& start_time,
    std::vector<SettingsResultPtr> sorted_results) {
  DCHECK_LE(sorted_results.size(), kNumRequestedResults);

  SearchProvider::Results search_results;

  for (const auto& result : FilterResults(query, sorted_results, hierarchy_)) {
    search_results.emplace_back(std::make_unique<OsSettingsResult>(
        profile_, result, result->relevance_score, icon_, last_query_));
  }

  UMA_HISTOGRAM_TIMES("Apps.AppList.OsSettingsProvider.QueryTime",
                      base::TimeTicks::Now() - start_time);
  // Log the OS setting search has been successfully proceeded.
  LogStatus(Status::kOk);
  SwapResults(&search_results);
}

std::vector<SettingsResultPtr> OsSettingsProvider::FilterResults(
    const std::u16string& query,
    const std::vector<SettingsResultPtr>& results,
    const ash::settings::Hierarchy* hierarchy) {
  base::flat_set<std::string> seen_urls;
  base::flat_map<Subpage, double> seen_subpages;
  base::flat_map<Section, double> seen_sections;
  std::vector<SettingsResultPtr> clean_results;

  for (const SettingsResultPtr& result : results) {
    // Filter results below the score threshold.
    if (result->relevance_score < min_score_) {
      continue;
    }

    // Check if query matched alternate text for the result. If so, only allow
    // results meeting extra requirements. Perform this check before checking
    // for duplicates to ensure a rejected alternate result doesn't preclude a
    // canonical result with a lower score from being shown.
    if (result->text != result->canonical_text &&
        (!accept_alternate_matches_ ||
         query.size() < min_query_length_for_alternates_ ||
         result->relevance_score < min_score_for_alternates_)) {
      continue;
    }

    // Check if URL has been seen.
    const std::string url = result->url_path_with_parameters;
    const auto it = seen_urls.find(url);
    if (it != seen_urls.end()) {
      continue;
    }

    seen_urls.insert(url);
    clean_results.push_back(result.Clone());
    if (result->type == SettingsResultType::kSubpage) {
      seen_subpages.insert(
          std::make_pair(result->id->get_subpage(), result->relevance_score));
    }
    if (result->type == SettingsResultType::kSection) {
      seen_sections.insert(
          std::make_pair(result->id->get_section(), result->relevance_score));
    }
  }

  // Iterate through the clean results a second time. Remove subpage or setting
  // results that have a higher-scoring ancestor subpage or section also present
  // in the results.
  for (size_t i = 0; i < clean_results.size(); ++i) {
    const auto& result = clean_results[i];
    if ((result->type == SettingsResultType::kSubpage &&
         ContainsBetterAncestor(result->id->get_subpage(),
                                result->relevance_score, hierarchy_,
                                seen_subpages, seen_sections)) ||
        (result->type == SettingsResultType::kSetting &&
         ContainsBetterAncestor(result->id->get_setting(),
                                result->relevance_score, hierarchy_,
                                seen_subpages, seen_sections))) {
      clean_results.erase(clean_results.begin() + i);
      --i;
    }
  }

  return clean_results;
}

void OsSettingsProvider::OnLoadIcon(bool is_from_constructor,
                                    apps::IconValuePtr icon_value) {
  if (icon_value && icon_value->icon_type == apps::IconType::kStandard) {
    icon_ = ui::ImageModel::FromImageSkia(icon_value->uncompressed);
    LogIconLoadStatus(is_from_constructor ? IconLoadStatus::kOkFromConstructor
                                          : IconLoadStatus::kOkFromOnAppUpdate);
  } else if (!icon_value) {
    LogIconLoadStatus(is_from_constructor
                          ? IconLoadStatus::kNoValueFromConstructor
                          : IconLoadStatus::kNoValueFromOnAppUpdate);
  } else {
    LogIconLoadStatus(is_from_constructor
                          ? IconLoadStatus::kNotStandardFromConstructor
                          : IconLoadStatus::kNotStandardFromOnAppUpdate);
  }
}

}  // namespace app_list