Fix input mirroring in platform views when system language is RTL, and support is set in manifest. (#183472)

Fixes https://github.com/flutter/flutter/issues/182823

We use a
[`FrameLayout`](https://developer.android.com/reference/android/widget/FrameLayout)
to [wrap platform
views](https://github.com/flutter/flutter/blob/master/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java#L30).
We construct new `FrameLayout.LayoutParams` and set them on the frame
layout
([here](https://github.com/flutter/flutter/blob/758463261fabe1b37f2a4752bfc44a57e10c79b7/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java#L107),
and a couple of other places). Because we don't specify a gravity, it
defaults to
[UNSPECIFIED_GRAVITY](https://developer.android.com/reference/android/widget/FrameLayout.LayoutParams#gravity),
which is `Gravity.TOP | Gravity.START`. When a developer indicates that
their app supports RTL (right-to-left) layouts (e.g. arabic), and the
system language is set to one with an RTL layout, START becomes left
https://developer.android.com/reference/android/view/Gravity#getAbsoluteGravity(int,%20int),
and the view ends up thinking it is positioned relative to the right of
the screen, even though we are controlling the drawing and putting it on
the left.

Because we are handling the drawing, and all that matters is that
flutter knows where the view is, we can just statically set the
horizontal gravity to be left. Then flutter knows where to find the view
and deliver the tap to it, whether we are in an RTL or LTR layout.

---------

Co-authored-by: Gray Mackall <mackall@google.com>
diff --git a/dev/integration_tests/android_engine_test/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/android_engine_test/android/app/src/main/AndroidManifest.xml
index aaef79f..45ddbe9 100644
--- a/dev/integration_tests/android_engine_test/android/app/src/main/AndroidManifest.xml
+++ b/dev/integration_tests/android_engine_test/android/app/src/main/AndroidManifest.xml
@@ -6,7 +6,8 @@
     <application
         android:label="android_engine_test"
         android:name="${applicationName}"
-        android:icon="@mipmap/ic_launcher">
+        android:icon="@mipmap/ic_launcher"
+        android:supportsRtl="true">
         <activity
             android:name=".MainActivity"
             android:exported="true"
diff --git a/dev/integration_tests/android_engine_test/lib/hcpp/rtl_mirror_main.dart b/dev/integration_tests/android_engine_test/lib/hcpp/rtl_mirror_main.dart
new file mode 100644
index 0000000..8dba1850
--- /dev/null
+++ b/dev/integration_tests/android_engine_test/lib/hcpp/rtl_mirror_main.dart
@@ -0,0 +1,116 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'package:android_driver_extensions/extension.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_driver/driver_extension.dart';
+
+import '../src/allow_list_devices.dart';
+
+bool redTapped = false;
+bool blueTapped = false;
+
+void main() async {
+  ensureAndroidDevice();
+  enableFlutterDriverExtension(
+    handler: (String? command) async {
+      if (command == 'red_tapped') {
+        return redTapped.toString();
+      }
+      if (command == 'blue_tapped') {
+        return blueTapped.toString();
+      }
+      return json.encode(<String, Object?>{
+        'supported': await HybridAndroidViewController.checkIfSupported(),
+        'devicePixelRatio': WidgetsBinding.instance.platformDispatcher.views.first.devicePixelRatio,
+      });
+    },
+    commands: <CommandExtension>[nativeDriverCommands],
+  );
+
+  // Run on full screen.
+  await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
+  runApp(const MainApp());
+}
+
+class MainApp extends StatelessWidget {
+  const MainApp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const MaterialApp(
+      debugShowCheckedModeBanner: false,
+      home: Scaffold(
+        body: Directionality(textDirection: TextDirection.rtl, child: RTLMirrorRepro()),
+      ),
+    );
+  }
+}
+
+class RTLMirrorRepro extends StatefulWidget {
+  const RTLMirrorRepro({super.key});
+
+  @override
+  State<RTLMirrorRepro> createState() => _RTLMirrorReproState();
+}
+
+class _RTLMirrorReproState extends State<RTLMirrorRepro> {
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceAround,
+      children: [
+        // Blue Box (Flutter)
+        // In RTL, Row adds children from Right to Left.
+        // So this first child will be on the RIGHT.
+        InkWell(
+          key: const ValueKey('blue_box'),
+          onTap: () {
+            setState(() {
+              blueTapped = true;
+            });
+          },
+          child: Container(
+            width: 150,
+            height: 150,
+            color: Colors.blue,
+            child: const Center(
+              child: Text('Blue Box', style: TextStyle(color: Colors.white)),
+            ),
+          ),
+        ),
+        // Red Box (Platform View 2)
+        // This second child will be on the LEFT.
+        SizedBox(
+          width: 150,
+          height: 150,
+          child: Stack(
+            children: [
+              const Positioned.fill(
+                child: AndroidView(viewType: 'blue_orange_gradient_platform_view'),
+              ),
+              Center(
+                child: SizedBox(
+                  width: 125,
+                  height: 125,
+                  child: InkWell(
+                    key: const ValueKey('red_box_overlay'),
+                    onTap: () {
+                      setState(() {
+                        redTapped = true;
+                      });
+                    },
+                    child: Container(color: Colors.red),
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/dev/integration_tests/android_engine_test/test_driver/hcpp/rtl_mirror_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/hcpp/rtl_mirror_main_test.dart
new file mode 100644
index 0000000..8933958
--- /dev/null
+++ b/dev/integration_tests/android_engine_test/test_driver/hcpp/rtl_mirror_main_test.dart
@@ -0,0 +1,85 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:android_driver_extensions/native_driver.dart';
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart';
+
+void main() async {
+  late final FlutterDriver flutterDriver;
+  late final NativeDriver nativeDriver;
+
+  setUpAll(() async {
+    flutterDriver = await FlutterDriver.connect();
+    nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
+    await flutterDriver.waitUntilFirstFrameRasterized();
+  });
+
+  tearDownAll(() async {
+    await nativeDriver.close();
+    await flutterDriver.close();
+  });
+
+  test(
+    'tapping blue box on the right at physical coordinates does not mirror to red platform view on the left',
+    () async {
+      // 1. Get location properties.
+      final response = json.decode(await flutterDriver.requestData('')) as Map<String, Object?>;
+      final double devicePixelRatio = (response['devicePixelRatio']! as num).toDouble();
+      expect(devicePixelRatio, isPositive);
+
+      final DriverOffset center = await flutterDriver.getCenter(find.byValueKey('blue_box'));
+      final int physicalX = (center.dx * devicePixelRatio).round();
+      final int physicalY = (center.dy * devicePixelRatio).round();
+
+      // 2. Verify initial state.
+      final String initialState = await flutterDriver.requestData('red_tapped');
+      expect(initialState, 'false');
+
+      // 3. Tapping the blue box on the right using a physical ADB tap.
+      // This bypasses accessibility and native view lookups, as well as
+      // flutter driver tap dispatching as it is the only way to reproduce
+      // https://github.com/flutter/flutter/issues/182823.
+      print('Sending adb tap to physical coordinates: ($physicalX, $physicalY)');
+      final ProcessResult result = await Process.run('adb', <String>[
+        'shell',
+        'input',
+        'tap',
+        '$physicalX',
+        '$physicalY',
+      ]);
+      if (result.exitCode != 0) {
+        fail('Failed to send adb tap: ${result.stderr}');
+      }
+
+      // 4. Check results.
+      final String redTapped = await flutterDriver.requestData('red_tapped');
+      expect(
+        redTapped,
+        'false',
+        reason:
+            'Physical tap on the right (blue box) should NOT have triggered the left box (red platform view) tap handler.',
+      );
+
+      final String blueTapped = await flutterDriver.requestData('blue_tapped');
+      expect(
+        blueTapped,
+        'true',
+        reason: 'Physical tap on the right SHOULD have triggered the blue box tap handler.',
+      );
+
+      // 5. Sanity check: Tap the red box natively.
+      await flutterDriver.tap(find.byValueKey('red_box_overlay'));
+      final String redTappedAfter = await flutterDriver.requestData('red_tapped');
+      expect(
+        redTappedAfter,
+        'true',
+        reason: 'Directly tapping the red box SHOULD trigger its tap handler.',
+      );
+    },
+    timeout: Timeout.none,
+  );
+}
diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
index f8b8f24..d3d257a 100644
--- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
+++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
@@ -12,6 +12,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
@@ -101,7 +102,8 @@
     this.mutatorsStack = mutatorsStack;
     this.left = left;
     this.top = top;
-    FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
+    FrameLayout.LayoutParams layoutParams =
+        new FrameLayout.LayoutParams(width, height, Gravity.LEFT | Gravity.TOP);
     layoutParams.leftMargin = left;
     layoutParams.topMargin = top;
     setLayoutParams(layoutParams);
diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index c82ea40..9fd5246 100644
--- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -12,6 +12,7 @@
 import android.content.MutableContextWrapper;
 import android.os.Build;
 import android.util.SparseArray;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
@@ -331,6 +332,7 @@
               (FrameLayout.LayoutParams) viewWrapper.getLayoutParams();
           layoutParams.topMargin = physicalTop;
           layoutParams.leftMargin = physicalLeft;
+          layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
           viewWrapper.setLayoutParams(layoutParams);
         }
 
@@ -389,6 +391,10 @@
           final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams();
           viewWrapperLayoutParams.width = physicalWidth;
           viewWrapperLayoutParams.height = physicalHeight;
+          if (viewWrapperLayoutParams instanceof FrameLayout.LayoutParams) {
+            ((FrameLayout.LayoutParams) viewWrapperLayoutParams).gravity =
+                Gravity.LEFT | Gravity.TOP;
+          }
           viewWrapper.setLayoutParams(viewWrapperLayoutParams);
 
           final View embeddedView = platformView.getView();
@@ -626,7 +632,7 @@
     viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight);
 
     final FrameLayout.LayoutParams viewWrapperLayoutParams =
-        new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
+        new FrameLayout.LayoutParams(physicalWidth, physicalHeight, Gravity.LEFT | Gravity.TOP);
 
     // Size and position the view wrapper.
     final int physicalTop = toPhysicalPixels(request.logicalTop);
diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java
index 82a6e58..402243d 100644
--- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java
+++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java
@@ -9,6 +9,7 @@
 import android.content.Context;
 import android.graphics.PixelFormat;
 import android.util.SparseArray;
+import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
@@ -518,7 +519,7 @@
     parentView.bringToFront();
 
     final FrameLayout.LayoutParams layoutParams =
-        new FrameLayout.LayoutParams(viewWidth, viewHeight);
+        new FrameLayout.LayoutParams(viewWidth, viewHeight, Gravity.LEFT | Gravity.TOP);
     final View view = platformViews.get(viewId).getView();
     if (view != null) {
       view.setLayoutParams(layoutParams);