Revert "Android embedding refactor pr3 add remaining systemchannels (#7874)" (flutter/engine#7886)
This reverts commit 08a8b11065.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user