1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/ui/app_list/search/os_settings_provider.h"
6 
7 #include <algorithm>
8 #include <memory>
9 #include <string>
10 
11 #include "ash/public/cpp/app_list/app_list_config.h"
12 #include "ash/public/cpp/app_list/app_list_features.h"
13 #include "base/containers/flat_set.h"
14 #include "base/macros.h"
15 #include "base/metrics/field_trial_params.h"
16 #include "base/metrics/histogram_macros.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "chrome/browser/apps/app_service/app_service_proxy.h"
20 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
21 #include "chrome/browser/chromeos/web_applications/default_web_app_ids.h"
22 #include "chrome/browser/profiles/profile.h"
23 #include "chrome/browser/ui/settings_window_manager_chromeos.h"
24 #include "chrome/browser/ui/webui/settings/chromeos/hierarchy.h"
25 #include "chrome/browser/ui/webui/settings/chromeos/os_settings_manager.h"
26 #include "chrome/browser/ui/webui/settings/chromeos/os_settings_manager_factory.h"
27 #include "chrome/browser/ui/webui/settings/chromeos/search/search_handler.h"
28 #include "chrome/common/chrome_features.h"
29 #include "ui/gfx/image/image_skia.h"
30 #include "url/gurl.h"
31 
32 namespace app_list {
33 namespace {
34 
35 using SettingsResultPtr = chromeos::settings::mojom::SearchResultPtr;
36 using SettingsResultType = chromeos::settings::mojom::SearchResultType;
37 using Setting = chromeos::settings::mojom::Setting;
38 using Subpage = chromeos::settings::mojom::Subpage;
39 using Section = chromeos::settings::mojom::Section;
40 
41 constexpr char kOsSettingsResultPrefix[] = "os-settings://";
42 constexpr float kScoreEps = 1e-5f;
43 
44 constexpr size_t kNumRequestedResults = 5u;
45 constexpr size_t kMaxShownResults = 2u;
46 
47 // Various error states of the OsSettingsProvider. kOk is currently not emitted,
48 // but may be used in future. These values persist to logs. Entries should not
49 // be renumbered and numeric values should never be reused.
50 enum class Error {
51   kOk = 0,
52   // No longer used.
53   // kAppServiceUnavailable = 1,
54   kNoSettingsIcon = 2,
55   kSearchHandlerUnavailable = 3,
56   kHierarchyEmpty = 4,
57   kNoHierarchy = 5,
58   kSettingsAppNotReady = 6,
59   kMaxValue = kSettingsAppNotReady,
60 };
61 
LogError(Error error)62 void LogError(Error error) {
63   UMA_HISTOGRAM_ENUMERATION("Apps.AppList.OsSettingsProvider.Error", error);
64 }
65 
ContainsAncestor(Subpage subpage,const chromeos::settings::Hierarchy * hierarchy,const base::flat_set<Subpage> & subpages,const base::flat_set<Section> & sections)66 bool ContainsAncestor(Subpage subpage,
67                       const chromeos::settings::Hierarchy* hierarchy,
68                       const base::flat_set<Subpage>& subpages,
69                       const base::flat_set<Section>& sections) {
70   // Returns whether or not an ancestor subpage or section of |subpage| is
71   // present within |subpages| or |sections|.
72   const auto& metadata = hierarchy->GetSubpageMetadata(subpage);
73 
74   // Check parent subpage if one exists.
75   if (metadata.parent_subpage) {
76     const auto it = subpages.find(metadata.parent_subpage);
77     if (it != subpages.end() ||
78         ContainsAncestor(metadata.parent_subpage.value(), hierarchy, subpages,
79                          sections))
80       return true;
81   }
82 
83   // Check section.
84   const auto it = sections.find(metadata.section);
85   return it != sections.end();
86 }
87 
ContainsAncestor(Setting setting,const chromeos::settings::Hierarchy * hierarchy,const base::flat_set<Subpage> & subpages,const base::flat_set<Section> & sections)88 bool ContainsAncestor(Setting setting,
89                       const chromeos::settings::Hierarchy* hierarchy,
90                       const base::flat_set<Subpage>& subpages,
91                       const base::flat_set<Section>& sections) {
92   // Returns whether or not an ancestor subpage or section of |setting| is
93   // present within |subpages| or |sections|.
94   const auto& metadata = hierarchy->GetSettingMetadata(setting);
95 
96   // Check primary subpage only. Alternate subpages aren't used enough for the
97   // check to be worthwhile.
98   if (metadata.primary.second) {
99     const auto parent_subpage = metadata.primary.second.value();
100     const auto it = subpages.find(parent_subpage);
101     if (it != subpages.end() ||
102         ContainsAncestor(parent_subpage, hierarchy, subpages, sections))
103       return true;
104   }
105 
106   // Check section.
107   const auto it = sections.find(metadata.primary.first);
108   return it != sections.end();
109 }
110 
111 }  // namespace
112 
OsSettingsResult(Profile * profile,const chromeos::settings::mojom::SearchResultPtr & result,const float relevance_score,const gfx::ImageSkia & icon)113 OsSettingsResult::OsSettingsResult(
114     Profile* profile,
115     const chromeos::settings::mojom::SearchResultPtr& result,
116     const float relevance_score,
117     const gfx::ImageSkia& icon)
118     : profile_(profile), url_path_(result->url_path_with_parameters) {
119   set_id(kOsSettingsResultPrefix + url_path_);
120   set_relevance(relevance_score);
121   SetTitle(result->canonical_result_text);
122   SetResultType(ResultType::kOsSettings);
123   SetDisplayType(DisplayType::kList);
124   SetMetricsType(ash::OS_SETTINGS);
125   SetIcon(icon);
126 
127   // If the result is not a top-level section, set the display text with
128   // information about the result's 'parent' category. This is the last element
129   // of |result->settings_page_hierarchy|, which is localized and ready for
130   // display. Some subpages have the same name as their section (namely,
131   // bluetooth), in which case we should leave the details blank.
132   const auto& hierarchy = result->settings_page_hierarchy;
133   if (hierarchy.empty()) {
134     LogError(Error::kHierarchyEmpty);
135   } else if (result->type != SettingsResultType::kSection) {
136     SetDetails(hierarchy.back());
137   }
138 
139   // Manually build the accessible name for the search result, in a way that
140   // parallels the regular accessible names set by
141   // SearchResultBaseView::ComputeAccessibleName.
142   base::string16 accessible_name = title();
143   if (!details().empty()) {
144     accessible_name += base::ASCIIToUTF16(", ");
145     accessible_name += details();
146   }
147   accessible_name += base::ASCIIToUTF16(", ");
148   // The first element in the settings hierarchy is always the top-level
149   // localized name of the Settings app.
150   accessible_name += hierarchy[0];
151   SetAccessibleName(accessible_name);
152 }
153 
154 OsSettingsResult::~OsSettingsResult() = default;
155 
Open(int event_flags)156 void OsSettingsResult::Open(int event_flags) {
157   chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile_,
158                                                                url_path_);
159 }
160 
OsSettingsProvider(Profile * profile)161 OsSettingsProvider::OsSettingsProvider(Profile* profile)
162     : profile_(profile),
163       settings_manager_(
164           chromeos::settings::OsSettingsManagerFactory::GetForProfile(
165               profile)) {
166   DCHECK(profile_);
167 
168   if (settings_manager_) {
169     search_handler_ = settings_manager_->search_handler();
170     hierarchy_ = settings_manager_->hierarchy();
171   }
172 
173   // |search_handler_| can be nullptr in the case that the new OS settings
174   // search chrome flag is disabled. If it is, we should effectively disable the
175   // search provider.
176   if (!search_handler_) {
177     LogError(Error::kSearchHandlerUnavailable);
178     return;
179   }
180 
181   if (!hierarchy_) {
182     LogError(Error::kNoHierarchy);
183   }
184 
185   search_handler_->Observe(
186       search_results_observer_receiver_.BindNewPipeAndPassRemote());
187 
188   app_service_proxy_ = apps::AppServiceProxyFactory::GetForProfile(profile_);
189   Observe(&app_service_proxy_->AppRegistryCache());
190   auto icon_type =
191       (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon))
192           ? apps::mojom::IconType::kStandard
193           : apps::mojom::IconType::kUncompressed;
194   app_service_proxy_->LoadIcon(
195       apps::mojom::AppType::kWeb, chromeos::default_web_apps::kOsSettingsAppId,
196       icon_type, ash::AppListConfig::instance().search_list_icon_dimension(),
197       /*allow_placeholder_icon=*/false,
198       base::BindOnce(&OsSettingsProvider::OnLoadIcon,
199                      weak_factory_.GetWeakPtr()));
200 
201   // Set parameters from Finch. Reasonable defaults are set in the header.
202   accept_alternate_matches_ = base::GetFieldTrialParamByFeatureAsBool(
203       app_list_features::kLauncherSettingsSearch, "accept_alternate_matches",
204       accept_alternate_matches_);
205   min_query_length_ = base::GetFieldTrialParamByFeatureAsInt(
206       app_list_features::kLauncherSettingsSearch, "min_query_length",
207       min_query_length_);
208   min_query_length_for_alternates_ = base::GetFieldTrialParamByFeatureAsInt(
209       app_list_features::kLauncherSettingsSearch,
210       "min_query_length_for_alternates", min_query_length_for_alternates_);
211   min_score_ = base::GetFieldTrialParamByFeatureAsDouble(
212       app_list_features::kLauncherSettingsSearch, "min_score", min_score_);
213   min_score_for_alternates_ = base::GetFieldTrialParamByFeatureAsDouble(
214       app_list_features::kLauncherSettingsSearch, "min_score_for_alternates",
215       min_score_for_alternates_);
216 }
217 
218 OsSettingsProvider::~OsSettingsProvider() = default;
219 
ResultType()220 ash::AppListSearchResultType OsSettingsProvider::ResultType() {
221   return ash::AppListSearchResultType::kOsSettings;
222 }
223 
Start(const base::string16 & query)224 void OsSettingsProvider::Start(const base::string16& query) {
225   const base::TimeTicks start_time = base::TimeTicks::Now();
226   last_query_ = query;
227   // Disable the provider if:
228   //  - the search backend isn't available
229   //  - the settings app isn't ready
230   //  - we don't have an icon to display with results.
231   if (!search_handler_) {
232     return;
233   } else if (!settings_app_ready_) {
234     LogError(Error::kSettingsAppNotReady);
235     return;
236   } else if (icon_.isNull()) {
237     LogError(Error::kNoSettingsIcon);
238     return;
239   }
240 
241   ClearResultsSilently();
242 
243   // Do not return results for queries that are too short, as the results
244   // generally aren't meaningful. Note this provider never provides zero-state
245   // results.
246   if (query.size() < min_query_length_)
247     return;
248 
249   // Invalidate weak pointers to cancel existing searches.
250   weak_factory_.InvalidateWeakPtrs();
251   search_handler_->Search(
252       query, kNumRequestedResults,
253       chromeos::settings::mojom::ParentResultBehavior::
254           kDoNotIncludeParentResults,
255       base::BindOnce(&OsSettingsProvider::OnSearchReturned,
256                      weak_factory_.GetWeakPtr(), query, start_time));
257 }
258 
ViewClosing()259 void OsSettingsProvider::ViewClosing() {
260   last_query_.clear();
261 }
262 
OnSearchReturned(const base::string16 & query,const base::TimeTicks & start_time,std::vector<chromeos::settings::mojom::SearchResultPtr> sorted_results)263 void OsSettingsProvider::OnSearchReturned(
264     const base::string16& query,
265     const base::TimeTicks& start_time,
266     std::vector<chromeos::settings::mojom::SearchResultPtr> sorted_results) {
267   // TODO(crbug.com/1068851): We are currently not ranking settings results.
268   // Instead, we are gluing at most two to the top of the search box. Consider
269   // ranking these with other results in the next version of the feature.
270   DCHECK_LE(sorted_results.size(), kNumRequestedResults);
271 
272   SearchProvider::Results search_results;
273   int i = 0;
274   for (const auto& result : FilterResults(query, sorted_results, hierarchy_)) {
275     const float score = 1.0f - i * kScoreEps;
276     search_results.emplace_back(
277         std::make_unique<OsSettingsResult>(profile_, result, score, icon_));
278     ++i;
279   }
280 
281   UMA_HISTOGRAM_TIMES("Apps.AppList.OsSettingsProvider.QueryTime",
282                       base::TimeTicks::Now() - start_time);
283   SwapResults(&search_results);
284 }
285 
OnAppUpdate(const apps::AppUpdate & update)286 void OsSettingsProvider::OnAppUpdate(const apps::AppUpdate& update) {
287   if (update.AppId() != chromeos::default_web_apps::kOsSettingsAppId)
288     return;
289 
290   // Request the Settings app icon when either the readiness is changed to
291   // kReady, or the icon has been updated, signalled by IconKeyChanged.
292   bool update_icon = false;
293   if (update.ReadinessChanged()) {
294     settings_app_ready_ = update.Readiness() == apps::mojom::Readiness::kReady;
295     if (settings_app_ready_)
296       update_icon = true;
297   } else if (update.IconKeyChanged()) {
298     update_icon = true;
299   }
300 
301   if (update_icon) {
302     auto icon_type =
303         (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon))
304             ? apps::mojom::IconType::kStandard
305             : apps::mojom::IconType::kUncompressed;
306     app_service_proxy_->LoadIcon(
307         apps::mojom::AppType::kWeb,
308         chromeos::default_web_apps::kOsSettingsAppId, icon_type,
309         ash::AppListConfig::instance().search_list_icon_dimension(),
310         /*allow_placeholder_icon=*/false,
311         base::BindOnce(&OsSettingsProvider::OnLoadIcon,
312                        weak_factory_.GetWeakPtr()));
313   }
314 }
315 
OnAppRegistryCacheWillBeDestroyed(apps::AppRegistryCache * cache)316 void OsSettingsProvider::OnAppRegistryCacheWillBeDestroyed(
317     apps::AppRegistryCache* cache) {
318   Observe(nullptr);
319 }
320 
OnSearchResultAvailabilityChanged()321 void OsSettingsProvider::OnSearchResultAvailabilityChanged() {
322   if (last_query_.empty())
323     return;
324 
325   Start(last_query_);
326 }
327 
328 std::vector<chromeos::settings::mojom::SearchResultPtr>
FilterResults(const base::string16 & query,const std::vector<chromeos::settings::mojom::SearchResultPtr> & results,const chromeos::settings::Hierarchy * hierarchy)329 OsSettingsProvider::FilterResults(
330     const base::string16& query,
331     const std::vector<chromeos::settings::mojom::SearchResultPtr>& results,
332     const chromeos::settings::Hierarchy* hierarchy) {
333   base::flat_set<std::string> seen_urls;
334   base::flat_set<Subpage> seen_subpages;
335   base::flat_set<Section> seen_sections;
336   std::vector<SettingsResultPtr> clean_results;
337 
338   for (const SettingsResultPtr& result : results) {
339     // Filter results below the score threshold.
340     if (result->relevance_score < min_score_) {
341       continue;
342     }
343 
344     // Check if query matched alternate text for the result. If so, only allow
345     // results meeting extra requirements. Perform this check before checking
346     // for duplicates to ensure a rejected alternate result doesn't preclude a
347     // canonical result with a lower score from being shown.
348     if (result->result_text != result->canonical_result_text &&
349         (!accept_alternate_matches_ ||
350          query.size() < min_query_length_for_alternates_ ||
351          result->relevance_score < min_score_for_alternates_)) {
352       continue;
353     }
354 
355     // Check if URL has been seen.
356     const std::string url = result->url_path_with_parameters;
357     const auto it = seen_urls.find(url);
358     if (it != seen_urls.end())
359       continue;
360 
361     seen_urls.insert(url);
362     clean_results.push_back(result.Clone());
363     if (result->type == SettingsResultType::kSubpage)
364       seen_subpages.insert(result->id->get_subpage());
365     if (result->type == SettingsResultType::kSection)
366       seen_sections.insert(result->id->get_section());
367   }
368 
369   // Iterate through the clean results a second time. Remove subpage or setting
370   // results that have an ancestor subpage or section also present in the
371   // results.
372   for (size_t i = 0; i < clean_results.size(); ++i) {
373     const auto& result = clean_results[i];
374     if ((result->type == SettingsResultType::kSubpage &&
375          ContainsAncestor(result->id->get_subpage(), hierarchy_, seen_subpages,
376                           seen_sections)) ||
377         (result->type == SettingsResultType::kSetting &&
378          ContainsAncestor(result->id->get_setting(), hierarchy_, seen_subpages,
379                           seen_sections))) {
380       clean_results.erase(clean_results.begin() + i);
381       --i;
382     }
383   }
384 
385   if (clean_results.size() > static_cast<size_t>(kMaxShownResults))
386     clean_results.resize(kMaxShownResults);
387   return clean_results;
388 }
389 
OnLoadIcon(apps::mojom::IconValuePtr icon_value)390 void OsSettingsProvider::OnLoadIcon(apps::mojom::IconValuePtr icon_value) {
391   auto icon_type =
392       (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon))
393           ? apps::mojom::IconType::kStandard
394           : apps::mojom::IconType::kUncompressed;
395   if (icon_value->icon_type == icon_type) {
396     icon_ = icon_value->uncompressed;
397   }
398 }
399 
400 }  // namespace app_list
401