[Android] expose "textShowPassword" to the framework (#30780)

diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart
index b57a7d5..9833b4a 100644
--- a/lib/ui/platform_dispatcher.dart
+++ b/lib/ui/platform_dispatcher.dart
@@ -826,6 +826,16 @@
     _onTextScaleFactorChangedZone = Zone.current;
   }
 
+  /// Whether briefly displaying the characters as you type in obscured text
+  /// fields is enabled in system settings.
+  ///
+  /// See also:
+  ///
+  ///  * [EditableText.obscureText], which when set to true hides the text in
+  ///    the text field.
+  bool get brieflyShowPassword => _brieflyShowPassword;
+  bool _brieflyShowPassword = true;
+
   /// The setting indicating the current brightness mode of the host platform.
   /// If the platform has no preference, [platformBrightness] defaults to
   /// [Brightness.light].
@@ -857,6 +867,11 @@
 
     final double textScaleFactor = (data['textScaleFactor'] as num).toDouble();
     final bool alwaysUse24HourFormat = data['alwaysUse24HourFormat'] as bool;
+    // This field is optional.
+    final bool? brieflyShowPassword = data['brieflyShowPassword'] as bool?;
+    if (brieflyShowPassword != null) {
+      _brieflyShowPassword = brieflyShowPassword;
+    }
     final Brightness platformBrightness =
     data['platformBrightness'] as String == 'dark' ? Brightness.dark : Brightness.light;
     final PlatformConfiguration previousConfiguration = configuration;
diff --git a/lib/ui/window.dart b/lib/ui/window.dart
index 31e6303..7fd11a9 100644
--- a/lib/ui/window.dart
+++ b/lib/ui/window.dart
@@ -425,6 +425,15 @@
   ///    observe when this value changes.
   double get textScaleFactor => platformDispatcher.textScaleFactor;
 
+  /// Whether briefly displaying the characters as you type in obscured text
+  /// fields is enabled in system settings.
+  ///
+  /// See also:
+  ///
+  ///  * [EditableText.obscureText], which when set to true hides the text in
+  ///    the text field.
+  bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword;
+
   /// The setting indicating whether time should always be shown in the 24-hour
   /// format.
   ///
diff --git a/lib/web_ui/lib/platform_dispatcher.dart b/lib/web_ui/lib/platform_dispatcher.dart
index 1027aa7..b9c1e9c 100644
--- a/lib/web_ui/lib/platform_dispatcher.dart
+++ b/lib/web_ui/lib/platform_dispatcher.dart
@@ -81,6 +81,8 @@
 
   double get textScaleFactor => configuration.textScaleFactor;
 
+  bool get brieflyShowPassword => true;
+
   VoidCallback? get onTextScaleFactorChanged;
   set onTextScaleFactorChanged(VoidCallback? callback);
 
diff --git a/lib/web_ui/lib/window.dart b/lib/web_ui/lib/window.dart
index a2a0dbb..b60ab11 100644
--- a/lib/web_ui/lib/window.dart
+++ b/lib/web_ui/lib/window.dart
@@ -47,6 +47,9 @@
   String get initialLifecycleState => platformDispatcher.initialLifecycleState;
 
   double get textScaleFactor => platformDispatcher.textScaleFactor;
+
+  bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword;
+
   bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat;
 
   VoidCallback? get onTextScaleFactorChanged => platformDispatcher.onTextScaleFactorChanged;
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index b172d2a..39b4ebd 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -9,9 +9,13 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.database.ContentObserver;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
 import android.text.format.DateFormat;
 import android.util.AttributeSet;
 import android.util.SparseArray;
@@ -140,6 +144,25 @@
         }
       };
 
+  private final ContentObserver systemSettingsObserver =
+      new ContentObserver(new Handler(Looper.getMainLooper())) {
+        @Override
+        public void onChange(boolean selfChange) {
+          super.onChange(selfChange);
+          if (flutterEngine == null) {
+            return;
+          }
+          Log.v(TAG, "System settings changed. Sending user settings to Flutter.");
+          sendUserSettingsToFlutter();
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+          // The Flutter app may change system settings.
+          return true;
+        }
+      };
+
   private final FlutterUiDisplayListener flutterUiDisplayListener =
       new FlutterUiDisplayListener() {
         @Override
@@ -1149,6 +1172,13 @@
 
     // Push View and Context related information from Android to Flutter.
     sendUserSettingsToFlutter();
+    getContext()
+        .getContentResolver()
+        .registerContentObserver(
+            Settings.System.getUriFor(Settings.System.TEXT_SHOW_PASSWORD),
+            false,
+            systemSettingsObserver);
+
     localizationPlugin.sendLocalesToFlutter(getResources().getConfiguration());
     sendViewportMetricsToFlutter();
 
@@ -1190,6 +1220,8 @@
       listener.onFlutterEngineDetachedFromFlutterView();
     }
 
+    getContext().getContentResolver().unregisterContentObserver(systemSettingsObserver);
+
     flutterEngine.getPlatformViewsController().detachFromView();
 
     // Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
@@ -1394,6 +1426,10 @@
         .getSettingsChannel()
         .startMessage()
         .setTextScaleFactor(getResources().getConfiguration().fontScale)
+        .setBrieflyShowPassword(
+            Settings.System.getInt(
+                    getContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1)
+                == 1)
         .setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
         .setPlatformBrightness(brightness)
         .send();
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
index cc01aea..1be798c 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
@@ -13,6 +13,7 @@
 
   public static final String CHANNEL_NAME = "flutter/settings";
   private static final String TEXT_SCALE_FACTOR = "textScaleFactor";
+  private static final String BRIEFLY_SHOW_PASSWORD = "brieflyShowPassword";
   private static final String ALWAYS_USE_24_HOUR_FORMAT = "alwaysUse24HourFormat";
   private static final String PLATFORM_BRIGHTNESS = "platformBrightness";
 
@@ -42,6 +43,12 @@
     }
 
     @NonNull
+    public MessageBuilder setBrieflyShowPassword(@NonNull boolean brieflyShowPassword) {
+      message.put(BRIEFLY_SHOW_PASSWORD, brieflyShowPassword);
+      return this;
+    }
+
+    @NonNull
     public MessageBuilder setUse24HourFormat(boolean use24HourFormat) {
       message.put(ALWAYS_USE_24_HOUR_FORMAT, use24HourFormat);
       return this;
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
index 4a95816..0c02fab 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -1042,6 +1042,8 @@
     when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class)))
         .thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setBrieflyShowPassword(any(Boolean.class)))
+        .thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder);
     when(fakeSettingsChannel.startMessage()).thenReturn(fakeMessageBuilder);
 
diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java
index 1dfa692..c7ede28 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java
@@ -28,6 +28,7 @@
 import android.media.Image.Plane;
 import android.media.ImageReader;
 import android.os.Build;
+import android.provider.Settings;
 import android.view.DisplayCutout;
 import android.view.Surface;
 import android.view.View;
@@ -213,6 +214,8 @@
     SettingsChannel fakeSettingsChannel = mock(SettingsChannel.class);
     SettingsChannel.MessageBuilder fakeMessageBuilder = mock(SettingsChannel.MessageBuilder.class);
     when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setBrieflyShowPassword(any(Boolean.class)))
+        .thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class)))
         .thenAnswer(
@@ -262,6 +265,8 @@
     SettingsChannel fakeSettingsChannel = mock(SettingsChannel.class);
     SettingsChannel.MessageBuilder fakeMessageBuilder = mock(SettingsChannel.MessageBuilder.class);
     when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setBrieflyShowPassword(any(Boolean.class)))
+        .thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder);
     when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class)))
         .thenAnswer(
@@ -285,6 +290,77 @@
     assertEquals(SettingsChannel.PlatformBrightness.dark, reportedBrightness.get());
   }
 
+  @Test
+  public void itSendsTextShowPasswordToFrameworkOnAttach() {
+    // Setup test.
+    AtomicReference<Boolean> reportedShowPassword = new AtomicReference<>();
+
+    FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class));
+    FlutterEngine flutterEngine =
+        spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
+    Settings.System.putInt(
+        flutterView.getContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1);
+
+    SettingsChannel fakeSettingsChannel = mock(SettingsChannel.class);
+    SettingsChannel.MessageBuilder fakeMessageBuilder = mock(SettingsChannel.MessageBuilder.class);
+    when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class)))
+        .thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setBrieflyShowPassword(any(Boolean.class)))
+        .thenAnswer(
+            new Answer<SettingsChannel.MessageBuilder>() {
+              @Override
+              public SettingsChannel.MessageBuilder answer(InvocationOnMock invocation)
+                  throws Throwable {
+                reportedShowPassword.set((Boolean) invocation.getArguments()[0]);
+                return fakeMessageBuilder;
+              }
+            });
+    when(fakeSettingsChannel.startMessage()).thenReturn(fakeMessageBuilder);
+    when(flutterEngine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
+
+    flutterView.attachToFlutterEngine(flutterEngine);
+
+    // Verify results.
+    assertTrue(reportedShowPassword.get());
+  }
+
+  public void itSendsTextHidePasswordToFrameworkOnAttach() {
+    // Setup test.
+    AtomicReference<Boolean> reportedShowPassword = new AtomicReference<>();
+
+    FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class));
+    FlutterEngine flutterEngine =
+        spy(new FlutterEngine(RuntimeEnvironment.application, mockFlutterLoader, mockFlutterJni));
+    Settings.System.putInt(
+        flutterView.getContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 0);
+
+    SettingsChannel fakeSettingsChannel = mock(SettingsChannel.class);
+    SettingsChannel.MessageBuilder fakeMessageBuilder = mock(SettingsChannel.MessageBuilder.class);
+    when(fakeMessageBuilder.setTextScaleFactor(any(Float.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setPlatformBrightness(any(SettingsChannel.PlatformBrightness.class)))
+        .thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setUse24HourFormat(any(Boolean.class))).thenReturn(fakeMessageBuilder);
+    when(fakeMessageBuilder.setBrieflyShowPassword(any(Boolean.class)))
+        .thenAnswer(
+            new Answer<SettingsChannel.MessageBuilder>() {
+              @Override
+              public SettingsChannel.MessageBuilder answer(InvocationOnMock invocation)
+                  throws Throwable {
+                reportedShowPassword.set((Boolean) invocation.getArguments()[0]);
+                return fakeMessageBuilder;
+              }
+            });
+    when(fakeSettingsChannel.startMessage()).thenReturn(fakeMessageBuilder);
+    when(flutterEngine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
+
+    flutterView.attachToFlutterEngine(flutterEngine);
+
+    // Verify results.
+    assertFalse(reportedShowPassword.get());
+  }
+
   // This test uses the API 30+ Algorithm for window insets. The legacy algorithm is
   // set to -1 values, so it is clear if the wrong algorithm is used.
   @Test