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