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