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.text.DynamicLayout; 9 import android.text.Editable; 10 import android.text.Layout; 11 import android.text.Layout.Directions; 12 import android.text.Selection; 13 import android.text.TextPaint; 14 import android.view.KeyEvent; 15 import android.view.View; 16 import android.view.inputmethod.BaseInputConnection; 17 import android.view.inputmethod.EditorInfo; 18 import android.view.inputmethod.InputMethodManager; 19 20 import io.flutter.embedding.engine.systemchannels.TextInputChannel; 21 import io.flutter.Log; 22 import io.flutter.plugin.common.ErrorLogResult; 23 import io.flutter.plugin.common.MethodChannel; 24 25 class InputConnectionAdaptor extends BaseInputConnection { 26 private final View mFlutterView; 27 private final int mClient; 28 private final TextInputChannel textInputChannel; 29 private final Editable mEditable; 30 private int mBatchCount; 31 private InputMethodManager mImm; 32 private final Layout mLayout; 33 34 @SuppressWarnings("deprecation") InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, Editable editable )35 public InputConnectionAdaptor( 36 View view, 37 int client, 38 TextInputChannel textInputChannel, 39 Editable editable 40 ) { 41 super(view, true); 42 mFlutterView = view; 43 mClient = client; 44 this.textInputChannel = textInputChannel; 45 mEditable = editable; 46 mBatchCount = 0; 47 // We create a dummy Layout with max width so that the selection 48 // shifting acts as if all text were in one line. 49 mLayout = new DynamicLayout(mEditable, new TextPaint(), Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); 50 mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 51 } 52 53 // Send the current state of the editable to Flutter. updateEditingState()54 private void updateEditingState() { 55 // If the IME is in the middle of a batch edit, then wait until it completes. 56 if (mBatchCount > 0) 57 return; 58 59 int selectionStart = Selection.getSelectionStart(mEditable); 60 int selectionEnd = Selection.getSelectionEnd(mEditable); 61 int composingStart = BaseInputConnection.getComposingSpanStart(mEditable); 62 int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable); 63 64 mImm.updateSelection(mFlutterView, 65 selectionStart, selectionEnd, 66 composingStart, composingEnd); 67 68 textInputChannel.updateEditingState( 69 mClient, 70 mEditable.toString(), 71 selectionStart, 72 selectionEnd, 73 composingStart, 74 composingEnd 75 ); 76 } 77 78 @Override getEditable()79 public Editable getEditable() { 80 return mEditable; 81 } 82 83 @Override beginBatchEdit()84 public boolean beginBatchEdit() { 85 mBatchCount++; 86 return super.beginBatchEdit(); 87 } 88 89 @Override endBatchEdit()90 public boolean endBatchEdit() { 91 boolean result = super.endBatchEdit(); 92 mBatchCount--; 93 updateEditingState(); 94 return result; 95 } 96 97 @Override commitText(CharSequence text, int newCursorPosition)98 public boolean commitText(CharSequence text, int newCursorPosition) { 99 boolean result = super.commitText(text, newCursorPosition); 100 updateEditingState(); 101 return result; 102 } 103 104 @Override deleteSurroundingText(int beforeLength, int afterLength)105 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 106 if (Selection.getSelectionStart(mEditable) == -1) 107 return true; 108 109 boolean result = super.deleteSurroundingText(beforeLength, afterLength); 110 updateEditingState(); 111 return result; 112 } 113 114 @Override setComposingRegion(int start, int end)115 public boolean setComposingRegion(int start, int end) { 116 boolean result = super.setComposingRegion(start, end); 117 updateEditingState(); 118 return result; 119 } 120 121 @Override setComposingText(CharSequence text, int newCursorPosition)122 public boolean setComposingText(CharSequence text, int newCursorPosition) { 123 boolean result; 124 if (text.length() == 0) { 125 result = super.commitText(text, newCursorPosition); 126 } else { 127 result = super.setComposingText(text, newCursorPosition); 128 } 129 updateEditingState(); 130 return result; 131 } 132 133 @Override setSelection(int start, int end)134 public boolean setSelection(int start, int end) { 135 boolean result = super.setSelection(start, end); 136 updateEditingState(); 137 return result; 138 } 139 140 // Sanitizes the index to ensure the index is within the range of the 141 // contents of editable. clampIndexToEditable(int index, Editable editable)142 private static int clampIndexToEditable(int index, Editable editable) { 143 int clamped = Math.max(0, Math.min(editable.length(), index)); 144 if (clamped != index) { 145 Log.d("flutter", "Text selection index was clamped (" 146 + index + "->" + clamped 147 + ") to remain in bounds. This may not be your fault, as some keyboards may select outside of bounds." 148 ); 149 } 150 return clamped; 151 } 152 153 @Override sendKeyEvent(KeyEvent event)154 public boolean sendKeyEvent(KeyEvent event) { 155 if (event.getAction() == KeyEvent.ACTION_DOWN) { 156 if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { 157 int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable); 158 int selEnd = clampIndexToEditable(Selection.getSelectionEnd(mEditable), mEditable); 159 if (selEnd > selStart) { 160 // Delete the selection. 161 Selection.setSelection(mEditable, selStart); 162 mEditable.delete(selStart, selEnd); 163 updateEditingState(); 164 return true; 165 } else if (selStart > 0) { 166 // Delete to the left/right of the cursor depending on direction of text. 167 // TODO(garyq): Explore how to obtain per-character direction. The 168 // isRTLCharAt() call below is returning blanket direction assumption 169 // based on the first character in the line. 170 boolean isRtl = mLayout.isRtlCharAt(mLayout.getLineForOffset(selStart)); 171 try { 172 if (isRtl) { 173 Selection.extendRight(mEditable, mLayout); 174 } else { 175 Selection.extendLeft(mEditable, mLayout); 176 } 177 } catch (IndexOutOfBoundsException e) { 178 // On some Chinese devices (primarily Huawei, some Xiaomi), 179 // on initial app startup before focus is lost, the 180 // Selection.extendLeft and extendRight calls always extend 181 // from the index of the initial contents of mEditable. This 182 // try-catch will prevent crashing on Huawei devices by falling 183 // back to a simple way of deletion, although this a hack and 184 // will not handle emojis. 185 Selection.setSelection(mEditable, selStart, selStart - 1); 186 } 187 int newStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable); 188 int newEnd = clampIndexToEditable(Selection.getSelectionEnd(mEditable), mEditable); 189 Selection.setSelection(mEditable, Math.min(newStart, newEnd)); 190 // Min/Max the values since RTL selections will start at a higher 191 // index than they end at. 192 mEditable.delete(Math.min(newStart, newEnd), Math.max(newStart, newEnd)); 193 updateEditingState(); 194 return true; 195 } 196 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { 197 int selStart = Selection.getSelectionStart(mEditable); 198 int newSel = Math.max(selStart - 1, 0); 199 setSelection(newSel, newSel); 200 return true; 201 } else if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { 202 int selStart = Selection.getSelectionStart(mEditable); 203 int newSel = Math.min(selStart + 1, mEditable.length()); 204 setSelection(newSel, newSel); 205 return true; 206 } else { 207 // Enter a character. 208 int character = event.getUnicodeChar(); 209 if (character != 0) { 210 int selStart = Math.max(0, Selection.getSelectionStart(mEditable)); 211 int selEnd = Math.max(0, Selection.getSelectionEnd(mEditable)); 212 if (selEnd != selStart) 213 mEditable.delete(selStart, selEnd); 214 mEditable.insert(selStart, String.valueOf((char) character)); 215 setSelection(selStart + 1, selStart + 1); 216 updateEditingState(); 217 } 218 return true; 219 } 220 } 221 return false; 222 } 223 224 @Override performEditorAction(int actionCode)225 public boolean performEditorAction(int actionCode) { 226 switch (actionCode) { 227 case EditorInfo.IME_ACTION_NONE: 228 textInputChannel.newline(mClient); 229 break; 230 case EditorInfo.IME_ACTION_UNSPECIFIED: 231 textInputChannel.unspecifiedAction(mClient); 232 break; 233 case EditorInfo.IME_ACTION_GO: 234 textInputChannel.go(mClient); 235 break; 236 case EditorInfo.IME_ACTION_SEARCH: 237 textInputChannel.search(mClient); 238 break; 239 case EditorInfo.IME_ACTION_SEND: 240 textInputChannel.send(mClient); 241 break; 242 case EditorInfo.IME_ACTION_NEXT: 243 textInputChannel.next(mClient); 244 break; 245 case EditorInfo.IME_ACTION_PREVIOUS: 246 textInputChannel.previous(mClient); 247 break; 248 default: 249 case EditorInfo.IME_ACTION_DONE: 250 textInputChannel.done(mClient); 251 break; 252 } 253 return true; 254 } 255 } 256