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