1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.inputmethod;
18 
19 import static android.view.inputmethod.InputConnectionProto.CURSOR_CAPS_MODE;
20 import static android.view.inputmethod.InputConnectionProto.EDITABLE_TEXT;
21 import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT;
22 import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_END;
23 import static android.view.inputmethod.InputConnectionProto.SELECTED_TEXT_START;
24 
25 import android.annotation.CallbackExecutor;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.graphics.RectF;
29 import android.os.Bundle;
30 import android.os.CancellationSignal;
31 import android.text.Editable;
32 import android.text.Selection;
33 import android.text.method.KeyListener;
34 import android.util.Log;
35 import android.util.proto.ProtoOutputStream;
36 import android.view.inputmethod.BaseInputConnection;
37 import android.view.inputmethod.CompletionInfo;
38 import android.view.inputmethod.CorrectionInfo;
39 import android.view.inputmethod.DeleteGesture;
40 import android.view.inputmethod.DeleteRangeGesture;
41 import android.view.inputmethod.DumpableInputConnection;
42 import android.view.inputmethod.ExtractedText;
43 import android.view.inputmethod.ExtractedTextRequest;
44 import android.view.inputmethod.HandwritingGesture;
45 import android.view.inputmethod.InputConnection;
46 import android.view.inputmethod.InsertGesture;
47 import android.view.inputmethod.InsertModeGesture;
48 import android.view.inputmethod.JoinOrSplitGesture;
49 import android.view.inputmethod.PreviewableHandwritingGesture;
50 import android.view.inputmethod.RemoveSpaceGesture;
51 import android.view.inputmethod.SelectGesture;
52 import android.view.inputmethod.SelectRangeGesture;
53 import android.view.inputmethod.TextBoundsInfo;
54 import android.view.inputmethod.TextBoundsInfoResult;
55 import android.widget.TextView;
56 
57 import java.util.concurrent.Executor;
58 import java.util.function.Consumer;
59 import java.util.function.IntConsumer;
60 
61 /**
62  * Base class for an editable InputConnection instance. This is created by {@link TextView} or
63  * {@link android.widget.EditText}.
64  */
65 public final class EditableInputConnection extends BaseInputConnection
66         implements DumpableInputConnection {
67     private static final boolean DEBUG = false;
68     private static final String TAG = "EditableInputConnection";
69 
70     private final TextView mTextView;
71 
72     // Keeps track of nested begin/end batch edit to ensure this connection always has a
73     // balanced impact on its associated TextView.
74     // A negative value means that this connection has been finished by the InputMethodManager.
75     private int mBatchEditNesting;
76 
EditableInputConnection(TextView textview)77     public EditableInputConnection(TextView textview) {
78         super(textview, true);
79         mTextView = textview;
80     }
81 
82     @Override
getEditable()83     public Editable getEditable() {
84         TextView tv = mTextView;
85         if (tv != null) {
86             return tv.getEditableText();
87         }
88         return null;
89     }
90 
91     @Override
beginBatchEdit()92     public boolean beginBatchEdit() {
93         synchronized (this) {
94             if (mBatchEditNesting >= 0) {
95                 mTextView.beginBatchEdit();
96                 mBatchEditNesting++;
97                 return true;
98             }
99         }
100         return false;
101     }
102 
103     @Override
endBatchEdit()104     public boolean endBatchEdit() {
105         synchronized (this) {
106             if (mBatchEditNesting > 0) {
107                 // When the connection is reset by the InputMethodManager and reportFinish
108                 // is called, some endBatchEdit calls may still be asynchronously received from the
109                 // IME. Do not take these into account, thus ensuring that this IC's final
110                 // contribution to mTextView's nested batch edit count is zero.
111                 mTextView.endBatchEdit();
112                 mBatchEditNesting--;
113                 return mBatchEditNesting > 0;
114             }
115         }
116         return false;
117     }
118 
119     @Override
endComposingRegionEditInternal()120     public void endComposingRegionEditInternal() {
121         // The ContentCapture service is interested in Composing-state changes.
122         mTextView.notifyContentCaptureTextChanged();
123     }
124 
125     @Override
closeConnection()126     public void closeConnection() {
127         super.closeConnection();
128         synchronized (this) {
129             while (mBatchEditNesting > 0) {
130                 endBatchEdit();
131             }
132             // Will prevent any further calls to begin or endBatchEdit
133             mBatchEditNesting = -1;
134         }
135     }
136 
137     @Override
clearMetaKeyStates(int states)138     public boolean clearMetaKeyStates(int states) {
139         final Editable content = getEditable();
140         if (content == null) return false;
141         KeyListener kl = mTextView.getKeyListener();
142         if (kl != null) {
143             try {
144                 kl.clearMetaKeyState(mTextView, content, states);
145             } catch (AbstractMethodError e) {
146                 // This is an old listener that doesn't implement the
147                 // new method.
148             }
149         }
150         return true;
151     }
152 
153     @Override
commitCompletion(CompletionInfo text)154     public boolean commitCompletion(CompletionInfo text) {
155         if (DEBUG) Log.v(TAG, "commitCompletion " + text);
156         mTextView.beginBatchEdit();
157         mTextView.onCommitCompletion(text);
158         mTextView.endBatchEdit();
159         return true;
160     }
161 
162     /**
163      * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
164      */
165     @Override
commitCorrection(CorrectionInfo correctionInfo)166     public boolean commitCorrection(CorrectionInfo correctionInfo) {
167         if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
168         mTextView.beginBatchEdit();
169         mTextView.onCommitCorrection(correctionInfo);
170         mTextView.endBatchEdit();
171         return true;
172     }
173 
174     @Override
performEditorAction(int actionCode)175     public boolean performEditorAction(int actionCode) {
176         if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
177         mTextView.onEditorAction(actionCode);
178         return true;
179     }
180 
181     @Override
performContextMenuAction(int id)182     public boolean performContextMenuAction(int id) {
183         if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
184         mTextView.beginBatchEdit();
185         mTextView.onTextContextMenuItem(id);
186         mTextView.endBatchEdit();
187         return true;
188     }
189 
190     @Override
getExtractedText(ExtractedTextRequest request, int flags)191     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
192         if (mTextView != null) {
193             ExtractedText et = new ExtractedText();
194             if (mTextView.extractText(request, et)) {
195                 if ((flags & GET_EXTRACTED_TEXT_MONITOR) != 0) {
196                     mTextView.setExtracting(request);
197                 }
198                 return et;
199             }
200         }
201         return null;
202     }
203 
204     @Override
performSpellCheck()205     public boolean performSpellCheck() {
206         mTextView.onPerformSpellCheck();
207         return true;
208     }
209 
210     @Override
performPrivateCommand(String action, Bundle data)211     public boolean performPrivateCommand(String action, Bundle data) {
212         mTextView.onPrivateIMECommand(action, data);
213         return true;
214     }
215 
216     @Override
commitText(CharSequence text, int newCursorPosition)217     public boolean commitText(CharSequence text, int newCursorPosition) {
218         if (mTextView == null) {
219             return super.commitText(text, newCursorPosition);
220         }
221         mTextView.resetErrorChangedFlag();
222         boolean success = super.commitText(text, newCursorPosition);
223         mTextView.hideErrorIfUnchanged();
224 
225         return success;
226     }
227 
228     @Override
requestCursorUpdates( @ursorUpdateMode int cursorUpdateMode, @CursorUpdateFilter int cursorUpdateFilter)229     public boolean requestCursorUpdates(
230             @CursorUpdateMode int cursorUpdateMode, @CursorUpdateFilter int cursorUpdateFilter) {
231         // TODO(b/210039666): use separate attrs for updateMode and updateFilter.
232         return requestCursorUpdates(cursorUpdateMode | cursorUpdateFilter);
233     }
234 
235     @Override
requestCursorUpdates(int cursorUpdateMode)236     public boolean requestCursorUpdates(int cursorUpdateMode) {
237         if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
238 
239         final int knownModeFlags = InputConnection.CURSOR_UPDATE_IMMEDIATE
240                 | InputConnection.CURSOR_UPDATE_MONITOR;
241         final int knownFilterFlags = InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS
242                 | InputConnection.CURSOR_UPDATE_FILTER_INSERTION_MARKER
243                 | InputConnection.CURSOR_UPDATE_FILTER_CHARACTER_BOUNDS
244                 | InputConnection.CURSOR_UPDATE_FILTER_VISIBLE_LINE_BOUNDS
245                 | InputConnection.CURSOR_UPDATE_FILTER_TEXT_APPEARANCE;
246 
247         // It is possible that any other bit is used as a valid flag in a future release.
248         // We should reject the entire request in such a case.
249         final int knownFlagMask = knownModeFlags | knownFilterFlags;
250         final int unknownFlags = cursorUpdateMode & ~knownFlagMask;
251         if (unknownFlags != 0) {
252             if (DEBUG) {
253                 Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags. "
254                         + "cursorUpdateMode=" + cursorUpdateMode + " unknownFlags=" + unknownFlags);
255             }
256             return false;
257         }
258 
259         if (mIMM == null) {
260             // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
261             // TODO: Return some notification code rather than false to indicate method that
262             // CursorAnchorInfo is temporarily unavailable.
263             return false;
264         }
265         mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode);  // for UnsupportedAppUsage
266         if (mTextView != null) {
267             mTextView.onRequestCursorUpdatesInternal(cursorUpdateMode & knownModeFlags,
268                     cursorUpdateMode & knownFilterFlags);
269         }
270         return true;
271     }
272 
273     @Override
requestTextBoundsInfo( @onNull RectF bounds, @Nullable @CallbackExecutor Executor executor, @NonNull Consumer<TextBoundsInfoResult> consumer)274     public void requestTextBoundsInfo(
275             @NonNull RectF bounds, @Nullable @CallbackExecutor Executor executor,
276             @NonNull Consumer<TextBoundsInfoResult> consumer) {
277         final TextBoundsInfo textBoundsInfo = mTextView.getTextBoundsInfo(bounds);
278         final int resultCode;
279         if (textBoundsInfo != null) {
280             resultCode = TextBoundsInfoResult.CODE_SUCCESS;
281         } else {
282             resultCode = TextBoundsInfoResult.CODE_FAILED;
283         }
284         final TextBoundsInfoResult textBoundsInfoResult =
285                 new TextBoundsInfoResult(resultCode, textBoundsInfo);
286 
287         executor.execute(() -> consumer.accept(textBoundsInfoResult));
288     }
289 
290     @Override
setImeConsumesInput(boolean imeConsumesInput)291     public boolean setImeConsumesInput(boolean imeConsumesInput) {
292         if (mTextView == null) {
293             return super.setImeConsumesInput(imeConsumesInput);
294         }
295         mTextView.setImeConsumesInput(imeConsumesInput);
296         return true;
297     }
298 
299     @Override
performHandwritingGesture( @onNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor, @Nullable IntConsumer consumer)300     public void performHandwritingGesture(
301             @NonNull HandwritingGesture gesture, @Nullable @CallbackExecutor Executor executor,
302             @Nullable IntConsumer consumer) {
303         int result;
304         if (gesture instanceof SelectGesture) {
305             result = mTextView.performHandwritingSelectGesture((SelectGesture) gesture);
306         } else if (gesture instanceof SelectRangeGesture) {
307             result = mTextView.performHandwritingSelectRangeGesture((SelectRangeGesture) gesture);
308         } else if (gesture instanceof DeleteGesture) {
309             result = mTextView.performHandwritingDeleteGesture((DeleteGesture) gesture);
310         } else if (gesture instanceof DeleteRangeGesture) {
311             result = mTextView.performHandwritingDeleteRangeGesture((DeleteRangeGesture) gesture);
312         } else if (gesture instanceof InsertGesture) {
313             result = mTextView.performHandwritingInsertGesture((InsertGesture) gesture);
314         } else if (gesture instanceof RemoveSpaceGesture) {
315             result = mTextView.performHandwritingRemoveSpaceGesture((RemoveSpaceGesture) gesture);
316         } else if (gesture instanceof JoinOrSplitGesture) {
317             result = mTextView.performHandwritingJoinOrSplitGesture((JoinOrSplitGesture) gesture);
318         } else if (gesture instanceof InsertModeGesture) {
319             result = mTextView.performHandwritingInsertModeGesture((InsertModeGesture) gesture);
320         } else {
321             result = HANDWRITING_GESTURE_RESULT_UNSUPPORTED;
322         }
323         if (executor != null && consumer != null) {
324             executor.execute(() -> consumer.accept(result));
325         }
326     }
327 
328     @Override
previewHandwritingGesture( @onNull PreviewableHandwritingGesture gesture, @Nullable CancellationSignal cancellationSignal)329     public boolean previewHandwritingGesture(
330             @NonNull PreviewableHandwritingGesture gesture,
331             @Nullable CancellationSignal cancellationSignal) {
332         return mTextView.previewHandwritingGesture(gesture, cancellationSignal);
333     }
334 
335     @Override
dumpDebug(ProtoOutputStream proto, long fieldId)336     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
337         final long token = proto.start(fieldId);
338         CharSequence editableText = mTextView.getText();
339         CharSequence selectedText = getSelectedText(0 /* flags */);
340         if (InputConnectionProtoDumper.DUMP_TEXT) {
341             if (editableText != null) {
342                 proto.write(EDITABLE_TEXT, editableText.toString());
343             }
344             if (selectedText != null) {
345                 proto.write(SELECTED_TEXT, selectedText.toString());
346             }
347         }
348         final Editable content = getEditable();
349         if (content != null) {
350             int start = Selection.getSelectionStart(content);
351             int end = Selection.getSelectionEnd(content);
352             proto.write(SELECTED_TEXT_START, start);
353             proto.write(SELECTED_TEXT_END, end);
354         }
355         proto.write(CURSOR_CAPS_MODE, getCursorCapsMode(0));
356         proto.end(token);
357     }
358 }
359