blob: 67443b8557d1d2341658479ce2066c4639b927b9 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/apps/intent_helper/apps_navigation_throttle.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/post_task.h"
#include "chrome/browser/chromeos/apps/intent_helper/apps_navigation_types.h"
#include "chrome/browser/chromeos/apps/intent_helper/intent_picker_auto_display_service.h"
#include "chrome/browser/chromeos/apps/intent_helper/page_transition_util.h"
#include "chrome/browser/chromeos/arc/arc_util.h"
#include "chrome/browser/chromeos/arc/arc_web_contents_data.h"
#include "chrome/browser/chromeos/arc/intent_helper/arc_navigation_throttle.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/prerender/prerender_contents.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/common/chrome_features.h"
#include "chrome/services/app_service/public/mojom/types.mojom.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/arc/metrics/arc_metrics_constants.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "url/origin.h"
namespace {
constexpr char kGoogleCom[] = "google.com";
// Removes the flag signaling that the current tab was started via
// ChromeShellDelegate if the current throttle corresponds to a navigation
// passing thru different domains or schemes, except if |current_url| has a
// scheme different than http(s).
void MaybeRemoveComingFromArcFlag(content::WebContents* web_contents,
const GURL& previous_url,
const GURL& current_url) {
// Let ArcExternalProtocolDialog handle these cases.
if (!current_url.SchemeIsHTTPOrHTTPS())
return;
if (url::Origin::Create(previous_url)
.IsSameOriginWith(url::Origin::Create(current_url))) {
return;
}
const char* key =
arc::ArcWebContentsData::ArcWebContentsData::kArcTransitionFlag;
arc::ArcWebContentsData* arc_data =
static_cast<arc::ArcWebContentsData*>(web_contents->GetUserData(key));
if (arc_data)
web_contents->RemoveUserData(key);
}
// Compares the host name of the referrer and target URL to decide whether
// the navigation needs to be overridden.
bool ShouldOverrideUrlLoading(const GURL& previous_url,
const GURL& current_url) {
// When the navigation is initiated in a web page where sending a referrer
// is disabled, |previous_url| can be empty. In this case, we should open
// it in the desktop browser.
if (!previous_url.is_valid() || previous_url.is_empty())
return false;
// Also check |current_url| just in case.
if (!current_url.is_valid() || current_url.is_empty()) {
DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome.";
return false;
}
// Check the scheme for both |previous_url| and |current_url| since an
// extension could have referred us (e.g. Google Docs).
if (!current_url.SchemeIsHTTPOrHTTPS() ||
!previous_url.SchemeIsHTTPOrHTTPS()) {
return false;
}
// TODO(dominickn): this was added as a special case for ARC. Reconsider if
// it's necessary for all app platforms.
if (net::registry_controlled_domains::SameDomainOrHost(
current_url, previous_url,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
if (net::registry_controlled_domains::GetDomainAndRegistry(
current_url,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) ==
kGoogleCom) {
// Navigation within the google.com domain are good candidates for this
// throttle (and consecuently the picker UI) only if they have different
// hosts, this is because multiple services are hosted within the same
// domain e.g. play.google.com, mail.google.com and so on.
return current_url.host_piece() != previous_url.host_piece();
}
return false;
}
return true;
}
GURL GetStartingGURL(content::NavigationHandle* navigation_handle) {
// This helps us determine a reference GURL for the current NavigationHandle.
// This is the order or preferrence: Referrer > LastCommittedURL > SiteURL,
// GetSiteURL *should* only be used on very rare cases, e.g. when the
// navigation goes from https: to http: on a new tab, thus losing the other
// potential referrers.
const GURL referrer_url = navigation_handle->GetReferrer().url;
if (referrer_url.is_valid() && !referrer_url.is_empty())
return referrer_url;
const GURL last_committed_url =
navigation_handle->GetWebContents()->GetLastCommittedURL();
if (last_committed_url.is_valid() && !last_committed_url.is_empty())
return last_committed_url;
return navigation_handle->GetStartingSiteInstance()->GetSiteURL();
}
bool IsDesktopPwasEnabled() {
return base::FeatureList::IsEnabled(features::kDesktopPWAWindowing);
}
} // namespace
namespace chromeos {
// static
std::unique_ptr<content::NavigationThrottle>
AppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
content::WebContents* web_contents = handle->GetWebContents();
const bool arc_enabled = arc::IsArcPlayStoreEnabledForProfile(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
// Do not create the throttle if no apps can be installed.
if (!arc_enabled && !IsDesktopPwasEnabled())
return nullptr;
// Do not create the throttle in incognito or for a prerender navigation.
if (web_contents->GetBrowserContext()->IsOffTheRecord() ||
prerender::PrerenderContents::FromWebContents(web_contents) != nullptr) {
return nullptr;
}
// Do not create the throttle if there is no browser for the WebContents or we
// are already in an app browser. The former can happen if an initial
// navigation is reparented into a new app browser instance.
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser || browser->is_app())
return nullptr;
return std::make_unique<AppsNavigationThrottle>(handle, arc_enabled);
}
// static
void AppsNavigationThrottle::ShowIntentPickerBubble(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url) {
arc::ArcNavigationThrottle::GetArcAppsForPicker(
web_contents, url,
base::BindOnce(
&AppsNavigationThrottle::FindPwaForUrlAndShowIntentPickerForApps,
web_contents, ui_auto_display_service, url));
}
// static
void AppsNavigationThrottle::OnIntentPickerClosed(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
const std::string& launch_name,
apps::mojom::AppType app_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
const bool should_launch_app =
close_reason == IntentPickerCloseReason::OPEN_APP;
switch (app_type) {
case apps::mojom::AppType::kWeb:
if (should_launch_app) {
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(
web_contents->GetBrowserContext())
->GetExtensionById(launch_name,
extensions::ExtensionRegistry::ENABLED);
DCHECK(extension);
ReparentWebContentsIntoAppBrowser(web_contents, extension);
}
break;
case apps::mojom::AppType::kArc:
if (arc::ArcNavigationThrottle::MaybeLaunchOrPersistArcApp(
url, launch_name, should_launch_app, should_persist)) {
CloseOrGoBack(web_contents);
} else {
close_reason = IntentPickerCloseReason::ERROR;
}
break;
case apps::mojom::AppType::kUnknown:
// TODO(crbug.com/826982): This workaround can be removed when preferences
// are no longer persisted within the ARC container, it was necessary
// since chrome browser is neither a PWA or ARC app.
if (close_reason == chromeos::IntentPickerCloseReason::STAY_IN_CHROME &&
should_persist) {
arc::ArcNavigationThrottle::MaybeLaunchOrPersistArcApp(
url, launch_name, /*should_launch_app=*/false,
/*should_persist=*/true);
}
// We reach here if the picker was closed without an app being chosen,
// e.g. due to the tab being closed. Keep count of this scenario so we can
// stop the UI from showing after 2+ dismissals.
if (close_reason ==
chromeos::IntentPickerCloseReason::DIALOG_DEACTIVATED) {
if (ui_auto_display_service)
ui_auto_display_service->IncrementCounter(url);
}
break;
case apps::mojom::AppType::kBuiltIn:
case apps::mojom::AppType::kCrostini:
case apps::mojom::AppType::kExtension:
NOTREACHED();
}
RecordUma(launch_name, app_type, close_reason, should_persist);
}
// static
void AppsNavigationThrottle::RecordUma(const std::string& selected_app_package,
apps::mojom::AppType app_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
PickerAction action = GetPickerAction(app_type, close_reason, should_persist);
Platform platform = GetDestinationPlatform(selected_app_package, action);
UMA_HISTOGRAM_ENUMERATION("ChromeOS.Apps.IntentPickerAction", action);
UMA_HISTOGRAM_ENUMERATION("ChromeOS.Apps.IntentPickerDestinationPlatform",
platform);
if (app_type == apps::mojom::AppType::kArc &&
(close_reason == IntentPickerCloseReason::PREFERRED_APP_FOUND ||
close_reason == IntentPickerCloseReason::OPEN_APP)) {
UMA_HISTOGRAM_ENUMERATION("Arc.UserInteraction",
arc::UserInteractionType::APP_STARTED_FROM_LINK);
}
}
// static
bool AppsNavigationThrottle::ShouldOverrideUrlLoadingForTesting(
const GURL& previous_url,
const GURL& current_url) {
return ShouldOverrideUrlLoading(previous_url, current_url);
}
AppsNavigationThrottle::AppsNavigationThrottle(
content::NavigationHandle* navigation_handle,
bool arc_enabled)
: content::NavigationThrottle(navigation_handle),
arc_enabled_(arc_enabled),
ui_displayed_(false),
ui_auto_display_service_(
IntentPickerAutoDisplayService::Get(Profile::FromBrowserContext(
navigation_handle->GetWebContents()->GetBrowserContext()))),
weak_factory_(this) {
// |ui_auto_display_service_| can be null iff the call is coming from
// IntentPickerView. Since the pointer to our service is never modified
// (in case it is successfully created here) this check covers all the
// non-static methods in this class.
DCHECK(ui_auto_display_service_);
}
AppsNavigationThrottle::~AppsNavigationThrottle() = default;
const char* AppsNavigationThrottle::GetNameForLogging() {
return "AppsNavigationThrottle";
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::WillStartRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
starting_url_ = GetStartingGURL(navigation_handle());
Browser* browser =
chrome::FindBrowserWithWebContents(navigation_handle()->GetWebContents());
if (browser)
browser->window()->SetIntentPickerViewVisibility(/*visible=*/false);
return HandleRequest();
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::WillRedirectRequest() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// TODO(dominickn): Consider what to do when there is another URL during the
// same navigation that could be handled by apps. Two ideas are:
// 1) update the bubble with a mix of both app candidates (if different)
// 2) show a bubble based on the last url, thus closing all the previous ones
if (ui_displayed_)
return content::NavigationThrottle::PROCEED;
return HandleRequest();
}
// static
AppsNavigationThrottle::Platform AppsNavigationThrottle::GetDestinationPlatform(
const std::string& selected_launch_name,
PickerAction picker_action) {
switch (picker_action) {
case PickerAction::ARC_APP_PRESSED:
case PickerAction::ARC_APP_PREFERRED_PRESSED:
return Platform::ARC;
case PickerAction::PWA_APP_PRESSED:
return Platform::PWA;
case PickerAction::ERROR:
case PickerAction::DIALOG_DEACTIVATED:
case PickerAction::CHROME_PRESSED:
case PickerAction::CHROME_PREFERRED_PRESSED:
return Platform::CHROME;
case PickerAction::PREFERRED_ACTIVITY_FOUND:
return arc::ArcIntentHelperBridge::IsIntentHelperPackage(
selected_launch_name)
? Platform::CHROME
: Platform::ARC;
case PickerAction::OBSOLETE_ALWAYS_PRESSED:
case PickerAction::OBSOLETE_JUST_ONCE_PRESSED:
case PickerAction::INVALID:
NOTREACHED();
}
NOTREACHED();
return Platform::ARC;
}
// static
AppsNavigationThrottle::PickerAction AppsNavigationThrottle::GetPickerAction(
apps::mojom::AppType app_type,
IntentPickerCloseReason close_reason,
bool should_persist) {
switch (close_reason) {
case IntentPickerCloseReason::ERROR:
return PickerAction::ERROR;
case IntentPickerCloseReason::DIALOG_DEACTIVATED:
return PickerAction::DIALOG_DEACTIVATED;
case IntentPickerCloseReason::PREFERRED_APP_FOUND:
return PickerAction::PREFERRED_ACTIVITY_FOUND;
case IntentPickerCloseReason::STAY_IN_CHROME:
return should_persist ? PickerAction::CHROME_PREFERRED_PRESSED
: PickerAction::CHROME_PRESSED;
case IntentPickerCloseReason::OPEN_APP:
switch (app_type) {
case apps::mojom::AppType::kUnknown:
return PickerAction::INVALID;
case apps::mojom::AppType::kArc:
return should_persist ? PickerAction::ARC_APP_PREFERRED_PRESSED
: PickerAction::ARC_APP_PRESSED;
case apps::mojom::AppType::kWeb:
return PickerAction::PWA_APP_PRESSED;
case apps::mojom::AppType::kBuiltIn:
case apps::mojom::AppType::kCrostini:
case apps::mojom::AppType::kExtension:
NOTREACHED();
}
}
NOTREACHED();
return PickerAction::INVALID;
}
// static
void AppsNavigationThrottle::FindPwaForUrlAndShowIntentPickerForApps(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
std::vector<IntentPickerAppInfo> apps_for_picker =
FindPwaForUrl(web_contents, url, std::move(apps));
ShowIntentPickerBubbleForApps(web_contents, ui_auto_display_service, url,
std::move(apps_for_picker));
}
// static
std::vector<IntentPickerAppInfo> AppsNavigationThrottle::FindPwaForUrl(
content::WebContents* web_contents,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
if (IsDesktopPwasEnabled()) {
// Check if the current URL has an installed desktop PWA, and add that to
// the list of apps if it exists.
const extensions::Extension* extension =
extensions::util::GetInstalledPwaForUrl(
web_contents->GetBrowserContext(), url,
extensions::LAUNCH_CONTAINER_WINDOW);
if (extension) {
auto* menu_manager =
extensions::MenuManager::Get(web_contents->GetBrowserContext());
// Prefer the web and place apps of type PWA before apps of type ARC.
// TODO(crbug.com/824598): deterministically sort this list.
apps.emplace(apps.begin(), apps::mojom::AppType::kWeb,
menu_manager->GetIconForExtension(extension->id()),
extension->id(), extension->name());
}
}
return apps;
}
// static
void AppsNavigationThrottle::ShowIntentPickerBubbleForApps(
content::WebContents* web_contents,
IntentPickerAutoDisplayService* ui_auto_display_service,
const GURL& url,
std::vector<IntentPickerAppInfo> apps) {
if (apps.empty())
return;
// It should be safe to bind |web_contents| since closing the current tab will
// close the intent picker and run the callback prior to the WebContents being
// deallocated.
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return;
browser->window()->ShowIntentPickerBubble(
std::move(apps),
/*disable_stay_in_chrome=*/false,
base::BindOnce(&AppsNavigationThrottle::OnIntentPickerClosed,
web_contents, ui_auto_display_service, url));
}
// static
void AppsNavigationThrottle::CloseOrGoBack(content::WebContents* web_contents) {
DCHECK(web_contents);
if (web_contents->GetController().CanGoBack())
web_contents->GetController().GoBack();
else
web_contents->ClosePage();
}
void AppsNavigationThrottle::CancelNavigation() {
content::WebContents* web_contents = navigation_handle()->GetWebContents();
if (web_contents && web_contents->GetController().IsInitialNavigation()) {
// Workaround for b/79167225, closing |web_contents| here may be dangerous.
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&AppsNavigationThrottle::CloseTab,
weak_factory_.GetWeakPtr()));
} else {
CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE);
}
}
void AppsNavigationThrottle::OnDeferredNavigationProcessed(
AppsNavigationAction action,
std::vector<IntentPickerAppInfo> apps) {
if (action == AppsNavigationAction::CANCEL) {
// We found a preferred ARC app to open; cancel the navigation and don't do
// anything else.
CancelNavigation();
return;
}
content::NavigationHandle* handle = navigation_handle();
content::WebContents* web_contents = handle->GetWebContents();
const GURL& url = handle->GetURL();
std::vector<IntentPickerAppInfo> apps_for_picker =
FindPwaForUrl(web_contents, url, std::move(apps));
// We will not show the UI if the apps list is empty.
if (apps_for_picker.empty())
ui_displayed_ = false;
// If we only have PWAs in the app list, do not show the intent picker.
// Instead just show the omnibox icon. This is to reduce annoyance to users
// until "Remember my choice" is available for desktop PWAs.
// TODO(crbug.com/826982): show the intent picker when the app registry is
// available to persist "Remember my choice" for PWAs.
if (ShouldAutoDisplayUi(apps_for_picker, web_contents, url)) {
ShowIntentPickerBubbleForApps(web_contents, ui_auto_display_service_, url,
std::move(apps_for_picker));
} else {
ui_displayed_ = false;
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (browser)
browser->window()->SetIntentPickerViewVisibility(/*visible=*/true);
}
// We are about to resume the navigation, which may destroy this object.
Resume();
}
content::NavigationThrottle::ThrottleCheckResult
AppsNavigationThrottle::HandleRequest() {
content::NavigationHandle* handle = navigation_handle();
DCHECK(!ui_displayed_);
// Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
// don't have a way to send POST data to ARC, and 2) intercepting http(s) form
// submissions is not very important because such submissions are usually
// done within the same domain. ShouldOverrideUrlLoading() below filters out
// such submissions anyway.
constexpr bool kAllowFormSubmit = false;
// Ignore navigations with the CLIENT_REDIRECT qualifier on.
constexpr bool kAllowClientRedirect = false;
// We must never handle navigations started within a context menu.
if (handle->WasStartedFromContextMenu())
return content::NavigationThrottle::PROCEED;
ui::PageTransition page_transition = handle->GetPageTransition();
content::WebContents* web_contents = handle->GetWebContents();
const GURL& url = handle->GetURL();
if (ShouldIgnoreNavigation(page_transition, kAllowFormSubmit,
kAllowClientRedirect)) {
if ((page_transition & ui::PAGE_TRANSITION_FORWARD_BACK) ||
(page_transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)) {
// This enforces that whether we ignore the navigation or not, we make
// sure that the user cannot copy/paste or type a url to reuse
// ArcWebContentsData.
MaybeRemoveComingFromArcFlag(web_contents, starting_url_, url);
}
return content::NavigationThrottle::PROCEED;
}
MaybeRemoveComingFromArcFlag(web_contents, starting_url_, url);
if (!ShouldOverrideUrlLoading(starting_url_, url))
return content::NavigationThrottle::PROCEED;
if (arc_enabled_ &&
arc::ArcNavigationThrottle::WillGetArcAppsForNavigation(
handle,
base::BindOnce(&AppsNavigationThrottle::OnDeferredNavigationProcessed,
weak_factory_.GetWeakPtr()))) {
// Handling is now deferred to ArcNavigationThrottle, which asynchronously
// queries ARC for apps, and runs OnDeferredNavigationProcessed() with an
// action based on whether an acceptable app was found and user consent to
// open received. We assume the UI is shown or a preferred app was found;
// reset to false if we resume the navigation.
ui_displayed_ = true;
return content::NavigationThrottle::DEFER;
}
// We didn't query ARC, so proceed with the navigation and query if we have an
// installed desktop PWA to handle the URL.
if (IsDesktopPwasEnabled()) {
std::vector<IntentPickerAppInfo> apps =
FindPwaForUrl(web_contents, url, {});
if (!apps.empty())
ui_displayed_ = true;
ShowIntentPickerBubbleForApps(web_contents, ui_auto_display_service_, url,
std::move(apps));
}
return content::NavigationThrottle::PROCEED;
}
void AppsNavigationThrottle::CloseTab() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::WebContents* web_contents = navigation_handle()->GetWebContents();
if (web_contents)
web_contents->ClosePage();
}
bool AppsNavigationThrottle::ShouldAutoDisplayUi(
const std::vector<IntentPickerAppInfo>& apps_for_picker,
content::WebContents* web_contents,
const GURL& url) {
// Check if all the app candidates are PWAs.
bool only_pwa_apps =
std::all_of(apps_for_picker.begin(), apps_for_picker.end(),
[](const IntentPickerAppInfo& app_info) {
return app_info.type == apps::mojom::AppType::kWeb;
});
if (only_pwa_apps)
return false;
DCHECK(ui_auto_display_service_);
return ui_auto_display_service_->ShouldAutoDisplayUi(url);
}
} // namespace chromeos