chromium/chrome/browser/chromeos/arc/arc_external_protocol_dialog.cc

// 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 <map>

#include "base/functional/bind.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/not_fatal_until.h"
#include "base/ranges/algorithm.h"
#include "build/chromeos_buildflags.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.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 "components/arc/common/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"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/apps/link_capturing/metrics/intent_handling_metrics.h"
#endif

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 =
      base::ranges::find(devices, device_guid, &SharingTargetDeviceInfo::guid);
  CHECK(it != devices.end(), base::NotFatalUntil::M130);
  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.
#if BUILDFLAG(IS_CHROMEOS_ASH)
  auto* context = web_contents ? web_contents->GetBrowserContext() : nullptr;
#endif

  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 BUILDFLAG(IS_CHROMEOS_ASH)
    if (context) {
      apps::IntentHandlingMetrics::RecordExternalProtocolUserInteractionMetrics(
          context, entry_type, reason, should_persist);
    }
#endif
    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_IN_MIGRATION();
      return;  // no UMA recording.
    case apps::IntentPickerCloseReason::STAY_IN_CHROME:
      LOG(ERROR) << "Chrome is not a valid option for external protocol URLs";
      NOTREACHED_IN_MIGRATION();
      return;  // no UMA recording.
    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 BUILDFLAG(IS_CHROMEOS_ASH)
  if (context) {
    apps::IntentHandlingMetrics::RecordExternalProtocolUserInteractionMetrics(
        context, entry_type, reason, should_persist);
  }
#endif
}

// 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())) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    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);
    }
#endif
    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