1 // Copyright 2019 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/chromeos/apps/intent_helper/common_apps_navigation_throttle.h"
6 
7 #include <utility>
8 
9 #include "base/bind.h"
10 #include "base/debug/dump_without_crashing.h"
11 #include "base/stl_util.h"
12 #include "chrome/browser/apps/app_service/app_service_proxy.h"
13 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
14 #include "chrome/browser/apps/app_service/launch_utils.h"
15 #include "chrome/browser/apps/intent_helper/apps_navigation_types.h"
16 #include "chrome/browser/apps/intent_helper/intent_picker_auto_display_service.h"
17 #include "chrome/browser/chromeos/apps/metrics/intent_handling_metrics.h"
18 #include "chrome/browser/profiles/profile.h"
19 #include "chrome/browser/ui/intent_picker_tab_helper.h"
20 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
21 #include "chrome/common/chrome_features.h"
22 #include "chromeos/constants/chromeos_switches.h"
23 #include "components/services/app_service/public/mojom/types.mojom.h"
24 #include "content/public/browser/navigation_handle.h"
25 #include "content/public/browser/web_contents.h"
26 #include "ui/display/types/display_constants.h"
27 
28 namespace {
29 
GetPickerEntryType(apps::mojom::AppType app_type)30 apps::PickerEntryType GetPickerEntryType(apps::mojom::AppType app_type) {
31   apps::PickerEntryType picker_entry_type = apps::PickerEntryType::kUnknown;
32   switch (app_type) {
33     case apps::mojom::AppType::kUnknown:
34     case apps::mojom::AppType::kBuiltIn:
35     case apps::mojom::AppType::kCrostini:
36     case apps::mojom::AppType::kPluginVm:
37     case apps::mojom::AppType::kExtension:
38     case apps::mojom::AppType::kLacros:
39     case apps::mojom::AppType::kRemote:
40     case apps::mojom::AppType::kBorealis:
41       break;
42     case apps::mojom::AppType::kArc:
43       picker_entry_type = apps::PickerEntryType::kArc;
44       break;
45     case apps::mojom::AppType::kWeb:
46       picker_entry_type = apps::PickerEntryType::kWeb;
47       break;
48     case apps::mojom::AppType::kMacOs:
49       picker_entry_type = apps::PickerEntryType::kMacOs;
50       break;
51   }
52   return picker_entry_type;
53 }
54 
55 }  // namespace
56 
57 namespace apps {
58 
59 // static
60 std::unique_ptr<apps::AppsNavigationThrottle>
MaybeCreate(content::NavigationHandle * handle)61 CommonAppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
62   if (!handle->IsInMainFrame())
63     return nullptr;
64 
65   content::WebContents* web_contents = handle->GetWebContents();
66 
67   Profile* profile =
68       Profile::FromBrowserContext(web_contents->GetBrowserContext());
69 
70   if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
71     return nullptr;
72 
73   if (!apps::AppsNavigationThrottle::CanCreate(web_contents))
74     return nullptr;
75 
76   return std::make_unique<CommonAppsNavigationThrottle>(handle);
77 }
78 
79 // static
ShowIntentPickerBubble(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)80 void CommonAppsNavigationThrottle::ShowIntentPickerBubble(
81     content::WebContents* web_contents,
82     IntentPickerAutoDisplayService* ui_auto_display_service,
83     const GURL& url) {
84   std::vector<apps::IntentPickerAppInfo> apps_for_picker =
85       FindAllAppsForUrl(web_contents, url, {});
86 
87   IntentPickerTabHelper::LoadAppIcons(
88       web_contents, std::move(apps_for_picker),
89       base::BindOnce(&OnAppIconsLoaded, web_contents, ui_auto_display_service,
90                      url));
91 }
92 
93 // static
OnIntentPickerClosed(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,const std::string & launch_name,apps::PickerEntryType entry_type,apps::IntentPickerCloseReason close_reason,bool should_persist)94 void CommonAppsNavigationThrottle::OnIntentPickerClosed(
95     content::WebContents* web_contents,
96     IntentPickerAutoDisplayService* ui_auto_display_service,
97     const GURL& url,
98     const std::string& launch_name,
99     apps::PickerEntryType entry_type,
100     apps::IntentPickerCloseReason close_reason,
101     bool should_persist) {
102   if (chromeos::switches::IsTabletFormFactor() && should_persist) {
103     // On devices of tablet form factor, until the user has decided to persist
104     // the setting, the browser-side intent picker should always be seen.
105     auto platform = IntentPickerAutoDisplayPref::Platform::kNone;
106     if (entry_type == apps::PickerEntryType::kArc) {
107       platform = IntentPickerAutoDisplayPref::Platform::kArc;
108     } else if (entry_type == apps::PickerEntryType::kUnknown &&
109                close_reason == apps::IntentPickerCloseReason::STAY_IN_CHROME) {
110       platform = IntentPickerAutoDisplayPref::Platform::kChrome;
111     }
112     IntentPickerAutoDisplayService::Get(
113         Profile::FromBrowserContext(web_contents->GetBrowserContext()))
114         ->UpdatePlatformForTablets(url, platform);
115   }
116 
117   const bool should_launch_app =
118       close_reason == apps::IntentPickerCloseReason::OPEN_APP;
119 
120   Profile* profile =
121       Profile::FromBrowserContext(web_contents->GetBrowserContext());
122 
123   apps::AppServiceProxy* proxy =
124       apps::AppServiceProxyFactory::GetForProfile(profile);
125 
126   // If the picker was closed without an app being chosen,
127   // e.g. due to the tab being closed. Keep count of this scenario so we can
128   // stop the UI from showing after 2+ dismissals.
129   if (entry_type == PickerEntryType::kUnknown &&
130       close_reason == IntentPickerCloseReason::DIALOG_DEACTIVATED) {
131     if (ui_auto_display_service)
132       ui_auto_display_service->IncrementCounter(url);
133   }
134 
135   if (should_persist) {
136     // TODO(https://crbug.com/853604): Remove this and convert to a DCHECK
137     // after finding out the root cause.
138     if (launch_name.empty()) {
139       base::debug::DumpWithoutCrashing();
140     } else {
141       proxy->AddPreferredApp(launch_name, url);
142     }
143   }
144 
145   if (should_launch_app) {
146     if (entry_type == PickerEntryType::kWeb) {
147       web_app::ReparentWebContentsIntoAppBrowser(web_contents, launch_name);
148     } else {
149       // TODO(crbug.com/853604): Distinguish the source from link and omnibox.
150       apps::mojom::LaunchSource launch_source =
151           apps::mojom::LaunchSource::kFromLink;
152       proxy->LaunchAppWithUrl(
153           launch_name,
154           GetEventFlags(apps::mojom::LaunchContainer::kLaunchContainerWindow,
155                         WindowOpenDisposition::NEW_WINDOW,
156                         /*prefer_container=*/true),
157           url, launch_source, display::kDefaultDisplayId);
158       CloseOrGoBack(web_contents);
159     }
160   }
161 
162   apps::AppsNavigationThrottle::PickerAction action =
163       apps::AppsNavigationThrottle::GetPickerAction(entry_type, close_reason,
164                                                     should_persist);
165   apps::AppsNavigationThrottle::Platform platform =
166       apps::AppsNavigationThrottle::GetDestinationPlatform(launch_name, action);
167   apps::IntentHandlingMetrics::RecordIntentPickerMetrics(
168       apps::Source::kHttpOrHttps, should_persist, action, platform);
169 }
170 
171 // static
OnAppIconsLoaded(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,std::vector<apps::IntentPickerAppInfo> apps)172 void CommonAppsNavigationThrottle::OnAppIconsLoaded(
173     content::WebContents* web_contents,
174     IntentPickerAutoDisplayService* ui_auto_display_service,
175     const GURL& url,
176     std::vector<apps::IntentPickerAppInfo> apps) {
177   apps::AppsNavigationThrottle::ShowIntentPickerBubbleForApps(
178       web_contents, std::move(apps),
179       /*show_stay_in_chrome=*/true,
180       /*show_remember_selection=*/true,
181       base::BindOnce(&OnIntentPickerClosed, web_contents,
182                      ui_auto_display_service, url));
183 }
184 
CommonAppsNavigationThrottle(content::NavigationHandle * navigation_handle)185 CommonAppsNavigationThrottle::CommonAppsNavigationThrottle(
186     content::NavigationHandle* navigation_handle)
187     : apps::AppsNavigationThrottle(navigation_handle) {}
188 
189 CommonAppsNavigationThrottle::~CommonAppsNavigationThrottle() = default;
190 
191 std::vector<apps::IntentPickerAppInfo>
FindAppsForUrl(content::WebContents * web_contents,const GURL & url,std::vector<apps::IntentPickerAppInfo> apps)192 CommonAppsNavigationThrottle::FindAppsForUrl(
193     content::WebContents* web_contents,
194     const GURL& url,
195     std::vector<apps::IntentPickerAppInfo> apps) {
196   return FindAllAppsForUrl(web_contents, url, std::move(apps));
197 }
198 
199 // static
200 std::vector<apps::IntentPickerAppInfo>
FindAllAppsForUrl(content::WebContents * web_contents,const GURL & url,std::vector<apps::IntentPickerAppInfo> apps)201 CommonAppsNavigationThrottle::FindAllAppsForUrl(
202     content::WebContents* web_contents,
203     const GURL& url,
204     std::vector<apps::IntentPickerAppInfo> apps) {
205   Profile* profile =
206       Profile::FromBrowserContext(web_contents->GetBrowserContext());
207 
208   apps::AppServiceProxy* proxy =
209       apps::AppServiceProxyFactory::GetForProfile(profile);
210 
211   std::vector<std::string> app_ids =
212       proxy->GetAppIdsForUrl(url, /*exclude_browser=*/true);
213 
214   for (const std::string& app_id : app_ids) {
215     proxy->AppRegistryCache().ForOneApp(
216         app_id, [&apps](const apps::AppUpdate& update) {
217           apps.emplace(apps.begin(), GetPickerEntryType(update.AppType()),
218                        gfx::Image(), update.AppId(), update.Name());
219         });
220   }
221   return apps;
222 }
223 
224 apps::AppsNavigationThrottle::PickerShowState
GetPickerShowState(const std::vector<apps::IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)225 CommonAppsNavigationThrottle::GetPickerShowState(
226     const std::vector<apps::IntentPickerAppInfo>& apps_for_picker,
227     content::WebContents* web_contents,
228     const GURL& url) {
229   return ShouldAutoDisplayUi(apps_for_picker, web_contents, url) &&
230                  navigate_from_link()
231              ? PickerShowState::kPopOut
232              : PickerShowState::kOmnibox;
233 }
234 
GetOnPickerClosedCallback(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)235 IntentPickerResponse CommonAppsNavigationThrottle::GetOnPickerClosedCallback(
236     content::WebContents* web_contents,
237     IntentPickerAutoDisplayService* ui_auto_display_service,
238     const GURL& url) {
239   return base::BindOnce(&OnIntentPickerClosed, web_contents,
240                         ui_auto_display_service, url);
241 }
242 
ShouldCancelNavigation(content::NavigationHandle * handle)243 bool CommonAppsNavigationThrottle::ShouldCancelNavigation(
244     content::NavigationHandle* handle) {
245   content::WebContents* web_contents = handle->GetWebContents();
246 
247   const GURL& url = handle->GetURL();
248 
249   Profile* profile =
250       Profile::FromBrowserContext(web_contents->GetBrowserContext());
251 
252   apps::AppServiceProxy* proxy =
253       apps::AppServiceProxyFactory::GetForProfile(profile);
254 
255   std::vector<std::string> app_ids =
256       proxy->GetAppIdsForUrl(url, /*exclude_browser=*/true);
257 
258   if (app_ids.empty())
259     return false;
260 
261   if (navigate_from_link()) {
262     auto preferred_app_id = proxy->PreferredApps().FindPreferredAppForUrl(url);
263 
264     if (preferred_app_id.has_value() &&
265         base::Contains(app_ids, preferred_app_id.value())) {
266       // Only automatically launch PWA if the flag is on.
267       auto app_type =
268           proxy->AppRegistryCache().GetAppType(preferred_app_id.value());
269       if (app_type == apps::mojom::AppType::kArc ||
270           (app_type == apps::mojom::AppType::kWeb &&
271            base::FeatureList::IsEnabled(
272                features::kIntentPickerPWAPersistence))) {
273         auto launch_source = apps::mojom::LaunchSource::kFromLink;
274         proxy->LaunchAppWithUrl(
275             preferred_app_id.value(),
276             GetEventFlags(apps::mojom::LaunchContainer::kLaunchContainerWindow,
277                           WindowOpenDisposition::NEW_WINDOW,
278                           /*prefer_container=*/true),
279             url, launch_source, display::kDefaultDisplayId);
280         CloseOrGoBack(web_contents);
281         apps::IntentHandlingMetrics::RecordIntentPickerUserInteractionMetrics(
282             /*selected_app_package=*/preferred_app_id.value(),
283             GetPickerEntryType(app_type),
284             apps::IntentPickerCloseReason::PREFERRED_APP_FOUND,
285             apps::Source::kHttpOrHttps, /*should_persist=*/false);
286         return true;
287       }
288     }
289   }
290   return false;
291 }
292 
ShowIntentPickerForApps(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,std::vector<IntentPickerAppInfo> apps,IntentPickerResponse callback)293 void CommonAppsNavigationThrottle::ShowIntentPickerForApps(
294     content::WebContents* web_contents,
295     IntentPickerAutoDisplayService* ui_auto_display_service,
296     const GURL& url,
297     std::vector<IntentPickerAppInfo> apps,
298     IntentPickerResponse callback) {
299   if (apps.empty()) {
300     IntentPickerTabHelper::SetShouldShowIcon(web_contents, false);
301     ui_displayed_ = false;
302     apps::IntentHandlingMetrics::RecordIntentPickerUserInteractionMetrics(
303         /*selected_app_package=*/std::string(), apps::PickerEntryType::kUnknown,
304         apps::IntentPickerCloseReason::ERROR_BEFORE_PICKER,
305         apps::Source::kHttpOrHttps, /*should_persist=*/false);
306     return;
307   }
308 
309   if (GetPickerShowState(apps, web_contents, url) ==
310       PickerShowState::kOmnibox) {
311     ui_displayed_ = false;
312     IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
313     return;
314   }
315 
316   ui_displayed_ = true;
317   IntentPickerTabHelper::LoadAppIcons(
318       web_contents, std::move(apps),
319       base::BindOnce(&OnAppIconsLoaded, web_contents, ui_auto_display_service,
320                      url));
321 }
322 
ShouldAutoDisplayUi(const std::vector<apps::IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)323 bool CommonAppsNavigationThrottle::ShouldAutoDisplayUi(
324     const std::vector<apps::IntentPickerAppInfo>& apps_for_picker,
325     content::WebContents* web_contents,
326     const GURL& url) {
327   // On devices with tablet form factor we should not pop out the intent
328   // picker if Chrome has been chosen by the user as the platform for this URL.
329   if (chromeos::switches::IsTabletFormFactor()) {
330     if (ui_auto_display_service_->GetLastUsedPlatformForTablets(url) ==
331         IntentPickerAutoDisplayPref::Platform::kChrome) {
332       return false;
333     }
334   }
335 
336   // If we only have PWAs in the app list, do not show the intent picker.
337   // Instead just show the omnibox icon. This is to reduce annoyance to users
338   // until "Remember my choice" is available for desktop PWAs.
339   // TODO(crbug.com/826982): show the intent picker when the app registry is
340   // available to persist "Remember my choice" for PWAs.
341   if (!base::FeatureList::IsEnabled(features::kIntentPickerPWAPersistence) &&
342       ContainsOnlyPwasAndMacApps(apps_for_picker)) {
343     return false;
344   }
345 
346   // If the preferred app is use browser, do not show the intent picker.
347   Profile* profile =
348       Profile::FromBrowserContext(web_contents->GetBrowserContext());
349 
350   apps::AppServiceProxy* proxy =
351       apps::AppServiceProxyFactory::GetForProfile(profile);
352 
353   if (proxy) {
354     auto preferred_app_id = proxy->PreferredApps().FindPreferredAppForUrl(url);
355     if (preferred_app_id.has_value() &&
356         preferred_app_id.value() == kUseBrowserForLink) {
357       apps::IntentHandlingMetrics::RecordIntentPickerUserInteractionMetrics(
358           /*selected_app_package=*/preferred_app_id.value(),
359           apps::PickerEntryType::kUnknown,
360           apps::IntentPickerCloseReason::PREFERRED_APP_FOUND,
361           apps::Source::kHttpOrHttps, /*should_persist=*/false);
362       return false;
363     }
364   }
365 
366   return AppsNavigationThrottle::ShouldAutoDisplayUi(apps_for_picker,
367                                                      web_contents, url);
368 }
369 
370 }  // namespace apps
371