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