From a9c8a6c06aa74f7dc954f1eef7af9ffcef28833b Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 20 Feb 2019 11:18:12 -0800 Subject: [PATCH] Revert "Android embedding refactor pr3 add remaining systemchannels (#7874)" (flutter/engine#7886) This reverts commit 08a8b11065ca3417490ed789bf5a642988d5e8d4. --- .../ci/licenses_golden/licenses_flutter | 2 - .../flutter/shell/platform/android/BUILD.gn | 2 - .../systemchannels/AccessibilityChannel.java | 120 ---- .../systemchannels/LocalizationChannel.java | 23 +- .../systemchannels/PlatformChannel.java | 597 +----------------- .../systemchannels/TextInputChannel.java | 406 ------------ .../editing/InputConnectionAdaptor.java | 58 +- .../plugin/editing/TextInputPlugin.java | 193 +++--- .../plugin/platform/PlatformPlugin.java | 383 ++++++----- .../io/flutter/view/AccessibilityBridge.java | 294 +++++---- .../android/io/flutter/view/FlutterView.java | 75 ++- 11 files changed, 572 insertions(+), 1581 deletions(-) delete mode 100644 engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java delete mode 100644 engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 39c7d86010..cea4f342c6 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -446,7 +446,6 @@ 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 @@ -454,7 +453,6 @@ 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/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index ea6ee24397..b6560fca38 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -114,7 +114,6 @@ 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", @@ -122,7 +121,6 @@ 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/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java deleted file mode 100644 index 4bfcd37973..0000000000 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java +++ /dev/null @@ -1,120 +0,0 @@ -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/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java index 52e4fbb461..ffb8f1f84a 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java @@ -6,17 +6,14 @@ 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; /** - * Sends the platform's locales to Dart. + * TODO(mattcarroll): fill in javadoc for LocalizationChannel. */ public class LocalizationChannel { @@ -27,18 +24,12 @@ public class LocalizationChannel { this.channel = new MethodChannel(dartExecutor, "flutter/localization", JSONMethodCodec.INSTANCE); } - /** - * 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); + public void setLocale(String language, String country) { + channel.invokeMethod("setLocale", Arrays.asList(language, country)); + } + + public void setMethodCallHandler(MethodChannel.MethodCallHandler handler) { + channel.setMethodCallHandler(handler); } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java index 44269313bb..b1ceb82b7a 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java @@ -4,613 +4,26 @@ 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; /** - * System channel that receives requests for host platform behavior, e.g., haptic and sound - * effects, system chrome configurations, and clipboard interaction. + * TODO(mattcarroll): fill in javadoc for PlatformChannel. */ 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) { - channel = new MethodChannel(dartExecutor, "flutter/platform", JSONMethodCodec.INSTANCE); - channel.setMethodCallHandler(parsingMethodCallHandler); + this.channel = new MethodChannel(dartExecutor, "flutter/platform", JSONMethodCodec.INSTANCE); } - /** - * 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; + public void setMethodCallHandler(@Nullable MethodChannel.MethodCallHandler handler) { + channel.setMethodCallHandler(handler); } - // 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 - ); - } - - /** - * 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/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java deleted file mode 100644 index fc4b755215..0000000000 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ /dev/null @@ -1,406 +0,0 @@ -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/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index b3a287820b..df907be0b7 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -11,16 +11,17 @@ 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 TextInputChannel textInputChannel; + private final MethodChannel mFlutterChannel; private final Editable mEditable; private int mBatchCount; private InputMethodManager mImm; @@ -28,16 +29,12 @@ class InputConnectionAdaptor extends BaseInputConnection { private static final MethodChannel.Result logger = new ErrorLogResult("FlutterTextInput"); - public InputConnectionAdaptor( - FlutterView view, - int client, - TextInputChannel textInputChannel, - Editable editable - ) { + public InputConnectionAdaptor(FlutterView view, int client, + MethodChannel flutterChannel, Editable editable) { super(view, true); mFlutterView = view; mClient = client; - this.textInputChannel = textInputChannel; + mFlutterChannel = flutterChannel; mEditable = editable; mBatchCount = 0; mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); @@ -58,14 +55,14 @@ class InputConnectionAdaptor extends BaseInputConnection { selectionStart, selectionEnd, composingStart, composingEnd); - textInputChannel.updateEditingState( - mClient, - mEditable.toString(), - 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); } @Override @@ -181,30 +178,39 @@ 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: - textInputChannel.newline(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.newline"), logger); break; case EditorInfo.IME_ACTION_UNSPECIFIED: - textInputChannel.unspecifiedAction(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.unspecified"), logger); break; case EditorInfo.IME_ACTION_GO: - textInputChannel.go(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.go"), logger); break; case EditorInfo.IME_ACTION_SEARCH: - textInputChannel.search(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.search"), logger); break; case EditorInfo.IME_ACTION_SEND: - textInputChannel.send(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.send"), logger); break; case EditorInfo.IME_ACTION_NEXT: - textInputChannel.next(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.next"), logger); break; case EditorInfo.IME_ACTION_PREVIOUS: - textInputChannel.previous(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.previous"), logger); break; default: case EditorInfo.IME_ACTION_DONE: - textInputChannel.done(mClient); + mFlutterChannel.invokeMethod("TextInputClient.performAction", + Arrays.asList(mClient, "TextInputAction.done"), logger); break; } return true; diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index bc54b3ed04..c59ee25148 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -5,7 +5,6 @@ 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; @@ -13,87 +12,84 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; - -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.systemchannels.TextInputChannel; +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.view.FlutterView; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; /** * Android implementation of the text input plugin. */ -public class TextInputPlugin { +public class TextInputPlugin implements MethodCallHandler { private final FlutterView mView; private final InputMethodManager mImm; - private final TextInputChannel textInputChannel; + private final MethodChannel mFlutterChannel; private int mClient = 0; - private TextInputChannel.Configuration configuration; + private JSONObject mConfiguration; private Editable mEditable; private boolean mRestartInputPending; - public TextInputPlugin(FlutterView view, @NonNull DartExecutor dartExecutor) { + public TextInputPlugin(FlutterView view) { mView = view; mImm = (InputMethodManager) view.getContext().getSystemService( Context.INPUT_METHOD_SERVICE); - - textInputChannel = new TextInputChannel(dartExecutor); - textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() { - @Override - public void show() { - showTextInput(mView); - } - - @Override - public void hide() { - hideTextInput(mView); - } - - @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(); - } - }); + mFlutterChannel = new MethodChannel(view, "flutter/textinput", JSONMethodCodec.INSTANCE); + mFlutterChannel.setMethodCallHandler(this); } - 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.isSigned) { - textType |= InputType.TYPE_NUMBER_FLAG_SIGNED; + @Override + public void onMethodCall(MethodCall call, Result result) { + String method = call.method; + Object args = call.arguments; + try { + if (method.equals("TextInput.show")) { + showTextInput(mView); + result.success(null); + } else if (method.equals("TextInput.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")) { + clearTextInputClient(); + result.success(null); + } else { + result.notImplemented(); } - if (type.isDecimal) { - textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL; - } - return textType; - } else if (type.type == TextInputChannel.TextInputType.PHONE) { - return InputType.TYPE_CLASS_PHONE; + } 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")) { + 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; + return textType; + } + if (inputType.equals("TextInputType.phone")) return InputType.TYPE_CLASS_PHONE; int textType = InputType.TYPE_CLASS_TEXT; - if (type.type == TextInputChannel.TextInputType.MULTILINE) { + if (inputType.equals("TextInputType.multiline")) textType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; - } else if (type.type == TextInputChannel.TextInputType.EMAIL_ADDRESS) { + else if (inputType.equals("TextInputType.emailAddress")) textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; - } else if (type.type == TextInputChannel.TextInputType.URL) { + else if (inputType.equals("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; @@ -101,50 +97,69 @@ public class TextInputPlugin { } else { if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; } - - if (textCapitalization == TextInputChannel.TextCapitalization.CHARACTERS) { + if (textCapitalization.equals("TextCapitalization.characters")) { textType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; - } else if (textCapitalization == TextInputChannel.TextCapitalization.WORDS) { + } else if (textCapitalization.equals("TextCapitalization.words")) { textType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; - } else if (textCapitalization == TextInputChannel.TextCapitalization.SENTENCES) { + } else if (textCapitalization.equals("TextCapitalization.sentences")) { textType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; } - return textType; } - public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs) { + 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; + } + } + + public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs) + throws JSONException { if (mClient == 0) return null; - outAttrs.inputType = inputTypeFromTextInputType( - configuration.inputType, - configuration.obscureText, - configuration.autocorrect, - configuration.textCapitalization - ); + outAttrs.inputType = inputTypeFromTextInputType(mConfiguration.getJSONObject("inputType"), + mConfiguration.optBoolean("obscureText"), + mConfiguration.optBoolean("autocorrect", true), + mConfiguration.getString("textCapitalization")); outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; int enterAction; - if (configuration.inputAction == null) { + if (mConfiguration.isNull("inputAction")) { // 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 = configuration.inputAction; + enterAction = inputActionFromTextInputAction(mConfiguration.getString("inputAction")); } - if (configuration.actionLabel != null) { - outAttrs.actionLabel = configuration.actionLabel; + if (!mConfiguration.isNull("actionLabel")) { + outAttrs.actionLabel = mConfiguration.getString("actionLabel"); outAttrs.actionId = enterAction; } outAttrs.imeOptions |= enterAction; - InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, - mClient, - textInputChannel, - mEditable - ); + InputConnectionAdaptor connection = + new InputConnectionAdaptor(view, mClient, mFlutterChannel, mEditable); outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); @@ -160,9 +175,9 @@ public class TextInputPlugin { mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); } - private void setTextInputClient(int client, TextInputChannel.Configuration configuration) { + private void setTextInputClient(FlutterView view, int client, JSONObject configuration) { mClient = client; - this.configuration = configuration; + mConfiguration = configuration; mEditable = Editable.Factory.getInstance().newEditable(""); // setTextInputClient will be followed by a call to setTextInputEditingState. @@ -170,9 +185,9 @@ public class TextInputPlugin { mRestartInputPending = true; } - private void applyStateToSelection(TextInputChannel.TextEditState state) { - int selStart = state.selectionStart; - int selEnd = state.selectionEnd; + private void applyStateToSelection(JSONObject state) throws JSONException { + int selStart = state.getInt("selectionBase"); + int selEnd = state.getInt("selectionExtent"); if (selStart >= 0 && selStart <= mEditable.length() && selEnd >= 0 && selEnd <= mEditable.length()) { Selection.setSelection(mEditable, selStart, selEnd); @@ -181,15 +196,15 @@ public class TextInputPlugin { } } - private void setTextInputEditingState(FlutterView view, TextInputChannel.TextEditState state) { - if (!mRestartInputPending && state.text.equals(mEditable.toString())) { + private void setTextInputEditingState(FlutterView view, JSONObject state) throws JSONException { + if (!mRestartInputPending && state.getString("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.text); + mEditable.replace(0, mEditable.length(), state.getString("text")); applyStateToSelection(state); mImm.restartInput(view); mRestartInputPending = false; diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java index e9d44001ef..b6f53d4574 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java @@ -9,159 +9,212 @@ 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.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.util.Log; 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 ActivityLifecycleListener { +public class PlatformPlugin implements MethodCallHandler, ActivityLifecycleListener { + private final Activity mActivity; + private JSONObject mCurrentTheme; 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"; - private final Activity activity; - private final PlatformChannel platformChannel; - private PlatformChannel.SystemChromeStyle currentTheme; - private int mEnabledOverlays; - - private final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() { - @Override - public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) { - PlatformPlugin.this.playSystemSound(soundType); - } - - @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(); - } - - @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); - + public PlatformPlugin(Activity activity) { + mActivity = activity; mEnabledOverlays = DEFAULT_SYSTEM_UI; } - private void playSystemSound(PlatformChannel.SoundType soundType) { - if (soundType == PlatformChannel.SoundType.CLICK) { - View view = activity.getWindow().getDecorView(); + @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 void playSystemSound(String soundType) { + if (soundType.equals("SystemSoundType.click")) { + View view = mActivity.getWindow().getDecorView(); view.playSoundEffect(SoundEffectConstants.CLICK); } } - private void vibrateHapticFeedback(PlatformChannel.HapticFeedbackType feedbackType) { - View view = activity.getWindow().getDecorView(); - switch (feedbackType) { - case STANDARD: - view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - break; - case LIGHT_IMPACT: - view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); - break; - case MEDIUM_IMPACT: - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); - break; - case HEAVY_IMPACT: - // HapticFeedbackConstants.CONTEXT_CLICK from API level 23. - view.performHapticFeedback(6); - break; - case SELECTION_CLICK: - view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - break; + 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); } } - private void setSystemChromePreferredOrientations(int androidOrientation) { - activity.setRequestedOrientation(androidOrientation); + 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; + } + } + 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); + break; + case 0x08: + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + break; + case 0x0a: + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE); + break; + case 0x0b: + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER); + break; + case 0x0f: + mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER); + 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 setSystemChromeApplicationSwitcherDescription(PlatformChannel.AppSwitcherDescription description) { + private void setSystemChromeApplicationSwitcherDescription(JSONObject description) throws JSONException { 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(description.label, 0, description.color) - : new TaskDescription(description.label, null, description.color); + ? new TaskDescription(label, 0, color) + : new TaskDescription(label, null, color); - activity.setTaskDescription(taskDescription); + mActivity.setTaskDescription(taskDescription); } - private void setSystemChromeEnabledSystemUIOverlays(List overlaysToShow) { - // Start by assuming we want to hide all system overlays (like an immersive game). + private int mEnabledOverlays; + + private void setSystemChromeEnabledSystemUIOverlays(JSONArray overlays) throws JSONException { int enabledOverlays = DEFAULT_SYSTEM_UI | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (overlaysToShow.size() == 0) { + if (overlays.length() == 0) { enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } - // 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; + 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; } } @@ -170,9 +223,9 @@ public class PlatformPlugin implements ActivityLifecycleListener { } private void updateSystemUiOverlays(){ - activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays); - if (currentTheme != null) { - setSystemChromeSystemUIOverlayStyle(currentTheme); + mActivity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays); + if (mCurrentTheme != null) { + setSystemChromeSystemUIOverlayStyle(mCurrentTheme); } } @@ -180,75 +233,83 @@ public class PlatformPlugin implements ActivityLifecycleListener { updateSystemUiOverlays(); } - private void setSystemChromeSystemUIOverlayStyle(PlatformChannel.SystemChromeStyle systemChromeStyle) { - Window window = activity.getWindow(); + private void setSystemChromeSystemUIOverlayStyle(JSONObject message) { + Window window = mActivity.getWindow(); View view = window.getDecorView(); int flags = view.getSystemUiVisibility(); - // 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; + 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")); } } - 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; + // 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.statusBarColor != null) { - window.setStatusBarColor(systemChromeStyle.statusBarColor); + if (!message.isNull("systemNavigationBarDividerColor")) { + // Not availible until Android P. + // window.setNavigationBarDividerColor(systemNavigationBarDividerColor); } + 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() { - activity.finish(); + mActivity.finish(); } - private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) { - ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + private JSONObject getClipboardData(String format) throws JSONException { + ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = clipboard.getPrimaryClip(); if (clip == null) return null; - if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) { - return clip.getItemAt(0).coerceToText(activity); + if (format == null || format.equals(kTextPlainFormat)) { + JSONObject result = new JSONObject(); + result.put("text", clip.getItemAt(0).coerceToText(mActivity)); + return result; } return null; } - private void setClipboardData(String text) { - ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("text label?", text); + private void setClipboardData(JSONObject data) throws JSONException { + ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("text label?", data.getString("text")); clipboard.setPrimaryClip(clip); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java index ac5e2f8f3d..d8c2de3749 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -9,21 +9,20 @@ 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.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.util.Predicate; import java.nio.ByteBuffer; import java.util.*; class AccessibilityBridge - extends AccessibilityNodeProvider { + extends AccessibilityNodeProvider implements BasicMessageChannel.MessageHandler { private static final String TAG = "FlutterView"; // Constants from higher API levels. @@ -35,42 +34,19 @@ class AccessibilityBridge private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f; private static final int ROOT_NODE_ID = 0; - 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 Map mObjects; + private Map mCustomAccessibilityActions; + private final FlutterView mOwner; + private boolean mAccessibilityEnabled = false; + private SemanticsObject mA11yFocusedObject; + private SemanticsObject mInputFocusedObject; + private SemanticsObject mHoveredObject; private int previousRouteId = ROOT_NODE_ID; private List previousRoutes; - private Integer lastLeftFrameInset = 0; + private final View mDecorView; + private Integer mLastLeftFrameInset = 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); - } - - @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); - } - }; + private final BasicMessageChannel mFlutterAccessibilityChannel; enum Action { TAP(1 << 0), @@ -130,21 +106,23 @@ class AccessibilityBridge final int value; } - 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<>(); + AccessibilityBridge(FlutterView owner) { + assert owner != null; + mOwner = owner; + mObjects = new HashMap<>(); + mCustomAccessibilityActions = new HashMap<>(); previousRoutes = new ArrayList<>(); + mFlutterAccessibilityChannel = new BasicMessageChannel<>( + owner, "flutter/accessibility", StandardMessageCodec.INSTANCE); + mDecorView = ((Activity) owner.getContext()).getWindow().getDecorView(); } void setAccessibilityEnabled(boolean accessibilityEnabled) { - this.accessibilityEnabled = accessibilityEnabled; + mAccessibilityEnabled = accessibilityEnabled; if (accessibilityEnabled) { - this.accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); + mFlutterAccessibilityChannel.setMessageHandler(this); } else { - this.accessibilityChannel.setAccessibilityMessageHandler(null); + mFlutterAccessibilityChannel.setMessageHandler(null); } } @@ -159,42 +137,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(a11yFocusedObject, o -> o == object) - || !hasSemanticsObjectAncestor(a11yFocusedObject, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING))); + && (hasSemanticsObjectAncestor(mA11yFocusedObject, o -> o == object) + || !hasSemanticsObjectAncestor(mA11yFocusedObject, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING))); } @Override @SuppressWarnings("deprecation") public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { if (virtualViewId == View.NO_ID) { - AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner); - owner.onInitializeAccessibilityNodeInfo(result); - if (objects.containsKey(ROOT_NODE_ID)) { - result.addChild(owner, ROOT_NODE_ID); + AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner); + mOwner.onInitializeAccessibilityNodeInfo(result); + if (mObjects.containsKey(ROOT_NODE_ID)) { + result.addChild(mOwner, ROOT_NODE_ID); } return result; } - SemanticsObject object = objects.get(virtualViewId); + SemanticsObject object = mObjects.get(virtualViewId); if (object == null) { return null; } - AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(owner, virtualViewId); + AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner, 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(owner.getContext().getPackageName()); + result.setPackageName(mOwner.getContext().getPackageName()); result.setClassName("android.view.View"); - result.setSource(owner, virtualViewId); + result.setSource(mOwner, virtualViewId); result.setFocusable(object.isFocusable()); - if (inputFocusedObject != null) { - result.setFocused(inputFocusedObject.id == virtualViewId); + if (mInputFocusedObject != null) { + result.setFocused(mInputFocusedObject.id == virtualViewId); } - if (a11yFocusedObject != null) { - result.setAccessibilityFocused(a11yFocusedObject.id == virtualViewId); + if (mA11yFocusedObject != null) { + result.setAccessibilityFocused(mA11yFocusedObject.id == virtualViewId); } if (object.hasFlag(Flag.IS_TEXT_FIELD)) { @@ -208,7 +186,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 && a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && mA11yFocusedObject != null && mA11yFocusedObject.id == virtualViewId) { result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); } } @@ -261,10 +239,10 @@ class AccessibilityBridge if (object.parent != null) { assert object.id > ROOT_NODE_ID; - result.setParent(owner, object.parent.id); + result.setParent(mOwner, object.parent.id); } else { assert object.id == ROOT_NODE_ID; - result.setParent(owner); + result.setParent(mOwner); } Rect bounds = object.getGlobalRect(); @@ -384,7 +362,7 @@ class AccessibilityBridge result.setSelected(object.hasFlag(Flag.IS_SELECTED)); // Accessibility Focus - if (a11yFocusedObject != null && a11yFocusedObject.id == virtualViewId) { + if (mA11yFocusedObject != null && mA11yFocusedObject.id == virtualViewId) { result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } else { result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); @@ -403,7 +381,7 @@ class AccessibilityBridge if (object.childrenInTraversalOrder != null) { for (SemanticsObject child : object.childrenInTraversalOrder) { if (!child.hasFlag(Flag.IS_HIDDEN)) { - result.addChild(owner, child.id); + result.addChild(mOwner, child.id); } } } @@ -413,7 +391,7 @@ class AccessibilityBridge @Override public boolean performAction(int virtualViewId, int action, Bundle arguments) { - SemanticsObject object = objects.get(virtualViewId); + SemanticsObject object = mObjects.get(virtualViewId); if (object == null) { return false; } @@ -422,27 +400,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. - owner.dispatchSemanticsAction(virtualViewId, Action.TAP); + mOwner.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. - owner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS); + mOwner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS); return true; } case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { if (object.hasAction(Action.SCROLL_UP)) { - owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP); + mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP); } else if (object.hasAction(Action.SCROLL_LEFT)) { // TODO(ianh): bidi support using textDirection - owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT); + mOwner.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); - owner.dispatchSemanticsAction(virtualViewId, Action.INCREASE); + mOwner.dispatchSemanticsAction(virtualViewId, Action.INCREASE); } else { return false; } @@ -450,15 +428,15 @@ class AccessibilityBridge } case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { if (object.hasAction(Action.SCROLL_DOWN)) { - owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN); + mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN); } else if (object.hasAction(Action.SCROLL_RIGHT)) { // TODO(ianh): bidi support using textDirection - owner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT); + mOwner.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); - owner.dispatchSemanticsAction(virtualViewId, Action.DECREASE); + mOwner.dispatchSemanticsAction(virtualViewId, Action.DECREASE); } else { return false; } @@ -471,24 +449,24 @@ class AccessibilityBridge return performCursorMoveAction(object, virtualViewId, arguments, true); } case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { - owner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS); + mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS); sendAccessibilityEvent( virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - a11yFocusedObject = null; + mA11yFocusedObject = null; return true; } case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { - owner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS); + mOwner.dispatchSemanticsAction(virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS); sendAccessibilityEvent( virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); - if (a11yFocusedObject == null) { + if (mA11yFocusedObject == 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.) - owner.invalidate(); + mOwner.invalidate(); } - a11yFocusedObject = object; + mA11yFocusedObject = object; if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) { // SeekBars only announce themselves after this event. @@ -498,7 +476,7 @@ class AccessibilityBridge return true; } case ACTION_SHOW_ON_SCREEN: { - owner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN); + mOwner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN); return true; } case AccessibilityNodeInfo.ACTION_SET_SELECTION: { @@ -520,32 +498,32 @@ class AccessibilityBridge selection.put("base", object.textSelectionExtent); selection.put("extent", object.textSelectionExtent); } - owner.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection); + mOwner.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection); return true; } case AccessibilityNodeInfo.ACTION_COPY: { - owner.dispatchSemanticsAction(virtualViewId, Action.COPY); + mOwner.dispatchSemanticsAction(virtualViewId, Action.COPY); return true; } case AccessibilityNodeInfo.ACTION_CUT: { - owner.dispatchSemanticsAction(virtualViewId, Action.CUT); + mOwner.dispatchSemanticsAction(virtualViewId, Action.CUT); return true; } case AccessibilityNodeInfo.ACTION_PASTE: { - owner.dispatchSemanticsAction(virtualViewId, Action.PASTE); + mOwner.dispatchSemanticsAction(virtualViewId, Action.PASTE); return true; } case AccessibilityNodeInfo.ACTION_DISMISS: { - owner.dispatchSemanticsAction(virtualViewId, Action.DISMISS); + mOwner.dispatchSemanticsAction(virtualViewId, Action.DISMISS); return true; } default: // might be a custom accessibility action. final int flutterId = action - firstResourceId; CustomAccessibilityAction contextAction = - customAccessibilityActions.get(flutterId); + mCustomAccessibilityActions.get(flutterId); if (contextAction != null) { - owner.dispatchSemanticsAction( + mOwner.dispatchSemanticsAction( virtualViewId, Action.CUSTOM_ACTION, contextAction.id); return true; } @@ -562,12 +540,12 @@ class AccessibilityBridge switch (granularity) { case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: { if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) { - owner.dispatchSemanticsAction(virtualViewId, + mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection); return true; } if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) { - owner.dispatchSemanticsAction(virtualViewId, + mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection); return true; } @@ -575,12 +553,12 @@ class AccessibilityBridge } case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) { - owner.dispatchSemanticsAction(virtualViewId, + mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_WORD, extendSelection); return true; } if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) { - owner.dispatchSemanticsAction(virtualViewId, + mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_WORD, extendSelection); return true; } @@ -595,65 +573,65 @@ class AccessibilityBridge public AccessibilityNodeInfo findFocus(int focus) { switch (focus) { case AccessibilityNodeInfo.FOCUS_INPUT: { - if (inputFocusedObject != null) - return createAccessibilityNodeInfo(inputFocusedObject.id); + if (mInputFocusedObject != null) + return createAccessibilityNodeInfo(mInputFocusedObject.id); } // Fall through to check FOCUS_ACCESSIBILITY case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { - if (a11yFocusedObject != null) - return createAccessibilityNodeInfo(a11yFocusedObject.id); + if (mA11yFocusedObject != null) + return createAccessibilityNodeInfo(mA11yFocusedObject.id); } } return null; } private SemanticsObject getRootObject() { - assert objects.containsKey(0); - return objects.get(0); + assert mObjects.containsKey(0); + return mObjects.get(0); } private SemanticsObject getOrCreateObject(int id) { - SemanticsObject object = objects.get(id); + SemanticsObject object = mObjects.get(id); if (object == null) { object = new SemanticsObject(); object.id = id; - objects.put(id, object); + mObjects.put(id, object); } return object; } private CustomAccessibilityAction getOrCreateAction(int id) { - CustomAccessibilityAction action = customAccessibilityActions.get(id); + CustomAccessibilityAction action = mCustomAccessibilityActions.get(id); if (action == null) { action = new CustomAccessibilityAction(); action.id = id; action.resourceId = id + firstResourceId; - customAccessibilityActions.put(id, action); + mCustomAccessibilityActions.put(id, action); } return action; } void handleTouchExplorationExit() { - if (hoveredObject != null) { - sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - hoveredObject = null; + if (mHoveredObject != null) { + sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mHoveredObject = null; } } void handleTouchExploration(float x, float y) { - if (objects.isEmpty()) { + if (mObjects.isEmpty()) { return; } SemanticsObject newObject = getRootObject().hitTest(new float[] {x, y, 0, 1}); - if (newObject != hoveredObject) { + if (newObject != mHoveredObject) { // sending ENTER before EXIT is how Android wants it if (newObject != null) { sendAccessibilityEvent(newObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); } - if (hoveredObject != null) { - sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + if (mHoveredObject != null) { + sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); } - hoveredObject = newObject; + mHoveredObject = newObject; } } @@ -679,7 +657,7 @@ class AccessibilityBridge continue; } if (object.hasFlag(Flag.IS_FOCUSED)) { - inputFocusedObject = object; + mInputFocusedObject = object; } if (object.hadPreviousConfig) { updated.add(object); @@ -697,12 +675,12 @@ class AccessibilityBridge // a11y nodes. if (Build.VERSION.SDK_INT >= 23) { Rect visibleFrame = new Rect(); - decorView.getWindowVisibleDisplayFrame(visibleFrame); - if (!lastLeftFrameInset.equals(visibleFrame.left)) { + mDecorView.getWindowVisibleDisplayFrame(visibleFrame); + if (!mLastLeftFrameInset.equals(visibleFrame.left)) { rootObject.globalGeometryDirty = true; rootObject.inverseTransformDirty = true; } - lastLeftFrameInset = visibleFrame.left; + mLastLeftFrameInset = visibleFrame.left; Matrix.translateM(identity, 0, visibleFrame.left, 0, 0); } rootObject.updateRecursively(identity, visitedObjects, false); @@ -729,7 +707,7 @@ class AccessibilityBridge previousRoutes.add(semanticsObject.id); } - Iterator> it = objects.entrySet().iterator(); + Iterator> it = mObjects.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); SemanticsObject object = entry.getValue(); @@ -809,25 +787,25 @@ class AccessibilityBridge sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel() - && inputFocusedObject != null && inputFocusedObject.id == object.id) { + && mInputFocusedObject != null && mInputFocusedObject.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 (a11yFocusedObject != null && a11yFocusedObject.id == object.id + if (mA11yFocusedObject != null && mA11yFocusedObject.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 (inputFocusedObject != null && inputFocusedObject.id == object.id + if (mInputFocusedObject != null && mInputFocusedObject.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 - && (a11yFocusedObject == null || (a11yFocusedObject.id == inputFocusedObject.id))) { + && (mA11yFocusedObject == null || (mA11yFocusedObject.id == mInputFocusedObject.id))) { String oldValue = object.previousValue != null ? object.previousValue : ""; String newValue = object.value != null ? object.value : ""; AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue); @@ -885,27 +863,65 @@ class AccessibilityBridge private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) { assert virtualViewId != ROOT_NODE_ID; AccessibilityEvent event = AccessibilityEvent.obtain(eventType); - event.setPackageName(owner.getContext().getPackageName()); - event.setSource(owner, virtualViewId); + event.setPackageName(mOwner.getContext().getPackageName()); + event.setSource(mOwner, virtualViewId); return event; } private void sendAccessibilityEvent(int virtualViewId, int eventType) { - if (!accessibilityEnabled) { + if (!mAccessibilityEnabled) { return; } if (virtualViewId == ROOT_NODE_ID) { - owner.sendAccessibilityEvent(eventType); + mOwner.sendAccessibilityEvent(eventType); } else { sendAccessibilityEvent(obtainAccessibilityEvent(virtualViewId, eventType)); } } private void sendAccessibilityEvent(AccessibilityEvent event) { - if (!accessibilityEnabled) { + if (!mAccessibilityEnabled) { return; } - owner.getParent().requestSendAccessibilityEvent(owner, event); + 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; + } + } } private void createWindowChangeEvent(SemanticsObject route) { @@ -917,29 +933,29 @@ class AccessibilityBridge } private void willRemoveSemanticsObject(SemanticsObject object) { - assert objects.containsKey(object.id); - assert objects.get(object.id) == object; + assert mObjects.containsKey(object.id); + assert mObjects.get(object.id) == object; object.parent = null; - if (a11yFocusedObject == object) { - sendAccessibilityEvent(a11yFocusedObject.id, + if (mA11yFocusedObject == object) { + sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - a11yFocusedObject = null; + mA11yFocusedObject = null; } - if (inputFocusedObject == object) { - inputFocusedObject = null; + if (mInputFocusedObject == object) { + mInputFocusedObject = null; } - if (hoveredObject == object) { - hoveredObject = null; + if (mHoveredObject == object) { + mHoveredObject = null; } } void reset() { - objects.clear(); - if (a11yFocusedObject != null) - sendAccessibilityEvent(a11yFocusedObject.id, + mObjects.clear(); + if (mA11yFocusedObject != null) + sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); - a11yFocusedObject = null; - hoveredObject = null; + mA11yFocusedObject = null; + mHoveredObject = null; sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java index b76c1fee83..c6927a3245 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java @@ -15,7 +15,6 @@ 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; @@ -30,18 +29,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.LocalizationChannel; 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.*; @@ -89,21 +87,18 @@ 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); @@ -165,25 +160,23 @@ public class FlutterView extends SurfaceView mActivityLifecycleListeners = new ArrayList<>(); mFirstFrameListeners = new ArrayList<>(); - // Create all platform channels - accessibilityChannel = new AccessibilityChannel(dartExecutor); + // Configure the platform plugins and flutter channels. 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); - // Create and setup plugins - PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel); + PlatformPlugin platformPlugin = new PlatformPlugin(activity); + MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE); + flutterPlatformChannel.setMethodCallHandler(platformPlugin); addActivityLifecycleListener(platformPlugin); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - mTextInputPlugin = new TextInputPlugin(this, dartExecutor); + mTextInputPlugin = new TextInputPlugin(this); androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel); - // Send initial platform information to Dart - sendLocalesToDart(getResources().getConfiguration()); + setLocales(getResources().getConfiguration()); sendUserPlatformSettingsToDart(); } @@ -318,21 +311,39 @@ public class FlutterView extends SurfaceView .send(); } - 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); + 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. + } } - localizationChannel.sendLocales(locales); + // 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())); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - sendLocalesToDart(newConfig); + setLocales(newConfig); sendUserPlatformSettingsToDart(); } @@ -363,7 +374,13 @@ public class FlutterView extends SurfaceView @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - return mTextInputPlugin.createInputConnection(this, outAttrs); + try { + mLastInputConnection = mTextInputPlugin.createInputConnection(this, outAttrs); + return mLastInputConnection; + } catch (JSONException e) { + Log.e(TAG, "Failed to create input connection", e); + return null; + } } // Must match the PointerChange enum in pointer.dart. @@ -989,12 +1006,14 @@ public class FlutterView extends SurfaceView return null; } + private AccessibilityBridge mAccessibilityNodeProvider; + void ensureAccessibilityEnabled() { if (!isAttached()) return; mAccessibilityEnabled = true; if (mAccessibilityNodeProvider == null) { - mAccessibilityNodeProvider = new AccessibilityBridge(this, accessibilityChannel); + mAccessibilityNodeProvider = new AccessibilityBridge(this); } mNativeView.getFlutterJNI().setSemanticsEnabled(true); mAccessibilityNodeProvider.setAccessibilityEnabled(true);