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