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/apps/intent_helper/apps_navigation_throttle.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/bind.h"
11 #include "base/optional.h"
12 #include "chrome/browser/apps/app_service/app_launch_params.h"
13 #include "chrome/browser/apps/app_service/app_service_proxy.h"
14 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
15 #include "chrome/browser/apps/app_service/browser_app_launcher.h"
16 #include "chrome/browser/apps/intent_helper/intent_picker_auto_display_service.h"
17 #include "chrome/browser/apps/intent_helper/page_transition_util.h"
18 #include "chrome/browser/prefetch/no_state_prefetch/chrome_prerender_contents_delegate.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/browser_finder.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #include "chrome/browser/ui/intent_picker_tab_helper.h"
24 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
25 #include "chrome/browser/web_applications/components/app_icon_manager.h"
26 #include "chrome/browser/web_applications/components/app_registrar.h"
27 #include "chrome/browser/web_applications/components/web_app_helpers.h"
28 #include "chrome/browser/web_applications/components/web_app_provider_base.h"
29 #include "chrome/browser/web_applications/components/web_app_tab_helper_base.h"
30 #include "chrome/common/chrome_features.h"
31 #include "components/no_state_prefetch/browser/prerender_contents.h"
32 #include "components/page_load_metrics/browser/page_load_metrics_util.h"
33 #include "content/public/browser/browser_context.h"
34 #include "content/public/browser/browser_thread.h"
35 #include "content/public/browser/navigation_handle.h"
36 #include "content/public/browser/site_instance.h"
37 #include "content/public/browser/web_contents.h"
38 #include "extensions/common/constants.h"
39 #include "third_party/blink/public/mojom/loader/referrer.mojom.h"
40 #include "ui/gfx/image/image.h"
41 #include "url/origin.h"
42
43 namespace {
44
45 using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
46
47 // Returns true if |url| is a known and valid redirector that will redirect a
48 // navigation elsewhere.
IsGoogleRedirectorUrl(const GURL & url)49 bool IsGoogleRedirectorUrl(const GURL& url) {
50 // This currently only check for redirectors on the "google" domain.
51 if (!page_load_metrics::IsGoogleSearchHostname(url))
52 return false;
53
54 return url.path_piece() == "/url" && url.has_query();
55 }
56
57 // Compares the host name of the referrer and target URL to decide whether
58 // the navigation needs to be overridden.
ShouldOverrideUrlLoading(const GURL & previous_url,const GURL & current_url)59 bool ShouldOverrideUrlLoading(const GURL& previous_url,
60 const GURL& current_url) {
61 // When the navigation is initiated in a web page where sending a referrer
62 // is disabled, |previous_url| can be empty. In this case, we should open
63 // it in the desktop browser.
64 if (!previous_url.is_valid() || previous_url.is_empty())
65 return false;
66
67 // Also check |current_url| just in case.
68 if (!current_url.is_valid() || current_url.is_empty()) {
69 DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome.";
70 return false;
71 }
72
73 // Check the scheme for both |previous_url| and |current_url| since an
74 // extension could have referred us (e.g. Google Docs).
75 if (!current_url.SchemeIsHTTPOrHTTPS() ||
76 previous_url.SchemeIs(extensions::kExtensionScheme)) {
77 return false;
78 }
79
80 // Skip URL redirectors that are intermediate pages redirecting towards a
81 // final URL.
82 if (IsGoogleRedirectorUrl(current_url))
83 return false;
84
85 return true;
86 }
87
GetStartingGURL(content::NavigationHandle * navigation_handle)88 GURL GetStartingGURL(content::NavigationHandle* navigation_handle) {
89 // This helps us determine a reference GURL for the current NavigationHandle.
90 // This is the order or preference: Referrer > LastCommittedURL >
91 // InitiatorOrigin. InitiatorOrigin *should* only be used on very rare cases,
92 // e.g. when the navigation goes from https: to http: on a new tab, thus
93 // losing the other potential referrers.
94 const GURL referrer_url = navigation_handle->GetReferrer().url;
95 if (referrer_url.is_valid() && !referrer_url.is_empty())
96 return referrer_url;
97
98 const GURL last_committed_url =
99 navigation_handle->GetWebContents()->GetLastCommittedURL();
100 if (last_committed_url.is_valid() && !last_committed_url.is_empty())
101 return last_committed_url;
102
103 const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
104 return initiator_origin.has_value() ? initiator_origin->GetURL() : GURL();
105 }
106
InAppBrowser(content::WebContents * web_contents)107 bool InAppBrowser(content::WebContents* web_contents) {
108 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
109 return !browser || browser->deprecated_is_app();
110 }
111
112 } // namespace
113
114 namespace apps {
115
116 // static
117 const char AppsNavigationThrottle::kUseBrowserForLink[] = "use_browser";
118
119 // static
120 std::unique_ptr<content::NavigationThrottle>
MaybeCreate(content::NavigationHandle * handle)121 AppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
122 if (!handle->IsInMainFrame())
123 return nullptr;
124
125 content::WebContents* web_contents = handle->GetWebContents();
126 if (!CanCreate(web_contents))
127 return nullptr;
128
129 return std::make_unique<AppsNavigationThrottle>(handle);
130 }
131
132 // static
ShowIntentPickerBubble(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)133 void AppsNavigationThrottle::ShowIntentPickerBubble(
134 content::WebContents* web_contents,
135 IntentPickerAutoDisplayService* ui_auto_display_service,
136 const GURL& url) {
137 std::vector<IntentPickerAppInfo> apps = FindPwaForUrl(web_contents, url, {});
138
139 bool show_persistence_options = ShouldShowPersistenceOptions(apps);
140 ShowIntentPickerBubbleForApps(
141 web_contents, std::move(apps),
142 /*show_stay_in_chrome=*/show_persistence_options,
143 /*show_remember_selection=*/show_persistence_options,
144 base::BindOnce(&OnIntentPickerClosed, web_contents,
145 ui_auto_display_service, url));
146 }
147
148 // static
OnIntentPickerClosed(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,const std::string & launch_name,PickerEntryType entry_type,IntentPickerCloseReason close_reason,bool should_persist)149 void AppsNavigationThrottle::OnIntentPickerClosed(
150 content::WebContents* web_contents,
151 IntentPickerAutoDisplayService* ui_auto_display_service,
152 const GURL& url,
153 const std::string& launch_name,
154 PickerEntryType entry_type,
155 IntentPickerCloseReason close_reason,
156 bool should_persist) {
157 const bool should_launch_app =
158 close_reason == IntentPickerCloseReason::OPEN_APP;
159 switch (entry_type) {
160 case PickerEntryType::kWeb:
161 if (should_launch_app)
162 web_app::ReparentWebContentsIntoAppBrowser(web_contents, launch_name);
163 break;
164 case PickerEntryType::kUnknown:
165 // We reach here if the picker was closed without an app being chosen,
166 // e.g. due to the tab being closed. Keep count of this scenario so we can
167 // stop the UI from showing after 2+ dismissals.
168 if (close_reason == IntentPickerCloseReason::DIALOG_DEACTIVATED) {
169 if (ui_auto_display_service)
170 ui_auto_display_service->IncrementCounter(url);
171 }
172 break;
173 case PickerEntryType::kArc:
174 case PickerEntryType::kDevice:
175 case PickerEntryType::kMacOs:
176 NOTREACHED();
177 }
178 }
179
180 // static
IsGoogleRedirectorUrlForTesting(const GURL & url)181 bool AppsNavigationThrottle::IsGoogleRedirectorUrlForTesting(const GURL& url) {
182 return IsGoogleRedirectorUrl(url);
183 }
184
185 // static
ShouldOverrideUrlLoadingForTesting(const GURL & previous_url,const GURL & current_url)186 bool AppsNavigationThrottle::ShouldOverrideUrlLoadingForTesting(
187 const GURL& previous_url,
188 const GURL& current_url) {
189 return ShouldOverrideUrlLoading(previous_url, current_url);
190 }
191
192 // static
ShowIntentPickerBubbleForApps(content::WebContents * web_contents,std::vector<IntentPickerAppInfo> apps,bool show_stay_in_chrome,bool show_remember_selection,IntentPickerResponse callback)193 void AppsNavigationThrottle::ShowIntentPickerBubbleForApps(
194 content::WebContents* web_contents,
195 std::vector<IntentPickerAppInfo> apps,
196 bool show_stay_in_chrome,
197 bool show_remember_selection,
198 IntentPickerResponse callback) {
199 if (apps.empty())
200 return;
201
202 // It should be safe to bind |web_contents| since closing the current tab will
203 // close the intent picker and run the callback prior to the WebContents being
204 // deallocated.
205 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
206 if (!browser)
207 return;
208
209 IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
210 browser->window()->ShowIntentPickerBubble(
211 std::move(apps), show_stay_in_chrome, show_remember_selection,
212 PageActionIconType::kIntentPicker, base::nullopt, std::move(callback));
213 }
214
AppsNavigationThrottle(content::NavigationHandle * navigation_handle)215 AppsNavigationThrottle::AppsNavigationThrottle(
216 content::NavigationHandle* navigation_handle)
217 : content::NavigationThrottle(navigation_handle),
218 ui_displayed_(false),
219 ui_auto_display_service_(
220 IntentPickerAutoDisplayService::Get(Profile::FromBrowserContext(
221 navigation_handle->GetWebContents()->GetBrowserContext()))),
222 navigate_from_link_(false) {
223 // |ui_auto_display_service_| can be null iff the call is coming from
224 // IntentPickerView. Since the pointer to our service is never modified
225 // (in case it is successfully created here) this check covers all the
226 // non-static methods in this class.
227 DCHECK(ui_auto_display_service_);
228 }
229
230 AppsNavigationThrottle::~AppsNavigationThrottle() = default;
231
GetNameForLogging()232 const char* AppsNavigationThrottle::GetNameForLogging() {
233 return "AppsNavigationThrottle";
234 }
235
WillStartRequest()236 ThrottleCheckResult AppsNavigationThrottle::WillStartRequest() {
237 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
238 starting_url_ = GetStartingGURL(navigation_handle());
239 IntentPickerTabHelper::SetShouldShowIcon(
240 navigation_handle()->GetWebContents(), false);
241 return HandleRequest();
242 }
243
WillRedirectRequest()244 ThrottleCheckResult AppsNavigationThrottle::WillRedirectRequest() {
245 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
246
247 // TODO(dominickn): Consider what to do when there is another URL during the
248 // same navigation that could be handled by apps. Two ideas are:
249 // 1) update the bubble with a mix of both app candidates (if different)
250 // 2) show a bubble based on the last url, thus closing all the previous ones
251 if (ui_displayed_)
252 return content::NavigationThrottle::PROCEED;
253
254 return HandleRequest();
255 }
256
257 // static
CanCreate(content::WebContents * web_contents)258 bool AppsNavigationThrottle::CanCreate(content::WebContents* web_contents) {
259 // Do not create the throttle if no apps can be installed.
260 // Do not create the throttle in incognito or for a prerender navigation.
261 if (web_contents->GetBrowserContext()->IsOffTheRecord() ||
262 prerender::ChromePrerenderContentsDelegate::FromWebContents(
263 web_contents) != nullptr) {
264 return false;
265 }
266
267 // Do not create the throttle if we are already in an app browser.
268 // It is possible that the web contents is not inserted to tab strip
269 // model at this stage (e.g. open url in new tab). So if we cannot
270 // find a browser at this moment, skip the check and this will be handled
271 // in later stage.
272 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
273 if (browser && browser->deprecated_is_app())
274 return false;
275
276 return true;
277 }
278
279 // static
GetDestinationPlatform(const std::string & selected_launch_name,PickerAction picker_action)280 AppsNavigationThrottle::Platform AppsNavigationThrottle::GetDestinationPlatform(
281 const std::string& selected_launch_name,
282 PickerAction picker_action) {
283 switch (picker_action) {
284 case PickerAction::ARC_APP_PRESSED:
285 case PickerAction::ARC_APP_PREFERRED_PRESSED:
286 case PickerAction::PREFERRED_ARC_ACTIVITY_FOUND:
287 return Platform::ARC;
288 case PickerAction::PWA_APP_PRESSED:
289 case PickerAction::PWA_APP_PREFERRED_PRESSED:
290 case PickerAction::PREFERRED_PWA_FOUND:
291 return Platform::PWA;
292 case PickerAction::MAC_OS_APP_PRESSED:
293 return Platform::MAC_OS;
294 case PickerAction::ERROR_BEFORE_PICKER:
295 case PickerAction::ERROR_AFTER_PICKER:
296 case PickerAction::DIALOG_DEACTIVATED:
297 case PickerAction::CHROME_PRESSED:
298 case PickerAction::CHROME_PREFERRED_PRESSED:
299 case PickerAction::PREFERRED_CHROME_BROWSER_FOUND:
300 return Platform::CHROME;
301 case PickerAction::DEVICE_PRESSED:
302 return Platform::DEVICE;
303 case PickerAction::OBSOLETE_ALWAYS_PRESSED:
304 case PickerAction::OBSOLETE_JUST_ONCE_PRESSED:
305 case PickerAction::INVALID:
306 break;
307 }
308 NOTREACHED();
309 return Platform::ARC;
310 }
311
312 // static
GetPickerAction(PickerEntryType entry_type,IntentPickerCloseReason close_reason,bool should_persist)313 AppsNavigationThrottle::PickerAction AppsNavigationThrottle::GetPickerAction(
314 PickerEntryType entry_type,
315 IntentPickerCloseReason close_reason,
316 bool should_persist) {
317 switch (close_reason) {
318 case IntentPickerCloseReason::ERROR_BEFORE_PICKER:
319 return PickerAction::ERROR_BEFORE_PICKER;
320 case IntentPickerCloseReason::ERROR_AFTER_PICKER:
321 return PickerAction::ERROR_AFTER_PICKER;
322 case IntentPickerCloseReason::DIALOG_DEACTIVATED:
323 return PickerAction::DIALOG_DEACTIVATED;
324 case IntentPickerCloseReason::PREFERRED_APP_FOUND:
325 switch (entry_type) {
326 case PickerEntryType::kUnknown:
327 return PickerAction::PREFERRED_CHROME_BROWSER_FOUND;
328 case PickerEntryType::kArc:
329 return PickerAction::PREFERRED_ARC_ACTIVITY_FOUND;
330 case PickerEntryType::kWeb:
331 return PickerAction::PREFERRED_PWA_FOUND;
332 case PickerEntryType::kDevice:
333 case PickerEntryType::kMacOs:
334 NOTREACHED();
335 return PickerAction::INVALID;
336 }
337 case IntentPickerCloseReason::STAY_IN_CHROME:
338 return should_persist ? PickerAction::CHROME_PREFERRED_PRESSED
339 : PickerAction::CHROME_PRESSED;
340 case IntentPickerCloseReason::OPEN_APP:
341 switch (entry_type) {
342 case PickerEntryType::kUnknown:
343 NOTREACHED();
344 return PickerAction::INVALID;
345 case PickerEntryType::kArc:
346 return should_persist ? PickerAction::ARC_APP_PREFERRED_PRESSED
347 : PickerAction::ARC_APP_PRESSED;
348 case PickerEntryType::kWeb:
349 return should_persist ? PickerAction::PWA_APP_PREFERRED_PRESSED
350 : PickerAction::PWA_APP_PRESSED;
351 case PickerEntryType::kDevice:
352 return PickerAction::DEVICE_PRESSED;
353 case PickerEntryType::kMacOs:
354 return PickerAction::MAC_OS_APP_PRESSED;
355 }
356 }
357
358 NOTREACHED();
359 return PickerAction::INVALID;
360 }
361
FindAppsForUrl(content::WebContents * web_contents,const GURL & url,std::vector<IntentPickerAppInfo> apps)362 std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindAppsForUrl(
363 content::WebContents* web_contents,
364 const GURL& url,
365 std::vector<IntentPickerAppInfo> apps) {
366 return FindPwaForUrl(web_contents, url, std::move(apps));
367 }
368
369 // static
FindPwaForUrl(content::WebContents * web_contents,const GURL & url,std::vector<IntentPickerAppInfo> apps)370 std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindPwaForUrl(
371 content::WebContents* web_contents,
372 const GURL& url,
373 std::vector<IntentPickerAppInfo> apps) {
374 // Check if the current URL has an installed desktop PWA, and add that to
375 // the list of apps if it exists.
376 Profile* const profile =
377 Profile::FromBrowserContext(web_contents->GetBrowserContext());
378
379 base::Optional<web_app::AppId> app_id =
380 web_app::FindInstalledAppWithUrlInScope(profile, url,
381 /*window_only=*/true);
382 if (!app_id)
383 return apps;
384
385 auto* const provider = web_app::WebAppProviderBase::GetProviderBase(profile);
386 gfx::Image icon = gfx::Image::CreateFrom1xBitmap(
387 provider->icon_manager().GetFavicon(*app_id));
388
389 // Prefer the web and place apps of type PWA before apps of type ARC.
390 // TODO(crbug.com/824598): deterministically sort this list.
391 apps.emplace(apps.begin(), PickerEntryType::kWeb, icon, *app_id,
392 provider->registrar().GetAppShortName(*app_id));
393
394 return apps;
395 }
396
397 // static
CloseOrGoBack(content::WebContents * web_contents)398 void AppsNavigationThrottle::CloseOrGoBack(content::WebContents* web_contents) {
399 DCHECK(web_contents);
400 if (web_contents->GetController().CanGoBack())
401 web_contents->GetController().GoBack();
402 else
403 web_contents->ClosePage();
404 }
405
406 // static
ContainsOnlyPwasAndMacApps(const std::vector<apps::IntentPickerAppInfo> & apps)407 bool AppsNavigationThrottle::ContainsOnlyPwasAndMacApps(
408 const std::vector<apps::IntentPickerAppInfo>& apps) {
409 return std::all_of(apps.begin(), apps.end(),
410 [](const apps::IntentPickerAppInfo& app_info) {
411 return app_info.type == PickerEntryType::kWeb ||
412 app_info.type == PickerEntryType::kMacOs;
413 });
414 }
415
416 // static
ShouldShowPersistenceOptions(std::vector<apps::IntentPickerAppInfo> & apps)417 bool AppsNavigationThrottle::ShouldShowPersistenceOptions(
418 std::vector<apps::IntentPickerAppInfo>& apps) {
419 // There is no support persistence for PWA so the selection should be hidden
420 // if only PWAs are present.
421 // TODO(crbug.com/826982): Provide the "Remember my choice" option when the
422 // app registry can support persistence for PWAs.
423 // This function is also used to hide the "Stay In Chrome" button when the
424 // "Remember my choice" option is hidden such that the bubble is easy to
425 // understand.
426 // TODO(avi): When Chrome gains a UI for managing the persistence of PWAs,
427 // reuse that UI for managing the persistent behavior of Universal Links.
428 return !ContainsOnlyPwasAndMacApps(apps);
429 }
430
ShouldCancelNavigation(content::NavigationHandle * handle)431 bool AppsNavigationThrottle::ShouldCancelNavigation(
432 content::NavigationHandle* handle) {
433 return false;
434 }
435
ShouldDeferNavigation(content::NavigationHandle * handle)436 bool AppsNavigationThrottle::ShouldDeferNavigation(
437 content::NavigationHandle* handle) {
438 return false;
439 }
440
ShowIntentPickerForApps(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url,std::vector<IntentPickerAppInfo> apps,IntentPickerResponse callback)441 void AppsNavigationThrottle::ShowIntentPickerForApps(
442 content::WebContents* web_contents,
443 IntentPickerAutoDisplayService* ui_auto_display_service,
444 const GURL& url,
445 std::vector<IntentPickerAppInfo> apps,
446 IntentPickerResponse callback) {
447 if (apps.empty()) {
448 IntentPickerTabHelper::SetShouldShowIcon(web_contents, false);
449 ui_displayed_ = false;
450 return;
451 }
452 IntentPickerTabHelper::SetShouldShowIcon(web_contents, true);
453 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
454 if (!browser)
455 return;
456 const PickerShowState picker_show_state =
457 GetPickerShowState(apps, web_contents, url);
458 switch (picker_show_state) {
459 case PickerShowState::kOmnibox:
460 ui_displayed_ = false;
461 break;
462 case PickerShowState::kPopOut: {
463 bool show_persistence_options = ShouldShowPersistenceOptions(apps);
464 ShowIntentPickerBubbleForApps(
465 web_contents, std::move(apps),
466 /*show_stay_in_chrome=*/show_persistence_options,
467 /*show_remember_selection=*/show_persistence_options,
468 std::move(callback));
469 break;
470 }
471 default:
472 NOTREACHED();
473 }
474 }
475
476 AppsNavigationThrottle::PickerShowState
GetPickerShowState(const std::vector<IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)477 AppsNavigationThrottle::GetPickerShowState(
478 const std::vector<IntentPickerAppInfo>& apps_for_picker,
479 content::WebContents* web_contents,
480 const GURL& url) {
481 return PickerShowState::kOmnibox;
482 }
483
GetOnPickerClosedCallback(content::WebContents * web_contents,IntentPickerAutoDisplayService * ui_auto_display_service,const GURL & url)484 IntentPickerResponse AppsNavigationThrottle::GetOnPickerClosedCallback(
485 content::WebContents* web_contents,
486 IntentPickerAutoDisplayService* ui_auto_display_service,
487 const GURL& url) {
488 return base::BindOnce(&OnIntentPickerClosed, web_contents,
489 ui_auto_display_service, url);
490 }
491
navigate_from_link() const492 bool AppsNavigationThrottle::navigate_from_link() const {
493 return navigate_from_link_;
494 }
495
ShouldAutoDisplayUi(const std::vector<apps::IntentPickerAppInfo> & apps_for_picker,content::WebContents * web_contents,const GURL & url)496 bool AppsNavigationThrottle::ShouldAutoDisplayUi(
497 const std::vector<apps::IntentPickerAppInfo>& apps_for_picker,
498 content::WebContents* web_contents,
499 const GURL& url) {
500 if (apps_for_picker.empty())
501 return false;
502
503 if (InAppBrowser(web_contents))
504 return false;
505
506 if (!ShouldOverrideUrlLoading(starting_url_, url))
507 return false;
508
509 DCHECK(ui_auto_display_service_);
510 return ui_auto_display_service_->ShouldAutoDisplayUi(url);
511 }
512
HandleRequest()513 ThrottleCheckResult AppsNavigationThrottle::HandleRequest() {
514 content::NavigationHandle* handle = navigation_handle();
515 // If the navigation won't update the current document, don't check intent for
516 // the navigation.
517 if (handle->IsSameDocument())
518 return content::NavigationThrottle::PROCEED;
519
520 DCHECK(!ui_displayed_);
521
522 navigate_from_link_ = false;
523
524 // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
525 // don't have a way to send POST data to ARC, and 2) intercepting http(s) form
526 // submissions is not very important because such submissions are usually
527 // done within the same domain. ShouldOverrideUrlLoading() below filters out
528 // such submissions anyway.
529 constexpr bool kAllowFormSubmit = false;
530
531 // Ignore navigations with the CLIENT_REDIRECT qualifier on.
532 constexpr bool kAllowClientRedirect = true;
533
534 ui::PageTransition page_transition = handle->GetPageTransition();
535 content::WebContents* web_contents = handle->GetWebContents();
536 const GURL& url = handle->GetURL();
537 if (!ShouldIgnoreNavigation(page_transition, kAllowFormSubmit,
538 kAllowClientRedirect) &&
539 !handle->WasStartedFromContextMenu()) {
540 navigate_from_link_ = true;
541 }
542
543 MaybeRemoveComingFromArcFlag(web_contents, starting_url_, url);
544
545 base::Optional<ThrottleCheckResult> tab_strip_capture =
546 CaptureExperimentalTabStripWebAppScopeNavigations(web_contents, handle);
547 if (tab_strip_capture.has_value())
548 return tab_strip_capture.value();
549
550 // Do not pop up the intent picker bubble or automatically launch the app if
551 // we shouldn't override url loading, or if we don't have a browser, or we are
552 // already in an app browser.
553 if (ShouldOverrideUrlLoading(starting_url_, url) &&
554 !InAppBrowser(web_contents)) {
555 // Handles apps that are automatically launched and the navigation needs to
556 // be cancelled. This only applies on the new intent picker system, because
557 // we don't need to defer the navigation to find out preferred app anymore.
558 if (ShouldCancelNavigation(handle)) {
559 return content::NavigationThrottle::CANCEL_AND_IGNORE;
560 }
561
562 if (ShouldDeferNavigation(handle)) {
563 // Handling is now deferred to ArcIntentPickerAppFetcher, which
564 // asynchronously queries ARC for apps, and runs
565 // OnDeferredNavigationProcessed() with an action based on whether an
566 // acceptable app was found and user consent to open received. We assume
567 // the UI is shown or a preferred app was found; reset to false if we
568 // resume the navigation.
569 ui_displayed_ = true;
570 return content::NavigationThrottle::DEFER;
571 }
572 }
573
574 // We didn't query ARC, so proceed with the navigation and query if we have an
575 // installed desktop PWA to handle the URL.
576 std::vector<IntentPickerAppInfo> apps = FindAppsForUrl(web_contents, url, {});
577
578 if (!apps.empty())
579 ui_displayed_ = true;
580
581 ShowIntentPickerForApps(
582 web_contents, ui_auto_display_service_, url, std::move(apps),
583 GetOnPickerClosedCallback(web_contents, ui_auto_display_service_, url));
584
585 return content::NavigationThrottle::PROCEED;
586 }
587
588 base::Optional<ThrottleCheckResult>
CaptureExperimentalTabStripWebAppScopeNavigations(content::WebContents * web_contents,content::NavigationHandle * handle) const589 AppsNavigationThrottle::CaptureExperimentalTabStripWebAppScopeNavigations(
590 content::WebContents* web_contents,
591 content::NavigationHandle* handle) const {
592 if (!navigate_from_link())
593 return base::nullopt;
594
595 if (!base::FeatureList::IsEnabled(features::kDesktopPWAsTabStrip) ||
596 !base::FeatureList::IsEnabled(
597 features::kDesktopPWAsTabStripLinkCapturing)) {
598 return base::nullopt;
599 }
600
601 Profile* const profile =
602 Profile::FromBrowserContext(web_contents->GetBrowserContext());
603 web_app::WebAppProviderBase* provider =
604 web_app::WebAppProviderBase::GetProviderBase(profile);
605 if (!provider)
606 return base::nullopt;
607
608 base::Optional<web_app::AppId> app_id =
609 provider->registrar().FindInstalledAppWithUrlInScope(
610 handle->GetURL(), /*window_only=*/true);
611 if (!app_id)
612 return base::nullopt;
613
614 if (!provider->registrar().IsInExperimentalTabbedWindowMode(*app_id))
615 return base::nullopt;
616
617 Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
618 if (web_app::AppBrowserController::IsForWebAppBrowser(browser, *app_id)) {
619 // Already in the app window; navigation already captured.
620 return base::nullopt;
621 }
622
623 // If |web_contents| hasn't loaded yet or has only loaded about:blank we
624 // should reparent it into the app window to avoid leaving behind a blank tab.
625 auto* tab_helper =
626 web_app::WebAppTabHelperBase::FromWebContents(web_contents);
627 if (tab_helper && !tab_helper->HasLoadedNonAboutBlankPage()) {
628 web_app::ReparentWebContentsIntoAppBrowser(web_contents, *app_id);
629 return content::NavigationThrottle::PROCEED;
630 }
631
632 apps::AppLaunchParams launch_params(
633 *app_id, apps::mojom::LaunchContainer::kLaunchContainerWindow,
634 WindowOpenDisposition::CURRENT_TAB,
635 apps::mojom::AppLaunchSource::kSourceUrlHandler);
636 launch_params.override_url = handle->GetURL();
637 apps::AppServiceProxyFactory::GetForProfile(profile)
638 ->BrowserAppLauncher()
639 ->LaunchAppWithParams(std::move(launch_params));
640 return content::NavigationThrottle::CANCEL_AND_IGNORE;
641 }
642
643 } // namespace apps
644