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