Home | History | Annotate | Download | only in inputmethod
      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 
     17 package android.view.inputmethod;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.os.Bundle;
     22 import android.os.Handler;
     23 import android.os.SystemClock;
     24 import android.text.Editable;
     25 import android.text.NoCopySpan;
     26 import android.text.Selection;
     27 import android.text.Spannable;
     28 import android.text.SpannableStringBuilder;
     29 import android.text.Spanned;
     30 import android.text.TextUtils;
     31 import android.text.method.MetaKeyKeyListener;
     32 import android.util.Log;
     33 import android.util.LogPrinter;
     34 import android.view.KeyCharacterMap;
     35 import android.view.KeyEvent;
     36 import android.view.View;
     37 import android.view.ViewRoot;
     38 
     39 class 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  */
     48 public 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