• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.plugin.editing;
6 
7 import android.content.Context;
8 import android.support.annotation.NonNull;
9 import android.support.annotation.Nullable;
10 import android.text.Editable;
11 import android.text.InputType;
12 import android.text.Selection;
13 import android.view.View;
14 import android.view.inputmethod.BaseInputConnection;
15 import android.view.inputmethod.EditorInfo;
16 import android.view.inputmethod.InputConnection;
17 import android.view.inputmethod.InputMethodManager;
18 
19 import io.flutter.embedding.engine.dart.DartExecutor;
20 import io.flutter.embedding.engine.systemchannels.TextInputChannel;
21 import io.flutter.plugin.platform.PlatformViewsController;
22 
23 /**
24  * Android implementation of the text input plugin.
25  */
26 public class TextInputPlugin {
27     @NonNull
28     private final View mView;
29     @NonNull
30     private final InputMethodManager mImm;
31     @NonNull
32     private final TextInputChannel textInputChannel;
33     @NonNull
34     private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
35     @Nullable
36     private TextInputChannel.Configuration configuration;
37     @Nullable
38     private Editable mEditable;
39     private boolean mRestartInputPending;
40     @Nullable
41     private InputConnection lastInputConnection;
42     @NonNull
43     private PlatformViewsController platformViewsController;
44 
45     // When true following calls to createInputConnection will return the cached lastInputConnection if the input
46     // target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
47     private boolean isInputConnectionLocked;
48 
TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, @NonNull PlatformViewsController platformViewsController)49     public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, @NonNull PlatformViewsController platformViewsController) {
50         mView = view;
51         mImm = (InputMethodManager) view.getContext().getSystemService(
52                 Context.INPUT_METHOD_SERVICE);
53 
54         textInputChannel = new TextInputChannel(dartExecutor);
55         textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() {
56             @Override
57             public void show() {
58                 showTextInput(mView);
59             }
60 
61             @Override
62             public void hide() {
63                 hideTextInput(mView);
64             }
65 
66             @Override
67             public void setClient(int textInputClientId, TextInputChannel.Configuration configuration) {
68                 setTextInputClient(textInputClientId, configuration);
69             }
70 
71             @Override
72             public void setPlatformViewClient(int platformViewId) {
73                 setPlatformViewTextInputClient(platformViewId);
74             }
75 
76             @Override
77             public void setEditingState(TextInputChannel.TextEditState editingState) {
78                 setTextInputEditingState(mView, editingState);
79             }
80 
81             @Override
82             public void clearClient() {
83                 clearTextInputClient();
84             }
85         });
86 
87         this.platformViewsController = platformViewsController;
88         this.platformViewsController.attachTextInputPlugin(this);
89     }
90 
91     @NonNull
getInputMethodManager()92     public InputMethodManager getInputMethodManager() {
93         return mImm;
94     }
95 
96     /***
97      * Use the current platform view input connection until unlockPlatformViewInputConnection is called.
98      *
99      * The current input connection instance is cached and any following call to @{link createInputConnection} returns
100      * the cached connection until unlockPlatformViewInputConnection is called.
101      *
102      * This is a no-op if the current input target isn't a platform view.
103      *
104      * This is used to preserve an input connection when moving a platform view from one virtual display to another.
105      */
lockPlatformViewInputConnection()106     public void lockPlatformViewInputConnection() {
107         if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
108             isInputConnectionLocked = true;
109         }
110     }
111 
112     /**
113      * Unlocks the input connection.
114      *
115      * See also: @{link lockPlatformViewInputConnection}.
116      */
unlockPlatformViewInputConnection()117     public void unlockPlatformViewInputConnection() {
118         isInputConnectionLocked = false;
119     }
120 
121     /**
122      * Detaches the text input plugin from the platform views controller.
123      *
124      * The TextInputPlugin instance should not be used after calling this.
125      */
destroy()126     public void destroy() {
127         platformViewsController.detachTextInputPlugin();
128     }
129 
inputTypeFromTextInputType( TextInputChannel.InputType type, boolean obscureText, boolean autocorrect, TextInputChannel.TextCapitalization textCapitalization )130     private static int inputTypeFromTextInputType(
131         TextInputChannel.InputType type,
132         boolean obscureText,
133         boolean autocorrect,
134         TextInputChannel.TextCapitalization textCapitalization
135     ) {
136         if (type.type == TextInputChannel.TextInputType.DATETIME) {
137             return InputType.TYPE_CLASS_DATETIME;
138         } else if (type.type == TextInputChannel.TextInputType.NUMBER) {
139             int textType = InputType.TYPE_CLASS_NUMBER;
140             if (type.isSigned) {
141                 textType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
142             }
143             if (type.isDecimal) {
144                 textType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
145             }
146             return textType;
147         } else if (type.type == TextInputChannel.TextInputType.PHONE) {
148             return InputType.TYPE_CLASS_PHONE;
149         }
150 
151         int textType = InputType.TYPE_CLASS_TEXT;
152         if (type.type == TextInputChannel.TextInputType.MULTILINE) {
153             textType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
154         } else if (type.type == TextInputChannel.TextInputType.EMAIL_ADDRESS) {
155             textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
156         } else if (type.type == TextInputChannel.TextInputType.URL) {
157             textType |= InputType.TYPE_TEXT_VARIATION_URI;
158         } else if (type.type == TextInputChannel.TextInputType.VISIBLE_PASSWORD) {
159             textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
160         }
161 
162         if (obscureText) {
163             // Note: both required. Some devices ignore TYPE_TEXT_FLAG_NO_SUGGESTIONS.
164             textType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
165             textType |= InputType.TYPE_TEXT_VARIATION_PASSWORD;
166         } else {
167             if (autocorrect) textType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
168         }
169 
170         if (textCapitalization == TextInputChannel.TextCapitalization.CHARACTERS) {
171             textType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
172         } else if (textCapitalization == TextInputChannel.TextCapitalization.WORDS) {
173             textType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
174         } else if (textCapitalization == TextInputChannel.TextCapitalization.SENTENCES) {
175             textType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
176         }
177 
178         return textType;
179     }
180 
createInputConnection(View view, EditorInfo outAttrs)181     public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
182         if (inputTarget.type == InputTarget.Type.NO_TARGET) {
183             lastInputConnection = null;
184             return null;
185         }
186 
187         if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
188             if (isInputConnectionLocked) {
189                 return lastInputConnection;
190             }
191             lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs);
192             return lastInputConnection;
193         }
194 
195         outAttrs.inputType = inputTypeFromTextInputType(
196             configuration.inputType,
197             configuration.obscureText,
198             configuration.autocorrect,
199             configuration.textCapitalization
200         );
201         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
202         int enterAction;
203         if (configuration.inputAction == null) {
204             // If an explicit input action isn't set, then default to none for multi-line fields
205             // and done for single line fields.
206             enterAction = (InputType.TYPE_TEXT_FLAG_MULTI_LINE & outAttrs.inputType) != 0
207                     ? EditorInfo.IME_ACTION_NONE
208                     : EditorInfo.IME_ACTION_DONE;
209         } else {
210             enterAction = configuration.inputAction;
211         }
212         if (configuration.actionLabel != null) {
213             outAttrs.actionLabel = configuration.actionLabel;
214             outAttrs.actionId = enterAction;
215         }
216         outAttrs.imeOptions |= enterAction;
217 
218         InputConnectionAdaptor connection = new InputConnectionAdaptor(
219             view,
220             inputTarget.id,
221             textInputChannel,
222             mEditable
223         );
224         outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
225         outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
226 
227         lastInputConnection = connection;
228         return lastInputConnection;
229     }
230 
231     @Nullable
getLastInputConnection()232     public InputConnection getLastInputConnection() {
233         return lastInputConnection;
234     }
235 
236     /**
237      * Clears a platform view text input client if it is the current input target.
238      *
239      * This is called when a platform view is disposed to make sure we're not hanging to a stale input
240      * connection.
241      */
clearPlatformViewClient(int platformViewId)242     public void clearPlatformViewClient(int platformViewId) {
243         if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
244             inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
245             hideTextInput(mView);
246             mImm.restartInput(mView);
247             mRestartInputPending = false;
248         }
249     }
250 
showTextInput(View view)251     private void showTextInput(View view) {
252         view.requestFocus();
253         mImm.showSoftInput(view, 0);
254     }
255 
hideTextInput(View view)256     private void hideTextInput(View view) {
257         // Note: a race condition may lead to us hiding the keyboard here just after a platform view has shown it.
258         // This can only potentially happen when switching focus from a Flutter text field to a platform view's text
259         // field(by text field here I mean anything that keeps the keyboard open).
260         // See: https://github.com/flutter/flutter/issues/34169
261         mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
262     }
263 
setTextInputClient(int client, TextInputChannel.Configuration configuration)264     private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
265         inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
266         this.configuration = configuration;
267         mEditable = Editable.Factory.getInstance().newEditable("");
268 
269         // setTextInputClient will be followed by a call to setTextInputEditingState.
270         // Do a restartInput at that time.
271         mRestartInputPending = true;
272         unlockPlatformViewInputConnection();
273     }
274 
setPlatformViewTextInputClient(int platformViewId)275     private void setPlatformViewTextInputClient(int platformViewId) {
276         // We need to make sure that the Flutter view is focused so that no imm operations get short circuited.
277         // Not asking for focus here specifically manifested in a but on API 28 devices where the platform view's
278         // request to show a keyboard was ignored.
279         mView.requestFocus();
280         inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
281         mImm.restartInput(mView);
282         mRestartInputPending = false;
283     }
284 
applyStateToSelection(TextInputChannel.TextEditState state)285     private void applyStateToSelection(TextInputChannel.TextEditState state) {
286         int selStart = state.selectionStart;
287         int selEnd = state.selectionEnd;
288         if (selStart >= 0 && selStart <= mEditable.length() && selEnd >= 0
289                 && selEnd <= mEditable.length()) {
290             Selection.setSelection(mEditable, selStart, selEnd);
291         } else {
292             Selection.removeSelection(mEditable);
293         }
294     }
295 
setTextInputEditingState(View view, TextInputChannel.TextEditState state)296     private void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
297         if (!mRestartInputPending && state.text.equals(mEditable.toString())) {
298             applyStateToSelection(state);
299             mImm.updateSelection(mView, Math.max(Selection.getSelectionStart(mEditable), 0),
300                     Math.max(Selection.getSelectionEnd(mEditable), 0),
301                     BaseInputConnection.getComposingSpanStart(mEditable),
302                     BaseInputConnection.getComposingSpanEnd(mEditable));
303         } else {
304             mEditable.replace(0, mEditable.length(), state.text);
305             applyStateToSelection(state);
306             mImm.restartInput(view);
307             mRestartInputPending = false;
308         }
309     }
310 
clearTextInputClient()311     private void clearTextInputClient() {
312         if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
313             // Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node
314             // that lost focus may be notified before or after a node that gained focus.
315             // When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text
316             // field's focus node will be notified that it lost focus after the AndroidView was notified that it gained
317             // focus. When this happens the text field will send a clearTextInput command which we ignore.
318             // By doing this we prevent the framework from clearing a platform view input client(the only way to do so
319             // is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views
320             // text input client, and it may be error prone as we don't know how the platform view manages the input
321             // connection and we probably shouldn't interfere.
322             // If we ever want to allow the framework to clear a platform view text client we should probably consider
323             // changing the focus manager such that focus nodes that lost focus are notified before focus nodes that
324             // gained focus as part of the same focus event.
325             return;
326         }
327         inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
328         unlockPlatformViewInputConnection();
329     }
330 
331     static private class InputTarget {
332         enum Type {
333             NO_TARGET,
334             // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework.
335             FRAMEWORK_CLIENT,
336             // InputConnection is managed by an embedded platform view.
337             PLATFORM_VIEW
338         }
339 
InputTarget(@onNull Type type, int id)340         public InputTarget(@NonNull Type type, int id) {
341             this.type = type;
342             this.id = id;
343         }
344 
345         @NonNull
346         Type type;
347         // The ID of the input target.
348         //
349         // For framework clients this is the framework input connection client ID.
350         // For platform views this is the platform view's ID.
351         int id;
352     }
353 }
354