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