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.os.Bundle; 20 import android.text.Editable; 21 import android.text.Spanned; 22 import android.text.method.KeyListener; 23 import android.text.style.SuggestionSpan; 24 import android.util.Log; 25 import android.view.inputmethod.BaseInputConnection; 26 import android.view.inputmethod.CompletionInfo; 27 import android.view.inputmethod.CorrectionInfo; 28 import android.view.inputmethod.ExtractedText; 29 import android.view.inputmethod.ExtractedTextRequest; 30 import android.view.inputmethod.InputConnection; 31 import android.widget.TextView; 32 33 public class EditableInputConnection extends BaseInputConnection { 34 private static final boolean DEBUG = false; 35 private static final String TAG = "EditableInputConnection"; 36 37 private final TextView mTextView; 38 39 // Keeps track of nested begin/end batch edit to ensure this connection always has a 40 // balanced impact on its associated TextView. 41 // A negative value means that this connection has been finished by the InputMethodManager. 42 private int mBatchEditNesting; 43 44 public EditableInputConnection(TextView textview) { 45 super(textview, true); 46 mTextView = textview; 47 } 48 49 @Override 50 public Editable getEditable() { 51 TextView tv = mTextView; 52 if (tv != null) { 53 return tv.getEditableText(); 54 } 55 return null; 56 } 57 58 @Override 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 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 87 protected void reportFinish() { 88 super.reportFinish(); 89 90 synchronized(this) { 91 while (mBatchEditNesting > 0) { 92 endBatchEdit(); 93 } 94 // Will prevent any further calls to begin or endBatchEdit 95 mBatchEditNesting = -1; 96 } 97 } 98 99 @Override 100 public boolean clearMetaKeyStates(int states) { 101 final Editable content = getEditable(); 102 if (content == null) return false; 103 KeyListener kl = mTextView.getKeyListener(); 104 if (kl != null) { 105 try { 106 kl.clearMetaKeyState(mTextView, content, states); 107 } catch (AbstractMethodError e) { 108 // This is an old listener that doesn't implement the 109 // new method. 110 } 111 } 112 return true; 113 } 114 115 @Override 116 public boolean commitCompletion(CompletionInfo text) { 117 if (DEBUG) Log.v(TAG, "commitCompletion " + text); 118 mTextView.beginBatchEdit(); 119 mTextView.onCommitCompletion(text); 120 mTextView.endBatchEdit(); 121 return true; 122 } 123 124 /** 125 * Calls the {@link TextView#onCommitCorrection} method of the associated TextView. 126 */ 127 @Override 128 public boolean commitCorrection(CorrectionInfo correctionInfo) { 129 if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo); 130 mTextView.beginBatchEdit(); 131 mTextView.onCommitCorrection(correctionInfo); 132 mTextView.endBatchEdit(); 133 return true; 134 } 135 136 @Override 137 public boolean performEditorAction(int actionCode) { 138 if (DEBUG) Log.v(TAG, "performEditorAction " + actionCode); 139 mTextView.onEditorAction(actionCode); 140 return true; 141 } 142 143 @Override 144 public boolean performContextMenuAction(int id) { 145 if (DEBUG) Log.v(TAG, "performContextMenuAction " + id); 146 mTextView.beginBatchEdit(); 147 mTextView.onTextContextMenuItem(id); 148 mTextView.endBatchEdit(); 149 return true; 150 } 151 152 @Override 153 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 154 if (mTextView != null) { 155 ExtractedText et = new ExtractedText(); 156 if (mTextView.extractText(request, et)) { 157 if ((flags&GET_EXTRACTED_TEXT_MONITOR) != 0) { 158 mTextView.setExtracting(request); 159 } 160 return et; 161 } 162 } 163 return null; 164 } 165 166 @Override 167 public boolean performPrivateCommand(String action, Bundle data) { 168 mTextView.onPrivateIMECommand(action, data); 169 return true; 170 } 171 172 @Override 173 public boolean commitText(CharSequence text, int newCursorPosition) { 174 if (mTextView == null) { 175 return super.commitText(text, newCursorPosition); 176 } 177 if (text instanceof Spanned) { 178 Spanned spanned = ((Spanned) text); 179 SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class); 180 mIMM.registerSuggestionSpansForNotification(spans); 181 } 182 183 mTextView.resetErrorChangedFlag(); 184 boolean success = super.commitText(text, newCursorPosition); 185 mTextView.hideErrorIfUnchanged(); 186 187 return success; 188 } 189 190 @Override 191 public boolean requestCursorUpdates(int cursorUpdateMode) { 192 if (DEBUG) Log.v(TAG, "requestUpdateCursorAnchorInfo " + cursorUpdateMode); 193 194 // It is possible that any other bit is used as a valid flag in a future release. 195 // We should reject the entire request in such a case. 196 final int KNOWN_FLAGS_MASK = InputConnection.CURSOR_UPDATE_IMMEDIATE | 197 InputConnection.CURSOR_UPDATE_MONITOR; 198 final int unknownFlags = cursorUpdateMode & ~KNOWN_FLAGS_MASK; 199 if (unknownFlags != 0) { 200 if (DEBUG) { 201 Log.d(TAG, "Rejecting requestUpdateCursorAnchorInfo due to unknown flags." + 202 " cursorUpdateMode=" + cursorUpdateMode + 203 " unknownFlags=" + unknownFlags); 204 } 205 return false; 206 } 207 208 if (mIMM == null) { 209 // In this case, TYPE_CURSOR_ANCHOR_INFO is not handled. 210 // TODO: Return some notification code rather than false to indicate method that 211 // CursorAnchorInfo is temporarily unavailable. 212 return false; 213 } 214 mIMM.setUpdateCursorAnchorInfoMode(cursorUpdateMode); 215 if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) { 216 if (mTextView == null) { 217 // In this case, FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is silently ignored. 218 // TODO: Return some notification code for the input method that indicates 219 // FLAG_CURSOR_ANCHOR_INFO_IMMEDIATE is ignored. 220 } else if (mTextView.isInLayout()) { 221 // In this case, the view hierarchy is currently undergoing a layout pass. 222 // IMM#updateCursorAnchorInfo is supposed to be called soon after the layout 223 // pass is finished. 224 } else { 225 // This will schedule a layout pass of the view tree, and the layout event 226 // eventually triggers IMM#updateCursorAnchorInfo. 227 mTextView.requestLayout(); 228 } 229 } 230 return true; 231 } 232 } 233