Flutter Engine
The Flutter Engine
InputConnectionAdaptor.java
Go to the documentation of this file.
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
5package io.flutter.plugin.editing;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.annotation.TargetApi;
10import android.content.ClipData;
11import android.content.ClipboardManager;
12import android.content.Context;
13import android.net.Uri;
14import android.os.Build;
15import android.os.Bundle;
16import android.text.DynamicLayout;
17import android.text.Editable;
18import android.text.InputType;
19import android.text.Layout;
20import android.text.Selection;
21import android.text.TextPaint;
22import android.view.KeyEvent;
23import android.view.View;
24import android.view.inputmethod.BaseInputConnection;
25import android.view.inputmethod.CursorAnchorInfo;
26import android.view.inputmethod.EditorInfo;
27import android.view.inputmethod.ExtractedText;
28import android.view.inputmethod.ExtractedTextRequest;
29import android.view.inputmethod.InputContentInfo;
30import android.view.inputmethod.InputMethodManager;
31import androidx.annotation.NonNull;
32import androidx.annotation.RequiresApi;
33import androidx.core.view.inputmethod.InputConnectionCompat;
34import io.flutter.Log;
35import io.flutter.embedding.engine.FlutterJNI;
36import io.flutter.embedding.engine.systemchannels.TextInputChannel;
37import java.io.ByteArrayOutputStream;
38import java.io.FileNotFoundException;
39import java.io.IOException;
40import java.io.InputStream;
41import java.util.HashMap;
42import java.util.Map;
43
44public class InputConnectionAdaptor extends BaseInputConnection
45 implements ListenableEditingState.EditingStateWatcher {
46 private static final String TAG = "InputConnectionAdaptor";
47
48 public interface KeyboardDelegate {
49 public boolean handleEvent(@NonNull KeyEvent keyEvent);
50 }
51
52 private final View mFlutterView;
53 private final int mClient;
54 private final TextInputChannel textInputChannel;
55 private final ListenableEditingState mEditable;
56 private final EditorInfo mEditorInfo;
57 private ExtractedTextRequest mExtractRequest;
58 private boolean mMonitorCursorUpdate = false;
59 private CursorAnchorInfo.Builder mCursorAnchorInfoBuilder;
60 private ExtractedText mExtractedText = new ExtractedText();
61 private InputMethodManager mImm;
62 private final Layout mLayout;
63 private FlutterTextUtils flutterTextUtils;
64 private final KeyboardDelegate keyboardDelegate;
65 private int batchEditNestDepth = 0;
66
67 @SuppressWarnings("deprecation")
69 View view,
70 int client,
71 TextInputChannel textInputChannel,
72 KeyboardDelegate keyboardDelegate,
74 EditorInfo editorInfo,
75 FlutterJNI flutterJNI) {
76 super(view, true);
77 mFlutterView = view;
78 mClient = client;
79 this.textInputChannel = textInputChannel;
80 mEditable = editable;
81 mEditable.addEditingStateListener(this);
82 mEditorInfo = editorInfo;
83 this.keyboardDelegate = keyboardDelegate;
84 this.flutterTextUtils = new FlutterTextUtils(flutterJNI);
85 // We create a dummy Layout with max width so that the selection
86 // shifting acts as if all text were in one line.
87 mLayout =
88 new DynamicLayout(
89 mEditable,
90 new TextPaint(),
91 Integer.MAX_VALUE,
92 Layout.Alignment.ALIGN_NORMAL,
93 1.0f,
94 0.0f,
95 false);
96 mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
97 }
98
100 View view,
101 int client,
102 TextInputChannel textInputChannel,
103 KeyboardDelegate keyboardDelegate,
104 ListenableEditingState editable,
105 EditorInfo editorInfo) {
106 this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI());
107 }
108
109 private ExtractedText getExtractedText(ExtractedTextRequest request) {
110 mExtractedText.startOffset = 0;
111 mExtractedText.partialStartOffset = -1;
112 mExtractedText.partialEndOffset = -1;
113 mExtractedText.selectionStart = mEditable.getSelectionStart();
114 mExtractedText.selectionEnd = mEditable.getSelectionEnd();
115 mExtractedText.text =
116 request == null || (request.flags & GET_TEXT_WITH_STYLES) == 0
117 ? mEditable.toString()
118 : mEditable;
119 return mExtractedText;
120 }
121
122 private CursorAnchorInfo getCursorAnchorInfo() {
123 if (mCursorAnchorInfoBuilder == null) {
124 mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
125 } else {
126 mCursorAnchorInfoBuilder.reset();
127 }
128
129 mCursorAnchorInfoBuilder.setSelectionRange(
130 mEditable.getSelectionStart(), mEditable.getSelectionEnd());
131 final int composingStart = mEditable.getComposingStart();
132 final int composingEnd = mEditable.getComposingEnd();
133 if (composingStart >= 0 && composingEnd > composingStart) {
134 mCursorAnchorInfoBuilder.setComposingText(
135 composingStart, mEditable.toString().subSequence(composingStart, composingEnd));
136 } else {
137 mCursorAnchorInfoBuilder.setComposingText(-1, "");
138 }
139 return mCursorAnchorInfoBuilder.build();
140 }
141
142 @Override
143 public Editable getEditable() {
144 return mEditable;
145 }
146
147 @Override
148 public boolean beginBatchEdit() {
149 mEditable.beginBatchEdit();
150 batchEditNestDepth += 1;
151 return super.beginBatchEdit();
152 }
153
154 @Override
155 public boolean endBatchEdit() {
156 boolean result = super.endBatchEdit();
157 batchEditNestDepth -= 1;
158 mEditable.endBatchEdit();
159 return result;
160 }
161
162 @Override
163 public boolean commitText(CharSequence text, int newCursorPosition) {
164 final boolean result = super.commitText(text, newCursorPosition);
165 return result;
166 }
167
168 @Override
169 public boolean deleteSurroundingText(int beforeLength, int afterLength) {
170 if (mEditable.getSelectionStart() == -1) {
171 return true;
172 }
173
174 final boolean result = super.deleteSurroundingText(beforeLength, afterLength);
175 return result;
176 }
177
178 @Override
179 public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
180 boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
181 return result;
182 }
183
184 @Override
185 public boolean setComposingRegion(int start, int end) {
186 final boolean result = super.setComposingRegion(start, end);
187 return result;
188 }
189
190 @Override
191 public boolean setComposingText(CharSequence text, int newCursorPosition) {
192 boolean result;
194 if (text.length() == 0) {
195 result = super.commitText(text, newCursorPosition);
196 } else {
197 result = super.setComposingText(text, newCursorPosition);
198 }
199 endBatchEdit();
200 return result;
201 }
202
203 @Override
204 public boolean finishComposingText() {
205 final boolean result = super.finishComposingText();
206 return result;
207 }
208
209 // When there's not enough vertical screen space, the IME may enter fullscreen mode and this
210 // method will be used to get (a portion of) the currently edited text. Samsung keyboard seems
211 // to use this method instead of InputConnection#getText{Before,After}Cursor.
212 // See https://github.com/flutter/engine/pull/17426.
213 // TODO(garyq): Implement a more feature complete version of getExtractedText
214 @Override
215 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
216 final boolean textMonitor = (flags & GET_EXTRACTED_TEXT_MONITOR) != 0;
217 if (textMonitor == (mExtractRequest == null)) {
218 Log.d(TAG, "The input method toggled text monitoring " + (textMonitor ? "on" : "off"));
219 }
220 // Enables text monitoring if the relevant flag is set. See
221 // InputConnectionAdaptor#didChangeEditingState.
222 mExtractRequest = textMonitor ? request : null;
223 return getExtractedText(request);
224 }
225
226 @Override
227 public boolean requestCursorUpdates(int cursorUpdateMode) {
228 if ((cursorUpdateMode & CURSOR_UPDATE_IMMEDIATE) != 0) {
229 mImm.updateCursorAnchorInfo(mFlutterView, getCursorAnchorInfo());
230 }
231
232 final boolean updated = (cursorUpdateMode & CURSOR_UPDATE_MONITOR) != 0;
233 if (updated != mMonitorCursorUpdate) {
234 Log.d(TAG, "The input method toggled cursor monitoring " + (updated ? "on" : "off"));
235 }
236
237 // Enables cursor monitoring. See InputConnectionAdaptor#didChangeEditingState.
238 mMonitorCursorUpdate = updated;
239 return true;
240 }
241
242 @Override
243 public boolean clearMetaKeyStates(int states) {
244 boolean result = super.clearMetaKeyStates(states);
245 return result;
246 }
247
248 @Override
249 public void closeConnection() {
250 super.closeConnection();
251 mEditable.removeEditingStateListener(this);
252 for (; batchEditNestDepth > 0; batchEditNestDepth--) {
253 endBatchEdit();
254 }
255 }
256
257 @Override
258 public boolean setSelection(int start, int end) {
260 boolean result = super.setSelection(start, end);
261 endBatchEdit();
262 return result;
263 }
264
265 // Sanitizes the index to ensure the index is within the range of the
266 // contents of editable.
267 private static int clampIndexToEditable(int index, Editable editable) {
268 int clamped = Math.max(0, Math.min(editable.length(), index));
269 if (clamped != index) {
270 Log.d(
271 "flutter",
272 "Text selection index was clamped ("
273 + index
274 + "->"
275 + clamped
276 + ") to remain in bounds. This may not be your fault, as some keyboards may select outside of bounds.");
277 }
278 return clamped;
279 }
280
281 // This function is called both when hardware key events occur and aren't
282 // handled by the framework, as well as when soft keyboard editing events
283 // occur, and need a chance to be handled by the framework.
284 @Override
285 public boolean sendKeyEvent(KeyEvent event) {
286 return keyboardDelegate.handleEvent(event);
287 }
288
289 public boolean handleKeyEvent(KeyEvent event) {
290 if (event.getAction() == KeyEvent.ACTION_DOWN) {
291 if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
292 return handleHorizontalMovement(true, event.isShiftPressed());
293 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
294 return handleHorizontalMovement(false, event.isShiftPressed());
295 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
296 return handleVerticalMovement(true, event.isShiftPressed());
297 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
298 return handleVerticalMovement(false, event.isShiftPressed());
299 // When the enter key is pressed on a non-multiline field, consider it a
300 // submit instead of a newline.
301 } else if ((event.getKeyCode() == KeyEvent.KEYCODE_ENTER
302 || event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER)
303 && (InputType.TYPE_TEXT_FLAG_MULTI_LINE & mEditorInfo.inputType) == 0) {
304 performEditorAction(mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION);
305 return true;
306 } else {
307 // Enter a character.
308 final int selStart = Selection.getSelectionStart(mEditable);
309 final int selEnd = Selection.getSelectionEnd(mEditable);
310 final int character = event.getUnicodeChar();
311 if (selStart < 0 || selEnd < 0 || character == 0) {
312 return false;
313 }
314
315 final int selMin = Math.min(selStart, selEnd);
316 final int selMax = Math.max(selStart, selEnd);
318 if (selMin != selMax) mEditable.delete(selMin, selMax);
319 mEditable.insert(selMin, String.valueOf((char) character));
320 setSelection(selMin + 1, selMin + 1);
321 endBatchEdit();
322 return true;
323 }
324 }
325 return false;
326 }
327
328 private boolean handleHorizontalMovement(boolean isLeft, boolean isShiftPressed) {
329 final int selStart = Selection.getSelectionStart(mEditable);
330 final int selEnd = Selection.getSelectionEnd(mEditable);
331
332 if (selStart < 0 || selEnd < 0) {
333 return false;
334 }
335
336 final int newSelectionEnd =
337 isLeft
338 ? Math.max(flutterTextUtils.getOffsetBefore(mEditable, selEnd), 0)
339 : Math.min(flutterTextUtils.getOffsetAfter(mEditable, selEnd), mEditable.length());
340
341 final boolean shouldCollapse = selStart == selEnd && !isShiftPressed;
342
343 if (shouldCollapse) {
344 setSelection(newSelectionEnd, newSelectionEnd);
345 } else {
346 setSelection(selStart, newSelectionEnd);
347 }
348 return true;
349 };
350
351 private boolean handleVerticalMovement(boolean isUp, boolean isShiftPressed) {
352 final int selStart = Selection.getSelectionStart(mEditable);
353 final int selEnd = Selection.getSelectionEnd(mEditable);
354
355 if (selStart < 0 || selEnd < 0) {
356 return false;
357 }
358
359 final boolean shouldCollapse = selStart == selEnd && !isShiftPressed;
360
362 if (shouldCollapse) {
363 if (isUp) {
364 Selection.moveUp(mEditable, mLayout);
365 } else {
366 Selection.moveDown(mEditable, mLayout);
367 }
368 final int newSelection = Selection.getSelectionStart(mEditable);
369 setSelection(newSelection, newSelection);
370 } else {
371 if (isUp) {
372 Selection.extendUp(mEditable, mLayout);
373 } else {
374 Selection.extendDown(mEditable, mLayout);
375 }
376 setSelection(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
377 }
378 endBatchEdit();
379 return true;
380 }
381
382 @Override
383 public boolean performContextMenuAction(int id) {
385 final boolean result = doPerformContextMenuAction(id);
386 endBatchEdit();
387 return result;
388 }
389
390 private boolean doPerformContextMenuAction(int id) {
391 if (id == android.R.id.selectAll) {
392 setSelection(0, mEditable.length());
393 return true;
394 } else if (id == android.R.id.cut) {
395 int selStart = Selection.getSelectionStart(mEditable);
396 int selEnd = Selection.getSelectionEnd(mEditable);
397 if (selStart != selEnd) {
398 int selMin = Math.min(selStart, selEnd);
399 int selMax = Math.max(selStart, selEnd);
400 CharSequence textToCut = mEditable.subSequence(selMin, selMax);
401 ClipboardManager clipboard =
402 (ClipboardManager)
403 mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
404 ClipData clip = ClipData.newPlainText("text label?", textToCut);
405 clipboard.setPrimaryClip(clip);
406 mEditable.delete(selMin, selMax);
407 setSelection(selMin, selMin);
408 }
409 return true;
410 } else if (id == android.R.id.copy) {
411 int selStart = Selection.getSelectionStart(mEditable);
412 int selEnd = Selection.getSelectionEnd(mEditable);
413 if (selStart != selEnd) {
414 CharSequence textToCopy =
415 mEditable.subSequence(Math.min(selStart, selEnd), Math.max(selStart, selEnd));
416 ClipboardManager clipboard =
417 (ClipboardManager)
418 mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
419 clipboard.setPrimaryClip(ClipData.newPlainText("text label?", textToCopy));
420 }
421 return true;
422 } else if (id == android.R.id.paste) {
423 ClipboardManager clipboard =
424 (ClipboardManager) mFlutterView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
425 ClipData clip = clipboard.getPrimaryClip();
426 if (clip != null) {
427 CharSequence textToPaste = clip.getItemAt(0).coerceToText(mFlutterView.getContext());
428 int selStart = Math.max(0, Selection.getSelectionStart(mEditable));
429 int selEnd = Math.max(0, Selection.getSelectionEnd(mEditable));
430 int selMin = Math.min(selStart, selEnd);
431 int selMax = Math.max(selStart, selEnd);
432 if (selMin != selMax) mEditable.delete(selMin, selMax);
433 mEditable.insert(selMin, textToPaste);
434 int newSelStart = selMin + textToPaste.length();
435 setSelection(newSelStart, newSelStart);
436 }
437 return true;
438 }
439 return false;
440 }
441
442 @Override
443 public boolean performPrivateCommand(String action, Bundle data) {
444 textInputChannel.performPrivateCommand(mClient, action, data);
445 return true;
446 }
447
448 @Override
449 public boolean performEditorAction(int actionCode) {
450 switch (actionCode) {
451 case EditorInfo.IME_ACTION_NONE:
452 textInputChannel.newline(mClient);
453 break;
454 case EditorInfo.IME_ACTION_UNSPECIFIED:
455 textInputChannel.unspecifiedAction(mClient);
456 break;
457 case EditorInfo.IME_ACTION_GO:
458 textInputChannel.go(mClient);
459 break;
460 case EditorInfo.IME_ACTION_SEARCH:
461 textInputChannel.search(mClient);
462 break;
463 case EditorInfo.IME_ACTION_SEND:
464 textInputChannel.send(mClient);
465 break;
466 case EditorInfo.IME_ACTION_NEXT:
467 textInputChannel.next(mClient);
468 break;
469 case EditorInfo.IME_ACTION_PREVIOUS:
470 textInputChannel.previous(mClient);
471 break;
472 default:
473 case EditorInfo.IME_ACTION_DONE:
474 textInputChannel.done(mClient);
475 break;
476 }
477 return true;
478 }
479
480 @Override
481 @TargetApi(API_LEVELS.API_25)
482 @RequiresApi(API_LEVELS.API_25)
483 public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
484 // Ensure permission is granted.
485 if (Build.VERSION.SDK_INT >= API_LEVELS.API_25
486 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
487 try {
488 inputContentInfo.requestPermission();
489 } catch (Exception e) {
490 return false;
491 }
492 } else {
493 return false;
494 }
495
496 if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
497 inputContentInfo.requestPermission();
498
499 final Uri uri = inputContentInfo.getContentUri();
500 final String mimeType = inputContentInfo.getDescription().getMimeType(0);
501 Context context = mFlutterView.getContext();
502
503 if (uri != null) {
504 InputStream is;
505 try {
506 // Extract byte data from the given URI.
507 is = context.getContentResolver().openInputStream(uri);
508 } catch (FileNotFoundException ex) {
509 inputContentInfo.releasePermission();
510 return false;
511 }
512
513 if (is != null) {
514 final byte[] data = this.readStreamFully(is, 64 * 1024);
515
516 final Map<String, Object> obj = new HashMap<>();
517 obj.put("mimeType", mimeType);
518 obj.put("data", data);
519 obj.put("uri", uri.toString());
520
521 // Commit the content to the text input channel and release the permission.
522 textInputChannel.commitContent(mClient, obj);
523 inputContentInfo.releasePermission();
524 return true;
525 }
526 }
527
528 inputContentInfo.releasePermission();
529 }
530 return false;
531 }
532
533 private byte[] readStreamFully(InputStream is, int blocksize) {
534 ByteArrayOutputStream baos = new ByteArrayOutputStream();
535
536 byte[] buffer = new byte[blocksize];
537 while (true) {
538 int len = -1;
539 try {
540 len = is.read(buffer);
541 } catch (IOException ex) {
542 }
543 if (len == -1) break;
544 baos.write(buffer, 0, len);
545 }
546 return baos.toByteArray();
547 }
548
549 // -------- Start: ListenableEditingState watcher implementation -------
550 @Override
552 boolean textChanged, boolean selectionChanged, boolean composingRegionChanged) {
553 // This method notifies the input method that the editing state has changed.
554 // updateSelection is mandatory. updateExtractedText and updateCursorAnchorInfo
555 // are on demand (if the input method set the correspoinding monitoring
556 // flags). See getExtractedText and requestCursorUpdates.
557
558 // Always send selection update. InputMethodManager#updateSelection skips
559 // sending the message if none of the parameters have changed since the last
560 // time we called it.
561 mImm.updateSelection(
562 mFlutterView,
563 mEditable.getSelectionStart(),
564 mEditable.getSelectionEnd(),
565 mEditable.getComposingStart(),
566 mEditable.getComposingEnd());
567
568 if (mExtractRequest != null) {
569 mImm.updateExtractedText(
570 mFlutterView, mExtractRequest.token, getExtractedText(mExtractRequest));
571 }
572 if (mMonitorCursorUpdate) {
573 final CursorAnchorInfo info = getCursorAnchorInfo();
574 mImm.updateCursorAnchorInfo(mFlutterView, info);
575 }
576 }
577 // -------- End: ListenableEditingState watcher implementation -------
578}
static void info(const char *fmt,...) SK_PRINTF_LIKE(1
Definition: DM.cpp:213
static SkPath clip(const SkPath &path, const SkHalfPlane &plane)
Definition: SkPath.cpp:3892
static final int API_25
Definition: Build.java:15
static void d(@NonNull String tag, @NonNull String message)
Definition: Log.java:64
void commitContent(int inputClientId, Map< String, Object > content)
void performPrivateCommand(int inputClientId, @NonNull String action, @NonNull Bundle data)
int getOffsetAfter(CharSequence text, int offset)
int getOffsetBefore(CharSequence text, int offset)
boolean setComposingText(CharSequence text, int newCursorPosition)
boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts)
boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength)
ExtractedText getExtractedText(ExtractedTextRequest request, int flags)
boolean performPrivateCommand(String action, Bundle data)
boolean deleteSurroundingText(int beforeLength, int afterLength)
boolean commitText(CharSequence text, int newCursorPosition)
InputConnectionAdaptor(View view, int client, TextInputChannel textInputChannel, KeyboardDelegate keyboardDelegate, ListenableEditingState editable, EditorInfo editorInfo)
void didChangeEditingState(boolean textChanged, boolean selectionChanged, boolean composingRegionChanged)
void removeEditingStateListener(EditingStateWatcher listener)
void addEditingStateListener(EditingStateWatcher listener)
FlutterSemanticsFlag flags
glong glong end
FlKeyEvent * event
GAsyncResult * result
std::u16string text
def Build(configs, env, options)
Definition: build.py:232
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace buffer
Definition: switches.h:126
#define TAG()
std::shared_ptr< const fml::Mapping > data
Definition: texture_gles.cc:63