• 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.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