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