1 package io.flutter.embedding.engine.systemchannels; 2 3 import android.support.annotation.NonNull; 4 import android.support.annotation.Nullable; 5 import android.view.inputmethod.EditorInfo; 6 7 import org.json.JSONArray; 8 import org.json.JSONException; 9 import org.json.JSONObject; 10 11 import java.util.Arrays; 12 import java.util.HashMap; 13 14 import io.flutter.Log; 15 import io.flutter.embedding.engine.dart.DartExecutor; 16 import io.flutter.plugin.common.JSONMethodCodec; 17 import io.flutter.plugin.common.MethodCall; 18 import io.flutter.plugin.common.MethodChannel; 19 20 /** 21 * {@link TextInputChannel} is a platform channel between Android and Flutter that is used to 22 * communicate information about the user's text input. 23 * <p> 24 * When the user presses an action button like "done" or "next", that action is sent from Android 25 * to Flutter through this {@link TextInputChannel}. 26 * <p> 27 * When an input system in the Flutter app wants to show the keyboard, or hide it, or configure 28 * editing state, etc. a message is sent from Flutter to Android through this {@link TextInputChannel}. 29 * <p> 30 * {@link TextInputChannel} comes with a default {@link io.flutter.plugin.common.MethodChannel.MethodCallHandler} 31 * that parses incoming messages from Flutter. Register a {@link TextInputMethodHandler} to respond 32 * to standard Flutter text input messages. 33 */ 34 public class TextInputChannel { 35 private static final String TAG = "TextInputChannel"; 36 37 @NonNull 38 public final MethodChannel channel; 39 @Nullable 40 private TextInputMethodHandler textInputMethodHandler; 41 42 private final MethodChannel.MethodCallHandler parsingMethodHandler = new MethodChannel.MethodCallHandler() { 43 @Override 44 public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { 45 if (textInputMethodHandler == null) { 46 // If no explicit TextInputMethodHandler has been registered then we don't 47 // need to forward this call to an API. Return. 48 return; 49 } 50 51 String method = call.method; 52 Object args = call.arguments; 53 Log.v(TAG, "Received '" + method + "' message."); 54 switch (method) { 55 case "TextInput.show": 56 textInputMethodHandler.show(); 57 result.success(null); 58 break; 59 case "TextInput.hide": 60 textInputMethodHandler.hide(); 61 result.success(null); 62 break; 63 case "TextInput.setClient": 64 try { 65 final JSONArray argumentList = (JSONArray) args; 66 final int textInputClientId = argumentList.getInt(0); 67 final JSONObject jsonConfiguration = argumentList.getJSONObject(1); 68 textInputMethodHandler.setClient(textInputClientId, Configuration.fromJson(jsonConfiguration)); 69 result.success(null); 70 } catch (JSONException | NoSuchFieldException exception) { 71 // JSONException: missing keys or bad value types. 72 // NoSuchFieldException: one or more values were invalid. 73 result.error("error", exception.getMessage(), null); 74 } 75 break; 76 case "TextInput.setPlatformViewClient": 77 final int id = (int) args; 78 textInputMethodHandler.setPlatformViewClient(id); 79 break; 80 case "TextInput.setEditingState": 81 try { 82 final JSONObject editingState = (JSONObject) args; 83 textInputMethodHandler.setEditingState(TextEditState.fromJson(editingState)); 84 result.success(null); 85 } catch (JSONException exception) { 86 result.error("error", exception.getMessage(), null); 87 } 88 break; 89 case "TextInput.clearClient": 90 textInputMethodHandler.clearClient(); 91 result.success(null); 92 break; 93 default: 94 result.notImplemented(); 95 break; 96 } 97 } 98 }; 99 100 /** 101 * Constructs a {@code TextInputChannel} that connects Android to the Dart code 102 * running in {@code dartExecutor}. 103 * 104 * The given {@code dartExecutor} is permitted to be idle or executing code. 105 * 106 * See {@link DartExecutor}. 107 */ TextInputChannel(@onNull DartExecutor dartExecutor)108 public TextInputChannel(@NonNull DartExecutor dartExecutor) { 109 this.channel = new MethodChannel(dartExecutor, "flutter/textinput", JSONMethodCodec.INSTANCE); 110 channel.setMethodCallHandler(parsingMethodHandler); 111 } 112 113 /** 114 * Instructs Flutter to update its text input editing state to reflect the given configuration. 115 */ updateEditingState(int inputClientId, String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd)116 public void updateEditingState(int inputClientId, String text, int selectionStart, int selectionEnd, int composingStart, int composingEnd) { 117 Log.v(TAG, "Sending message to update editing state: \n" 118 + "Text: " + text + "\n" 119 + "Selection start: " + selectionStart + "\n" 120 + "Selection end: " + selectionEnd + "\n" 121 + "Composing start: " + composingStart + "\n" 122 + "Composing end: " + composingEnd); 123 124 HashMap<Object, Object> state = new HashMap<>(); 125 state.put("text", text); 126 state.put("selectionBase", selectionStart); 127 state.put("selectionExtent", selectionEnd); 128 state.put("composingBase", composingStart); 129 state.put("composingExtent", composingEnd); 130 131 channel.invokeMethod( 132 "TextInputClient.updateEditingState", 133 Arrays.asList(inputClientId, state) 134 ); 135 } 136 137 /** 138 * Instructs Flutter to execute a "newline" action. 139 */ newline(int inputClientId)140 public void newline(int inputClientId) { 141 Log.v(TAG, "Sending 'newline' message."); 142 channel.invokeMethod( 143 "TextInputClient.performAction", 144 Arrays.asList(inputClientId, "TextInputAction.newline") 145 ); 146 } 147 148 /** 149 * Instructs Flutter to execute a "go" action. 150 */ go(int inputClientId)151 public void go(int inputClientId) { 152 Log.v(TAG, "Sending 'go' message."); 153 channel.invokeMethod( 154 "TextInputClient.performAction", 155 Arrays.asList(inputClientId, "TextInputAction.go") 156 ); 157 } 158 159 /** 160 * Instructs Flutter to execute a "search" action. 161 */ search(int inputClientId)162 public void search(int inputClientId) { 163 Log.v(TAG, "Sending 'search' message."); 164 channel.invokeMethod( 165 "TextInputClient.performAction", 166 Arrays.asList(inputClientId, "TextInputAction.search") 167 ); 168 } 169 170 /** 171 * Instructs Flutter to execute a "send" action. 172 */ send(int inputClientId)173 public void send(int inputClientId) { 174 Log.v(TAG, "Sending 'send' message."); 175 channel.invokeMethod( 176 "TextInputClient.performAction", 177 Arrays.asList(inputClientId, "TextInputAction.send") 178 ); 179 } 180 181 /** 182 * Instructs Flutter to execute a "done" action. 183 */ done(int inputClientId)184 public void done(int inputClientId) { 185 Log.v(TAG, "Sending 'done' message."); 186 channel.invokeMethod( 187 "TextInputClient.performAction", 188 Arrays.asList(inputClientId, "TextInputAction.done") 189 ); 190 } 191 192 /** 193 * Instructs Flutter to execute a "next" action. 194 */ next(int inputClientId)195 public void next(int inputClientId) { 196 Log.v(TAG, "Sending 'next' message."); 197 channel.invokeMethod( 198 "TextInputClient.performAction", 199 Arrays.asList(inputClientId, "TextInputAction.next") 200 ); 201 } 202 203 /** 204 * Instructs Flutter to execute a "previous" action. 205 */ previous(int inputClientId)206 public void previous(int inputClientId) { 207 Log.v(TAG, "Sending 'previous' message."); 208 channel.invokeMethod( 209 "TextInputClient.performAction", 210 Arrays.asList(inputClientId, "TextInputAction.previous") 211 ); 212 } 213 214 /** 215 * Instructs Flutter to execute an "unspecified" action. 216 */ unspecifiedAction(int inputClientId)217 public void unspecifiedAction(int inputClientId) { 218 Log.v(TAG, "Sending 'unspecified' message."); 219 channel.invokeMethod( 220 "TextInputClient.performAction", 221 Arrays.asList(inputClientId, "TextInputAction.unspecified") 222 ); 223 } 224 225 /** 226 * Sets the {@link TextInputMethodHandler} which receives all events and requests 227 * that are parsed from the underlying platform channel. 228 */ setTextInputMethodHandler(@ullable TextInputMethodHandler textInputMethodHandler)229 public void setTextInputMethodHandler(@Nullable TextInputMethodHandler textInputMethodHandler) { 230 this.textInputMethodHandler = textInputMethodHandler; 231 } 232 233 public interface TextInputMethodHandler { 234 // TODO(mattcarroll): javadoc show()235 void show(); 236 237 // TODO(mattcarroll): javadoc hide()238 void hide(); 239 240 // TODO(mattcarroll): javadoc setClient(int textInputClientId, @NonNull Configuration configuration)241 void setClient(int textInputClientId, @NonNull Configuration configuration); 242 243 /** 244 * Sets a platform view as the text input client. 245 * 246 * Subsequent calls to createInputConnection will be delegated to the platform view until a 247 * different client is set. 248 * 249 * @param id the ID of the platform view to be set as a text input client. 250 */ setPlatformViewClient(int id)251 void setPlatformViewClient(int id); 252 253 // TODO(mattcarroll): javadoc setEditingState(@onNull TextEditState editingState)254 void setEditingState(@NonNull TextEditState editingState); 255 256 // TODO(mattcarroll): javadoc clearClient()257 void clearClient(); 258 } 259 260 /** 261 * A text editing configuration. 262 */ 263 public static class Configuration { fromJson(@onNull JSONObject json)264 public static Configuration fromJson(@NonNull JSONObject json) throws JSONException, NoSuchFieldException { 265 final String inputActionName = json.getString("inputAction"); 266 if (inputActionName == null) { 267 throw new JSONException("Configuration JSON missing 'inputAction' property."); 268 } 269 270 final Integer inputAction = inputActionFromTextInputAction(inputActionName); 271 return new Configuration( 272 json.optBoolean("obscureText"), 273 json.optBoolean("autocorrect", true), 274 TextCapitalization.fromValue(json.getString("textCapitalization")), 275 InputType.fromJson(json.getJSONObject("inputType")), 276 inputAction, 277 json.isNull("actionLabel") ? null : json.getString("actionLabel") 278 ); 279 } 280 281 @NonNull inputActionFromTextInputAction(@onNull String inputAction)282 private static Integer inputActionFromTextInputAction(@NonNull String inputAction) { 283 switch (inputAction) { 284 case "TextInputAction.newline": 285 return EditorInfo.IME_ACTION_NONE; 286 case "TextInputAction.none": 287 return EditorInfo.IME_ACTION_NONE; 288 case "TextInputAction.unspecified": 289 return EditorInfo.IME_ACTION_UNSPECIFIED; 290 case "TextInputAction.done": 291 return EditorInfo.IME_ACTION_DONE; 292 case "TextInputAction.go": 293 return EditorInfo.IME_ACTION_GO; 294 case "TextInputAction.search": 295 return EditorInfo.IME_ACTION_SEARCH; 296 case "TextInputAction.send": 297 return EditorInfo.IME_ACTION_SEND; 298 case "TextInputAction.next": 299 return EditorInfo.IME_ACTION_NEXT; 300 case "TextInputAction.previous": 301 return EditorInfo.IME_ACTION_PREVIOUS; 302 default: 303 // Present default key if bad input type is given. 304 return EditorInfo.IME_ACTION_UNSPECIFIED; 305 } 306 } 307 308 public final boolean obscureText; 309 public final boolean autocorrect; 310 @NonNull 311 public final TextCapitalization textCapitalization; 312 @NonNull 313 public final InputType inputType; 314 @Nullable 315 public final Integer inputAction; 316 @Nullable 317 public final String actionLabel; 318 Configuration( boolean obscureText, boolean autocorrect, @NonNull TextCapitalization textCapitalization, @NonNull InputType inputType, @Nullable Integer inputAction, @Nullable String actionLabel )319 public Configuration( 320 boolean obscureText, 321 boolean autocorrect, 322 @NonNull TextCapitalization textCapitalization, 323 @NonNull InputType inputType, 324 @Nullable Integer inputAction, 325 @Nullable String actionLabel 326 ) { 327 this.obscureText = obscureText; 328 this.autocorrect = autocorrect; 329 this.textCapitalization = textCapitalization; 330 this.inputType = inputType; 331 this.inputAction = inputAction; 332 this.actionLabel = actionLabel; 333 } 334 } 335 336 /** 337 * A text input type. 338 * 339 * If the {@link #type} is {@link TextInputType#NUMBER}, this {@code InputType} also 340 * reports whether that number {@link #isSigned} and {@link #isDecimal}. 341 */ 342 public static class InputType { 343 @NonNull fromJson(@onNull JSONObject json)344 public static InputType fromJson(@NonNull JSONObject json) throws JSONException, NoSuchFieldException { 345 return new InputType( 346 TextInputType.fromValue(json.getString("name")), 347 json.optBoolean("signed", false), 348 json.optBoolean("decimal", false) 349 ); 350 } 351 352 @NonNull 353 public final TextInputType type; 354 public final boolean isSigned; 355 public final boolean isDecimal; 356 InputType(@onNull TextInputType type, boolean isSigned, boolean isDecimal)357 public InputType(@NonNull TextInputType type, boolean isSigned, boolean isDecimal) { 358 this.type = type; 359 this.isSigned = isSigned; 360 this.isDecimal = isDecimal; 361 } 362 } 363 364 /** 365 * Types of text input. 366 */ 367 public enum TextInputType { 368 TEXT("TextInputType.text"), 369 DATETIME("TextInputType.datetime"), 370 NUMBER("TextInputType.number"), 371 PHONE("TextInputType.phone"), 372 MULTILINE("TextInputType.multiline"), 373 EMAIL_ADDRESS("TextInputType.emailAddress"), 374 URL("TextInputType.url"), 375 VISIBLE_PASSWORD("TextInputType.visiblePassword"); 376 fromValue(@onNull String encodedName)377 static TextInputType fromValue(@NonNull String encodedName) throws NoSuchFieldException { 378 for (TextInputType textInputType : TextInputType.values()) { 379 if (textInputType.encodedName.equals(encodedName)) { 380 return textInputType; 381 } 382 } 383 throw new NoSuchFieldException("No such TextInputType: " + encodedName); 384 } 385 386 @NonNull 387 private final String encodedName; 388 TextInputType(@onNull String encodedName)389 TextInputType(@NonNull String encodedName) { 390 this.encodedName = encodedName; 391 } 392 } 393 394 /** 395 * Text capitalization schemes. 396 */ 397 public enum TextCapitalization { 398 CHARACTERS("TextCapitalization.characters"), 399 WORDS("TextCapitalization.words"), 400 SENTENCES("TextCapitalization.sentences"), 401 NONE("TextCapitalization.none"); 402 fromValue(@onNull String encodedName)403 static TextCapitalization fromValue(@NonNull String encodedName) throws NoSuchFieldException { 404 for (TextCapitalization textCapitalization : TextCapitalization.values()) { 405 if (textCapitalization.encodedName.equals(encodedName)) { 406 return textCapitalization; 407 } 408 } 409 throw new NoSuchFieldException("No such TextCapitalization: " + encodedName); 410 } 411 412 @NonNull 413 private final String encodedName; 414 TextCapitalization(@onNull String encodedName)415 TextCapitalization(@NonNull String encodedName) { 416 this.encodedName = encodedName; 417 } 418 } 419 420 /** 421 * State of an on-going text editing session. 422 */ 423 public static class TextEditState { fromJson(@onNull JSONObject textEditState)424 public static TextEditState fromJson(@NonNull JSONObject textEditState) throws JSONException { 425 return new TextEditState( 426 textEditState.getString("text"), 427 textEditState.getInt("selectionBase"), 428 textEditState.getInt("selectionExtent") 429 ); 430 } 431 432 @NonNull 433 public final String text; 434 public final int selectionStart; 435 public final int selectionEnd; 436 TextEditState(@onNull String text, int selectionStart, int selectionEnd)437 public TextEditState(@NonNull String text, int selectionStart, int selectionEnd) { 438 this.text = text; 439 this.selectionStart = selectionStart; 440 this.selectionEnd = selectionEnd; 441 } 442 } 443 } 444