// 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 "components/input/web_input_event_builders_android.h"

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

#include "base/android/jni_android.h"
#include "base/android/scoped_java_ref.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "ui/events/android/key_event_utils.h"
#include "ui/events/android/motion_event_android_factory.h"
#include "ui/events/android/motion_event_android_java.h"
#include "ui/events/features.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/motionevent_jni_headers/MotionEvent_jni.h"
#include "ui/events/test/scoped_event_test_tick_clock.h"
#include "ui/events/velocity_tracker/motion_event.h"

using base::android::AttachCurrentThread;
using base::android::ScopedJavaLocalRef;
using blink::WebKeyboardEvent;
using blink::WebMouseEvent;
using blink::WebMouseWheelEvent;

namespace {

const int kCombiningAccent = 0x80000000;
const int kCombiningAccentMask = 0x7fffffff;
const int kCompositionKeyCode = 229;

WebKeyboardEvent CreateFakeWebKeyboardEvent(JNIEnv* env,
                                            int key_code,
                                            int web_modifier,
                                            int unicode_character) {
  ScopedJavaLocalRef<jobject> keydown_event =
      ui::events::android::CreateKeyEvent(env, 0, key_code);

  WebKeyboardEvent web_event = input::WebKeyboardEventBuilder::Build(
      env, keydown_event, WebKeyboardEvent::Type::kKeyDown, web_modifier,
      blink::WebInputEvent::GetStaticTimeStampForTests(), key_code, 0,
      unicode_character, false);
  return web_event;
}

}  // anonymous namespace

class WebInputEventBuilderAndroidTest : public testing::Test {};

// This test case is based on VirtualKeyboard layout.
// https://github.com/android/platform_frameworks_base/blob/master/data/keyboards/Virtual.kcm
TEST(WebInputEventBuilderAndroidTest, DomKeyCtrlShift) {
  JNIEnv* env = AttachCurrentThread();

  struct DomKeyTestCase {
    int key_code;
    int character;
    int shift_character;
  } table[] = {
      {AKEYCODE_0, '0', ')'}, {AKEYCODE_1, '1', '!'}, {AKEYCODE_2, '2', '@'},
      {AKEYCODE_3, '3', '#'}, {AKEYCODE_4, '4', '$'}, {AKEYCODE_5, '5', '%'},
      {AKEYCODE_6, '6', '^'}, {AKEYCODE_7, '7', '&'}, {AKEYCODE_8, '8', '*'},
      {AKEYCODE_9, '9', '('}, {AKEYCODE_A, 'a', 'A'}, {AKEYCODE_B, 'b', 'B'},
      {AKEYCODE_C, 'c', 'C'}, {AKEYCODE_D, 'd', 'D'}, {AKEYCODE_E, 'e', 'E'},
      {AKEYCODE_F, 'f', 'F'}, {AKEYCODE_G, 'g', 'G'}, {AKEYCODE_H, 'h', 'H'},
      {AKEYCODE_I, 'i', 'I'}, {AKEYCODE_J, 'j', 'J'}, {AKEYCODE_K, 'k', 'K'},
      {AKEYCODE_L, 'l', 'L'}, {AKEYCODE_M, 'm', 'M'}, {AKEYCODE_N, 'n', 'N'},
      {AKEYCODE_O, 'o', 'O'}, {AKEYCODE_P, 'p', 'P'}, {AKEYCODE_Q, 'q', 'Q'},
      {AKEYCODE_R, 'r', 'R'}, {AKEYCODE_S, 's', 'S'}, {AKEYCODE_T, 't', 'T'},
      {AKEYCODE_U, 'u', 'U'}, {AKEYCODE_V, 'v', 'V'}, {AKEYCODE_W, 'w', 'W'},
      {AKEYCODE_X, 'x', 'X'}, {AKEYCODE_Y, 'y', 'Y'}, {AKEYCODE_Z, 'z', 'Z'}};

  for (const DomKeyTestCase& entry : table) {
    // Tests DomKey without modifier.
    WebKeyboardEvent web_event =
        CreateFakeWebKeyboardEvent(env, entry.key_code, 0, entry.character);
    EXPECT_EQ(ui::DomKey::FromCharacter(entry.character),
              ui::DomKey(web_event.dom_key));

    // Tests DomKey with Ctrl.
    web_event = CreateFakeWebKeyboardEvent(env, entry.key_code,
                                           WebKeyboardEvent::kControlKey, 0);
    EXPECT_EQ(ui::DomKey::FromCharacter(entry.character),
              ui::DomKey(web_event.dom_key));

    // Tests DomKey with Ctrl and Shift.
    web_event = CreateFakeWebKeyboardEvent(
        env, entry.key_code,
        WebKeyboardEvent::kControlKey | WebKeyboardEvent::kShiftKey, 0);
    EXPECT_EQ(ui::DomKey::FromCharacter(entry.shift_character),
              ui::DomKey(web_event.dom_key));
  }
}

// This test case is based on VirtualKeyboard layout.
// https://github.com/android/platform_frameworks_base/blob/master/data/keyboards/Virtual.kcm
TEST(WebInputEventBuilderAndroidTest, DomKeyCtrlAlt) {
  JNIEnv* env = AttachCurrentThread();

  struct DomKeyTestCase {
    int key_code;
    int character;
    int alt_character;
  } table[] = {{AKEYCODE_0, '0', 0},         {AKEYCODE_1, '1', 0},
               {AKEYCODE_2, '2', 0},         {AKEYCODE_3, '3', 0},
               {AKEYCODE_4, '4', 0},         {AKEYCODE_5, '5', 0},
               {AKEYCODE_6, '6', 0},         {AKEYCODE_7, '7', 0},
               {AKEYCODE_8, '8', 0},         {AKEYCODE_9, '9', 0},
               {AKEYCODE_A, 'a', 0},         {AKEYCODE_B, 'b', 0},
               {AKEYCODE_C, 'c', u'\u00e7'}, {AKEYCODE_D, 'd', 0},
               {AKEYCODE_E, 'e', u'\u0301'}, {AKEYCODE_F, 'f', 0},
               {AKEYCODE_G, 'g', 0},         {AKEYCODE_H, 'h', 0},
               {AKEYCODE_I, 'i', u'\u0302'}, {AKEYCODE_J, 'j', 0},
               {AKEYCODE_K, 'k', 0},         {AKEYCODE_L, 'l', 0},
               {AKEYCODE_M, 'm', 0},         {AKEYCODE_N, 'n', u'\u0303'},
               {AKEYCODE_O, 'o', 0},         {AKEYCODE_P, 'p', 0},
               {AKEYCODE_Q, 'q', 0},         {AKEYCODE_R, 'r', 0},
               {AKEYCODE_S, 's', u'\u00df'}, {AKEYCODE_T, 't', 0},
               {AKEYCODE_U, 'u', u'\u0308'}, {AKEYCODE_V, 'v', 0},
               {AKEYCODE_W, 'w', 0},         {AKEYCODE_X, 'x', 0},
               {AKEYCODE_Y, 'y', 0},         {AKEYCODE_Z, 'z', 0}};

  for (const DomKeyTestCase& entry : table) {
    // Tests DomKey with Alt.
    WebKeyboardEvent web_event = CreateFakeWebKeyboardEvent(
        env, entry.key_code, WebKeyboardEvent::kAltKey, entry.alt_character);
    ui::DomKey expected_alt_dom_key;
    if (entry.alt_character == 0) {
      expected_alt_dom_key = ui::DomKey::FromCharacter(entry.character);
    } else if (entry.alt_character & kCombiningAccent) {
      expected_alt_dom_key = ui::DomKey::DeadKeyFromCombiningCharacter(
          entry.alt_character & kCombiningAccentMask);
    } else {
      expected_alt_dom_key = ui::DomKey::FromCharacter(entry.alt_character);
    }
    EXPECT_EQ(expected_alt_dom_key, ui::DomKey(web_event.dom_key));

    // Tests DomKey with Ctrl and Alt.
    web_event = CreateFakeWebKeyboardEvent(
        env, entry.key_code,
        WebKeyboardEvent::kControlKey | WebKeyboardEvent::kAltKey, 0);
    EXPECT_EQ(ui::DomKey::FromCharacter(entry.character),
              ui::DomKey(web_event.dom_key));
  }
}

// Testing AKEYCODE_LAST_CHANNEL because it's overlapping with
// COMPOSITION_KEY_CODE (both 229).
TEST(WebInputEventBuilderAndroidTest, LastChannelKey) {
  JNIEnv* env = AttachCurrentThread();

  // AKEYCODE_LAST_CHANNEL (229) is not defined in minimum NDK.
  WebKeyboardEvent web_event = CreateFakeWebKeyboardEvent(env, 229, 0, 0);
  EXPECT_EQ(229, web_event.native_key_code);
  EXPECT_EQ(ui::KeyboardCode::VKEY_UNKNOWN, web_event.windows_key_code);
  EXPECT_EQ(static_cast<int>(ui::DomCode::NONE), web_event.dom_code);
  EXPECT_EQ(ui::DomKey::MEDIA_LAST, ui::DomKey(web_event.dom_key));
}

// Synthetic key event should produce DomKey::UNIDENTIFIED.
TEST(WebInputEventBuilderAndroidTest, DomKeySyntheticEvent) {
  WebKeyboardEvent web_event = input::WebKeyboardEventBuilder::Build(
      nullptr, nullptr, WebKeyboardEvent::Type::kKeyDown, 0,
      blink::WebInputEvent::GetStaticTimeStampForTests(), kCompositionKeyCode,
      0, 0, false);
  EXPECT_EQ(kCompositionKeyCode, web_event.native_key_code);
  EXPECT_EQ(ui::KeyboardCode::VKEY_UNKNOWN, web_event.windows_key_code);
  EXPECT_EQ(static_cast<int>(ui::DomCode::NONE), web_event.dom_code);
  EXPECT_EQ(ui::DomKey::UNIDENTIFIED, ui::DomKey(web_event.dom_key));
}

// Verifies that supplementary-plane code points (e.g. emojis or extra-plane
// chars) are encoded as surrogate pairs in unmodified_text and text, matching
// dom_key and avoiding truncation / narrowing.
TEST(WebInputEventBuilderAndroidTest, SupplementaryPlaneTextVsDomKeyMatch) {
  constexpr int kCodePoint = 0x1F600;  // U+1F600 GRINNING FACE

  // Use the synthetic-event form (env=nullptr) so the test doesn't depend on a
  // device KCM; |unicode_character| is supplied directly, mirroring what
  // ImeAdapterImpl.sendKeyEvent forwards from KeyEvent.getUnicodeChar().
  WebKeyboardEvent web_event = input::WebKeyboardEventBuilder::Build(
      /*env=*/nullptr, /*android_key_event=*/nullptr,
      WebKeyboardEvent::Type::kKeyDown, /*modifiers=*/0,
      blink::WebInputEvent::GetStaticTimeStampForTests(),
      /*keycode=*/AKEYCODE_A, /*scancode=*/0,
      /*unicode_character=*/kCodePoint,
      /*is_system_key=*/false);

  // dom_key preserves the full 21-bit scalar -> JS event.key will be "😀".
  EXPECT_EQ(ui::DomKey::FromCharacter(kCodePoint),
            ui::DomKey(web_event.dom_key))
      << "dom_key must hold the full code point";
  std::string js_event_key =
      ui::KeycodeConverter::DomKeyToKeyString(ui::DomKey(web_event.dom_key));
  EXPECT_EQ("\xF0\x9F\x98\x80", js_event_key);  // UTF-8 of U+1F600

  // text[] now holds the properly encoded UTF-16 surrogate pair.
  std::u16string text(web_event.text.data());
  EXPECT_EQ(2u, text.size()) << "text[] must hold the 2 surrogate code units";
  EXPECT_EQ(std::u16string(u"\U0001F600"), text)
      << "text[] must match the saved-password encoding (surrogate pair)";
}

// Testing new Android keycode introduced in API 24.
TEST(WebInputEventBuilderAndroidTest, CutCopyPasteKey) {
  JNIEnv* env = AttachCurrentThread();

  struct DomKeyTestCase {
    int key_code;
    ui::DomKey key;
  } test_cases[] = {
      {AKEYCODE_CUT, ui::DomKey::CUT},
      {AKEYCODE_COPY, ui::DomKey::COPY},
      {AKEYCODE_PASTE, ui::DomKey::PASTE},
  };

  for (const auto& entry : test_cases) {
    WebKeyboardEvent web_event =
        CreateFakeWebKeyboardEvent(env, entry.key_code, 0, 0);
    EXPECT_EQ(entry.key, ui::DomKey(web_event.dom_key));
  }
}

TEST(WebInputEventBuilderAndroidTest, WebMouseEventCoordinates) {
  constexpr int kEventTimeNs = 5'000'000;
  const base::TimeTicks event_time =
      base::TimeTicks() + base::Nanoseconds(kEventTimeNs);

  ui::test::ScopedEventTestTickClock clock;
  clock.SetNowTicks(event_time);

  const float pressure = 0.4f;
  ui::MotionEventAndroid::Pointer p0(1, 13.7f, -7.13f, 5.3f, 1.2f, pressure,
                                     0.1f, 0.2f,
                                     ui::MotionEventAndroid::GetAndroidToolType(
                                         ui::MotionEvent::ToolType::MOUSE));
  const float raw_offset_x = 11.f;
  const float raw_offset_y = 22.f;
  const float kPixToDip = 0.5f;

  JNIEnv* env = AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> obj =
      JNI_MotionEvent::Java_MotionEvent_obtain(
          env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/0, /*y=*/0,
          /*metaState=*/AMETA_ALT_ON);
  auto motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
      env, obj, kPixToDip,
      /*ticks_x=*/0.f,
      /*ticks_y=*/0.f,
      /*tick_multiplier=*/0.f,
      /*oldest_event_time=*/
      base::TimeTicks() + base::Nanoseconds(kEventTimeNs),
      /*android_action=*/AMOTION_EVENT_ACTION_DOWN,
      /*pointer_count=*/1,
      /*history_size=*/0,
      /*action_index=*/-1,
      /*android_action_button=*/0,
      /*android_gesture_classification=*/0,
      /*android_button_state=*/1,
      /*raw_offset_x_pixels=*/raw_offset_x,
      /*raw_offset_y_pixels=*/raw_offset_y,
      /*for_touch_handle=*/false,
      /*pointer0=*/&p0,
      /*pointer1=*/nullptr);

  WebMouseEvent web_event = input::WebMouseEventBuilder::Build(
      *motion_event, blink::WebInputEvent::Type::kMouseDown, 1,
      ui::MotionEvent::BUTTON_PRIMARY);
  EXPECT_EQ(web_event.PositionInWidget().x(), p0.pos_x_pixels * kPixToDip);
  EXPECT_EQ(web_event.PositionInWidget().y(), p0.pos_y_pixels * kPixToDip);
  EXPECT_EQ(web_event.PositionInScreen().x(),
            (p0.pos_x_pixels + raw_offset_x) * kPixToDip);
  EXPECT_EQ(web_event.PositionInScreen().y(),
            (p0.pos_y_pixels + raw_offset_y) * kPixToDip);
  EXPECT_EQ(web_event.button, blink::WebPointerProperties::Button::kLeft);
  EXPECT_EQ(web_event.TimeStamp(), event_time);
  EXPECT_EQ(web_event.force, pressure);
}

TEST(WebInputEventBuilderAndroidTest, WebMouseWheelEventScrollDirection) {
  constexpr int kEventTimeNs = 5'000'000;
  const base::TimeTicks event_time =
      base::TimeTicks() + base::Nanoseconds(kEventTimeNs);

  ui::test::ScopedEventTestTickClock clock;
  clock.SetNowTicks(event_time);

  const float kPixToDip = 0.5f;
  // Android MotionEvents with positive ticks request a scroll RIGHT (X) and
  // DOWN (Y).
  // For Physical Mouse (Source Mouse + Tool Mouse), we expect "Traditional"
  // behavior:
  // - Vertical: Natural (Content moves with scroll).
  // - Horizontal: Desktop/Inverse (Content moves opposite to scroll).
  // Since Blink expects Desktop behavior by default for Mouse, but Android
  // inputs for "Scroll Right" are negative (historically), we must NEGATE
  // the input to get Positive Delta (Scroll Right).
  const float kTicksX = 10.0f;
  const float kTicksY = 5.0f;
  const float kTickMultiplier = 2.0f;

  ui::MotionEventAndroid::Pointer p0(0, 10.f, 20.f, 0.f, 0.f, 0.f, 0.f, 0.f,
                                     ui::MotionEventAndroid::GetAndroidToolType(
                                         ui::MotionEvent::ToolType::MOUSE));

  JNIEnv* env = AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> obj =
      JNI_MotionEvent::Java_MotionEvent_obtain(
          env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/10.f,
          /*y=*/20.f,
          /*metaState=*/0);
  JNI_MotionEvent::Java_MotionEvent_setSource(env, obj, AINPUT_SOURCE_MOUSE);
  auto motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
      env, obj, kPixToDip, kTicksX, kTicksY, kTickMultiplier, event_time,
      AMOTION_EVENT_ACTION_HOVER_MOVE,
      /*pointer_count=*/1,
      /*history_size=*/0,
      /*action_index=*/-1,
      /*android_action_button=*/0,
      /*android_gesture_classification=*/0,
      /*android_button_state=*/0,
      /*raw_offset_x_pixels=*/0.f,
      /*raw_offset_y_pixels=*/0.f,
      /*for_touch_handle=*/false, &p0,
      /*pointer1=*/nullptr);

  WebMouseWheelEvent web_event =
      input::WebMouseWheelEventBuilder::Build(*motion_event);

  // For Physical Mouse, horizontal scrolling IS negated.
  EXPECT_EQ(web_event.delta_x, -kTicksX * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_x, -kTicksX);
  EXPECT_EQ(web_event.delta_y, kTicksY * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_y, kTicksY);
  EXPECT_EQ(web_event.TimeStamp(), event_time);
}

TEST(WebInputEventBuilderAndroidTest,
     WebMouseWheelEventTouchpadScrollDirection_MouseSource) {
  constexpr int kEventTimeNs = 5'000'000;
  const base::TimeTicks event_time =
      base::TimeTicks() + base::Nanoseconds(kEventTimeNs);

  ui::test::ScopedEventTestTickClock clock;
  clock.SetNowTicks(event_time);

  const float kPixToDip = 0.5f;
  // For Touchpad-as-Mouse (Source Mouse + Tool Finger), we expect "Natural"
  // behavior, where the content moves with the scroll direction. This matches
  // Android's default behavior for these inputs. Therefore, we pass the values
  // as-is without negation.
  const float kTicksX = 10.0f;
  const float kTicksY = 5.0f;
  const float kTickMultiplier = 2.0f;

  ui::MotionEventAndroid::Pointer p0(0, 10.f, 20.f, 0.f, 0.f, 0.f, 0.f, 0.f,
                                     ui::MotionEventAndroid::GetAndroidToolType(
                                         ui::MotionEvent::ToolType::FINGER));

  JNIEnv* env = AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> obj =
      JNI_MotionEvent::Java_MotionEvent_obtain(
          env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/10.f,
          /*y=*/20.f,
          /*metaState=*/0);
  JNI_MotionEvent::Java_MotionEvent_setSource(env, obj, AINPUT_SOURCE_MOUSE);
  auto motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
      env, obj, kPixToDip, kTicksX, kTicksY, kTickMultiplier, event_time,
      AMOTION_EVENT_ACTION_HOVER_MOVE,
      /*pointer_count=*/1,
      /*history_size=*/0,
      /*action_index=*/-1,
      /*android_action_button=*/0,
      /*android_gesture_classification=*/0,
      /*android_button_state=*/0,
      /*raw_offset_x_pixels=*/0.f,
      /*raw_offset_y_pixels=*/0.f,
      /*for_touch_handle=*/false, &p0,
      /*pointer1=*/nullptr);

  WebMouseWheelEvent web_event =
      input::WebMouseWheelEventBuilder::Build(*motion_event);

  // For Touchpad (Mouse Source + Finger Tool), horizontal scrolling is NOT
  // negated.
  EXPECT_EQ(web_event.delta_x, kTicksX * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_x, kTicksX);
  EXPECT_EQ(web_event.delta_y, kTicksY * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_y, kTicksY);
  EXPECT_EQ(web_event.TimeStamp(), event_time);
}

TEST(WebInputEventBuilderAndroidTest,
     WebMouseWheelEventTouchpadScrollDirection) {
  constexpr int kEventTimeNs = 5'000'000;
  const base::TimeTicks event_time =
      base::TimeTicks() + base::Nanoseconds(kEventTimeNs);

  ui::test::ScopedEventTestTickClock clock;
  clock.SetNowTicks(event_time);

  const float kPixToDip = 0.5f;
  const float kTicksX = 10.0f;
  const float kTicksY = 5.0f;
  const float kTickMultiplier = 2.0f;

  ui::MotionEventAndroid::Pointer p0(0, 10.f, 20.f, 0.f, 0.f, 0.f, 0.f, 0.f,
                                     ui::MotionEventAndroid::GetAndroidToolType(
                                         ui::MotionEvent::ToolType::MOUSE));

  JNIEnv* env = AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> obj =
      JNI_MotionEvent::Java_MotionEvent_obtain(
          env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/10.f,
          /*y=*/20.f,
          /*metaState=*/0);
  JNI_MotionEvent::Java_MotionEvent_setSource(env, obj, AINPUT_SOURCE_TOUCHPAD);
  auto motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
      env, obj, kPixToDip, kTicksX, kTicksY, kTickMultiplier, event_time,
      AMOTION_EVENT_ACTION_HOVER_MOVE,
      /*pointer_count=*/1,
      /*history_size=*/0,
      /*action_index=*/-1,
      /*android_action_button=*/0,
      /*android_gesture_classification=*/0,
      /*android_button_state=*/0,
      /*raw_offset_x_pixels=*/0.f,
      /*raw_offset_y_pixels=*/0.f,
      /*for_touch_handle=*/false, &p0,
      /*pointer1=*/nullptr);

  WebMouseWheelEvent web_event =
      input::WebMouseWheelEventBuilder::Build(*motion_event);

  // For Touchpads, horizontal scrolling is NOT negated.
  EXPECT_EQ(web_event.delta_x, kTicksX * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_x, kTicksX);
  EXPECT_EQ(web_event.delta_y, kTicksY * kTickMultiplier * kPixToDip);
  EXPECT_EQ(web_event.wheel_ticks_y, kTicksY);
  EXPECT_EQ(web_event.delta_units,
            ui::ScrollGranularity::kScrollByPrecisePixel);
  EXPECT_EQ(web_event.TimeStamp(), event_time);
}

TEST(WebInputEventBuilderAndroidTest,
     WebMouseWheelEventTouchpadDetectionFeature) {
  const base::TimeTicks event_time = base::TimeTicks();
  const float kPixToDip = 0.5f;
  const float kTicksX = 10.0f;
  const float kTicksY = 5.0f;
  const float kTickMultiplier = 2.0f;

  // Use ToolType::UNKNOWN to trigger the new detection logic.
  ui::MotionEventAndroid::Pointer p0(0, 10.f, 20.f, 0.f, 0.f, 0.f, 0.f, 0.f,
                                     ui::MotionEventAndroid::GetAndroidToolType(
                                         ui::MotionEvent::ToolType::UNKNOWN));

  JNIEnv* env = AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jobject> obj =
      JNI_MotionEvent::Java_MotionEvent_obtain(
          env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/10.f,
          /*y=*/20.f,
          /*metaState=*/0);
  JNI_MotionEvent::Java_MotionEvent_setSource(env, obj, AINPUT_SOURCE_MOUSE);
  auto motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
      env, obj, kPixToDip, kTicksX, kTicksY, kTickMultiplier, event_time,
      AMOTION_EVENT_ACTION_SCROLL,
      /*pointer_count=*/1,
      /*history_size=*/0,
      /*action_index=*/-1,
      /*android_action_button=*/0,
      /*android_gesture_classification=*/0,
      /*android_button_state=*/0,
      /*raw_offset_x_pixels=*/0.f,
      /*raw_offset_y_pixels=*/0.f,
      /*for_touch_handle=*/false, &p0,
      /*pointer1=*/nullptr);

  {
    base::test::ScopedFeatureList scoped_feature_list;
    scoped_feature_list.InitAndDisableFeature(ui::kAndroidTouchpadDetection);
    WebMouseWheelEvent web_event =
        input::WebMouseWheelEventBuilder::Build(*motion_event);
    EXPECT_EQ(web_event.delta_units, ui::ScrollGranularity::kScrollByPixel);
  }

  {
    base::test::ScopedFeatureList scoped_feature_list;
    scoped_feature_list.InitAndEnableFeature(ui::kAndroidTouchpadDetection);
    WebMouseWheelEvent web_event =
        input::WebMouseWheelEventBuilder::Build(*motion_event);
    EXPECT_EQ(web_event.delta_units,
              ui::ScrollGranularity::kScrollByPrecisePixel);

    // Mouse is still recognized as not precise.
    p0.tool_type = static_cast<int>(ui::MotionEvent::ToolType::MOUSE);
    motion_event = ui::MotionEventAndroidFactory::CreateFromJava(
        env, obj, kPixToDip, kTicksX, kTicksY, kTickMultiplier, event_time,
        AMOTION_EVENT_ACTION_SCROLL,
        /*pointer_count=*/1,
        /*history_size=*/0,
        /*action_index=*/-1,
        /*android_action_button=*/0,
        /*android_gesture_classification=*/0,
        /*android_button_state=*/0,
        /*raw_offset_x_pixels=*/0.f,
        /*raw_offset_y_pixels=*/0.f,
        /*for_touch_handle=*/false, &p0,
        /*pointer1=*/nullptr);
    web_event = input::WebMouseWheelEventBuilder::Build(*motion_event);
    EXPECT_EQ(web_event.delta_units, ui::ScrollGranularity::kScrollByPixel);
  }
}

// TODO(crbug.com/41353469): Add more tests for WebMouseEventBuilder
