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
17package com.android.internal.widget;
18
19import android.os.Bundle;
20import android.text.Editable;
21import android.text.Spanned;
22import android.text.method.KeyListener;
23import android.text.style.SuggestionSpan;
24import android.util.Log;
25import android.view.inputmethod.BaseInputConnection;
26import android.view.inputmethod.CompletionInfo;
27import android.view.inputmethod.CorrectionInfo;
28import android.view.inputmethod.ExtractedText;
29import android.view.inputmethod.ExtractedTextRequest;
30import android.view.inputmethod.InputConnection;
31import android.widget.TextView;
32
33public 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