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