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