[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