diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 761a54549f62f374e20c33954fbeb929b8778269..8696116e1a11d76e323de5ca878eb6fc91a8b2a5 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -446,6 +446,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/D
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
@@ -453,6 +454,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 37002cf5fd4ebd035be266df92d09682e52c14ed..73e9b462b4b486fd2ad2ed8247a6d5f00ac6b4d3 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -114,6 +114,7 @@ java_library("flutter_shell_java") {
"io/flutter/embedding/engine/dart/PlatformMessageHandler.java",
"io/flutter/embedding/engine/renderer/FlutterRenderer.java",
"io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java",
+ "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java",
"io/flutter/embedding/engine/systemchannels/KeyEventChannel.java",
"io/flutter/embedding/engine/systemchannels/LifecycleChannel.java",
"io/flutter/embedding/engine/systemchannels/LocalizationChannel.java",
@@ -121,6 +122,7 @@ java_library("flutter_shell_java") {
"io/flutter/embedding/engine/systemchannels/PlatformChannel.java",
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
"io/flutter/embedding/engine/systemchannels/SystemChannel.java",
+ "io/flutter/embedding/engine/systemchannels/TextInputChannel.java",
"io/flutter/plugin/common/ActivityLifecycleListener.java",
"io/flutter/plugin/common/BasicMessageChannel.java",
"io/flutter/plugin/common/BinaryCodec.java",
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..4bfcd37973b587c5c666d9624130cced9fdbd0c2
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
@@ -0,0 +1,120 @@
+package io.flutter.embedding.engine.systemchannels;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.StandardMessageCodec;
+
+/**
+ * System channel that sends accessibility requests and events from Flutter to Android.
+ *
+ * See {@link AccessibilityMessageHandler}, which lists all accessibility requests and
+ * events that might be sent from Flutter to the Android platform.
+ */
+public class AccessibilityChannel {
+ @NonNull
+ public BasicMessageChannel channel;
+ @Nullable
+ private AccessibilityMessageHandler handler;
+
+ private final BasicMessageChannel.MessageHandler parsingMessageHandler = new BasicMessageChannel.MessageHandler() {
+ @Override
+ public void onMessage(Object message, BasicMessageChannel.Reply reply) {
+ // If there is no handler to respond to this message then we don't need to
+ // parse it. Return.
+ if (handler == null) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ final HashMap annotatedEvent = (HashMap) message;
+ final String type = (String) annotatedEvent.get("type");
+ @SuppressWarnings("unchecked")
+ final HashMap data = (HashMap) annotatedEvent.get("data");
+
+ switch (type) {
+ case "announce":
+ String announceMessage = (String) data.get("message");
+ if (announceMessage != null) {
+ handler.announce(announceMessage);
+ }
+ break;
+ case "tap": {
+ Integer nodeId = (Integer) annotatedEvent.get("nodeId");
+ if (nodeId != null) {
+ handler.onTap(nodeId);
+ }
+ break;
+ }
+ case "longPress": {
+ Integer nodeId = (Integer) annotatedEvent.get("nodeId");
+ if (nodeId != null) {
+ handler.onLongPress(nodeId);
+ }
+ break;
+ }
+ case "tooltip": {
+ String tooltipMessage = (String) data.get("message");
+ if (tooltipMessage != null) {
+ handler.onTooltip(tooltipMessage);
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ /**
+ * Constructs an {@code AccessibilityChannel} that connects Android to the Dart code
+ * running in {@code dartExecutor}.
+ *
+ * The given {@code dartExecutor} is permitted to be idle or executing code.
+ *
+ * See {@link DartExecutor}.
+ */
+ public AccessibilityChannel(@NonNull DartExecutor dartExecutor) {
+ channel = new BasicMessageChannel<>(dartExecutor, "flutter/accessibility", StandardMessageCodec.INSTANCE);
+ channel.setMessageHandler(parsingMessageHandler);
+ }
+
+ /**
+ * Sets the {@link AccessibilityMessageHandler} which receives all events and requests
+ * that are parsed from the underlying accessibility channel.
+ */
+ public void setAccessibilityMessageHandler(@Nullable AccessibilityMessageHandler handler) {
+ this.handler = handler;
+ }
+
+ /**
+ * Handler that receives accessibility messages sent from Flutter to Android
+ * through a given {@link AccessibilityChannel}.
+ *
+ * To register an {@code AccessibilityMessageHandler} with a {@link AccessibilityChannel},
+ * see {@link AccessibilityChannel#setAccessibilityMessageHandler(AccessibilityMessageHandler)}.
+ */
+ public interface AccessibilityMessageHandler {
+ /**
+ * The Dart application would like the given {@code message} to be announced.
+ */
+ void announce(@NonNull String message);
+
+ /**
+ * The user has tapped on the artifact with the given {@code nodeId}.
+ */
+ void onTap(int nodeId);
+
+ /**
+ * The user has long pressed on the artifact with the given {@code nodeId}.
+ */
+ void onLongPress(int nodeId);
+
+ /**
+ * The user has opened a popup window, menu, dialog, etc.
+ */
+ void onTooltip(@NonNull String message);
+ }
+}
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
index ffb8f1f84a155db8e6b9c7e9d7ed4a285110ada0..52e4fbb46171de8ed00a03ad970790f7c09e4ee2 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
@@ -6,14 +6,17 @@ package io.flutter.embedding.engine.systemchannels;
import android.support.annotation.NonNull;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodChannel;
/**
- * TODO(mattcarroll): fill in javadoc for LocalizationChannel.
+ * Sends the platform's locales to Dart.
*/
public class LocalizationChannel {
@@ -24,12 +27,18 @@ public class LocalizationChannel {
this.channel = new MethodChannel(dartExecutor, "flutter/localization", JSONMethodCodec.INSTANCE);
}
- public void setLocale(String language, String country) {
- channel.invokeMethod("setLocale", Arrays.asList(language, country));
- }
-
- public void setMethodCallHandler(MethodChannel.MethodCallHandler handler) {
- channel.setMethodCallHandler(handler);
+ /**
+ * Send the given {@code locales} to Dart.
+ */
+ public void sendLocales(List locales) {
+ List data = new ArrayList<>();
+ for (Locale locale : locales) {
+ data.add(locale.getLanguage());
+ data.add(locale.getCountry());
+ data.add(locale.getScript());
+ data.add(locale.getVariant());
+ }
+ channel.invokeMethod("setLocale", data);
}
}
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
index b1ceb82b7a74d2b586030a0d86d629eb26e123c3..44269313bb9838d31dd57ea2ca46d4a5a4c7a6e6 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
@@ -4,26 +4,613 @@
package io.flutter.embedding.engine.systemchannels;
+import android.content.pm.ActivityInfo;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
/**
- * TODO(mattcarroll): fill in javadoc for PlatformChannel.
+ * System channel that receives requests for host platform behavior, e.g., haptic and sound
+ * effects, system chrome configurations, and clipboard interaction.
*/
public class PlatformChannel {
-
+ @NonNull
public final MethodChannel channel;
+ @Nullable
+ private PlatformMessageHandler platformMessageHandler;
+
+ private final MethodChannel.MethodCallHandler parsingMethodCallHandler = new MethodChannel.MethodCallHandler() {
+ @Override
+ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
+ if (platformMessageHandler == null) {
+ // If no explicit PlatformMessageHandler has been registered then we don't
+ // need to forward this call to an API. Return.
+ return;
+ }
+
+ String method = call.method;
+ Object arguments = call.arguments;
+ try {
+ switch (method) {
+ case "SystemSound.play":
+ try {
+ SoundType soundType = SoundType.fromValue((String) arguments);
+ platformMessageHandler.playSystemSound(soundType);
+ result.success(null);
+ } catch (NoSuchFieldException exception) {
+ // The desired sound type does not exist.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "HapticFeedback.vibrate":
+ try {
+ HapticFeedbackType feedbackType = HapticFeedbackType.fromValue((String) arguments);
+ platformMessageHandler.vibrateHapticFeedback(feedbackType);
+ result.success(null);
+ } catch (NoSuchFieldException exception) {
+ // The desired feedback type does not exist.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "SystemChrome.setPreferredOrientations":
+ try {
+ int androidOrientation = decodeOrientations((JSONArray) arguments);
+ platformMessageHandler.setPreferredOrientations(androidOrientation);
+ result.success(null);
+ } catch (JSONException | NoSuchFieldException exception) {
+ // JSONException: One or more expected fields were either omitted or referenced an invalid type.
+ // NoSuchFieldException: One or more expected fields were either omitted or referenced an invalid type.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "SystemChrome.setApplicationSwitcherDescription":
+ try {
+ AppSwitcherDescription description = decodeAppSwitcherDescription((JSONObject) arguments);
+ platformMessageHandler.setApplicationSwitcherDescription(description);
+ result.success(null);
+ } catch (JSONException exception) {
+ // One or more expected fields were either omitted or referenced an invalid type.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "SystemChrome.setEnabledSystemUIOverlays":
+ try {
+ List overlays = decodeSystemUiOverlays((JSONArray) arguments);
+ platformMessageHandler.showSystemOverlays(overlays);
+ result.success(null);
+ } catch (JSONException | NoSuchFieldException exception) {
+ // JSONException: One or more expected fields were either omitted or referenced an invalid type.
+ // NoSuchFieldException: One or more of the overlay names are invalid.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "SystemChrome.restoreSystemUIOverlays":
+ platformMessageHandler.restoreSystemUiOverlays();
+ result.success(null);
+ break;
+ case "SystemChrome.setSystemUIOverlayStyle":
+ try {
+ SystemChromeStyle systemChromeStyle = decodeSystemChromeStyle((JSONObject) arguments);
+ platformMessageHandler.setSystemUiOverlayStyle(systemChromeStyle);
+ result.success(null);
+ } catch (JSONException | NoSuchFieldException exception) {
+ // JSONException: One or more expected fields were either omitted or referenced an invalid type.
+ // NoSuchFieldException: One or more of the brightness names are invalid.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "SystemNavigator.pop":
+ platformMessageHandler.popSystemNavigator();
+ result.success(null);
+ break;
+ case "Clipboard.getData": {
+ String contentFormatName = (String) arguments;
+ ClipboardContentFormat clipboardFormat = null;
+ if (contentFormatName != null) {
+ try {
+ clipboardFormat = ClipboardContentFormat.fromValue(contentFormatName);
+ } catch (NoSuchFieldException exception) {
+ // An unsupported content format was requested. Return failure.
+ result.error("error", "No such clipboard content format: " + contentFormatName, null);
+ }
+ }
+ CharSequence clipboardContent = platformMessageHandler.getClipboardData(clipboardFormat);
+ if (clipboardContent != null) {
+ JSONObject response = new JSONObject();
+ response.put("text", clipboardContent);
+ result.success(response);
+ } else {
+ result.success(null);
+ }
+ break;
+ }
+ case "Clipboard.setData": {
+ String clipboardContent = ((JSONObject) arguments).getString("text");
+ platformMessageHandler.setClipboardData(clipboardContent);
+ result.success(null);
+ break;
+ }
+ default:
+ result.notImplemented();
+ break;
+ }
+ } catch (JSONException e) {
+ result.error("error", "JSON error: " + e.getMessage(), null);
+ }
+ }
+ };
+
+ /**
+ * Constructs a {@code PlatformChannel} that connects Android to the Dart code
+ * running in {@code dartExecutor}.
+ *
+ * The given {@code dartExecutor} is permitted to be idle or executing code.
+ *
+ * See {@link DartExecutor}.
+ */
public PlatformChannel(@NonNull DartExecutor dartExecutor) {
- this.channel = new MethodChannel(dartExecutor, "flutter/platform", JSONMethodCodec.INSTANCE);
+ channel = new MethodChannel(dartExecutor, "flutter/platform", JSONMethodCodec.INSTANCE);
+ channel.setMethodCallHandler(parsingMethodCallHandler);
+ }
+
+ /**
+ * Sets the {@link PlatformMessageHandler} which receives all events and requests
+ * that are parsed from the underlying platform channel.
+ */
+ public void setPlatformMessageHandler(@Nullable PlatformMessageHandler platformMessageHandler) {
+ this.platformMessageHandler = platformMessageHandler;
+ }
+
+ // TODO(mattcarroll): add support for IntDef annotations, then add @ScreenOrientation
+
+ /**
+ * Decodes a series of orientations to an aggregate desired orientation.
+ *
+ * @throws JSONException if {@code encodedOrientations} does not contain expected keys and value types.
+ * @throws NoSuchFieldException if any given encoded orientation is not a valid orientation name.
+ */
+ private int decodeOrientations(@NonNull JSONArray encodedOrientations) throws JSONException, NoSuchFieldException {
+ int requestedOrientation = 0x00;
+ int firstRequestedOrientation = 0x00;
+ for (int index = 0; index < encodedOrientations.length(); index += 1) {
+ String encodedOrientation = encodedOrientations.getString(index);
+ DeviceOrientation orientation = DeviceOrientation.fromValue(encodedOrientation);
+
+ switch (orientation) {
+ case PORTRAIT_UP:
+ requestedOrientation |= 0x01;
+ break;
+ case PORTRAIT_DOWN:
+ requestedOrientation |= 0x04;
+ break;
+ case LANDSCAPE_LEFT:
+ requestedOrientation |= 0x02;
+ break;
+ case LANDSCAPE_RIGHT:
+ requestedOrientation |= 0x08;
+ break;
+ }
+
+ if (firstRequestedOrientation == 0x00) {
+ firstRequestedOrientation = requestedOrientation;
+ }
+ }
+
+ switch (requestedOrientation) {
+ case 0x00:
+ return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ case 0x01:
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case 0x02:
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ case 0x04:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case 0x05:
+ return ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT;
+ case 0x08:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ case 0x0a:
+ return ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE;
+ case 0x0b:
+ return ActivityInfo.SCREEN_ORIENTATION_USER;
+ case 0x0f:
+ return ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
+ case 0x03: // portraitUp and landscapeLeft
+ case 0x06: // portraitDown and landscapeLeft
+ case 0x07: // portraitUp, portraitDown, and landscapeLeft
+ case 0x09: // portraitUp and landscapeRight
+ case 0x0c: // portraitDown and landscapeRight
+ case 0x0d: // portraitUp, portraitDown, and landscapeRight
+ case 0x0e: // portraitDown, landscapeLeft, and landscapeRight
+ // Android can't describe these cases, so just default to whatever the first
+ // specified value was.
+ switch (firstRequestedOrientation) {
+ case 0x01:
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ case 0x02:
+ return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ case 0x04:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ case 0x08:
+ return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ }
+ }
+
+ // Execution should never get this far, but if it does then we default
+ // to a portrait orientation.
+ return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ }
+
+ private AppSwitcherDescription decodeAppSwitcherDescription(@NonNull JSONObject encodedDescription) throws JSONException {
+ int color = encodedDescription.getInt("primaryColor");
+ if (color != 0) { // 0 means color isn't set, use system default
+ color = color | 0xFF000000; // color must be opaque if set
+ }
+ String label = encodedDescription.getString("label");
+ return new AppSwitcherDescription(color, label);
+ }
+
+ /**
+ * Decodes a list of JSON-encoded overlays to a list of {@link SystemUiOverlay}.
+ *
+ * @throws JSONException if {@code encodedSystemUiOverlay} does not contain expected keys and value types.
+ * @throws NoSuchFieldException if any of the given encoded overlay names are invalid.
+ */
+ private List decodeSystemUiOverlays(@NonNull JSONArray encodedSystemUiOverlay) throws JSONException, NoSuchFieldException {
+ List overlays = new ArrayList<>();
+ for (int i = 0; i < encodedSystemUiOverlay.length(); ++i) {
+ String encodedOverlay = encodedSystemUiOverlay.getString(i);
+ SystemUiOverlay overlay = SystemUiOverlay.fromValue(encodedOverlay);
+ switch(overlay) {
+ case TOP_OVERLAYS:
+ overlays.add(SystemUiOverlay.TOP_OVERLAYS);
+ break;
+ case BOTTOM_OVERLAYS:
+ overlays.add(SystemUiOverlay.BOTTOM_OVERLAYS);
+ break;
+ }
+ }
+ return overlays;
+ }
+
+ /**
+ * Decodes a JSON-encoded {@code encodedStyle} to a {@link SystemChromeStyle}.
+ *
+ * @throws JSONException if {@code encodedStyle} does not contain expected keys and value types.
+ * @throws NoSuchFieldException if any provided brightness name is invalid.
+ */
+ private SystemChromeStyle decodeSystemChromeStyle(@NonNull JSONObject encodedStyle) throws JSONException, NoSuchFieldException {
+ Brightness systemNavigationBarIconBrightness = null;
+ // TODO(mattcarroll): add color annotation
+ Integer systemNavigationBarColor = null;
+ // TODO(mattcarroll): add color annotation
+ Integer systemNavigationBarDividerColor = null;
+ Brightness statusBarIconBrightness = null;
+ // TODO(mattcarroll): add color annotation
+ Integer statusBarColor = null;
+
+ if (!encodedStyle.isNull("systemNavigationBarIconBrightness")) {
+ systemNavigationBarIconBrightness = Brightness.fromValue(encodedStyle.getString("systemNavigationBarIconBrightness"));
+ }
+
+ if (!encodedStyle.isNull("systemNavigationBarColor")) {
+ systemNavigationBarColor = encodedStyle.getInt("systemNavigationBarColor");
+ }
+
+ if (!encodedStyle.isNull("statusBarIconBrightness")) {
+ statusBarIconBrightness = Brightness.fromValue(encodedStyle.getString("statusBarIconBrightness"));
+ }
+
+ if (!encodedStyle.isNull("statusBarColor")) {
+ statusBarColor = encodedStyle.getInt("statusBarColor");
+ }
+
+ if (!encodedStyle.isNull("systemNavigationBarDividerColor")) {
+ systemNavigationBarDividerColor = encodedStyle.getInt("systemNavigationBarDividerColor");
+ }
+
+ return new SystemChromeStyle(
+ statusBarColor,
+ statusBarIconBrightness,
+ systemNavigationBarColor,
+ systemNavigationBarIconBrightness,
+ systemNavigationBarDividerColor
+ );
}
- public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handler) {
- channel.setMethodCallHandler(handler);
+ /**
+ * Handler that receives platform messages sent from Flutter to Android
+ * through a given {@link PlatformChannel}.
+ *
+ * To register a {@code PlatformMessageHandler} with a {@link PlatformChannel},
+ * see {@link PlatformChannel#setPlatformMessageHandler(PlatformMessageHandler)}.
+ */
+ public interface PlatformMessageHandler {
+ /**
+ * The Flutter application would like to play the given {@code soundType}.
+ */
+ void playSystemSound(@NonNull SoundType soundType);
+
+ /**
+ * The Flutter application would like to play the given haptic {@code feedbackType}.
+ */
+ void vibrateHapticFeedback(@NonNull HapticFeedbackType feedbackType);
+
+ /**
+ * The Flutter application would like to display in the given {@code androidOrientation}.
+ */
+ // TODO(mattcarroll): add @ScreenOrientation annotation
+ void setPreferredOrientations(int androidOrientation);
+
+ /**
+ * The Flutter application would like to be displayed in Android's app switcher with
+ * the visual representation described in the given {@code description}.
+ *
+ * See the related Android documentation:
+ * https://developer.android.com/guide/components/activities/recents
+ */
+ void setApplicationSwitcherDescription(@NonNull AppSwitcherDescription description);
+
+ /**
+ * The Flutter application would like the Android system to display the given
+ * {@code overlays}.
+ *
+ * {@link SystemUiOverlay#TOP_OVERLAYS} refers to system overlays such as the
+ * status bar, while {@link SystemUiOverlay#BOTTOM_OVERLAYS} refers to system
+ * overlays such as the back/home/recents navigation on the bottom of the screen.
+ *
+ * An empty list of {@code overlays} should hide all system overlays.
+ */
+ void showSystemOverlays(@NonNull List overlays);
+
+ /**
+ * The Flutter application would like to restore the visibility of system
+ * overlays to the last set of overlays sent via {@link #showSystemOverlays(List)}.
+ *
+ * If {@link #showSystemOverlays(List)} has yet to be called, then a default
+ * system overlay appearance is desired:
+ *
+ * {@code
+ * View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ * }
+ */
+ void restoreSystemUiOverlays();
+
+ /**
+ * The Flutter application would like the system chrome to present itself with
+ * the given {@code systemUiOverlayStyle}, i.e., the given status bar and
+ * navigation bar colors and brightness.
+ */
+ void setSystemUiOverlayStyle(@NonNull SystemChromeStyle systemUiOverlayStyle);
+
+ /**
+ * The Flutter application would like to pop the top item off of the Android
+ * app's navigation back stack.
+ */
+ void popSystemNavigator();
+
+ /**
+ * The Flutter application would like to receive the current data in the
+ * clipboard and have it returned in the given {@code format}.
+ */
+ @Nullable
+ CharSequence getClipboardData(@Nullable ClipboardContentFormat format);
+
+ /**
+ * The Flutter application would like to set the current data in the
+ * clipboard to the given {@code text}.
+ */
+ void setClipboardData(@NonNull String text);
}
+ /**
+ * Types of sounds the Android OS can play on behalf of an application.
+ */
+ public enum SoundType {
+ CLICK("SoundType.click");
+
+ static SoundType fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (SoundType soundType : SoundType.values()) {
+ if (soundType.encodedName.equals(encodedName)) {
+ return soundType;
+ }
+ }
+ throw new NoSuchFieldException("No such SoundType: " + encodedName);
+ }
+
+ @NonNull
+ private final String encodedName;
+
+ SoundType(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * The types of haptic feedback that the Android OS can generate on behalf
+ * of an application.
+ */
+ public enum HapticFeedbackType {
+ STANDARD(null),
+ LIGHT_IMPACT("HapticFeedbackType.lightImpact"),
+ MEDIUM_IMPACT("HapticFeedbackType.mediumImpact"),
+ HEAVY_IMPACT("HapticFeedbackType.heavyImpact"),
+ SELECTION_CLICK("HapticFeedbackType.selectionClick");
+
+ static HapticFeedbackType fromValue(@Nullable String encodedName) throws NoSuchFieldException {
+ for (HapticFeedbackType feedbackType : HapticFeedbackType.values()) {
+ if ((feedbackType.encodedName == null && encodedName == null)
+ || (feedbackType.encodedName != null && feedbackType.encodedName.equals(encodedName))) {
+ return feedbackType;
+ }
+ }
+ throw new NoSuchFieldException("No such HapticFeedbackType: " + encodedName);
+ }
+
+ @Nullable
+ private final String encodedName;
+
+ HapticFeedbackType(@Nullable String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * The possible desired orientations of a Flutter application.
+ */
+ public enum DeviceOrientation {
+ PORTRAIT_UP("DeviceOrientation.portraitUp"),
+ PORTRAIT_DOWN("DeviceOrientation.portraitDown"),
+ LANDSCAPE_LEFT("DeviceOrientation.landscapeLeft"),
+ LANDSCAPE_RIGHT("DeviceOrientation.landscapeRight");
+
+ static DeviceOrientation fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (DeviceOrientation orientation : DeviceOrientation.values()) {
+ if (orientation.encodedName.equals(encodedName)) {
+ return orientation;
+ }
+ }
+ throw new NoSuchFieldException("No such DeviceOrientation: " + encodedName);
+ }
+
+ @NonNull
+ private String encodedName;
+
+ DeviceOrientation(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * The set of Android system UI overlays as perceived by the Flutter application.
+ *
+ * Android includes many more overlay options and flags than what is provided by
+ * {@code SystemUiOverlay}. Flutter only requires control over a subset of the
+ * overlays and those overlays are represented by {@code SystemUiOverlay} values.
+ */
+ public enum SystemUiOverlay {
+ TOP_OVERLAYS("SystemUiOverlay.top"),
+ BOTTOM_OVERLAYS("SystemUiOverlay.bottom");
+
+ static SystemUiOverlay fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (SystemUiOverlay overlay : SystemUiOverlay.values()) {
+ if (overlay.encodedName.equals(encodedName)) {
+ return overlay;
+ }
+ }
+ throw new NoSuchFieldException("No such SystemUiOverlay: " + encodedName);
+ }
+
+ @NonNull
+ private String encodedName;
+
+ SystemUiOverlay(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * The color and label of an application that appears in Android's app switcher, AKA
+ * recents screen.
+ */
+ public static class AppSwitcherDescription {
+ // TODO(mattcarroll): add color annotation
+ public final int color;
+ @NonNull
+ public final String label;
+
+ public AppSwitcherDescription(int color, @NonNull String label) {
+ this.color = color;
+ this.label = label;
+ }
+ }
+
+ /**
+ * The color and brightness of system chrome, e.g., status bar and system navigation bar.
+ */
+ public static class SystemChromeStyle {
+ // TODO(mattcarroll): add color annotation
+ @Nullable
+ public final Integer statusBarColor;
+ @Nullable
+ public final Brightness statusBarIconBrightness;
+ // TODO(mattcarroll): add color annotation
+ @Nullable
+ public final Integer systemNavigationBarColor;
+ @Nullable
+ public final Brightness systemNavigationBarIconBrightness;
+ // TODO(mattcarroll): add color annotation
+ @Nullable
+ public final Integer systemNavigationBarDividerColor;
+
+ public SystemChromeStyle(
+ @Nullable Integer statusBarColor,
+ @Nullable Brightness statusBarIconBrightness,
+ @Nullable Integer systemNavigationBarColor,
+ @Nullable Brightness systemNavigationBarIconBrightness,
+ @Nullable Integer systemNavigationBarDividerColor
+ ) {
+ this.statusBarColor = statusBarColor;
+ this.statusBarIconBrightness = statusBarIconBrightness;
+ this.systemNavigationBarColor = systemNavigationBarColor;
+ this.systemNavigationBarIconBrightness = systemNavigationBarIconBrightness;
+ this.systemNavigationBarDividerColor = systemNavigationBarDividerColor;
+ }
+ }
+
+ public enum Brightness {
+ LIGHT("Brightness.light"),
+ DARK("Brightness.dark");
+
+ static Brightness fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (Brightness brightness : Brightness.values()) {
+ if (brightness.encodedName.equals(encodedName)) {
+ return brightness;
+ }
+ }
+ throw new NoSuchFieldException("No such Brightness: " + encodedName);
+ }
+
+ @NonNull
+ private String encodedName;
+
+ Brightness(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * Data formats of clipboard content.
+ */
+ public enum ClipboardContentFormat {
+ PLAIN_TEXT("text/plain");
+
+ static ClipboardContentFormat fromValue(String encodedName) throws NoSuchFieldException {
+ for (ClipboardContentFormat format : ClipboardContentFormat.values()) {
+ if (format.encodedName.equals(encodedName)) {
+ return format;
+ }
+ }
+ throw new NoSuchFieldException("No such ClipboardContentFormat: " + encodedName);
+ }
+
+ @NonNull
+ private String encodedName;
+
+ ClipboardContentFormat(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
}
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc4b75521549d425aee2076e461ff3818a08473d
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
@@ -0,0 +1,406 @@
+package io.flutter.embedding.engine.systemchannels;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.inputmethod.EditorInfo;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+/**
+ * {@link TextInputChannel} is a platform channel between Android and Flutter that is used to
+ * communicate information about the user's text input.
+ *
+ * When the user presses an action button like "done" or "next", that action is sent from Android
+ * to Flutter through this {@link TextInputChannel}.
+ *
+ * When an input system in the Flutter app wants to show the keyboard, or hide it, or configure
+ * editing state, etc. a message is sent from Flutter to Android through this {@link TextInputChannel}.
+ *
+ * {@link TextInputChannel} comes with a default {@link io.flutter.plugin.common.MethodChannel.MethodCallHandler}
+ * that parses incoming messages from Flutter. Register a {@link TextInputMethodHandler} to respond
+ * to standard Flutter text input messages.
+ */
+public class TextInputChannel {
+ @NonNull
+ public final MethodChannel channel;
+ @Nullable
+ private TextInputMethodHandler textInputMethodHandler;
+
+ private final MethodChannel.MethodCallHandler parsingMethodHandler = new MethodChannel.MethodCallHandler() {
+ @Override
+ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
+ if (textInputMethodHandler == null) {
+ // If no explicit TextInputMethodHandler has been registered then we don't
+ // need to forward this call to an API. Return.
+ return;
+ }
+
+ String method = call.method;
+ Object args = call.arguments;
+ switch (method) {
+ case "TextInput.show":
+ textInputMethodHandler.show();
+ result.success(null);
+ break;
+ case "TextInput.hide":
+ textInputMethodHandler.hide();
+ result.success(null);
+ break;
+ case "TextInput.setClient":
+ try {
+ final JSONArray argumentList = (JSONArray) args;
+ final int textInputClientId = argumentList.getInt(0);
+ final JSONObject jsonConfiguration = argumentList.getJSONObject(1);
+ textInputMethodHandler.setClient(textInputClientId, Configuration.fromJson(jsonConfiguration));
+ result.success(null);
+ } catch (JSONException | NoSuchFieldException exception) {
+ // JSONException: missing keys or bad value types.
+ // NoSuchFieldException: one or more values were invalid.
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "TextInput.setEditingState":
+ try {
+ final JSONObject editingState = (JSONObject) args;
+ textInputMethodHandler.setEditingState(TextEditState.fromJson(editingState));
+ result.success(null);
+ } catch (JSONException exception) {
+ result.error("error", exception.getMessage(), null);
+ }
+ break;
+ case "TextInput.clearClient":
+ textInputMethodHandler.clearClient();
+ result.success(null);
+ break;
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Constructs a {@code TextInputChannel} that connects Android to the Dart code
+ * running in {@code dartExecutor}.
+ *
+ * The given {@code dartExecutor} is permitted to be idle or executing code.
+ *
+ * See {@link DartExecutor}.
+ */
+ public TextInputChannel(@NonNull DartExecutor dartExecutor) {
+ this.channel = new MethodChannel(dartExecutor, "flutter/textinput", JSONMethodCodec.INSTANCE);
+ channel.setMethodCallHandler(parsingMethodHandler);
+ }
+
+ /**
+ * Instructs Flutter to update its text input editing state to reflect the given configuration.
+ */
+ public void updateEditingState(int inputClientId, String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd) {
+ HashMap state = new HashMap<>();
+ state.put("text", text);
+ state.put("selectionBase", selectionStart);
+ state.put("selectionExtent", selectionEnd);
+ state.put("composingBase", composingStart);
+ state.put("composingExtent", composingEnd);
+
+ channel.invokeMethod(
+ "TextInputClient.updateEditingState",
+ Arrays.asList(inputClientId, state)
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "newline" action.
+ */
+ public void newline(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.newline")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "go" action.
+ */
+ public void go(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.go")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "search" action.
+ */
+ public void search(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.search")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "send" action.
+ */
+ public void send(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.send")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "done" action.
+ */
+ public void done(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.done")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "next" action.
+ */
+ public void next(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.next")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute a "previous" action.
+ */
+ public void previous(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.previous")
+ );
+ }
+
+ /**
+ * Instructs Flutter to execute an "unspecified" action.
+ */
+ public void unspecifiedAction(int inputClientId) {
+ channel.invokeMethod(
+ "TextInputClient.performAction",
+ Arrays.asList(inputClientId, "TextInputAction.unspecified")
+ );
+ }
+
+ /**
+ * Sets the {@link TextInputMethodHandler} which receives all events and requests
+ * that are parsed from the underlying platform channel.
+ */
+ public void setTextInputMethodHandler(@Nullable TextInputMethodHandler textInputMethodHandler) {
+ this.textInputMethodHandler = textInputMethodHandler;
+ }
+
+ public interface TextInputMethodHandler {
+ // TODO(mattcarroll): javadoc
+ void show();
+
+ // TODO(mattcarroll): javadoc
+ void hide();
+
+ // TODO(mattcarroll): javadoc
+ void setClient(int textInputClientId, @NonNull Configuration configuration);
+
+ // TODO(mattcarroll): javadoc
+ void setEditingState(@NonNull TextEditState editingState);
+
+ // TODO(mattcarroll): javadoc
+ void clearClient();
+ }
+
+ /**
+ * A text editing configuration.
+ */
+ public static class Configuration {
+ public static Configuration fromJson(@NonNull JSONObject json) throws JSONException, NoSuchFieldException {
+ final String inputActionName = json.getString("inputAction");
+ if (inputActionName == null) {
+ throw new JSONException("Configuration JSON missing 'inputAction' property.");
+ }
+
+ final Integer inputAction = inputActionFromTextInputAction(inputActionName);
+ return new Configuration(
+ json.optBoolean("obscureText"),
+ json.optBoolean("autocorrect", true),
+ TextCapitalization.fromValue(json.getString("textCapitalization")),
+ InputType.fromJson(json.getJSONObject("inputType")),
+ inputAction,
+ json.optString("actionLabel")
+ );
+ }
+
+ private static Integer inputActionFromTextInputAction(@NonNull String inputAction) {
+ switch (inputAction) {
+ case "TextInputAction.newline":
+ return EditorInfo.IME_ACTION_NONE;
+ case "TextInputAction.none":
+ return EditorInfo.IME_ACTION_NONE;
+ case "TextInputAction.unspecified":
+ return EditorInfo.IME_ACTION_UNSPECIFIED;
+ case "TextInputAction.done":
+ return EditorInfo.IME_ACTION_DONE;
+ case "TextInputAction.go":
+ return EditorInfo.IME_ACTION_GO;
+ case "TextInputAction.search":
+ return EditorInfo.IME_ACTION_SEARCH;
+ case "TextInputAction.send":
+ return EditorInfo.IME_ACTION_SEND;
+ case "TextInputAction.next":
+ return EditorInfo.IME_ACTION_NEXT;
+ case "TextInputAction.previous":
+ return EditorInfo.IME_ACTION_PREVIOUS;
+ default:
+ // Present default key if bad input type is given.
+ return EditorInfo.IME_ACTION_UNSPECIFIED;
+ }
+ }
+
+ public final boolean obscureText;
+ public final boolean autocorrect;
+ @NonNull
+ public final TextCapitalization textCapitalization;
+ @NonNull
+ public final InputType inputType;
+ @Nullable
+ public final Integer inputAction;
+ @Nullable
+ public final String actionLabel;
+
+ public Configuration(
+ boolean obscureText,
+ boolean autocorrect,
+ @NonNull TextCapitalization textCapitalization,
+ @NonNull InputType inputType,
+ @Nullable Integer inputAction,
+ @Nullable String actionLabel
+ ) {
+ this.obscureText = obscureText;
+ this.autocorrect = autocorrect;
+ this.textCapitalization = textCapitalization;
+ this.inputType = inputType;
+ this.inputAction = inputAction;
+ this.actionLabel = actionLabel;
+ }
+ }
+
+ /**
+ * A text input type.
+ *
+ * If the {@link #type} is {@link TextInputType#NUMBER}, this {@code InputType} also
+ * reports whether that number {@link #isSigned} and {@link #isDecimal}.
+ */
+ public static class InputType {
+ @NonNull
+ public static InputType fromJson(@NonNull JSONObject json) throws JSONException, NoSuchFieldException {
+ return new InputType(
+ TextInputType.fromValue(json.getString("name")),
+ json.optBoolean("signed", false),
+ json.optBoolean("decimal", false)
+ );
+ }
+
+ @NonNull
+ public final TextInputType type;
+ public final boolean isSigned;
+ public final boolean isDecimal;
+
+ public InputType(@NonNull TextInputType type, boolean isSigned, boolean isDecimal) {
+ this.type = type;
+ this.isSigned = isSigned;
+ this.isDecimal = isDecimal;
+ }
+ }
+
+ /**
+ * Types of text input.
+ */
+ public enum TextInputType {
+ DATETIME("TextInputType.datetime"),
+ NUMBER("TextInputType.number"),
+ PHONE("TextInputType.phone"),
+ MULTILINE("TextInputType.multiline"),
+ EMAIL_ADDRESS("TextInputType.emailAddress"),
+ URL("TextInputType.url");
+
+ static TextInputType fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (TextInputType textInputType : TextInputType.values()) {
+ if (textInputType.encodedName.equals(encodedName)) {
+ return textInputType;
+ }
+ }
+ throw new NoSuchFieldException("No such TextInputType: " + encodedName);
+ }
+
+ @NonNull
+ private final String encodedName;
+
+ TextInputType(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * Text capitalization schemes.
+ */
+ public enum TextCapitalization {
+ CHARACTERS("TextCapitalization.characters"),
+ WORDS("TextCapitalization.words"),
+ SENTENCES("TextCapitalization.sentences");
+
+ static TextCapitalization fromValue(@NonNull String encodedName) throws NoSuchFieldException {
+ for (TextCapitalization textCapitalization : TextCapitalization.values()) {
+ if (textCapitalization.encodedName.equals(encodedName)) {
+ return textCapitalization;
+ }
+ }
+ throw new NoSuchFieldException("No such TextCapitalization: " + encodedName);
+ }
+
+ @NonNull
+ private final String encodedName;
+
+ TextCapitalization(@NonNull String encodedName) {
+ this.encodedName = encodedName;
+ }
+ }
+
+ /**
+ * State of an on-going text editing session.
+ */
+ public static class TextEditState {
+ public static TextEditState fromJson(@NonNull JSONObject textEditState) throws JSONException {
+ return new TextEditState(
+ textEditState.getString("text"),
+ textEditState.getInt("selectionBase"),
+ textEditState.getInt("selectionExtent")
+ );
+ }
+
+ @NonNull
+ public final String text;
+ public final int selectionStart;
+ public final int selectionEnd;
+
+ public TextEditState(@NonNull String text, int selectionStart, int selectionEnd) {
+ this.text = text;
+ this.selectionStart = selectionStart;
+ this.selectionEnd = selectionEnd;
+ }
+ }
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
index df907be0b70a1eaf9cff981ce46944563accaaf8..b3a287820bfe936996f10536d1742e28dec42941 100644
--- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
+++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
@@ -11,17 +11,16 @@ import android.view.KeyEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
+
+import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.ErrorLogResult;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterView;
-import java.util.Arrays;
-import java.util.HashMap;
-
class InputConnectionAdaptor extends BaseInputConnection {
private final FlutterView mFlutterView;
private final int mClient;
- private final MethodChannel mFlutterChannel;
+ private final TextInputChannel textInputChannel;
private final Editable mEditable;
private int mBatchCount;
private InputMethodManager mImm;
@@ -29,12 +28,16 @@ class InputConnectionAdaptor extends BaseInputConnection {
private static final MethodChannel.Result logger =
new ErrorLogResult("FlutterTextInput");
- public InputConnectionAdaptor(FlutterView view, int client,
- MethodChannel flutterChannel, Editable editable) {
+ public InputConnectionAdaptor(
+ FlutterView view,
+ int client,
+ TextInputChannel textInputChannel,
+ Editable editable
+ ) {
super(view, true);
mFlutterView = view;
mClient = client;
- mFlutterChannel = flutterChannel;
+ this.textInputChannel = textInputChannel;
mEditable = editable;
mBatchCount = 0;
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -55,14 +58,14 @@ class InputConnectionAdaptor extends BaseInputConnection {
selectionStart, selectionEnd,
composingStart, composingEnd);
- HashMap state = new HashMap<>();
- state.put("text", mEditable.toString());
- state.put("selectionBase", selectionStart);
- state.put("selectionExtent", selectionEnd);
- state.put("composingBase", composingStart);
- state.put("composingExtent", composingEnd);
- mFlutterChannel.invokeMethod("TextInputClient.updateEditingState",
- Arrays.asList(mClient, state), logger);
+ textInputChannel.updateEditingState(
+ mClient,
+ mEditable.toString(),
+ selectionStart,
+ selectionEnd,
+ composingStart,
+ composingEnd
+ );
}
@Override
@@ -178,39 +181,30 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public boolean performEditorAction(int actionCode) {
switch (actionCode) {
- // TODO(mattcarroll): is newline an appropriate action for "none"?
case EditorInfo.IME_ACTION_NONE:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.newline"), logger);
+ textInputChannel.newline(mClient);
break;
case EditorInfo.IME_ACTION_UNSPECIFIED:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.unspecified"), logger);
+ textInputChannel.unspecifiedAction(mClient);
break;
case EditorInfo.IME_ACTION_GO:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.go"), logger);
+ textInputChannel.go(mClient);
break;
case EditorInfo.IME_ACTION_SEARCH:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.search"), logger);
+ textInputChannel.search(mClient);
break;
case EditorInfo.IME_ACTION_SEND:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.send"), logger);
+ textInputChannel.send(mClient);
break;
case EditorInfo.IME_ACTION_NEXT:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.next"), logger);
+ textInputChannel.next(mClient);
break;
case EditorInfo.IME_ACTION_PREVIOUS:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.previous"), logger);
+ textInputChannel.previous(mClient);
break;
default:
case EditorInfo.IME_ACTION_DONE:
- mFlutterChannel.invokeMethod("TextInputClient.performAction",
- Arrays.asList(mClient, "TextInputAction.done"), logger);
+ textInputChannel.done(mClient);
break;
}
return true;
diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index c59ee25148e2d6f8b33a8cbf714a655ef9bf1557..bc54b3ed045ac01262197a41df355841a283c104 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -5,6 +5,7 @@
package io.flutter.plugin.editing;
import android.content.Context;
+import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
@@ -12,84 +13,87 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
-import io.flutter.plugin.common.JSONMethodCodec;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
-import io.flutter.plugin.common.MethodChannel.Result;
+
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.view.FlutterView;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
/**
* Android implementation of the text input plugin.
*/
-public class TextInputPlugin implements MethodCallHandler {
+public class TextInputPlugin {
private final FlutterView mView;
private final InputMethodManager mImm;
- private final MethodChannel mFlutterChannel;
+ private final TextInputChannel textInputChannel;
private int mClient = 0;
- private JSONObject mConfiguration;
+ private TextInputChannel.Configuration configuration;
private Editable mEditable;
private boolean mRestartInputPending;
- public TextInputPlugin(FlutterView view) {
+ public TextInputPlugin(FlutterView view, @NonNull DartExecutor dartExecutor) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
- mFlutterChannel = new MethodChannel(view, "flutter/textinput", JSONMethodCodec.INSTANCE);
- mFlutterChannel.setMethodCallHandler(this);
- }
- @Override
- public void onMethodCall(MethodCall call, Result result) {
- String method = call.method;
- Object args = call.arguments;
- try {
- if (method.equals("TextInput.show")) {
+ textInputChannel = new TextInputChannel(dartExecutor);
+ textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() {
+ @Override
+ public void show() {
showTextInput(mView);
- result.success(null);
- } else if (method.equals("TextInput.hide")) {
+ }
+
+ @Override
+ public void hide() {
hideTextInput(mView);
- result.success(null);
- } else if (method.equals("TextInput.setClient")) {
- final JSONArray argumentList = (JSONArray) args;
- setTextInputClient(mView, argumentList.getInt(0), argumentList.getJSONObject(1));
- result.success(null);
- } else if (method.equals("TextInput.setEditingState")) {
- setTextInputEditingState(mView, (JSONObject) args);
- result.success(null);
- } else if (method.equals("TextInput.clearClient")) {
+ }
+
+ @Override
+ public void setClient(int textInputClientId, TextInputChannel.Configuration configuration) {
+ setTextInputClient(textInputClientId, configuration);
+ }
+
+ @Override
+ public void setEditingState(TextInputChannel.TextEditState editingState) {
+ setTextInputEditingState(mView, editingState);
+ }
+
+ @Override
+ public void clearClient() {
clearTextInputClient();
- result.success(null);
- } else {
- result.notImplemented();
}
- } catch (JSONException e) {
- result.error("error", "JSON error: " + e.getMessage(), null);
- }
+ });
}
- private static int inputTypeFromTextInputType(JSONObject type, boolean obscureText,
- boolean autocorrect, String textCapitalization) throws JSONException {
- String inputType = type.getString("name");
- if (inputType.equals("TextInputType.datetime")) return InputType.TYPE_CLASS_DATETIME;
- if (inputType.equals("TextInputType.number")) {
+ private static int inputTypeFromTextInputType(
+ TextInputChannel.InputType type,
+ boolean obscureText,
+ boolean autocorrect,
+ TextInputChannel.TextCapitalization textCapitalization
+ ) {
+ if (type.type == TextInputChannel.TextInputType.DATETIME) {
+ return InputType.TYPE_CLASS_DATETIME;
+ } else if (type.type == TextInputChannel.TextInputType.NUMBER) {
int textType = InputType.TYPE_CLASS_NUMBER;
- if (type.optBoolean("signed")) textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
- if (type.optBoolean("decimal")) textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ if (type.isSigned) {
+ textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+ }
+ if (type.isDecimal) {
+ textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ }
return textType;
+ } else if (type.type == TextInputChannel.TextInputType.PHONE) {
+ return InputType.TYPE_CLASS_PHONE;
}
- if (inputType.equals("TextInputType.phone")) return InputType.TYPE_CLASS_PHONE;
int textType = InputType.TYPE_CLASS_TEXT;
- if (inputType.equals("TextInputType.multiline"))
+ if (type.type == TextInputChannel.TextInputType.MULTILINE) {
textType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
- else if (inputType.equals("TextInputType.emailAddress"))
+ } else if (type.type == TextInputChannel.TextInputType.EMAIL_ADDRESS) {
textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
- else if (inputType.equals("TextInputType.url"))
+ } else if (type.type == TextInputChannel.TextInputType.URL) {
textType |= InputType.TYPE_TEXT_VARIATION_URI;
+ }
+
if (obscureText) {
// Note: both required. Some devices ignore TYPE_TEXT_FLAG_NO_SUGGESTIONS.
textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
@@ -97,69 +101,50 @@ public class TextInputPlugin implements MethodCallHandler {
} else {
if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
}
- if (textCapitalization.equals("TextCapitalization.characters")) {
+
+ if (textCapitalization == TextInputChannel.TextCapitalization.CHARACTERS) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
- } else if (textCapitalization.equals("TextCapitalization.words")) {
+ } else if (textCapitalization == TextInputChannel.TextCapitalization.WORDS) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
- } else if (textCapitalization.equals("TextCapitalization.sentences")) {
+ } else if (textCapitalization == TextInputChannel.TextCapitalization.SENTENCES) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
}
- return textType;
- }
- private static int inputActionFromTextInputAction(String inputAction) {
- switch (inputAction) {
- case "TextInputAction.newline":
- return EditorInfo.IME_ACTION_NONE;
- case "TextInputAction.none":
- return EditorInfo.IME_ACTION_NONE;
- case "TextInputAction.unspecified":
- return EditorInfo.IME_ACTION_UNSPECIFIED;
- case "TextInputAction.done":
- return EditorInfo.IME_ACTION_DONE;
- case "TextInputAction.go":
- return EditorInfo.IME_ACTION_GO;
- case "TextInputAction.search":
- return EditorInfo.IME_ACTION_SEARCH;
- case "TextInputAction.send":
- return EditorInfo.IME_ACTION_SEND;
- case "TextInputAction.next":
- return EditorInfo.IME_ACTION_NEXT;
- case "TextInputAction.previous":
- return EditorInfo.IME_ACTION_PREVIOUS;
- default:
- // Present default key if bad input type is given.
- return EditorInfo.IME_ACTION_UNSPECIFIED;
- }
+ return textType;
}
- public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs)
- throws JSONException {
+ public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs) {
if (mClient == 0) return null;
- outAttrs.inputType = inputTypeFromTextInputType(mConfiguration.getJSONObject("inputType"),
- mConfiguration.optBoolean("obscureText"),
- mConfiguration.optBoolean("autocorrect", true),
- mConfiguration.getString("textCapitalization"));
+ outAttrs.inputType = inputTypeFromTextInputType(
+ configuration.inputType,
+ configuration.obscureText,
+ configuration.autocorrect,
+ configuration.textCapitalization
+ );
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
int enterAction;
- if (mConfiguration.isNull("inputAction")) {
+ if (configuration.inputAction == null) {
// If an explicit input action isn't set, then default to none for multi-line fields
// and done for single line fields.
enterAction = (InputType.TYPE_TEXT_FLAG_MULTI_LINE & outAttrs.inputType) != 0
? EditorInfo.IME_ACTION_NONE
: EditorInfo.IME_ACTION_DONE;
} else {
- enterAction = inputActionFromTextInputAction(mConfiguration.getString("inputAction"));
+ enterAction = configuration.inputAction;
}
- if (!mConfiguration.isNull("actionLabel")) {
- outAttrs.actionLabel = mConfiguration.getString("actionLabel");
+ if (configuration.actionLabel != null) {
+ outAttrs.actionLabel = configuration.actionLabel;
outAttrs.actionId = enterAction;
}
outAttrs.imeOptions |= enterAction;
- InputConnectionAdaptor connection =
- new InputConnectionAdaptor(view, mClient, mFlutterChannel, mEditable);
+ InputConnectionAdaptor connection = new InputConnectionAdaptor(
+ view,
+ mClient,
+ textInputChannel,
+ mEditable
+ );
outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
@@ -175,9 +160,9 @@ public class TextInputPlugin implements MethodCallHandler {
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
}
- private void setTextInputClient(FlutterView view, int client, JSONObject configuration) {
+ private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
mClient = client;
- mConfiguration = configuration;
+ this.configuration = configuration;
mEditable = Editable.Factory.getInstance().newEditable("");
// setTextInputClient will be followed by a call to setTextInputEditingState.
@@ -185,9 +170,9 @@ public class TextInputPlugin implements MethodCallHandler {
mRestartInputPending = true;
}
- private void applyStateToSelection(JSONObject state) throws JSONException {
- int selStart = state.getInt("selectionBase");
- int selEnd = state.getInt("selectionExtent");
+ private void applyStateToSelection(TextInputChannel.TextEditState state) {
+ int selStart = state.selectionStart;
+ int selEnd = state.selectionEnd;
if (selStart >= 0 && selStart <= mEditable.length() && selEnd >= 0
&& selEnd <= mEditable.length()) {
Selection.setSelection(mEditable, selStart, selEnd);
@@ -196,15 +181,15 @@ public class TextInputPlugin implements MethodCallHandler {
}
}
- private void setTextInputEditingState(FlutterView view, JSONObject state) throws JSONException {
- if (!mRestartInputPending && state.getString("text").equals(mEditable.toString())) {
+ private void setTextInputEditingState(FlutterView view, TextInputChannel.TextEditState state) {
+ if (!mRestartInputPending && state.text.equals(mEditable.toString())) {
applyStateToSelection(state);
mImm.updateSelection(mView, Math.max(Selection.getSelectionStart(mEditable), 0),
Math.max(Selection.getSelectionEnd(mEditable), 0),
BaseInputConnection.getComposingSpanStart(mEditable),
BaseInputConnection.getComposingSpanEnd(mEditable));
} else {
- mEditable.replace(0, mEditable.length(), state.getString("text"));
+ mEditable.replace(0, mEditable.length(), state.text);
applyStateToSelection(state);
mImm.restartInput(view);
mRestartInputPending = false;
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
index b6f53d45741f158e7a5ef5b660645c87c6e1d24c..e9d44001ef0f5be122d1cb38b125da4df946e5ab 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
@@ -9,212 +9,159 @@ import android.app.ActivityManager.TaskDescription;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
-import android.content.pm.ActivityInfo;
import android.os.Build;
-import android.util.Log;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.view.HapticFeedbackConstants;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.Window;
+
+import java.util.List;
+
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.ActivityLifecycleListener;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
-import io.flutter.plugin.common.MethodChannel.Result;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
/**
* Android implementation of the platform plugin.
*/
-public class PlatformPlugin implements MethodCallHandler, ActivityLifecycleListener {
- private final Activity mActivity;
- private JSONObject mCurrentTheme;
+public class PlatformPlugin implements ActivityLifecycleListener {
public static final int DEFAULT_SYSTEM_UI = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
- private static final String kTextPlainFormat = "text/plain";
- public PlatformPlugin(Activity activity) {
- mActivity = activity;
- mEnabledOverlays = DEFAULT_SYSTEM_UI;
- }
+ private final Activity activity;
+ private final PlatformChannel platformChannel;
+ private PlatformChannel.SystemChromeStyle currentTheme;
+ private int mEnabledOverlays;
- @Override
- public void onMethodCall(MethodCall call, Result result) {
- String method = call.method;
- Object arguments = call.arguments;
- try {
- if (method.equals("SystemSound.play")) {
- playSystemSound((String) arguments);
- result.success(null);
- } else if (method.equals("HapticFeedback.vibrate")) {
- vibrateHapticFeedback((String) arguments);
- result.success(null);
- } else if (method.equals("SystemChrome.setPreferredOrientations")) {
- setSystemChromePreferredOrientations((JSONArray) arguments);
- result.success(null);
- } else if (method.equals("SystemChrome.setApplicationSwitcherDescription")) {
- setSystemChromeApplicationSwitcherDescription((JSONObject) arguments);
- result.success(null);
- } else if (method.equals("SystemChrome.setEnabledSystemUIOverlays")) {
- setSystemChromeEnabledSystemUIOverlays((JSONArray) arguments);
- result.success(null);
- } else if (method.equals("SystemChrome.restoreSystemUIOverlays")) {
- restoreSystemChromeSystemUIOverlays();
- result.success(null);
- } else if (method.equals("SystemChrome.setSystemUIOverlayStyle")) {
- setSystemChromeSystemUIOverlayStyle((JSONObject) arguments);
- result.success(null);
- } else if (method.equals("SystemNavigator.pop")) {
- popSystemNavigator();
- result.success(null);
- } else if (method.equals("Clipboard.getData")) {
- result.success(getClipboardData((String) arguments));
- } else if (method.equals("Clipboard.setData")) {
- setClipboardData((JSONObject) arguments);
- result.success(null);
- } else {
- result.notImplemented();
- }
- } catch (JSONException e) {
- result.error("error", "JSON error: " + e.getMessage(), null);
+ private final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() {
+ @Override
+ public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) {
+ PlatformPlugin.this.playSystemSound(soundType);
}
- }
- private void playSystemSound(String soundType) {
- if (soundType.equals("SystemSoundType.click")) {
- View view = mActivity.getWindow().getDecorView();
- view.playSoundEffect(SoundEffectConstants.CLICK);
+ @Override
+ public void vibrateHapticFeedback(@NonNull PlatformChannel.HapticFeedbackType feedbackType) {
+ PlatformPlugin.this.vibrateHapticFeedback(feedbackType);
+ }
+
+ @Override
+ public void setPreferredOrientations(int androidOrientation) {
+ setSystemChromePreferredOrientations(androidOrientation);
+ }
+
+ @Override
+ public void setApplicationSwitcherDescription(@NonNull PlatformChannel.AppSwitcherDescription description) {
+ setSystemChromeApplicationSwitcherDescription(description);
+ }
+
+ @Override
+ public void showSystemOverlays(@NonNull List overlays) {
+ setSystemChromeEnabledSystemUIOverlays(overlays);
+ }
+
+ @Override
+ public void restoreSystemUiOverlays() {
+ restoreSystemChromeSystemUIOverlays();
+ }
+
+ @Override
+ public void setSystemUiOverlayStyle(@NonNull PlatformChannel.SystemChromeStyle systemUiOverlayStyle) {
+ setSystemChromeSystemUIOverlayStyle(systemUiOverlayStyle);
+ }
+
+ @Override
+ public void popSystemNavigator() {
+ PlatformPlugin.this.popSystemNavigator();
}
- }
- private void vibrateHapticFeedback(String feedbackType) {
- View view = mActivity.getWindow().getDecorView();
- if (feedbackType == null) {
- view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- } else if (feedbackType.equals("HapticFeedbackType.lightImpact")) {
- view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
- } else if (feedbackType.equals("HapticFeedbackType.mediumImpact")) {
- view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
- } else if (feedbackType.equals("HapticFeedbackType.heavyImpact")) {
- // HapticFeedbackConstants.CONTEXT_CLICK from API level 23.
- view.performHapticFeedback(6);
- } else if (feedbackType.equals("HapticFeedbackType.selectionClick")) {
- view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+ @Override
+ public CharSequence getClipboardData(@Nullable PlatformChannel.ClipboardContentFormat format) {
+ return PlatformPlugin.this.getClipboardData(format);
}
+
+ @Override
+ public void setClipboardData(@NonNull String text) {
+ PlatformPlugin.this.setClipboardData(text);
+ }
+ };
+
+ public PlatformPlugin(Activity activity, PlatformChannel platformChannel) {
+ this.activity = activity;
+ this.platformChannel = platformChannel;
+ this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler);
+
+ mEnabledOverlays = DEFAULT_SYSTEM_UI;
}
- private void setSystemChromePreferredOrientations(JSONArray orientations) throws JSONException {
- int requestedOrientation = 0x00;
- int firstRequestedOrientation = 0x00;
- for (int index = 0; index < orientations.length(); index += 1) {
- if (orientations.getString(index).equals("DeviceOrientation.portraitUp")) {
- requestedOrientation |= 0x01;
- } else if (orientations.getString(index).equals("DeviceOrientation.landscapeLeft")) {
- requestedOrientation |= 0x02;
- } else if (orientations.getString(index).equals("DeviceOrientation.portraitDown")) {
- requestedOrientation |= 0x04;
- } else if (orientations.getString(index).equals("DeviceOrientation.landscapeRight")) {
- requestedOrientation |= 0x08;
- }
- if (firstRequestedOrientation == 0x00) {
- firstRequestedOrientation = requestedOrientation;
- }
+ private void playSystemSound(PlatformChannel.SoundType soundType) {
+ if (soundType == PlatformChannel.SoundType.CLICK) {
+ View view = activity.getWindow().getDecorView();
+ view.playSoundEffect(SoundEffectConstants.CLICK);
}
- switch (requestedOrientation) {
- case 0x00:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- break;
- case 0x01:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- break;
- case 0x02:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- break;
- case 0x04:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
- break;
- case 0x05:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
+ }
+
+ private void vibrateHapticFeedback(PlatformChannel.HapticFeedbackType feedbackType) {
+ View view = activity.getWindow().getDecorView();
+ switch (feedbackType) {
+ case STANDARD:
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
break;
- case 0x08:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ case LIGHT_IMPACT:
+ view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
break;
- case 0x0a:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
+ case MEDIUM_IMPACT:
+ view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
break;
- case 0x0b:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
+ case HEAVY_IMPACT:
+ // HapticFeedbackConstants.CONTEXT_CLICK from API level 23.
+ view.performHapticFeedback(6);
break;
- case 0x0f:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
+ case SELECTION_CLICK:
+ view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
break;
- case 0x03: // portraitUp and landscapeLeft
- case 0x06: // portraitDown and landscapeLeft
- case 0x07: // portraitUp, portraitDown, and landscapeLeft
- case 0x09: // portraitUp and landscapeRight
- case 0x0c: // portraitDown and landscapeRight
- case 0x0d: // portraitUp, portraitDown, and landscapeRight
- case 0x0e: // portraitDown, landscapeLeft, and landscapeRight
- // Android can't describe these cases, so just default to whatever the first
- // specified value was.
- switch (firstRequestedOrientation) {
- case 0x01:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- break;
- case 0x02:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- break;
- case 0x04:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
- break;
- case 0x08:
- mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
- break;
- }
- break;
- }
+ }
+ }
+
+ private void setSystemChromePreferredOrientations(int androidOrientation) {
+ activity.setRequestedOrientation(androidOrientation);
}
- private void setSystemChromeApplicationSwitcherDescription(JSONObject description) throws JSONException {
+ private void setSystemChromeApplicationSwitcherDescription(PlatformChannel.AppSwitcherDescription description) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
- int color = description.getInt("primaryColor");
- if (color != 0) { // 0 means color isn't set, use system default
- color = color | 0xFF000000; // color must be opaque if set
- }
-
- String label = description.getString("label");
-
@SuppressWarnings("deprecation")
TaskDescription taskDescription = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- ? new TaskDescription(label, 0, color)
- : new TaskDescription(label, null, color);
+ ? new TaskDescription(description.label, 0, description.color)
+ : new TaskDescription(description.label, null, description.color);
- mActivity.setTaskDescription(taskDescription);
+ activity.setTaskDescription(taskDescription);
}
- private int mEnabledOverlays;
-
- private void setSystemChromeEnabledSystemUIOverlays(JSONArray overlays) throws JSONException {
+ private void setSystemChromeEnabledSystemUIOverlays(List overlaysToShow) {
+ // Start by assuming we want to hide all system overlays (like an immersive game).
int enabledOverlays = DEFAULT_SYSTEM_UI
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
- if (overlays.length() == 0) {
+ if (overlaysToShow.size() == 0) {
enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
- for (int i = 0; i < overlays.length(); ++i) {
- String overlay = overlays.getString(i);
- if (overlay.equals("SystemUiOverlay.top")) {
- enabledOverlays &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
- } else if (overlay.equals("SystemUiOverlay.bottom")) {
- enabledOverlays &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
- enabledOverlays &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ // Re-add any desired system overlays.
+ for (int i = 0; i < overlaysToShow.size(); ++i) {
+ PlatformChannel.SystemUiOverlay overlayToShow = overlaysToShow.get(i);
+ switch (overlayToShow) {
+ case TOP_OVERLAYS:
+ enabledOverlays &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
+ break;
+ case BOTTOM_OVERLAYS:
+ enabledOverlays &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+ enabledOverlays &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ break;
}
}
@@ -223,9 +170,9 @@ public class PlatformPlugin implements MethodCallHandler, ActivityLifecycleListe
}
private void updateSystemUiOverlays(){
- mActivity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays);
- if (mCurrentTheme != null) {
- setSystemChromeSystemUIOverlayStyle(mCurrentTheme);
+ activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays);
+ if (currentTheme != null) {
+ setSystemChromeSystemUIOverlayStyle(currentTheme);
}
}
@@ -233,83 +180,75 @@ public class PlatformPlugin implements MethodCallHandler, ActivityLifecycleListe
updateSystemUiOverlays();
}
- private void setSystemChromeSystemUIOverlayStyle(JSONObject message) {
- Window window = mActivity.getWindow();
+ private void setSystemChromeSystemUIOverlayStyle(PlatformChannel.SystemChromeStyle systemChromeStyle) {
+ Window window = activity.getWindow();
View view = window.getDecorView();
int flags = view.getSystemUiVisibility();
- try {
- // You can change the navigation bar color (including translucent colors)
- // in Android, but you can't change the color of the navigation buttons until Android O.
- // LIGHT vs DARK effectively isn't supported until then.
- // Build.VERSION_CODES.O
- if (Build.VERSION.SDK_INT >= 26) {
- if (!message.isNull("systemNavigationBarIconBrightness")) {
- String systemNavigationBarIconBrightness = message.getString("systemNavigationBarIconBrightness");
- switch (systemNavigationBarIconBrightness) {
- case "Brightness.dark":
- //View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
- flags |= 0x10;
- break;
- case "Brightness.light":
- flags &= ~0x10;
- break;
- }
- }
- if (!message.isNull("systemNavigationBarColor")) {
- window.setNavigationBarColor(message.getInt("systemNavigationBarColor"));
+ // You can change the navigation bar color (including translucent colors)
+ // in Android, but you can't change the color of the navigation buttons until Android O.
+ // LIGHT vs DARK effectively isn't supported until then.
+ // Build.VERSION_CODES.O
+ if (Build.VERSION.SDK_INT >= 26) {
+ if (systemChromeStyle.systemNavigationBarIconBrightness != null) {
+ switch (systemChromeStyle.systemNavigationBarIconBrightness) {
+ case DARK:
+ //View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ flags |= 0x10;
+ break;
+ case LIGHT:
+ flags &= ~0x10;
+ break;
}
}
- // Build.VERSION_CODES.M
- if (Build.VERSION.SDK_INT >= 23) {
- if (!message.isNull("statusBarIconBrightness")) {
- String statusBarIconBrightness = message.getString("statusBarIconBrightness");
- switch (statusBarIconBrightness) {
- case "Brightness.dark":
- // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
- flags |= 0x2000;
- break;
- case "Brightness.light":
- flags &= ~0x2000;
- break;
- }
- }
- if (!message.isNull("statusBarColor")) {
- window.setStatusBarColor(message.getInt("statusBarColor"));
+ if (systemChromeStyle.systemNavigationBarColor != null) {
+ window.setNavigationBarColor(systemChromeStyle.systemNavigationBarColor);
+ }
+ }
+ // Build.VERSION_CODES.M
+ if (Build.VERSION.SDK_INT >= 23) {
+ if (systemChromeStyle.statusBarIconBrightness != null) {
+ switch (systemChromeStyle.statusBarIconBrightness) {
+ case DARK:
+ // View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ flags |= 0x2000;
+ break;
+ case LIGHT:
+ flags &= ~0x2000;
+ break;
}
}
- if (!message.isNull("systemNavigationBarDividerColor")) {
- // Not availible until Android P.
- // window.setNavigationBarDividerColor(systemNavigationBarDividerColor);
+ if (systemChromeStyle.statusBarColor != null) {
+ window.setStatusBarColor(systemChromeStyle.statusBarColor);
}
- view.setSystemUiVisibility(flags);
- mCurrentTheme = message;
- } catch (JSONException err) {
- Log.i("PlatformPlugin", err.toString());
}
+ if (systemChromeStyle.systemNavigationBarDividerColor != null) {
+ // Not availible until Android P.
+ // window.setNavigationBarDividerColor(systemNavigationBarDividerColor);
+ }
+ view.setSystemUiVisibility(flags);
+ currentTheme = systemChromeStyle;
}
private void popSystemNavigator() {
- mActivity.finish();
+ activity.finish();
}
- private JSONObject getClipboardData(String format) throws JSONException {
- ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
+ private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) {
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboard.getPrimaryClip();
if (clip == null)
return null;
- if (format == null || format.equals(kTextPlainFormat)) {
- JSONObject result = new JSONObject();
- result.put("text", clip.getItemAt(0).coerceToText(mActivity));
- return result;
+ if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) {
+ return clip.getItemAt(0).coerceToText(activity);
}
return null;
}
- private void setClipboardData(JSONObject data) throws JSONException {
- ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("text label?", data.getString("text"));
+ private void setClipboardData(String text) {
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("text label?", text);
clipboard.setPrimaryClip(clip);
}
diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
index d8c2de374919534533f4bcfecb39fa470f4d4dcc..ac5e2f8f3d9952b76797d7426fc012a49143475f 100644
--- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java
+++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
@@ -9,20 +9,21 @@ import android.graphics.Rect;
import android.opengl.Matrix;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
-import io.flutter.plugin.common.BasicMessageChannel;
-import io.flutter.plugin.common.StandardMessageCodec;
+
+import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.util.Predicate;
import java.nio.ByteBuffer;
import java.util.*;
class AccessibilityBridge
- extends AccessibilityNodeProvider implements BasicMessageChannel.MessageHandler {
+ extends AccessibilityNodeProvider {
private static final String TAG = "FlutterView";
// Constants from higher API levels.
@@ -34,19 +35,42 @@ class AccessibilityBridge
private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f;
private static final int ROOT_NODE_ID = 0;
- private Map mObjects;
- private Map mCustomAccessibilityActions;
- private final FlutterView mOwner;
- private boolean mAccessibilityEnabled = false;
- private SemanticsObject mA11yFocusedObject;
- private SemanticsObject mInputFocusedObject;
- private SemanticsObject mHoveredObject;
+ private final FlutterView owner;
+ private final AccessibilityChannel accessibilityChannel;
+ private final View decorView;
+ private Map objects;
+ private Map customAccessibilityActions;
+ private boolean accessibilityEnabled = false;
+ private SemanticsObject a11yFocusedObject;
+ private SemanticsObject inputFocusedObject;
+ private SemanticsObject hoveredObject;
private int previousRouteId = ROOT_NODE_ID;
private List previousRoutes;
- private final View mDecorView;
- private Integer mLastLeftFrameInset = 0;
+ private Integer lastLeftFrameInset = 0;
+
+ private final AccessibilityChannel.AccessibilityMessageHandler accessibilityMessageHandler = new AccessibilityChannel.AccessibilityMessageHandler() {
+ @Override
+ public void announce(@NonNull String message) {
+ owner.announceForAccessibility(message);
+ }
+
+ @Override
+ public void onTap(int nodeId) {
+ sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED);
+ }
- private final BasicMessageChannel mFlutterAccessibilityChannel;
+ @Override
+ public void onLongPress(int nodeId) {
+ sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+ }
+
+ @Override
+ public void onTooltip(@NonNull String message) {
+ AccessibilityEvent e = obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ e.getText().add(message);
+ sendAccessibilityEvent(e);
+ }
+ };
enum Action {
TAP(1 << 0),
@@ -106,23 +130,21 @@ class AccessibilityBridge
final int value;
}
- AccessibilityBridge(FlutterView owner) {
- assert owner != null;
- mOwner = owner;
- mObjects = new HashMap<>();
- mCustomAccessibilityActions = new HashMap<>();
+ AccessibilityBridge(@NonNull FlutterView owner, @NonNull AccessibilityChannel accessibilityChannel) {
+ this.owner = owner;
+ this.accessibilityChannel = accessibilityChannel;
+ decorView = ((Activity) owner.getContext()).getWindow().getDecorView();
+ objects = new HashMap<>();
+ customAccessibilityActions = new HashMap<>();
previousRoutes = new ArrayList<>();
- mFlutterAccessibilityChannel = new BasicMessageChannel<>(
- owner, "flutter/accessibility", StandardMessageCodec.INSTANCE);
- mDecorView = ((Activity) owner.getContext()).getWindow().getDecorView();
}
void setAccessibilityEnabled(boolean accessibilityEnabled) {
- mAccessibilityEnabled = accessibilityEnabled;
+ this.accessibilityEnabled = accessibilityEnabled;
if (accessibilityEnabled) {
- mFlutterAccessibilityChannel.setMessageHandler(this);
+ this.accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
} else {
- mFlutterAccessibilityChannel.setMessageHandler(null);
+ this.accessibilityChannel.setAccessibilityMessageHandler(null);
}
}
@@ -137,42 +159,42 @@ class AccessibilityBridge
// to set it if we're exiting a list to a non-list, so that we can get the "out of list"
// announcement when A11y focus moves out of a list and not into another list.
return object.scrollChildren > 0
- && (hasSemanticsObjectAncestor(mA11yFocusedObject, o -> o == object)
- || !hasSemanticsObjectAncestor(mA11yFocusedObject, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
+ && (hasSemanticsObjectAncestor(a11yFocusedObject, o -> o == object)
+ || !hasSemanticsObjectAncestor(a11yFocusedObject, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
}
@Override
@SuppressWarnings("deprecation")
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
if (virtualViewId == View.NO_ID) {
- AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner);
- mOwner.onInitializeAccessibilityNodeInfo(result);
- if (mObjects.containsKey(ROOT_NODE_ID)) {
- result.addChild(mOwner, ROOT_NODE_ID);
+ AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner);
+ owner.onInitializeAccessibilityNodeInfo(result);
+ if (objects.containsKey(ROOT_NODE_ID)) {
+ result.addChild(owner, ROOT_NODE_ID);
}
return result;
}
- SemanticsObject object = mObjects.get(virtualViewId);
+ SemanticsObject object = objects.get(virtualViewId);
if (object == null) {
return null;
}
- AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner, virtualViewId);
+ AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner, virtualViewId);
// Work around for https://github.com/flutter/flutter/issues/2101
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result.setViewIdResourceName("");
}
- result.setPackageName(mOwner.getContext().getPackageName());
+ result.setPackageName(owner.getContext().getPackageName());
result.setClassName("android.view.View");
- result.setSource(mOwner, virtualViewId);
+ result.setSource(owner, virtualViewId);
result.setFocusable(object.isFocusable());
- if (mInputFocusedObject != null) {
- result.setFocused(mInputFocusedObject.id == virtualViewId);
+ if (inputFocusedObject != null) {
+ result.setFocused(inputFocusedObject.id == virtualViewId);
}
- if (mA11yFocusedObject != null) {
- result.setAccessibilityFocused(mA11yFocusedObject.id == virtualViewId);
+ if (a11yFocusedObject != null) {
+ result.setAccessibilityFocused(a11yFocusedObject.id == virtualViewId);
}
if (object.hasFlag(Flag.IS_TEXT_FIELD)) {
@@ -186,7 +208,7 @@ class AccessibilityBridge
// Text fields will always be created as a live region when they have input focus,
// so that updates to the label trigger polite announcements. This makes it easy to
// follow a11y guidelines for text fields on Android.
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && mA11yFocusedObject != null && mA11yFocusedObject.id == virtualViewId) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) {
result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
}
}
@@ -239,10 +261,10 @@ class AccessibilityBridge
if (object.parent != null) {
assert object.id > ROOT_NODE_ID;
- result.setParent(mOwner, object.parent.id);
+ result.setParent(owner, object.parent.id);
} else {
assert object.id == ROOT_NODE_ID;
- result.setParent(mOwner);
+ result.setParent(owner);
}
Rect bounds = object.getGlobalRect();
@@ -362,7 +384,7 @@ class AccessibilityBridge
result.setSelected(object.hasFlag(Flag.IS_SELECTED));
// Accessibility Focus
- if (mA11yFocusedObject != null && mA11yFocusedObject.id == virtualViewId) {
+ if (a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) {
result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
} else {
result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
@@ -381,7 +403,7 @@ class AccessibilityBridge
if (object.childrenInTraversalOrder != null) {
for (SemanticsObject child : object.childrenInTraversalOrder) {
if (!child.hasFlag(Flag.IS_HIDDEN)) {
- result.addChild(mOwner, child.id);
+ result.addChild(owner, child.id);
}
}
}
@@ -391,7 +413,7 @@ class AccessibilityBridge
@Override
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
- SemanticsObject object = mObjects.get(virtualViewId);
+ SemanticsObject object = objects.get(virtualViewId);
if (object == null) {
return false;
}
@@ -400,27 +422,27 @@ class AccessibilityBridge
// Note: TalkBack prior to Oreo doesn't use this handler and instead simulates a
// click event at the center of the SemanticsNode. Other a11y services might go
// through this handler though.
- mOwner.dispatchSemanticsAction(virtualViewId, Action.TAP);
+ owner.dispatchSemanticsAction(virtualViewId, Action.TAP);
return true;
}
case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
// Note: TalkBack doesn't use this handler and instead simulates a long click event
// at the center of the SemanticsNode. Other a11y services might go through this
// handler though.
- mOwner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
+ owner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
return true;
}
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
if (object.hasAction(Action.SCROLL_UP)) {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
} else if (object.hasAction(Action.SCROLL_LEFT)) {
// TODO(ianh): bidi support using textDirection
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
} else if (object.hasAction(Action.INCREASE)) {
object.value = object.increasedValue;
// Event causes Android to read out the updated value.
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
- mOwner.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
+ owner.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
} else {
return false;
}
@@ -428,15 +450,15 @@ class AccessibilityBridge
}
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
if (object.hasAction(Action.SCROLL_DOWN)) {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
} else if (object.hasAction(Action.SCROLL_RIGHT)) {
// TODO(ianh): bidi support using textDirection
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
} else if (object.hasAction(Action.DECREASE)) {
object.value = object.decreasedValue;
// Event causes Android to read out the updated value.
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
- mOwner.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
+ owner.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
} else {
return false;
}
@@ -449,24 +471,24 @@ class AccessibilityBridge
return performCursorMoveAction(object, virtualViewId, arguments, true);
}
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
+ owner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- mA11yFocusedObject = null;
+ a11yFocusedObject = null;
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
+ owner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
- if (mA11yFocusedObject == null) {
+ if (a11yFocusedObject == null) {
// When Android focuses a node, it doesn't invalidate the view.
// (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
// we only have to worry about this when the focused node is null.)
- mOwner.invalidate();
+ owner.invalidate();
}
- mA11yFocusedObject = object;
+ a11yFocusedObject = object;
if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) {
// SeekBars only announce themselves after this event.
@@ -476,7 +498,7 @@ class AccessibilityBridge
return true;
}
case ACTION_SHOW_ON_SCREEN: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
return true;
}
case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
@@ -498,32 +520,32 @@ class AccessibilityBridge
selection.put("base", object.textSelectionExtent);
selection.put("extent", object.textSelectionExtent);
}
- mOwner.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection);
+ owner.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection);
return true;
}
case AccessibilityNodeInfo.ACTION_COPY: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.COPY);
+ owner.dispatchSemanticsAction(virtualViewId, Action.COPY);
return true;
}
case AccessibilityNodeInfo.ACTION_CUT: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.CUT);
+ owner.dispatchSemanticsAction(virtualViewId, Action.CUT);
return true;
}
case AccessibilityNodeInfo.ACTION_PASTE: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.PASTE);
+ owner.dispatchSemanticsAction(virtualViewId, Action.PASTE);
return true;
}
case AccessibilityNodeInfo.ACTION_DISMISS: {
- mOwner.dispatchSemanticsAction(virtualViewId, Action.DISMISS);
+ owner.dispatchSemanticsAction(virtualViewId, Action.DISMISS);
return true;
}
default:
// might be a custom accessibility action.
final int flutterId = action - firstResourceId;
CustomAccessibilityAction contextAction =
- mCustomAccessibilityActions.get(flutterId);
+ customAccessibilityActions.get(flutterId);
if (contextAction != null) {
- mOwner.dispatchSemanticsAction(
+ owner.dispatchSemanticsAction(
virtualViewId, Action.CUSTOM_ACTION, contextAction.id);
return true;
}
@@ -540,12 +562,12 @@ class AccessibilityBridge
switch (granularity) {
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: {
if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
- mOwner.dispatchSemanticsAction(virtualViewId,
+ owner.dispatchSemanticsAction(virtualViewId,
Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection);
return true;
}
if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
- mOwner.dispatchSemanticsAction(virtualViewId,
+ owner.dispatchSemanticsAction(virtualViewId,
Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection);
return true;
}
@@ -553,12 +575,12 @@ class AccessibilityBridge
}
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
- mOwner.dispatchSemanticsAction(virtualViewId,
+ owner.dispatchSemanticsAction(virtualViewId,
Action.MOVE_CURSOR_FORWARD_BY_WORD, extendSelection);
return true;
}
if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
- mOwner.dispatchSemanticsAction(virtualViewId,
+ owner.dispatchSemanticsAction(virtualViewId,
Action.MOVE_CURSOR_BACKWARD_BY_WORD, extendSelection);
return true;
}
@@ -573,65 +595,65 @@ class AccessibilityBridge
public AccessibilityNodeInfo findFocus(int focus) {
switch (focus) {
case AccessibilityNodeInfo.FOCUS_INPUT: {
- if (mInputFocusedObject != null)
- return createAccessibilityNodeInfo(mInputFocusedObject.id);
+ if (inputFocusedObject != null)
+ return createAccessibilityNodeInfo(inputFocusedObject.id);
}
// Fall through to check FOCUS_ACCESSIBILITY
case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
- if (mA11yFocusedObject != null)
- return createAccessibilityNodeInfo(mA11yFocusedObject.id);
+ if (a11yFocusedObject != null)
+ return createAccessibilityNodeInfo(a11yFocusedObject.id);
}
}
return null;
}
private SemanticsObject getRootObject() {
- assert mObjects.containsKey(0);
- return mObjects.get(0);
+ assert objects.containsKey(0);
+ return objects.get(0);
}
private SemanticsObject getOrCreateObject(int id) {
- SemanticsObject object = mObjects.get(id);
+ SemanticsObject object = objects.get(id);
if (object == null) {
object = new SemanticsObject();
object.id = id;
- mObjects.put(id, object);
+ objects.put(id, object);
}
return object;
}
private CustomAccessibilityAction getOrCreateAction(int id) {
- CustomAccessibilityAction action = mCustomAccessibilityActions.get(id);
+ CustomAccessibilityAction action = customAccessibilityActions.get(id);
if (action == null) {
action = new CustomAccessibilityAction();
action.id = id;
action.resourceId = id + firstResourceId;
- mCustomAccessibilityActions.put(id, action);
+ customAccessibilityActions.put(id, action);
}
return action;
}
void handleTouchExplorationExit() {
- if (mHoveredObject != null) {
- sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
- mHoveredObject = null;
+ if (hoveredObject != null) {
+ sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ hoveredObject = null;
}
}
void handleTouchExploration(float x, float y) {
- if (mObjects.isEmpty()) {
+ if (objects.isEmpty()) {
return;
}
SemanticsObject newObject = getRootObject().hitTest(new float[] {x, y, 0, 1});
- if (newObject != mHoveredObject) {
+ if (newObject != hoveredObject) {
// sending ENTER before EXIT is how Android wants it
if (newObject != null) {
sendAccessibilityEvent(newObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
}
- if (mHoveredObject != null) {
- sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ if (hoveredObject != null) {
+ sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
}
- mHoveredObject = newObject;
+ hoveredObject = newObject;
}
}
@@ -657,7 +679,7 @@ class AccessibilityBridge
continue;
}
if (object.hasFlag(Flag.IS_FOCUSED)) {
- mInputFocusedObject = object;
+ inputFocusedObject = object;
}
if (object.hadPreviousConfig) {
updated.add(object);
@@ -675,12 +697,12 @@ class AccessibilityBridge
// a11y nodes.
if (Build.VERSION.SDK_INT >= 23) {
Rect visibleFrame = new Rect();
- mDecorView.getWindowVisibleDisplayFrame(visibleFrame);
- if (!mLastLeftFrameInset.equals(visibleFrame.left)) {
+ decorView.getWindowVisibleDisplayFrame(visibleFrame);
+ if (!lastLeftFrameInset.equals(visibleFrame.left)) {
rootObject.globalGeometryDirty = true;
rootObject.inverseTransformDirty = true;
}
- mLastLeftFrameInset = visibleFrame.left;
+ lastLeftFrameInset = visibleFrame.left;
Matrix.translateM(identity, 0, visibleFrame.left, 0, 0);
}
rootObject.updateRecursively(identity, visitedObjects, false);
@@ -707,7 +729,7 @@ class AccessibilityBridge
previousRoutes.add(semanticsObject.id);
}
- Iterator> it = mObjects.entrySet().iterator();
+ Iterator> it = objects.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
SemanticsObject object = entry.getValue();
@@ -787,25 +809,25 @@ class AccessibilityBridge
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
} else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel()
- && mInputFocusedObject != null && mInputFocusedObject.id == object.id) {
+ && inputFocusedObject != null && inputFocusedObject.id == object.id) {
// Text fields should announce when their label changes while focused. We use a live
// region tag to do so, and this event triggers that update.
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
- if (mA11yFocusedObject != null && mA11yFocusedObject.id == object.id
+ if (a11yFocusedObject != null && a11yFocusedObject.id == object.id
&& !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) {
AccessibilityEvent event =
obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED);
event.getText().add(object.label);
sendAccessibilityEvent(event);
}
- if (mInputFocusedObject != null && mInputFocusedObject.id == object.id
+ if (inputFocusedObject != null && inputFocusedObject.id == object.id
&& object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD)
// If we have a TextField that has InputFocus, we should avoid announcing it if something
// else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus
// or the "PASTE" popup is used though.
// See more discussion at https://github.com/flutter/flutter/issues/23180
- && (mA11yFocusedObject == null || (mA11yFocusedObject.id == mInputFocusedObject.id))) {
+ && (a11yFocusedObject == null || (a11yFocusedObject.id == inputFocusedObject.id))) {
String oldValue = object.previousValue != null ? object.previousValue : "";
String newValue = object.value != null ? object.value : "";
AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue);
@@ -863,65 +885,27 @@ class AccessibilityBridge
private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
assert virtualViewId != ROOT_NODE_ID;
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
- event.setPackageName(mOwner.getContext().getPackageName());
- event.setSource(mOwner, virtualViewId);
+ event.setPackageName(owner.getContext().getPackageName());
+ event.setSource(owner, virtualViewId);
return event;
}
private void sendAccessibilityEvent(int virtualViewId, int eventType) {
- if (!mAccessibilityEnabled) {
+ if (!accessibilityEnabled) {
return;
}
if (virtualViewId == ROOT_NODE_ID) {
- mOwner.sendAccessibilityEvent(eventType);
+ owner.sendAccessibilityEvent(eventType);
} else {
sendAccessibilityEvent(obtainAccessibilityEvent(virtualViewId, eventType));
}
}
private void sendAccessibilityEvent(AccessibilityEvent event) {
- if (!mAccessibilityEnabled) {
+ if (!accessibilityEnabled) {
return;
}
- mOwner.getParent().requestSendAccessibilityEvent(mOwner, event);
- }
-
- // Message Handler for [mFlutterAccessibilityChannel].
- public void onMessage(Object message, BasicMessageChannel.Reply reply) {
- @SuppressWarnings("unchecked")
- final HashMap annotatedEvent = (HashMap) message;
- final String type = (String) annotatedEvent.get("type");
- @SuppressWarnings("unchecked")
- final HashMap data = (HashMap) annotatedEvent.get("data");
-
- switch (type) {
- case "announce":
- mOwner.announceForAccessibility((String) data.get("message"));
- break;
- case "longPress": {
- Integer nodeId = (Integer) annotatedEvent.get("nodeId");
- if (nodeId == null) {
- return;
- }
- sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
- break;
- }
- case "tap": {
- Integer nodeId = (Integer) annotatedEvent.get("nodeId");
- if (nodeId == null) {
- return;
- }
- sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED);
- break;
- }
- case "tooltip": {
- AccessibilityEvent e = obtainAccessibilityEvent(
- ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
- e.getText().add((String) data.get("message"));
- sendAccessibilityEvent(e);
- break;
- }
- }
+ owner.getParent().requestSendAccessibilityEvent(owner, event);
}
private void createWindowChangeEvent(SemanticsObject route) {
@@ -933,29 +917,29 @@ class AccessibilityBridge
}
private void willRemoveSemanticsObject(SemanticsObject object) {
- assert mObjects.containsKey(object.id);
- assert mObjects.get(object.id) == object;
+ assert objects.containsKey(object.id);
+ assert objects.get(object.id) == object;
object.parent = null;
- if (mA11yFocusedObject == object) {
- sendAccessibilityEvent(mA11yFocusedObject.id,
+ if (a11yFocusedObject == object) {
+ sendAccessibilityEvent(a11yFocusedObject.id,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- mA11yFocusedObject = null;
+ a11yFocusedObject = null;
}
- if (mInputFocusedObject == object) {
- mInputFocusedObject = null;
+ if (inputFocusedObject == object) {
+ inputFocusedObject = null;
}
- if (mHoveredObject == object) {
- mHoveredObject = null;
+ if (hoveredObject == object) {
+ hoveredObject = null;
}
}
void reset() {
- mObjects.clear();
- if (mA11yFocusedObject != null)
- sendAccessibilityEvent(mA11yFocusedObject.id,
+ objects.clear();
+ if (a11yFocusedObject != null)
+ sendAccessibilityEvent(a11yFocusedObject.id,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
- mA11yFocusedObject = null;
- mHoveredObject = null;
+ a11yFocusedObject = null;
+ hoveredObject = null;
sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
}
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index c6927a32457115664f9d35d45cbee9851c412adb..00bd03663ae8f9f4986c9d458c4f593601d2c867 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -15,6 +15,7 @@ import android.graphics.SurfaceTexture;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
+import android.os.LocaleList;
import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.AttributeSet;
@@ -29,17 +30,17 @@ import io.flutter.app.FlutterPluginRegistry;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.android.AndroidKeyProcessor;
import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
+import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.plugin.common.*;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
-import org.json.JSONException;
-import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
@@ -87,18 +88,21 @@ public class FlutterView extends SurfaceView
}
private final DartExecutor dartExecutor;
+ private final AccessibilityChannel accessibilityChannel;
private final NavigationChannel navigationChannel;
private final KeyEventChannel keyEventChannel;
private final LifecycleChannel lifecycleChannel;
+ private final LocalizationChannel localizationChannel;
+ private final PlatformChannel platformChannel;
private final SettingsChannel settingsChannel;
private final SystemChannel systemChannel;
private final InputMethodManager mImm;
private final TextInputPlugin mTextInputPlugin;
private final AndroidKeyProcessor androidKeyProcessor;
+ private AccessibilityBridge mAccessibilityNodeProvider;
private final SurfaceHolder.Callback mSurfaceCallback;
private final ViewportMetrics mMetrics;
private final AccessibilityManager mAccessibilityManager;
- private final MethodChannel mFlutterLocalizationChannel;
private final List mActivityLifecycleListeners;
private final List mFirstFrameListeners;
private final AtomicLong nextTextureId = new AtomicLong(0L);
@@ -160,23 +164,25 @@ public class FlutterView extends SurfaceView
mActivityLifecycleListeners = new ArrayList<>();
mFirstFrameListeners = new ArrayList<>();
- // Configure the platform plugins and flutter channels.
+ // Create all platform channels
+ accessibilityChannel = new AccessibilityChannel(dartExecutor);
navigationChannel = new NavigationChannel(dartExecutor);
keyEventChannel = new KeyEventChannel(dartExecutor);
lifecycleChannel = new LifecycleChannel(dartExecutor);
+ localizationChannel = new LocalizationChannel(dartExecutor);
+ platformChannel = new PlatformChannel(dartExecutor);
systemChannel = new SystemChannel(dartExecutor);
settingsChannel = new SettingsChannel(dartExecutor);
- mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
- PlatformPlugin platformPlugin = new PlatformPlugin(activity);
- MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);
- flutterPlatformChannel.setMethodCallHandler(platformPlugin);
+ // Create and setup plugins
+ PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel);
addActivityLifecycleListener(platformPlugin);
mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- mTextInputPlugin = new TextInputPlugin(this);
+ mTextInputPlugin = new TextInputPlugin(this, dartExecutor);
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel);
- setLocales(getResources().getConfiguration());
+ // Send initial platform information to Dart
+ sendLocalesToDart(getResources().getConfiguration());
sendUserPlatformSettingsToDart();
}
@@ -311,39 +317,21 @@ public class FlutterView extends SurfaceView
.send();
}
- private void setLocales(Configuration config) {
- if (Build.VERSION.SDK_INT >= 24) {
- try {
- // Passes the full list of locales for android API >= 24 with reflection.
- Object localeList = config.getClass().getDeclaredMethod("getLocales").invoke(config);
- Method localeListGet = localeList.getClass().getDeclaredMethod("get", int.class);
- Method localeListSize = localeList.getClass().getDeclaredMethod("size");
- int localeCount = (int)localeListSize.invoke(localeList);
- List data = new ArrayList<>();
- for (int index = 0; index < localeCount; ++index) {
- Locale locale = (Locale)localeListGet.invoke(localeList, index);
- data.add(locale.getLanguage());
- data.add(locale.getCountry());
- data.add(locale.getScript());
- data.add(locale.getVariant());
- }
- mFlutterLocalizationChannel.invokeMethod("setLocale", data);
- return;
- } catch (Exception exception) {
- // Any exception is a failure. Resort to fallback of sending only one locale.
- }
+ private void sendLocalesToDart(Configuration config) {
+ LocaleList localeList = config.getLocales();
+ int localeCount = localeList.size();
+ List locales = new ArrayList<>();
+ for (int index = 0; index < localeCount; ++index) {
+ Locale locale = localeList.get(index);
+ locales.add(locale);
}
- // Fallback single locale passing for android API < 24. Should work always.
- @SuppressWarnings("deprecation")
- Locale locale = config.locale;
- // getScript() is gated because it is added in API 21.
- mFlutterLocalizationChannel.invokeMethod("setLocale", Arrays.asList(locale.getLanguage(), locale.getCountry(), Build.VERSION.SDK_INT >= 21 ? locale.getScript() : "", locale.getVariant()));
+ localizationChannel.sendLocales(locales);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
- setLocales(newConfig);
+ sendLocalesToDart(newConfig);
sendUserPlatformSettingsToDart();
}
@@ -374,13 +362,7 @@ public class FlutterView extends SurfaceView
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- try {
- mLastInputConnection = mTextInputPlugin.createInputConnection(this, outAttrs);
- return mLastInputConnection;
- } catch (JSONException e) {
- Log.e(TAG, "Failed to create input connection", e);
- return null;
- }
+ return mTextInputPlugin.createInputConnection(this, outAttrs);
}
// Must match the PointerChange enum in pointer.dart.
@@ -1006,14 +988,12 @@ public class FlutterView extends SurfaceView
return null;
}
- private AccessibilityBridge mAccessibilityNodeProvider;
-
void ensureAccessibilityEnabled() {
if (!isAttached())
return;
mAccessibilityEnabled = true;
if (mAccessibilityNodeProvider == null) {
- mAccessibilityNodeProvider = new AccessibilityBridge(this);
+ mAccessibilityNodeProvider = new AccessibilityBridge(this, accessibilityChannel);
}
mNativeView.getFlutterJNI().setSemanticsEnabled(true);
mAccessibilityNodeProvider.setAccessibilityEnabled(true);