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