// Copyright 2012 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/android/tab_web_contents_delegate_android.h"

#include <android/keycodes.h>
#include <stddef.h>

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/actor/actor_util.h"
#include "chrome/browser/android/customtabs/client_data_header_web_contents_observer.h"
#include "chrome/browser/android/framebust_intervention/framebust_blocked_delegate_android.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/content_settings/sound_content_setting_observer.h"
#include "chrome/browser/file_select_helper.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/history/history_tab_helper.h"
#include "chrome/browser/media/protected_media_identifier_permission_context.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/preloading/preloading_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/chrome_password_reuse_detection_manager_client.h"
#include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager_factory.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/blocked_content/chrome_popup_navigation_delegate.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/prefs/prefs_tab_helper.h"
#include "chrome/browser/ui/tab_helpers.h"
#include "chrome/browser/vr/vr_tab_helper.h"
#include "chrome/browser/webapps/installable/installed_webapp_bridge.h"
#include "chrome/browser/webapps/installable/installed_webapp_geolocation_context.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/url_constants.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/content/browser/content_autofill_driver_factory.h"
#include "components/blocked_content/popup_blocker.h"
#include "components/blocked_content/popup_tracker.h"
#include "components/browser_ui/util/android/url_constants.h"
#include "components/external_intents/android/external_intents_features.h"
#include "components/find_in_page/find_notification_details.h"
#include "components/find_in_page/find_tab_helper.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/javascript_dialogs/app_modal_dialog_manager.h"
#include "components/javascript_dialogs/tab_modal_dialog_manager.h"
#include "components/navigation_interception/intercept_navigation_delegate.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/paint_preview/buildflags/buildflags.h"
#include "components/safe_browsing/content/browser/safe_browsing_navigation_observer.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "media/mojo/mojom/media_types.mojom.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom.h"
#include "third_party/blink/public/mojom/frame/blocked_navigation_types.mojom.h"
#include "third_party/blink/public/mojom/input/pointer_lock_result.mojom.h"
#include "third_party/blink/public/mojom/page/draggable_region.mojom.h"
#include "third_party/blink/public/mojom/picture_in_picture/picture_in_picture.mojom.h"
#include "third_party/blink/public/mojom/window_features/window_features.mojom.h"
#include "third_party/jni_zero/default_conversions.h"
#include "third_party/skia/include/core/SkRegion.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/android/rect_jni_conversion.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "url/android/gurl_android.h"
#include "url/origin.h"
#if BUILDFLAG(ENABLE_PAINT_PREVIEW)
#include "components/paint_preview/browser/paint_preview_client.h"
#endif

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/TabWebContentsDelegateAndroidImpl_jni.h"

using base::android::AttachCurrentThread;
using base::android::JavaRef;
using base::android::ScopedJavaLocalRef;
using blink::mojom::FileChooserParams;
using content::WebContents;

namespace {

static ScopedJavaLocalRef<jobject>
JNI_TabWebContentsDelegateAndroidImpl_CreateJavaWindowFeatures(
    JNIEnv* env,
    const blink::mojom::WindowFeatures& window_features) {
  return ScopedJavaLocalRef<jobject>(
      Java_TabWebContentsDelegateAndroidImpl_createWindowFeatures(
          env, window_features.bounds.x(), window_features.bounds.y(),
          window_features.bounds.width(), window_features.bounds.height(),
          window_features.has_x, window_features.has_y,
          window_features.has_width, window_features.has_height));
}

static ScopedJavaLocalRef<jobject>
JNI_TabWebContentsDelegateAndroidImpl_CreateJavaPictureInPictureWindowOptions(
    JNIEnv* env,
    const display::Display& display,
    const blink::mojom::PictureInPictureWindowOptions& options) {
  gfx::Rect bounds =
      PictureInPictureWindowManager::GetInstance()
          ->CalculateInitialPictureInPictureWindowBounds(options, display);

  return ScopedJavaLocalRef<jobject>(
      Java_TabWebContentsDelegateAndroidImpl_createPictureInPictureWindowOptions(
          env, bounds, options.disallow_return_to_opener));
}

void ShowFramebustBlockMessageInternal(content::WebContents* web_contents,
                                       const GURL& url,
                                       const url::Origin& initiator_origin) {
  auto intervention_outcome =
      [](blocked_content::FramebustBlockedMessageDelegate::InterventionOutcome
             outcome) {
        UMA_HISTOGRAM_ENUMERATION("WebCore.Framebust.InterventionOutcome",
                                  outcome);
      };
  blocked_content::FramebustBlockedMessageDelegate::CreateForWebContents(
      web_contents);
  blocked_content::FramebustBlockedMessageDelegate*
      framebust_blocked_message_delegate =
          blocked_content::FramebustBlockedMessageDelegate::FromWebContents(
              web_contents);
  framebust_blocked_message_delegate->ShowMessage(
      url, initiator_origin,
      HostContentSettingsMapFactory::GetForProfile(
          web_contents->GetBrowserContext()),
      base::BindOnce(intervention_outcome));
}

// The amount of time to disallow repeated pointer lock calls after the user
// successfully escapes from one lock request.
constexpr base::TimeDelta kEffectiveUserEscapeDuration =
    base::Milliseconds(1250);

}  // anonymous namespace

namespace android {

TabWebContentsDelegateAndroid::TabWebContentsDelegateAndroid(
    JNIEnv* env,
    const jni_zero::JavaRef<jobject>& obj)
    : WebContentsDelegateAndroid(env, obj) {}

TabWebContentsDelegateAndroid::~TabWebContentsDelegateAndroid() = default;

void TabWebContentsDelegateAndroid::RunFileChooser(
    content::RenderFrameHost* render_frame_host,
    scoped_refptr<content::FileSelectListener> listener,
    const FileChooserParams& params) {
  FileSelectHelper::RunFileChooser(render_frame_host, std::move(listener),
                                   params);
}

bool TabWebContentsDelegateAndroid::ShouldFocusLocationBarByDefault(
    WebContents* source) {
  content::NavigationEntry* entry = source->GetController().GetActiveEntry();
  if (entry) {
    GURL url = entry->GetURL();
    GURL virtual_url = entry->GetVirtualURL();
    if ((url.SchemeIs(browser_ui::kChromeUINativeScheme) &&
         url.host() == chrome::kChromeUINewTabHost) ||
        (virtual_url.SchemeIs(browser_ui::kChromeUINativeScheme) &&
         virtual_url.host() == chrome::kChromeUINewTabHost)) {
      return true;
    }
  }
  return false;
}

void TabWebContentsDelegateAndroid::NavigationStateChanged(
    WebContents* source,
    content::InvalidateTypes changed_flags) {
  if (base::FeatureList::IsEnabled(
          chrome::android::kDeferNavigationStateChanged)) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            &TabWebContentsDelegateAndroid::NavigationStateChangedDeferred,
            weak_ptr_factory_.GetWeakPtr(), source, changed_flags));
    return;
  }
  NavigationStateChangedDeferred(source, changed_flags);
}

void TabWebContentsDelegateAndroid::NavigationStateChangedDeferred(
    WebContents* source,
    content::InvalidateTypes changed_flags) {
  WebContentsDelegateAndroid::NavigationStateChanged(source, changed_flags);
}

void TabWebContentsDelegateAndroid::FindReply(
    WebContents* web_contents,
    int request_id,
    int number_of_matches,
    const gfx::Rect& selection_rect,
    int active_match_ordinal,
    bool final_update) {
  find_in_page::FindTabHelper* find_tab_helper =
      find_in_page::FindTabHelper::FromWebContents(web_contents);
  if (!find_result_observations_.IsObservingSource(find_tab_helper))
    find_result_observations_.AddObservation(find_tab_helper);

  find_tab_helper->HandleFindReply(request_id,
                                   number_of_matches,
                                   selection_rect,
                                   active_match_ordinal,
                                   final_update);
}

void TabWebContentsDelegateAndroid::FindMatchRectsReply(
    WebContents* web_contents,
    int version,
    const std::vector<gfx::RectF>& rects,
    const gfx::RectF& active_rect) {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return;
  }

  Java_TabWebContentsDelegateAndroidImpl_onFindMatchRectsAvailable(
      env, obj, version, rects, active_rect);
}

// TODO(b/420669167): Remove this once actor tasks don't need to suppress new
// tabs on Android.
bool TabWebContentsDelegateAndroid::IsWebContentsCreationOverridden(
    content::RenderFrameHost* opener,
    content::SiteInstance* source_site_instance,
    content::mojom::WindowContainerType window_container_type,
    const GURL& opener_url,
    const std::string& frame_name,
    const GURL& target_url) {
  if (actor::HasActorTaskPreventingNewWebContents(opener)) {
    // If an ExecutionEngine is acting on the opener, prevent it from creating a
    // new WebContents. We'll instead force the navigation to happen in the same
    // tab. Note, we do this even if the task isn't active (e.g. paused) so that
    // a user action on behalf of the actor has the same behavior since the
    // resumed task will still be fixed to the tab.

    // However, if the opener is sandboxed and restricted from top-level
    // navigation, we cannot force a same-tab redirection as it would violate
    // the sandbox. Instead, we decline to override creation, allowing the
    // browser to safely open a new popup window (since kPopups is allowed).
    if (opener &&
        opener->IsSandboxed(network::mojom::WebSandboxFlags::kTopNavigation)) {
      return false;
    }
    return true;
  }

  return WebContentsDelegateAndroid::IsWebContentsCreationOverridden(
      opener, source_site_instance, window_container_type, opener_url,
      frame_name, target_url);
}

// TODO(b/420669167): Remove this once actor tasks don't need to suppress new
// tabs on Android.
content::WebContents* TabWebContentsDelegateAndroid::CreateCustomWebContents(
    content::RenderFrameHost* opener,
    content::SiteInstance* source_site_instance,
    bool is_new_browsing_instance,
    const GURL& opener_url,
    const std::string& frame_name,
    const GURL& target_url,
    WindowOpenDisposition disposition,
    const blink::mojom::WindowFeatures& window_features,
    const content::StoragePartitionConfig& partition_config,
    content::SessionStorageNamespace* session_storage_namespace) {
  if (actor::HasActorTaskPreventingNewWebContents(opener)) {
    // If an ExecutionEngine is acting on the opener, we force the navigation
    // to happen in the same tab.
    content::NavigationController::LoadURLParams params(target_url);
    params.initiator_frame_token = opener->GetFrameToken();
    params.initiator_process_id = opener->GetProcess()->GetID();
    params.initiator_origin = opener->GetLastCommittedOrigin();
    params.source_site_instance = source_site_instance;
    params.transition_type = ui::PAGE_TRANSITION_LINK;
    params.is_renderer_initiated = true;
    auto* opener_contents = content::WebContents::FromRenderFrameHost(opener);
    opener_contents->GetController().LoadURLWithParams(params);
    VLOG(1) << "Actor treated window open as same tab navigation. "
            << target_url;
    return nullptr;
  }

  return WebContentsDelegateAndroid::CreateCustomWebContents(
      opener, source_site_instance, is_new_browsing_instance, opener_url,
      frame_name, target_url, disposition, window_features, partition_config,
      session_storage_namespace);
}

content::JavaScriptDialogManager*
TabWebContentsDelegateAndroid::GetJavaScriptDialogManager(
    WebContents* source) {
  return javascript_dialogs::TabModalDialogManager::FromWebContents(source);
}

void TabWebContentsDelegateAndroid::RequestMediaAccessPermission(
    content::WebContents* web_contents,
    const content::MediaStreamRequest& request,
    content::MediaResponseCallback callback) {
  MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
      web_contents, request, std::move(callback), nullptr);
}

bool TabWebContentsDelegateAndroid::CheckMediaAccessPermission(
    content::RenderFrameHost* render_frame_host,
    const url::Origin& security_origin,
    blink::mojom::MediaStreamType type) {
  return MediaCaptureDevicesDispatcher::GetInstance()
      ->CheckMediaAccessPermission(render_frame_host, security_origin, type);
}

void TabWebContentsDelegateAndroid::SetOverlayMode(bool use_overlay_mode) {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return;

  Java_TabWebContentsDelegateAndroidImpl_setOverlayMode(env, obj,
                                                        use_overlay_mode);
}

WebContents* TabWebContentsDelegateAndroid::OpenURLFromTab(
    WebContents* source,
    const content::OpenURLParams& params,
    base::OnceCallback<void(content::NavigationHandle&)>
        navigation_handle_callback) {
  WindowOpenDisposition disposition = params.disposition;
  if (!source || (disposition != WindowOpenDisposition::CURRENT_TAB &&
                  disposition != WindowOpenDisposition::NEW_FOREGROUND_TAB &&
                  disposition != WindowOpenDisposition::NEW_BACKGROUND_TAB &&
                  disposition != WindowOpenDisposition::OFF_THE_RECORD &&
                  disposition != WindowOpenDisposition::NEW_POPUP &&
                  disposition != WindowOpenDisposition::NEW_WINDOW)) {
    // We can't handle this here.  Give the parent a chance.
    return WebContentsDelegateAndroid::OpenURLFromTab(
        source, params, std::move(navigation_handle_callback));
  }

  Profile* profile = Profile::FromBrowserContext(source->GetBrowserContext());
  NavigateParams nav_params(profile, params.url, params.transition);
  nav_params.FillNavigateParamsFromOpenURLParams(params);
  nav_params.source_contents = source;
  nav_params.window_action = NavigateParams::WindowAction::kShowWindow;
  auto popup_delegate =
      std::make_unique<ChromePopupNavigationDelegate>(std::move(nav_params));
  if (blocked_content::ConsiderForPopupBlocking(params.disposition)) {
    popup_delegate.reset(static_cast<ChromePopupNavigationDelegate*>(
        blocked_content::MaybeBlockPopup(
            source, nullptr, std::move(popup_delegate), &params,
            blink::mojom::WindowFeatures(),
            HostContentSettingsMapFactory::GetForProfile(
                source->GetBrowserContext()))
            .release()));
    if (!popup_delegate)
      return nullptr;
  }

  if (disposition == WindowOpenDisposition::CURRENT_TAB) {
    // Ask the parent to handle in-place opening.
    return WebContentsDelegateAndroid::OpenURLFromTab(
        source, params, std::move(navigation_handle_callback));
  }

  popup_delegate->nav_params()->opened_by_another_window = true;
  TabModelList::HandlePopupNavigation(popup_delegate->nav_params());
  return nullptr;
}

bool TabWebContentsDelegateAndroid::ShouldResumeRequestsForCreatedWindow() {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return true;

  return Java_TabWebContentsDelegateAndroidImpl_shouldResumeRequestsForCreatedWindow(
      env, obj);
}

WebContents* TabWebContentsDelegateAndroid::AddNewContents(
    WebContents* source,
    std::unique_ptr<WebContents> new_contents,
    const GURL& target_url,
    WindowOpenDisposition disposition,
    const blink::mojom::WindowFeatures& window_features,
    bool user_gesture,
    bool* was_blocked) {
  // No code for this yet.
  DCHECK_NE(disposition, WindowOpenDisposition::SAVE_TO_DISK);
  // Can't create a new contents for the current tab - invalid case.
  DCHECK_NE(disposition, WindowOpenDisposition::CURRENT_TAB);

  // At this point the |new_contents| is beyond the popup blocker, but we use
  // the same logic for determining if the popup tracker needs to be attached.
  if (source && blocked_content::ConsiderForPopupBlocking(disposition)) {
    blocked_content::PopupTracker::CreateForWebContents(new_contents.get(),
                                                        source, disposition);
  }

  // Add the CCT header observer if it was present on the source contents.
  if (source) {
    auto* source_observer =
        customtabs::ClientDataHeaderWebContentsObserver::FromWebContents(
            source);
    if (source_observer) {
      customtabs::ClientDataHeaderWebContentsObserver::CreateForWebContents(
          new_contents.get());
      customtabs::ClientDataHeaderWebContentsObserver::FromWebContents(
          new_contents.get())
          ->SetHeader(source_observer->header());
    }
  }

  TabHelpers::AttachTabHelpers(new_contents.get());

  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  bool handled = false;
  if (!obj.is_null()) {
    ScopedJavaLocalRef<jobject> jwindow_features =
        JNI_TabWebContentsDelegateAndroidImpl_CreateJavaWindowFeatures(
            env, window_features);

    ScopedJavaLocalRef<jobject> jpicture_in_picture_options;
    if (new_contents->GetPictureInPictureOptions().has_value()) {
      const display::Screen* screen = display::Screen::Get();
      const display::Display display =
          screen->GetDisplayNearestWindow(source->GetTopLevelNativeWindow());

      jpicture_in_picture_options =
          JNI_TabWebContentsDelegateAndroidImpl_CreateJavaPictureInPictureWindowOptions(
              env, display, new_contents->GetPictureInPictureOptions().value());
    }

    handled = Java_TabWebContentsDelegateAndroidImpl_addNewContents(
        env, obj, source, new_contents.get(), target_url,
        static_cast<int32_t>(disposition), jwindow_features, user_gesture,
        jpicture_in_picture_options);
  }

  if (was_blocked)
    *was_blocked = !handled;

  // When handled is |true|, ownership has been passed to java, which in turn
  // creates a new TabAndroid instance to own the WebContents.
  if (handled) {
    if (disposition == WindowOpenDisposition::NEW_PICTURE_IN_PICTURE) {
      // For Document PiP, immediately enter PiP mode on the native side so that
      // we start observing the opener for navigations immediately.
      PictureInPictureWindowManager::GetInstance()
          ->EnterDocumentPictureInPicture(source, new_contents.get());
    }
    return new_contents.release();
  }

  return nullptr;
}

void TabWebContentsDelegateAndroid::OnDidBlockNavigation(
    content::WebContents* web_contents,
    const GURL& blocked_url,
    const GURL& initiator_url,
    const url::Origin& initiator_origin,
    blink::mojom::NavigationBlockedReason reason) {
  ShowFramebustBlockMessageInternal(web_contents, blocked_url,
                                    initiator_origin);
}

void TabWebContentsDelegateAndroid::UpdateUserGestureCarryoverInfo(
    content::WebContents* web_contents) {
  auto* intercept_navigation_delegate =
      navigation_interception::InterceptNavigationDelegate::Get(web_contents);
  if (intercept_navigation_delegate)
    intercept_navigation_delegate->OnResourceRequestWithGesture();
}

content::PictureInPictureResult
TabWebContentsDelegateAndroid::EnterPictureInPicture(
    content::WebContents* web_contents) {
  return PictureInPictureWindowManager::GetInstance()
      ->EnterVideoPictureInPicture(web_contents);
}

void TabWebContentsDelegateAndroid::ExitPictureInPicture() {
  PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture();
}

bool TabWebContentsDelegateAndroid::IsBackForwardCacheSupported(
    content::WebContents& web_contents) {
  return true;
}

content::PreloadingEligibility
TabWebContentsDelegateAndroid::IsPrerender2Supported(
    content::WebContents& web_contents,
    content::PreloadingTriggerType trigger_type) {
  Profile* profile =
      Profile::FromBrowserContext(web_contents.GetBrowserContext());
  return prefetch::IsSomePreloadingEnabled(*profile->GetPrefs());
}

device::mojom::GeolocationContext*
TabWebContentsDelegateAndroid::GetInstalledWebappGeolocationContext() {
  if (!IsInstalledWebappDelegateGeolocation())
    return nullptr;

  if (!installed_webapp_geolocation_context_) {
    installed_webapp_geolocation_context_ =
        std::make_unique<InstalledWebappGeolocationContext>();
  }
  return installed_webapp_geolocation_context_.get();
}

void TabWebContentsDelegateAndroid::GetAIPageContent(
    content::WebContents* web_contents,
    bool include_actionable_elements,
    base::OnceCallback<void(const std::string&)> callback) {
  auto options = include_actionable_elements
                     ? optimization_guide::ActionableAIPageContentOptions(
                           /*on_critical_path=*/false)
                     : optimization_guide::DefaultAIPageContentOptions(
                           /*on_critical_path=*/false);

  optimization_guide::GetAIPageContent(
      web_contents, std::move(options),
      base::BindOnce([](optimization_guide::AIPageContentResultOrError result)
                         -> std::string {
        if (!result.has_value()) {
          return "";
        }
        return result->proto.SerializeAsString();
      }).Then(std::move(callback)));
}

#if BUILDFLAG(ENABLE_PAINT_PREVIEW)
void TabWebContentsDelegateAndroid::CapturePaintPreviewOfSubframe(
    content::WebContents* web_contents,
    const gfx::Rect& rect,
    const base::UnguessableToken& guid,
    content::RenderFrameHost* render_frame_host) {
  auto* client =
      paint_preview::PaintPreviewClient::FromWebContents(web_contents);
  if (client)
    client->CaptureSubframePaintPreview(guid, rect, render_frame_host);
}
#endif

void TabWebContentsDelegateAndroid::OnFindResultAvailable(
    WebContents* web_contents) {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return;
  }

  const find_in_page::FindNotificationDetails& find_result =
      find_in_page::FindTabHelper::FromWebContents(web_contents)->find_result();

  Java_TabWebContentsDelegateAndroidImpl_onFindResultAvailable(
      env, obj, find_result.number_of_matches(), find_result.selection_rect(),
      find_result.active_match_ordinal(), find_result.final_update());
}

void TabWebContentsDelegateAndroid::OnFindTabHelperDestroyed(
    find_in_page::FindTabHelper* helper) {
  find_result_observations_.RemoveObservation(helper);
}

bool TabWebContentsDelegateAndroid::ShouldEnableEmbeddedMediaExperience()
    const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return false;
  return Java_TabWebContentsDelegateAndroidImpl_shouldEnableEmbeddedMediaExperience(
      env, obj);
}

bool TabWebContentsDelegateAndroid::IsDocumentPictureInPictureBlockedBySystem()
    const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return false;
  }

  return Java_TabWebContentsDelegateAndroidImpl_isDocumentPictureInPictureBlockedBySystem(
      env, obj);
}

bool TabWebContentsDelegateAndroid::IsPictureInPictureEnabled() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return false;
  return Java_TabWebContentsDelegateAndroidImpl_isPictureInPictureEnabled(env,
                                                                          obj);
}

bool TabWebContentsDelegateAndroid::CanShowAppBanners() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return false;
  return Java_TabWebContentsDelegateAndroidImpl_canShowAppBanners(env, obj);
}

const GURL TabWebContentsDelegateAndroid::GetManifestScope() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return GURL();
  return GURL(
      Java_TabWebContentsDelegateAndroidImpl_getManifestScope(env, obj));
}

bool TabWebContentsDelegateAndroid::IsCustomTab() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return false;
  return Java_TabWebContentsDelegateAndroidImpl_isCustomTab(env, obj);
}

bool TabWebContentsDelegateAndroid::IsInstalledWebappDelegateGeolocation()
    const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return false;
  return Java_TabWebContentsDelegateAndroidImpl_isInstalledWebappDelegateGeolocation(
      env, obj);
}

bool TabWebContentsDelegateAndroid::IsModalContextMenu() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null())
    return true;
  return Java_TabWebContentsDelegateAndroidImpl_isModalContextMenu(env, obj);
}

bool TabWebContentsDelegateAndroid::IsDynamicSafeAreaInsetsEnabled() const {
  JNIEnv* env = base::android::AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return false;
  }
  return Java_TabWebContentsDelegateAndroidImpl_isDynamicSafeAreaInsetsEnabled(
      env, obj);
}

content::KeyboardEventProcessingResult
TabWebContentsDelegateAndroid::PreHandleKeyboardEvent(
    WebContents* source,
    const input::NativeWebKeyboardEvent& event) {
  if (event.native_key_code == AKEYCODE_ESCAPE) {
    JNIEnv* env = AttachCurrentThread();
    ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);

    if (!obj.is_null() &&
        Java_TabWebContentsDelegateAndroidImpl_preHandleKeyboardEvent(
            env, obj, reinterpret_cast<intptr_t>(&event))) {
      return content::KeyboardEventProcessingResult::HANDLED;
    }

    // ExclusiveAccessManager handles the pointer lock escape.
    if (!base::FeatureList::IsEnabled(
            features::kEnableExclusiveAccessManager)) {
      auto* rwhva = source->GetTopLevelRenderWidgetHostView();
      if (rwhva && rwhva->IsPointerLocked()) {
        rwhva->UnlockPointer();
        pointer_lock_last_user_escape_time_ = base::TimeTicks::Now();
        return content::KeyboardEventProcessingResult::HANDLED;
      }
    }
  }

  return WebContentsDelegateAndroid::PreHandleKeyboardEvent(source, event);
}

void TabWebContentsDelegateAndroid::RequestPointerLock(
    WebContents* web_contents,
    bool user_gesture,
    bool last_unlocked_by_target) {
  if (!base::FeatureList::IsEnabled(blink::features::kPointerLockOnAndroid)) {
    WebContentsDelegateAndroid::RequestPointerLock(web_contents, user_gesture,
                                                   last_unlocked_by_target);
    return;
  }

  if (base::FeatureList::IsEnabled(features::kEnableExclusiveAccessManager)) {
    JNIEnv* env = AttachCurrentThread();

    ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
    if (obj.is_null()) {
      return;
    }

    Java_TabWebContentsDelegateAndroidImpl_requestPointerLock(
        env, obj, web_contents, user_gesture, last_unlocked_by_target);
    return;
  }

  // TODO(https://crbug.com/415732870): remove this part once
  // ExclusiveAccessManager is released.
  if (!last_unlocked_by_target && !web_contents->IsFullscreen()) {
    if (!user_gesture) {
      web_contents->GotResponseToPointerLockRequest(
          blink::mojom::PointerLockResult::kRequiresUserGesture);
      return;
    }
    if (base::TimeTicks::Now() <
        pointer_lock_last_user_escape_time_ + kEffectiveUserEscapeDuration) {
      web_contents->GotResponseToPointerLockRequest(
          blink::mojom::PointerLockResult::kUserRejected);
      return;
    }
  }

  web_contents->GotResponseToPointerLockRequest(
      blink::mojom::PointerLockResult::kSuccess);
}

void TabWebContentsDelegateAndroid::LostPointerLock() {
  if (!base::FeatureList::IsEnabled(features::kEnableExclusiveAccessManager)) {
    WebContentsDelegateAndroid::LostPointerLock();
    return;
  }

  JNIEnv* env = AttachCurrentThread();

  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return;
  }

  Java_TabWebContentsDelegateAndroidImpl_lostPointerLock(env, obj);
}

void TabWebContentsDelegateAndroid::DraggableRegionsChanged(
    const std::vector<blink::mojom::DraggableRegionPtr>& regions,
    WebContents* contents) {
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return;
  }

  // See AppBrowserController::DraggableRegionsChanged in
  // chrome/browser/ui/web_applications/app_browser_controller.cc.
  // This is inverted from that logic. On most OSes, we're doing a hit test to
  // determine whether we should allow a region to be dragged. On android, we
  // need to provide a list of *undraggable* Rects.
  float dip_scale = contents->GetNativeView()->GetDipScale();
  const gfx::Rect& wco_rect = contents->GetWindowsControlsOverlayRect();
  SkRegion sk_region(SkIRect::MakeLTRB(
      wco_rect.x() * dip_scale, wco_rect.y() * dip_scale,
      wco_rect.right() * dip_scale, wco_rect.bottom() * dip_scale));
  for (const auto& region : regions) {
    sk_region.op(
        SkIRect::MakeLTRB(region->bounds.x() * dip_scale,
                          region->bounds.y() * dip_scale,
                          region->bounds.right() * dip_scale,
                          region->bounds.bottom() * dip_scale),
        region->draggable ? SkRegion::kDifference_Op : SkRegion::kUnion_Op);
  }

  std::vector<gfx::Rect> non_draggable_rects;
  for (SkRegion::Iterator i(sk_region); !i.done(); i.next()) {
    non_draggable_rects.push_back(gfx::SkIRectToRect(i.rect()));
  }

  Java_TabWebContentsDelegateAndroidImpl_nonDraggableRegionsChanged(
      env, obj, non_draggable_rects);
}

bool TabWebContentsDelegateAndroid::IsImmersivePlaybackEnabled() const {
  JNIEnv* env = AttachCurrentThread();
  ScopedJavaLocalRef<jobject> obj = GetJavaDelegate(env);
  if (obj.is_null()) {
    return false;
  }
  return Java_TabWebContentsDelegateAndroidImpl_isImmersivePlaybackEnabled(env,
                                                                           obj);
}

}  // namespace android

static void JNI_TabWebContentsDelegateAndroidImpl_OnRendererUnresponsive(
    content::WebContents* web_contents) {
  // Rate limit the number of stack dumps so we don't overwhelm our crash
  // reports.
  if (web_contents && web_contents->GetPrimaryMainFrame() &&
      web_contents->GetPrimaryMainFrame()->GetProcess() &&
      base::RandDouble() < 0.01) {
    web_contents->GetPrimaryMainFrame()->GetProcess()->DumpProcessStack();
  }
}

DEFINE_JNI(TabWebContentsDelegateAndroidImpl)
