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