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 android.compat.annotation.UnsupportedAppUsage;
20 import android.os.Bundle;
21 import android.text.Editable;
22 import android.text.method.KeyListener;
23 import android.util.Log;
24 import android.view.inputmethod.BaseInputConnection;
25 import android.view.inputmethod.CompletionInfo;
26 import android.view.inputmethod.CorrectionInfo;
27 import android.view.inputmethod.ExtractedText;
28 import android.view.inputmethod.ExtractedTextRequest;
29 import android.view.inputmethod.InputConnection;
30 import android.widget.TextView;
31 
32 public class EditableInputConnection extends BaseInputConnection {
33     private static final boolean DEBUG = false;
34     private static final String TAG = "EditableInputConnection";
35 
36     private final TextView mTextView;
37 
38     // Keeps track of nested begin/end batch edit to ensure this connection always has a
39     // balanced impact on its associated TextView.
40     // A negative value means that this connection has been finished by the InputMethodManager.
41     private int mBatchEditNesting;
42 
43     @UnsupportedAppUsage
EditableInputConnection(TextView textview)44     public EditableInputConnection(TextView textview) {
45         super(textview, true);
46         mTextView = textview;
47     }
48 
49     @Override
getEditable()50     public Editable getEditable() {
51         TextView tv = mTextView;
52         if (tv != null) {
53             return tv.getEditableText();
54         }
55         return null;
56     }
57 
58     @Override
beginBatchEdit()59     public boolean beginBatchEdit() {
60         synchronized(this) {
61             if (mBatchEditNesting >= 0) {
62                 mTextView.beginBatchEdit();
63                 mBatchEditNesting++;
64                 return true;
65             }
66         }
67         return false;
68     }
69 
70     @Override
endBatchEdit()71     public boolean endBatchEdit() {
72         synchronized(this) {
73             if (mBatchEditNesting > 0) {
74                 // When the connection is reset by the InputMethodManager and reportFinish
75                 // is called, some endBatchEdit calls may still be asynchronously received from the
76                 // IME. Do not take these into account, thus ensuring that this IC's final
77                 // contribution to mTextView's nested batch edit count is zero.
78                 mTextView.endBatchEdit();
79                 mBatchEditNesting--;
80                 return true;
81             }
82         }
83         return false;
84     }
85 
86     @Override
closeConnection()87     public void closeConnection() {
88         super.closeConnection();
89         synchronized(this) {
90             while (mBatchEditNesting > 0) {
91                 endBatchEdit();
92             }
93             // Will prevent any further calls to begin or endBatchEdit
94             mBatchEditNesting = -1;
95         }
96     }
97 
98     @Override
clearMetaKeyStates(int states)99     public boolean clearMetaKeyStates(int states) {
100         final Editable content = getEditable();
101         if (content == null) return false;
102         KeyListener kl = mTextView.getKeyListener();
103         if (kl != null) {
104             try {
105                 kl.clearMetaKeyState(mTextView, content, states);
106             } catch (AbstractMethodError e) {
107                 // This is an old listener that doesn't implement the
108                 // new method.
109             }
110         }
111         return true;
112     }
113 
114     @Override
commitCompletion(CompletionInfo text)115     public boolean commitCompletion(CompletionInfo text) {
116         if (DEBUG) Log.v(TAG, "commitCompletion " + text);
117         mTextView.beginBatchEdit();
118         mTextView.onCommitCompletion(text);
119         mTextView.endBatchEdit();
120         return true;
121     }
122 
123     /**
124      * Calls the {@link TextView#onCommitCorrection} method of the associated TextView.
125      */
126     @Override
commitCorrection(CorrectionInfo correctionInfo)127     public boolean commitCorrection(CorrectionInfo correctionInfo) {
128         if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
129         mTextView.beginBatchEdit();
130         mTextView.onCommitCorrection(correctionInfo);
131         mTextView.endBatchEdit();
132         return true;
133     }
134 
135     @Override
performEditorAction(int actionCode)136     public boolean performEditorAction(int actionCode) {
137         if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode);
138         mTextView.onEditorAction(actionCode);
139         return true;
140     }
141 
142     @Override
performContextMenuAction(int id)143     public boolean performContextMenuAction(int id) {
144         if (DEBUG) Log.v(TAG, "performContextMenuAction " + id);
145         mTextView.beginBatchEdit();
146         mTextView.onTextContextMenuItem(id);
147         mTextView.endBatchEdit();
148         return true;
149     }
150 
151     @Override
getExtractedText(ExtractedTextRequest request, int flags)152     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
153         if (mTextView != null) {
154             ExtractedText et = new ExtractedText();
155             if (mTextView.extractText(request, et)) {
156                 if ((flags&GET_EXTRACTED_TEXT_MONITOR) != 0) {
157                     mTextView.setExtracting(request);
158                 }
159                 return et;
160             }
161         }
162         return null;
163     }
164 
165     @Override
performPrivateCommand(String action, Bundle data)166     public boolean performPrivateCommand(String action, Bundle data) {
167         mTextView.onPrivateIMECommand(action, data);
168         return true;
169     }
170 
171     @Override
commitText(CharSequence text, int newCursorPosition)172     public boolean commitText(CharSequence text, int newCursorPosition) {
173         if (mTextView == null) {
174             return super.commitText(text, newCursorPosition);
175         }
176         mTextView.resetErrorChangedFlag();
177         boolean success = super.commitText(text, newCursorPosition);
178         mTextView.hideErrorIfUnchanged();
179 
180         return success;
181     }
182 
183     @Override
requestCursorUpdates(int cursorUpdateMode)184     public boolean requestCursorUpdates(int cursorUpdateMode) {
185         if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode);
186 
187         // It is possible that any other bit is used as a valid flag in a future release.
188         // We should reject the entire request in such a case.
189         final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE |
190                 InputConnection.CURSOR_UPDATE_MONITOR;
191         final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK;
192         if (unknownFlags != 0) {
193             if (DEBUG) {
194                 Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." +
195                         " cursorUpdateMode=" + cursorUpdateMode +
196                         " unknownFlags=" + unknownFlags);
197             }
198             return false;
199         }
200 
201         if (mIMM == null) {
202             // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled.
203             // TODO: Return some notification code rather than false to indicate method that
204             // CursorAnchorInfo is temporarily unavailable.
205             return false;
206         }
207         mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode);
208         if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
209             if (mTextView == null) {
210                 // In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored.
211                 // TODO: Return some notification code for the input method that indicates
212                 // FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored.
213             } else if (mTextView.isInLayout()) {
214                 // In this case, the view hierarchy is currently undergoing a layout pass.
215                 // IMM#updateCursorAnchorInfo is supposed to be called soon after the layout
216                 // pass is finished.
217             } else {
218                 // This will schedule a layout pass of the view tree, and the layout event
219                 // eventually triggers IMM#updateCursorAnchorInfo.
220                 mTextView.requestLayout();
221             }
222         }
223         return true;
224     }
225 }
226