1 /*
2  * Copyright (C) 2007-2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.internal.widget;
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.compat.annotation.UnsupportedAppUsage;
26 import android.os.Bundle;
27 import android.text.Editable;
28 import android.text.Selection;
29 import android.text.method.KeyListener;
30 import android.util.Log;
31 import android.util.imetracing.InputConnectionHelper;
32 import android.util.proto.ProtoOutputStream;
33 import android.view.inputmethod.BaseInputConnection;
34 import android.view.inputmethod.CompletionInfo;
35 import android.view.inputmethod.CorrectionInfo;
36 import android.view.inputmethod.DumpableInputConnection;
37 import android.view.inputmethod.ExtractedText;
38 import android.view.inputmethod.ExtractedTextRequest;
39 import android.view.inputmethod.InputConnection;
40 import android.widget.TextView;
41 
42 /**
43  * Base class for an editable InputConnection instance. This is created by {@link TextView} or
44  * {@link EditText}.
45  */
46 public class EditableInputConnection extends BaseInputConnection
47         implements DumpableInputConnection {
48     private static final boolean DEBUG = false;
49     private static final String TAG = "EditableInputConnection";
50 
51     private final TextView mTextView;
52 
53     // Keeps track of nested begin/end batch edit to ensure this connection always has a
54     // balanced impact on its associated TextView.
55     // A negative value means that this connection has been finished by the InputMethodManager.
56     private int mBatchEditNesting;
57 
58     @UnsupportedAppUsage
EditableInputConnection(TextView textview)59     public EditableInputConnection(TextView textview) {
60         super(textview, true);
61         mTextView = textview;
62     }
63 
64     @Override
getEditable()65     public Editable getEditable() {
66         TextView tv = mTextView;
67         if (tv != null) {
68             return tv.getEditableText();
69         }
70         return null;
71     }
72 
73     @Override
beginBatchEdit()74     public boolean beginBatchEdit() {
75         synchronized(this) {
76             if (mBatchEditNesting >= 0) {
77                 mTextView.beginBatchEdit();
78                 mBatchEditNesting++;
79                 return true;
80             }
81         }
82         return false;
83     }
84 
85     @Override
endBatchEdit()86     public boolean endBatchEdit() {
87         synchronized(this) {
88             if (mBatchEditNesting > 0) {
89                 // When the connection is reset by the InputMethodManager and reportFinish
90                 // is called, some endBatchEdit calls may still be asynchronously received from the
91                 // IME. Do not take these into account, thus ensuring that this IC's final
92                 // contribution to mTextView's nested batch edit count is zero.
93                 mTextView.endBatchEdit();
94                 mBatchEditNesting--;
95                 return true;
96             }
97         }
98         return false;
99     }
100 
101     @Override
endComposingRegionEditInternal()102     public void endComposingRegionEditInternal() {
103         // The ContentCapture service is interested in Composing-state changes.
104         mTextView.notifyContentCaptureTextChanged();
105     }
106 
107     @Override
closeConnection()108     public void closeConnection() {
109         super.closeConnection();
110         synchronized(this) {
111             while (mBatchEditNesting > 0) {
112                 endBatchEdit();
113             }
114             // Will prevent any further calls to begin or endBatchEdit
115             mBatchEditNesting = -1;
116         }
117     }
118 
119     @Override
clearMetaKeyStates(int states)120     public boolean clearMetaKeyStates(int states) {
121         final Editable content = getEditable();
122         if (content == null) return false;
123         KeyListener kl = mTextView.getKeyListener();
124         if (kl != null) {
125             try {
126                 kl.clearMetaKeyState(mTextView, content, states);
127             } catch (AbstractMethodError e) {
128                 // This is an old listener that doesn't implement the
129                 // new method.
130             }
131         }
132         return true;
133     }
134 
135     @Override
commitCompletion(CompletionInfo text)136     public boolean commitCompletion(CompletionInfo text) {
137         if (DEBUG) Log.v(TAG, "commitCompletion " + text);
138         mTextView.beginBatchEdit();
139         mTextView.onCommitCompletion(text);
140         mTextView.endBatchEdit();
141         return true;
142     }
143 
144     /**
145      * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
146      */
147     @Override
commitCorrection(CorrectionInfo correctionInfo)148     public boolean commitCorrection(CorrectionInfo correctionInfo) {
149         if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
150         mTextView.beginBatchEdit();
151         mTextView.onCommitCorrection(correctionInfo);
152         mTextView.endBatchEdit();
153         return true;
154     }
155 
156     @Override
performEditorAction(int actionCode)157     public boolean performEditorAction(int actionCode) {
158         if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
159         mTextView.onEditorAction(actionCode);
160         return true;
161     }
162 
163     @Override
performContextMenuAction(int id)164     public boolean performContextMenuAction(int id) {
165         if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
166         mTextView.beginBatchEdit();
167         mTextView.onTextContextMenuItem(id);
168         mTextView.endBatchEdit();
169         return true;
170     }
171 
172     @Override
getExtractedText(ExtractedTextRequest request, int flags)173     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
174         if (mTextView != null) {
175             ExtractedText et = new ExtractedText();
176             if (mTextView.extractText(request, et)) {
177                 if ((flags&GET_EXTRACTED_TEXT_MONITOR) != 0) {
178                     mTextView.setExtracting(request);
179                 }
180                 return et;
181             }
182         }
183         return null;
184     }
185 
186     @Override
performSpellCheck()187     public boolean performSpellCheck() {
188         mTextView.onPerformSpellCheck();
189         return true;
190     }
191 
192     @Override
performPrivateCommand(String action, Bundle data)193     public boolean performPrivateCommand(String action, Bundle data) {
194         mTextView.onPrivateIMECommand(action, data);
195         return true;
196     }
197 
198     @Override
commitText(CharSequence text, int newCursorPosition)199     public boolean commitText(CharSequence text, int newCursorPosition) {
200         if (mTextView == null) {
201             return super.commitText(text, newCursorPosition);
202         }
203         mTextView.resetErrorChangedFlag();
204         boolean success = super.commitText(text, newCursorPosition);
205         mTextView.hideErrorIfUnchanged();
206 
207         return success;
208     }
209 
210     @Override
requestCursorUpdates(int cursorUpdateMode)211     public boolean requestCursorUpdates(int cursorUpdateMode) {
212         if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
213 
214         // It is possible that any other bit is used as a valid flag in a future release.
215         // We should reject the entire request in such a case.
216         final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE |
217                 InputConnection.CURSOR_UPDATE_MONITOR;
218         final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
219         if (unknownFlags != 0) {
220             if (DEBUG) {
221                 Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." +
222                         " cursorUpdateMode=" + cursorUpdateMode +
223                         " unknownFlags=" + unknownFlags);
224             }
225             return false;
226         }
227 
228         if (mIMM == null) {
229             // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
230             // TODO: Return some notification code rather than false to indicate method that
231             // CursorAnchorInfo is temporarily unavailable.
232             return false;
233         }
234         mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode);
235         if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
236             if (mTextView == null) {
237                 // In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored.
238                 // TODO: Return some notification code for the input method that indicates
239                 // FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored.
240             } else if (mTextView.isInLayout()) {
241                 // In this case, the view hierarchy is currently undergoing a layout pass.
242                 // IMM#updateCursorAnchorInfo is supposed to be called soon after the layout
243                 // pass is finished.
244             } else {
245                 // This will schedule a layout pass of the view tree, and the layout event
246                 // eventually triggers IMM#updateCursorAnchorInfo.
247                 mTextView.requestLayout();
248             }
249         }
250         return true;
251     }
252 
253     @Override
setImeConsumesInput(boolean imeConsumesInput)254     public boolean setImeConsumesInput(boolean imeConsumesInput) {
255         if (mTextView == null) {
256             return super.setImeConsumesInput(imeConsumesInput);
257         }
258         mTextView.setImeConsumesInput(imeConsumesInput);
259         return true;
260     }
261 
262     @Override
dumpDebug(ProtoOutputStream proto, long fieldId)263     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
264         final long token = proto.start(fieldId);
265         CharSequence editableText = mTextView.getText();
266         CharSequence selectedText = getSelectedText(0 /* flags */);
267         if (InputConnectionHelper.DUMP_TEXT) {
268             if (editableText != null) {
269                 proto.write(EDITABLE_TEXT, editableText.toString());
270             }
271             if (selectedText != null) {
272                 proto.write(SELECTED_TEXT, selectedText.toString());
273             }
274         }
275         final Editable content = getEditable();
276         if (content != null) {
277             int start = Selection.getSelectionStart(content);
278             int end = Selection.getSelectionEnd(content);
279             proto.write(SELECTED_TEXT_START, start);
280             proto.write(SELECTED_TEXT_END, end);
281         }
282         proto.write(CURSOR_CAPS_MODE, getCursorCapsMode(0));
283         proto.end(token);
284     }
285 }
286