未验证 提交 256db4bc 编写于 作者: M Matt Carroll 提交者: GitHub

Android embedding refactor pr3 add remaining systemchannels (#7738)

上级 02d7ca31
......@@ -446,6 +446,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/D
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
......@@ -453,6 +454,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java
......
......@@ -114,6 +114,7 @@ java_library("flutter_shell_java") {
"io/flutter/embedding/engine/dart/PlatformMessageHandler.java",
"io/flutter/embedding/engine/renderer/FlutterRenderer.java",
"io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java",
"io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java",
"io/flutter/embedding/engine/systemchannels/KeyEventChannel.java",
"io/flutter/embedding/engine/systemchannels/LifecycleChannel.java",
"io/flutter/embedding/engine/systemchannels/LocalizationChannel.java",
......@@ -121,6 +122,7 @@ java_library("flutter_shell_java") {
"io/flutter/embedding/engine/systemchannels/PlatformChannel.java",
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
"io/flutter/embedding/engine/systemchannels/SystemChannel.java",
"io/flutter/embedding/engine/systemchannels/TextInputChannel.java",
"io/flutter/plugin/common/ActivityLifecycleListener.java",
"io/flutter/plugin/common/BasicMessageChannel.java",
"io/flutter/plugin/common/BinaryCodec.java",
......
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);
}
}
......@@ -6,14 +6,17 @@ package io.flutter.embedding.engine.systemchannels;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodChannel;
/**
* TODO(mattcarroll): fill in javadoc for LocalizationChannel.
* Sends the platform's locales to Dart.
*/
public class LocalizationChannel {
......@@ -24,12 +27,18 @@ public class LocalizationChannel {
this.channel = new MethodChannel(dartExecutor, "flutter/localization", JSONMethodCodec.INSTANCE);
}
public void setLocale(String language, String country) {
channel.invokeMethod("setLocale", Arrays.asList(language, country));
}
public void setMethodCallHandler(MethodChannel.MethodCallHandler handler) {
channel.setMethodCallHandler(handler);
/**
* Send the given {@code locales} to Dart.
*/
public void sendLocales(List<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);
}
}
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;
}
}
}
......@@ -11,17 +11,16 @@ import android.view.KeyEvent;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.ErrorLogResult;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterView;
import java.util.Arrays;
import java.util.HashMap;
class InputConnectionAdaptor extends BaseInputConnection {
private final FlutterView mFlutterView;
private final int mClient;
private final MethodChannel mFlutterChannel;
private final TextInputChannel textInputChannel;
private final Editable mEditable;
private int mBatchCount;
private InputMethodManager mImm;
......@@ -29,12 +28,16 @@ class InputConnectionAdaptor extends BaseInputConnection {
private static final MethodChannel.Result logger =
new ErrorLogResult("FlutterTextInput");
public InputConnectionAdaptor(FlutterView view, int client,
MethodChannel flutterChannel, Editable editable) {
public InputConnectionAdaptor(
FlutterView view,
int client,
TextInputChannel textInputChannel,
Editable editable
) {
super(view, true);
mFlutterView = view;
mClient = client;
mFlutterChannel = flutterChannel;
this.textInputChannel = textInputChannel;
mEditable = editable;
mBatchCount = 0;
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
......@@ -55,14 +58,14 @@ class InputConnectionAdaptor extends BaseInputConnection {
selectionStart, selectionEnd,
composingStart, composingEnd);
HashMap<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);
textInputChannel.updateEditingState(
mClient,
mEditable.toString(),
selectionStart,
selectionEnd,
composingStart,
composingEnd
);
}
@Override
......@@ -178,39 +181,30 @@ class InputConnectionAdaptor extends BaseInputConnection {
@Override
public boolean performEditorAction(int actionCode) {
switch (actionCode) {
// TODO(mattcarroll): is newline an appropriate action for "none"?
case EditorInfo.IME_ACTION_NONE:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.newline"), logger);
textInputChannel.newline(mClient);
break;
case EditorInfo.IME_ACTION_UNSPECIFIED:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.unspecified"), logger);
textInputChannel.unspecifiedAction(mClient);
break;
case EditorInfo.IME_ACTION_GO:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.go"), logger);
textInputChannel.go(mClient);
break;
case EditorInfo.IME_ACTION_SEARCH:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.search"), logger);
textInputChannel.search(mClient);
break;
case EditorInfo.IME_ACTION_SEND:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.send"), logger);
textInputChannel.send(mClient);
break;
case EditorInfo.IME_ACTION_NEXT:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.next"), logger);
textInputChannel.next(mClient);
break;
case EditorInfo.IME_ACTION_PREVIOUS:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.previous"), logger);
textInputChannel.previous(mClient);
break;
default:
case EditorInfo.IME_ACTION_DONE:
mFlutterChannel.invokeMethod("TextInputClient.performAction",
Arrays.asList(mClient, "TextInputAction.done"), logger);
textInputChannel.done(mClient);
break;
}
return true;
......
......@@ -5,6 +5,7 @@
package io.flutter.plugin.editing;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
......@@ -12,84 +13,87 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.view.FlutterView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Android implementation of the text input plugin.
*/
public class TextInputPlugin implements MethodCallHandler {
public class TextInputPlugin {
private final FlutterView mView;
private final InputMethodManager mImm;
private final MethodChannel mFlutterChannel;
private final TextInputChannel textInputChannel;
private int mClient = 0;
private JSONObject mConfiguration;
private TextInputChannel.Configuration configuration;
private Editable mEditable;
private boolean mRestartInputPending;
public TextInputPlugin(FlutterView view) {
public TextInputPlugin(FlutterView view, @NonNull DartExecutor dartExecutor) {
mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
mFlutterChannel = new MethodChannel(view, "flutter/textinput", JSONMethodCodec.INSTANCE);
mFlutterChannel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall call, Result result) {
String method = call.method;
Object args = call.arguments;
try {
if (method.equals("TextInput.show")) {
textInputChannel = new TextInputChannel(dartExecutor);
textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() {
@Override
public void show() {
showTextInput(mView);
result.success(null);
} else if (method.equals("TextInput.hide")) {
}
@Override
public void hide() {
hideTextInput(mView);
result.success(null);
} else if (method.equals("TextInput.setClient")) {
final JSONArray argumentList = (JSONArray) args;
setTextInputClient(mView, argumentList.getInt(0), argumentList.getJSONObject(1));
result.success(null);
} else if (method.equals("TextInput.setEditingState")) {
setTextInputEditingState(mView, (JSONObject) args);
result.success(null);
} else if (method.equals("TextInput.clearClient")) {
}
@Override
public void setClient(int textInputClientId, TextInputChannel.Configuration configuration) {
setTextInputClient(textInputClientId, configuration);
}
@Override
public void setEditingState(TextInputChannel.TextEditState editingState) {
setTextInputEditingState(mView, editingState);
}
@Override
public void clearClient() {
clearTextInputClient();
result.success(null);
} else {
result.notImplemented();
}
} catch (JSONException e) {
result.error("error", "JSON error: " + e.getMessage(), null);
}
});
}
private static int inputTypeFromTextInputType(JSONObject type, boolean obscureText,
boolean autocorrect, String textCapitalization) throws JSONException {
String inputType = type.getString("name");
if (inputType.equals("TextInputType.datetime")) return InputType.TYPE_CLASS_DATETIME;
if (inputType.equals("TextInputType.number")) {
private static int inputTypeFromTextInputType(
TextInputChannel.InputType type,
boolean obscureText,
boolean autocorrect,
TextInputChannel.TextCapitalization textCapitalization
) {
if (type.type == TextInputChannel.TextInputType.DATETIME) {
return InputType.TYPE_CLASS_DATETIME;
} else if (type.type == TextInputChannel.TextInputType.NUMBER) {
int textType = InputType.TYPE_CLASS_NUMBER;
if (type.optBoolean("signed")) textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
if (type.optBoolean("decimal")) textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
if (type.isSigned) {
textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
}
if (type.isDecimal) {
textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
}
return textType;
} else if (type.type == TextInputChannel.TextInputType.PHONE) {
return InputType.TYPE_CLASS_PHONE;
}
if (inputType.equals("TextInputType.phone")) return InputType.TYPE_CLASS_PHONE;
int textType = InputType.TYPE_CLASS_TEXT;
if (inputType.equals("TextInputType.multiline"))
if (type.type == TextInputChannel.TextInputType.MULTILINE) {
textType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
else if (inputType.equals("TextInputType.emailAddress"))
} else if (type.type == TextInputChannel.TextInputType.EMAIL_ADDRESS) {
textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
else if (inputType.equals("TextInputType.url"))
} else if (type.type == TextInputChannel.TextInputType.URL) {
textType |= InputType.TYPE_TEXT_VARIATION_URI;
}
if (obscureText) {
// Note: both required. Some devices ignore TYPE_TEXT_FLAG_NO_SUGGESTIONS.
textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
......@@ -97,69 +101,50 @@ public class TextInputPlugin implements MethodCallHandler {
} else {
if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
}
if (textCapitalization.equals("TextCapitalization.characters")) {
if (textCapitalization == TextInputChannel.TextCapitalization.CHARACTERS) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
} else if (textCapitalization.equals("TextCapitalization.words")) {
} else if (textCapitalization == TextInputChannel.TextCapitalization.WORDS) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
} else if (textCapitalization.equals("TextCapitalization.sentences")) {
} else if (textCapitalization == TextInputChannel.TextCapitalization.SENTENCES) {
textType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
}
return textType;
}
private static int inputActionFromTextInputAction(String inputAction) {
switch (inputAction) {
case "TextInputAction.newline":
return EditorInfo.IME_ACTION_NONE;
case "TextInputAction.none":
return EditorInfo.IME_ACTION_NONE;
case "TextInputAction.unspecified":
return EditorInfo.IME_ACTION_UNSPECIFIED;
case "TextInputAction.done":
return EditorInfo.IME_ACTION_DONE;
case "TextInputAction.go":
return EditorInfo.IME_ACTION_GO;
case "TextInputAction.search":
return EditorInfo.IME_ACTION_SEARCH;
case "TextInputAction.send":
return EditorInfo.IME_ACTION_SEND;
case "TextInputAction.next":
return EditorInfo.IME_ACTION_NEXT;
case "TextInputAction.previous":
return EditorInfo.IME_ACTION_PREVIOUS;
default:
// Present default key if bad input type is given.
return EditorInfo.IME_ACTION_UNSPECIFIED;
}
return textType;
}
public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs)
throws JSONException {
public InputConnection createInputConnection(FlutterView view, EditorInfo outAttrs) {
if (mClient == 0) return null;
outAttrs.inputType = inputTypeFromTextInputType(mConfiguration.getJSONObject("inputType"),
mConfiguration.optBoolean("obscureText"),
mConfiguration.optBoolean("autocorrect", true),
mConfiguration.getString("textCapitalization"));
outAttrs.inputType = inputTypeFromTextInputType(
configuration.inputType,
configuration.obscureText,
configuration.autocorrect,
configuration.textCapitalization
);
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
int enterAction;
if (mConfiguration.isNull("inputAction")) {
if (configuration.inputAction == null) {
// If an explicit input action isn't set, then default to none for multi-line fields
// and done for single line fields.
enterAction = (InputType.TYPE_TEXT_FLAG_MULTI_LINE & outAttrs.inputType) != 0
? EditorInfo.IME_ACTION_NONE
: EditorInfo.IME_ACTION_DONE;
} else {
enterAction = inputActionFromTextInputAction(mConfiguration.getString("inputAction"));
enterAction = configuration.inputAction;
}
if (!mConfiguration.isNull("actionLabel")) {
outAttrs.actionLabel = mConfiguration.getString("actionLabel");
if (configuration.actionLabel != null) {
outAttrs.actionLabel = configuration.actionLabel;
outAttrs.actionId = enterAction;
}
outAttrs.imeOptions |= enterAction;
InputConnectionAdaptor connection =
new InputConnectionAdaptor(view, mClient, mFlutterChannel, mEditable);
InputConnectionAdaptor connection = new InputConnectionAdaptor(
view,
mClient,
textInputChannel,
mEditable
);
outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
......@@ -175,9 +160,9 @@ public class TextInputPlugin implements MethodCallHandler {
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
}
private void setTextInputClient(FlutterView view, int client, JSONObject configuration) {
private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
mClient = client;
mConfiguration = configuration;
this.configuration = configuration;
mEditable = Editable.Factory.getInstance().newEditable("");
// setTextInputClient will be followed by a call to setTextInputEditingState.
......@@ -185,9 +170,9 @@ public class TextInputPlugin implements MethodCallHandler {
mRestartInputPending = true;
}
private void applyStateToSelection(JSONObject state) throws JSONException {
int selStart = state.getInt("selectionBase");
int selEnd = state.getInt("selectionExtent");
private void applyStateToSelection(TextInputChannel.TextEditState state) {
int selStart = state.selectionStart;
int selEnd = state.selectionEnd;
if (selStart >= 0 && selStart <= mEditable.length() && selEnd >= 0
&& selEnd <= mEditable.length()) {
Selection.setSelection(mEditable, selStart, selEnd);
......@@ -196,15 +181,15 @@ public class TextInputPlugin implements MethodCallHandler {
}
}
private void setTextInputEditingState(FlutterView view, JSONObject state) throws JSONException {
if (!mRestartInputPending && state.getString("text").equals(mEditable.toString())) {
private void setTextInputEditingState(FlutterView view, TextInputChannel.TextEditState state) {
if (!mRestartInputPending && state.text.equals(mEditable.toString())) {
applyStateToSelection(state);
mImm.updateSelection(mView, Math.max(Selection.getSelectionStart(mEditable), 0),
Math.max(Selection.getSelectionEnd(mEditable), 0),
BaseInputConnection.getComposingSpanStart(mEditable),
BaseInputConnection.getComposingSpanEnd(mEditable));
} else {
mEditable.replace(0, mEditable.length(), state.getString("text"));
mEditable.replace(0, mEditable.length(), state.text);
applyStateToSelection(state);
mImm.restartInput(view);
mRestartInputPending = false;
......
......@@ -15,6 +15,7 @@ import android.graphics.SurfaceTexture;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.LocaleList;
import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.AttributeSet;
......@@ -29,17 +30,17 @@ import io.flutter.app.FlutterPluginRegistry;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.android.AndroidKeyProcessor;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.NavigationChannel;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.plugin.common.*;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformPlugin;
import org.json.JSONException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
......@@ -87,18 +88,21 @@ public class FlutterView extends SurfaceView
}
private final DartExecutor dartExecutor;
private final AccessibilityChannel accessibilityChannel;
private final NavigationChannel navigationChannel;
private final KeyEventChannel keyEventChannel;
private final LifecycleChannel lifecycleChannel;
private final LocalizationChannel localizationChannel;
private final PlatformChannel platformChannel;
private final SettingsChannel settingsChannel;
private final SystemChannel systemChannel;
private final InputMethodManager mImm;
private final TextInputPlugin mTextInputPlugin;
private final AndroidKeyProcessor androidKeyProcessor;
private AccessibilityBridge mAccessibilityNodeProvider;
private final SurfaceHolder.Callback mSurfaceCallback;
private final ViewportMetrics mMetrics;
private final AccessibilityManager mAccessibilityManager;
private final MethodChannel mFlutterLocalizationChannel;
private final List<ActivityLifecycleListener> mActivityLifecycleListeners;
private final List<FirstFrameListener> mFirstFrameListeners;
private final AtomicLong nextTextureId = new AtomicLong(0L);
......@@ -160,23 +164,25 @@ public class FlutterView extends SurfaceView
mActivityLifecycleListeners = new ArrayList<>();
mFirstFrameListeners = new ArrayList<>();
// Configure the platform plugins and flutter channels.
// Create all platform channels
accessibilityChannel = new AccessibilityChannel(dartExecutor);
navigationChannel = new NavigationChannel(dartExecutor);
keyEventChannel = new KeyEventChannel(dartExecutor);
lifecycleChannel = new LifecycleChannel(dartExecutor);
localizationChannel = new LocalizationChannel(dartExecutor);
platformChannel = new PlatformChannel(dartExecutor);
systemChannel = new SystemChannel(dartExecutor);
settingsChannel = new SettingsChannel(dartExecutor);
mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
PlatformPlugin platformPlugin = new PlatformPlugin(activity);
MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);
flutterPlatformChannel.setMethodCallHandler(platformPlugin);
// Create and setup plugins
PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel);
addActivityLifecycleListener(platformPlugin);
mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
mTextInputPlugin = new TextInputPlugin(this);
mTextInputPlugin = new TextInputPlugin(this, dartExecutor);
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel);
setLocales(getResources().getConfiguration());
// Send initial platform information to Dart
sendLocalesToDart(getResources().getConfiguration());
sendUserPlatformSettingsToDart();
}
......@@ -311,39 +317,21 @@ public class FlutterView extends SurfaceView
.send();
}
private void setLocales(Configuration config) {
if (Build.VERSION.SDK_INT >= 24) {
try {
// Passes the full list of locales for android API >= 24 with reflection.
Object localeList = config.getClass().getDeclaredMethod("getLocales").invoke(config);
Method localeListGet = localeList.getClass().getDeclaredMethod("get", int.class);
Method localeListSize = localeList.getClass().getDeclaredMethod("size");
int localeCount = (int)localeListSize.invoke(localeList);
List<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.
}
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);
}
// Fallback single locale passing for android API < 24. Should work always.
@SuppressWarnings("deprecation")
Locale locale = config.locale;
// getScript() is gated because it is added in API 21.
mFlutterLocalizationChannel.invokeMethod("setLocale", Arrays.asList(locale.getLanguage(), locale.getCountry(), Build.VERSION.SDK_INT >= 21 ? locale.getScript() : "", locale.getVariant()));
localizationChannel.sendLocales(locales);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setLocales(newConfig);
sendLocalesToDart(newConfig);
sendUserPlatformSettingsToDart();
}
......@@ -374,13 +362,7 @@ public class FlutterView extends SurfaceView
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
try {
mLastInputConnection = mTextInputPlugin.createInputConnection(this, outAttrs);
return mLastInputConnection;
} catch (JSONException e) {
Log.e(TAG, "Failed to create input connection", e);
return null;
}
return mTextInputPlugin.createInputConnection(this, outAttrs);
}
// Must match the PointerChange enum in pointer.dart.
......@@ -1006,14 +988,12 @@ public class FlutterView extends SurfaceView
return null;
}
private AccessibilityBridge mAccessibilityNodeProvider;
void ensureAccessibilityEnabled() {
if (!isAttached())
return;
mAccessibilityEnabled = true;
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new AccessibilityBridge(this);
mAccessibilityNodeProvider = new AccessibilityBridge(this, accessibilityChannel);
}
mNativeView.getFlutterJNI().setSemanticsEnabled(true);
mAccessibilityNodeProvider.setAccessibilityEnabled(true);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册