Revert "Android embedding refactor pr3 add remaining systemchannels (#7874)" (flutter/engine#7886)

This reverts commit 08a8b11065.
This commit is contained in:
Dan Field
2019-02-20 11:18:12 -08:00
committed by GitHub
parent fa05d1e39c
commit a9c8a6c06a
11 changed files with 572 additions and 1581 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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.
* <p>
* 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<Object> channel;
@Nullable
private AccessibilityMessageHandler handler;
private final BasicMessageChannel.MessageHandler<Object> parsingMessageHandler = new BasicMessageChannel.MessageHandler<Object>() {
@Override
public void onMessage(Object message, BasicMessageChannel.Reply<Object> 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<String, Object> annotatedEvent = (HashMap<String, Object>) message;
final String type = (String) annotatedEvent.get("type");
@SuppressWarnings("unchecked")
final HashMap<String, Object> data = (HashMap<String, Object>) 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);
}
}

View File

@@ -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<Locale> locales) {
List<String> 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);
}
}

View File

@@ -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<SystemUiOverlay> 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<SystemUiOverlay> decodeSystemUiOverlays(@NonNull JSONArray encodedSystemUiOverlay) throws JSONException, NoSuchFieldException {
List<SystemUiOverlay> 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}.
* <p>
* 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}.
* <p>
* {@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.
* <p>
* An empty list of {@code overlays} should hide all system overlays.
*/
void showSystemOverlays(@NonNull List<SystemUiOverlay> overlays);
/**
* The Flutter application would like to restore the visibility of system
* overlays to the last set of overlays sent via {@link #showSystemOverlays(List)}.
* <p>
* If {@link #showSystemOverlays(List)} has yet to be called, then a default
* system overlay appearance is desired:
* <p>
* {@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.
* <p>
* 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;
}
}
}

View File

@@ -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.
* <p>
* When the user presses an action button like "done" or "next", that action is sent from Android
* to Flutter through this {@link TextInputChannel}.
* <p>
* 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}.
* <p>
* {@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<Object, Object> 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;
}
}
}

View File

@@ -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<Object, Object> 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;

View File

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

View File

@@ -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<PlatformChannel.SystemUiOverlay> 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<PlatformChannel.SystemUiOverlay> 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);
}

View File

@@ -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<Object> {
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<Integer, SemanticsObject> objects;
private Map<Integer, CustomAccessibilityAction> customAccessibilityActions;
private boolean accessibilityEnabled = false;
private SemanticsObject a11yFocusedObject;
private SemanticsObject inputFocusedObject;
private SemanticsObject hoveredObject;
private Map<Integer, SemanticsObject> mObjects;
private Map<Integer, CustomAccessibilityAction> 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<Integer> 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<Object> 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<Map.Entry<Integer, SemanticsObject>> it = objects.entrySet().iterator();
Iterator<Map.Entry<Integer, SemanticsObject>> it = mObjects.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, SemanticsObject> 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<Object> reply) {
@SuppressWarnings("unchecked")
final HashMap<String, Object> annotatedEvent = (HashMap<String, Object>) message;
final String type = (String) annotatedEvent.get("type");
@SuppressWarnings("unchecked")
final HashMap<String, Object> data = (HashMap<String, Object>) 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);
}

View File

@@ -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<ActivityLifecycleListener> mActivityLifecycleListeners;
private final List<FirstFrameListener> 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<Locale> 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<String> 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);