BaseInputConnection.java revision e507c840c83f83baf724780e79215a40fbbf02c9
1/*
2 * Copyright (C) 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 android.view.inputmethod;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Bundle;
22import android.os.SystemClock;
23import android.text.Editable;
24import android.text.NoCopySpan;
25import android.text.Selection;
26import android.text.Spannable;
27import android.text.SpannableStringBuilder;
28import android.text.Spanned;
29import android.text.TextUtils;
30import android.text.method.MetaKeyKeyListener;
31import android.util.Log;
32import android.util.LogPrinter;
33import android.view.KeyCharacterMap;
34import android.view.KeyEvent;
35import android.view.View;
36import android.view.ViewRootImpl;
37
38class ComposingText implements NoCopySpan {
39}
40
41/**
42 * Base class for implementors of the InputConnection interface, taking care
43 * of most of the common behavior for providing a connection to an Editable.
44 * Implementors of this class will want to be sure to implement
45 * {@link #getEditable} to provide access to their own editable object, and
46 * to refer to the documentation in {@link InputConnection}.
47 */
48public class BaseInputConnection implements InputConnection {
49    private static final boolean DEBUG = false;
50    private static final String TAG = "BaseInputConnection";
51    static final Object COMPOSING = new ComposingText();
52
53    /** @hide */
54    protected final InputMethodManager mIMM;
55    final View mTargetView;
56    final boolean mDummyMode;
57
58    private Object[] mDefaultComposingSpans;
59
60    Editable mEditable;
61    KeyCharacterMap mKeyCharacterMap;
62
63    BaseInputConnection(InputMethodManager mgr, boolean fullEditor) {
64        mIMM = mgr;
65        mTargetView = null;
66        mDummyMode = !fullEditor;
67    }
68
69    public BaseInputConnection(View targetView, boolean fullEditor) {
70        mIMM = (InputMethodManager)targetView.getContext().getSystemService(
71                Context.INPUT_METHOD_SERVICE);
72        mTargetView = targetView;
73        mDummyMode = !fullEditor;
74    }
75
76    public static final void removeComposingSpans(Spannable text) {
77        text.removeSpan(COMPOSING);
78        Object[] sps = text.getSpans(0, text.length(), Object.class);
79        if (sps != null) {
80            for (int i=sps.length-1; i>=0; i--) {
81                Object o = sps[i];
82                if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) {
83                    text.removeSpan(o);
84                }
85            }
86        }
87    }
88
89    public static void setComposingSpans(Spannable text) {
90        setComposingSpans(text, 0, text.length());
91    }
92
93    /** @hide */
94    public static void setComposingSpans(Spannable text, int start, int end) {
95        final Object[] sps = text.getSpans(start, end, Object.class);
96        if (sps != null) {
97            for (int i=sps.length-1; i>=0; i--) {
98                final Object o = sps[i];
99                if (o == COMPOSING) {
100                    text.removeSpan(o);
101                    continue;
102                }
103
104                final int fl = text.getSpanFlags(o);
105                if ((fl&(Spanned.SPAN_COMPOSING|Spanned.SPAN_POINT_MARK_MASK))
106                        != (Spanned.SPAN_COMPOSING|Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) {
107                    text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o),
108                            (fl & ~Spanned.SPAN_POINT_MARK_MASK)
109                                    | Spanned.SPAN_COMPOSING
110                                    | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
111                }
112            }
113        }
114
115        text.setSpan(COMPOSING, start, end,
116                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
117    }
118
119    public static int getComposingSpanStart(Spannable text) {
120        return text.getSpanStart(COMPOSING);
121    }
122
123    public static int getComposingSpanEnd(Spannable text) {
124        return text.getSpanEnd(COMPOSING);
125    }
126
127    /**
128     * Return the target of edit operations.  The default implementation
129     * returns its own fake editable that is just used for composing text;
130     * subclasses that are real text editors should override this and
131     * supply their own.
132     */
133    public Editable getEditable() {
134        if (mEditable == null) {
135            mEditable = Editable.Factory.getInstance().newEditable("");
136            Selection.setSelection(mEditable, 0);
137        }
138        return mEditable;
139    }
140
141    /**
142     * Default implementation does nothing.
143     */
144    public boolean beginBatchEdit() {
145        return false;
146    }
147
148    /**
149     * Default implementation does nothing.
150     */
151    public boolean endBatchEdit() {
152        return false;
153    }
154
155    /**
156     * Called when this InputConnection is no longer used by the InputMethodManager.
157     *
158     * @hide
159     */
160    protected void reportFinish() {
161        // Intentionaly empty
162    }
163
164    /**
165     * Default implementation uses
166     * {@link MetaKeyKeyListener#clearMetaKeyState(long, int)
167     * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state.
168     */
169    public boolean clearMetaKeyStates(int states) {
170        final Editable content = getEditable();
171        if (content == null) return false;
172        MetaKeyKeyListener.clearMetaKeyState(content, states);
173        return true;
174    }
175
176    /**
177     * Default implementation does nothing and returns false.
178     */
179    public boolean commitCompletion(CompletionInfo text) {
180        return false;
181    }
182
183    /**
184     * Default implementation does nothing and returns false.
185     */
186    public boolean commitCorrection(CorrectionInfo correctionInfo) {
187        return false;
188    }
189
190    /**
191     * Default implementation replaces any existing composing text with
192     * the given text.  In addition, only if dummy mode, a key event is
193     * sent for the new text and the current editable buffer cleared.
194     */
195    public boolean commitText(CharSequence text, int newCursorPosition) {
196        if (DEBUG) Log.v(TAG, "commitText " + text);
197        replaceText(text, newCursorPosition, false);
198        sendCurrentText();
199        return true;
200    }
201
202    /**
203     * The default implementation performs the deletion around the current
204     * selection position of the editable text.
205     * @param beforeLength
206     * @param afterLength
207     */
208    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
209        if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
210                + " / " + afterLength);
211        final Editable content = getEditable();
212        if (content == null) return false;
213
214        beginBatchEdit();
215
216        int a = Selection.getSelectionStart(content);
217        int b = Selection.getSelectionEnd(content);
218
219        if (a > b) {
220            int tmp = a;
221            a = b;
222            b = tmp;
223        }
224
225        // ignore the composing text.
226        int ca = getComposingSpanStart(content);
227        int cb = getComposingSpanEnd(content);
228        if (cb < ca) {
229            int tmp = ca;
230            ca = cb;
231            cb = tmp;
232        }
233        if (ca != -1 && cb != -1) {
234            if (ca < a) a = ca;
235            if (cb > b) b = cb;
236        }
237
238        int deleted = 0;
239
240        if (beforeLength > 0) {
241            int start = a - beforeLength;
242            if (start < 0) start = 0;
243            content.delete(start, a);
244            deleted = a - start;
245        }
246
247        if (afterLength > 0) {
248            b = b - deleted;
249
250            int end = b + afterLength;
251            if (end > content.length()) end = content.length();
252
253            content.delete(b, end);
254        }
255
256        endBatchEdit();
257
258        return true;
259    }
260
261    /**
262     * The default implementation removes the composing state from the
263     * current editable text.  In addition, only if dummy mode, a key event is
264     * sent for the new text and the current editable buffer cleared.
265     */
266    public boolean finishComposingText() {
267        if (DEBUG) Log.v(TAG, "finishComposingText");
268        final Editable content = getEditable();
269        if (content != null) {
270            beginBatchEdit();
271            removeComposingSpans(content);
272            // Note: sendCurrentText does nothing unless mDummyMode is set
273            sendCurrentText();
274            endBatchEdit();
275        }
276        return true;
277    }
278
279    /**
280     * The default implementation uses TextUtils.getCapsMode to get the
281     * cursor caps mode for the current selection position in the editable
282     * text, unless in dummy mode in which case 0 is always returned.
283     */
284    public int getCursorCapsMode(int reqModes) {
285        if (mDummyMode) return 0;
286
287        final Editable content = getEditable();
288        if (content == null) return 0;
289
290        int a = Selection.getSelectionStart(content);
291        int b = Selection.getSelectionEnd(content);
292
293        if (a > b) {
294            int tmp = a;
295            a = b;
296            b = tmp;
297        }
298
299        return TextUtils.getCapsMode(content, a, reqModes);
300    }
301
302    /**
303     * The default implementation always returns null.
304     */
305    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
306        return null;
307    }
308
309    /**
310     * The default implementation returns the given amount of text from the
311     * current cursor position in the buffer.
312     */
313    public CharSequence getTextBeforeCursor(int length, int flags) {
314        final Editable content = getEditable();
315        if (content == null) return null;
316
317        int a = Selection.getSelectionStart(content);
318        int b = Selection.getSelectionEnd(content);
319
320        if (a > b) {
321            int tmp = a;
322            a = b;
323            b = tmp;
324        }
325
326        if (a <= 0) {
327            return "";
328        }
329
330        if (length > a) {
331            length = a;
332        }
333
334        if ((flags&GET_TEXT_WITH_STYLES) != 0) {
335            return content.subSequence(a - length, a);
336        }
337        return TextUtils.substring(content, a - length, a);
338    }
339
340    /**
341     * The default implementation returns the text currently selected, or null if none is
342     * selected.
343     */
344    public CharSequence getSelectedText(int flags) {
345        final Editable content = getEditable();
346        if (content == null) return null;
347
348        int a = Selection.getSelectionStart(content);
349        int b = Selection.getSelectionEnd(content);
350
351        if (a > b) {
352            int tmp = a;
353            a = b;
354            b = tmp;
355        }
356
357        if (a == b) return null;
358
359        if ((flags&GET_TEXT_WITH_STYLES) != 0) {
360            return content.subSequence(a, b);
361        }
362        return TextUtils.substring(content, a, b);
363    }
364
365    /**
366     * The default implementation returns the given amount of text from the
367     * current cursor position in the buffer.
368     */
369    public CharSequence getTextAfterCursor(int length, int flags) {
370        final Editable content = getEditable();
371        if (content == null) return null;
372
373        int a = Selection.getSelectionStart(content);
374        int b = Selection.getSelectionEnd(content);
375
376        if (a > b) {
377            int tmp = a;
378            a = b;
379            b = tmp;
380        }
381
382        // Guard against the case where the cursor has not been positioned yet.
383        if (b < 0) {
384            b = 0;
385        }
386
387        if (b + length > content.length()) {
388            length = content.length() - b;
389        }
390
391
392        if ((flags&GET_TEXT_WITH_STYLES) != 0) {
393            return content.subSequence(b, b + length);
394        }
395        return TextUtils.substring(content, b, b + length);
396    }
397
398    /**
399     * The default implementation turns this into the enter key.
400     */
401    public boolean performEditorAction(int actionCode) {
402        long eventTime = SystemClock.uptimeMillis();
403        sendKeyEvent(new KeyEvent(eventTime, eventTime,
404                KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
405                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
406                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
407                | KeyEvent.FLAG_EDITOR_ACTION));
408        sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
409                KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
410                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
411                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
412                | KeyEvent.FLAG_EDITOR_ACTION));
413        return true;
414    }
415
416    /**
417     * The default implementation does nothing.
418     */
419    public boolean performContextMenuAction(int id) {
420        return false;
421    }
422
423    /**
424     * The default implementation does nothing.
425     */
426    public boolean performPrivateCommand(String action, Bundle data) {
427        return false;
428    }
429
430    /**
431     * The default implementation places the given text into the editable,
432     * replacing any existing composing text.  The new text is marked as
433     * in a composing state with the composing style.
434     */
435    public boolean setComposingText(CharSequence text, int newCursorPosition) {
436        if (DEBUG) Log.v(TAG, "setComposingText " + text);
437        replaceText(text, newCursorPosition, true);
438        return true;
439    }
440
441    public boolean setComposingRegion(int start, int end) {
442        final Editable content = getEditable();
443        if (content != null) {
444            beginBatchEdit();
445            removeComposingSpans(content);
446            int a = start;
447            int b = end;
448            if (a > b) {
449                int tmp = a;
450                a = b;
451                b = tmp;
452            }
453            // Clip the end points to be within the content bounds.
454            final int length = content.length();
455            if (a < 0) a = 0;
456            if (b < 0) b = 0;
457            if (a > length) a = length;
458            if (b > length) b = length;
459
460            ensureDefaultComposingSpans();
461            if (mDefaultComposingSpans != null) {
462                for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
463                    content.setSpan(mDefaultComposingSpans[i], a, b,
464                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
465                }
466            }
467
468            content.setSpan(COMPOSING, a, b,
469                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
470
471            // Note: sendCurrentText does nothing unless mDummyMode is set
472            sendCurrentText();
473            endBatchEdit();
474        }
475        return true;
476    }
477
478    /**
479     * The default implementation changes the selection position in the
480     * current editable text.
481     */
482    public boolean setSelection(int start, int end) {
483        if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end);
484        final Editable content = getEditable();
485        if (content == null) return false;
486        int len = content.length();
487        if (start > len || end > len) {
488            // If the given selection is out of bounds, just ignore it.
489            // Most likely the text was changed out from under the IME,
490            // the the IME is going to have to update all of its state
491            // anyway.
492            return true;
493        }
494        if (start == end && MetaKeyKeyListener.getMetaState(content,
495                MetaKeyKeyListener.META_SELECTING) != 0) {
496            // If we are in selection mode, then we want to extend the
497            // selection instead of replacing it.
498            Selection.extendSelection(content, start);
499        } else {
500            Selection.setSelection(content, start, end);
501        }
502        return true;
503    }
504
505    /**
506     * Provides standard implementation for sending a key event to the window
507     * attached to the input connection's view.
508     */
509    public boolean sendKeyEvent(KeyEvent event) {
510        synchronized (mIMM.mH) {
511            ViewRootImpl viewRootImpl = mTargetView != null ? mTargetView.getViewRootImpl() : null;
512            if (viewRootImpl == null) {
513                if (mIMM.mServedView != null) {
514                    viewRootImpl = mIMM.mServedView.getViewRootImpl();
515                }
516            }
517            if (viewRootImpl != null) {
518                viewRootImpl.dispatchKeyFromIme(event);
519            }
520        }
521        return false;
522    }
523
524    /**
525     * Updates InputMethodManager with the current fullscreen mode.
526     */
527    public boolean reportFullscreenMode(boolean enabled) {
528        mIMM.setFullscreenMode(enabled);
529        return true;
530    }
531
532    private void sendCurrentText() {
533        if (!mDummyMode) {
534            return;
535        }
536
537        Editable content = getEditable();
538        if (content != null) {
539            final int N = content.length();
540            if (N == 0) {
541                return;
542            }
543            if (N == 1) {
544                // If it's 1 character, we have a chance of being
545                // able to generate normal key events...
546                if (mKeyCharacterMap == null) {
547                    mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
548                }
549                char[] chars = new char[1];
550                content.getChars(0, 1, chars, 0);
551                KeyEvent[] events = mKeyCharacterMap.getEvents(chars);
552                if (events != null) {
553                    for (int i=0; i<events.length; i++) {
554                        if (DEBUG) Log.v(TAG, "Sending: " + events[i]);
555                        sendKeyEvent(events[i]);
556                    }
557                    content.clear();
558                    return;
559                }
560            }
561
562            // Otherwise, revert to the special key event containing
563            // the actual characters.
564            KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(),
565                    content.toString(), KeyCharacterMap.VIRTUAL_KEYBOARD, 0);
566            sendKeyEvent(event);
567            content.clear();
568        }
569    }
570
571    private void ensureDefaultComposingSpans() {
572        if (mDefaultComposingSpans == null) {
573            Context context;
574            if (mTargetView != null) {
575                context = mTargetView.getContext();
576            } else if (mIMM.mServedView != null) {
577                context = mIMM.mServedView.getContext();
578            } else {
579                context = null;
580            }
581            if (context != null) {
582                TypedArray ta = context.getTheme()
583                        .obtainStyledAttributes(new int[] {
584                                com.android.internal.R.attr.candidatesTextStyleSpans
585                        });
586                CharSequence style = ta.getText(0);
587                ta.recycle();
588                if (style != null && style instanceof Spanned) {
589                    mDefaultComposingSpans = ((Spanned)style).getSpans(
590                            0, style.length(), Object.class);
591                }
592            }
593        }
594    }
595
596    private void replaceText(CharSequence text, int newCursorPosition,
597            boolean composing) {
598        final Editable content = getEditable();
599        if (content == null) {
600            return;
601        }
602
603        beginBatchEdit();
604
605        // delete composing text set previously.
606        int a = getComposingSpanStart(content);
607        int b = getComposingSpanEnd(content);
608
609        if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b);
610
611        if (b < a) {
612            int tmp = a;
613            a = b;
614            b = tmp;
615        }
616
617        if (a != -1 && b != -1) {
618            removeComposingSpans(content);
619        } else {
620            a = Selection.getSelectionStart(content);
621            b = Selection.getSelectionEnd(content);
622            if (a < 0) a = 0;
623            if (b < 0) b = 0;
624            if (b < a) {
625                int tmp = a;
626                a = b;
627                b = tmp;
628            }
629        }
630
631        if (composing) {
632            Spannable sp = null;
633            if (!(text instanceof Spannable)) {
634                sp = new SpannableStringBuilder(text);
635                text = sp;
636                ensureDefaultComposingSpans();
637                if (mDefaultComposingSpans != null) {
638                    for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
639                        sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(),
640                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
641                    }
642                }
643            } else {
644                sp = (Spannable)text;
645            }
646            setComposingSpans(sp);
647        }
648
649        if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \""
650                + text + "\", composing=" + composing
651                + ", type=" + text.getClass().getCanonicalName());
652
653        if (DEBUG) {
654            LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
655            lp.println("Current text:");
656            TextUtils.dumpSpans(content, lp, "  ");
657            lp.println("Composing text:");
658            TextUtils.dumpSpans(text, lp, "  ");
659        }
660
661        // Position the cursor appropriately, so that after replacing the
662        // desired range of text it will be located in the correct spot.
663        // This allows us to deal with filters performing edits on the text
664        // we are providing here.
665        if (newCursorPosition > 0) {
666            newCursorPosition += b - 1;
667        } else {
668            newCursorPosition += a;
669        }
670        if (newCursorPosition < 0) newCursorPosition = 0;
671        if (newCursorPosition > content.length())
672            newCursorPosition = content.length();
673        Selection.setSelection(content, newCursorPosition);
674
675        content.replace(a, b, text);
676
677        if (DEBUG) {
678            LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
679            lp.println("Final text:");
680            TextUtils.dumpSpans(content, lp, "  ");
681        }
682
683        endBatchEdit();
684    }
685}
686