46 private static final String TAG =
"InputConnectionAdaptor";
52 private final View mFlutterView;
53 private final int mClient;
54 private final TextInputChannel textInputChannel;
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;
65 private int batchEditNestDepth = 0;
67 @SuppressWarnings(
"deprecation")
71 TextInputChannel textInputChannel,
74 EditorInfo editorInfo,
75 FlutterJNI flutterJNI) {
79 this.textInputChannel = textInputChannel;
82 mEditorInfo = editorInfo;
83 this.keyboardDelegate = keyboardDelegate;
92 Layout.Alignment.ALIGN_NORMAL,
96 mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
102 TextInputChannel textInputChannel,
105 EditorInfo editorInfo) {
106 this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo,
new FlutterJNI());
109 private ExtractedText getExtractedText(ExtractedTextRequest request) {
110 mExtractedText.startOffset = 0;
111 mExtractedText.partialStartOffset = -1;
112 mExtractedText.partialEndOffset = -1;
115 mExtractedText.text =
116 request ==
null || (request.flags & GET_TEXT_WITH_STYLES) == 0
119 return mExtractedText;
122 private CursorAnchorInfo getCursorAnchorInfo() {
123 if (mCursorAnchorInfoBuilder ==
null) {
124 mCursorAnchorInfoBuilder =
new CursorAnchorInfo.Builder();
126 mCursorAnchorInfoBuilder.reset();
129 mCursorAnchorInfoBuilder.setSelectionRange(
133 if (composingStart >= 0 && composingEnd > composingStart) {
134 mCursorAnchorInfoBuilder.setComposingText(
135 composingStart, mEditable.
toString().subSequence(composingStart, composingEnd));
137 mCursorAnchorInfoBuilder.setComposingText(-1,
"");
139 return mCursorAnchorInfoBuilder.build();
150 batchEditNestDepth += 1;
151 return super.beginBatchEdit();
156 boolean result = super.endBatchEdit();
157 batchEditNestDepth -= 1;
163 public boolean commitText(CharSequence text,
int newCursorPosition) {
164 final boolean result = super.commitText(
text, newCursorPosition);
174 final boolean result = super.deleteSurroundingText(beforeLength, afterLength);
180 boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
194 if (
text.length() == 0) {
195 result = super.commitText(
text, newCursorPosition);
197 result = super.setComposingText(
text, newCursorPosition);
205 final boolean result = super.finishComposingText();
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"));
222 mExtractRequest = textMonitor ? request :
null;
223 return getExtractedText(request);
228 if ((cursorUpdateMode & CURSOR_UPDATE_IMMEDIATE) != 0) {
229 mImm.updateCursorAnchorInfo(mFlutterView, getCursorAnchorInfo());
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"));
238 mMonitorCursorUpdate = updated;
244 boolean result = super.clearMetaKeyStates(states);
250 super.closeConnection();
252 for (; batchEditNestDepth > 0; batchEditNestDepth--) {
267 private static int clampIndexToEditable(
int index, Editable editable) {
268 int clamped = Math.max(0, Math.min(editable.length(), index));
269 if (clamped != index) {
272 "Text selection index was clamped ("
276 +
") to remain in bounds. This may not be your fault, as some keyboards may select outside of bounds.");
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());
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) {
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) {
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));
328 private boolean handleHorizontalMovement(
boolean isLeft,
boolean isShiftPressed) {
329 final int selStart = Selection.getSelectionStart(mEditable);
330 final int selEnd = Selection.getSelectionEnd(mEditable);
332 if (selStart < 0 || selEnd < 0) {
336 final int newSelectionEnd =
339 : Math.min(flutterTextUtils.
getOffsetAfter(mEditable, selEnd), mEditable.length());
341 final boolean shouldCollapse = selStart == selEnd && !isShiftPressed;
343 if (shouldCollapse) {
351 private boolean handleVerticalMovement(
boolean isUp,
boolean isShiftPressed) {
352 final int selStart = Selection.getSelectionStart(mEditable);
353 final int selEnd = Selection.getSelectionEnd(mEditable);
355 if (selStart < 0 || selEnd < 0) {
359 final boolean shouldCollapse = selStart == selEnd && !isShiftPressed;
362 if (shouldCollapse) {
364 Selection.moveUp(mEditable, mLayout);
366 Selection.moveDown(mEditable, mLayout);
368 final int newSelection = Selection.getSelectionStart(mEditable);
372 Selection.extendUp(mEditable, mLayout);
374 Selection.extendDown(mEditable, mLayout);
376 setSelection(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
385 final boolean result = doPerformContextMenuAction(
id);
390 private boolean doPerformContextMenuAction(
int id) {
391 if (
id ==
android.R.id.selectAll) {
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 =
403 mFlutterView.getContext().getSystemService(
Context.CLIPBOARD_SERVICE);
404 ClipData
clip = ClipData.newPlainText(
"text label?", textToCut);
405 clipboard.setPrimaryClip(
clip);
406 mEditable.delete(selMin, selMax);
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 =
418 mFlutterView.getContext().getSystemService(
Context.CLIPBOARD_SERVICE);
419 clipboard.setPrimaryClip(ClipData.newPlainText(
"text label?", textToCopy));
422 }
else if (
id ==
android.R.id.paste) {
423 ClipboardManager clipboard =
424 (ClipboardManager) mFlutterView.getContext().getSystemService(
Context.CLIPBOARD_SERVICE);
425 ClipData
clip = clipboard.getPrimaryClip();
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();
444 textInputChannel.performPrivateCommand(mClient,
action, data);
450 switch (actionCode) {
451 case EditorInfo.IME_ACTION_NONE:
452 textInputChannel.newline(mClient);
454 case EditorInfo.IME_ACTION_UNSPECIFIED:
455 textInputChannel.unspecifiedAction(mClient);
457 case EditorInfo.IME_ACTION_GO:
458 textInputChannel.go(mClient);
460 case EditorInfo.IME_ACTION_SEARCH:
461 textInputChannel.search(mClient);
463 case EditorInfo.IME_ACTION_SEND:
464 textInputChannel.send(mClient);
466 case EditorInfo.IME_ACTION_NEXT:
467 textInputChannel.next(mClient);
469 case EditorInfo.IME_ACTION_PREVIOUS:
470 textInputChannel.previous(mClient);
473 case EditorInfo.IME_ACTION_DONE:
474 textInputChannel.done(mClient);
481 @TargetApi(API_LEVELS.API_25)
482 @RequiresApi(API_LEVELS.API_25)
485 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_25
486 && (
flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
488 inputContentInfo.requestPermission();
489 }
catch (Exception e) {
496 if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
497 inputContentInfo.requestPermission();
499 final Uri uri = inputContentInfo.getContentUri();
500 final String mimeType = inputContentInfo.getDescription().getMimeType(0);
501 Context context = mFlutterView.getContext();
507 is = context.getContentResolver().openInputStream(uri);
508 }
catch (FileNotFoundException ex) {
509 inputContentInfo.releasePermission();
514 final byte[] data = this.readStreamFully(is, 64 * 1024);
516 final Map<String, Object> obj =
new HashMap<>();
517 obj.put(
"mimeType", mimeType);
518 obj.put(
"data", data);
519 obj.put(
"uri", uri.toString());
522 textInputChannel.commitContent(mClient, obj);
523 inputContentInfo.releasePermission();
528 inputContentInfo.releasePermission();
533 private byte[] readStreamFully(InputStream is,
int blocksize) {
534 ByteArrayOutputStream baos =
new ByteArrayOutputStream();
536 byte[]
buffer =
new byte[blocksize];
541 }
catch (IOException ex) {
543 if (len == -1)
break;
544 baos.write(
buffer, 0, len);
546 return baos.toByteArray();
552 boolean textChanged,
boolean selectionChanged,
boolean composingRegionChanged) {
561 mImm.updateSelection(
568 if (mExtractRequest !=
null) {
569 mImm.updateExtractedText(
570 mFlutterView, mExtractRequest.token, getExtractedText(mExtractRequest));
572 if (mMonitorCursorUpdate) {
573 final CursorAnchorInfo
info = getCursorAnchorInfo();
574 mImm.updateCursorAnchorInfo(mFlutterView,
info);