| // Copyright 2016 The Chromium Authors |
| // 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/arc_external_protocol_dialog.h" |
| |
| #include <algorithm> |
| #include <map> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h" |
| #include "chrome/browser/apps/link_capturing/metrics/intent_handling_metrics.h" |
| #include "chrome/browser/chromeos/arc/arc_web_contents_data.h" |
| #include "chrome/browser/sharing/click_to_call/click_to_call_metrics.h" |
| #include "chrome/browser/sharing/click_to_call/click_to_call_ui_controller.h" |
| #include "chrome/browser/sharing/click_to_call/click_to_call_utils.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/intent_picker_tab_helper.h" |
| #include "chromeos/ash/experiences/arc/intent_helper/arc_intent_helper_package.h" |
| #include "components/sharing_message/sharing_target_device_info.h" |
| #include "components/sync/protocol/sync_enums.pb.h" |
| #include "components/sync_device_info/device_info.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/page_navigator.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/referrer.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "ui/color/color_id.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "url/gurl.h" |
| |
| using content::WebContents; |
| |
| namespace arc { |
| |
| namespace { |
| |
| // The proxy activity for launching an ARC IME's settings activity. These names |
| // have to be in sync with the ones used in ArcInputMethodManagerService.java on |
| // the container side. Otherwise, the picker dialog might pop up unexpectedly. |
| constexpr char kPackageForOpeningArcImeSettingsPage[] = |
| "org.chromium.arc.applauncher"; |
| constexpr char kActivityForOpeningArcImeSettingsPage[] = |
| "org.chromium.arc.applauncher.InputMethodSettingsActivity"; |
| |
| // Size of device icons in DIPs. |
| constexpr int kDeviceIconSize = 16; |
| |
| using IntentPickerResponseWithDevices = |
| base::OnceCallback<void(std::vector<SharingTargetDeviceInfo> devices, |
| apps::IntentPickerBubbleType intent_picker_type, |
| const std::string& launch_name, |
| apps::PickerEntryType entry_type, |
| apps::IntentPickerCloseReason close_reason, |
| bool should_persist)>; |
| |
| // Creates an icon for a specific |device_form_factor|. |
| ui::ImageModel CreateDeviceIcon( |
| const syncer::DeviceInfo::FormFactor device_form_factor) { |
| const gfx::VectorIcon& icon = |
| device_form_factor == syncer::DeviceInfo::FormFactor::kTablet |
| ? kTabletIcon |
| : kHardwareSmartphoneIcon; |
| return ui::ImageModel::FromVectorIcon(icon, ui::kColorIcon, kDeviceIconSize); |
| } |
| |
| // Adds |devices| to |picker_entries| and returns the new list. The devices are |
| // added to the beginning of the list. |
| std::vector<apps::IntentPickerAppInfo> AddDevices( |
| const std::vector<SharingTargetDeviceInfo>& devices, |
| std::vector<apps::IntentPickerAppInfo> picker_entries) { |
| DCHECK(!devices.empty()); |
| |
| // First add all devices to the list. |
| std::vector<apps::IntentPickerAppInfo> all_entries; |
| for (const SharingTargetDeviceInfo& device : devices) { |
| all_entries.emplace_back(apps::PickerEntryType::kDevice, |
| CreateDeviceIcon(device.form_factor()), |
| device.guid(), device.client_name()); |
| } |
| |
| // Append the previous list by moving its elements. |
| for (auto& entry : picker_entries) { |
| all_entries.emplace_back(std::move(entry)); |
| } |
| |
| return all_entries; |
| } |
| |
| // Adds remote devices to |app_info| and shows the intent picker dialog if there |
| // is at least one app or device to choose from. |
| bool MaybeAddDevicesAndShowPicker( |
| const GURL& url, |
| const std::optional<url::Origin>& initiating_origin, |
| WebContents* web_contents, |
| std::vector<apps::IntentPickerAppInfo> app_info, |
| bool stay_in_chrome, |
| bool show_remember_selection, |
| IntentPickerResponseWithDevices callback) { |
| Browser* browser = |
| web_contents ? chrome::FindBrowserWithTab(web_contents) : nullptr; |
| if (!browser) { |
| return false; |
| } |
| |
| bool has_apps = !app_info.empty(); |
| bool has_devices = false; |
| |
| auto bubble_type = apps::IntentPickerBubbleType::kExternalProtocol; |
| ClickToCallUiController* controller = nullptr; |
| std::vector<SharingTargetDeviceInfo> devices; |
| |
| if (ShouldOfferClickToCallForURL(web_contents->GetBrowserContext(), url)) { |
| bubble_type = apps::IntentPickerBubbleType::kClickToCall; |
| controller = |
| ClickToCallUiController::GetOrCreateFromWebContents(web_contents); |
| devices = controller->GetDevices(); |
| has_devices = !devices.empty(); |
| if (has_devices) { |
| app_info = AddDevices(devices, std::move(app_info)); |
| } |
| } |
| |
| if (app_info.empty()) { |
| return false; |
| } |
| |
| IntentPickerTabHelper::ShowOrHideIcon( |
| web_contents, |
| bubble_type == apps::IntentPickerBubbleType::kExternalProtocol); |
| browser->window()->ShowIntentPickerBubble( |
| std::move(app_info), stay_in_chrome, show_remember_selection, bubble_type, |
| initiating_origin, |
| base::BindOnce(std::move(callback), std::move(devices), bubble_type)); |
| |
| if (controller) { |
| controller->OnIntentPickerShown(has_devices, has_apps); |
| } |
| |
| return true; |
| } |
| |
| void CloseTabIfNeeded(base::WeakPtr<WebContents> web_contents, |
| bool safe_to_bypass_ui) { |
| if (!web_contents) { |
| return; |
| } |
| |
| if (web_contents->GetController().IsInitialNavigation() || |
| safe_to_bypass_ui) { |
| web_contents->ClosePage(); |
| } |
| } |
| |
| // Tells whether or not Chrome is an app candidate for the current navigation. |
| bool IsChromeAnAppCandidate( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| for (const auto& handler : handlers) { |
| if (handler.package_name == kArcIntentHelperPackageName) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Returns true if |handlers| only contains Chrome as an app candidate for the |
| // current navigation. |
| bool IsChromeOnlyAppCandidate( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| return handlers.size() == 1 && IsChromeAnAppCandidate(handlers); |
| } |
| |
| // Returns true if the |handler| is for opening ARC IME settings page. |
| bool ForOpeningArcImeSettingsPage( |
| const ArcIntentHelperMojoDelegate::IntentHandlerInfo& handler) { |
| return (handler.package_name == kPackageForOpeningArcImeSettingsPage) && |
| (handler.activity_name == kActivityForOpeningArcImeSettingsPage); |
| } |
| |
| // Shows |url| in the current tab. |
| void OpenUrlInChrome(base::WeakPtr<WebContents> web_contents, const GURL& url) { |
| if (!web_contents) { |
| return; |
| } |
| |
| const ui::PageTransition page_transition_type = |
| ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK); |
| constexpr bool kIsRendererInitiated = false; |
| const content::OpenURLParams params( |
| url, |
| content::Referrer(web_contents->GetLastCommittedURL(), |
| network::mojom::ReferrerPolicy::kDefault), |
| WindowOpenDisposition::CURRENT_TAB, page_transition_type, |
| kIsRendererInitiated); |
| web_contents->OpenURL(params, /*navigation_handle_callback=*/{}); |
| } |
| |
| ArcIntentHelperMojoDelegate::IntentInfo CreateIntentInfo(const GURL& url, |
| bool ui_bypassed) { |
| // Create an intent with action VIEW, the |url| we are redirecting the user to |
| // and a flag that tells whether or not the user interacted with the picker UI |
| |
| constexpr char kArcIntentActionView[] = "org.chromium.arc.intent.action.VIEW"; |
| return ArcIntentHelperMojoDelegate::IntentInfo( |
| kArcIntentActionView, /*categories=*/std::nullopt, url.spec(), |
| /*type=*/std::nullopt, ui_bypassed, /*extras=*/std::nullopt); |
| } |
| |
| // Sends |url| to ARC. |
| void HandleUrlInArc(base::WeakPtr<WebContents> web_contents, |
| const GurlAndActivityInfo& url_and_activity, |
| bool ui_bypassed, |
| ArcIntentHelperMojoDelegate* mojo_delegate) { |
| // ArcIntentHelperMojoDelegate is already varified non-null. |
| DCHECK(mojo_delegate); |
| |
| // We want to inform ARC about whether or not the user interacted with the |
| // picker UI, also since we want to be more explicit about the package and |
| // activity we are using, we are relying in HandleIntent() to comunicate back |
| // to ARC. |
| if (mojo_delegate->HandleIntent( |
| CreateIntentInfo(url_and_activity.first, ui_bypassed), |
| ArcIntentHelperMojoDelegate::ActivityName( |
| std::move(url_and_activity.second.package_name), |
| std::move(url_and_activity.second.activity_name)))) { |
| CloseTabIfNeeded(web_contents, ui_bypassed); |
| } |
| } |
| |
| // A helper function called by GetAction(). |
| GetActionResult GetActionInternal( |
| const GURL& original_url, |
| const ArcIntentHelperMojoDelegate::IntentHandlerInfo& handler, |
| GurlAndActivityInfo* out_url_and_activity_name) { |
| if (handler.fallback_url.has_value()) { |
| *out_url_and_activity_name = |
| GurlAndActivityInfo(GURL(*handler.fallback_url), |
| ArcIntentHelperMojoDelegate::ActivityName( |
| handler.package_name, handler.activity_name)); |
| if (handler.package_name == kArcIntentHelperPackageName) { |
| // Since |package_name| is "Chrome", and |fallback_url| is not null, the |
| // URL must be either http or https. Check it just in case, and if not, |
| // fallback to HANDLE_URL_IN_ARC; |
| if (out_url_and_activity_name->first.SchemeIsHTTPOrHTTPS()) { |
| return GetActionResult::OPEN_URL_IN_CHROME; |
| } |
| |
| LOG(WARNING) << "Failed to handle " << out_url_and_activity_name->first |
| << " in Chrome. Falling back to ARC..."; |
| } |
| // |fallback_url| which Chrome doesn't support is passed (e.g. market:). |
| return GetActionResult::HANDLE_URL_IN_ARC; |
| } |
| |
| // Unlike |handler->fallback_url|, the |original_url| should always be handled |
| // in ARC since it's external to Chrome. |
| *out_url_and_activity_name = GurlAndActivityInfo( |
| original_url, ArcIntentHelperMojoDelegate::ActivityName( |
| handler.package_name, handler.activity_name)); |
| return GetActionResult::HANDLE_URL_IN_ARC; |
| } |
| |
| // Gets an action that should be done when ARC has the |handlers| for the |
| // |original_url| and the user selects |selected_app_index|. When the user |
| // hasn't selected any app, |selected_app_index| must be set to |
| // |handlers.size()|. |
| // |
| // When the returned action is either OPEN_URL_IN_CHROME or HANDLE_URL_IN_ARC, |
| // |out_url_and_activity_name| is filled accordingly. |
| // |
| // |in_out_safe_to_bypass_ui| is used to reflect whether or not we should |
| // display the UI: it initially informs whether or not this navigation was |
| // initiated within ARC, and then gets double-checked and used to store whether |
| // or not the user can safely bypass the UI. |
| GetActionResult GetAction( |
| const GURL& original_url, |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& handlers, |
| size_t selected_app_index, |
| GurlAndActivityInfo* out_url_and_activity_name, |
| bool* in_out_safe_to_bypass_ui) { |
| DCHECK(out_url_and_activity_name); |
| DCHECK(!handlers.empty()); |
| |
| if (selected_app_index == handlers.size()) { |
| // The user hasn't made the selection yet. |
| |
| // If |handlers| has only one element and either of the following conditions |
| // is met, open the URL in Chrome or ARC without showing the picker UI. |
| // 1) its package is "Chrome", open the fallback URL in the current tab |
| // without showing the dialog. |
| // 2) its package is not "Chrome" but it has been marked as |
| // |in_out_safe_to_bypass_ui|, this means that we trust the current tab |
| // since its content was originated from ARC. |
| // 3) its package and activity are for opening ARC IME settings page. The |
| // activity is launched with an explicit user action in chrome://settings. |
| if (handlers.size() == 1) { |
| const GetActionResult internal_result = GetActionInternal( |
| original_url, handlers[0], out_url_and_activity_name); |
| |
| if ((internal_result == GetActionResult::HANDLE_URL_IN_ARC && |
| (*in_out_safe_to_bypass_ui || |
| ForOpeningArcImeSettingsPage(handlers[0]))) || |
| internal_result == GetActionResult::OPEN_URL_IN_CHROME) { |
| // Make sure the |in_out_safe_to_bypass_ui| flag is actually marked, its |
| // maybe not important for OPEN_URL_IN_CHROME but just for consistency. |
| *in_out_safe_to_bypass_ui = true; |
| return internal_result; |
| } |
| } |
| |
| // Since we have 2+ app candidates we should display the UI, unless there is |
| // an already preferred app. |is_preferred| will never be true unless the |
| // user explicitly marked it as such. |
| *in_out_safe_to_bypass_ui = false; |
| for (size_t i = 0; i < handlers.size(); ++i) { |
| const ArcIntentHelperMojoDelegate::IntentHandlerInfo& handler = |
| handlers[i]; |
| if (!handler.is_preferred) { |
| continue; |
| } |
| // This is another way to bypass the UI, since the user already expressed |
| // some sort of preference. |
| *in_out_safe_to_bypass_ui = true; |
| // A preferred activity is found. Decide how to open it, either in Chrome |
| // or ARC. |
| return GetActionInternal(original_url, handler, |
| out_url_and_activity_name); |
| } |
| // Ask the user to pick one. |
| return GetActionResult::ASK_USER; |
| } |
| |
| // The user already made a selection so this should be false. |
| *in_out_safe_to_bypass_ui = false; |
| return GetActionInternal(original_url, handlers[selected_app_index], |
| out_url_and_activity_name); |
| } |
| |
| // Returns true if the |url| is safe to be forwarded to ARC without showing the |
| // disambig dialog, besides having this flag set we need to check that there is |
| // only one app candidate, this is enforced via GetAction(). Any navigation |
| // coming from ARC via ChromeShellDelegate MUST be marked as such. |
| // |
| // Mark as not "safe" (aka return false) on the contrary, most likely those |
| // cases will require the user to pass thru the intent picker UI. |
| bool GetAndResetSafeToRedirectToArcWithoutUserConfirmationFlag( |
| WebContents* web_contents) { |
| const char* key = |
| arc::ArcWebContentsData::ArcWebContentsData::kArcTransitionFlag; |
| arc::ArcWebContentsData* arc_data = |
| static_cast<arc::ArcWebContentsData*>(web_contents->GetUserData(key)); |
| if (!arc_data) { |
| return false; |
| } |
| |
| web_contents->RemoveUserData(key); |
| return true; |
| } |
| |
| void HandleDeviceSelection(WebContents* web_contents, |
| const std::vector<SharingTargetDeviceInfo>& devices, |
| const std::string& device_guid, |
| const GURL& url) { |
| if (!web_contents) { |
| return; |
| } |
| |
| const auto it = |
| std::ranges::find(devices, device_guid, &SharingTargetDeviceInfo::guid); |
| CHECK(it != devices.end()); |
| const SharingTargetDeviceInfo& device = *it; |
| |
| ClickToCallUiController::GetOrCreateFromWebContents(web_contents) |
| ->OnDeviceSelected(url.GetContent(), device, |
| SharingClickToCallEntryPoint::kLeftClickLink); |
| } |
| |
| // Handles |url| if possible. Returns true if it is actually handled. |
| bool HandleUrl( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& handlers, |
| size_t selected_app_index, |
| GetActionResult* out_result, |
| bool safe_to_bypass_ui, |
| ArcIntentHelperMojoDelegate* mojo_delegate) { |
| GurlAndActivityInfo url_and_activity_name( |
| GURL(), |
| ArcIntentHelperMojoDelegate::ActivityName{/*package=*/std::string(), |
| /*activity=*/std::string()}); |
| |
| const GetActionResult result = |
| GetAction(url, handlers, selected_app_index, &url_and_activity_name, |
| &safe_to_bypass_ui); |
| if (out_result) { |
| *out_result = result; |
| } |
| |
| switch (result) { |
| case GetActionResult::OPEN_URL_IN_CHROME: |
| OpenUrlInChrome(web_contents, url_and_activity_name.first); |
| return true; |
| case GetActionResult::HANDLE_URL_IN_ARC: |
| HandleUrlInArc(web_contents, url_and_activity_name, safe_to_bypass_ui, |
| mojo_delegate); |
| return true; |
| case GetActionResult::ASK_USER: |
| break; |
| } |
| |
| return false; |
| } |
| |
| // Returns a fallback http(s) in |handlers| which Chrome can handle. Returns |
| // an empty GURL if none found. |
| GURL GetUrlToNavigateOnDeactivate( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| const GURL empty_url; |
| for (size_t i = 0; i < handlers.size(); ++i) { |
| GurlAndActivityInfo url_and_package( |
| GURL(), |
| ArcIntentHelperMojoDelegate::ActivityName{/*package=*/std::string(), |
| /*activity=*/std::string()}); |
| if (GetActionInternal(empty_url, handlers[i], &url_and_package) == |
| GetActionResult::OPEN_URL_IN_CHROME) { |
| DCHECK(url_and_package.first.SchemeIsHTTPOrHTTPS()); |
| return url_and_package.first; |
| } |
| } |
| return empty_url; // nothing found. |
| } |
| |
| // Called when the dialog is just deactivated without pressing one of the |
| // buttons. |
| void OnIntentPickerDialogDeactivated( |
| base::WeakPtr<WebContents> web_contents, |
| bool safe_to_bypass_ui, |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| const GURL url_to_open_in_chrome = GetUrlToNavigateOnDeactivate(handlers); |
| if (url_to_open_in_chrome.is_empty()) { |
| CloseTabIfNeeded(web_contents, safe_to_bypass_ui); |
| } else { |
| OpenUrlInChrome(web_contents, url_to_open_in_chrome); |
| } |
| } |
| |
| size_t GetAppIndex( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| app_candidates, |
| const std::string& selected_app_package) { |
| for (size_t i = 0; i < app_candidates.size(); ++i) { |
| if (app_candidates[i].package_name == selected_app_package) { |
| return i; |
| } |
| } |
| return app_candidates.size(); |
| } |
| |
| // Called when the dialog is closed. Note that once we show the UI, we should |
| // never show the Chrome OS' fallback dialog. |
| void OnIntentPickerClosed( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| bool safe_to_bypass_ui, |
| std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo> handlers, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| std::vector<SharingTargetDeviceInfo> devices, |
| apps::IntentPickerBubbleType intent_picker_type, |
| const std::string& selected_app_package, |
| apps::PickerEntryType entry_type, |
| apps::IntentPickerCloseReason reason, |
| bool should_persist) { |
| // ArcIntentHelperMojoDelegate is already varified non-null. |
| DCHECK(mojo_delegate); |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Even if ArcExternalProtocolDialog shares the same icon on the omnibox as an |
| // http(s) request (via AppsNavigationThrottle), the UI here shouldn't stay in |
| // the omnibox since the decision should be taken right away in a kind of |
| // blocking fashion. |
| auto* context = web_contents ? web_contents->GetBrowserContext() : nullptr; |
| |
| if (web_contents) { |
| if (intent_picker_type == apps::IntentPickerBubbleType::kClickToCall) { |
| ClickToCallUiController::GetOrCreateFromWebContents(web_contents.get()) |
| ->OnIntentPickerClosed(); |
| } else { |
| IntentPickerTabHelper::ShowOrHideIcon(web_contents.get(), |
| /*should_show_icon=*/false); |
| } |
| } |
| |
| if (entry_type == apps::PickerEntryType::kDevice) { |
| DCHECK_EQ(apps::IntentPickerCloseReason::OPEN_APP, reason); |
| DCHECK(!should_persist); |
| HandleDeviceSelection(web_contents.get(), devices, selected_app_package, |
| url); |
| if (context) { |
| apps::IntentHandlingMetrics::RecordExternalProtocolUserInteractionMetrics( |
| context, entry_type, reason, should_persist); |
| } |
| return; |
| } |
| |
| // If the user selected an app to continue the navigation, confirm that the |
| // |package_name| matches a valid option and return the index. |
| const size_t selected_app_index = GetAppIndex(handlers, selected_app_package); |
| |
| // Make sure ARC intent helper instance is connected. |
| if (!mojo_delegate->IsArcAvailable()) { |
| reason = apps::IntentPickerCloseReason::ERROR_AFTER_PICKER; |
| } |
| |
| if (reason == apps::IntentPickerCloseReason::OPEN_APP || |
| reason == apps::IntentPickerCloseReason::STAY_IN_CHROME) { |
| if (selected_app_index == handlers.size()) { |
| // Selected app does not exist. |
| reason = apps::IntentPickerCloseReason::ERROR_AFTER_PICKER; |
| } |
| } |
| |
| switch (reason) { |
| case apps::IntentPickerCloseReason::OPEN_APP: |
| // Only ARC apps are offered in the external protocol intent picker, so if |
| // the user decided to open in app the type must be ARC. |
| DCHECK_EQ(apps::PickerEntryType::kArc, entry_type); |
| |
| if (should_persist) { |
| mojo_delegate->AddPreferredPackage( |
| handlers[selected_app_index].package_name); |
| } |
| |
| // Launch the selected app. |
| // As the current web page is closed, |web_contents| will be invalidated. |
| HandleUrl(web_contents, url, handlers, selected_app_index, |
| /*out_result=*/nullptr, safe_to_bypass_ui, mojo_delegate.get()); |
| break; |
| case apps::IntentPickerCloseReason::PREFERRED_APP_FOUND: |
| // We shouldn't be here if a preferred app was found. |
| NOTREACHED(); |
| case apps::IntentPickerCloseReason::STAY_IN_CHROME: |
| NOTREACHED() << "Chrome is not a valid option for external protocol URLs"; |
| case apps::IntentPickerCloseReason::ERROR_BEFORE_PICKER: |
| // This can happen since an error could occur right before invoking |
| // Show() on the bubble's UI code. |
| [[fallthrough]]; |
| case apps::IntentPickerCloseReason::ERROR_AFTER_PICKER: |
| LOG(ERROR) << "IntentPickerBubbleView returned CloseReason::ERROR: " |
| << "selected_app_index=" << selected_app_index |
| << ", handlers.size=" << handlers.size(); |
| [[fallthrough]]; |
| case apps::IntentPickerCloseReason::DIALOG_DEACTIVATED: |
| // The user didn't select any ARC activity. |
| OnIntentPickerDialogDeactivated(web_contents, safe_to_bypass_ui, |
| handlers); |
| break; |
| } |
| |
| if (context) { |
| apps::IntentHandlingMetrics::RecordExternalProtocolUserInteractionMetrics( |
| context, entry_type, reason, should_persist); |
| } |
| } |
| |
| // Called when ARC returned activity icons for the |handlers|. |
| void OnAppIconsReceived( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| const std::optional<url::Origin>& initiating_origin, |
| bool safe_to_bypass_ui, |
| std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo> handlers, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| base::OnceCallback<void(bool)> handled_cb, |
| bool show_stay_in_chrome, |
| std::unique_ptr<ArcIconCacheDelegate::ActivityToIconsMap> icons) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| using AppInfo = apps::IntentPickerAppInfo; |
| std::vector<AppInfo> app_info; |
| |
| for (const auto& handler : handlers) { |
| const ArcIconCacheDelegate::ActivityName activity(handler.package_name, |
| handler.activity_name); |
| const auto it = icons->find(activity); |
| app_info.emplace_back(apps::PickerEntryType::kArc, |
| it != icons->end() |
| ? ui::ImageModel::FromImage(it->second.icon16) |
| : ui::ImageModel(), |
| handler.package_name, handler.name); |
| } |
| |
| Browser* browser = |
| web_contents ? chrome::FindBrowserWithTab(web_contents.get()) : nullptr; |
| |
| if (!browser) { |
| return std::move(handled_cb).Run(false); |
| } |
| |
| bool handled = MaybeAddDevicesAndShowPicker( |
| url, initiating_origin, web_contents.get(), std::move(app_info), |
| show_stay_in_chrome, |
| /*show_remember_selection=*/true, |
| base::BindOnce(OnIntentPickerClosed, web_contents, url, safe_to_bypass_ui, |
| std::move(handlers), std::move(mojo_delegate))); |
| return std::move(handled_cb).Run(handled); |
| } |
| |
| void ShowExternalProtocolDialogWithoutApps( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| const std::optional<url::Origin>& initiating_origin, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| base::OnceCallback<void(bool)> handled_cb) { |
| // Try to show the device picker and fallback to the default dialog otherwise. |
| bool handled = MaybeAddDevicesAndShowPicker( |
| url, initiating_origin, web_contents.get(), |
| /*app_info=*/{}, /*stay_in_chrome=*/false, |
| /*show_remember_selection=*/false, |
| base::BindOnce( |
| OnIntentPickerClosed, web_contents, url, |
| /*safe_to_bypass_ui=*/false, |
| std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>(), |
| std::move(mojo_delegate))); |
| |
| return std::move(handled_cb).Run(handled); |
| } |
| |
| // Called when ARC returned a handler list for the |url|. |
| void OnUrlHandlerList( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| const std::optional<url::Origin>& initiating_origin, |
| bool safe_to_bypass_ui, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| base::OnceCallback<void(bool)> handled_cb, |
| std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo> handlers) { |
| // ArcIntentHelperMojoDelegate is already varified non-null. |
| DCHECK(mojo_delegate); |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // We only reach here if Chrome doesn't think it can handle the URL. If ARC is |
| // not running anymore, or Chrome is the only candidate returned, show the |
| // usual Chrome OS dialog that says we cannot handle the URL. |
| if (!mojo_delegate->IsArcAvailable() || |
| !ArcIconCacheDelegate::GetInstance() || handlers.empty() || |
| IsChromeOnlyAppCandidate(handlers)) { |
| ShowExternalProtocolDialogWithoutApps(web_contents, url, initiating_origin, |
| std::move(mojo_delegate), |
| std::move(handled_cb)); |
| return; |
| } |
| |
| // Check if the |url| should be handled right away without showing the UI. |
| GetActionResult result; |
| if (HandleUrl(web_contents, url, handlers, handlers.size(), &result, |
| safe_to_bypass_ui, mojo_delegate.get())) { |
| auto* context = web_contents ? web_contents->GetBrowserContext() : nullptr; |
| if (context && result == GetActionResult::HANDLE_URL_IN_ARC) { |
| apps::IntentHandlingMetrics::RecordExternalProtocolUserInteractionMetrics( |
| context, apps::PickerEntryType::kArc, |
| apps::IntentPickerCloseReason::PREFERRED_APP_FOUND, |
| /*should_persist=*/false); |
| } |
| return std::move(handled_cb).Run(/*handled=*/true); |
| } |
| |
| // Otherwise, retrieve icons of the activities. Since this function is for |
| // handling external protocols, Chrome is rarely in the list, but if the |url| |
| // is intent: with fallback or geo:, for example, it may be. In this case, we |
| // remove it from the handler list and show the "Stay in Chrome" button |
| // instead. |
| bool show_stay_in_chrome = false; |
| std::vector<ArcIntentHelperMojoDelegate::ActivityName> activities; |
| auto it = handlers.begin(); |
| while (it != handlers.end()) { |
| if (it->package_name == kArcIntentHelperPackageName) { |
| it = handlers.erase(it); |
| show_stay_in_chrome = true; |
| } else { |
| activities.emplace_back(it->package_name, it->activity_name); |
| ++it; |
| } |
| } |
| ArcIconCacheDelegate::GetInstance()->GetActivityIcons( |
| activities, base::BindOnce(OnAppIconsReceived, web_contents, url, |
| initiating_origin, safe_to_bypass_ui, |
| std::move(handlers), std::move(mojo_delegate), |
| std::move(handled_cb), show_stay_in_chrome)); |
| } |
| |
| } // namespace |
| |
| void RunArcExternalProtocolDialog( |
| const GURL& url, |
| const std::optional<url::Origin>& initiating_origin, |
| base::WeakPtr<WebContents> web_contents, |
| ui::PageTransition page_transition, |
| bool has_user_gesture, |
| bool is_in_fenced_frame_tree, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| base::OnceCallback<void(bool)> handled_cb) { |
| // This function is for external protocols that Chrome cannot handle. |
| DCHECK(!url.SchemeIsHTTPOrHTTPS()) << url; |
| |
| // For external protocol navigation, always ignore the FROM_API qualifier. |
| const ui::PageTransition masked_page_transition = |
| apps::LinkCapturingNavigationThrottle::MaskOutPageTransition( |
| page_transition, ui::PAGE_TRANSITION_FROM_API); |
| |
| if (!apps::LinkCapturingNavigationThrottle::IsCapturableLinkNavigation( |
| masked_page_transition, |
| /*allow_form_submit=*/true, is_in_fenced_frame_tree, |
| has_user_gesture)) { |
| LOG(WARNING) << "RunArcExternalProtocolDialog: ignoring " << url |
| << " with PageTransition=" << masked_page_transition |
| << ", is_in_fenced_frame_tree=" << is_in_fenced_frame_tree |
| << ", has_user_gesture=" << has_user_gesture; |
| return std::move(handled_cb).Run(false); |
| } |
| |
| // Check ArcIntentHelperMojoDelegate is set. |
| if (!mojo_delegate) { |
| LOG(ERROR) << "ArcIntentHelperMojoDelegate is null. " |
| << "This is required for mojo connection ." |
| << "For testing, set FakeArcIntentHelperMojo."; |
| return std::move(handled_cb).Run(false); |
| } |
| |
| // Make sure that RequestUrlHandlerList API is supported before resetting |
| // user data. |
| if (!mojo_delegate->IsRequestUrlHandlerListAvailable()) { |
| // RequestUrlHandlerList is either not supported or not yet ready. |
| ShowExternalProtocolDialogWithoutApps(web_contents, url, initiating_origin, |
| std::move(mojo_delegate), |
| std::move(handled_cb)); |
| return; |
| } |
| |
| if (!web_contents || !web_contents->GetBrowserContext() || |
| web_contents->GetBrowserContext()->IsOffTheRecord()) { |
| return std::move(handled_cb).Run(/*handled=*/false); |
| } |
| |
| const bool safe_to_bypass_ui = |
| GetAndResetSafeToRedirectToArcWithoutUserConfirmationFlag( |
| web_contents.get()); |
| |
| // Show ARC version of the dialog, which is IntentPickerBubbleView. To show |
| // the bubble view, we need to ask ARC for a handler list first. |
| mojo_delegate->RequestUrlHandlerList( |
| url.spec(), |
| base::BindOnce(OnUrlHandlerList, web_contents, url, initiating_origin, |
| safe_to_bypass_ui, std::move(mojo_delegate), |
| std::move(handled_cb))); |
| } |
| |
| GetActionResult GetActionForTesting( |
| const GURL& original_url, |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& handlers, |
| size_t selected_app_index, |
| GurlAndActivityInfo* out_url_and_activity_name, |
| bool* safe_to_bypass_ui) { |
| return GetAction(original_url, handlers, selected_app_index, |
| out_url_and_activity_name, safe_to_bypass_ui); |
| } |
| |
| GURL GetUrlToNavigateOnDeactivateForTesting( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| return GetUrlToNavigateOnDeactivate(handlers); |
| } |
| |
| bool GetAndResetSafeToRedirectToArcWithoutUserConfirmationFlagForTesting( |
| WebContents* web_contents) { |
| return GetAndResetSafeToRedirectToArcWithoutUserConfirmationFlag( |
| web_contents); |
| } |
| |
| bool IsChromeAnAppCandidateForTesting( |
| const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>& |
| handlers) { |
| return IsChromeAnAppCandidate(handlers); |
| } |
| |
| void OnIntentPickerClosedForTesting( |
| base::WeakPtr<WebContents> web_contents, |
| const GURL& url, |
| bool safe_to_bypass_ui, |
| std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo> handlers, |
| std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate, |
| std::vector<SharingTargetDeviceInfo> devices, |
| const std::string& selected_app_package, |
| apps::PickerEntryType entry_type, |
| apps::IntentPickerCloseReason reason, |
| bool should_persist) { |
| OnIntentPickerClosed( |
| web_contents, url, safe_to_bypass_ui, std::move(handlers), |
| std::move(mojo_delegate), std::move(devices), |
| apps::IntentPickerBubbleType::kExternalProtocol, selected_app_package, |
| entry_type, reason, should_persist); |
| } |
| |
| } // namespace arc |