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