summaryrefslogtreecommitdiff
path: root/android/text/method
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/text/method
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/text/method')
-rw-r--r--android/text/method/AllCapsTransformationMethod.java77
-rw-r--r--android/text/method/ArrowKeyMovementMethod.java337
-rw-r--r--android/text/method/BaseKeyListener.java524
-rw-r--r--android/text/method/BaseMovementMethod.java673
-rw-r--r--android/text/method/CharacterPickerDialog.java143
-rw-r--r--android/text/method/DateKeyListener.java128
-rw-r--r--android/text/method/DateTimeKeyListener.java139
-rw-r--r--android/text/method/DialerKeyListener.java117
-rw-r--r--android/text/method/DigitsKeyListener.java430
-rw-r--r--android/text/method/HideReturnsTransformationMethod.java52
-rw-r--r--android/text/method/KeyListener.java85
-rw-r--r--android/text/method/LinkMovementMethod.java257
-rw-r--r--android/text/method/MetaKeyKeyListener.java660
-rw-r--r--android/text/method/MovementMethod.java60
-rw-r--r--android/text/method/MultiTapKeyListener.java293
-rw-r--r--android/text/method/NumberKeyListener.java255
-rw-r--r--android/text/method/PasswordTransformationMethod.java266
-rw-r--r--android/text/method/QwertyKeyListener.java529
-rw-r--r--android/text/method/ReplacementTransformationMethod.java205
-rw-r--r--android/text/method/ScrollingMovementMethod.java121
-rw-r--r--android/text/method/SingleLineTransformationMethod.java53
-rw-r--r--android/text/method/TextKeyListener.java316
-rw-r--r--android/text/method/TimeKeyListener.java139
-rw-r--r--android/text/method/Touch.java213
-rw-r--r--android/text/method/TransformationMethod.java45
-rw-r--r--android/text/method/TransformationMethod2.java33
-rw-r--r--android/text/method/WordIterator.java388
27 files changed, 6538 insertions, 0 deletions
diff --git a/android/text/method/AllCapsTransformationMethod.java b/android/text/method/AllCapsTransformationMethod.java
new file mode 100644
index 00000000..c807e7da
--- /dev/null
+++ b/android/text/method/AllCapsTransformationMethod.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.Locale;
+
+/**
+ * Transforms source text into an ALL CAPS string, locale-aware.
+ *
+ * @hide
+ */
+public class AllCapsTransformationMethod implements TransformationMethod2 {
+ private static final String TAG = "AllCapsTransformationMethod";
+
+ private boolean mEnabled;
+ private Locale mLocale;
+
+ public AllCapsTransformationMethod(@NonNull Context context) {
+ mLocale = context.getResources().getConfiguration().getLocales().get(0);
+ }
+
+ @Override
+ public CharSequence getTransformation(@Nullable CharSequence source, View view) {
+ if (!mEnabled) {
+ Log.w(TAG, "Caller did not enable length changes; not transforming text");
+ return source;
+ }
+
+ if (source == null) {
+ return null;
+ }
+
+ Locale locale = null;
+ if (view instanceof TextView) {
+ locale = ((TextView)view).getTextLocale();
+ }
+ if (locale == null) {
+ locale = mLocale;
+ }
+ final boolean copySpans = source instanceof Spanned;
+ return TextUtils.toUpperCase(locale, source, copySpans);
+ }
+
+ @Override
+ public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ }
+
+ @Override
+ public void setLengthChangesAllowed(boolean allowLengthChanges) {
+ mEnabled = allowLengthChanges;
+ }
+
+}
diff --git a/android/text/method/ArrowKeyMovementMethod.java b/android/text/method/ArrowKeyMovementMethod.java
new file mode 100644
index 00000000..57fe1315
--- /dev/null
+++ b/android/text/method/ArrowKeyMovementMethod.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A movement method that provides cursor movement and selection.
+ * Supports displaying the context menu on DPad Center.
+ */
+public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
+ private static boolean isSelecting(Spannable buffer) {
+ return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
+ (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
+ }
+
+ private static int getCurrentLineTop(Spannable buffer, Layout layout) {
+ return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
+ }
+
+ private static int getPageHeight(TextView widget) {
+ // This calculation does not take into account the view transformations that
+ // may have been applied to the child or its containers. In case of scaling or
+ // rotation, the calculated page height may be incorrect.
+ final Rect rect = new Rect();
+ return widget.getGlobalVisibleRect(rect) ? rect.height() : 0;
+ }
+
+ @Override
+ protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
+ int movementMetaState, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0
+ && MetaKeyKeyListener.getMetaState(buffer,
+ MetaKeyKeyListener.META_SELECTING, event) != 0) {
+ return widget.showContextMenu();
+ }
+ }
+ break;
+ }
+ return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
+ }
+
+ @Override
+ protected boolean left(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendLeft(buffer, layout);
+ } else {
+ return Selection.moveLeft(buffer, layout);
+ }
+ }
+
+ @Override
+ protected boolean right(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendRight(buffer, layout);
+ } else {
+ return Selection.moveRight(buffer, layout);
+ }
+ }
+
+ @Override
+ protected boolean up(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendUp(buffer, layout);
+ } else {
+ return Selection.moveUp(buffer, layout);
+ }
+ }
+
+ @Override
+ protected boolean down(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendDown(buffer, layout);
+ } else {
+ return Selection.moveDown(buffer, layout);
+ }
+ }
+
+ @Override
+ protected boolean pageUp(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ final boolean selecting = isSelecting(buffer);
+ final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget);
+ boolean handled = false;
+ for (;;) {
+ final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
+ if (selecting) {
+ Selection.extendUp(buffer, layout);
+ } else {
+ Selection.moveUp(buffer, layout);
+ }
+ if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
+ break;
+ }
+ handled = true;
+ if (getCurrentLineTop(buffer, layout) <= targetY) {
+ break;
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ protected boolean pageDown(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ final boolean selecting = isSelecting(buffer);
+ final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget);
+ boolean handled = false;
+ for (;;) {
+ final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
+ if (selecting) {
+ Selection.extendDown(buffer, layout);
+ } else {
+ Selection.moveDown(buffer, layout);
+ }
+ if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
+ break;
+ }
+ handled = true;
+ if (getCurrentLineTop(buffer, layout) >= targetY) {
+ break;
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ protected boolean top(TextView widget, Spannable buffer) {
+ if (isSelecting(buffer)) {
+ Selection.extendSelection(buffer, 0);
+ } else {
+ Selection.setSelection(buffer, 0);
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean bottom(TextView widget, Spannable buffer) {
+ if (isSelecting(buffer)) {
+ Selection.extendSelection(buffer, buffer.length());
+ } else {
+ Selection.setSelection(buffer, buffer.length());
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean lineStart(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendToLeftEdge(buffer, layout);
+ } else {
+ return Selection.moveToLeftEdge(buffer, layout);
+ }
+ }
+
+ @Override
+ protected boolean lineEnd(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (isSelecting(buffer)) {
+ return Selection.extendToRightEdge(buffer, layout);
+ } else {
+ return Selection.moveToRightEdge(buffer, layout);
+ }
+ }
+
+ /** {@hide} */
+ @Override
+ protected boolean leftWord(TextView widget, Spannable buffer) {
+ final int selectionEnd = widget.getSelectionEnd();
+ final WordIterator wordIterator = widget.getWordIterator();
+ wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
+ return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer));
+ }
+
+ /** {@hide} */
+ @Override
+ protected boolean rightWord(TextView widget, Spannable buffer) {
+ final int selectionEnd = widget.getSelectionEnd();
+ final WordIterator wordIterator = widget.getWordIterator();
+ wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
+ return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer));
+ }
+
+ @Override
+ protected boolean home(TextView widget, Spannable buffer) {
+ return lineStart(widget, buffer);
+ }
+
+ @Override
+ protected boolean end(TextView widget, Spannable buffer) {
+ return lineEnd(widget, buffer);
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ int initialScrollX = -1;
+ int initialScrollY = -1;
+ final int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP) {
+ initialScrollX = Touch.getInitialScrollX(widget, buffer);
+ initialScrollY = Touch.getInitialScrollY(widget, buffer);
+ }
+
+ boolean wasTouchSelecting = isSelecting(buffer);
+ boolean handled = Touch.onTouchEvent(widget, buffer, event);
+
+ if (widget.didTouchFocusSelect()) {
+ return handled;
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ // For touch events, the code should run only when selection is active.
+ if (isSelecting(buffer)) {
+ if (!widget.isFocused()) {
+ if (!widget.requestFocus()) {
+ return handled;
+ }
+ }
+ int offset = widget.getOffsetForPosition(event.getX(), event.getY());
+ buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
+ // Disallow intercepting of the touch events, so that
+ // users can scroll and select at the same time.
+ // without this, users would get booted out of select
+ // mode once the view detected it needed to scroll.
+ widget.getParent().requestDisallowInterceptTouchEvent(true);
+ }
+ } else if (widget.isFocused()) {
+ if (action == MotionEvent.ACTION_MOVE) {
+ if (isSelecting(buffer) && handled) {
+ final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
+ // Before selecting, make sure we've moved out of the "slop".
+ // handled will be true, if we're in select mode AND we're
+ // OUT of the slop
+
+ // Turn long press off while we're selecting. User needs to
+ // re-tap on the selection to enable long press
+ widget.cancelLongPress();
+
+ // Update selection as we're moving the selection area.
+
+ // Get the current touch position
+ final int offset = widget.getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection(buffer, Math.min(startOffset, offset),
+ Math.max(startOffset, offset));
+ return true;
+ }
+ } else if (action == MotionEvent.ACTION_UP) {
+ // If we have scrolled, then the up shouldn't move the cursor,
+ // but we do need to make sure the cursor is still visible at
+ // the current scroll offset to avoid the scroll jumping later
+ // to show it.
+ if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
+ (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
+ widget.moveCursorToVisibleOffset();
+ return true;
+ }
+
+ if (wasTouchSelecting) {
+ final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
+ final int endOffset = widget.getOffsetForPosition(event.getX(), event.getY());
+ Selection.setSelection(buffer, Math.min(startOffset, endOffset),
+ Math.max(startOffset, endOffset));
+ buffer.removeSpan(LAST_TAP_DOWN);
+ }
+
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+
+ return true;
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean canSelectArbitrarily() {
+ return true;
+ }
+
+ @Override
+ public void initialize(TextView widget, Spannable text) {
+ Selection.setSelection(text, 0);
+ }
+
+ @Override
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
+ if (view.getLayout() == null) {
+ // This shouldn't be null, but do something sensible if it is.
+ Selection.setSelection(text, text.length());
+ }
+ } else {
+ Selection.setSelection(text, text.length());
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null) {
+ sInstance = new ArrowKeyMovementMethod();
+ }
+
+ return sInstance;
+ }
+
+ private static final Object LAST_TAP_DOWN = new Object();
+ private static ArrowKeyMovementMethod sInstance;
+}
diff --git a/android/text/method/BaseKeyListener.java b/android/text/method/BaseKeyListener.java
new file mode 100644
index 00000000..5f0a46d8
--- /dev/null
+++ b/android/text/method/BaseKeyListener.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Paint;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UProperty;
+import android.text.Editable;
+import android.text.Emoji;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.method.TextKeyListener.Capitalize;
+import android.text.style.ReplacementSpan;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.text.BreakIterator;
+
+/**
+ * Abstract base class for key listeners.
+ *
+ * Provides a basic foundation for entering and editing text.
+ * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
+ * characters as keys are pressed.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public abstract class BaseKeyListener extends MetaKeyKeyListener
+ implements KeyListener {
+ /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
+
+ private static final int LINE_FEED = 0x0A;
+ private static final int CARRIAGE_RETURN = 0x0D;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ static Paint sCachedPaint = null;
+
+ /**
+ * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
+ * a {@link TextView}. If there is a selection, deletes the selection; otherwise,
+ * deletes the character before the cursor, if any; ALT+DEL deletes everything on
+ * the line the cursor is on.
+ *
+ * @return true if anything was deleted; false otherwise.
+ */
+ public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
+ return backspaceOrForwardDelete(view, content, keyCode, event, false);
+ }
+
+ /**
+ * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
+ * key in a {@link TextView}. If there is a selection, deletes the selection; otherwise,
+ * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
+ * the line the cursor is on.
+ *
+ * @return true if anything was deleted; false otherwise.
+ */
+ public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
+ return backspaceOrForwardDelete(view, content, keyCode, event, true);
+ }
+
+ // Returns true if the given code point is a variation selector.
+ private static boolean isVariationSelector(int codepoint) {
+ return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR);
+ }
+
+ // Returns the offset of the replacement span edge if the offset is inside of the replacement
+ // span. Otherwise, does nothing and returns the input offset value.
+ private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) {
+ if (!(text instanceof Spanned)) {
+ return offset;
+ }
+
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class);
+ for (int i = 0; i < spans.length; i++) {
+ final int start = ((Spanned) text).getSpanStart(spans[i]);
+ final int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset) {
+ offset = moveToStart ? start : end;
+ }
+ }
+ return offset;
+ }
+
+ // Returns the start offset to be deleted by a backspace key from the given offset.
+ private static int getOffsetForBackspaceKey(CharSequence text, int offset) {
+ if (offset <= 1) {
+ return 0;
+ }
+
+ // Initial state
+ final int STATE_START = 0;
+
+ // The offset is immediately before line feed.
+ final int STATE_LF = 1;
+
+ // The offset is immediately before a KEYCAP.
+ final int STATE_BEFORE_KEYCAP = 2;
+ // The offset is immediately before a variation selector and a KEYCAP.
+ final int STATE_BEFORE_VS_AND_KEYCAP = 3;
+
+ // The offset is immediately before an emoji modifier.
+ final int STATE_BEFORE_EMOJI_MODIFIER = 4;
+ // The offset is immediately before a variation selector and an emoji modifier.
+ final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5;
+
+ // The offset is immediately before a variation selector.
+ final int STATE_BEFORE_VS = 6;
+
+ // The offset is immediately before an emoji.
+ final int STATE_BEFORE_EMOJI = 7;
+ // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
+ final int STATE_BEFORE_ZWJ = 8;
+ // The offset is immediately before a variation selector and a ZWJ that were seen before a
+ // ZWJ emoji.
+ final int STATE_BEFORE_VS_AND_ZWJ = 9;
+
+ // The number of following RIS code points is odd.
+ final int STATE_ODD_NUMBERED_RIS = 10;
+ // The number of following RIS code points is even.
+ final int STATE_EVEN_NUMBERED_RIS = 11;
+
+ // The offset is in emoji tag sequence.
+ final int STATE_IN_TAG_SEQUENCE = 12;
+
+ // The state machine has been stopped.
+ final int STATE_FINISHED = 13;
+
+ int deleteCharCount = 0; // Char count to be deleted by backspace.
+ int lastSeenVSCharCount = 0; // Char count of previous variation selector.
+
+ int state = STATE_START;
+
+ int tmpOffset = offset;
+ do {
+ final int codePoint = Character.codePointBefore(text, tmpOffset);
+ tmpOffset -= Character.charCount(codePoint);
+
+ switch (state) {
+ case STATE_START:
+ deleteCharCount = Character.charCount(codePoint);
+ if (codePoint == LINE_FEED) {
+ state = STATE_LF;
+ } else if (isVariationSelector(codePoint)) {
+ state = STATE_BEFORE_VS;
+ } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
+ state = STATE_ODD_NUMBERED_RIS;
+ } else if (Emoji.isEmojiModifier(codePoint)) {
+ state = STATE_BEFORE_EMOJI_MODIFIER;
+ } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
+ state = STATE_BEFORE_KEYCAP;
+ } else if (Emoji.isEmoji(codePoint)) {
+ state = STATE_BEFORE_EMOJI;
+ } else if (codePoint == Emoji.CANCEL_TAG) {
+ state = STATE_IN_TAG_SEQUENCE;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_LF:
+ if (codePoint == CARRIAGE_RETURN) {
+ ++deleteCharCount;
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_ODD_NUMBERED_RIS:
+ if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
+ deleteCharCount += 2; /* Char count of RIS */
+ state = STATE_EVEN_NUMBERED_RIS;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_EVEN_NUMBERED_RIS:
+ if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
+ deleteCharCount -= 2; /* Char count of RIS */
+ state = STATE_ODD_NUMBERED_RIS;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_BEFORE_KEYCAP:
+ if (isVariationSelector(codePoint)) {
+ lastSeenVSCharCount = Character.charCount(codePoint);
+ state = STATE_BEFORE_VS_AND_KEYCAP;
+ break;
+ }
+
+ if (Emoji.isKeycapBase(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint);
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_BEFORE_VS_AND_KEYCAP:
+ if (Emoji.isKeycapBase(codePoint)) {
+ deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_BEFORE_EMOJI_MODIFIER:
+ if (isVariationSelector(codePoint)) {
+ lastSeenVSCharCount = Character.charCount(codePoint);
+ state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
+ break;
+ } else if (Emoji.isEmojiModifierBase(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint);
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
+ if (Emoji.isEmojiModifierBase(codePoint)) {
+ deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_BEFORE_VS:
+ if (Emoji.isEmoji(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint);
+ state = STATE_BEFORE_EMOJI;
+ break;
+ }
+
+ if (!isVariationSelector(codePoint) &&
+ UCharacter.getCombiningClass(codePoint) == 0) {
+ deleteCharCount += Character.charCount(codePoint);
+ }
+ state = STATE_FINISHED;
+ break;
+ case STATE_BEFORE_EMOJI:
+ if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
+ state = STATE_BEFORE_ZWJ;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_BEFORE_ZWJ:
+ if (Emoji.isEmoji(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ.
+ state = Emoji.isEmojiModifier(codePoint) ?
+ STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI;
+ } else if (isVariationSelector(codePoint)) {
+ lastSeenVSCharCount = Character.charCount(codePoint);
+ state = STATE_BEFORE_VS_AND_ZWJ;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_BEFORE_VS_AND_ZWJ:
+ if (Emoji.isEmoji(codePoint)) {
+ // +1 for ZWJ.
+ deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
+ lastSeenVSCharCount = 0;
+ state = STATE_BEFORE_EMOJI;
+ } else {
+ state = STATE_FINISHED;
+ }
+ break;
+ case STATE_IN_TAG_SEQUENCE:
+ if (Emoji.isTagSpecChar(codePoint)) {
+ deleteCharCount += 2; /* Char count of emoji tag spec character. */
+ // Keep the same state.
+ } else if (Emoji.isEmoji(codePoint)) {
+ deleteCharCount += Character.charCount(codePoint);
+ state = STATE_FINISHED;
+ } else {
+ // Couldn't find tag_base character. Delete the last tag_term character.
+ deleteCharCount = 2; // for U+E007F
+ state = STATE_FINISHED;
+ }
+ // TODO: Need handle emoji variation selectors. Issue 35224297
+ break;
+ default:
+ throw new IllegalArgumentException("state " + state + " is unknown");
+ }
+ } while (tmpOffset > 0 && state != STATE_FINISHED);
+
+ return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
+ }
+
+ // Returns the end offset to be deleted by a forward delete key from the given offset.
+ private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
+ final int len = text.length();
+
+ if (offset >= len - 1) {
+ return len;
+ }
+
+ offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
+ offset, Paint.CURSOR_AFTER);
+
+ return adjustReplacementSpan(text, offset, false /* move to the end */);
+ }
+
+ private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
+ KeyEvent event, boolean isForwardDelete) {
+ // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
+ if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
+ & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
+ return false;
+ }
+
+ // If there is a current selection, delete it.
+ if (deleteSelection(view, content)) {
+ return true;
+ }
+
+ // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
+ boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
+ boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
+ boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
+
+ if (isCtrlActive) {
+ if (isAltActive || isShiftActive) {
+ // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
+ return false;
+ }
+ return deleteUntilWordBoundary(view, content, isForwardDelete);
+ }
+
+ // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
+ if (isAltActive && deleteLine(view, content)) {
+ return true;
+ }
+
+ // Delete a character.
+ final int start = Selection.getSelectionEnd(content);
+ final int end;
+ if (isForwardDelete) {
+ final Paint paint;
+ if (view instanceof TextView) {
+ paint = ((TextView)view).getPaint();
+ } else {
+ synchronized (mLock) {
+ if (sCachedPaint == null) {
+ sCachedPaint = new Paint();
+ }
+ paint = sCachedPaint;
+ }
+ }
+ end = getOffsetForForwardDeleteKey(content, start, paint);
+ } else {
+ end = getOffsetForBackspaceKey(content, start);
+ }
+ if (start != end) {
+ content.delete(Math.min(start, end), Math.max(start, end));
+ return true;
+ }
+ return false;
+ }
+
+ private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
+ int currentCursorOffset = Selection.getSelectionStart(content);
+
+ // If there is a selection, do nothing.
+ if (currentCursorOffset != Selection.getSelectionEnd(content)) {
+ return false;
+ }
+
+ // Early exit if there is no contents to delete.
+ if ((!isForwardDelete && currentCursorOffset == 0) ||
+ (isForwardDelete && currentCursorOffset == content.length())) {
+ return false;
+ }
+
+ WordIterator wordIterator = null;
+ if (view instanceof TextView) {
+ wordIterator = ((TextView)view).getWordIterator();
+ }
+
+ if (wordIterator == null) {
+ // Default locale is used for WordIterator since the appropriate locale is not clear
+ // here.
+ // TODO: Use appropriate locale for WordIterator.
+ wordIterator = new WordIterator();
+ }
+
+ int deleteFrom;
+ int deleteTo;
+
+ if (isForwardDelete) {
+ deleteFrom = currentCursorOffset;
+ wordIterator.setCharSequence(content, deleteFrom, content.length());
+ deleteTo = wordIterator.following(currentCursorOffset);
+ if (deleteTo == BreakIterator.DONE) {
+ deleteTo = content.length();
+ }
+ } else {
+ deleteTo = currentCursorOffset;
+ wordIterator.setCharSequence(content, 0, deleteTo);
+ deleteFrom = wordIterator.preceding(currentCursorOffset);
+ if (deleteFrom == BreakIterator.DONE) {
+ deleteFrom = 0;
+ }
+ }
+ content.delete(deleteFrom, deleteTo);
+ return true;
+ }
+
+ private boolean deleteSelection(View view, Editable content) {
+ int selectionStart = Selection.getSelectionStart(content);
+ int selectionEnd = Selection.getSelectionEnd(content);
+ if (selectionEnd < selectionStart) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+ if (selectionStart != selectionEnd) {
+ content.delete(selectionStart, selectionEnd);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean deleteLine(View view, Editable content) {
+ if (view instanceof TextView) {
+ final Layout layout = ((TextView) view).getLayout();
+ if (layout != null) {
+ final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
+ final int start = layout.getLineStart(line);
+ final int end = layout.getLineEnd(line);
+ if (end != start) {
+ content.delete(start, end);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ static int makeTextContentType(Capitalize caps, boolean autoText) {
+ int contentType = InputType.TYPE_CLASS_TEXT;
+ switch (caps) {
+ case CHARACTERS:
+ contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ break;
+ case WORDS:
+ contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
+ break;
+ case SENTENCES:
+ contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ break;
+ }
+ if (autoText) {
+ contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
+ }
+ return contentType;
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ boolean handled;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DEL:
+ handled = backspace(view, content, keyCode, event);
+ break;
+ case KeyEvent.KEYCODE_FORWARD_DEL:
+ handled = forwardDelete(view, content, keyCode, event);
+ break;
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled) {
+ adjustMetaAfterKeypress(content);
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ /**
+ * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
+ * the event's text into the content.
+ */
+ public boolean onKeyOther(View view, Editable content, KeyEvent event) {
+ if (event.getAction() != KeyEvent.ACTION_MULTIPLE
+ || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
+ // Not something we are interested in.
+ return false;
+ }
+
+ int selectionStart = Selection.getSelectionStart(content);
+ int selectionEnd = Selection.getSelectionEnd(content);
+ if (selectionEnd < selectionStart) {
+ int temp = selectionEnd;
+ selectionEnd = selectionStart;
+ selectionStart = temp;
+ }
+
+ CharSequence text = event.getCharacters();
+ if (text == null) {
+ return false;
+ }
+
+ content.replace(selectionStart, selectionEnd, text);
+ return true;
+ }
+}
diff --git a/android/text/method/BaseMovementMethod.java b/android/text/method/BaseMovementMethod.java
new file mode 100644
index 00000000..155a2c4f
--- /dev/null
+++ b/android/text/method/BaseMovementMethod.java
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Layout;
+import android.text.Spannable;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * Base classes for movement methods.
+ */
+public class BaseMovementMethod implements MovementMethod {
+ @Override
+ public boolean canSelectArbitrarily() {
+ return false;
+ }
+
+ @Override
+ public void initialize(TextView widget, Spannable text) {
+ }
+
+ @Override
+ public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
+ final int movementMetaState = getMovementMetaState(text, event);
+ boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event);
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(text);
+ MetaKeyKeyListener.resetLockedMeta(text);
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) {
+ final int movementMetaState = getMovementMetaState(text, event);
+ final int keyCode = event.getKeyCode();
+ if (keyCode != KeyEvent.KEYCODE_UNKNOWN
+ && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+ final int repeat = event.getRepeatCount();
+ boolean handled = false;
+ for (int i = 0; i < repeat; i++) {
+ if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) {
+ break;
+ }
+ handled = true;
+ }
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(text);
+ MetaKeyKeyListener.resetLockedMeta(text);
+ }
+ return handled;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onTakeFocus(TextView widget, Spannable text, int direction) {
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
+ if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_SCROLL: {
+ final float vscroll;
+ final float hscroll;
+ if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+ vscroll = 0;
+ hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ } else {
+ vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ }
+
+ boolean handled = false;
+ if (hscroll < 0) {
+ handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll));
+ } else if (hscroll > 0) {
+ handled |= scrollRight(widget, text, (int)Math.ceil(hscroll));
+ }
+ if (vscroll < 0) {
+ handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll));
+ } else if (vscroll > 0) {
+ handled |= scrollDown(widget, text, (int)Math.ceil(vscroll));
+ }
+ return handled;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the meta state used for movement using the modifiers tracked by the text
+ * buffer as well as those present in the key event.
+ *
+ * The movement meta state excludes the state of locked modifiers or the SHIFT key
+ * since they are not used by movement actions (but they may be used for selection).
+ *
+ * @param buffer The text buffer.
+ * @param event The key event.
+ * @return The keyboard meta states used for movement.
+ */
+ protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
+ // We ignore locked modifiers and SHIFT.
+ int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
+ & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED);
+ return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK;
+ }
+
+ /**
+ * Performs a movement key action.
+ * The default implementation decodes the key down and invokes movement actions
+ * such as {@link #down} and {@link #up}.
+ * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once
+ * to handle an {@link KeyEvent#ACTION_DOWN}.
+ * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly
+ * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @param event The key event.
+ * @param keyCode The key code.
+ * @param movementMetaState The keyboard meta states used for movement.
+ * @param event The key event.
+ * @return True if the event was handled.
+ */
+ protected boolean handleMovementKey(TextView widget, Spannable buffer,
+ int keyCode, int movementMetaState, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return left(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_CTRL_ON)) {
+ return leftWord(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return lineStart(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return right(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_CTRL_ON)) {
+ return rightWord(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return lineEnd(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return up(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return top(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return down(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return bottom(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_UP:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return pageUp(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return top(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_PAGE_DOWN:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return pageDown(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_ALT_ON)) {
+ return bottom(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_HOME:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return home(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_CTRL_ON)) {
+ return top(widget, buffer);
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MOVE_END:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ return end(widget, buffer);
+ } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
+ KeyEvent.META_CTRL_ON)) {
+ return bottom(widget, buffer);
+ }
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a left movement action.
+ * Moves the cursor or scrolls left by one character.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean left(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a right movement action.
+ * Moves the cursor or scrolls right by one character.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean right(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs an up movement action.
+ * Moves the cursor or scrolls up by one line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean up(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a down movement action.
+ * Moves the cursor or scrolls down by one line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean down(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a page-up movement action.
+ * Moves the cursor or scrolls up by one page.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean pageUp(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a page-down movement action.
+ * Moves the cursor or scrolls down by one page.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean pageDown(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a top movement action.
+ * Moves the cursor or scrolls to the top of the buffer.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean top(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a bottom movement action.
+ * Moves the cursor or scrolls to the bottom of the buffer.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean bottom(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a line-start movement action.
+ * Moves the cursor or scrolls to the start of the line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean lineStart(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a line-end movement action.
+ * Moves the cursor or scrolls to the end of the line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean lineEnd(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /** {@hide} */
+ protected boolean leftWord(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /** {@hide} */
+ protected boolean rightWord(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs a home movement action.
+ * Moves the cursor or scrolls to the start of the line or to the top of the
+ * document depending on whether the insertion point is being moved or
+ * the document is being scrolled.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean home(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ /**
+ * Performs an end movement action.
+ * Moves the cursor or scrolls to the start of the line or to the top of the
+ * document depending on whether the insertion point is being moved or
+ * the document is being scrolled.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ */
+ protected boolean end(TextView widget, Spannable buffer) {
+ return false;
+ }
+
+ private int getTopLine(TextView widget) {
+ return widget.getLayout().getLineForVertical(widget.getScrollY());
+ }
+
+ private int getBottomLine(TextView widget) {
+ return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
+ }
+
+ private int getInnerWidth(TextView widget) {
+ return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
+ }
+
+ private int getInnerHeight(TextView widget) {
+ return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
+ }
+
+ private int getCharacterWidth(TextView widget) {
+ return (int) Math.ceil(widget.getPaint().getFontSpacing());
+ }
+
+ private int getScrollBoundsLeft(TextView widget) {
+ final Layout layout = widget.getLayout();
+ final int topLine = getTopLine(widget);
+ final int bottomLine = getBottomLine(widget);
+ if (topLine > bottomLine) {
+ return 0;
+ }
+ int left = Integer.MAX_VALUE;
+ for (int line = topLine; line <= bottomLine; line++) {
+ final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
+ if (lineLeft < left) {
+ left = lineLeft;
+ }
+ }
+ return left;
+ }
+
+ private int getScrollBoundsRight(TextView widget) {
+ final Layout layout = widget.getLayout();
+ final int topLine = getTopLine(widget);
+ final int bottomLine = getBottomLine(widget);
+ if (topLine > bottomLine) {
+ return 0;
+ }
+ int right = Integer.MIN_VALUE;
+ for (int line = topLine; line <= bottomLine; line++) {
+ final int lineRight = (int) Math.ceil(layout.getLineRight(line));
+ if (lineRight > right) {
+ right = lineRight;
+ }
+ }
+ return right;
+ }
+
+ /**
+ * Performs a scroll left action.
+ * Scrolls left by the specified number of characters.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @param amount The number of characters to scroll by. Must be at least 1.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
+ final int minScrollX = getScrollBoundsLeft(widget);
+ int scrollX = widget.getScrollX();
+ if (scrollX > minScrollX) {
+ scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
+ widget.scrollTo(scrollX, widget.getScrollY());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll right action.
+ * Scrolls right by the specified number of characters.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @param amount The number of characters to scroll by. Must be at least 1.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
+ final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
+ int scrollX = widget.getScrollX();
+ if (scrollX < maxScrollX) {
+ scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
+ widget.scrollTo(scrollX, widget.getScrollY());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll up action.
+ * Scrolls up by the specified number of lines.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @param amount The number of lines to scroll by. Must be at least 1.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
+ final Layout layout = widget.getLayout();
+ final int top = widget.getScrollY();
+ int topLine = layout.getLineForVertical(top);
+ if (layout.getLineTop(topLine) == top) {
+ // If the top line is partially visible, bring it all the way
+ // into view; otherwise, bring the previous line into view.
+ topLine -= 1;
+ }
+ if (topLine >= 0) {
+ topLine = Math.max(topLine - amount + 1, 0);
+ Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll down action.
+ * Scrolls down by the specified number of lines.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @param amount The number of lines to scroll by. Must be at least 1.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
+ final Layout layout = widget.getLayout();
+ final int innerHeight = getInnerHeight(widget);
+ final int bottom = widget.getScrollY() + innerHeight;
+ int bottomLine = layout.getLineForVertical(bottom);
+ if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
+ // Less than a pixel of this line is out of view,
+ // so we must have tried to make it entirely in view
+ // and now want the next line to be in view instead.
+ bottomLine += 1;
+ }
+ final int limit = layout.getLineCount() - 1;
+ if (bottomLine <= limit) {
+ bottomLine = Math.min(bottomLine + amount - 1, limit);
+ Touch.scrollTo(widget, layout, widget.getScrollX(),
+ layout.getLineTop(bottomLine + 1) - innerHeight);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll page up action.
+ * Scrolls up by one page.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollPageUp(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ final int top = widget.getScrollY() - getInnerHeight(widget);
+ int topLine = layout.getLineForVertical(top);
+ if (topLine >= 0) {
+ Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll page up action.
+ * Scrolls down by one page.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollPageDown(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ final int innerHeight = getInnerHeight(widget);
+ final int bottom = widget.getScrollY() + innerHeight + innerHeight;
+ int bottomLine = layout.getLineForVertical(bottom);
+ if (bottomLine <= layout.getLineCount() - 1) {
+ Touch.scrollTo(widget, layout, widget.getScrollX(),
+ layout.getLineTop(bottomLine + 1) - innerHeight);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll to top action.
+ * Scrolls to the top of the document.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollTop(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ if (getTopLine(widget) >= 0) {
+ Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll to bottom action.
+ * Scrolls to the bottom of the document.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollBottom(TextView widget, Spannable buffer) {
+ final Layout layout = widget.getLayout();
+ final int lineCount = layout.getLineCount();
+ if (getBottomLine(widget) <= lineCount - 1) {
+ Touch.scrollTo(widget, layout, widget.getScrollX(),
+ layout.getLineTop(lineCount) - getInnerHeight(widget));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll to line start action.
+ * Scrolls to the start of the line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollLineStart(TextView widget, Spannable buffer) {
+ final int minScrollX = getScrollBoundsLeft(widget);
+ int scrollX = widget.getScrollX();
+ if (scrollX > minScrollX) {
+ widget.scrollTo(minScrollX, widget.getScrollY());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs a scroll to line end action.
+ * Scrolls to the end of the line.
+ *
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ * @return True if the event was handled.
+ * @hide
+ */
+ protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
+ final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
+ int scrollX = widget.getScrollX();
+ if (scrollX < maxScrollX) {
+ widget.scrollTo(maxScrollX, widget.getScrollY());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/android/text/method/CharacterPickerDialog.java b/android/text/method/CharacterPickerDialog.java
new file mode 100644
index 00000000..7d838e06
--- /dev/null
+++ b/android/text/method/CharacterPickerDialog.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.GridView;
+
+import com.android.internal.R;
+
+/**
+ * Dialog for choosing accented characters related to a base character.
+ */
+public class CharacterPickerDialog extends Dialog
+ implements OnItemClickListener, OnClickListener {
+ private View mView;
+ private Editable mText;
+ private String mOptions;
+ private boolean mInsert;
+ private LayoutInflater mInflater;
+ private Button mCancelButton;
+
+ /**
+ * Creates a new CharacterPickerDialog that presents the specified
+ * <code>options</code> for insertion or replacement (depending on
+ * the sense of <code>insert</code>) into <code>text</code>.
+ */
+ public CharacterPickerDialog(Context context, View view,
+ Editable text, String options,
+ boolean insert) {
+ super(context, com.android.internal.R.style.Theme_Panel);
+
+ mView = view;
+ mText = text;
+ mOptions = options;
+ mInsert = insert;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ WindowManager.LayoutParams params = getWindow().getAttributes();
+ params.token = mView.getApplicationWindowToken();
+ params.type = params.TYPE_APPLICATION_ATTACHED_DIALOG;
+ params.flags = params.flags | Window.FEATURE_NO_TITLE;
+
+ setContentView(R.layout.character_picker);
+
+ GridView grid = (GridView) findViewById(R.id.characterPicker);
+ grid.setAdapter(new OptionsAdapter(getContext()));
+ grid.setOnItemClickListener(this);
+
+ mCancelButton = (Button) findViewById(R.id.cancel);
+ mCancelButton.setOnClickListener(this);
+ }
+
+ /**
+ * Handles clicks on the character buttons.
+ */
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ String result = String.valueOf(mOptions.charAt(position));
+ replaceCharacterAndClose(result);
+ }
+
+ private void replaceCharacterAndClose(CharSequence replace) {
+ int selEnd = Selection.getSelectionEnd(mText);
+ if (mInsert || selEnd == 0) {
+ mText.insert(selEnd, replace);
+ } else {
+ mText.replace(selEnd - 1, selEnd, replace);
+ }
+
+ dismiss();
+ }
+
+ /**
+ * Handles clicks on the Cancel button.
+ */
+ public void onClick(View v) {
+ if (v == mCancelButton) {
+ dismiss();
+ } else if (v instanceof Button) {
+ CharSequence result = ((Button) v).getText();
+ replaceCharacterAndClose(result);
+ }
+ }
+
+ private class OptionsAdapter extends BaseAdapter {
+
+ public OptionsAdapter(Context context) {
+ super();
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Button b = (Button)
+ mInflater.inflate(R.layout.character_picker_button, null);
+ b.setText(String.valueOf(mOptions.charAt(position)));
+ b.setOnClickListener(CharacterPickerDialog.this);
+ return b;
+ }
+
+ public final int getCount() {
+ return mOptions.length();
+ }
+
+ public final Object getItem(int position) {
+ return String.valueOf(mOptions.charAt(position));
+ }
+
+ public final long getItemId(int position) {
+ return position;
+ }
+ }
+}
diff --git a/android/text/method/DateKeyListener.java b/android/text/method/DateKeyListener.java
new file mode 100644
index 00000000..0accbf6c
--- /dev/null
+++ b/android/text/method/DateKeyListener.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.InputType;
+import android.view.KeyEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+/**
+ * For entering dates in a text field.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class DateKeyListener extends NumberKeyListener
+{
+ public int getInputType() {
+ if (mNeedsAdvancedInput) {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+ } else {
+ return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE;
+ }
+ }
+
+ @Override
+ @NonNull
+ protected char[] getAcceptedChars() {
+ return mCharacters;
+ }
+
+ /**
+ * @deprecated Use {@link #DateKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public DateKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "yMLd";
+ private static final String[] SKELETONS = {"yMd", "yM", "Md"};
+
+ public DateKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet<Character> chars = new LinkedHashSet<>();
+ // First add the digits, then add all the non-pattern characters seen in the pattern for
+ // "yMd", which is supposed to only have numerical fields.
+ final boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeletons(
+ chars, locale, SKELETONS, SYMBOLS_TO_IGNORE);
+ if (success) {
+ mCharacters = NumberKeyListener.collectionToArray(chars);
+ mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+ } else {
+ mCharacters = CHARACTERS;
+ mNeedsAdvancedInput = false;
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DateKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of DateKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static DateKeyListener getInstance(@Nullable Locale locale) {
+ DateKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new DateKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
+ */
+ @Deprecated
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '/', '-', '.'
+ };
+
+ private final char[] mCharacters;
+ private final boolean mNeedsAdvancedInput;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap<Locale, DateKeyListener> sInstanceCache = new HashMap<>();
+}
diff --git a/android/text/method/DateTimeKeyListener.java b/android/text/method/DateTimeKeyListener.java
new file mode 100644
index 00000000..1593db5d
--- /dev/null
+++ b/android/text/method/DateTimeKeyListener.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.InputType;
+import android.view.KeyEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+/**
+ * For entering dates and times in the same text field.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class DateTimeKeyListener extends NumberKeyListener
+{
+ public int getInputType() {
+ if (mNeedsAdvancedInput) {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+ } else {
+ return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL;
+ }
+ }
+
+ @Override
+ @NonNull
+ protected char[] getAcceptedChars()
+ {
+ return mCharacters;
+ }
+
+ /**
+ * @deprecated Use {@link #DateTimeKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public DateTimeKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "yMLdahHKkms";
+ private static final String SKELETON_12HOUR = "yMdhms";
+ private static final String SKELETON_24HOUR = "yMdHms";
+
+ public DateTimeKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet<Character> chars = new LinkedHashSet<>();
+ // First add the digits. Then, add all the character in AM and PM markers. Finally, add all
+ // the non-pattern characters seen in the patterns for "yMdhms" and "yMdHms".
+ final boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addAmPmChars(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
+ if (success) {
+ mCharacters = NumberKeyListener.collectionToArray(chars);
+ if (locale != null && "en".equals(locale.getLanguage())) {
+ // For backward compatibility reasons, assume we don't need advanced input for
+ // English locales, although English locales literally also need a comma and perhaps
+ // uppercase letters for AM and PM.
+ mNeedsAdvancedInput = false;
+ } else {
+ mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+ }
+ } else {
+ mCharacters = CHARACTERS;
+ mNeedsAdvancedInput = false;
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DateTimeKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of DateTimeKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static DateTimeKeyListener getInstance(@Nullable Locale locale) {
+ DateTimeKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new DateTimeKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
+ 'p', ':', '/', '-', ' '
+ };
+
+ private final char[] mCharacters;
+ private final boolean mNeedsAdvancedInput;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap<Locale, DateTimeKeyListener> sInstanceCache = new HashMap<>();
+}
diff --git a/android/text/method/DialerKeyListener.java b/android/text/method/DialerKeyListener.java
new file mode 100644
index 00000000..17abed6c
--- /dev/null
+++ b/android/text/method/DialerKeyListener.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.InputType;
+import android.text.Spannable;
+import android.view.KeyCharacterMap.KeyData;
+import android.view.KeyEvent;
+
+/**
+ * For dialing-only text entry
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class DialerKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DialerKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DialerKeyListener();
+ return sInstance;
+ }
+
+ public int getInputType() {
+ return InputType.TYPE_CLASS_PHONE;
+ }
+
+ /**
+ * Overrides the superclass's lookup method to prefer the number field
+ * from the KeyEvent.
+ */
+ protected int lookup(KeyEvent event, Spannable content) {
+ int meta = getMetaState(content, event);
+ int number = event.getNumber();
+
+ /*
+ * Prefer number if no meta key is active, or if it produces something
+ * valid and the meta lookup does not.
+ */
+ if ((meta & (MetaKeyKeyListener.META_ALT_ON | MetaKeyKeyListener.META_SHIFT_ON)) == 0) {
+ if (number != 0) {
+ return number;
+ }
+ }
+
+ int match = super.lookup(event, content);
+
+ if (match != 0) {
+ return match;
+ } else {
+ /*
+ * If a meta key is active but the lookup with the meta key
+ * did not produce anything, try some other meta keys, because
+ * the user might have pressed SHIFT when they meant ALT,
+ * or vice versa.
+ */
+
+ if (meta != 0) {
+ KeyData kd = new KeyData();
+ char[] accepted = getAcceptedChars();
+
+ if (event.getKeyData(kd)) {
+ for (int i = 1; i < kd.meta.length; i++) {
+ if (ok(accepted, kd.meta[i])) {
+ return kd.meta[i];
+ }
+ }
+ }
+ }
+
+ /*
+ * Otherwise, use the number associated with the key, since
+ * whatever they wanted to do with the meta key does not
+ * seem to be valid here.
+ */
+
+ return number;
+ }
+ }
+
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*',
+ '+', '-', '(', ')', ',', '/', 'N', '.', ' ', ';'
+ };
+
+ private static DialerKeyListener sInstance;
+}
diff --git a/android/text/method/DigitsKeyListener.java b/android/text/method/DigitsKeyListener.java
new file mode 100644
index 00000000..d9f2dcf2
--- /dev/null
+++ b/android/text/method/DigitsKeyListener.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UProperty;
+import android.icu.text.DecimalFormatSymbols;
+import android.text.InputType;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.view.KeyEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+/**
+ * For digits-only text entry
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class DigitsKeyListener extends NumberKeyListener
+{
+ private char[] mAccepted;
+ private boolean mNeedsAdvancedInput;
+ private final boolean mSign;
+ private final boolean mDecimal;
+ private final boolean mStringMode;
+ @Nullable
+ private final Locale mLocale;
+
+ private static final String DEFAULT_DECIMAL_POINT_CHARS = ".";
+ private static final String DEFAULT_SIGN_CHARS = "-+";
+
+ private static final char HYPHEN_MINUS = '-';
+ // Various locales use this as minus sign
+ private static final char MINUS_SIGN = '\u2212';
+ // Slovenian uses this as minus sign (a bug?): http://unicode.org/cldr/trac/ticket/10050
+ private static final char EN_DASH = '\u2013';
+
+ private String mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
+ private String mSignChars = DEFAULT_SIGN_CHARS;
+
+ private static final int SIGN = 1;
+ private static final int DECIMAL = 2;
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return mAccepted;
+ }
+
+ /**
+ * The characters that are used in compatibility mode.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ private static final char[][] COMPATIBILITY_CHARACTERS = {
+ { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
+ { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
+ { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
+ { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
+ };
+
+ private boolean isSignChar(final char c) {
+ return mSignChars.indexOf(c) != -1;
+ }
+
+ private boolean isDecimalPointChar(final char c) {
+ return mDecimalPointChars.indexOf(c) != -1;
+ }
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9.
+ *
+ * @deprecated Use {@link #DigitsKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public DigitsKeyListener() {
+ this(null, false, false);
+ }
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
+ * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
+ * (only one per field) if specified.
+ *
+ * @deprecated Use {@link #DigitsKeyListener(Locale, boolean, boolean)} instead.
+ */
+ @Deprecated
+ public DigitsKeyListener(boolean sign, boolean decimal) {
+ this(null, sign, decimal);
+ }
+
+ public DigitsKeyListener(@Nullable Locale locale) {
+ this(locale, false, false);
+ }
+
+ private void setToCompat() {
+ mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS;
+ mSignChars = DEFAULT_SIGN_CHARS;
+ final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
+ mAccepted = COMPATIBILITY_CHARACTERS[kind];
+ mNeedsAdvancedInput = false;
+ }
+
+ private void calculateNeedForAdvancedInput() {
+ final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0);
+ mNeedsAdvancedInput = !ArrayUtils.containsAll(COMPATIBILITY_CHARACTERS[kind], mAccepted);
+ }
+
+ // Takes a sign string and strips off its bidi controls, if any.
+ @NonNull
+ private static String stripBidiControls(@NonNull String sign) {
+ // For the sake of simplicity, we operate on code units, since all bidi controls are
+ // in the BMP. We also expect the string to be very short (almost always 1 character), so we
+ // don't need to use StringBuilder.
+ String result = "";
+ for (int i = 0; i < sign.length(); i++) {
+ final char c = sign.charAt(i);
+ if (!UCharacter.hasBinaryProperty(c, UProperty.BIDI_CONTROL)) {
+ if (result.isEmpty()) {
+ result = String.valueOf(c);
+ } else {
+ // This should happen very rarely, only if we have a multi-character sign,
+ // or a sign outside BMP.
+ result += c;
+ }
+ }
+ }
+ return result;
+ }
+
+ public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) {
+ mSign = sign;
+ mDecimal = decimal;
+ mStringMode = false;
+ mLocale = locale;
+ if (locale == null) {
+ setToCompat();
+ return;
+ }
+ LinkedHashSet<Character> chars = new LinkedHashSet<>();
+ final boolean success = NumberKeyListener.addDigits(chars, locale);
+ if (!success) {
+ setToCompat();
+ return;
+ }
+ if (sign || decimal) {
+ final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
+ if (sign) {
+ final String minusString = stripBidiControls(symbols.getMinusSignString());
+ final String plusString = stripBidiControls(symbols.getPlusSignString());
+ if (minusString.length() > 1 || plusString.length() > 1) {
+ // non-BMP and multi-character signs are not supported.
+ setToCompat();
+ return;
+ }
+ final char minus = minusString.charAt(0);
+ final char plus = plusString.charAt(0);
+ chars.add(Character.valueOf(minus));
+ chars.add(Character.valueOf(plus));
+ mSignChars = "" + minus + plus;
+
+ if (minus == MINUS_SIGN || minus == EN_DASH) {
+ // If the minus sign is U+2212 MINUS SIGN or U+2013 EN DASH, we also need to
+ // accept the ASCII hyphen-minus.
+ chars.add(HYPHEN_MINUS);
+ mSignChars += HYPHEN_MINUS;
+ }
+ }
+ if (decimal) {
+ final String separatorString = symbols.getDecimalSeparatorString();
+ if (separatorString.length() > 1) {
+ // non-BMP and multi-character decimal separators are not supported.
+ setToCompat();
+ return;
+ }
+ final Character separatorChar = Character.valueOf(separatorString.charAt(0));
+ chars.add(separatorChar);
+ mDecimalPointChars = separatorChar.toString();
+ }
+ }
+ mAccepted = NumberKeyListener.collectionToArray(chars);
+ calculateNeedForAdvancedInput();
+ }
+
+ private DigitsKeyListener(@NonNull final String accepted) {
+ mSign = false;
+ mDecimal = false;
+ mStringMode = true;
+ mLocale = null;
+ mAccepted = new char[accepted.length()];
+ accepted.getChars(0, accepted.length(), mAccepted, 0);
+ // Theoretically we may need advanced input, but for backward compatibility, we don't change
+ // the input type.
+ mNeedsAdvancedInput = false;
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9.
+ *
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DigitsKeyListener getInstance() {
+ return getInstance(false, false);
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus
+ * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point
+ * (only one per field) if specified.
+ *
+ * @deprecated Use {@link #getInstance(Locale, boolean, boolean)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
+ return getInstance(null, sign, decimal);
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the locale-appropriate digits.
+ */
+ @NonNull
+ public static DigitsKeyListener getInstance(@Nullable Locale locale) {
+ return getInstance(locale, false, false);
+ }
+
+ private static final Object sLocaleCacheLock = new Object();
+ @GuardedBy("sLocaleCacheLock")
+ private static final HashMap<Locale, DigitsKeyListener[]> sLocaleInstanceCache =
+ new HashMap<>();
+
+ /**
+ * Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the
+ * locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate
+ * decimal separator (only one per field) if specified.
+ */
+ @NonNull
+ public static DigitsKeyListener getInstance(
+ @Nullable Locale locale, boolean sign, boolean decimal) {
+ final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+ synchronized (sLocaleCacheLock) {
+ DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale);
+ if (cachedValue != null && cachedValue[kind] != null) {
+ return cachedValue[kind];
+ }
+ if (cachedValue == null) {
+ cachedValue = new DigitsKeyListener[4];
+ sLocaleInstanceCache.put(locale, cachedValue);
+ }
+ return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal);
+ }
+ }
+
+ private static final Object sStringCacheLock = new Object();
+ @GuardedBy("sStringCacheLock")
+ private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>();
+
+ /**
+ * Returns a DigitsKeyListener that accepts only the characters
+ * that appear in the specified String. Note that not all characters
+ * may be available on every keyboard.
+ */
+ @NonNull
+ public static DigitsKeyListener getInstance(@NonNull String accepted) {
+ DigitsKeyListener result;
+ synchronized (sStringCacheLock) {
+ result = sStringInstanceCache.get(accepted);
+ if (result == null) {
+ result = new DigitsKeyListener(accepted);
+ sStringInstanceCache.put(accepted, result);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns a DigitsKeyListener based on an the settings of a existing DigitsKeyListener, with
+ * the locale modified.
+ *
+ * @hide
+ */
+ @NonNull
+ public static DigitsKeyListener getInstance(
+ @Nullable Locale locale,
+ @NonNull DigitsKeyListener listener) {
+ if (listener.mStringMode) {
+ return listener; // string-mode DigitsKeyListeners have no locale.
+ } else {
+ return getInstance(locale, listener.mSign, listener.mDecimal);
+ }
+ }
+
+ /**
+ * Returns the input type for the listener.
+ */
+ public int getInputType() {
+ int contentType;
+ if (mNeedsAdvancedInput) {
+ contentType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+ } else {
+ contentType = InputType.TYPE_CLASS_NUMBER;
+ if (mSign) {
+ contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
+ }
+ if (mDecimal) {
+ contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
+ }
+ }
+ return contentType;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ CharSequence out = super.filter(source, start, end, dest, dstart, dend);
+
+ if (mSign == false && mDecimal == false) {
+ return out;
+ }
+
+ if (out != null) {
+ source = out;
+ start = 0;
+ end = out.length();
+ }
+
+ int sign = -1;
+ int decimal = -1;
+ int dlen = dest.length();
+
+ /*
+ * Find out if the existing text has a sign or decimal point characters.
+ */
+
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+
+ if (isSignChar(c)) {
+ sign = i;
+ } else if (isDecimalPointChar(c)) {
+ decimal = i;
+ }
+ }
+ for (int i = dend; i < dlen; i++) {
+ char c = dest.charAt(i);
+
+ if (isSignChar(c)) {
+ return ""; // Nothing can be inserted in front of a sign character.
+ } else if (isDecimalPointChar(c)) {
+ decimal = i;
+ }
+ }
+
+ /*
+ * If it does, we must strip them out from the source.
+ * In addition, a sign character must be the very first character,
+ * and nothing can be inserted before an existing sign character.
+ * Go in reverse order so the offsets are stable.
+ */
+
+ SpannableStringBuilder stripped = null;
+
+ for (int i = end - 1; i >= start; i--) {
+ char c = source.charAt(i);
+ boolean strip = false;
+
+ if (isSignChar(c)) {
+ if (i != start || dstart != 0) {
+ strip = true;
+ } else if (sign >= 0) {
+ strip = true;
+ } else {
+ sign = i;
+ }
+ } else if (isDecimalPointChar(c)) {
+ if (decimal >= 0) {
+ strip = true;
+ } else {
+ decimal = i;
+ }
+ }
+
+ if (strip) {
+ if (end == start + 1) {
+ return ""; // Only one character, and it was stripped.
+ }
+
+ if (stripped == null) {
+ stripped = new SpannableStringBuilder(source, start, end);
+ }
+
+ stripped.delete(i - start, i + 1 - start);
+ }
+ }
+
+ if (stripped != null) {
+ return stripped;
+ } else if (out != null) {
+ return out;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/android/text/method/HideReturnsTransformationMethod.java b/android/text/method/HideReturnsTransformationMethod.java
new file mode 100644
index 00000000..c6a90ca9
--- /dev/null
+++ b/android/text/method/HideReturnsTransformationMethod.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+/**
+ * This transformation method causes any carriage return characters (\r)
+ * to be hidden by displaying them as zero-width non-breaking space
+ * characters (\uFEFF).
+ */
+public class HideReturnsTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\r' };
+ private static char[] REPLACEMENT = new char[] { '\uFEFF' };
+
+ /**
+ * The character to be replaced is \r.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character that \r is replaced with is \uFEFF.
+ */
+ protected char[] getReplacement() {
+ return REPLACEMENT;
+ }
+
+ public static HideReturnsTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new HideReturnsTransformationMethod();
+ return sInstance;
+ }
+
+ private static HideReturnsTransformationMethod sInstance;
+}
diff --git a/android/text/method/KeyListener.java b/android/text/method/KeyListener.java
new file mode 100644
index 00000000..ce7054c4
--- /dev/null
+++ b/android/text/method/KeyListener.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Editable;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * Interface for converting text key events into edit operations on an
+ * Editable class. Note that for most cases this interface has been
+ * superceded by general soft input methods as defined by
+ * {@link android.view.inputmethod.InputMethod}; it should only be used
+ * for cases where an application has its own on-screen keypad and also wants
+ * to process hard keyboard events to match it.
+ * <p></p>
+ * Key presses on soft input methods are not required to trigger the methods
+ * in this listener, and are in fact discouraged to do so. The default
+ * android keyboard will not trigger these for any key to any application
+ * targetting Jelly Bean or later, and will only deliver it for some
+ * key presses to applications targetting Ice Cream Sandwich or earlier.
+ */
+public interface KeyListener {
+ /**
+ * Return the type of text that this key listener is manipulating,
+ * as per {@link android.text.InputType}. This is used to
+ * determine the mode of the soft keyboard that is shown for the editor.
+ *
+ * <p>If you return
+ * {@link android.text.InputType#TYPE_NULL}
+ * then <em>no</em> soft keyboard will provided. In other words, you
+ * must be providing your own key pad for on-screen input and the key
+ * listener will be used to handle input from a hard keyboard.
+ *
+ * <p>If you
+ * return any other value, a soft input method will be created when the
+ * user puts focus in the editor, which will provide a keypad and also
+ * consume hard key events. This means that the key listener will generally
+ * not be used, instead the soft input method will take care of managing
+ * key input as per the content type returned here.
+ */
+ public int getInputType();
+
+ /**
+ * If the key listener wants to handle this key, return true,
+ * otherwise return false and the caller (i.e.&nbsp;the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyDown(View view, Editable text,
+ int keyCode, KeyEvent event);
+
+ /**
+ * If the key listener wants to handle this key release, return true,
+ * otherwise return false and the caller (i.e.&nbsp;the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyUp(View view, Editable text,
+ int keyCode, KeyEvent event);
+
+ /**
+ * If the key listener wants to other kinds of key events, return true,
+ * otherwise return false and the caller (i.e.&nbsp;the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyOther(View view, Editable text, KeyEvent event);
+
+ /**
+ * Remove the given shift states from the edited text.
+ */
+ public void clearMetaKeyState(View view, Editable content, int states);
+}
diff --git a/android/text/method/LinkMovementMethod.java b/android/text/method/LinkMovementMethod.java
new file mode 100644
index 00000000..31ed5492
--- /dev/null
+++ b/android/text/method/LinkMovementMethod.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Layout;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.style.ClickableSpan;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A movement method that traverses links in the text buffer and scrolls if necessary.
+ * Supports clicking on links with DPad Center or Enter.
+ */
+public class LinkMovementMethod extends ScrollingMovementMethod {
+ private static final int CLICK = 1;
+ private static final int UP = 2;
+ private static final int DOWN = 3;
+
+ @Override
+ public boolean canSelectArbitrarily() {
+ return true;
+ }
+
+ @Override
+ protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
+ int movementMetaState, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN &&
+ event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
+ return true;
+ }
+ }
+ break;
+ }
+ return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
+ }
+
+ @Override
+ protected boolean up(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.up(widget, buffer);
+ }
+
+ @Override
+ protected boolean down(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.down(widget, buffer);
+ }
+
+ @Override
+ protected boolean left(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.left(widget, buffer);
+ }
+
+ @Override
+ protected boolean right(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.right(widget, buffer);
+ }
+
+ private boolean action(int what, TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int areaTop = widget.getScrollY();
+ int areaBot = areaTop + widget.getHeight() - padding;
+
+ int lineTop = layout.getLineForVertical(areaTop);
+ int lineBot = layout.getLineForVertical(areaBot);
+
+ int first = layout.getLineStart(lineTop);
+ int last = layout.getLineEnd(lineBot);
+
+ ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
+
+ int a = Selection.getSelectionStart(buffer);
+ int b = Selection.getSelectionEnd(buffer);
+
+ int selStart = Math.min(a, b);
+ int selEnd = Math.max(a, b);
+
+ if (selStart < 0) {
+ if (buffer.getSpanStart(FROM_BELOW) >= 0) {
+ selStart = selEnd = buffer.length();
+ }
+ }
+
+ if (selStart > last)
+ selStart = selEnd = Integer.MAX_VALUE;
+ if (selEnd < first)
+ selStart = selEnd = -1;
+
+ switch (what) {
+ case CLICK:
+ if (selStart == selEnd) {
+ return false;
+ }
+
+ ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
+
+ if (link.length != 1)
+ return false;
+
+ link[0].onClick(widget);
+ break;
+
+ case UP:
+ int bestStart, bestEnd;
+
+ bestStart = -1;
+ bestEnd = -1;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int end = buffer.getSpanEnd(candidates[i]);
+
+ if (end < selEnd || selStart == selEnd) {
+ if (end > bestEnd) {
+ bestStart = buffer.getSpanStart(candidates[i]);
+ bestEnd = end;
+ }
+ }
+ }
+
+ if (bestStart >= 0) {
+ Selection.setSelection(buffer, bestEnd, bestStart);
+ return true;
+ }
+
+ break;
+
+ case DOWN:
+ bestStart = Integer.MAX_VALUE;
+ bestEnd = Integer.MAX_VALUE;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int start = buffer.getSpanStart(candidates[i]);
+
+ if (start > selStart || selStart == selEnd) {
+ if (start < bestStart) {
+ bestStart = start;
+ bestEnd = buffer.getSpanEnd(candidates[i]);
+ }
+ }
+ }
+
+ if (bestEnd < Integer.MAX_VALUE) {
+ Selection.setSelection(buffer, bestStart, bestEnd);
+ return true;
+ }
+
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
+
+ if (links.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ links[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
+ buffer.getSpanStart(links[0]),
+ buffer.getSpanEnd(links[0]));
+ }
+ return true;
+ } else {
+ Selection.removeSelection(buffer);
+ }
+ }
+
+ return super.onTouchEvent(widget, buffer, event);
+ }
+
+ @Override
+ public void initialize(TextView widget, Spannable text) {
+ Selection.removeSelection(text);
+ text.removeSpan(FROM_BELOW);
+ }
+
+ @Override
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ Selection.removeSelection(text);
+
+ if ((dir & View.FOCUS_BACKWARD) != 0) {
+ text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
+ } else {
+ text.removeSpan(FROM_BELOW);
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new LinkMovementMethod();
+
+ return sInstance;
+ }
+
+ private static LinkMovementMethod sInstance;
+ private static Object FROM_BELOW = new NoCopySpan.Concrete();
+}
diff --git a/android/text/method/MetaKeyKeyListener.java b/android/text/method/MetaKeyKeyListener.java
new file mode 100644
index 00000000..c3c7302c
--- /dev/null
+++ b/android/text/method/MetaKeyKeyListener.java
@@ -0,0 +1,660 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Editable;
+import android.text.NoCopySpan;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * This base class encapsulates the behavior for tracking the state of
+ * meta keys such as SHIFT, ALT and SYM as well as the pseudo-meta state of selecting text.
+ * <p>
+ * Key listeners that care about meta state should inherit from this class;
+ * you should not instantiate this class directly in a client.
+ * </p><p>
+ * This class provides two mechanisms for tracking meta state that can be used
+ * together or independently.
+ * </p>
+ * <ul>
+ * <li>Methods such as {@link #handleKeyDown(long, int, KeyEvent)} and
+ * {@link #getMetaState(long)} operate on a meta key state bit mask.</li>
+ * <li>Methods such as {@link #onKeyDown(View, Editable, int, KeyEvent)} and
+ * {@link #getMetaState(CharSequence, int)} operate on meta key state flags stored
+ * as spans in an {@link Editable} text buffer. The spans only describe the current
+ * meta key state of the text editor; they do not carry any positional information.</li>
+ * </ul>
+ * <p>
+ * The behavior of this class varies according to the keyboard capabilities
+ * described by the {@link KeyCharacterMap} of the keyboard device such as
+ * the {@link KeyCharacterMap#getModifierBehavior() key modifier behavior}.
+ * </p><p>
+ * {@link MetaKeyKeyListener} implements chorded and toggled key modifiers.
+ * When key modifiers are toggled into a latched or locked state, the state
+ * of the modifier is stored in the {@link Editable} text buffer or in a
+ * meta state integer managed by the client. These latched or locked modifiers
+ * should be considered to be held <b>in addition to</b> those that the
+ * keyboard already reported as being pressed in {@link KeyEvent#getMetaState()}.
+ * In other words, the {@link MetaKeyKeyListener} augments the meta state
+ * provided by the keyboard; it does not replace it. This distinction is important
+ * to ensure that meta keys not handled by {@link MetaKeyKeyListener} such as
+ * {@link KeyEvent#KEYCODE_CAPS_LOCK} or {@link KeyEvent#KEYCODE_NUM_LOCK} are
+ * taken into consideration.
+ * </p><p>
+ * To ensure correct meta key behavior, the following pattern should be used
+ * when mapping key codes to characters:
+ * </p>
+ * <code>
+ * private char getUnicodeChar(TextKeyListener listener, KeyEvent event, Editable textBuffer) {
+ * // Use the combined meta states from the event and the key listener.
+ * int metaState = event.getMetaState() | listener.getMetaState(textBuffer);
+ * return event.getUnicodeChar(metaState);
+ * }
+ * </code>
+ */
+public abstract class MetaKeyKeyListener {
+ /**
+ * Flag that indicates that the SHIFT key is on.
+ * Value equals {@link KeyEvent#META_SHIFT_ON}.
+ */
+ public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON;
+ /**
+ * Flag that indicates that the ALT key is on.
+ * Value equals {@link KeyEvent#META_ALT_ON}.
+ */
+ public static final int META_ALT_ON = KeyEvent.META_ALT_ON;
+ /**
+ * Flag that indicates that the SYM key is on.
+ * Value equals {@link KeyEvent#META_SYM_ON}.
+ */
+ public static final int META_SYM_ON = KeyEvent.META_SYM_ON;
+
+ /**
+ * Flag that indicates that the SHIFT key is locked in CAPS mode.
+ */
+ public static final int META_CAP_LOCKED = KeyEvent.META_CAP_LOCKED;
+ /**
+ * Flag that indicates that the ALT key is locked.
+ */
+ public static final int META_ALT_LOCKED = KeyEvent.META_ALT_LOCKED;
+ /**
+ * Flag that indicates that the SYM key is locked.
+ */
+ public static final int META_SYM_LOCKED = KeyEvent.META_SYM_LOCKED;
+
+ /**
+ * @hide pending API review
+ */
+ public static final int META_SELECTING = KeyEvent.META_SELECTING;
+
+ // These bits are privately used by the meta key key listener.
+ // They are deliberately assigned values outside of the representable range of an 'int'
+ // so as not to conflict with any meta key states publicly defined by KeyEvent.
+ private static final long META_CAP_USED = 1L << 32;
+ private static final long META_ALT_USED = 1L << 33;
+ private static final long META_SYM_USED = 1L << 34;
+
+ private static final long META_CAP_PRESSED = 1L << 40;
+ private static final long META_ALT_PRESSED = 1L << 41;
+ private static final long META_SYM_PRESSED = 1L << 42;
+
+ private static final long META_CAP_RELEASED = 1L << 48;
+ private static final long META_ALT_RELEASED = 1L << 49;
+ private static final long META_SYM_RELEASED = 1L << 50;
+
+ private static final long META_SHIFT_MASK = META_SHIFT_ON
+ | META_CAP_LOCKED | META_CAP_USED
+ | META_CAP_PRESSED | META_CAP_RELEASED;
+ private static final long META_ALT_MASK = META_ALT_ON
+ | META_ALT_LOCKED | META_ALT_USED
+ | META_ALT_PRESSED | META_ALT_RELEASED;
+ private static final long META_SYM_MASK = META_SYM_ON
+ | META_SYM_LOCKED | META_SYM_USED
+ | META_SYM_PRESSED | META_SYM_RELEASED;
+
+ private static final Object CAP = new NoCopySpan.Concrete();
+ private static final Object ALT = new NoCopySpan.Concrete();
+ private static final Object SYM = new NoCopySpan.Concrete();
+ private static final Object SELECTING = new NoCopySpan.Concrete();
+
+ private static final int PRESSED_RETURN_VALUE = 1;
+ private static final int LOCKED_RETURN_VALUE = 2;
+
+ /**
+ * Resets all meta state to inactive.
+ */
+ public static void resetMetaState(Spannable text) {
+ text.removeSpan(CAP);
+ text.removeSpan(ALT);
+ text.removeSpan(SYM);
+ text.removeSpan(SELECTING);
+ }
+
+ /**
+ * Gets the state of the meta keys.
+ *
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return an integer in which each bit set to one represents a pressed
+ * or locked meta key.
+ */
+ public static final int getMetaState(CharSequence text) {
+ return getActive(text, CAP, META_SHIFT_ON, META_CAP_LOCKED) |
+ getActive(text, ALT, META_ALT_ON, META_ALT_LOCKED) |
+ getActive(text, SYM, META_SYM_ON, META_SYM_LOCKED) |
+ getActive(text, SELECTING, META_SELECTING, META_SELECTING);
+ }
+
+ /**
+ * Gets the state of the meta keys for a specific key event.
+ *
+ * For input devices that use toggled key modifiers, the `toggled' state
+ * is stored into the text buffer. This method retrieves the meta state
+ * for this event, accounting for the stored state. If the event has been
+ * created by a device that does not support toggled key modifiers, like
+ * a virtual device for example, the stored state is ignored.
+ *
+ * @param text the buffer in which the meta key would have been pressed.
+ * @param event the event for which to evaluate the meta state.
+ * @return an integer in which each bit set to one represents a pressed
+ * or locked meta key.
+ */
+ public static final int getMetaState(final CharSequence text, final KeyEvent event) {
+ int metaState = event.getMetaState();
+ if (event.getKeyCharacterMap().getModifierBehavior()
+ == KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED) {
+ metaState |= getMetaState(text);
+ }
+ return metaState;
+ }
+
+ // As META_SELECTING is @hide we should not mention it in public comments, hence the
+ // omission in @param meta
+ /**
+ * Gets the state of a particular meta key.
+ *
+ * @param meta META_SHIFT_ON, META_ALT_ON, META_SYM_ON
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return 0 if inactive, 1 if active, 2 if locked.
+ */
+ public static final int getMetaState(CharSequence text, int meta) {
+ switch (meta) {
+ case META_SHIFT_ON:
+ return getActive(text, CAP, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE);
+
+ case META_ALT_ON:
+ return getActive(text, ALT, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE);
+
+ case META_SYM_ON:
+ return getActive(text, SYM, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE);
+
+ case META_SELECTING:
+ return getActive(text, SELECTING, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE);
+
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Gets the state of a particular meta key to use with a particular key event.
+ *
+ * If the key event has been created by a device that does not support toggled
+ * key modifiers, like a virtual keyboard for example, only the meta state in
+ * the key event is considered.
+ *
+ * @param meta META_SHIFT_ON, META_ALT_ON, META_SYM_ON
+ * @param text the buffer in which the meta key would have been pressed.
+ * @param event the event for which to evaluate the meta state.
+ * @return 0 if inactive, 1 if active, 2 if locked.
+ */
+ public static final int getMetaState(final CharSequence text, final int meta,
+ final KeyEvent event) {
+ int metaState = event.getMetaState();
+ if (event.getKeyCharacterMap().getModifierBehavior()
+ == KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED) {
+ metaState |= getMetaState(text);
+ }
+ if (META_SELECTING == meta) {
+ // #getMetaState(long, int) does not support META_SELECTING, but we want the same
+ // behavior as #getMetaState(CharSequence, int) so we need to do it here
+ if ((metaState & META_SELECTING) != 0) {
+ // META_SELECTING is only ever set to PRESSED and can't be LOCKED, so return 1
+ return 1;
+ }
+ return 0;
+ }
+ return getMetaState(metaState, meta);
+ }
+
+ private static int getActive(CharSequence text, Object meta,
+ int on, int lock) {
+ if (!(text instanceof Spanned)) {
+ return 0;
+ }
+
+ Spanned sp = (Spanned) text;
+ int flag = sp.getSpanFlags(meta);
+
+ if (flag == LOCKED) {
+ return lock;
+ } else if (flag != 0) {
+ return on;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Call this method after you handle a keypress so that the meta
+ * state will be reset to unshifted (if it is not still down)
+ * or primed to be reset to unshifted (once it is released).
+ */
+ public static void adjustMetaAfterKeypress(Spannable content) {
+ adjust(content, CAP);
+ adjust(content, ALT);
+ adjust(content, SYM);
+ }
+
+ /**
+ * Returns true if this object is one that this class would use to
+ * keep track of any meta state in the specified text.
+ */
+ public static boolean isMetaTracker(CharSequence text, Object what) {
+ return what == CAP || what == ALT || what == SYM ||
+ what == SELECTING;
+ }
+
+ /**
+ * Returns true if this object is one that this class would use to
+ * keep track of the selecting meta state in the specified text.
+ */
+ public static boolean isSelectingMetaTracker(CharSequence text, Object what) {
+ return what == SELECTING;
+ }
+
+ private static void adjust(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == PRESSED)
+ content.setSpan(what, 0, 0, USED);
+ else if (current == RELEASED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Call this if you are a method that ignores the locked meta state
+ * (arrow keys, for example) and you handle a key.
+ */
+ protected static void resetLockedMeta(Spannable content) {
+ resetLock(content, CAP);
+ resetLock(content, ALT);
+ resetLock(content, SYM);
+ resetLock(content, SELECTING);
+ }
+
+ private static void resetLock(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == LOCKED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Handles presses of the meta keys.
+ */
+ public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ press(content, CAP);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ press(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ press(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void press(Editable content, Object what) {
+ int state = content.getSpanFlags(what);
+
+ if (state == PRESSED)
+ ; // repeat before use
+ else if (state == RELEASED)
+ content.setSpan(what, 0, 0, LOCKED);
+ else if (state == USED)
+ ; // repeat after use
+ else if (state == LOCKED)
+ content.removeSpan(what);
+ else
+ content.setSpan(what, 0, 0, PRESSED);
+ }
+
+ /**
+ * Start selecting text.
+ * @hide pending API review
+ */
+ public static void startSelecting(View view, Spannable content) {
+ content.setSpan(SELECTING, 0, 0, PRESSED);
+ }
+
+ /**
+ * Stop selecting text. This does not actually collapse the selection;
+ * call {@link android.text.Selection#setSelection} too.
+ * @hide pending API review
+ */
+ public static void stopSelecting(View view, Spannable content) {
+ content.removeSpan(SELECTING);
+ }
+
+ /**
+ * Handles release of the meta keys.
+ */
+ public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ release(content, CAP, event);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ release(content, ALT, event);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ release(content, SYM, event);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void release(Editable content, Object what, KeyEvent event) {
+ int current = content.getSpanFlags(what);
+
+ switch (event.getKeyCharacterMap().getModifierBehavior()) {
+ case KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED:
+ if (current == USED)
+ content.removeSpan(what);
+ else if (current == PRESSED)
+ content.setSpan(what, 0, 0, RELEASED);
+ break;
+
+ default:
+ content.removeSpan(what);
+ break;
+ }
+ }
+
+ public void clearMetaKeyState(View view, Editable content, int states) {
+ clearMetaKeyState(content, states);
+ }
+
+ public static void clearMetaKeyState(Editable content, int states) {
+ if ((states&META_SHIFT_ON) != 0) content.removeSpan(CAP);
+ if ((states&META_ALT_ON) != 0) content.removeSpan(ALT);
+ if ((states&META_SYM_ON) != 0) content.removeSpan(SYM);
+ if ((states&META_SELECTING) != 0) content.removeSpan(SELECTING);
+ }
+
+ /**
+ * Call this if you are a method that ignores the locked meta state
+ * (arrow keys, for example) and you handle a key.
+ */
+ public static long resetLockedMeta(long state) {
+ if ((state & META_CAP_LOCKED) != 0) {
+ state &= ~META_SHIFT_MASK;
+ }
+ if ((state & META_ALT_LOCKED) != 0) {
+ state &= ~META_ALT_MASK;
+ }
+ if ((state & META_SYM_LOCKED) != 0) {
+ state &= ~META_SYM_MASK;
+ }
+ return state;
+ }
+
+ // ---------------------------------------------------------------------
+ // Version of API that operates on a state bit mask
+ // ---------------------------------------------------------------------
+
+ /**
+ * Gets the state of the meta keys.
+ *
+ * @param state the current meta state bits.
+ *
+ * @return an integer in which each bit set to one represents a pressed
+ * or locked meta key.
+ */
+ public static final int getMetaState(long state) {
+ int result = 0;
+
+ if ((state & META_CAP_LOCKED) != 0) {
+ result |= META_CAP_LOCKED;
+ } else if ((state & META_SHIFT_ON) != 0) {
+ result |= META_SHIFT_ON;
+ }
+
+ if ((state & META_ALT_LOCKED) != 0) {
+ result |= META_ALT_LOCKED;
+ } else if ((state & META_ALT_ON) != 0) {
+ result |= META_ALT_ON;
+ }
+
+ if ((state & META_SYM_LOCKED) != 0) {
+ result |= META_SYM_LOCKED;
+ } else if ((state & META_SYM_ON) != 0) {
+ result |= META_SYM_ON;
+ }
+
+ return result;
+ }
+
+ /**
+ * Gets the state of a particular meta key.
+ *
+ * @param state the current state bits.
+ * @param meta META_SHIFT_ON, META_ALT_ON, or META_SYM_ON
+ *
+ * @return 0 if inactive, 1 if active, 2 if locked.
+ */
+ public static final int getMetaState(long state, int meta) {
+ switch (meta) {
+ case META_SHIFT_ON:
+ if ((state & META_CAP_LOCKED) != 0) return LOCKED_RETURN_VALUE;
+ if ((state & META_SHIFT_ON) != 0) return PRESSED_RETURN_VALUE;
+ return 0;
+
+ case META_ALT_ON:
+ if ((state & META_ALT_LOCKED) != 0) return LOCKED_RETURN_VALUE;
+ if ((state & META_ALT_ON) != 0) return PRESSED_RETURN_VALUE;
+ return 0;
+
+ case META_SYM_ON:
+ if ((state & META_SYM_LOCKED) != 0) return LOCKED_RETURN_VALUE;
+ if ((state & META_SYM_ON) != 0) return PRESSED_RETURN_VALUE;
+ return 0;
+
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Call this method after you handle a keypress so that the meta
+ * state will be reset to unshifted (if it is not still down)
+ * or primed to be reset to unshifted (once it is released). Takes
+ * the current state, returns the new state.
+ */
+ public static long adjustMetaAfterKeypress(long state) {
+ if ((state & META_CAP_PRESSED) != 0) {
+ state = (state & ~META_SHIFT_MASK) | META_SHIFT_ON | META_CAP_USED;
+ } else if ((state & META_CAP_RELEASED) != 0) {
+ state &= ~META_SHIFT_MASK;
+ }
+
+ if ((state & META_ALT_PRESSED) != 0) {
+ state = (state & ~META_ALT_MASK) | META_ALT_ON | META_ALT_USED;
+ } else if ((state & META_ALT_RELEASED) != 0) {
+ state &= ~META_ALT_MASK;
+ }
+
+ if ((state & META_SYM_PRESSED) != 0) {
+ state = (state & ~META_SYM_MASK) | META_SYM_ON | META_SYM_USED;
+ } else if ((state & META_SYM_RELEASED) != 0) {
+ state &= ~META_SYM_MASK;
+ }
+ return state;
+ }
+
+ /**
+ * Handles presses of the meta keys.
+ */
+ public static long handleKeyDown(long state, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ return press(state, META_SHIFT_ON, META_SHIFT_MASK,
+ META_CAP_LOCKED, META_CAP_PRESSED, META_CAP_RELEASED, META_CAP_USED);
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ return press(state, META_ALT_ON, META_ALT_MASK,
+ META_ALT_LOCKED, META_ALT_PRESSED, META_ALT_RELEASED, META_ALT_USED);
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ return press(state, META_SYM_ON, META_SYM_MASK,
+ META_SYM_LOCKED, META_SYM_PRESSED, META_SYM_RELEASED, META_SYM_USED);
+ }
+ return state;
+ }
+
+ private static long press(long state, int what, long mask,
+ long locked, long pressed, long released, long used) {
+ if ((state & pressed) != 0) {
+ // repeat before use
+ } else if ((state & released) != 0) {
+ state = (state &~ mask) | what | locked;
+ } else if ((state & used) != 0) {
+ // repeat after use
+ } else if ((state & locked) != 0) {
+ state &= ~mask;
+ } else {
+ state |= what | pressed;
+ }
+ return state;
+ }
+
+ /**
+ * Handles release of the meta keys.
+ */
+ public static long handleKeyUp(long state, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ return release(state, META_SHIFT_ON, META_SHIFT_MASK,
+ META_CAP_PRESSED, META_CAP_RELEASED, META_CAP_USED, event);
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ return release(state, META_ALT_ON, META_ALT_MASK,
+ META_ALT_PRESSED, META_ALT_RELEASED, META_ALT_USED, event);
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ return release(state, META_SYM_ON, META_SYM_MASK,
+ META_SYM_PRESSED, META_SYM_RELEASED, META_SYM_USED, event);
+ }
+ return state;
+ }
+
+ private static long release(long state, int what, long mask,
+ long pressed, long released, long used, KeyEvent event) {
+ switch (event.getKeyCharacterMap().getModifierBehavior()) {
+ case KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED:
+ if ((state & used) != 0) {
+ state &= ~mask;
+ } else if ((state & pressed) != 0) {
+ state |= what | released;
+ }
+ break;
+
+ default:
+ state &= ~mask;
+ break;
+ }
+ return state;
+ }
+
+ /**
+ * Clears the state of the specified meta key if it is locked.
+ * @param state the meta key state
+ * @param which meta keys to clear, may be a combination of {@link #META_SHIFT_ON},
+ * {@link #META_ALT_ON} or {@link #META_SYM_ON}.
+ */
+ public long clearMetaKeyState(long state, int which) {
+ if ((which & META_SHIFT_ON) != 0 && (state & META_CAP_LOCKED) != 0) {
+ state &= ~META_SHIFT_MASK;
+ }
+ if ((which & META_ALT_ON) != 0 && (state & META_ALT_LOCKED) != 0) {
+ state &= ~META_ALT_MASK;
+ }
+ if ((which & META_SYM_ON) != 0 && (state & META_SYM_LOCKED) != 0) {
+ state &= ~META_SYM_MASK;
+ }
+ return state;
+ }
+
+ /**
+ * The meta key has been pressed but has not yet been used.
+ */
+ private static final int PRESSED =
+ Spannable.SPAN_MARK_MARK | (1 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released but has still
+ * not yet been used.
+ */
+ private static final int RELEASED =
+ Spannable.SPAN_MARK_MARK | (2 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and used but has not yet been released.
+ */
+ private static final int USED =
+ Spannable.SPAN_MARK_MARK | (3 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released without use, and then
+ * pressed again; it may also have been released again.
+ */
+ private static final int LOCKED =
+ Spannable.SPAN_MARK_MARK | (4 << Spannable.SPAN_USER_SHIFT);
+}
diff --git a/android/text/method/MovementMethod.java b/android/text/method/MovementMethod.java
new file mode 100644
index 00000000..f6fe575a
--- /dev/null
+++ b/android/text/method/MovementMethod.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Spannable;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * Provides cursor positioning, scrolling and text selection functionality in a {@link TextView}.
+ * <p>
+ * The {@link TextView} delegates handling of key events, trackball motions and touches to
+ * the movement method for purposes of content navigation. The framework automatically
+ * selects an appropriate movement method based on the content of the {@link TextView}.
+ * </p><p>
+ * This interface is intended for use by the framework; it should not be implemented
+ * directly by applications.
+ * </p>
+ */
+public interface MovementMethod {
+ public void initialize(TextView widget, Spannable text);
+ public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event);
+ public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event);
+
+ /**
+ * If the key listener wants to other kinds of key events, return true,
+ * otherwise return false and the caller (i.e. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyOther(TextView view, Spannable text, KeyEvent event);
+
+ public void onTakeFocus(TextView widget, Spannable text, int direction);
+ public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event);
+ public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event);
+ public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event);
+
+ /**
+ * Returns true if this movement method allows arbitrary selection
+ * of any text; false if it has no selection (like a movement method
+ * that only scrolls) or a constrained selection (for example
+ * limited to links. The "Select All" menu item is disabled
+ * if arbitrary selection is not allowed.
+ */
+ public boolean canSelectArbitrarily();
+}
diff --git a/android/text/method/MultiTapKeyListener.java b/android/text/method/MultiTapKeyListener.java
new file mode 100644
index 00000000..5770482b
--- /dev/null
+++ b/android/text/method/MultiTapKeyListener.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.method.TextKeyListener.Capitalize;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * This is the standard key listener for alphabetic input on 12-key
+ * keyboards. You should generally not need to instantiate this yourself;
+ * TextKeyListener will do it for you.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class MultiTapKeyListener extends BaseKeyListener
+ implements SpanWatcher {
+ private static MultiTapKeyListener[] sInstance =
+ new MultiTapKeyListener[Capitalize.values().length * 2];
+
+ private static final SparseArray<String> sRecs = new SparseArray<String>();
+
+ private Capitalize mCapitalize;
+ private boolean mAutoText;
+
+ static {
+ sRecs.put(KeyEvent.KEYCODE_1, ".,1!@#$%^&*:/?'=()");
+ sRecs.put(KeyEvent.KEYCODE_2, "abc2ABC");
+ sRecs.put(KeyEvent.KEYCODE_3, "def3DEF");
+ sRecs.put(KeyEvent.KEYCODE_4, "ghi4GHI");
+ sRecs.put(KeyEvent.KEYCODE_5, "jkl5JKL");
+ sRecs.put(KeyEvent.KEYCODE_6, "mno6MNO");
+ sRecs.put(KeyEvent.KEYCODE_7, "pqrs7PQRS");
+ sRecs.put(KeyEvent.KEYCODE_8, "tuv8TUV");
+ sRecs.put(KeyEvent.KEYCODE_9, "wxyz9WXYZ");
+ sRecs.put(KeyEvent.KEYCODE_0, "0+");
+ sRecs.put(KeyEvent.KEYCODE_POUND, " ");
+ };
+
+ public MultiTapKeyListener(Capitalize cap,
+ boolean autotext) {
+ mCapitalize = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ */
+ public static MultiTapKeyListener getInstance(boolean autotext,
+ Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new MultiTapKeyListener(cap, autotext);
+ }
+
+ return sInstance[off];
+ }
+
+ public int getInputType() {
+ return makeTextContentType(mCapitalize, mAutoText);
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+ int pref = 0;
+
+ if (view != null) {
+ pref = TextKeyListener.getInstance().getPrefs(view.getContext());
+ }
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
+ int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
+
+ // now for the multitap cases...
+
+ // Try to increment the character we were working on before
+ // if we have one and it's still the same key.
+
+ int rec = (content.getSpanFlags(TextKeyListener.ACTIVE)
+ & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT;
+
+ if (activeStart == selStart && activeEnd == selEnd &&
+ selEnd - selStart == 1 &&
+ rec >= 0 && rec < sRecs.size()) {
+ if (keyCode == KeyEvent.KEYCODE_STAR) {
+ char current = content.charAt(selStart);
+
+ if (Character.isLowerCase(current)) {
+ content.replace(selStart, selEnd,
+ String.valueOf(current).toUpperCase());
+ removeTimeouts(content);
+ new Timeout(content); // for its side effects
+
+ return true;
+ }
+ if (Character.isUpperCase(current)) {
+ content.replace(selStart, selEnd,
+ String.valueOf(current).toLowerCase());
+ removeTimeouts(content);
+ new Timeout(content); // for its side effects
+
+ return true;
+ }
+ }
+
+ if (sRecs.indexOfKey(keyCode) == rec) {
+ String val = sRecs.valueAt(rec);
+ char ch = content.charAt(selStart);
+ int ix = val.indexOf(ch);
+
+ if (ix >= 0) {
+ ix = (ix + 1) % (val.length());
+
+ content.replace(selStart, selEnd, val, ix, ix + 1);
+ removeTimeouts(content);
+ new Timeout(content); // for its side effects
+
+ return true;
+ }
+ }
+
+ // Is this key one we know about at all? If so, acknowledge
+ // that the selection is our fault but the key has changed
+ // or the text no longer matches, so move the selection over
+ // so that it inserts instead of replaces.
+
+ rec = sRecs.indexOfKey(keyCode);
+
+ if (rec >= 0) {
+ Selection.setSelection(content, selEnd, selEnd);
+ selStart = selEnd;
+ }
+ } else {
+ rec = sRecs.indexOfKey(keyCode);
+ }
+
+ if (rec >= 0) {
+ // We have a valid key. Replace the selection or insertion point
+ // with the first character for that key, and remember what
+ // record it came from for next time.
+
+ String val = sRecs.valueAt(rec);
+
+ int off = 0;
+ if ((pref & TextKeyListener.AUTO_CAP) != 0 &&
+ TextKeyListener.shouldCap(mCapitalize, content, selStart)) {
+ for (int i = 0; i < val.length(); i++) {
+ if (Character.isUpperCase(val.charAt(i))) {
+ off = i;
+ break;
+ }
+ }
+ }
+
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+
+ content.setSpan(OLD_SEL_START, selStart, selStart,
+ Spannable.SPAN_MARK_MARK);
+
+ content.replace(selStart, selEnd, val, off, off + 1);
+
+ int oldStart = content.getSpanStart(OLD_SEL_START);
+ selEnd = Selection.getSelectionEnd(content);
+
+ if (selEnd != oldStart) {
+ Selection.setSelection(content, oldStart, selEnd);
+
+ content.setSpan(TextKeyListener.LAST_TYPED,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ content.setSpan(TextKeyListener.ACTIVE,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
+ (rec << Spannable.SPAN_USER_SHIFT));
+
+ }
+
+ removeTimeouts(content);
+ new Timeout(content); // for its side effects
+
+ // Set up the callback so we can remove the timeout if the
+ // cursor moves.
+
+ if (content.getSpanStart(this) < 0) {
+ KeyListener[] methods = content.getSpans(0, content.length(),
+ KeyListener.class);
+ for (Object method : methods) {
+ content.removeSpan(method);
+ }
+ content.setSpan(this, 0, content.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ public void onSpanChanged(Spannable buf,
+ Object what, int s, int e, int start, int stop) {
+ if (what == Selection.SELECTION_END) {
+ buf.removeSpan(TextKeyListener.ACTIVE);
+ removeTimeouts(buf);
+ }
+ }
+
+ private static void removeTimeouts(Spannable buf) {
+ Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class);
+
+ for (int i = 0; i < timeout.length; i++) {
+ Timeout t = timeout[i];
+
+ t.removeCallbacks(t);
+ t.mBuffer = null;
+ buf.removeSpan(t);
+ }
+ }
+
+ private class Timeout
+ extends Handler
+ implements Runnable
+ {
+ public Timeout(Editable buffer) {
+ mBuffer = buffer;
+ mBuffer.setSpan(Timeout.this, 0, mBuffer.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+
+ postAtTime(this, SystemClock.uptimeMillis() + 2000);
+ }
+
+ public void run() {
+ Spannable buf = mBuffer;
+
+ if (buf != null) {
+ int st = Selection.getSelectionStart(buf);
+ int en = Selection.getSelectionEnd(buf);
+
+ int start = buf.getSpanStart(TextKeyListener.ACTIVE);
+ int end = buf.getSpanEnd(TextKeyListener.ACTIVE);
+
+ if (st == start && en == end) {
+ Selection.setSelection(buf, Selection.getSelectionEnd(buf));
+ }
+
+ buf.removeSpan(Timeout.this);
+ }
+ }
+
+ private Editable mBuffer;
+ }
+
+ public void onSpanAdded(Spannable s, Object what, int start, int end) { }
+ public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
+}
+
diff --git a/android/text/method/NumberKeyListener.java b/android/text/method/NumberKeyListener.java
new file mode 100644
index 00000000..d40015ee
--- /dev/null
+++ b/android/text/method/NumberKeyListener.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.icu.text.DecimalFormatSymbols;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.format.DateFormat;
+import android.view.KeyEvent;
+import android.view.View;
+
+import libcore.icu.LocaleData;
+
+import java.util.Collection;
+import java.util.Locale;
+
+/**
+ * For numeric text entry
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public abstract class NumberKeyListener extends BaseKeyListener
+ implements InputFilter
+{
+ /**
+ * You can say which characters you can accept.
+ */
+ @NonNull
+ protected abstract char[] getAcceptedChars();
+
+ protected int lookup(KeyEvent event, Spannable content) {
+ return event.getMatch(getAcceptedChars(), getMetaState(content, event));
+ }
+
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ char[] accept = getAcceptedChars();
+ boolean filter = false;
+
+ int i;
+ for (i = start; i < end; i++) {
+ if (!ok(accept, source.charAt(i))) {
+ break;
+ }
+ }
+
+ if (i == end) {
+ // It was all OK.
+ return null;
+ }
+
+ if (end - start == 1) {
+ // It was not OK, and there is only one char, so nothing remains.
+ return "";
+ }
+
+ SpannableStringBuilder filtered =
+ new SpannableStringBuilder(source, start, end);
+ i -= start;
+ end -= start;
+
+ int len = end - start;
+ // Only count down to i because the chars before that were all OK.
+ for (int j = end - 1; j >= i; j--) {
+ if (!ok(accept, source.charAt(j))) {
+ filtered.delete(j, j + 1);
+ }
+ }
+
+ return filtered;
+ }
+
+ protected static boolean ok(char[] accept, char c) {
+ for (int i = accept.length - 1; i >= 0; i--) {
+ if (accept[i] == c) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ if (selStart < 0 || selEnd < 0) {
+ selStart = selEnd = 0;
+ Selection.setSelection(content, 0);
+ }
+
+ int i = event != null ? lookup(event, content) : 0;
+ int repeatCount = event != null ? event.getRepeatCount() : 0;
+ if (repeatCount == 0) {
+ if (i != 0) {
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+
+ content.replace(selStart, selEnd, String.valueOf((char) i));
+
+ adjustMetaAfterKeypress(content);
+ return true;
+ }
+ } else if (i == '0' && repeatCount == 1) {
+ // Pretty hackish, it replaces the 0 with the +
+
+ if (selStart == selEnd && selEnd > 0 &&
+ content.charAt(selStart - 1) == '0') {
+ content.replace(selStart - 1, selEnd, String.valueOf('+'));
+ adjustMetaAfterKeypress(content);
+ return true;
+ }
+ }
+
+ adjustMetaAfterKeypress(content);
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ /* package */
+ @Nullable
+ static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) {
+ if (locale == null) {
+ return false;
+ }
+ final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings();
+ for (int i = 0; i < 10; i++) {
+ if (digits[i].length() > 1) { // multi-codeunit digits. Not supported.
+ return false;
+ }
+ collection.add(Character.valueOf(digits[i].charAt(0)));
+ }
+ return true;
+ }
+
+ // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
+ private static final String DATE_TIME_FORMAT_SYMBOLS =
+ "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx";
+ private static final char SINGLE_QUOTE = '\'';
+
+ /* package */
+ static boolean addFormatCharsFromSkeleton(
+ @NonNull Collection<Character> collection, @Nullable Locale locale,
+ @NonNull String skeleton, @NonNull String symbolsToIgnore) {
+ if (locale == null) {
+ return false;
+ }
+ final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton);
+ boolean outsideQuotes = true;
+ for (int i = 0; i < pattern.length(); i++) {
+ final char ch = pattern.charAt(i);
+ if (Character.isSurrogate(ch)) { // characters outside BMP are not supported.
+ return false;
+ } else if (ch == SINGLE_QUOTE) {
+ outsideQuotes = !outsideQuotes;
+ // Single quote characters should be considered if and only if they follow
+ // another single quote.
+ if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) {
+ continue;
+ }
+ }
+
+ if (outsideQuotes) {
+ if (symbolsToIgnore.indexOf(ch) != -1) {
+ // Skip expected pattern characters.
+ continue;
+ } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) {
+ // An unexpected symbols is seen. We've failed.
+ return false;
+ }
+ }
+ // If we are here, we are either inside quotes, or we have seen a non-pattern
+ // character outside quotes. So ch is a valid character in a date.
+ collection.add(Character.valueOf(ch));
+ }
+ return true;
+ }
+
+ /* package */
+ static boolean addFormatCharsFromSkeletons(
+ @NonNull Collection<Character> collection, @Nullable Locale locale,
+ @NonNull String[] skeletons, @NonNull String symbolsToIgnore) {
+ for (int i = 0; i < skeletons.length; i++) {
+ final boolean success = addFormatCharsFromSkeleton(
+ collection, locale, skeletons[i], symbolsToIgnore);
+ if (!success) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ /* package */
+ static boolean addAmPmChars(@NonNull Collection<Character> collection,
+ @Nullable Locale locale) {
+ if (locale == null) {
+ return false;
+ }
+ final String[] amPm = LocaleData.get(locale).amPm;
+ for (int i = 0; i < amPm.length; i++) {
+ for (int j = 0; j < amPm[i].length(); j++) {
+ final char ch = amPm[i].charAt(j);
+ if (Character.isBmpCodePoint(ch)) {
+ collection.add(Character.valueOf(ch));
+ } else { // We don't support non-BMP characters.
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /* package */
+ @NonNull
+ static char[] collectionToArray(@NonNull Collection<Character> chars) {
+ final char[] result = new char[chars.size()];
+ int i = 0;
+ for (Character ch : chars) {
+ result[i++] = ch;
+ }
+ return result;
+ }
+}
diff --git a/android/text/method/PasswordTransformationMethod.java b/android/text/method/PasswordTransformationMethod.java
new file mode 100644
index 00000000..4485e385
--- /dev/null
+++ b/android/text/method/PasswordTransformationMethod.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.NoCopySpan;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.style.UpdateLayout;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+
+public class PasswordTransformationMethod
+implements TransformationMethod, TextWatcher
+{
+ public CharSequence getTransformation(CharSequence source, View view) {
+ if (source instanceof Spannable) {
+ Spannable sp = (Spannable) source;
+
+ /*
+ * Remove any references to other views that may still be
+ * attached. This will happen when you flip the screen
+ * while a password field is showing; there will still
+ * be references to the old EditText in the text.
+ */
+ ViewReference[] vr = sp.getSpans(0, sp.length(),
+ ViewReference.class);
+ for (int i = 0; i < vr.length; i++) {
+ sp.removeSpan(vr[i]);
+ }
+
+ removeVisibleSpans(sp);
+
+ sp.setSpan(new ViewReference(view), 0, 0,
+ Spannable.SPAN_POINT_POINT);
+ }
+
+ return new PasswordCharSequence(source);
+ }
+
+ public static PasswordTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new PasswordTransformationMethod();
+ return sInstance;
+ }
+
+ public void beforeTextChanged(CharSequence s, int start,
+ int count, int after) {
+ // This callback isn't used.
+ }
+
+ public void onTextChanged(CharSequence s, int start,
+ int before, int count) {
+ if (s instanceof Spannable) {
+ Spannable sp = (Spannable) s;
+ ViewReference[] vr = sp.getSpans(0, s.length(),
+ ViewReference.class);
+ if (vr.length == 0) {
+ return;
+ }
+
+ /*
+ * There should generally only be one ViewReference in the text,
+ * but make sure to look through all of them if necessary in case
+ * something strange is going on. (We might still end up with
+ * multiple ViewReferences if someone moves text from one password
+ * field to another.)
+ */
+ View v = null;
+ for (int i = 0; v == null && i < vr.length; i++) {
+ v = vr[i].get();
+ }
+
+ if (v == null) {
+ return;
+ }
+
+ int pref = TextKeyListener.getInstance().getPrefs(v.getContext());
+ if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) {
+ if (count > 0) {
+ removeVisibleSpans(sp);
+
+ if (count == 1) {
+ sp.setSpan(new Visible(sp, this), start, start + count,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+ }
+ }
+
+ public void afterTextChanged(Editable s) {
+ // This callback isn't used.
+ }
+
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ if (!focused) {
+ if (sourceText instanceof Spannable) {
+ Spannable sp = (Spannable) sourceText;
+
+ removeVisibleSpans(sp);
+ }
+ }
+ }
+
+ private static void removeVisibleSpans(Spannable sp) {
+ Visible[] old = sp.getSpans(0, sp.length(), Visible.class);
+ for (int i = 0; i < old.length; i++) {
+ sp.removeSpan(old[i]);
+ }
+ }
+
+ private static class PasswordCharSequence
+ implements CharSequence, GetChars
+ {
+ public PasswordCharSequence(CharSequence source) {
+ mSource = source;
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public char charAt(int i) {
+ if (mSource instanceof Spanned) {
+ Spanned sp = (Spanned) mSource;
+
+ int st = sp.getSpanStart(TextKeyListener.ACTIVE);
+ int en = sp.getSpanEnd(TextKeyListener.ACTIVE);
+
+ if (i >= st && i < en) {
+ return mSource.charAt(i);
+ }
+
+ Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
+
+ for (int a = 0; a < visible.length; a++) {
+ if (sp.getSpanStart(visible[a].mTransformer) >= 0) {
+ st = sp.getSpanStart(visible[a]);
+ en = sp.getSpanEnd(visible[a]);
+
+ if (i >= st && i < en) {
+ return mSource.charAt(i);
+ }
+ }
+ }
+ }
+
+ return DOT;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] buf = new char[end - start];
+
+ getChars(start, end, buf, 0);
+ return new String(buf);
+ }
+
+ public String toString() {
+ return subSequence(0, length()).toString();
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+
+ int st = -1, en = -1;
+ int nvisible = 0;
+ int[] starts = null, ends = null;
+
+ if (mSource instanceof Spanned) {
+ Spanned sp = (Spanned) mSource;
+
+ st = sp.getSpanStart(TextKeyListener.ACTIVE);
+ en = sp.getSpanEnd(TextKeyListener.ACTIVE);
+
+ Visible[] visible = sp.getSpans(0, sp.length(), Visible.class);
+ nvisible = visible.length;
+ starts = new int[nvisible];
+ ends = new int[nvisible];
+
+ for (int i = 0; i < nvisible; i++) {
+ if (sp.getSpanStart(visible[i].mTransformer) >= 0) {
+ starts[i] = sp.getSpanStart(visible[i]);
+ ends[i] = sp.getSpanEnd(visible[i]);
+ }
+ }
+ }
+
+ for (int i = start; i < end; i++) {
+ if (! (i >= st && i < en)) {
+ boolean visible = false;
+
+ for (int a = 0; a < nvisible; a++) {
+ if (i >= starts[a] && i < ends[a]) {
+ visible = true;
+ break;
+ }
+ }
+
+ if (!visible) {
+ dest[i - start + off] = DOT;
+ }
+ }
+ }
+ }
+
+ private CharSequence mSource;
+ }
+
+ private static class Visible
+ extends Handler
+ implements UpdateLayout, Runnable
+ {
+ public Visible(Spannable sp, PasswordTransformationMethod ptm) {
+ mText = sp;
+ mTransformer = ptm;
+ postAtTime(this, SystemClock.uptimeMillis() + 1500);
+ }
+
+ public void run() {
+ mText.removeSpan(this);
+ }
+
+ private Spannable mText;
+ private PasswordTransformationMethod mTransformer;
+ }
+
+ /**
+ * Used to stash a reference back to the View in the Editable so we
+ * can use it to check the settings.
+ */
+ private static class ViewReference extends WeakReference<View>
+ implements NoCopySpan {
+ public ViewReference(View v) {
+ super(v);
+ }
+ }
+
+ private static PasswordTransformationMethod sInstance;
+ private static char DOT = '\u2022';
+}
diff --git a/android/text/method/QwertyKeyListener.java b/android/text/method/QwertyKeyListener.java
new file mode 100644
index 00000000..bea68b12
--- /dev/null
+++ b/android/text/method/QwertyKeyListener.java
@@ -0,0 +1,529 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.AutoText;
+import android.text.Editable;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.TextKeyListener.Capitalize;
+import android.util.SparseArray;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * This is the standard key listener for alphabetic input on qwerty
+ * keyboards. You should generally not need to instantiate this yourself;
+ * TextKeyListener will do it for you.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class QwertyKeyListener extends BaseKeyListener {
+ private static QwertyKeyListener[] sInstance =
+ new QwertyKeyListener[Capitalize.values().length * 2];
+ private static QwertyKeyListener sFullKeyboardInstance;
+
+ private Capitalize mAutoCap;
+ private boolean mAutoText;
+ private boolean mFullKeyboard;
+
+ private QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard) {
+ mAutoCap = cap;
+ mAutoText = autoText;
+ mFullKeyboard = fullKeyboard;
+ }
+
+ public QwertyKeyListener(Capitalize cap, boolean autoText) {
+ this(cap, autoText, false);
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ */
+ public static QwertyKeyListener getInstance(boolean autoText, Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autoText ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new QwertyKeyListener(cap, autoText);
+ }
+
+ return sInstance[off];
+ }
+
+ /**
+ * Gets an instance of the listener suitable for use with full keyboards.
+ * Disables auto-capitalization, auto-text and long-press initiated on-screen
+ * character pickers.
+ */
+ public static QwertyKeyListener getInstanceForFullKeyboard() {
+ if (sFullKeyboardInstance == null) {
+ sFullKeyboardInstance = new QwertyKeyListener(Capitalize.NONE, false, true);
+ }
+ return sFullKeyboardInstance;
+ }
+
+ public int getInputType() {
+ return makeTextContentType(mAutoCap, mAutoText);
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ int selStart, selEnd;
+ int pref = 0;
+
+ if (view != null) {
+ pref = TextKeyListener.getInstance().getPrefs(view.getContext());
+ }
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+
+ if (selStart < 0 || selEnd < 0) {
+ selStart = selEnd = 0;
+ Selection.setSelection(content, 0, 0);
+ }
+ }
+
+ int activeStart = content.getSpanStart(TextKeyListener.ACTIVE);
+ int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE);
+
+ // QWERTY keyboard normal case
+
+ int i = event.getUnicodeChar(getMetaState(content, event));
+
+ if (!mFullKeyboard) {
+ int count = event.getRepeatCount();
+ if (count > 0 && selStart == selEnd && selStart > 0) {
+ char c = content.charAt(selStart - 1);
+
+ if ((c == i || c == Character.toUpperCase(i)) && view != null) {
+ if (showCharacterPicker(view, content, c, false, count)) {
+ resetMetaState(content);
+ return true;
+ }
+ }
+ }
+ }
+
+ if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) {
+ if (view != null) {
+ showCharacterPicker(view, content,
+ KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1);
+ }
+ resetMetaState(content);
+ return true;
+ }
+
+ if (i == KeyCharacterMap.HEX_INPUT) {
+ int start;
+
+ if (selStart == selEnd) {
+ start = selEnd;
+
+ while (start > 0 && selEnd - start < 4 &&
+ Character.digit(content.charAt(start - 1), 16) >= 0) {
+ start--;
+ }
+ } else {
+ start = selStart;
+ }
+
+ int ch = -1;
+ try {
+ String hex = TextUtils.substring(content, start, selEnd);
+ ch = Integer.parseInt(hex, 16);
+ } catch (NumberFormatException nfe) { }
+
+ if (ch >= 0) {
+ selStart = start;
+ Selection.setSelection(content, selStart, selEnd);
+ i = ch;
+ } else {
+ i = 0;
+ }
+ }
+
+ if (i != 0) {
+ boolean dead = false;
+
+ if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+ dead = true;
+ i = i & KeyCharacterMap.COMBINING_ACCENT_MASK;
+ }
+
+ if (activeStart == selStart && activeEnd == selEnd) {
+ boolean replace = false;
+
+ if (selEnd - selStart - 1 == 0) {
+ char accent = content.charAt(selStart);
+ int composed = event.getDeadChar(accent, i);
+
+ if (composed != 0) {
+ i = composed;
+ replace = true;
+ dead = false;
+ }
+ }
+
+ if (!replace) {
+ Selection.setSelection(content, selEnd);
+ content.removeSpan(TextKeyListener.ACTIVE);
+ selStart = selEnd;
+ }
+ }
+
+ if ((pref & TextKeyListener.AUTO_CAP) != 0
+ && Character.isLowerCase(i)
+ && TextKeyListener.shouldCap(mAutoCap, content, selStart)) {
+ int where = content.getSpanEnd(TextKeyListener.CAPPED);
+ int flags = content.getSpanFlags(TextKeyListener.CAPPED);
+
+ if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) {
+ content.removeSpan(TextKeyListener.CAPPED);
+ } else {
+ flags = i << 16;
+ i = Character.toUpperCase(i);
+
+ if (selStart == 0)
+ content.setSpan(TextKeyListener.CAPPED, 0, 0,
+ Spannable.SPAN_MARK_MARK | flags);
+ else
+ content.setSpan(TextKeyListener.CAPPED,
+ selStart - 1, selStart,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE |
+ flags);
+ }
+ }
+
+ if (selStart != selEnd) {
+ Selection.setSelection(content, selEnd);
+ }
+ content.setSpan(OLD_SEL_START, selStart, selStart,
+ Spannable.SPAN_MARK_MARK);
+
+ content.replace(selStart, selEnd, String.valueOf((char) i));
+
+ int oldStart = content.getSpanStart(OLD_SEL_START);
+ selEnd = Selection.getSelectionEnd(content);
+
+ if (oldStart < selEnd) {
+ content.setSpan(TextKeyListener.LAST_TYPED,
+ oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ if (dead) {
+ Selection.setSelection(content, oldStart, selEnd);
+ content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ adjustMetaAfterKeypress(content);
+
+ // potentially do autotext replacement if the character
+ // that was typed was an autotext terminator
+
+ if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText &&
+ (i == ' ' || i == '\t' || i == '\n' ||
+ i == ',' || i == '.' || i == '!' || i == '?' ||
+ i == '"' || Character.getType(i) == Character.END_PUNCTUATION) &&
+ content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT)
+ != oldStart) {
+ int x;
+
+ for (x = oldStart; x > 0; x--) {
+ char c = content.charAt(x - 1);
+ if (c != '\'' && !Character.isLetter(c)) {
+ break;
+ }
+ }
+
+ String rep = getReplacement(content, x, oldStart, view);
+
+ if (rep != null) {
+ Replaced[] repl = content.getSpans(0, content.length(),
+ Replaced.class);
+ for (int a = 0; a < repl.length; a++)
+ content.removeSpan(repl[a]);
+
+ char[] orig = new char[oldStart - x];
+ TextUtils.getChars(content, x, oldStart, orig, 0);
+
+ content.setSpan(new Replaced(orig), x, oldStart,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ content.replace(x, oldStart, rep);
+ }
+ }
+
+ // Replace two spaces by a period and a space.
+
+ if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) {
+ selEnd = Selection.getSelectionEnd(content);
+ if (selEnd - 3 >= 0) {
+ if (content.charAt(selEnd - 1) == ' ' &&
+ content.charAt(selEnd - 2) == ' ') {
+ char c = content.charAt(selEnd - 3);
+
+ for (int j = selEnd - 3; j > 0; j--) {
+ if (c == '"' ||
+ Character.getType(c) == Character.END_PUNCTUATION) {
+ c = content.charAt(j - 1);
+ } else {
+ break;
+ }
+ }
+
+ if (Character.isLetter(c) || Character.isDigit(c)) {
+ content.replace(selEnd - 2, selEnd - 1, ".");
+ }
+ }
+ }
+ }
+
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DEL
+ && (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_ALT_ON))
+ && selStart == selEnd) {
+ // special backspace case for undoing autotext
+
+ int consider = 1;
+
+ // if backspacing over the last typed character,
+ // it undoes the autotext prior to that character
+ // (unless the character typed was newline, in which
+ // case this behavior would be confusing)
+
+ if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) {
+ if (content.charAt(selStart - 1) != '\n')
+ consider = 2;
+ }
+
+ Replaced[] repl = content.getSpans(selStart - consider, selStart,
+ Replaced.class);
+
+ if (repl.length > 0) {
+ int st = content.getSpanStart(repl[0]);
+ int en = content.getSpanEnd(repl[0]);
+ String old = new String(repl[0].mText);
+
+ content.removeSpan(repl[0]);
+
+ // only cancel the autocomplete if the cursor is at the end of
+ // the replaced span (or after it, because the user is
+ // backspacing over the space after the word, not the word
+ // itself).
+ if (selStart >= en) {
+ content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
+ en, en, Spannable.SPAN_POINT_POINT);
+ content.replace(st, en, old);
+
+ en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT);
+ if (en - 1 >= 0) {
+ content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT,
+ en - 1, en,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT);
+ }
+ adjustMetaAfterKeypress(content);
+ } else {
+ adjustMetaAfterKeypress(content);
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ return true;
+ }
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+
+ private String getReplacement(CharSequence src, int start, int end,
+ View view) {
+ int len = end - start;
+ boolean changecase = false;
+
+ String replacement = AutoText.get(src, start, end, view);
+
+ if (replacement == null) {
+ String key = TextUtils.substring(src, start, end).toLowerCase();
+ replacement = AutoText.get(key, 0, end - start, view);
+ changecase = true;
+
+ if (replacement == null)
+ return null;
+ }
+
+ int caps = 0;
+
+ if (changecase) {
+ for (int j = start; j < end; j++) {
+ if (Character.isUpperCase(src.charAt(j)))
+ caps++;
+ }
+ }
+
+ String out;
+
+ if (caps == 0)
+ out = replacement;
+ else if (caps == 1)
+ out = toTitleCase(replacement);
+ else if (caps == len)
+ out = replacement.toUpperCase();
+ else
+ out = toTitleCase(replacement);
+
+ if (out.length() == len &&
+ TextUtils.regionMatches(src, start, out, 0, len))
+ return null;
+
+ return out;
+ }
+
+ /**
+ * Marks the specified region of <code>content</code> as having
+ * contained <code>original</code> prior to AutoText replacement.
+ * Call this method when you have done or are about to do an
+ * AutoText-style replacement on a region of text and want to let
+ * the same mechanism (the user pressing DEL immediately after the
+ * change) undo the replacement.
+ *
+ * @param content the Editable text where the replacement was made
+ * @param start the start of the replaced region
+ * @param end the end of the replaced region; the location of the cursor
+ * @param original the text to be restored if the user presses DEL
+ */
+ public static void markAsReplaced(Spannable content, int start, int end,
+ String original) {
+ Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
+ for (int a = 0; a < repl.length; a++) {
+ content.removeSpan(repl[a]);
+ }
+
+ int len = original.length();
+ char[] orig = new char[len];
+ original.getChars(0, len, orig, 0);
+
+ content.setSpan(new Replaced(orig), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static SparseArray<String> PICKER_SETS =
+ new SparseArray<String>();
+ static {
+ PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100");
+ PICKER_SETS.put('C', "\u00C7\u0106\u010C");
+ PICKER_SETS.put('D', "\u010E");
+ PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112");
+ PICKER_SETS.put('G', "\u011E");
+ PICKER_SETS.put('L', "\u0141");
+ PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130");
+ PICKER_SETS.put('N', "\u00D1\u0143\u0147");
+ PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C");
+ PICKER_SETS.put('R', "\u0158");
+ PICKER_SETS.put('S', "\u015A\u0160\u015E");
+ PICKER_SETS.put('T', "\u0164");
+ PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A");
+ PICKER_SETS.put('Y', "\u00DD\u0178");
+ PICKER_SETS.put('Z', "\u0179\u017B\u017D");
+ PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101");
+ PICKER_SETS.put('c', "\u00E7\u0107\u010D");
+ PICKER_SETS.put('d', "\u010F");
+ PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113");
+ PICKER_SETS.put('g', "\u011F");
+ PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131");
+ PICKER_SETS.put('l', "\u0142");
+ PICKER_SETS.put('n', "\u00F1\u0144\u0148");
+ PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D");
+ PICKER_SETS.put('r', "\u0159");
+ PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F");
+ PICKER_SETS.put('t', "\u0165");
+ PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B");
+ PICKER_SETS.put('y', "\u00FD\u00FF");
+ PICKER_SETS.put('z', "\u017A\u017C\u017E");
+ PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
+ "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\|");
+ PICKER_SETS.put('/', "\\");
+
+ // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml
+
+ PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b");
+ PICKER_SETS.put('2', "\u00b2\u2154");
+ PICKER_SETS.put('3', "\u00b3\u00be\u215c");
+ PICKER_SETS.put('4', "\u2074");
+ PICKER_SETS.put('5', "\u215d");
+ PICKER_SETS.put('7', "\u215e");
+ PICKER_SETS.put('0', "\u207f\u2205");
+ PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1");
+ PICKER_SETS.put('%', "\u2030");
+ PICKER_SETS.put('*', "\u2020\u2021");
+ PICKER_SETS.put('-', "\u2013\u2014");
+ PICKER_SETS.put('+', "\u00b1");
+ PICKER_SETS.put('(', "[{<");
+ PICKER_SETS.put(')', "]}>");
+ PICKER_SETS.put('!', "\u00a1");
+ PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd");
+ PICKER_SETS.put('?', "\u00bf");
+ PICKER_SETS.put(',', "\u201a\u201e");
+
+ // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml
+
+ PICKER_SETS.put('=', "\u2260\u2248\u221e");
+ PICKER_SETS.put('<', "\u2264\u00ab\u2039");
+ PICKER_SETS.put('>', "\u2265\u00bb\u203a");
+ };
+
+ private boolean showCharacterPicker(View view, Editable content, char c,
+ boolean insert, int count) {
+ String set = PICKER_SETS.get(c);
+ if (set == null) {
+ return false;
+ }
+
+ if (count == 1) {
+ new CharacterPickerDialog(view.getContext(),
+ view, content, set, insert).show();
+ }
+
+ return true;
+ }
+
+ private static String toTitleCase(String src) {
+ return Character.toUpperCase(src.charAt(0)) + src.substring(1);
+ }
+
+ /* package */ static class Replaced implements NoCopySpan
+ {
+ public Replaced(char[] text) {
+ mText = text;
+ }
+
+ private char[] mText;
+ }
+}
+
diff --git a/android/text/method/ReplacementTransformationMethod.java b/android/text/method/ReplacementTransformationMethod.java
new file mode 100644
index 00000000..d6f879aa
--- /dev/null
+++ b/android/text/method/ReplacementTransformationMethod.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * This transformation method causes the characters in the {@link #getOriginal}
+ * array to be replaced by the corresponding characters in the
+ * {@link #getReplacement} array.
+ */
+public abstract class ReplacementTransformationMethod
+implements TransformationMethod
+{
+ /**
+ * Returns the list of characters that are to be replaced by other
+ * characters when displayed.
+ */
+ protected abstract char[] getOriginal();
+ /**
+ * Returns a parallel array of replacement characters for the ones
+ * that are to be replaced.
+ */
+ protected abstract char[] getReplacement();
+
+ /**
+ * Returns a CharSequence that will mirror the contents of the
+ * source CharSequence but with the characters in {@link #getOriginal}
+ * replaced by ones from {@link #getReplacement}.
+ */
+ public CharSequence getTransformation(CharSequence source, View v) {
+ char[] original = getOriginal();
+ char[] replacement = getReplacement();
+
+ /*
+ * Short circuit for faster display if the text will never change.
+ */
+ if (!(source instanceof Editable)) {
+ /*
+ * Check whether the text does not contain any of the
+ * source characters so can be used unchanged.
+ */
+ boolean doNothing = true;
+ int n = original.length;
+ for (int i = 0; i < n; i++) {
+ if (TextUtils.indexOf(source, original[i]) >= 0) {
+ doNothing = false;
+ break;
+ }
+ }
+ if (doNothing) {
+ return source;
+ }
+
+ if (!(source instanceof Spannable)) {
+ /*
+ * The text contains some of the source characters,
+ * but they can be flattened out now instead of
+ * at display time.
+ */
+ if (source instanceof Spanned) {
+ return new SpannedString(new SpannedReplacementCharSequence(
+ (Spanned) source,
+ original, replacement));
+ } else {
+ return new ReplacementCharSequence(source,
+ original,
+ replacement).toString();
+ }
+ }
+ }
+
+ if (source instanceof Spanned) {
+ return new SpannedReplacementCharSequence((Spanned) source,
+ original, replacement);
+ } else {
+ return new ReplacementCharSequence(source, original, replacement);
+ }
+ }
+
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect) {
+ // This callback isn't used.
+ }
+
+ private static class ReplacementCharSequence
+ implements CharSequence, GetChars {
+ private char[] mOriginal, mReplacement;
+
+ public ReplacementCharSequence(CharSequence source, char[] original,
+ char[] replacement) {
+ mSource = source;
+ mOriginal = original;
+ mReplacement = replacement;
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public char charAt(int i) {
+ char c = mSource.charAt(i);
+
+ int n = mOriginal.length;
+ for (int j = 0; j < n; j++) {
+ if (c == mOriginal[j]) {
+ c = mReplacement[j];
+ }
+ }
+
+ return c;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] c = new char[end - start];
+
+ getChars(start, end, c, 0);
+ return new String(c);
+ }
+
+ public String toString() {
+ char[] c = new char[length()];
+
+ getChars(0, length(), c, 0);
+ return new String(c);
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+ int offend = end - start + off;
+ int n = mOriginal.length;
+
+ for (int i = off; i < offend; i++) {
+ char c = dest[i];
+
+ for (int j = 0; j < n; j++) {
+ if (c == mOriginal[j]) {
+ dest[i] = mReplacement[j];
+ }
+ }
+ }
+ }
+
+ private CharSequence mSource;
+ }
+
+ private static class SpannedReplacementCharSequence
+ extends ReplacementCharSequence
+ implements Spanned
+ {
+ public SpannedReplacementCharSequence(Spanned source, char[] original,
+ char[] replacement) {
+ super(source, original, replacement);
+ mSpanned = source;
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return new SpannedString(this).subSequence(start, end);
+ }
+
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ return mSpanned.getSpans(start, end, type);
+ }
+
+ public int getSpanStart(Object tag) {
+ return mSpanned.getSpanStart(tag);
+ }
+
+ public int getSpanEnd(Object tag) {
+ return mSpanned.getSpanEnd(tag);
+ }
+
+ public int getSpanFlags(Object tag) {
+ return mSpanned.getSpanFlags(tag);
+ }
+
+ public int nextSpanTransition(int start, int end, Class type) {
+ return mSpanned.nextSpanTransition(start, end, type);
+ }
+
+ private Spanned mSpanned;
+ }
+}
diff --git a/android/text/method/ScrollingMovementMethod.java b/android/text/method/ScrollingMovementMethod.java
new file mode 100644
index 00000000..4f422cbf
--- /dev/null
+++ b/android/text/method/ScrollingMovementMethod.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Layout;
+import android.text.Spannable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * A movement method that interprets movement keys by scrolling the text buffer.
+ */
+public class ScrollingMovementMethod extends BaseMovementMethod implements MovementMethod {
+ @Override
+ protected boolean left(TextView widget, Spannable buffer) {
+ return scrollLeft(widget, buffer, 1);
+ }
+
+ @Override
+ protected boolean right(TextView widget, Spannable buffer) {
+ return scrollRight(widget, buffer, 1);
+ }
+
+ @Override
+ protected boolean up(TextView widget, Spannable buffer) {
+ return scrollUp(widget, buffer, 1);
+ }
+
+ @Override
+ protected boolean down(TextView widget, Spannable buffer) {
+ return scrollDown(widget, buffer, 1);
+ }
+
+ @Override
+ protected boolean pageUp(TextView widget, Spannable buffer) {
+ return scrollPageUp(widget, buffer);
+ }
+
+ @Override
+ protected boolean pageDown(TextView widget, Spannable buffer) {
+ return scrollPageDown(widget, buffer);
+ }
+
+ @Override
+ protected boolean top(TextView widget, Spannable buffer) {
+ return scrollTop(widget, buffer);
+ }
+
+ @Override
+ protected boolean bottom(TextView widget, Spannable buffer) {
+ return scrollBottom(widget, buffer);
+ }
+
+ @Override
+ protected boolean lineStart(TextView widget, Spannable buffer) {
+ return scrollLineStart(widget, buffer);
+ }
+
+ @Override
+ protected boolean lineEnd(TextView widget, Spannable buffer) {
+ return scrollLineEnd(widget, buffer);
+ }
+
+ @Override
+ protected boolean home(TextView widget, Spannable buffer) {
+ return top(widget, buffer);
+ }
+
+ @Override
+ protected boolean end(TextView widget, Spannable buffer) {
+ return bottom(widget, buffer);
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ return Touch.onTouchEvent(widget, buffer, event);
+ }
+
+ @Override
+ public void onTakeFocus(TextView widget, Spannable text, int dir) {
+ Layout layout = widget.getLayout();
+
+ if (layout != null && (dir & View.FOCUS_FORWARD) != 0) {
+ widget.scrollTo(widget.getScrollX(),
+ layout.getLineTop(0));
+ }
+ if (layout != null && (dir & View.FOCUS_BACKWARD) != 0) {
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int line = layout.getLineCount() - 1;
+
+ widget.scrollTo(widget.getScrollX(),
+ layout.getLineTop(line+1) -
+ (widget.getHeight() - padding));
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new ScrollingMovementMethod();
+
+ return sInstance;
+ }
+
+ private static ScrollingMovementMethod sInstance;
+}
diff --git a/android/text/method/SingleLineTransformationMethod.java b/android/text/method/SingleLineTransformationMethod.java
new file mode 100644
index 00000000..818526a7
--- /dev/null
+++ b/android/text/method/SingleLineTransformationMethod.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+/**
+ * This transformation method causes any newline characters (\n) to be
+ * displayed as spaces instead of causing line breaks, and causes
+ * carriage return characters (\r) to have no appearance.
+ */
+public class SingleLineTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\n', '\r' };
+ private static char[] REPLACEMENT = new char[] { ' ', '\uFEFF' };
+
+ /**
+ * The characters to be replaced are \n and \r.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character \n is replaced with is space;
+ * the character \r is replaced with is FEFF (zero width space).
+ */
+ protected char[] getReplacement() {
+ return REPLACEMENT;
+ }
+
+ public static SingleLineTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new SingleLineTransformationMethod();
+ return sInstance;
+ }
+
+ private static SingleLineTransformationMethod sInstance;
+}
diff --git a/android/text/method/TextKeyListener.java b/android/text/method/TextKeyListener.java
new file mode 100644
index 00000000..9cbda9c0
--- /dev/null
+++ b/android/text/method/TextKeyListener.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.System;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This is the key listener for typing normal text. It delegates to
+ * other key listeners appropriate to the current keyboard and language.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class TextKeyListener extends BaseKeyListener implements SpanWatcher {
+ private static TextKeyListener[] sInstance =
+ new TextKeyListener[Capitalize.values().length * 2];
+
+ /* package */ static final Object ACTIVE = new NoCopySpan.Concrete();
+ /* package */ static final Object CAPPED = new NoCopySpan.Concrete();
+ /* package */ static final Object INHIBIT_REPLACEMENT = new NoCopySpan.Concrete();
+ /* package */ static final Object LAST_TYPED = new NoCopySpan.Concrete();
+
+ private Capitalize mAutoCap;
+ private boolean mAutoText;
+
+ private int mPrefs;
+ private boolean mPrefsInited;
+
+ /* package */ static final int AUTO_CAP = 1;
+ /* package */ static final int AUTO_TEXT = 2;
+ /* package */ static final int AUTO_PERIOD = 4;
+ /* package */ static final int SHOW_PASSWORD = 8;
+ private WeakReference<ContentResolver> mResolver;
+ private TextKeyListener.SettingsObserver mObserver;
+
+ /**
+ * Creates a new TextKeyListener with the specified capitalization
+ * and correction properties.
+ *
+ * @param cap when, if ever, to automatically capitalize.
+ * @param autotext whether to automatically do spelling corrections.
+ */
+ public TextKeyListener(Capitalize cap, boolean autotext) {
+ mAutoCap = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * Returns a new or existing instance with the specified capitalization
+ * and correction properties.
+ *
+ * @param cap when, if ever, to automatically capitalize.
+ * @param autotext whether to automatically do spelling corrections.
+ */
+ public static TextKeyListener getInstance(boolean autotext,
+ Capitalize cap) {
+ int off = cap.ordinal() * 2 + (autotext ? 1 : 0);
+
+ if (sInstance[off] == null) {
+ sInstance[off] = new TextKeyListener(cap, autotext);
+ }
+
+ return sInstance[off];
+ }
+
+ /**
+ * Returns a new or existing instance with no automatic capitalization
+ * or correction.
+ */
+ public static TextKeyListener getInstance() {
+ return getInstance(false, Capitalize.NONE);
+ }
+
+ /**
+ * Returns whether it makes sense to automatically capitalize at the
+ * specified position in the specified text, with the specified rules.
+ *
+ * @param cap the capitalization rules to consider.
+ * @param cs the text in which an insertion is being made.
+ * @param off the offset into that text where the insertion is being made.
+ *
+ * @return whether the character being inserted should be capitalized.
+ */
+ public static boolean shouldCap(Capitalize cap, CharSequence cs, int off) {
+ int i;
+ char c;
+
+ if (cap == Capitalize.NONE) {
+ return false;
+ }
+ if (cap == Capitalize.CHARACTERS) {
+ return true;
+ }
+
+ return TextUtils.getCapsMode(cs, off, cap == Capitalize.WORDS
+ ? TextUtils.CAP_MODE_WORDS : TextUtils.CAP_MODE_SENTENCES)
+ != 0;
+ }
+
+ public int getInputType() {
+ return makeTextContentType(mAutoCap, mAutoText);
+ }
+
+ @Override
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ KeyListener im = getKeyListener(event);
+
+ return im.onKeyDown(view, content, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ KeyListener im = getKeyListener(event);
+
+ return im.onKeyUp(view, content, keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyOther(View view, Editable content, KeyEvent event) {
+ KeyListener im = getKeyListener(event);
+
+ return im.onKeyOther(view, content, event);
+ }
+
+ /**
+ * Clear all the input state (autotext, autocap, multitap, undo)
+ * from the specified Editable, going beyond Editable.clear(), which
+ * just clears the text but not the input state.
+ *
+ * @param e the buffer whose text and state are to be cleared.
+ */
+ public static void clear(Editable e) {
+ e.clear();
+ e.removeSpan(ACTIVE);
+ e.removeSpan(CAPPED);
+ e.removeSpan(INHIBIT_REPLACEMENT);
+ e.removeSpan(LAST_TYPED);
+
+ QwertyKeyListener.Replaced[] repl = e.getSpans(0, e.length(),
+ QwertyKeyListener.Replaced.class);
+ final int count = repl.length;
+ for (int i = 0; i < count; i++) {
+ e.removeSpan(repl[i]);
+ }
+ }
+
+ public void onSpanAdded(Spannable s, Object what, int start, int end) { }
+ public void onSpanRemoved(Spannable s, Object what, int start, int end) { }
+
+ public void onSpanChanged(Spannable s, Object what, int start, int end,
+ int st, int en) {
+ if (what == Selection.SELECTION_END) {
+ s.removeSpan(ACTIVE);
+ }
+ }
+
+ private KeyListener getKeyListener(KeyEvent event) {
+ KeyCharacterMap kmap = event.getKeyCharacterMap();
+ int kind = kmap.getKeyboardType();
+
+ if (kind == KeyCharacterMap.ALPHA) {
+ return QwertyKeyListener.getInstance(mAutoText, mAutoCap);
+ } else if (kind == KeyCharacterMap.NUMERIC) {
+ return MultiTapKeyListener.getInstance(mAutoText, mAutoCap);
+ } else if (kind == KeyCharacterMap.FULL
+ || kind == KeyCharacterMap.SPECIAL_FUNCTION) {
+ // We consider special function keyboards full keyboards as a workaround for
+ // devices that do not have built-in keyboards. Applications may try to inject
+ // key events using the built-in keyboard device id which may be configured as
+ // a special function keyboard using a default key map. Ideally, as of Honeycomb,
+ // these applications should be modified to use KeyCharacterMap.VIRTUAL_KEYBOARD.
+ return QwertyKeyListener.getInstanceForFullKeyboard();
+ }
+
+ return NullKeyListener.getInstance();
+ }
+
+ public enum Capitalize {
+ NONE, SENTENCES, WORDS, CHARACTERS,
+ }
+
+ private static class NullKeyListener implements KeyListener
+ {
+ public int getInputType() {
+ return InputType.TYPE_NULL;
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyUp(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ return false;
+ }
+
+ public boolean onKeyOther(View view, Editable content, KeyEvent event) {
+ return false;
+ }
+
+ public void clearMetaKeyState(View view, Editable content, int states) {
+ }
+
+ public static NullKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new NullKeyListener();
+ return sInstance;
+ }
+
+ private static NullKeyListener sInstance;
+ }
+
+ public void release() {
+ if (mResolver != null) {
+ final ContentResolver contentResolver = mResolver.get();
+ if (contentResolver != null) {
+ contentResolver.unregisterContentObserver(mObserver);
+ mResolver.clear();
+ }
+ mObserver = null;
+ mResolver = null;
+ mPrefsInited = false;
+ }
+ }
+
+ private void initPrefs(Context context) {
+ final ContentResolver contentResolver = context.getContentResolver();
+ mResolver = new WeakReference<ContentResolver>(contentResolver);
+ if (mObserver == null) {
+ mObserver = new SettingsObserver();
+ contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, mObserver);
+ }
+
+ updatePrefs(contentResolver);
+ mPrefsInited = true;
+ }
+
+ private class SettingsObserver extends ContentObserver {
+ public SettingsObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (mResolver != null) {
+ final ContentResolver contentResolver = mResolver.get();
+ if (contentResolver == null) {
+ mPrefsInited = false;
+ } else {
+ updatePrefs(contentResolver);
+ }
+ } else {
+ mPrefsInited = false;
+ }
+ }
+ }
+
+ private void updatePrefs(ContentResolver resolver) {
+ boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0;
+ boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0;
+ boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0;
+ boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0;
+
+ mPrefs = (cap ? AUTO_CAP : 0) |
+ (text ? AUTO_TEXT : 0) |
+ (period ? AUTO_PERIOD : 0) |
+ (pw ? SHOW_PASSWORD : 0);
+ }
+
+ /* package */ int getPrefs(Context context) {
+ synchronized (this) {
+ if (!mPrefsInited || mResolver.get() == null) {
+ initPrefs(context);
+ }
+ }
+
+ return mPrefs;
+ }
+}
diff --git a/android/text/method/TimeKeyListener.java b/android/text/method/TimeKeyListener.java
new file mode 100644
index 00000000..f11f4009
--- /dev/null
+++ b/android/text/method/TimeKeyListener.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.InputType;
+import android.view.KeyEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+/**
+ * For entering times in a text field.
+ * <p></p>
+ * As for all implementations of {@link KeyListener}, this class is only concerned
+ * with hardware keyboards. Software input methods have no obligation to trigger
+ * the methods in this class.
+ */
+public class TimeKeyListener extends NumberKeyListener
+{
+ public int getInputType() {
+ if (mNeedsAdvancedInput) {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+ } else {
+ return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME;
+ }
+ }
+
+ @Override
+ @NonNull
+ protected char[] getAcceptedChars()
+ {
+ return mCharacters;
+ }
+
+ /**
+ * @deprecated Use {@link #TimeKeyListener(Locale)} instead.
+ */
+ @Deprecated
+ public TimeKeyListener() {
+ this(null);
+ }
+
+ private static final String SYMBOLS_TO_IGNORE = "ahHKkms";
+ private static final String SKELETON_12HOUR = "hms";
+ private static final String SKELETON_24HOUR = "Hms";
+
+ public TimeKeyListener(@Nullable Locale locale) {
+ final LinkedHashSet<Character> chars = new LinkedHashSet<>();
+ // First add the digits. Then, add all the character in AM and PM markers. Finally, add all
+ // the non-pattern characters seen in the patterns for "hms" and "Hms".
+ final boolean success = NumberKeyListener.addDigits(chars, locale)
+ && NumberKeyListener.addAmPmChars(chars, locale)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE)
+ && NumberKeyListener.addFormatCharsFromSkeleton(
+ chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE);
+ if (success) {
+ mCharacters = NumberKeyListener.collectionToArray(chars);
+ if (locale != null && "en".equals(locale.getLanguage())) {
+ // For backward compatibility reasons, assume we don't need advanced input for
+ // English locales, although English locales may need uppercase letters for
+ // AM and PM.
+ mNeedsAdvancedInput = false;
+ } else {
+ mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters);
+ }
+ } else {
+ mCharacters = CHARACTERS;
+ mNeedsAdvancedInput = false;
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #getInstance(Locale)} instead.
+ */
+ @Deprecated
+ @NonNull
+ public static TimeKeyListener getInstance() {
+ return getInstance(null);
+ }
+
+ /**
+ * Returns an instance of TimeKeyListener appropriate for the given locale.
+ */
+ @NonNull
+ public static TimeKeyListener getInstance(@Nullable Locale locale) {
+ TimeKeyListener instance;
+ synchronized (sLock) {
+ instance = sInstanceCache.get(locale);
+ if (instance == null) {
+ instance = new TimeKeyListener(locale);
+ sInstanceCache.put(locale, instance);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * This field used to list the characters that were used. But is now a fixed data
+ * field that is the list of code units used for the deprecated case where the class
+ * is instantiated with null or no input parameter.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ *
+ * @deprecated Use {@link #getAcceptedChars()} instead.
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
+ 'p', ':'
+ };
+
+ private final char[] mCharacters;
+ private final boolean mNeedsAdvancedInput;
+
+ private static final Object sLock = new Object();
+ @GuardedBy("sLock")
+ private static final HashMap<Locale, TimeKeyListener> sInstanceCache = new HashMap<>();
+}
diff --git a/android/text/method/Touch.java b/android/text/method/Touch.java
new file mode 100644
index 00000000..44811cb3
--- /dev/null
+++ b/android/text/method/Touch.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.text.Layout;
+import android.text.Layout.Alignment;
+import android.text.NoCopySpan;
+import android.text.Spannable;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.TextView;
+
+public class Touch {
+ private Touch() { }
+
+ /**
+ * Scrolls the specified widget to the specified coordinates, except
+ * constrains the X scrolling position to the horizontal regions of
+ * the text that will be visible after scrolling to the specified
+ * Y position.
+ */
+ public static void scrollTo(TextView widget, Layout layout, int x, int y) {
+ final int horizontalPadding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
+ final int availableWidth = widget.getWidth() - horizontalPadding;
+
+ final int top = layout.getLineForVertical(y);
+ Alignment a = layout.getParagraphAlignment(top);
+ boolean ltr = layout.getParagraphDirection(top) > 0;
+
+ int left, right;
+ if (widget.getHorizontallyScrolling()) {
+ final int verticalPadding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
+ final int bottom = layout.getLineForVertical(y + widget.getHeight() - verticalPadding);
+
+ left = Integer.MAX_VALUE;
+ right = 0;
+
+ for (int i = top; i <= bottom; i++) {
+ left = (int) Math.min(left, layout.getLineLeft(i));
+ right = (int) Math.max(right, layout.getLineRight(i));
+ }
+ } else {
+ left = 0;
+ right = availableWidth;
+ }
+
+ final int actualWidth = right - left;
+
+ if (actualWidth < availableWidth) {
+ if (a == Alignment.ALIGN_CENTER) {
+ x = left - ((availableWidth - actualWidth) / 2);
+ } else if ((ltr && (a == Alignment.ALIGN_OPPOSITE)) ||
+ (!ltr && (a == Alignment.ALIGN_NORMAL)) ||
+ (a == Alignment.ALIGN_RIGHT)) {
+ // align_opposite does NOT mean align_right, we need the paragraph
+ // direction to resolve it to left or right
+ x = left - (availableWidth - actualWidth);
+ } else {
+ x = left;
+ }
+ } else {
+ x = Math.min(x, right - availableWidth);
+ x = Math.max(x, left);
+ }
+
+ widget.scrollTo(x, y);
+ }
+
+ /**
+ * Handles touch events for dragging. You may want to do other actions
+ * like moving the cursor on touch as well.
+ */
+ public static boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ DragState[] ds;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ ds = buffer.getSpans(0, buffer.length(), DragState.class);
+
+ for (int i = 0; i < ds.length; i++) {
+ buffer.removeSpan(ds[i]);
+ }
+
+ buffer.setSpan(new DragState(event.getX(), event.getY(),
+ widget.getScrollX(), widget.getScrollY()),
+ 0, 0, Spannable.SPAN_MARK_MARK);
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ ds = buffer.getSpans(0, buffer.length(), DragState.class);
+
+ for (int i = 0; i < ds.length; i++) {
+ buffer.removeSpan(ds[i]);
+ }
+
+ if (ds.length > 0 && ds[0].mUsed) {
+ return true;
+ } else {
+ return false;
+ }
+
+ case MotionEvent.ACTION_MOVE:
+ ds = buffer.getSpans(0, buffer.length(), DragState.class);
+
+ if (ds.length > 0) {
+ if (ds[0].mFarEnough == false) {
+ int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
+
+ if (Math.abs(event.getX() - ds[0].mX) >= slop ||
+ Math.abs(event.getY() - ds[0].mY) >= slop) {
+ ds[0].mFarEnough = true;
+ }
+ }
+
+ if (ds[0].mFarEnough) {
+ ds[0].mUsed = true;
+ boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0
+ || MetaKeyKeyListener.getMetaState(buffer,
+ MetaKeyKeyListener.META_SHIFT_ON) == 1
+ || MetaKeyKeyListener.getMetaState(buffer,
+ MetaKeyKeyListener.META_SELECTING) != 0;
+
+ float dx;
+ float dy;
+ if (cap) {
+ // if we're selecting, we want the scroll to go in
+ // the direction of the drag
+ dx = event.getX() - ds[0].mX;
+ dy = event.getY() - ds[0].mY;
+ } else {
+ dx = ds[0].mX - event.getX();
+ dy = ds[0].mY - event.getY();
+ }
+ ds[0].mX = event.getX();
+ ds[0].mY = event.getY();
+
+ int nx = widget.getScrollX() + (int) dx;
+ int ny = widget.getScrollY() + (int) dy;
+
+ int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
+ Layout layout = widget.getLayout();
+
+ ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - padding));
+ ny = Math.max(ny, 0);
+
+ int oldX = widget.getScrollX();
+ int oldY = widget.getScrollY();
+
+ scrollTo(widget, layout, nx, ny);
+
+ // If we actually scrolled, then cancel the up action.
+ if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
+ widget.cancelLongPress();
+ }
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ */
+ public static int getInitialScrollX(TextView widget, Spannable buffer) {
+ DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class);
+ return ds.length > 0 ? ds[0].mScrollX : -1;
+ }
+
+ /**
+ * @param widget The text view.
+ * @param buffer The text buffer.
+ */
+ public static int getInitialScrollY(TextView widget, Spannable buffer) {
+ DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class);
+ return ds.length > 0 ? ds[0].mScrollY : -1;
+ }
+
+ private static class DragState implements NoCopySpan {
+ public float mX;
+ public float mY;
+ public int mScrollX;
+ public int mScrollY;
+ public boolean mFarEnough;
+ public boolean mUsed;
+
+ public DragState(float x, float y, int scrollX, int scrollY) {
+ mX = x;
+ mY = y;
+ mScrollX = scrollX;
+ mScrollY = scrollY;
+ }
+ }
+}
diff --git a/android/text/method/TransformationMethod.java b/android/text/method/TransformationMethod.java
new file mode 100644
index 00000000..b542109d
--- /dev/null
+++ b/android/text/method/TransformationMethod.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.graphics.Rect;
+import android.view.View;
+
+/**
+ * TextView uses TransformationMethods to do things like replacing the
+ * characters of passwords with dots, or keeping the newline characters
+ * from causing line breaks in single-line text fields.
+ */
+public interface TransformationMethod
+{
+ /**
+ * Returns a CharSequence that is a transformation of the source text --
+ * for example, replacing each character with a dot in a password field.
+ * Beware that the returned text must be exactly the same length as
+ * the source text, and that if the source text is Editable, the returned
+ * text must mirror it dynamically instead of doing a one-time copy.
+ */
+ public CharSequence getTransformation(CharSequence source, View view);
+
+ /**
+ * This method is called when the TextView that uses this
+ * TransformationMethod gains or loses focus.
+ */
+ public void onFocusChanged(View view, CharSequence sourceText,
+ boolean focused, int direction,
+ Rect previouslyFocusedRect);
+}
diff --git a/android/text/method/TransformationMethod2.java b/android/text/method/TransformationMethod2.java
new file mode 100644
index 00000000..ef00ecdb
--- /dev/null
+++ b/android/text/method/TransformationMethod2.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.text.method;
+
+/**
+ * TransformationMethod2 extends the TransformationMethod interface
+ * and adds the ability to relax restrictions of TransformationMethod.
+ *
+ * @hide
+ */
+public interface TransformationMethod2 extends TransformationMethod {
+ /**
+ * Relax the contract of TransformationMethod to allow length changes,
+ * or revert to the length-restricted behavior.
+ *
+ * @param allowLengthChanges true to allow the transformation to change the length
+ * of the input string.
+ */
+ public void setLengthChangesAllowed(boolean allowLengthChanges);
+}
diff --git a/android/text/method/WordIterator.java b/android/text/method/WordIterator.java
new file mode 100644
index 00000000..33e96a86
--- /dev/null
+++ b/android/text/method/WordIterator.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text.method;
+
+import android.annotation.NonNull;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UProperty;
+import android.icu.text.BreakIterator;
+import android.text.CharSequenceCharacterIterator;
+import android.text.Selection;
+
+import java.util.Locale;
+
+/**
+ * Walks through cursor positions at word boundaries. Internally uses
+ * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
+ * for performance reasons.
+ *
+ * Also provides methods to determine word boundaries.
+ * {@hide}
+ */
+public class WordIterator implements Selection.PositionIterator {
+ // Size of the window for the word iterator, should be greater than the longest word's length
+ private static final int WINDOW_WIDTH = 50;
+
+ private int mStart, mEnd;
+ private CharSequence mCharSeq;
+ private final BreakIterator mIterator;
+
+ /**
+ * Constructs a WordIterator using the default locale.
+ */
+ public WordIterator() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Constructs a new WordIterator for the specified locale.
+ * @param locale The locale to be used for analyzing the text.
+ */
+ public WordIterator(Locale locale) {
+ mIterator = BreakIterator.getWordInstance(locale);
+ }
+
+ public void setCharSequence(@NonNull CharSequence charSequence, int start, int end) {
+ if (0 <= start && end <= charSequence.length()) {
+ mCharSeq = charSequence;
+ mStart = Math.max(0, start - WINDOW_WIDTH);
+ mEnd = Math.min(charSequence.length(), end + WINDOW_WIDTH);
+ mIterator.setText(new CharSequenceCharacterIterator(charSequence, mStart, mEnd));
+ } else {
+ throw new IndexOutOfBoundsException("input indexes are outside the CharSequence");
+ }
+ }
+
+ /** {@inheritDoc} */
+ public int preceding(int offset) {
+ checkOffsetIsValid(offset);
+ while (true) {
+ offset = mIterator.preceding(offset);
+ if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
+ return offset;
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public int following(int offset) {
+ checkOffsetIsValid(offset);
+ while (true) {
+ offset = mIterator.following(offset);
+ if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
+ return offset;
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public boolean isBoundary(int offset) {
+ checkOffsetIsValid(offset);
+ return mIterator.isBoundary(offset);
+ }
+
+ /**
+ * Returns the position of next boundary after the given offset. Returns
+ * {@code DONE} if there is no boundary after the given offset.
+ *
+ * @param offset the given start position to search from.
+ * @return the position of the last boundary preceding the given offset.
+ */
+ public int nextBoundary(int offset) {
+ checkOffsetIsValid(offset);
+ return mIterator.following(offset);
+ }
+
+ /**
+ * Returns the position of boundary preceding the given offset or
+ * {@code DONE} if the given offset specifies the starting position.
+ *
+ * @param offset the given start position to search from.
+ * @return the position of the last boundary preceding the given offset.
+ */
+ public int prevBoundary(int offset) {
+ checkOffsetIsValid(offset);
+ return mIterator.preceding(offset);
+ }
+
+ /** If <code>offset</code> is within a word, returns the index of the first character of that
+ * word, otherwise returns BreakIterator.DONE.
+ *
+ * The offsets that are considered to be part of a word are the indexes of its characters,
+ * <i>as well as</i> the index of its last character plus one.
+ * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
+ *
+ * Valid range for offset is [0..textLength] (note the inclusive upper bound).
+ * The returned value is within [0..offset] or BreakIterator.DONE.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ public int getBeginning(int offset) {
+ // TODO: Check if usage of this can be updated to getBeginning(offset, true) if
+ // so this method can be removed.
+ return getBeginning(offset, false);
+ }
+
+ /**
+ * If <code>offset</code> is within a word, returns the index of the last character of that
+ * word plus one, otherwise returns BreakIterator.DONE.
+ *
+ * The offsets that are considered to be part of a word are the indexes of its characters,
+ * <i>as well as</i> the index of its last character plus one.
+ * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
+ *
+ * Valid range for offset is [0..textLength] (note the inclusive upper bound).
+ * The returned value is within [offset..textLength] or BreakIterator.DONE.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ public int getEnd(int offset) {
+ // TODO: Check if usage of this can be updated to getEnd(offset, true), if
+ // so this method can be removed.
+ return getEnd(offset, false);
+ }
+
+ /**
+ * If the <code>offset</code> is within a word or on a word boundary that can only be
+ * considered the start of a word (e.g. _word where "_" is any character that would not
+ * be considered part of the word) then this returns the index of the first character of
+ * that word.
+ *
+ * If the offset is on a word boundary that can be considered the start and end of a
+ * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary
+ * between AA and BB, this would return the start of the previous word, AA.
+ *
+ * Returns BreakIterator.DONE if there is no previous boundary.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ public int getPrevWordBeginningOnTwoWordsBoundary(int offset) {
+ return getBeginning(offset, true);
+ }
+
+ /**
+ * If the <code>offset</code> is within a word or on a word boundary that can only be
+ * considered the end of a word (e.g. word_ where "_" is any character that would not
+ * be considered part of the word) then this returns the index of the last character
+ * plus one of that word.
+ *
+ * If the offset is on a word boundary that can be considered the start and end of a
+ * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary
+ * between AA and BB, this would return the end of the next word, BB.
+ *
+ * Returns BreakIterator.DONE if there is no next boundary.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ public int getNextWordEndOnTwoWordBoundary(int offset) {
+ return getEnd(offset, true);
+ }
+
+ /**
+ * If the <code>offset</code> is within a word or on a word boundary that can only be
+ * considered the start of a word (e.g. _word where "_" is any character that would not
+ * be considered part of the word) then this returns the index of the first character of
+ * that word.
+ *
+ * If the offset is on a word boundary that can be considered the start and end of a
+ * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary
+ * between AA and BB, and getPrevWordBeginningOnTwoWordsBoundary is true then this would
+ * return the start of the previous word, AA. Otherwise it would return the current offset,
+ * the start of BB.
+ *
+ * Returns BreakIterator.DONE if there is no previous boundary.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ private int getBeginning(int offset, boolean getPrevWordBeginningOnTwoWordsBoundary) {
+ checkOffsetIsValid(offset);
+
+ if (isOnLetterOrDigit(offset)) {
+ if (mIterator.isBoundary(offset)
+ && (!isAfterLetterOrDigit(offset)
+ || !getPrevWordBeginningOnTwoWordsBoundary)) {
+ return offset;
+ } else {
+ return mIterator.preceding(offset);
+ }
+ } else {
+ if (isAfterLetterOrDigit(offset)) {
+ return mIterator.preceding(offset);
+ }
+ }
+ return BreakIterator.DONE;
+ }
+
+ /**
+ * If the <code>offset</code> is within a word or on a word boundary that can only be
+ * considered the end of a word (e.g. word_ where "_" is any character that would not be
+ * considered part of the word) then this returns the index of the last character plus one
+ * of that word.
+ *
+ * If the offset is on a word boundary that can be considered the start and end of a
+ * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary
+ * between AA and BB, and getNextWordEndOnTwoWordBoundary is true then this would return
+ * the end of the next word, BB. Otherwise it would return the current offset, the end
+ * of AA.
+ *
+ * Returns BreakIterator.DONE if there is no next boundary.
+ *
+ * @throws IllegalArgumentException is offset is not valid.
+ */
+ private int getEnd(int offset, boolean getNextWordEndOnTwoWordBoundary) {
+ checkOffsetIsValid(offset);
+
+ if (isAfterLetterOrDigit(offset)) {
+ if (mIterator.isBoundary(offset)
+ && (!isOnLetterOrDigit(offset) || !getNextWordEndOnTwoWordBoundary)) {
+ return offset;
+ } else {
+ return mIterator.following(offset);
+ }
+ } else {
+ if (isOnLetterOrDigit(offset)) {
+ return mIterator.following(offset);
+ }
+ }
+ return BreakIterator.DONE;
+ }
+
+ /**
+ * If <code>offset</code> is within a group of punctuation as defined
+ * by {@link #isPunctuation(int)}, returns the index of the first character
+ * of that group, otherwise returns BreakIterator.DONE.
+ *
+ * @param offset the offset to search from.
+ */
+ public int getPunctuationBeginning(int offset) {
+ checkOffsetIsValid(offset);
+ while (offset != BreakIterator.DONE && !isPunctuationStartBoundary(offset)) {
+ offset = prevBoundary(offset);
+ }
+ // No need to shift offset, prevBoundary handles that.
+ return offset;
+ }
+
+ /**
+ * If <code>offset</code> is within a group of punctuation as defined
+ * by {@link #isPunctuation(int)}, returns the index of the last character
+ * of that group plus one, otherwise returns BreakIterator.DONE.
+ *
+ * @param offset the offset to search from.
+ */
+ public int getPunctuationEnd(int offset) {
+ checkOffsetIsValid(offset);
+ while (offset != BreakIterator.DONE && !isPunctuationEndBoundary(offset)) {
+ offset = nextBoundary(offset);
+ }
+ // No need to shift offset, nextBoundary handles that.
+ return offset;
+ }
+
+ /**
+ * Indicates if the provided offset is after a punctuation character
+ * as defined by {@link #isPunctuation(int)}.
+ *
+ * @param offset the offset to check from.
+ * @return Whether the offset is after a punctuation character.
+ */
+ public boolean isAfterPunctuation(int offset) {
+ if (mStart < offset && offset <= mEnd) {
+ final int codePoint = Character.codePointBefore(mCharSeq, offset);
+ return isPunctuation(codePoint);
+ }
+ return false;
+ }
+
+ /**
+ * Indicates if the provided offset is at a punctuation character
+ * as defined by {@link #isPunctuation(int)}.
+ *
+ * @param offset the offset to check from.
+ * @return Whether the offset is at a punctuation character.
+ */
+ public boolean isOnPunctuation(int offset) {
+ if (mStart <= offset && offset < mEnd) {
+ final int codePoint = Character.codePointAt(mCharSeq, offset);
+ return isPunctuation(codePoint);
+ }
+ return false;
+ }
+
+ /**
+ * Indicates if the codepoint is a mid-word-only punctuation.
+ *
+ * At the moment, this is locale-independent, and includes all the characters in
+ * the MidLetter, MidNumLet, and Single_Quote class of Unicode word breaking algorithm (see
+ * UAX #29 "Unicode Text Segmentation" at http://unicode.org/reports/tr29/). These are all the
+ * characters that according to the rules WB6 and WB7 of UAX #29 prevent word breaks if they are
+ * in the middle of a word, but they become word breaks if they happen at the end of a word
+ * (accroding to rule WB999 that breaks word in any place that is not prohibited otherwise).
+ *
+ * @param locale the locale to consider the codepoint in. Presently ignored.
+ * @param codePoint the codepoint to check.
+ * @return True if the codepoint is a mid-word punctuation.
+ */
+ public static boolean isMidWordPunctuation(Locale locale, int codePoint) {
+ final int wb = UCharacter.getIntPropertyValue(codePoint, UProperty.WORD_BREAK);
+ return (wb == UCharacter.WordBreak.MIDLETTER
+ || wb == UCharacter.WordBreak.MIDNUMLET
+ || wb == UCharacter.WordBreak.SINGLE_QUOTE);
+ }
+
+ private boolean isPunctuationStartBoundary(int offset) {
+ return isOnPunctuation(offset) && !isAfterPunctuation(offset);
+ }
+
+ private boolean isPunctuationEndBoundary(int offset) {
+ return !isOnPunctuation(offset) && isAfterPunctuation(offset);
+ }
+
+ private static boolean isPunctuation(int cp) {
+ final int type = Character.getType(cp);
+ return (type == Character.CONNECTOR_PUNCTUATION
+ || type == Character.DASH_PUNCTUATION
+ || type == Character.END_PUNCTUATION
+ || type == Character.FINAL_QUOTE_PUNCTUATION
+ || type == Character.INITIAL_QUOTE_PUNCTUATION
+ || type == Character.OTHER_PUNCTUATION
+ || type == Character.START_PUNCTUATION);
+ }
+
+ private boolean isAfterLetterOrDigit(int offset) {
+ if (mStart < offset && offset <= mEnd) {
+ final int codePoint = Character.codePointBefore(mCharSeq, offset);
+ if (Character.isLetterOrDigit(codePoint)) return true;
+ }
+ return false;
+ }
+
+ private boolean isOnLetterOrDigit(int offset) {
+ if (mStart <= offset && offset < mEnd) {
+ final int codePoint = Character.codePointAt(mCharSeq, offset);
+ if (Character.isLetterOrDigit(codePoint)) return true;
+ }
+ return false;
+ }
+
+ private void checkOffsetIsValid(int offset) {
+ if (!(mStart <= offset && offset <= mEnd)) {
+ throw new IllegalArgumentException("Invalid offset: " + (offset) +
+ ". Valid range is [" + mStart + ", " + mEnd + "]");
+ }
+ }
+}