1 // Copyright (c) 2012 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/ash/launcher/app_shortcut_launcher_item_controller.h"
6
7 #include <stddef.h>
8
9 #include <algorithm>
10 #include <memory>
11 #include <utility>
12
13 #include "base/callback_forward.h"
14 #include "base/callback_helpers.h"
15 #include "base/memory/ptr_util.h"
16 #include "chrome/browser/extensions/launch_util.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/ui/app_list/arc/arc_app_utils.h"
19 #include "chrome/browser/ui/ash/ash_util.h"
20 #include "chrome/browser/ui/ash/launcher/arc_playstore_shortcut_launcher_item_controller.h"
21 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
22 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_util.h"
23 #include "chrome/browser/ui/ash/launcher/launcher_controller_helper.h"
24 #include "chrome/browser/ui/ash/launcher/shelf_context_menu.h"
25 #include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
26 #include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
27 #include "chrome/browser/ui/browser.h"
28 #include "chrome/browser/ui/browser_finder.h"
29 #include "chrome/browser/ui/browser_list.h"
30 #include "chrome/browser/ui/browser_window.h"
31 #include "chrome/browser/ui/tabs/tab_strip_model.h"
32 #include "chrome/browser/web_applications/components/app_registrar.h"
33 #include "chrome/browser/web_applications/components/web_app_helpers.h"
34 #include "chrome/browser/web_applications/components/web_app_provider_base.h"
35 #include "chrome/common/chrome_features.h"
36 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
37 #include "content/public/browser/navigation_entry.h"
38 #include "content/public/browser/web_contents.h"
39 #include "extensions/browser/app_window/native_app_window.h"
40 #include "extensions/browser/extension_registry.h"
41 #include "extensions/browser/process_manager.h"
42 #include "ui/aura/window.h"
43 #include "ui/events/event.h"
44
45 using extensions::Extension;
46 using extensions::ExtensionRegistry;
47
48 namespace {
49
50 // The time delta between clicks in which clicks to launch V2 apps are ignored.
51 const int kClickSuppressionInMS = 1000;
52
IsAppBrowser(Browser * browser)53 bool IsAppBrowser(Browser* browser) {
54 return browser->is_type_app() || browser->is_type_app_popup();
55 }
56
57 // Activate the browser with the given |content| and show the associated tab,
58 // or minimize the browser if it is already active. Returns the action
59 // performed by activating the content.
ActivateContentOrMinimize(content::WebContents * content,bool allow_minimize)60 ash::ShelfAction ActivateContentOrMinimize(content::WebContents* content,
61 bool allow_minimize) {
62 Browser* browser = chrome::FindBrowserWithWebContents(content);
63 TabStripModel* tab_strip = browser->tab_strip_model();
64 int index = tab_strip->GetIndexOfWebContents(content);
65 DCHECK_NE(TabStripModel::kNoTab, index);
66
67 int old_index = tab_strip->active_index();
68 if (index != old_index)
69 tab_strip->ActivateTabAt(index);
70 return ChromeLauncherController::instance()->ActivateWindowOrMinimizeIfActive(
71 browser->window(), index == old_index && allow_minimize);
72 }
73
74 // Advance to the next window of an app if possible. |items| is the list of
75 // browsers/web contents associated with this app. |active_item_callback|
76 // retrieves the window that is currently active, if available.
77 // |activate_callback| will activate the next window selected by this function.
78 template <class T>
AdvanceApp(const std::vector<T * > & items,base::OnceCallback<T * (const std::vector<T * > &,aura::Window **)> active_item_callback,base::OnceCallback<void (T *)> activate_callback)79 base::Optional<ash::ShelfAction> AdvanceApp(
80 const std::vector<T*>& items,
81 base::OnceCallback<T*(const std::vector<T*>&, aura::Window**)>
82 active_item_callback,
83 base::OnceCallback<void(T*)> activate_callback) {
84 if (items.empty())
85 return base::nullopt;
86
87 // Get the active item and associated aura::Window if it exists.
88 aura::Window* active_item_window = nullptr;
89 T* active_item =
90 std::move(active_item_callback).Run(items, &active_item_window);
91
92 // If there is only one of the app, and it is the current active item,
93 // bounce the window to signal nothing happened.
94 if (items.size() == 1u && active_item) {
95 DCHECK(active_item_window);
96 ash_util::BounceWindow(active_item_window);
97 return ash::SHELF_ACTION_NONE;
98 }
99
100 // If one of the items is active, active the next one in the list, otherwise
101 // activate the first item in the list.
102 size_t index = 0;
103 if (active_item) {
104 DCHECK(base::Contains(items, active_item));
105 auto it = std::find(items.cbegin(), items.cend(), active_item);
106 index = (it - items.cbegin() + 1) % items.size();
107 }
108 std::move(activate_callback).Run(items[index]);
109 return ash::SHELF_ACTION_WINDOW_ACTIVATED;
110 }
111
112 // AppMatcher is used to determine if various WebContents instances are
113 // associated with a specific app. Clients should call CanMatchWebContents()
114 // before iterating through WebContents instances and calling
115 // WebContentMatchesApp().
116 class AppMatcher {
117 public:
AppMatcher(Profile * profile,const std::string & app_id,const URLPattern & refocus_pattern)118 AppMatcher(Profile* profile,
119 const std::string& app_id,
120 const URLPattern& refocus_pattern)
121 : app_id_(app_id), refocus_pattern_(refocus_pattern) {
122 DCHECK(profile);
123 if (web_app::WebAppProviderBase* provider =
124 web_app::WebAppProviderBase::GetProviderBase(profile)) {
125 if (provider->registrar().IsLocallyInstalled(app_id)) {
126 registrar_ = &provider->registrar();
127 }
128 }
129 if (!registrar_)
130 extension_ = GetExtensionForAppID(app_id, profile);
131 }
132
133 AppMatcher(const AppMatcher&) = delete;
134 AppMatcher& operator=(const AppMatcher&) = delete;
135
CanMatchWebContents() const136 bool CanMatchWebContents() const { return registrar_ || extension_; }
137
138 // Returns true if this app matches the given |web_contents|. If
139 // the browser is an app browser, the application gets first checked against
140 // its original URL since a windowed app might have navigated away from its
141 // app domain.
142 // May only be called if CanMatchWebContents() return true.
WebContentMatchesApp(content::WebContents * web_contents,Browser * browser) const143 bool WebContentMatchesApp(content::WebContents* web_contents,
144 Browser* browser) const {
145 DCHECK(CanMatchWebContents());
146 return extension_ ? WebContentMatchesHostedApp(web_contents, browser)
147 : WebContentMatchesWebApp(web_contents, browser);
148 }
149
150 private:
WebContentMatchesHostedApp(content::WebContents * web_contents,Browser * browser) const151 bool WebContentMatchesHostedApp(content::WebContents* web_contents,
152 Browser* browser) const {
153 DCHECK(extension_);
154 DCHECK(!registrar_);
155
156 // If the browser is an app window, and the app name matches the extension,
157 // then the contents match the app.
158 if (IsAppBrowser(browser)) {
159 const Extension* browser_extension =
160 ExtensionRegistry::Get(browser->profile())
161 ->GetExtensionById(
162 web_app::GetAppIdFromApplicationName(browser->app_name()),
163 ExtensionRegistry::EVERYTHING);
164 return browser_extension == extension_;
165 }
166
167 // Apps set to launch in app windows should not match contents running in
168 // tabs.
169 if (extensions::LaunchesInWindow(browser->profile(), extension_))
170 return false;
171
172 // There are three ways to identify the association of a URL with this
173 // extension:
174 // - The refocus pattern is matched (needed for apps like drive).
175 // - The extension's origin + extent gets matched.
176 // - The launcher controller knows that the tab got created for this app.
177 const GURL tab_url = web_contents->GetURL();
178 return (
179 (!refocus_pattern_.match_all_urls() &&
180 refocus_pattern_.MatchesURL(tab_url)) ||
181 (extension_->OverlapsWithOrigin(tab_url) &&
182 extension_->web_extent().MatchesURL(tab_url)) ||
183 ChromeLauncherController::instance()->IsWebContentHandledByApplication(
184 web_contents, app_id_));
185 }
186
187 // Returns true if this web app matches the given |web_contents|. If
188 // |deprecated_is_app| is true, the application gets first checked against its
189 // original URL since a windowed app might have navigated away from its app
190 // domain.
WebContentMatchesWebApp(content::WebContents * web_contents,Browser * browser) const191 bool WebContentMatchesWebApp(content::WebContents* web_contents,
192 Browser* browser) const {
193 DCHECK(registrar_);
194 DCHECK(!extension_);
195
196 // If the browser is a web app window, and the window app id matches,
197 // then the contents match the app.
198 if (browser->app_controller() && browser->app_controller()->HasAppId())
199 return browser->app_controller()->GetAppId() == app_id_;
200
201 // Bookmark apps set to launch in app windows should not match contents
202 // running in tabs.
203 if (registrar_->GetAppUserDisplayMode(app_id_) !=
204 web_app::DisplayMode::kBrowser &&
205 // TODO(crbug.com/1054116): when the flag is on, we allow web
206 // contents in a normal browser to match a web app. This is going to
207 // be weird because GetAppMenuItems(0) and HasRunningApplications()
208 // does not consider normal browsers.
209 !base::FeatureList::IsEnabled(
210 features::kDesktopPWAsWithoutExtensions)) {
211 return false;
212 }
213
214 // There are three ways to identify the association of a URL with this
215 // web app:
216 // - The refocus pattern is matched (needed for apps like drive).
217 // - The web app's scope gets matched.
218 // - The launcher controller knows that the tab got created for this web
219 // app.
220 const GURL tab_url = web_contents->GetURL();
221 base::Optional<GURL> app_scope = registrar_->GetAppScope(app_id_);
222 DCHECK(app_scope.has_value());
223
224 return (
225 (!refocus_pattern_.match_all_urls() &&
226 refocus_pattern_.MatchesURL(tab_url)) ||
227 (base::StartsWith(tab_url.spec(), app_scope->spec(),
228 base::CompareCase::SENSITIVE)) ||
229 ChromeLauncherController::instance()->IsWebContentHandledByApplication(
230 web_contents, app_id_));
231 }
232
233 const std::string app_id_;
234 const URLPattern refocus_pattern_;
235
236 // AppMatcher is stack allocated. Pointer members below are not owned.
237
238 // registrar_ is set when app_id_ is a web app.
239 const web_app::AppRegistrar* registrar_ = nullptr;
240
241 // extension_ is set when app_id_ is a hosted app.
242 const Extension* extension_ = nullptr;
243 };
244
245 } // namespace
246
247 // static
248 std::unique_ptr<AppShortcutLauncherItemController>
Create(const ash::ShelfID & shelf_id)249 AppShortcutLauncherItemController::Create(const ash::ShelfID& shelf_id) {
250 if (shelf_id.app_id == arc::kPlayStoreAppId)
251 return std::make_unique<ArcPlaystoreShortcutLauncherItemController>();
252 return base::WrapUnique<AppShortcutLauncherItemController>(
253 new AppShortcutLauncherItemController(shelf_id));
254 }
255
AppShortcutLauncherItemController(const ash::ShelfID & shelf_id)256 AppShortcutLauncherItemController::AppShortcutLauncherItemController(
257 const ash::ShelfID& shelf_id)
258 : ash::ShelfItemDelegate(shelf_id) {
259 BrowserList::AddObserver(this);
260
261 // To detect V1 applications we use their domain and match them against the
262 // used URL. This will also work with applications like Google Drive.
263 const Extension* extension = GetExtensionForAppID(
264 shelf_id.app_id, ChromeLauncherController::instance()->profile());
265 // Some unit tests have no real extension.
266 if (extension) {
267 set_refocus_url(GURL(
268 extensions::AppLaunchInfo::GetLaunchWebURL(extension).spec() + "*"));
269 }
270 }
271
~AppShortcutLauncherItemController()272 AppShortcutLauncherItemController::~AppShortcutLauncherItemController() {
273 BrowserList::RemoveObserver(this);
274 }
275
ItemSelected(std::unique_ptr<ui::Event> event,int64_t display_id,ash::ShelfLaunchSource source,ItemSelectedCallback callback,const ItemFilterPredicate & filter_predicate)276 void AppShortcutLauncherItemController::ItemSelected(
277 std::unique_ptr<ui::Event> event,
278 int64_t display_id,
279 ash::ShelfLaunchSource source,
280 ItemSelectedCallback callback,
281 const ItemFilterPredicate& filter_predicate) {
282 // In case of a keyboard event, we were called by a hotkey. In that case we
283 // activate the next item in line if an item of our list is already active.
284 if (event && event->type() == ui::ET_KEY_RELEASED) {
285 auto optional_action = AdvanceToNextApp(filter_predicate);
286 if (optional_action.has_value()) {
287 std::move(callback).Run(optional_action.value(), {});
288 return;
289 }
290 }
291
292 AppMenuItems items =
293 GetAppMenuItems(event ? event->flags() : ui::EF_NONE, filter_predicate);
294 if (items.empty()) {
295 // Ideally we come here only once. After that ShellLauncherItemController
296 // will take over when the shell window gets opened. However there are apps
297 // which take a lot of time for pre-processing (like the files app) before
298 // they open a window. Since there is currently no other way to detect if an
299 // app was started we suppress any further clicks within a special time out.
300 if (IsV2App() && !AllowNextLaunchAttempt()) {
301 std::move(callback).Run(ash::SHELF_ACTION_NONE, std::move(items));
302 return;
303 }
304
305 // LaunchApp may replace and destroy this item controller instance. Run the
306 // callback first and copy the id to avoid crashes.
307 std::move(callback).Run(ash::SHELF_ACTION_NEW_WINDOW_CREATED, {});
308 ChromeLauncherController::instance()->LaunchApp(
309 ash::ShelfID(shelf_id()), source, ui::EF_NONE, display_id);
310 return;
311 }
312
313 if (source != ash::LAUNCH_FROM_SHELF || items.size() == 1) {
314 const bool can_minimize = source != ash::LAUNCH_FROM_APP_LIST &&
315 source != ash::LAUNCH_FROM_APP_LIST_SEARCH;
316 std::move(callback).Run(
317 app_menu_cached_by_browsers_
318 ? ChromeLauncherController::instance()
319 ->ActivateWindowOrMinimizeIfActive(
320 // We don't need to check nullptr here because
321 // we just called GetAppMenuItems() above to update it.
322 app_menu_browsers_[0]->window(), can_minimize)
323 : ActivateContentOrMinimize(app_menu_web_contents_[0],
324 can_minimize),
325 {});
326 } else {
327 // Multiple items, a menu will be shown. No need to activate the most
328 // recently active item.
329 std::move(callback).Run(ash::SHELF_ACTION_NONE, std::move(items));
330 }
331 }
332
HasRunningApplications()333 bool AppShortcutLauncherItemController::HasRunningApplications() {
334 return IsWindowedWebApp() ? !GetAppBrowsers(base::NullCallback()).empty()
335 : !GetAppWebContents(base::NullCallback()).empty();
336 }
337
338 ash::ShelfItemDelegate::AppMenuItems
GetAppMenuItems(int event_flags,const ItemFilterPredicate & filter_predicate)339 AppShortcutLauncherItemController::GetAppMenuItems(
340 int event_flags,
341 const ItemFilterPredicate& filter_predicate) {
342 ChromeLauncherController* controller = ChromeLauncherController::instance();
343 AppMenuItems items;
344 auto add_menu_item = [&controller,
345 &items](content::WebContents* web_contents) {
346 items.push_back({items.size(), controller->GetAppMenuTitle(web_contents),
347 controller->GetAppMenuIcon(web_contents).AsImageSkia()});
348 };
349
350 if (IsWindowedWebApp() && !(event_flags & ui::EF_SHIFT_DOWN)) {
351 app_menu_browsers_ = GetAppBrowsers(filter_predicate);
352 app_menu_cached_by_browsers_ = true;
353 for (auto* browser : app_menu_browsers_) {
354 add_menu_item(browser->tab_strip_model()->GetActiveWebContents());
355 }
356 } else {
357 app_menu_web_contents_ = GetAppWebContents(filter_predicate);
358 app_menu_cached_by_browsers_ = false;
359 for (auto* web_contents : app_menu_web_contents_) {
360 add_menu_item(web_contents);
361 }
362 }
363
364 return items;
365 }
366
GetContextMenu(int64_t display_id,GetContextMenuCallback callback)367 void AppShortcutLauncherItemController::GetContextMenu(
368 int64_t display_id,
369 GetContextMenuCallback callback) {
370 ChromeLauncherController* controller = ChromeLauncherController::instance();
371 const ash::ShelfItem* item = controller->GetItem(shelf_id());
372 context_menu_ = ShelfContextMenu::Create(controller, item, display_id);
373 context_menu_->GetMenuModel(std::move(callback));
374 }
375
ExecuteCommand(bool from_context_menu,int64_t command_id,int32_t event_flags,int64_t display_id)376 void AppShortcutLauncherItemController::ExecuteCommand(bool from_context_menu,
377 int64_t command_id,
378 int32_t event_flags,
379 int64_t display_id) {
380 if (from_context_menu && ExecuteContextMenuCommand(command_id, event_flags))
381 return;
382
383 if (static_cast<size_t>(command_id) >= AppMenuSize()) {
384 ClearAppMenu();
385 return;
386 }
387
388 bool should_close =
389 event_flags & (ui::EF_SHIFT_DOWN | ui::EF_MIDDLE_MOUSE_BUTTON);
390 auto activate_browser = [](Browser* browser) {
391 multi_user_util::MoveWindowToCurrentDesktop(
392 browser->window()->GetNativeWindow());
393 browser->window()->Show();
394 browser->window()->Activate();
395 };
396
397 if (app_menu_cached_by_browsers_) {
398 Browser* browser = app_menu_browsers_[command_id];
399 if (browser) {
400 if (should_close)
401 browser->tab_strip_model()->CloseAllTabs();
402 else
403 activate_browser(browser);
404 }
405 } else {
406 // If the web contents was destroyed while the menu was open, then the
407 // invalid pointer cached in |app_menu_web_contents_| should yield a null
408 // browser or kNoTab.
409 content::WebContents* web_contents = app_menu_web_contents_[command_id];
410 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
411 TabStripModel* tab_strip = browser ? browser->tab_strip_model() : nullptr;
412 const int index = tab_strip ? tab_strip->GetIndexOfWebContents(web_contents)
413 : TabStripModel::kNoTab;
414 if (index != TabStripModel::kNoTab) {
415 if (should_close) {
416 tab_strip->CloseWebContentsAt(index, TabStripModel::CLOSE_USER_GESTURE);
417 } else {
418 tab_strip->ActivateTabAt(index);
419 activate_browser(browser);
420 }
421 }
422 }
423
424 ClearAppMenu();
425 }
426
Close()427 void AppShortcutLauncherItemController::Close() {
428 // Close all running 'programs' of this type.
429 if (IsWindowedWebApp()) {
430 for (Browser* browser : GetAppBrowsers(base::NullCallback()))
431 browser->tab_strip_model()->CloseAllTabs();
432 } else {
433 for (content::WebContents* item : GetAppWebContents(base::NullCallback())) {
434 Browser* browser = chrome::FindBrowserWithWebContents(item);
435 if (!browser ||
436 !multi_user_util::IsProfileFromActiveUser(browser->profile())) {
437 continue;
438 }
439 TabStripModel* tab_strip = browser->tab_strip_model();
440 int index = tab_strip->GetIndexOfWebContents(item);
441 DCHECK(index != TabStripModel::kNoTab);
442 tab_strip->CloseWebContentsAt(index, TabStripModel::CLOSE_NONE);
443 }
444 }
445 }
446
OnBrowserClosing(Browser * browser)447 void AppShortcutLauncherItemController::OnBrowserClosing(Browser* browser) {
448 if (!app_menu_cached_by_browsers_)
449 return;
450 // Reset pointers to the closed browser, but leave menu indices intact.
451 auto it =
452 std::find(app_menu_browsers_.begin(), app_menu_browsers_.end(), browser);
453 if (it != app_menu_browsers_.end())
454 *it = nullptr;
455 }
456
457 std::vector<content::WebContents*>
GetAppWebContents(const ItemFilterPredicate & filter_predicate)458 AppShortcutLauncherItemController::GetAppWebContents(
459 const ItemFilterPredicate& filter_predicate) {
460 URLPattern refocus_pattern(URLPattern::SCHEME_ALL);
461 refocus_pattern.SetMatchAllURLs(true);
462
463 if (!refocus_url_.is_empty()) {
464 refocus_pattern.SetMatchAllURLs(false);
465 refocus_pattern.Parse(refocus_url_.spec());
466 }
467
468 Profile* const profile = ChromeLauncherController::instance()->profile();
469 AppMatcher matcher(profile, app_id(), refocus_pattern);
470
471 std::vector<content::WebContents*> items;
472 // It is possible to come here while an app gets loaded.
473 if (!matcher.CanMatchWebContents())
474 return items;
475
476 for (auto* browser : *BrowserList::GetInstance()) {
477 if (!filter_predicate.is_null() &&
478 !filter_predicate.Run(browser->window()->GetNativeWindow())) {
479 continue;
480 }
481 if (!multi_user_util::IsProfileFromActiveUser(browser->profile()))
482 continue;
483 TabStripModel* tab_strip = browser->tab_strip_model();
484 for (int index = 0; index < tab_strip->count(); index++) {
485 content::WebContents* web_contents = tab_strip->GetWebContentsAt(index);
486 if (matcher.WebContentMatchesApp(web_contents, browser))
487 items.push_back(web_contents);
488 }
489 }
490 return items;
491 }
492
GetAppBrowsers(const ItemFilterPredicate & filter_predicate)493 std::vector<Browser*> AppShortcutLauncherItemController::GetAppBrowsers(
494 const ItemFilterPredicate& filter_predicate) {
495 DCHECK(IsWindowedWebApp());
496 std::vector<Browser*> browsers;
497 for (Browser* browser : *BrowserList::GetInstance()) {
498 if (!filter_predicate.is_null() &&
499 !filter_predicate.Run(browser->window()->GetNativeWindow())) {
500 continue;
501 }
502 if (!multi_user_util::IsProfileFromActiveUser(browser->profile()))
503 continue;
504 if (!IsAppBrowser(browser))
505 continue;
506
507 if (web_app::GetAppIdFromApplicationName(browser->app_name()) == app_id() &&
508 browser->tab_strip_model()->GetActiveWebContents()) {
509 browsers.push_back(browser);
510 }
511 }
512 return browsers;
513 }
514
515 base::Optional<ash::ShelfAction>
AdvanceToNextApp(const ItemFilterPredicate & filter_predicate)516 AppShortcutLauncherItemController::AdvanceToNextApp(
517 const ItemFilterPredicate& filter_predicate) {
518 if (!chrome::FindLastActive())
519 return base::nullopt;
520
521 if (IsWindowedWebApp()) {
522 return AdvanceApp(GetAppBrowsers(filter_predicate),
523 base::BindOnce([](const std::vector<Browser*>& browsers,
524 aura::Window** out_window) -> Browser* {
525 for (auto* browser : browsers) {
526 if (browser->window()->IsActive()) {
527 *out_window = browser->window()->GetNativeWindow();
528 return browser;
529 }
530 }
531 return nullptr;
532 }),
533 base::BindOnce([](Browser* browser) -> void {
534 browser->window()->Show();
535 browser->window()->Activate();
536 }));
537 }
538
539 return AdvanceApp(
540 GetAppWebContents(filter_predicate),
541 base::BindOnce([](const std::vector<content::WebContents*>& web_contents,
542 aura::Window** out_window) -> content::WebContents* {
543 for (auto* web_content : web_contents) {
544 Browser* browser = chrome::FindBrowserWithWebContents(web_content);
545 // The active web contents is on the active browser, and matches the
546 // index of the current active tab.
547 if (browser->window()->IsActive()) {
548 TabStripModel* tab_strip = browser->tab_strip_model();
549 int index = tab_strip->GetIndexOfWebContents(web_content);
550 if (tab_strip->active_index() == index) {
551 *out_window = browser->window()->GetNativeWindow();
552 return web_content;
553 }
554 }
555 }
556 return nullptr;
557 }),
558 base::BindOnce([](content::WebContents* web_contents) -> void {
559 ActivateContentOrMinimize(web_contents, /*allow_minimize=*/false);
560 }));
561 }
562
IsV2App()563 bool AppShortcutLauncherItemController::IsV2App() {
564 const Extension* extension = GetExtensionForAppID(
565 app_id(), ChromeLauncherController::instance()->profile());
566 return extension && extension->is_platform_app();
567 }
568
AllowNextLaunchAttempt()569 bool AppShortcutLauncherItemController::AllowNextLaunchAttempt() {
570 if (last_launch_attempt_.is_null() ||
571 last_launch_attempt_ +
572 base::TimeDelta::FromMilliseconds(kClickSuppressionInMS) <
573 base::Time::Now()) {
574 last_launch_attempt_ = base::Time::Now();
575 return true;
576 }
577 return false;
578 }
579
IsWindowedWebApp()580 bool AppShortcutLauncherItemController::IsWindowedWebApp() {
581 if (web_app::WebAppProviderBase* provider =
582 web_app::WebAppProviderBase::GetProviderBase(
583 ChromeLauncherController::instance()->profile())) {
584 web_app::AppRegistrar& registrar = provider->registrar();
585 if (registrar.IsLocallyInstalled(app_id())) {
586 return registrar.GetAppUserDisplayMode(app_id()) !=
587 web_app::DisplayMode::kBrowser;
588 }
589 }
590 return false;
591 }
592
AppMenuSize()593 size_t AppShortcutLauncherItemController::AppMenuSize() {
594 return app_menu_cached_by_browsers_ ? app_menu_browsers_.size()
595 : app_menu_web_contents_.size();
596 }
597
ClearAppMenu()598 void AppShortcutLauncherItemController::ClearAppMenu() {
599 app_menu_browsers_.clear();
600 app_menu_web_contents_.clear();
601 }
602