1 // Copyright 2018 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/app_list_client_impl.h"
6
7 #include <stddef.h>
8
9 #include <utility>
10 #include <vector>
11
12 #include "ash/public/cpp/app_list/app_list_controller.h"
13 #include "ash/public/cpp/new_window_delegate.h"
14 #include "ash/public/cpp/tablet_mode.h"
15 #include "base/bind.h"
16 #include "base/metrics/histogram_functions.h"
17 #include "base/metrics/histogram_macros.h"
18 #include "base/metrics/user_metrics.h"
19 #include "base/strings/strcat.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "chrome/browser/search_engines/template_url_service_factory.h"
23 #include "chrome/browser/ui/app_list/app_list_controller_delegate.h"
24 #include "chrome/browser/ui/app_list/app_list_model_updater.h"
25 #include "chrome/browser/ui/app_list/app_list_notifier_impl.h"
26 #include "chrome/browser/ui/app_list/app_list_syncable_service.h"
27 #include "chrome/browser/ui/app_list/app_list_syncable_service_factory.h"
28 #include "chrome/browser/ui/app_list/app_sync_ui_state_watcher.h"
29 #include "chrome/browser/ui/app_list/search/app_result.h"
30 #include "chrome/browser/ui/app_list/search/chrome_search_result.h"
31 #include "chrome/browser/ui/app_list/search/cros_action_history/cros_action_recorder.h"
32 #include "chrome/browser/ui/app_list/search/search_controller.h"
33 #include "chrome/browser/ui/app_list/search/search_controller_factory.h"
34 #include "chrome/browser/ui/app_list/search/search_result_ranker/app_launch_data.h"
35 #include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h"
36 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
37 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_util.h"
38 #include "chrome/browser/ui/browser_commands.h"
39 #include "chrome/browser/ui/browser_navigator.h"
40 #include "chrome/browser/ui/browser_navigator_params.h"
41 #include "extensions/common/extension.h"
42 #include "ui/display/display.h"
43 #include "ui/display/screen.h"
44 #include "ui/display/types/display_constants.h"
45 #include "ui/gfx/geometry/rect.h"
46
47 namespace {
48
49 AppListClientImpl* g_app_list_client_instance = nullptr;
50
IsTabletMode()51 bool IsTabletMode() {
52 return ash::TabletMode::Get() && ash::TabletMode::Get()->InTabletMode();
53 }
54
55 } // namespace
56
AppListClientImpl()57 AppListClientImpl::AppListClientImpl()
58 : app_list_controller_(ash::AppListController::Get()),
59 app_list_notifier_(
60 std::make_unique<AppListNotifierImpl>(app_list_controller_)) {
61 app_list_controller_->SetClient(this);
62 user_manager::UserManager::Get()->AddSessionStateObserver(this);
63
64 DCHECK(!g_app_list_client_instance);
65 g_app_list_client_instance = this;
66 }
67
~AppListClientImpl()68 AppListClientImpl::~AppListClientImpl() {
69 SetProfile(nullptr);
70
71 user_manager::UserManager::Get()->RemoveSessionStateObserver(this);
72
73 DCHECK_EQ(this, g_app_list_client_instance);
74 g_app_list_client_instance = nullptr;
75
76 if (app_list_controller_)
77 app_list_controller_->SetClient(nullptr);
78 }
79
80 // static
GetInstance()81 AppListClientImpl* AppListClientImpl::GetInstance() {
82 return g_app_list_client_instance;
83 }
84
OnAppListControllerDestroyed()85 void AppListClientImpl::OnAppListControllerDestroyed() {
86 // |app_list_controller_| could be released earlier, e.g. starting a kiosk
87 // next session.
88 app_list_controller_ = nullptr;
89 if (current_model_updater_)
90 current_model_updater_->SetActive(false);
91 }
92
StartSearch(const base::string16 & trimmed_query)93 void AppListClientImpl::StartSearch(const base::string16& trimmed_query) {
94 if (search_controller_) {
95 search_controller_->Start(trimmed_query);
96 OnSearchStarted();
97 }
98 }
99
OpenSearchResult(const std::string & result_id,int event_flags,ash::AppListLaunchedFrom launched_from,ash::AppListLaunchType launch_type,int suggestion_index,bool launch_as_default)100 void AppListClientImpl::OpenSearchResult(const std::string& result_id,
101 int event_flags,
102 ash::AppListLaunchedFrom launched_from,
103 ash::AppListLaunchType launch_type,
104 int suggestion_index,
105 bool launch_as_default) {
106 if (!search_controller_)
107 return;
108
109 ChromeSearchResult* result = search_controller_->FindSearchResult(result_id);
110 if (!result)
111 return;
112
113 app_list::AppLaunchData app_launch_data;
114 app_launch_data.id = result_id;
115 app_launch_data.ranking_item_type =
116 app_list::RankingItemTypeFromSearchResult(*result);
117 app_launch_data.launch_type = launch_type;
118 app_launch_data.launched_from = launched_from;
119 app_launch_data.suggestion_index = suggestion_index;
120
121 if (launch_type == ash::AppListLaunchType::kAppSearchResult &&
122 launched_from == ash::AppListLaunchedFrom::kLaunchedFromSearchBox &&
123 app_launch_data.ranking_item_type == app_list::RankingItemType::kApp &&
124 search_controller_->GetLastQueryLength() != 0) {
125 ash::RecordSuccessfulAppLaunchUsingSearch(
126 launched_from, search_controller_->GetLastQueryLength());
127 }
128
129 // Send training signal to search controller.
130 search_controller_->Train(std::move(app_launch_data));
131
132 RecordSearchResultOpenTypeHistogram(launched_from, result->metrics_type(),
133 IsTabletMode());
134
135 if (launch_as_default)
136 RecordDefaultSearchResultOpenTypeHistogram(result->metrics_type());
137
138 if (!search_controller_->GetLastQueryLength() &&
139 launched_from == ash::AppListLaunchedFrom::kLaunchedFromSearchBox)
140 RecordZeroStateSuggestionOpenTypeHistogram(result->metrics_type());
141
142 // OpenResult may cause |result| to be deleted.
143 search_controller_->OpenResult(result, event_flags);
144 }
145
InvokeSearchResultAction(const std::string & result_id,int action_index)146 void AppListClientImpl::InvokeSearchResultAction(const std::string& result_id,
147 int action_index) {
148 if (!search_controller_)
149 return;
150 ChromeSearchResult* result = search_controller_->FindSearchResult(result_id);
151 if (result)
152 search_controller_->InvokeResultAction(result, action_index);
153 }
154
GetSearchResultContextMenuModel(const std::string & result_id,GetContextMenuModelCallback callback)155 void AppListClientImpl::GetSearchResultContextMenuModel(
156 const std::string& result_id,
157 GetContextMenuModelCallback callback) {
158 if (!search_controller_) {
159 std::move(callback).Run(nullptr);
160 return;
161 }
162 ChromeSearchResult* result = search_controller_->FindSearchResult(result_id);
163 if (!result) {
164 std::move(callback).Run(nullptr);
165 return;
166 }
167 result->GetContextMenuModel(base::BindOnce(
168 [](GetContextMenuModelCallback callback,
169 std::unique_ptr<ui::SimpleMenuModel> menu_model) {
170 std::move(callback).Run(std::move(menu_model));
171 },
172 std::move(callback)));
173 }
174
ViewClosing()175 void AppListClientImpl::ViewClosing() {
176 display_id_ = display::kInvalidDisplayId;
177 if (search_controller_)
178 search_controller_->ViewClosing();
179 }
180
ViewShown(int64_t display_id)181 void AppListClientImpl::ViewShown(int64_t display_id) {
182 if (current_model_updater_) {
183 base::RecordAction(base::UserMetricsAction("Launcher_Show"));
184 base::UmaHistogramSparse("Apps.AppListBadgedAppsCount",
185 current_model_updater_->BadgedItemCount());
186 }
187 display_id_ = display_id;
188 }
189
ActivateItem(int profile_id,const std::string & id,int event_flags)190 void AppListClientImpl::ActivateItem(int profile_id,
191 const std::string& id,
192 int event_flags) {
193 auto* requested_model_updater = profile_model_mappings_[profile_id];
194
195 // Pointless to notify the AppListModelUpdater of the activated item if the
196 // |requested_model_updater| is not the current one, which means that the
197 // active profile is changed. The same rule applies to the GetContextMenuModel
198 // and ContextMenuItemSelected.
199 if (requested_model_updater != current_model_updater_ ||
200 !requested_model_updater) {
201 return;
202 }
203
204 // Send a training signal to the search controller.
205 const auto* item = current_model_updater_->FindItem(id);
206 if (item) {
207 app_list::AppLaunchData app_launch_data;
208 app_launch_data.id = id;
209 app_launch_data.ranking_item_type =
210 app_list::RankingItemTypeFromChromeAppListItem(*item);
211 app_launch_data.launched_from = ash::AppListLaunchedFrom::kLaunchedFromGrid;
212 search_controller_->Train(std::move(app_launch_data));
213 }
214
215 requested_model_updater->ActivateChromeItem(id, event_flags);
216 }
217
GetContextMenuModel(int profile_id,const std::string & id,GetContextMenuModelCallback callback)218 void AppListClientImpl::GetContextMenuModel(
219 int profile_id,
220 const std::string& id,
221 GetContextMenuModelCallback callback) {
222 auto* requested_model_updater = profile_model_mappings_[profile_id];
223 if (requested_model_updater != current_model_updater_ ||
224 !requested_model_updater) {
225 std::move(callback).Run(nullptr);
226 return;
227 }
228 requested_model_updater->GetContextMenuModel(
229 id, base::BindOnce(
230 [](GetContextMenuModelCallback callback,
231 std::unique_ptr<ui::SimpleMenuModel> menu_model) {
232 std::move(callback).Run(std::move(menu_model));
233 },
234 std::move(callback)));
235 }
236
OnAppListVisibilityWillChange(bool visible)237 void AppListClientImpl::OnAppListVisibilityWillChange(bool visible) {
238 app_list_target_visibility_ = visible;
239 if (visible && search_controller_)
240 search_controller_->Start(base::string16());
241 }
242
OnAppListVisibilityChanged(bool visible)243 void AppListClientImpl::OnAppListVisibilityChanged(bool visible) {
244 app_list_visible_ = visible;
245 if (visible && search_controller_)
246 search_controller_->AppListShown();
247 }
248
OnItemAdded(int profile_id,std::unique_ptr<ash::AppListItemMetadata> item)249 void AppListClientImpl::OnItemAdded(
250 int profile_id,
251 std::unique_ptr<ash::AppListItemMetadata> item) {
252 auto* requested_model_updater = profile_model_mappings_[profile_id];
253 if (!requested_model_updater)
254 return;
255 requested_model_updater->OnItemAdded(std::move(item));
256 }
257
OnItemUpdated(int profile_id,std::unique_ptr<ash::AppListItemMetadata> item)258 void AppListClientImpl::OnItemUpdated(
259 int profile_id,
260 std::unique_ptr<ash::AppListItemMetadata> item) {
261 auto* requested_model_updater = profile_model_mappings_[profile_id];
262 if (!requested_model_updater)
263 return;
264 requested_model_updater->OnItemUpdated(std::move(item));
265 }
266
OnFolderDeleted(int profile_id,std::unique_ptr<ash::AppListItemMetadata> item)267 void AppListClientImpl::OnFolderDeleted(
268 int profile_id,
269 std::unique_ptr<ash::AppListItemMetadata> item) {
270 auto* requested_model_updater = profile_model_mappings_[profile_id];
271 if (!requested_model_updater)
272 return;
273 DCHECK(item->is_folder);
274 requested_model_updater->OnFolderDeleted(std::move(item));
275 }
276
OnPageBreakItemDeleted(int profile_id,const std::string & id)277 void AppListClientImpl::OnPageBreakItemDeleted(int profile_id,
278 const std::string& id) {
279 auto* requested_model_updater = profile_model_mappings_[profile_id];
280 if (!requested_model_updater)
281 return;
282 requested_model_updater->OnPageBreakItemDeleted(id);
283 }
284
OnSearchResultVisibilityChanged(const std::string & id,bool visibility)285 void AppListClientImpl::OnSearchResultVisibilityChanged(const std::string& id,
286 bool visibility) {
287 if (!search_controller_)
288 return;
289
290 ChromeSearchResult* result = search_controller_->FindSearchResult(id);
291 if (result == nullptr) {
292 return;
293 }
294 result->OnVisibilityChanged(visibility);
295 }
296
OnQuickSettingsChanged(const std::string & setting_name,const std::map<std::string,int> & values)297 void AppListClientImpl::OnQuickSettingsChanged(
298 const std::string& setting_name,
299 const std::map<std::string, int>& values) {
300 // CrOS action recorder.
301 app_list::CrOSActionRecorder::GetCrosActionRecorder()->RecordAction(
302 {base::StrCat({"SettingsChanged-", setting_name})}, values);
303 }
304
ActiveUserChanged(user_manager::User * active_user)305 void AppListClientImpl::ActiveUserChanged(user_manager::User* active_user) {
306 if (!active_user->is_profile_created())
307 return;
308
309 UpdateProfile();
310 }
311
UpdateProfile()312 void AppListClientImpl::UpdateProfile() {
313 Profile* profile = ProfileManager::GetActiveUserProfile();
314 app_list::AppListSyncableService* syncable_service =
315 app_list::AppListSyncableServiceFactory::GetForProfile(profile);
316 // AppListSyncableService is null in tests.
317 if (syncable_service)
318 SetProfile(profile);
319 }
320
SetProfile(Profile * new_profile)321 void AppListClientImpl::SetProfile(Profile* new_profile) {
322 if (profile_ == new_profile)
323 return;
324
325 if (profile_) {
326 DCHECK(current_model_updater_);
327 current_model_updater_->SetActive(false);
328
329 search_controller_.reset();
330 app_sync_ui_state_watcher_.reset();
331 current_model_updater_ = nullptr;
332 }
333
334 template_url_service_observer_.RemoveAll();
335
336 profile_ = new_profile;
337 if (!profile_)
338 return;
339
340 // If we are in guest mode, the new profile should be an OffTheRecord profile.
341 // Otherwise, this may later hit a check (same condition as this one) in
342 // Browser::Browser when opening links in a browser window (see
343 // http://crbug.com/460437).
344 DCHECK(!profile_->IsGuestSession() || profile_->IsOffTheRecord())
345 << "Guest mode must use OffTheRecord profile";
346
347 template_url_service_observer_.Add(
348 TemplateURLServiceFactory::GetForProfile(profile_));
349
350 app_list::AppListSyncableService* syncable_service =
351 app_list::AppListSyncableServiceFactory::GetForProfile(profile_);
352
353 current_model_updater_ = syncable_service->GetModelUpdater();
354 current_model_updater_->SetActive(true);
355
356 // On ChromeOS, there is no way to sign-off just one user. When signing off
357 // all users, AppListClientImpl instance is destructed before profiles are
358 // unloaded. So we don't need to remove elements from
359 // |profile_model_mappings_| explicitly.
360 profile_model_mappings_[current_model_updater_->model_id()] =
361 current_model_updater_;
362
363 app_sync_ui_state_watcher_ =
364 std::make_unique<AppSyncUIStateWatcher>(profile_, current_model_updater_);
365
366 SetUpSearchUI();
367 OnTemplateURLServiceChanged();
368
369 // Clear search query.
370 current_model_updater_->UpdateSearchBox(base::string16(),
371 false /* initiated_by_user */);
372 }
373
SetUpSearchUI()374 void AppListClientImpl::SetUpSearchUI() {
375 search_controller_ = app_list::CreateSearchController(
376 profile_, current_model_updater_, this, GetNotifier());
377
378 // Refresh the results used for the suggestion chips with empty query.
379 // This fixes crbug.com/999287.
380 StartSearch(base::string16());
381 }
382
search_controller()383 app_list::SearchController* AppListClientImpl::search_controller() {
384 return search_controller_.get();
385 }
386
GetModelUpdaterForTest()387 AppListModelUpdater* AppListClientImpl::GetModelUpdaterForTest() {
388 return current_model_updater_;
389 }
390
OnTemplateURLServiceChanged()391 void AppListClientImpl::OnTemplateURLServiceChanged() {
392 DCHECK(current_model_updater_);
393
394 TemplateURLService* template_url_service =
395 TemplateURLServiceFactory::GetForProfile(profile_);
396 const TemplateURL* default_provider =
397 template_url_service->GetDefaultSearchProvider();
398 const bool is_google =
399 default_provider &&
400 default_provider->GetEngineType(
401 template_url_service->search_terms_data()) == SEARCH_ENGINE_GOOGLE;
402
403 current_model_updater_->SetSearchEngineIsGoogle(is_google);
404 }
405
ShowAppList()406 void AppListClientImpl::ShowAppList() {
407 // This may not work correctly if the profile passed in is different from the
408 // one the ash Shell is currently using.
409 if (!app_list_controller_)
410 return;
411 app_list_controller_->ShowAppList();
412 }
413
GetCurrentAppListProfile() const414 Profile* AppListClientImpl::GetCurrentAppListProfile() const {
415 return ChromeLauncherController::instance()->profile();
416 }
417
GetAppListController() const418 ash::AppListController* AppListClientImpl::GetAppListController() const {
419 return app_list_controller_;
420 }
421
DismissView()422 void AppListClientImpl::DismissView() {
423 if (!app_list_controller_)
424 return;
425 app_list_controller_->DismissAppList();
426 }
427
GetAppListWindow()428 aura::Window* AppListClientImpl::GetAppListWindow() {
429 return app_list_controller_->GetWindow();
430 }
431
GetAppListDisplayId()432 int64_t AppListClientImpl::GetAppListDisplayId() {
433 return display_id_;
434 }
435
GetAppInfoDialogBounds(GetAppInfoDialogBoundsCallback callback)436 void AppListClientImpl::GetAppInfoDialogBounds(
437 GetAppInfoDialogBoundsCallback callback) {
438 if (!app_list_controller_) {
439 LOG(ERROR) << "app_list_controller_ is null";
440 std::move(callback).Run(gfx::Rect());
441 return;
442 }
443 app_list_controller_->GetAppInfoDialogBounds(std::move(callback));
444 }
445
IsAppPinned(const std::string & app_id)446 bool AppListClientImpl::IsAppPinned(const std::string& app_id) {
447 return ChromeLauncherController::instance()->IsAppPinned(app_id);
448 }
449
IsAppOpen(const std::string & app_id) const450 bool AppListClientImpl::IsAppOpen(const std::string& app_id) const {
451 return ChromeLauncherController::instance()->IsOpen(ash::ShelfID(app_id));
452 }
453
PinApp(const std::string & app_id)454 void AppListClientImpl::PinApp(const std::string& app_id) {
455 ChromeLauncherController::instance()->PinAppWithID(app_id);
456 }
457
UnpinApp(const std::string & app_id)458 void AppListClientImpl::UnpinApp(const std::string& app_id) {
459 ChromeLauncherController::instance()->UnpinAppWithID(app_id);
460 }
461
GetPinnable(const std::string & app_id)462 AppListControllerDelegate::Pinnable AppListClientImpl::GetPinnable(
463 const std::string& app_id) {
464 return GetPinnableForAppID(app_id,
465 ChromeLauncherController::instance()->profile());
466 }
467
CreateNewWindow(bool incognito)468 void AppListClientImpl::CreateNewWindow(bool incognito) {
469 ash::NewWindowDelegate::GetInstance()->NewWindow(incognito);
470 }
471
OpenURL(Profile * profile,const GURL & url,ui::PageTransition transition,WindowOpenDisposition disposition)472 void AppListClientImpl::OpenURL(Profile* profile,
473 const GURL& url,
474 ui::PageTransition transition,
475 WindowOpenDisposition disposition) {
476 NavigateParams params(profile, url, transition);
477 params.disposition = disposition;
478 Navigate(¶ms);
479 }
480
ActivateApp(Profile * profile,const extensions::Extension * extension,AppListSource source,int event_flags)481 void AppListClientImpl::ActivateApp(Profile* profile,
482 const extensions::Extension* extension,
483 AppListSource source,
484 int event_flags) {
485 // Platform apps treat activations as a launch. The app can decide whether to
486 // show a new window or focus an existing window as it sees fit.
487 if (extension->is_platform_app()) {
488 LaunchApp(profile, extension, source, event_flags, GetAppListDisplayId());
489 return;
490 }
491
492 ChromeLauncherController::instance()->ActivateApp(
493 extension->id(), AppListSourceToLaunchSource(source), event_flags,
494 GetAppListDisplayId());
495
496 if (!IsTabletMode())
497 DismissView();
498 }
499
LaunchApp(Profile * profile,const extensions::Extension * extension,AppListSource source,int event_flags,int64_t display_id)500 void AppListClientImpl::LaunchApp(Profile* profile,
501 const extensions::Extension* extension,
502 AppListSource source,
503 int event_flags,
504 int64_t display_id) {
505 ChromeLauncherController::instance()->LaunchApp(
506 ash::ShelfID(extension->id()), AppListSourceToLaunchSource(source),
507 event_flags, display_id);
508
509 if (!IsTabletMode())
510 DismissView();
511 }
512
NotifySearchResultsForLogging(const base::string16 & trimmed_query,const ash::SearchResultIdWithPositionIndices & results,int position_index)513 void AppListClientImpl::NotifySearchResultsForLogging(
514 const base::string16& trimmed_query,
515 const ash::SearchResultIdWithPositionIndices& results,
516 int position_index) {
517 if (search_controller_) {
518 search_controller_->OnSearchResultsDisplayed(trimmed_query, results,
519 position_index);
520 }
521 }
522
GetNotifier()523 ash::AppListNotifier* AppListClientImpl::GetNotifier() {
524 return app_list_notifier_.get();
525 }
526
AppListSourceToLaunchSource(AppListSource source)527 ash::ShelfLaunchSource AppListClientImpl::AppListSourceToLaunchSource(
528 AppListSource source) {
529 switch (source) {
530 case LAUNCH_FROM_APP_LIST:
531 return ash::LAUNCH_FROM_APP_LIST;
532 case LAUNCH_FROM_APP_LIST_SEARCH:
533 return ash::LAUNCH_FROM_APP_LIST_SEARCH;
534 default:
535 return ash::LAUNCH_FROM_UNKNOWN;
536 }
537 }
538