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