| // Copyright 2016 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/arc/intent_helper/arc_navigation_throttle.h" |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/arc/arc_bridge_service.h" |
| #include "components/arc/arc_service_manager.h" |
| #include "components/arc/intent_helper/arc_intent_helper_bridge.h" |
| #include "components/arc/intent_helper/page_transition_util.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace arc { |
| |
| namespace { |
| |
| constexpr char kGoogleCom[] = "google.com"; |
| |
| // Compares the host name of the referrer and target URL to decide whether |
| // the navigation needs to be overriden. |
| 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; |
| } |
| |
| 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; |
| } |
| |
| // Searches for a preferred app in |handlers| and returns its index. If not |
| // found, returns |handlers.size()|. |
| size_t FindPreferredApp( |
| const std::vector<mojom::IntentHandlerInfoPtr>& handlers, |
| const GURL& url_for_logging) { |
| for (size_t i = 0; i < handlers.size(); ++i) { |
| if (!handlers[i]->is_preferred) |
| continue; |
| if (ArcIntentHelperBridge::IsIntentHelperPackage( |
| handlers[i]->package_name)) { |
| // If Chrome browser was selected as the preferred app, we shouldn't |
| // create a throttle. |
| DVLOG(1) |
| << "Chrome browser is selected as the preferred app for this URL: " |
| << url_for_logging; |
| } |
| return i; |
| } |
| return handlers.size(); // not found |
| } |
| |
| } // namespace |
| |
| ArcNavigationThrottle::ArcNavigationThrottle( |
| content::NavigationHandle* navigation_handle) |
| : content::NavigationThrottle(navigation_handle), |
| ui_displayed_(false), |
| weak_ptr_factory_(this) {} |
| |
| ArcNavigationThrottle::~ArcNavigationThrottle() = default; |
| |
| const char* ArcNavigationThrottle::GetNameForLogging() { |
| return "ArcNavigationThrottle"; |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ArcNavigationThrottle::WillStartRequest() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| starting_gurl_ = GetStartingGURL(); |
| Browser* browser = |
| chrome::FindBrowserWithWebContents(navigation_handle()->GetWebContents()); |
| if (browser) |
| chrome::SetIntentPickerViewVisibility(browser, false); |
| return HandleRequest(); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ArcNavigationThrottle::WillRedirectRequest() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // TODO(djacobo): Consider what to do when there is another url during the |
| // same navigation that could be handled by ARC 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(); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ArcNavigationThrottle::HandleRequest() { |
| DCHECK(!ui_displayed_); |
| content::NavigationHandle* handle = navigation_handle(); |
| const GURL& url = handle->GetURL(); |
| |
| // 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; |
| |
| if (ShouldIgnoreNavigation(handle->GetPageTransition(), kAllowFormSubmit, |
| kAllowClientRedirect)) |
| return content::NavigationThrottle::PROCEED; |
| |
| if (!ShouldOverrideUrlLoading(starting_gurl_, url)) |
| return content::NavigationThrottle::PROCEED; |
| |
| ArcServiceManager* arc_service_manager = ArcServiceManager::Get(); |
| if (!arc_service_manager) |
| return content::NavigationThrottle::PROCEED; |
| |
| auto* intent_helper_bridge = ArcIntentHelperBridge::GetForBrowserContext( |
| handle->GetWebContents()->GetBrowserContext()); |
| if (!intent_helper_bridge) |
| return content::NavigationThrottle::PROCEED; |
| |
| if (intent_helper_bridge->ShouldChromeHandleUrl(url)) { |
| // Allow navigation to proceed if there isn't an android app that handles |
| // the given URL. |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| RequestUrlHandlerList); |
| if (!instance) |
| return content::NavigationThrottle::PROCEED; |
| |
| // Assume the UI or a preferred app was found, reset to false only if we don't |
| // find a valid app candidate. |
| ui_displayed_ = true; |
| instance->RequestUrlHandlerList( |
| url.spec(), |
| base::BindOnce(&ArcNavigationThrottle::OnAppCandidatesReceived, |
| weak_ptr_factory_.GetWeakPtr())); |
| // We don't want to block the navigation, the only exception is here since we |
| // need to know if we really need to launch the UI or not, navigation is |
| // resumed right after we receive an answer from ARC's side (no user |
| // interaction needed). |
| return content::NavigationThrottle::DEFER; |
| } |
| |
| GURL ArcNavigationThrottle::GetStartingGURL() const { |
| // 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(); |
| } |
| |
| void ArcNavigationThrottle::OnAppCandidatesReceived( |
| std::vector<mojom::IntentHandlerInfoPtr> handlers) { |
| if (FoundPreferredOrVerifiedArcApp(std::move(handlers))) { |
| content::WebContents* tab = navigation_handle()->GetWebContents(); |
| if (tab && tab->GetController().IsInitialNavigation()) |
| tab->Close(); |
| else |
| CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE); |
| } else { |
| Resume(); |
| } |
| } |
| |
| bool ArcNavigationThrottle::FoundPreferredOrVerifiedArcApp( |
| std::vector<mojom::IntentHandlerInfoPtr> handlers) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| content::NavigationHandle* handle = navigation_handle(); |
| const GURL& url = handle->GetURL(); |
| |
| bool cancel_navigation = false; |
| if (!IsAppAvailable(handlers)) { |
| // This scenario shouldn't be accessed as ArcNavigationThrottle is created |
| // iff there are ARC apps which can actually handle the given URL. |
| DVLOG(1) << "There are no app candidates for this URL: " << url; |
| ui_displayed_ = false; |
| RecordUma(CloseReason::ERROR, Platform::CHROME); |
| return cancel_navigation; |
| } |
| |
| // If one of the apps is marked as preferred, use it right away without |
| // showing the UI. |
| const size_t index = FindPreferredApp(handlers, url); |
| if (index != handlers.size()) { |
| CloseReason close_reason = CloseReason::PREFERRED_ACTIVITY_FOUND; |
| const std::string package_name = handlers[index]->package_name; |
| |
| // Make sure that the instance at least supports HandleUrl. |
| auto* arc_service_manager = ArcServiceManager::Get(); |
| mojom::IntentHelperInstance* instance = nullptr; |
| if (arc_service_manager) { |
| instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| HandleUrl); |
| } |
| |
| if (!instance) { |
| close_reason = CloseReason::ERROR; |
| } else if (ArcIntentHelperBridge::IsIntentHelperPackage(package_name)) { |
| chrome::SetIntentPickerViewVisibility( |
| chrome::FindBrowserWithWebContents(handle->GetWebContents()), true); |
| } else { |
| instance->HandleUrl(url.spec(), package_name); |
| cancel_navigation = true; |
| } |
| |
| Platform platform = GetDestinationPlatform(package_name, close_reason); |
| RecordUma(close_reason, platform); |
| } else { |
| auto* intent_helper_bridge = ArcIntentHelperBridge::GetForBrowserContext( |
| handle->GetWebContents()->GetBrowserContext()); |
| if (!intent_helper_bridge) { |
| LOG(ERROR) << "Cannot get an instance of ArcIntentHelperBridge"; |
| RecordUma(CloseReason::ERROR, Platform::CHROME); |
| return cancel_navigation; |
| } |
| std::vector<ArcIntentHelperBridge::ActivityName> activities; |
| for (const auto& handler : handlers) |
| activities.emplace_back(handler->package_name, handler->activity_name); |
| |
| intent_helper_bridge->GetActivityIcons( |
| activities, |
| base::BindOnce( |
| &ArcNavigationThrottle::AsyncOnAppIconsReceived, |
| chrome::FindBrowserWithWebContents(handle->GetWebContents()), |
| std::move(handlers), url)); |
| } |
| |
| return cancel_navigation; |
| } |
| |
| // static |
| void ArcNavigationThrottle::AsyncOnAppIconsReceived( |
| const Browser* browser, |
| std::vector<arc::mojom::IntentHandlerInfoPtr> handlers, |
| const GURL& url, |
| std::unique_ptr<arc::ArcIntentHelperBridge::ActivityToIconsMap> icons) { |
| std::vector<AppInfo> app_info; |
| |
| for (const auto& handler : handlers) { |
| gfx::Image icon; |
| const arc::ArcIntentHelperBridge::ActivityName activity( |
| handler->package_name, handler->activity_name); |
| const auto it = icons->find(activity); |
| |
| app_info.emplace_back(it != icons->end() ? it->second.icon16 : gfx::Image(), |
| handler->package_name, handler->name); |
| } |
| |
| chrome::QueryAndDisplayArcApps( |
| browser, app_info, |
| base::Bind(&ArcNavigationThrottle::AsyncOnIntentPickerClosed, url)); |
| } |
| |
| // static |
| void ArcNavigationThrottle::AsyncOnIntentPickerClosed( |
| const GURL& url, |
| const std::string& pkg, |
| arc::ArcNavigationThrottle::CloseReason close_reason) { |
| auto* arc_service_manager = arc::ArcServiceManager::Get(); |
| arc::mojom::IntentHelperInstance* instance = nullptr; |
| if (arc_service_manager) { |
| instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), HandleUrl); |
| } |
| if (!instance) |
| close_reason = CloseReason::ERROR; |
| |
| switch (close_reason) { |
| case CloseReason::ERROR: |
| case CloseReason::CHROME_PRESSED: |
| case CloseReason::DIALOG_DEACTIVATED: { |
| break; |
| } |
| case CloseReason::PREFERRED_ACTIVITY_FOUND: { |
| if (!arc::ArcIntentHelperBridge::IsIntentHelperPackage(pkg)) |
| instance->HandleUrl(url.spec(), pkg); |
| break; |
| } |
| case arc::ArcNavigationThrottle::CloseReason::ARC_APP_PREFERRED_PRESSED: { |
| DCHECK(arc_service_manager); |
| if (ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| AddPreferredPackage)) { |
| instance->AddPreferredPackage(pkg); |
| } |
| instance->HandleUrl(url.spec(), pkg); |
| break; |
| } |
| case arc::ArcNavigationThrottle::CloseReason::CHROME_PREFERRED_PRESSED: { |
| DCHECK(arc_service_manager); |
| if (ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| AddPreferredPackage)) { |
| instance->AddPreferredPackage(pkg); |
| } |
| break; |
| } |
| case arc::ArcNavigationThrottle::CloseReason::ARC_APP_PRESSED: { |
| instance->HandleUrl(url.spec(), pkg); |
| break; |
| } |
| case arc::ArcNavigationThrottle::CloseReason::OBSOLETE_ALWAYS_PRESSED: |
| case arc::ArcNavigationThrottle::CloseReason::OBSOLETE_JUST_ONCE_PRESSED: |
| case arc::ArcNavigationThrottle::CloseReason::INVALID: { |
| NOTREACHED(); |
| return; |
| } |
| } |
| |
| arc::ArcNavigationThrottle::Platform platform = |
| arc::ArcNavigationThrottle::GetDestinationPlatform(pkg, close_reason); |
| arc::ArcNavigationThrottle::RecordUma(close_reason, platform); |
| } |
| |
| // static |
| size_t ArcNavigationThrottle::GetAppIndex( |
| const std::vector<mojom::IntentHandlerInfoPtr>& handlers, |
| const std::string& selected_app_package) { |
| for (size_t i = 0; i < handlers.size(); ++i) { |
| if (handlers[i]->package_name == selected_app_package) |
| return i; |
| } |
| return handlers.size(); |
| } |
| |
| // static |
| ArcNavigationThrottle::Platform ArcNavigationThrottle::GetDestinationPlatform( |
| const std::string& selected_app_package, |
| CloseReason close_reason) { |
| return (close_reason != CloseReason::ERROR && |
| close_reason != CloseReason::DIALOG_DEACTIVATED && |
| !ArcIntentHelperBridge::IsIntentHelperPackage(selected_app_package)) |
| ? Platform::ARC |
| : Platform::CHROME; |
| } |
| |
| // static |
| void ArcNavigationThrottle::RecordUma(CloseReason close_reason, |
| Platform platform) { |
| UMA_HISTOGRAM_ENUMERATION("Arc.IntentHandlerAction", |
| static_cast<int>(close_reason), |
| static_cast<int>(CloseReason::SIZE)); |
| |
| UMA_HISTOGRAM_ENUMERATION("Arc.IntentHandlerDestinationPlatform", |
| static_cast<int>(platform), |
| static_cast<int>(Platform::SIZE)); |
| } |
| |
| // static |
| bool ArcNavigationThrottle::IsAppAvailable( |
| const std::vector<mojom::IntentHandlerInfoPtr>& handlers) { |
| return handlers.size() > 1 || |
| (handlers.size() == 1 && !ArcIntentHelperBridge::IsIntentHelperPackage( |
| handlers[0]->package_name)); |
| } |
| |
| // static |
| bool ArcNavigationThrottle::ShouldOverrideUrlLoadingForTesting( |
| const GURL& previous_url, |
| const GURL& current_url) { |
| return ShouldOverrideUrlLoading(previous_url, current_url); |
| } |
| |
| // static |
| bool ArcNavigationThrottle::IsAppAvailableForTesting( |
| const std::vector<mojom::IntentHandlerInfoPtr>& handlers) { |
| return IsAppAvailable(handlers); |
| } |
| |
| // static |
| size_t ArcNavigationThrottle::FindPreferredAppForTesting( |
| const std::vector<mojom::IntentHandlerInfoPtr>& handlers) { |
| return FindPreferredApp(handlers, GURL()); |
| } |
| |
| // static |
| void ArcNavigationThrottle::AsyncShowIntentPickerBubble(const Browser* browser, |
| const GURL& url) { |
| arc::ArcServiceManager* arc_service_manager = arc::ArcServiceManager::Get(); |
| if (!arc_service_manager) { |
| DVLOG(1) << "Cannot get an instance of ArcServiceManager"; |
| return; |
| } |
| |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| RequestUrlHandlerList); |
| if (!instance) { |
| DVLOG(1) << "Cannot get access to RequestUrlHandlerList"; |
| return; |
| } |
| |
| instance->RequestUrlHandlerList( |
| url.spec(), |
| base::BindOnce(&ArcNavigationThrottle::AsyncOnAppCandidatesReceived, |
| browser, url)); |
| } |
| |
| // static |
| void ArcNavigationThrottle::AsyncOnAppCandidatesReceived( |
| const Browser* browser, |
| const GURL& url, |
| std::vector<arc::mojom::IntentHandlerInfoPtr> handlers) { |
| if (!IsAppAvailable(handlers)) { |
| DVLOG(1) << "There are no app candidates for this URL"; |
| return; |
| } |
| |
| auto* intent_helper_bridge = arc::ArcIntentHelperBridge::GetForBrowserContext( |
| browser->tab_strip_model()->GetActiveWebContents()->GetBrowserContext()); |
| if (!intent_helper_bridge) { |
| DVLOG(1) << "Cannot get an instance of ArcIntentHelperBridge"; |
| return; |
| } |
| |
| std::vector<arc::ArcIntentHelperBridge::ActivityName> activities; |
| for (const auto& handler : handlers) |
| activities.emplace_back(handler->package_name, handler->activity_name); |
| |
| intent_helper_bridge->GetActivityIcons( |
| activities, |
| base::BindOnce(&ArcNavigationThrottle::AsyncOnAppIconsReceived, browser, |
| std::move(handlers), url)); |
| } |
| |
| } // namespace arc |