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);