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