• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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