summaryrefslogtreecommitdiffstats
path: root/core/java/android/text
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/text')
-rw-r--r--core/java/android/text/AlteredCharSequence.java127
-rw-r--r--core/java/android/text/AndroidCharacter.java45
-rw-r--r--core/java/android/text/Annotation.java40
-rw-r--r--core/java/android/text/AutoText.java246
-rw-r--r--core/java/android/text/BoringLayout.java388
-rw-r--r--core/java/android/text/ClipboardManager.java88
-rw-r--r--core/java/android/text/DynamicLayout.java503
-rw-r--r--core/java/android/text/Editable.java142
-rw-r--r--core/java/android/text/GetChars.java33
-rw-r--r--core/java/android/text/GraphicsOperations.java46
-rw-r--r--core/java/android/text/Html.java750
-rw-r--r--core/java/android/text/IClipboard.aidl42
-rw-r--r--core/java/android/text/InputFilter.java92
-rw-r--r--core/java/android/text/Layout.java1745
-rw-r--r--core/java/android/text/LoginFilter.java206
-rw-r--r--core/java/android/text/PackedIntVector.java368
-rw-r--r--core/java/android/text/PackedObjectVector.java188
-rw-r--r--core/java/android/text/Selection.java426
-rw-r--r--core/java/android/text/SpanWatcher.java42
-rw-r--r--core/java/android/text/Spannable.java70
-rw-r--r--core/java/android/text/SpannableString.java56
-rw-r--r--core/java/android/text/SpannableStringBuilder.java1136
-rw-r--r--core/java/android/text/SpannableStringInternal.java372
-rw-r--r--core/java/android/text/Spanned.java160
-rw-r--r--core/java/android/text/SpannedString.java48
-rw-r--r--core/java/android/text/StaticLayout.java1118
-rw-r--r--core/java/android/text/Styled.java298
-rw-r--r--core/java/android/text/TextPaint.java55
-rw-r--r--core/java/android/text/TextUtils.java1570
-rw-r--r--core/java/android/text/TextWatcher.java57
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java266
-rw-r--r--core/java/android/text/method/BaseKeyListener.java112
-rw-r--r--core/java/android/text/method/CharacterPickerDialog.java134
-rw-r--r--core/java/android/text/method/DateKeyListener.java52
-rw-r--r--core/java/android/text/method/DateTimeKeyListener.java52
-rw-r--r--core/java/android/text/method/DialerKeyListener.java109
-rw-r--r--core/java/android/text/method/DigitsKeyListener.java206
-rw-r--r--core/java/android/text/method/HideReturnsTransformationMethod.java59
-rw-r--r--core/java/android/text/method/KeyListener.java42
-rw-r--r--core/java/android/text/method/LinkMovementMethod.java256
-rw-r--r--core/java/android/text/method/MetaKeyKeyListener.java250
-rw-r--r--core/java/android/text/method/MovementMethod.java43
-rw-r--r--core/java/android/text/method/MultiTapKeyListener.java285
-rw-r--r--core/java/android/text/method/NumberKeyListener.java132
-rw-r--r--core/java/android/text/method/PasswordTransformationMethod.java260
-rw-r--r--core/java/android/text/method/QwertyKeyListener.java444
-rw-r--r--core/java/android/text/method/ReplacementTransformationMethod.java205
-rw-r--r--core/java/android/text/method/ScrollingMovementMethod.java234
-rw-r--r--core/java/android/text/method/SingleLineTransformationMethod.java60
-rw-r--r--core/java/android/text/method/TextKeyListener.java339
-rw-r--r--core/java/android/text/method/TimeKeyListener.java52
-rw-r--r--core/java/android/text/method/Touch.java138
-rw-r--r--core/java/android/text/method/TransformationMethod.java46
-rw-r--r--core/java/android/text/method/package.html21
-rw-r--r--core/java/android/text/package.html13
-rw-r--r--core/java/android/text/style/AbsoluteSizeSpan.java43
-rw-r--r--core/java/android/text/style/AlignmentSpan.java39
-rw-r--r--core/java/android/text/style/BackgroundColorSpan.java37
-rw-r--r--core/java/android/text/style/BulletSpan.java76
-rw-r--r--core/java/android/text/style/CharacterStyle.java87
-rw-r--r--core/java/android/text/style/ClickableSpan.java43
-rw-r--r--core/java/android/text/style/DrawableMarginSpan.java79
-rw-r--r--core/java/android/text/style/DynamicDrawableSpan.java82
-rw-r--r--core/java/android/text/style/ForegroundColorSpan.java38
-rw-r--r--core/java/android/text/style/IconMarginSpan.java73
-rw-r--r--core/java/android/text/style/ImageSpan.java99
-rw-r--r--core/java/android/text/style/LeadingMarginSpan.java59
-rw-r--r--core/java/android/text/style/LineBackgroundSpan.java30
-rw-r--r--core/java/android/text/style/LineHeightSpan.java29
-rw-r--r--core/java/android/text/style/MaskFilterSpan.java39
-rw-r--r--core/java/android/text/style/MetricAffectingSpan.java85
-rw-r--r--core/java/android/text/style/ParagraphStyle.java26
-rw-r--r--core/java/android/text/style/QuoteSpan.java64
-rw-r--r--core/java/android/text/style/RasterizerSpan.java39
-rw-r--r--core/java/android/text/style/RelativeSizeSpan.java43
-rw-r--r--core/java/android/text/style/ReplacementSpan.java43
-rw-r--r--core/java/android/text/style/ScaleXSpan.java43
-rw-r--r--core/java/android/text/style/StrikethroughSpan.java27
-rw-r--r--core/java/android/text/style/StyleSpan.java93
-rw-r--r--core/java/android/text/style/SubscriptSpan.java31
-rw-r--r--core/java/android/text/style/SuperscriptSpan.java31
-rw-r--r--core/java/android/text/style/TabStopSpan.java37
-rw-r--r--core/java/android/text/style/TextAppearanceSpan.java198
-rw-r--r--core/java/android/text/style/TypefaceSpan.java77
-rw-r--r--core/java/android/text/style/URLSpan.java43
-rw-r--r--core/java/android/text/style/UnderlineSpan.java27
-rw-r--r--core/java/android/text/style/UpdateLayout.java24
-rw-r--r--core/java/android/text/style/WrapTogetherSpan.java23
-rw-r--r--core/java/android/text/style/package.html10
-rw-r--r--core/java/android/text/util/Linkify.java533
-rw-r--r--core/java/android/text/util/Regex.java192
-rw-r--r--core/java/android/text/util/Rfc822Token.java172
-rw-r--r--core/java/android/text/util/Rfc822Tokenizer.java292
-rw-r--r--core/java/android/text/util/Rfc822Validator.java128
-rw-r--r--core/java/android/text/util/package.html6
95 files changed, 17407 insertions, 0 deletions
diff --git a/core/java/android/text/AlteredCharSequence.java b/core/java/android/text/AlteredCharSequence.java
new file mode 100644
index 0000000..4cc71fd
--- /dev/null
+++ b/core/java/android/text/AlteredCharSequence.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+// XXX should this really be in the public API at all?
+/**
+ * An AlteredCharSequence is a CharSequence that is largely mirrored from
+ * another CharSequence, except that a specified range of characters are
+ * mirrored from a different char array instead.
+ */
+public class AlteredCharSequence
+implements CharSequence, GetChars
+{
+ /**
+ * Create an AlteredCharSequence whose text (and possibly spans)
+ * are mirrored from <code>source</code>, except that the range of
+ * offsets <code>substart</code> inclusive to <code>subend</code> exclusive
+ * are mirrored instead from <code>sub</code>, beginning at offset 0.
+ */
+ public static AlteredCharSequence make(CharSequence source, char[] sub,
+ int substart, int subend) {
+ if (source instanceof Spanned)
+ return new AlteredSpanned(source, sub, substart, subend);
+ else
+ return new AlteredCharSequence(source, sub, substart, subend);
+ }
+
+ private AlteredCharSequence(CharSequence source, char[] sub,
+ int substart, int subend) {
+ mSource = source;
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ /* package */ void update(char[] sub, int substart, int subend) {
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ private static class AlteredSpanned
+ extends AlteredCharSequence
+ implements Spanned
+ {
+ private AlteredSpanned(CharSequence source, char[] sub,
+ int substart, int subend) {
+ super(source, sub, substart, subend);
+ mSpanned = (Spanned) source;
+ }
+
+ public <T> T[] getSpans(int start, int end, Class<T> kind) {
+ return mSpanned.getSpans(start, end, kind);
+ }
+
+ public int getSpanStart(Object span) {
+ return mSpanned.getSpanStart(span);
+ }
+
+ public int getSpanEnd(Object span) {
+ return mSpanned.getSpanEnd(span);
+ }
+
+ public int getSpanFlags(Object span) {
+ return mSpanned.getSpanFlags(span);
+ }
+
+ public int nextSpanTransition(int start, int end, Class kind) {
+ return mSpanned.nextSpanTransition(start, end, kind);
+ }
+
+ private Spanned mSpanned;
+ }
+
+ public char charAt(int off) {
+ if (off >= mStart && off < mEnd)
+ return mChars[off - mStart];
+ else
+ return mSource.charAt(off);
+ }
+
+ public int length() {
+ return mSource.length();
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return AlteredCharSequence.make(mSource.subSequence(start, end),
+ mChars, mStart - start, mEnd - start);
+ }
+
+ public void getChars(int start, int end, char[] dest, int off) {
+ TextUtils.getChars(mSource, start, end, dest, off);
+
+ start = Math.max(mStart, start);
+ end = Math.min(mEnd, end);
+
+ if (start > end)
+ System.arraycopy(mChars, start - mStart, dest, off, end - start);
+ }
+
+ public String toString() {
+ int len = length();
+
+ char[] ret = new char[len];
+ getChars(0, len, ret, 0);
+ return String.valueOf(ret);
+ }
+
+ private int mStart;
+ private int mEnd;
+ private char[] mChars;
+ private CharSequence mSource;
+}
diff --git a/core/java/android/text/AndroidCharacter.java b/core/java/android/text/AndroidCharacter.java
new file mode 100644
index 0000000..6dfd64d
--- /dev/null
+++ b/core/java/android/text/AndroidCharacter.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;
+
+/**
+ * AndroidCharacter exposes some character properties that are not
+ * easily accessed from java.lang.Character.
+ */
+public class AndroidCharacter
+{
+ /**
+ * Fill in the first <code>count</code> bytes of <code>dest</code> with the
+ * directionalities from the first <code>count</code> chars of <code>src</code>.
+ * This is just like Character.getDirectionality() except it is a
+ * batch operation.
+ */
+ public native static void getDirectionalities(char[] src, byte[] dest,
+ int count);
+ /**
+ * Replace the specified slice of <code>text</code> with the chars'
+ * right-to-left mirrors (if any), returning true if any
+ * replacements were made.
+ */
+ public native static boolean mirror(char[] text, int start, int count);
+
+ /**
+ * Return the right-to-left mirror (or the original char if none)
+ * of the specified char.
+ */
+ public native static char getMirror(char ch);
+}
diff --git a/core/java/android/text/Annotation.java b/core/java/android/text/Annotation.java
new file mode 100644
index 0000000..a3812a8
--- /dev/null
+++ b/core/java/android/text/Annotation.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Annotations are simple key-value pairs that are preserved across
+ * TextView save/restore cycles and can be used to keep application-specific
+ * data that needs to be maintained for regions of text.
+ */
+public class Annotation {
+ private String mKey;
+ private String mValue;
+
+ public Annotation(String key, String value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+}
diff --git a/core/java/android/text/AutoText.java b/core/java/android/text/AutoText.java
new file mode 100644
index 0000000..508d740
--- /dev/null
+++ b/core/java/android/text/AutoText.java
@@ -0,0 +1,246 @@
+/*
+ * 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;
+
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import com.android.internal.util.XmlUtils;
+import android.view.View;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class accesses a dictionary of corrections to frequent misspellings.
+ */
+public class AutoText {
+ // struct trie {
+ // char c;
+ // int off;
+ // struct trie *child;
+ // struct trie *next;
+ // };
+
+ private static final int TRIE_C = 0;
+ private static final int TRIE_OFF = 1;
+ private static final int TRIE_CHILD = 2;
+ private static final int TRIE_NEXT = 3;
+
+ private static final int TRIE_SIZEOF = 4;
+ private static final char TRIE_NULL = (char) -1;
+ private static final int TRIE_ROOT = 0;
+
+ private static final int INCREMENT = 1024;
+
+ private static final int DEFAULT = 14337; // Size of the Trie 13 Aug 2007
+
+ private static final int RIGHT = 9300; // Size of 'right' 13 Aug 2007
+
+ private static AutoText sInstance = new AutoText(Resources.getSystem());
+ private static Object sLock = new Object();
+
+ // TODO:
+ //
+ // Note the assumption that the destination strings total less than
+ // 64K characters and that the trie for the source side totals less
+ // than 64K chars/offsets/child pointers/next pointers.
+ //
+ // This seems very safe for English (currently 7K of destination,
+ // 14K of trie) but may need to be revisited.
+
+ private char[] mTrie;
+ private char mTrieUsed;
+ private String mText;
+ private Locale mLocale;
+
+ private AutoText(Resources resources) {
+ mLocale = resources.getConfiguration().locale;
+ init(resources);
+ }
+
+ /**
+ * Retrieves a possible spelling correction for the specified range
+ * of text. Returns null if no correction can be found.
+ * The View is used to get the current Locale and Resources.
+ */
+ public static String get(CharSequence src, final int start, final int end,
+ View view) {
+ Resources res = view.getContext().getResources();
+ Locale locale = res.getConfiguration().locale;
+ AutoText instance;
+
+ synchronized (sLock) {
+ instance = sInstance;
+
+ if (!locale.equals(instance.mLocale)) {
+ instance = new AutoText(res);
+ sInstance = instance;
+ }
+ }
+
+ return instance.lookup(src, start, end);
+ }
+
+ private String lookup(CharSequence src, final int start, final int end) {
+ int here = mTrie[TRIE_ROOT];
+
+ for (int i = start; i < end; i++) {
+ char c = src.charAt(i);
+
+ for (; here != TRIE_NULL; here = mTrie[here + TRIE_NEXT]) {
+ if (c == mTrie[here + TRIE_C]) {
+ if ((i == end - 1)
+ && (mTrie[here + TRIE_OFF] != TRIE_NULL)) {
+ int off = mTrie[here + TRIE_OFF];
+ int len = mText.charAt(off);
+
+ return mText.substring(off + 1, off + 1 + len);
+ }
+
+ here = mTrie[here + TRIE_CHILD];
+ break;
+ }
+ }
+
+ if (here == TRIE_NULL) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ private void init(Resources r) {
+ XmlResourceParser parser = r.getXml(com.android.internal.R.xml.autotext);
+
+ StringBuilder right = new StringBuilder(RIGHT);
+ mTrie = new char[DEFAULT];
+ mTrie[TRIE_ROOT] = TRIE_NULL;
+ mTrieUsed = TRIE_ROOT + 1;
+
+ try {
+ XmlUtils.beginDocument(parser, "words");
+ String odest = "";
+ char ooff = 0;
+
+ while (true) {
+ XmlUtils.nextElement(parser);
+
+ String element = parser.getName();
+ if (element == null || !(element.equals("word"))) {
+ break;
+ }
+
+ String src = parser.getAttributeValue(null, "src");
+ if (parser.next() == XmlPullParser.TEXT) {
+ String dest = parser.getText();
+ char off;
+
+ if (dest.equals(odest)) {
+ off = ooff;
+ } else {
+ off = (char) right.length();
+ right.append((char) dest.length());
+ right.append(dest);
+ }
+
+ add(src, off);
+ }
+ }
+
+ // Don't let Resources cache a copy of all these strings.
+ r.flushLayoutCache();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+
+ mText = right.toString();
+ }
+
+ private void add(String src, char off) {
+ int slen = src.length();
+ int herep = TRIE_ROOT;
+
+ for (int i = 0; i < slen; i++) {
+ char c = src.charAt(i);
+ boolean found = false;
+
+ for (; mTrie[herep] != TRIE_NULL;
+ herep = mTrie[herep] + TRIE_NEXT) {
+ if (c == mTrie[mTrie[herep] + TRIE_C]) {
+ // There is a node for this letter, and this is the
+ // end, so fill in the right hand side fields.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // There is a node for this letter, and we need
+ // to go deeper into it to fill in the rest.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // No node for this letter yet. Make one.
+
+ char node = newTrieNode();
+ mTrie[herep] = node;
+
+ mTrie[mTrie[herep] + TRIE_C] = c;
+ mTrie[mTrie[herep] + TRIE_OFF] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_NEXT] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_CHILD] = TRIE_NULL;
+
+ // If this is the end of the word, fill in the offset.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // Otherwise, step in deeper and go to the next letter.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ }
+ }
+ }
+
+ private char newTrieNode() {
+ if (mTrieUsed + TRIE_SIZEOF > mTrie.length) {
+ char[] copy = new char[mTrie.length + INCREMENT];
+ System.arraycopy(mTrie, 0, copy, 0, mTrie.length);
+ mTrie = copy;
+ }
+
+ char ret = mTrieUsed;
+ mTrieUsed += TRIE_SIZEOF;
+
+ return ret;
+ }
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
new file mode 100644
index 0000000..2ee4f62
--- /dev/null
+++ b/core/java/android/text/BoringLayout.java
@@ -0,0 +1,388 @@
+/*
+ * 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;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.FloatMath;
+
+/**
+ * A BoringLayout is a very simple Layout implementation for text that
+ * fits on a single line and is all left-to-right characters.
+ * You will probably never want to make one of these yourself;
+ * if you do, be sure to call {@link #isBoring} first to make sure
+ * the text meets the criteria.
+ * <p>This class is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, in which case
+ * you are encouraged to use a Layout instead of calling
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
+ public static BoringLayout make(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad) {
+ return new BoringLayout(source, paint, outerwidth, align,
+ spacingmult, spacingadd, metrics,
+ includepad);
+ }
+
+ public static BoringLayout make(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ return new BoringLayout(source, paint, outerwidth, align,
+ spacingmult, spacingadd, metrics,
+ includepad, ellipsize, ellipsizedWidth);
+ }
+
+ /**
+ * Returns a BoringLayout for the specified text, potentially reusing
+ * this one if it is already suitable. The caller must make sure that
+ * no one is still using this Layout.
+ */
+ public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
+ int outerwidth, Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics,
+ boolean includepad) {
+ replaceWith(source, paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+
+ init(source, paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, true);
+ return this;
+ }
+
+ /**
+ * Returns a BoringLayout for the specified text, potentially reusing
+ * this one if it is already suitable. The caller must make sure that
+ * no one is still using this Layout.
+ */
+ public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
+ int outerwidth, Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize,
+ int ellipsizedWidth) {
+ boolean trust;
+
+ if (ellipsize == null) {
+ replaceWith(source, paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+ trust = true;
+ } else {
+ replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
+ ellipsize, true, this),
+ paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+ mEllipsizedWidth = ellipsizedWidth;
+ trust = false;
+ }
+
+ init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, trust);
+ return this;
+ }
+
+ public BoringLayout(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad) {
+ super(source, paint, outerwidth, align, spacingmult, spacingadd);
+
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+
+ init(source, paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, true);
+ }
+
+ public BoringLayout(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ /*
+ * It is silly to have to call super() and then replaceWith(),
+ * but we can't use "this" for the callback until the call to
+ * super() finishes.
+ */
+ super(source, paint, outerwidth, align, spacingmult, spacingadd);
+
+ boolean trust;
+
+ if (ellipsize == null) {
+ mEllipsizedWidth = outerwidth;
+ mEllipsizedStart = 0;
+ mEllipsizedCount = 0;
+ trust = true;
+ } else {
+ replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
+ ellipsize, true, this),
+ paint, outerwidth, align, spacingmult,
+ spacingadd);
+
+
+ mEllipsizedWidth = ellipsizedWidth;
+ trust = false;
+ }
+
+ init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
+ metrics, includepad, trust);
+ }
+
+ /* package */ void init(CharSequence source,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ BoringLayout.Metrics metrics, boolean includepad,
+ boolean trustWidth) {
+ int spacing;
+
+ if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
+ mDirect = source.toString();
+ } else {
+ mDirect = null;
+ }
+
+ mPaint = paint;
+
+ if (includepad) {
+ spacing = metrics.bottom - metrics.top;
+ } else {
+ spacing = metrics.descent - metrics.ascent;
+ }
+
+ if (spacingmult != 1 || spacingadd != 0) {
+ spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
+ }
+
+ mBottom = spacing;
+
+ if (includepad) {
+ mDesc = spacing + metrics.top;
+ } else {
+ mDesc = spacing + metrics.ascent;
+ }
+
+ if (trustWidth) {
+ mMax = metrics.width;
+ } else {
+ /*
+ * If we have ellipsized, we have to actually calculate the
+ * width because the width that was passed in was for the
+ * full text, not the ellipsized form.
+ */
+ synchronized (sTemp) {
+ mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
+ source, 0, source.length(),
+ null)));
+ }
+ }
+
+ if (includepad) {
+ mTopPadding = metrics.top - metrics.ascent;
+ mBottomPadding = metrics.bottom - metrics.descent;
+ }
+ }
+
+ /**
+ * Returns null if not boring; the width, ascent, and descent if boring.
+ */
+ public static Metrics isBoring(CharSequence text,
+ TextPaint paint) {
+ return isBoring(text, paint, null);
+ }
+
+ /**
+ * Returns null if not boring; the width, ascent, and descent in the
+ * provided Metrics object (or a new one if the provided one was null)
+ * if boring.
+ */
+ public static Metrics isBoring(CharSequence text, TextPaint paint,
+ Metrics metrics) {
+ char[] temp = TextUtils.obtain(500);
+ int len = text.length();
+ boolean boring = true;
+
+ outer:
+ for (int i = 0; i < len; i += 500) {
+ int j = i + 500;
+
+ if (j > len)
+ j = len;
+
+ TextUtils.getChars(text, i, j, temp, 0);
+
+ int n = j - i;
+
+ for (int a = 0; a < n; a++) {
+ char c = temp[a];
+
+ if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
+ boring = false;
+ break outer;
+ }
+ }
+ }
+
+ TextUtils.recycle(temp);
+
+ if (boring) {
+ Metrics fm = metrics;
+ if (fm == null) {
+ fm = new Metrics();
+ }
+
+ int wid;
+
+ synchronized (sTemp) {
+ wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
+ text, 0, text.length(), fm)));
+ }
+ fm.width = wid;
+ return fm;
+ } else {
+ return null;
+ }
+ }
+
+ @Override public int getHeight() {
+ return mBottom;
+ }
+
+ @Override public int getLineCount() {
+ return 1;
+ }
+
+ @Override public int getLineTop(int line) {
+ if (line == 0)
+ return 0;
+ else
+ return mBottom;
+ }
+
+ @Override public int getLineDescent(int line) {
+ return mDesc;
+ }
+
+ @Override public int getLineStart(int line) {
+ if (line == 0)
+ return 0;
+ else
+ return getText().length();
+ }
+
+ @Override public int getParagraphDirection(int line) {
+ return DIR_LEFT_TO_RIGHT;
+ }
+
+ @Override public boolean getLineContainsTab(int line) {
+ return false;
+ }
+
+ @Override public float getLineMax(int line) {
+ return mMax;
+ }
+
+ @Override public final Directions getLineDirections(int line) {
+ return Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsisCount(int line) {
+ return mEllipsizedCount;
+ }
+
+ @Override
+ public int getEllipsisStart(int line) {
+ return mEllipsizedStart;
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ // Override draw so it will be faster.
+ @Override
+ public void draw(Canvas c, Path highlight, Paint highlightpaint,
+ int cursorOffset) {
+ if (mDirect != null && highlight == null) {
+ c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
+ } else {
+ super.draw(c, highlight, highlightpaint, cursorOffset);
+ }
+ }
+
+ /**
+ * Callback for the ellipsizer to report what region it ellipsized.
+ */
+ public void ellipsized(int start, int end) {
+ mEllipsizedStart = start;
+ mEllipsizedCount = end - start;
+ }
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ private String mDirect;
+ private Paint mPaint;
+
+ /* package */ int mBottom, mDesc; // for Direct
+ private int mTopPadding, mBottomPadding;
+ private float mMax;
+ private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
+
+ private static final TextPaint sTemp =
+ new TextPaint();
+
+ public static class Metrics extends Paint.FontMetricsInt {
+ public int width;
+
+ @Override public String toString() {
+ return super.toString() + " width=" + width;
+ }
+ }
+}
diff --git a/core/java/android/text/ClipboardManager.java b/core/java/android/text/ClipboardManager.java
new file mode 100644
index 0000000..52039af
--- /dev/null
+++ b/core/java/android/text/ClipboardManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.util.Log;
+
+/**
+ * Interface to the clipboard service, for placing and retrieving text in
+ * the global clipboard.
+ *
+ * <p>
+ * You do not instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService}.
+ *
+ * @see android.content.Context#getSystemService
+ */
+public class ClipboardManager {
+ private static IClipboard sService;
+
+ private Context mContext;
+
+ static private IClipboard getService() {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService("clipboard");
+ sService = IClipboard.Stub.asInterface(b);
+ return sService;
+ }
+
+ /** {@hide} */
+ public ClipboardManager(Context context, Handler handler) {
+ mContext = context;
+ }
+
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ public CharSequence getText() {
+ try {
+ return getService().getClipboardText();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ public void setText(CharSequence text) {
+ try {
+ getService().setClipboardText(text);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ public boolean hasText() {
+ try {
+ return getService().hasClipboardText();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
new file mode 100644
index 0000000..14e5655
--- /dev/null
+++ b/core/java/android/text/DynamicLayout.java
@@ -0,0 +1,503 @@
+/*
+ * 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;
+
+import android.graphics.Paint;
+import android.text.style.UpdateLayout;
+import android.text.style.WrapTogetherSpan;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * DynamicLayout is a text layout that updates itself as the text is edited.
+ * <p>This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or need to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class DynamicLayout
+extends Layout
+{
+ private static final int PRIORITY = 128;
+
+ /**
+ * Make a layout for the specified text that will be updated as
+ * the text is changed.
+ */
+ public DynamicLayout(CharSequence base,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(base, base, paint, width, align, spacingmult, spacingadd,
+ includepad);
+ }
+
+ /**
+ * Make a layout for the transformed text (password transformation
+ * being the primary example of a transformation)
+ * that will be updated as the base text is changed.
+ */
+ public DynamicLayout(CharSequence base, CharSequence display,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(base, display, paint, width, align, spacingmult, spacingadd,
+ includepad, null, 0);
+ }
+
+ /**
+ * Make a layout for the transformed text (password transformation
+ * being the primary example of a transformation)
+ * that will be updated as the base text is changed.
+ * If ellipsize is non-null, the Layout will ellipsize the text
+ * down to ellipsizedWidth.
+ */
+ public DynamicLayout(CharSequence base, CharSequence display,
+ TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ super((ellipsize == null)
+ ? display
+ : (display instanceof Spanned)
+ ? new SpannedEllipsizer(display)
+ : new Ellipsizer(display),
+ paint, width, align, spacingmult, spacingadd);
+
+ mBase = base;
+ mDisplay = display;
+
+ if (ellipsize != null) {
+ mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
+ mEllipsizedWidth = ellipsizedWidth;
+ mEllipsizeAt = ellipsize;
+ } else {
+ mInts = new PackedIntVector(COLUMNS_NORMAL);
+ mEllipsizedWidth = width;
+ mEllipsizeAt = ellipsize;
+ }
+
+ mObjects = new PackedObjectVector<Directions>(1);
+
+ mIncludePad = includepad;
+
+ /*
+ * This is annoying, but we can't refer to the layout until
+ * superclass construction is finished, and the superclass
+ * constructor wants the reference to the display text.
+ *
+ * This will break if the superclass constructor ever actually
+ * cares about the content instead of just holding the reference.
+ */
+ if (ellipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = ellipsizedWidth;
+ e.mMethod = ellipsize;
+ mEllipsize = true;
+ }
+
+ // Initial state is a single line with 0 characters (0 to 0),
+ // with top at 0 and bottom at whatever is natural, and
+ // undefined ellipsis.
+
+ int[] start;
+
+ if (ellipsize != null) {
+ start = new int[COLUMNS_ELLIPSIZE];
+ start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
+ } else {
+ start = new int[COLUMNS_NORMAL];
+ }
+
+ Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
+
+ Paint.FontMetricsInt fm = paint.getFontMetricsInt();
+ int asc = fm.ascent;
+ int desc = fm.descent;
+
+ start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
+ start[TOP] = 0;
+ start[DESCENT] = desc;
+ mInts.insertAt(0, start);
+
+ start[TOP] = desc - asc;
+ mInts.insertAt(1, start);
+
+ mObjects.insertAt(0, dirs);
+
+ // Update from 0 characters to whatever the real text is
+
+ reflow(base, 0, 0, base.length());
+
+ if (base instanceof Spannable) {
+ if (mWatcher == null)
+ mWatcher = new ChangeWatcher(this);
+
+ // Strip out any watchers for other DynamicLayouts.
+ Spannable sp = (Spannable) base;
+ ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
+ for (int i = 0; i < spans.length; i++)
+ sp.removeSpan(spans[i]);
+
+ sp.setSpan(mWatcher, 0, base.length(),
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE |
+ (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
+ }
+ }
+
+ private void reflow(CharSequence s, int where, int before, int after) {
+ if (s != mBase)
+ return;
+
+ CharSequence text = mDisplay;
+ int len = text.length();
+
+ // seek back to the start of the paragraph
+
+ int find = TextUtils.lastIndexOf(text, '\n', where - 1);
+ if (find < 0)
+ find = 0;
+ else
+ find = find + 1;
+
+ {
+ int diff = where - find;
+ before += diff;
+ after += diff;
+ where -= diff;
+ }
+
+ // seek forward to the end of the paragraph
+
+ int look = TextUtils.indexOf(text, '\n', where + after);
+ if (look < 0)
+ look = len;
+ else
+ look++; // we want the index after the \n
+
+ int change = look - (where + after);
+ before += change;
+ after += change;
+
+ // seek further out to cover anything that is forced to wrap together
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ boolean again;
+
+ do {
+ again = false;
+
+ Object[] force = sp.getSpans(where, where + after,
+ WrapTogetherSpan.class);
+
+ for (int i = 0; i < force.length; i++) {
+ int st = sp.getSpanStart(force[i]);
+ int en = sp.getSpanEnd(force[i]);
+
+ if (st < where) {
+ again = true;
+
+ int diff = where - st;
+ before += diff;
+ after += diff;
+ where -= diff;
+ }
+
+ if (en > where + after) {
+ again = true;
+
+ int diff = en - (where + after);
+ before += diff;
+ after += diff;
+ }
+ }
+ } while (again);
+ }
+
+ // find affected region of old layout
+
+ int startline = getLineForOffset(where);
+ int startv = getLineTop(startline);
+
+ int endline = getLineForOffset(where + before);
+ if (where + after == len)
+ endline = getLineCount();
+ int endv = getLineTop(endline);
+ boolean islast = (endline == getLineCount());
+
+ // generate new layout for affected text
+
+ StaticLayout reflowed;
+
+ synchronized (sLock) {
+ reflowed = sStaticLayout;
+ sStaticLayout = null;
+ }
+
+ if (reflowed == null)
+ reflowed = new StaticLayout(true);
+
+ reflowed.generate(text, where, where + after,
+ getPaint(), getWidth(), getAlignment(),
+ getSpacingMultiplier(), getSpacingAdd(),
+ false, true, mEllipsize,
+ mEllipsizedWidth, mEllipsizeAt);
+ int n = reflowed.getLineCount();
+
+ // If the new layout has a blank line at the end, but it is not
+ // the very end of the buffer, then we already have a line that
+ // starts there, so disregard the blank line.
+
+ if (where + after != len &&
+ reflowed.getLineStart(n - 1) == where + after)
+ n--;
+
+ // remove affected lines from old layout
+
+ mInts.deleteAt(startline, endline - startline);
+ mObjects.deleteAt(startline, endline - startline);
+
+ // adjust offsets in layout for new height and offsets
+
+ int ht = reflowed.getLineTop(n);
+ int toppad = 0, botpad = 0;
+
+ if (mIncludePad && startline == 0) {
+ toppad = reflowed.getTopPadding();
+ mTopPadding = toppad;
+ ht -= toppad;
+ }
+ if (mIncludePad && islast) {
+ botpad = reflowed.getBottomPadding();
+ mBottomPadding = botpad;
+ ht += botpad;
+ }
+
+ mInts.adjustValuesBelow(startline, START, after - before);
+ mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
+
+ // insert new layout
+
+ int[] ints;
+
+ if (mEllipsize) {
+ ints = new int[COLUMNS_ELLIPSIZE];
+ ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
+ } else {
+ ints = new int[COLUMNS_NORMAL];
+ }
+
+ Directions[] objects = new Directions[1];
+
+
+ for (int i = 0; i < n; i++) {
+ ints[START] = reflowed.getLineStart(i) |
+ (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
+ (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
+
+ int top = reflowed.getLineTop(i) + startv;
+ if (i > 0)
+ top -= toppad;
+ ints[TOP] = top;
+
+ int desc = reflowed.getLineDescent(i);
+ if (i == n - 1)
+ desc += botpad;
+
+ ints[DESCENT] = desc;
+ objects[0] = reflowed.getLineDirections(i);
+
+ if (mEllipsize) {
+ ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
+ ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
+ }
+
+ mInts.insertAt(startline + i, ints);
+ mObjects.insertAt(startline + i, objects);
+ }
+
+ synchronized (sLock) {
+ sStaticLayout = reflowed;
+ }
+ }
+
+ private void dump(boolean show) {
+ int n = getLineCount();
+
+ for (int i = 0; i < n; i++) {
+ System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " ");
+
+ if (show) {
+ System.out.print(getText().subSequence(getLineStart(i),
+ getLineEnd(i)));
+ }
+
+ System.out.println("");
+ }
+
+ System.out.println("");
+ }
+
+ public int getLineCount() {
+ return mInts.size() - 1;
+ }
+
+ public int getLineTop(int line) {
+ return mInts.getValue(line, TOP);
+ }
+
+ public int getLineDescent(int line) {
+ return mInts.getValue(line, DESCENT);
+ }
+
+ public int getLineStart(int line) {
+ return mInts.getValue(line, START) & START_MASK;
+ }
+
+ public boolean getLineContainsTab(int line) {
+ return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
+ }
+
+ public int getParagraphDirection(int line) {
+ return mInts.getValue(line, DIR) >> DIR_SHIFT;
+ }
+
+ public final Directions getLineDirections(int line) {
+ return mObjects.getValue(line, 0);
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ private static class ChangeWatcher
+ implements TextWatcher, SpanWatcher
+ {
+ public ChangeWatcher(DynamicLayout layout) {
+ mLayout = new WeakReference(layout);
+ }
+
+ private void reflow(CharSequence s, int where, int before, int after) {
+ DynamicLayout ml = (DynamicLayout) mLayout.get();
+
+ if (ml != null)
+ ml.reflow(s, where, before, after);
+ else if (s instanceof Spannable)
+ ((Spannable) s).removeSpan(this);
+ }
+
+ public void beforeTextChanged(CharSequence s,
+ int where, int before, int after) {
+ ;
+ }
+
+ public void onTextChanged(CharSequence s,
+ int where, int before, int after) {
+ reflow(s, where, before, after);
+ }
+
+ public void afterTextChanged(Editable s) {
+ ;
+ }
+
+ public void onSpanAdded(Spannable s, Object o, int start, int end) {
+ if (o instanceof UpdateLayout)
+ reflow(s, start, end - start, end - start);
+ }
+
+ public void onSpanRemoved(Spannable s, Object o, int start, int end) {
+ if (o instanceof UpdateLayout)
+ reflow(s, start, end - start, end - start);
+ }
+
+ public void onSpanChanged(Spannable s, Object o, int start, int end,
+ int nstart, int nend) {
+ if (o instanceof UpdateLayout) {
+ reflow(s, start, end - start, end - start);
+ reflow(s, nstart, nend - nstart, nend - nstart);
+ }
+ }
+
+ private WeakReference mLayout;
+ }
+
+ public int getEllipsisStart(int line) {
+ if (mEllipsizeAt == null) {
+ return 0;
+ }
+
+ return mInts.getValue(line, ELLIPSIS_START);
+ }
+
+ public int getEllipsisCount(int line) {
+ if (mEllipsizeAt == null) {
+ return 0;
+ }
+
+ return mInts.getValue(line, ELLIPSIS_COUNT);
+ }
+
+ private CharSequence mBase;
+ private CharSequence mDisplay;
+ private ChangeWatcher mWatcher;
+ private boolean mIncludePad;
+ private boolean mEllipsize;
+ private int mEllipsizedWidth;
+ private TextUtils.TruncateAt mEllipsizeAt;
+
+ private PackedIntVector mInts;
+ private PackedObjectVector<Directions> mObjects;
+
+ private int mTopPadding, mBottomPadding;
+
+ private static StaticLayout sStaticLayout = new StaticLayout(true);
+ private static Object sLock = new Object();
+
+ private static final int START = 0;
+ private static final int DIR = START;
+ private static final int TAB = START;
+ private static final int TOP = 1;
+ private static final int DESCENT = 2;
+ private static final int COLUMNS_NORMAL = 3;
+
+ private static final int ELLIPSIS_START = 3;
+ private static final int ELLIPSIS_COUNT = 4;
+ private static final int COLUMNS_ELLIPSIZE = 5;
+
+ private static final int START_MASK = 0x1FFFFFFF;
+ private static final int DIR_MASK = 0xC0000000;
+ private static final int DIR_SHIFT = 30;
+ private static final int TAB_MASK = 0x20000000;
+
+ private static final int ELLIPSIS_UNDEFINED = 0x80000000;
+}
diff --git a/core/java/android/text/Editable.java b/core/java/android/text/Editable.java
new file mode 100644
index 0000000..a284a00
--- /dev/null
+++ b/core/java/android/text/Editable.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+/**
+ * This is the interface for text whose content and markup
+ * can be changed (as opposed
+ * to immutable text like Strings). If you make a {@link DynamicLayout}
+ * of an Editable, the layout will be reflowed as the text is changed.
+ */
+public interface Editable
+extends CharSequence, GetChars, Spannable, Appendable
+{
+ /**
+ * Replaces the specified range (<code>st&hellip;en</code>) of text in this
+ * Editable with a copy of the slice <code>start&hellip;end</code> from
+ * <code>source</code>. The destination slice may be empty, in which case
+ * the operation is an insertion, or the source slice may be empty,
+ * in which case the operation is a deletion.
+ * <p>
+ * Before the change is committed, each filter that was set with
+ * {@link #setFilters} is given the opportunity to modify the
+ * <code>source</code> text.
+ * <p>
+ * If <code>source</code>
+ * is Spanned, the spans from it are preserved into the Editable.
+ * Existing spans within the Editable that entirely cover the replaced
+ * range are retained, but any that were strictly within the range
+ * that was replaced are removed. As a special case, the cursor
+ * position is preserved even when the entire range where it is
+ * located is replaced.
+ * @return a reference to this object.
+ */
+ public Editable replace(int st, int en, CharSequence source, int start, int end);
+
+ /**
+ * Convenience for replace(st, en, text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable replace(int st, int en, CharSequence text);
+
+ /**
+ * Convenience for replace(where, where, text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text, int start, int end);
+
+ /**
+ * Convenience for replace(where, where, text, 0, text.length());
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text);
+
+ /**
+ * Convenience for replace(st, en, "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable delete(int st, int en);
+
+ /**
+ * Convenience for replace(length(), length(), text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text);
+
+ /**
+ * Convenience for replace(length(), length(), text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text, int start, int end);
+
+ /**
+ * Convenience for append(String.valueOf(text)).
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(char text);
+
+ /**
+ * Convenience for replace(0, length(), "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ * Note that this clears the text, not the spans;
+ * use {@link #clearSpans} if you need that.
+ */
+ public void clear();
+
+ /**
+ * Removes all spans from the Editable, as if by calling
+ * {@link #removeSpan} on each of them.
+ */
+ public void clearSpans();
+
+ /**
+ * Sets the series of filters that will be called in succession
+ * whenever the text of this Editable is changed, each of which has
+ * the opportunity to limit or transform the text that is being inserted.
+ */
+ public void setFilters(InputFilter[] filters);
+
+ /**
+ * Returns the array of input filters that are currently applied
+ * to changes to this Editable.
+ */
+ public InputFilter[] getFilters();
+
+ /**
+ * Factory used by TextView to create new Editables. You can subclass
+ * it to provide something other than SpannableStringBuilder.
+ */
+ public static class Factory {
+ private static Editable.Factory sInstance = new Editable.Factory();
+
+ /**
+ * Returns the standard Editable Factory.
+ */
+ public static Editable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannedStringBuilder from the specified
+ * CharSequence. You can override this to provide
+ * a different kind of Spanned.
+ */
+ public Editable newEditable(CharSequence source) {
+ return new SpannableStringBuilder(source);
+ }
+ }
+}
diff --git a/core/java/android/text/GetChars.java b/core/java/android/text/GetChars.java
new file mode 100644
index 0000000..348a911
--- /dev/null
+++ b/core/java/android/text/GetChars.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Please implement this interface if your CharSequence has a
+ * getChars() method like the one in String that is faster than
+ * calling charAt() multiple times.
+ */
+public interface GetChars
+extends CharSequence
+{
+ /**
+ * Exactly like String.getChars(): copy chars <code>start</code>
+ * through <code>end - 1</code> from this CharSequence into <code>dest</code>
+ * beginning at offset <code>destoff</code>.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff);
+}
diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java
new file mode 100644
index 0000000..c3bd0ae
--- /dev/null
+++ b/core/java/android/text/GraphicsOperations.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+/**
+ * Please implement this interface if your CharSequence can do quick
+ * draw/measure/widths calculations from an internal array.
+ * {@hide}
+ */
+public interface GraphicsOperations
+extends CharSequence
+{
+ /**
+ * Just like {@link Canvas#drawText}.
+ */
+ void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p);
+
+ /**
+ * Just like {@link Paint#measureText}.
+ */
+ float measureText(int start, int end, Paint p);
+
+
+ /**
+ * Just like {@link Paint#getTextWidths}.
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p);
+}
diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java
new file mode 100644
index 0000000..90f5e4c
--- /dev/null
+++ b/core/java/android/text/Html.java
@@ -0,0 +1,750 @@
+/*
+ * 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;
+
+import org.ccil.cowan.tagsoup.HTMLSchema;
+import org.ccil.cowan.tagsoup.Parser;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.CharBuffer;
+
+/**
+ * This class processes HTML strings into displayable styled text.
+ * Not all HTML tags are supported.
+ */
+public class Html {
+ /**
+ * Retrieves images for HTML &lt;img&gt; tags.
+ */
+ public static interface ImageGetter {
+ /**
+ * This methos is called when the HTML parser encounters an
+ * &lt;img&gt; tag. The <code>source</code> argument is the
+ * string from the "src" attribute; the return value should be
+ * a Drawable representation of the image or <code>null</code>
+ * for a generic replacement image. Make sure you call
+ * setBounds() on your Drawable if it doesn't already have
+ * its bounds set.
+ */
+ public Drawable getDrawable(String source);
+ }
+
+ /**
+ * Is notified when HTML tags are encountered that the parser does
+ * not know how to interpret.
+ */
+ public static interface TagHandler {
+ /**
+ * This method will be called whenn the HTML parser encounters
+ * a tag that it does not know how to interpret.
+ */
+ public void handleTag(boolean opening, String tag,
+ Editable output, XMLReader xmlReader);
+ }
+
+ private Html() { }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any &lt;img&gt; tags in the HTML will display as a generic
+ * replacement image which your program can then go through and
+ * replace with real images.
+ *
+ * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null, null);
+ }
+
+ /**
+ * Lazy initialization holder for HTML parser. This class will
+ * a) be preloaded by the zygote, or b) not loaded until absolutely
+ * necessary.
+ */
+ private static class HtmlParser {
+ private static final HTMLSchema schema = new HTMLSchema();
+ }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any &lt;img&gt; tags in the HTML will use the specified ImageGetter
+ * to request a representation of the image (use null if you don't
+ * want this) and the specified TagHandler to handle unknown tags
+ * (specify null if you don't want this).
+ *
+ * <p>This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source, ImageGetter imageGetter,
+ TagHandler tagHandler) {
+ Parser parser = new Parser();
+ try {
+ parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
+ } catch (org.xml.sax.SAXNotRecognizedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ } catch (org.xml.sax.SAXNotSupportedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+
+ HtmlToSpannedConverter converter =
+ new HtmlToSpannedConverter(source, imageGetter, tagHandler,
+ parser);
+ return converter.convert();
+ }
+
+ /**
+ * Returns an HTML representation of the provided Spanned text.
+ */
+ public static String toHtml(Spanned text) {
+ StringBuilder out = new StringBuilder();
+ int len = text.length();
+
+ int next;
+ for (int i = 0; i < text.length(); i = next) {
+ next = text.nextSpanTransition(i, len, QuoteSpan.class);
+ QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
+
+ for (QuoteSpan quote: quotes) {
+ out.append("<blockquote>");
+ }
+
+ withinBlockquote(out, text, i, next);
+
+ for (QuoteSpan quote: quotes) {
+ out.append("</blockquote>\n");
+ }
+ }
+
+ return out.toString();
+ }
+
+ private static void withinBlockquote(StringBuilder out, Spanned text,
+ int start, int end) {
+ out.append("<p>");
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = TextUtils.indexOf(text, '\n', i, end);
+ if (next < 0) {
+ next = end;
+ }
+
+ int nl = 0;
+
+ while (next < end && text.charAt(next) == '\n') {
+ nl++;
+ next++;
+ }
+
+ withinParagraph(out, text, i, next - nl, nl, next == end);
+ }
+
+ out.append("</p>\n");
+ }
+
+ private static void withinParagraph(StringBuilder out, Spanned text,
+ int start, int end, int nl,
+ boolean last) {
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = text.nextSpanTransition(i, end, CharacterStyle.class);
+ CharacterStyle[] style = text.getSpans(i, next,
+ CharacterStyle.class);
+
+ for (int j = 0; j < style.length; j++) {
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("<b>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("<i>");
+ }
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("<tt>");
+ }
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("<sup>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("<sub>");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("<u>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("<strike>");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("<a href=\"");
+ out.append(((URLSpan) style[j]).getURL());
+ out.append("\">");
+ }
+ if (style[j] instanceof ImageSpan) {
+ out.append("<img src=\"");
+ out.append(((ImageSpan) style[j]).getSource());
+ out.append("\">");
+
+ // Don't output the dummy character underlying the image.
+ i = next;
+ }
+ }
+
+ withinStyle(out, text, i, next);
+
+ for (int j = style.length - 1; j >= 0; j--) {
+ if (style[j] instanceof URLSpan) {
+ out.append("</a>");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("</strike>");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("</u>");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("</sub>");
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("</sup>");
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("</tt>");
+ }
+ }
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("</b>");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("</i>");
+ }
+ }
+ }
+ }
+
+ String p = last ? "" : "</p>\n<p>";
+
+ if (nl == 1) {
+ out.append("<br>\n");
+ } else if (nl == 2) {
+ out.append(p);
+ } else {
+ for (int i = 2; i < nl; i++) {
+ out.append("<br>");
+ }
+
+ out.append(p);
+ }
+ }
+
+ private static void withinStyle(StringBuilder out, Spanned text,
+ int start, int end) {
+ for (int i = start; i < end; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("&lt;");
+ } else if (c == '>') {
+ out.append("&gt;");
+ } else if (c == '&') {
+ out.append("&amp;");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("&#" + ((int) c) + ";");
+ } else if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append("&nbsp;");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+ }
+}
+
+class HtmlToSpannedConverter implements ContentHandler {
+
+ private static final float[] HEADER_SIZES = {
+ 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
+ };
+
+ private String mSource;
+ private XMLReader mReader;
+ private SpannableStringBuilder mSpannableStringBuilder;
+ private Html.ImageGetter mImageGetter;
+ private Html.TagHandler mTagHandler;
+
+ public HtmlToSpannedConverter(
+ String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
+ Parser parser) {
+ mSource = source;
+ mSpannableStringBuilder = new SpannableStringBuilder();
+ mImageGetter = imageGetter;
+ mTagHandler = tagHandler;
+ mReader = parser;
+ }
+
+ public Spanned convert() {
+
+ mReader.setContentHandler(this);
+ try {
+ mReader.parse(new InputSource(new StringReader(mSource)));
+ } catch (IOException e) {
+ // We are reading from a string. There should not be IO problems.
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ // TagSoup doesn't throw parse exceptions.
+ throw new RuntimeException(e);
+ }
+
+ // Fix flags and range for paragraph-type markup.
+ Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
+ for (int i = 0; i < obj.length; i++) {
+ int start = mSpannableStringBuilder.getSpanStart(obj[i]);
+ int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
+
+ // If the last line of the range is blank, back off by one.
+ if (end - 2 >= 0) {
+ if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
+ mSpannableStringBuilder.charAt(end - 2) == '\n') {
+ end--;
+ }
+ }
+
+ if (end == start) {
+ mSpannableStringBuilder.removeSpan(obj[i]);
+ } else {
+ mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
+ }
+ }
+
+ return mSpannableStringBuilder;
+ }
+
+ private void handleStartTag(String tag, Attributes attributes) {
+ if (tag.equalsIgnoreCase("br")) {
+ // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
+ // so we can safely emite the linebreaks when we handle the close tag.
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("b")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("strong")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("cite")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("i")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("big")) {
+ start(mSpannableStringBuilder, new Big());
+ } else if (tag.equalsIgnoreCase("small")) {
+ start(mSpannableStringBuilder, new Small());
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Blockquote());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ start(mSpannableStringBuilder, new Monospace());
+ } else if (tag.equalsIgnoreCase("a")) {
+ startA(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("u")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ start(mSpannableStringBuilder, new Super());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ start(mSpannableStringBuilder, new Sub());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
+ } else if (tag.equalsIgnoreCase("img")) {
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private void handleEndTag(String tag) {
+ if (tag.equalsIgnoreCase("br")) {
+ handleBr(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("b")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("strong")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("cite")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("i")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("big")) {
+ end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
+ } else if (tag.equalsIgnoreCase("small")) {
+ end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ end(mSpannableStringBuilder, Monospace.class,
+ new TypefaceSpan("monospace"));
+ } else if (tag.equalsIgnoreCase("a")) {
+ endA(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("u")) {
+ end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ endHeader(mSpannableStringBuilder);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private static void handleP(SpannableStringBuilder text) {
+ int len = text.length();
+
+ if (len >= 1 && text.charAt(len - 1) == '\n') {
+ if (len >= 2 && text.charAt(len - 2) == '\n') {
+ return;
+ }
+
+ text.append("\n");
+ return;
+ }
+
+ if (len != 0) {
+ text.append("\n\n");
+ }
+ }
+
+ private static void handleBr(SpannableStringBuilder text) {
+ text.append("\n");
+ }
+
+ private static Object getLast(Spanned text, Class kind) {
+ /*
+ * This knows that the last returned object from getSpans()
+ * will be the most recently added.
+ */
+ Object[] objs = text.getSpans(0, text.length(), kind);
+
+ if (objs.length == 0) {
+ return null;
+ } else {
+ return objs[objs.length - 1];
+ }
+ }
+
+ private static void start(SpannableStringBuilder text, Object mark) {
+ int len = text.length();
+ text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void end(SpannableStringBuilder text, Class kind,
+ Object repl) {
+ int len = text.length();
+ Object obj = getLast(text, kind);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return;
+ }
+
+ private static void startImg(SpannableStringBuilder text,
+ Attributes attributes, Html.ImageGetter img) {
+ String src = attributes.getValue("", "src");
+ Drawable d = null;
+
+ if (img != null) {
+ d = img.getDrawable(src);
+ }
+
+ if (d == null) {
+ d = Resources.getSystem().
+ getDrawable(com.android.internal.R.drawable.unknown_image);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ }
+
+ int len = text.length();
+ text.append("\uFFFC");
+
+ text.setSpan(new ImageSpan(d, src), len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void startFont(SpannableStringBuilder text,
+ Attributes attributes) {
+ String color = attributes.getValue("", "color");
+ String face = attributes.getValue("", "face");
+
+ int len = text.length();
+ text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endFont(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Font.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Font f = (Font) obj;
+
+ if (f.mColor != null) {
+ int c = -1;
+
+ if (f.mColor.equalsIgnoreCase("aqua")) {
+ c = 0x00FFFF;
+ } else if (f.mColor.equalsIgnoreCase("black")) {
+ c = 0x000000;
+ } else if (f.mColor.equalsIgnoreCase("blue")) {
+ c = 0x0000FF;
+ } else if (f.mColor.equalsIgnoreCase("fuchsia")) {
+ c = 0xFF00FF;
+ } else if (f.mColor.equalsIgnoreCase("green")) {
+ c = 0x008000;
+ } else if (f.mColor.equalsIgnoreCase("grey")) {
+ c = 0x808080;
+ } else if (f.mColor.equalsIgnoreCase("lime")) {
+ c = 0x00FF00;
+ } else if (f.mColor.equalsIgnoreCase("maroon")) {
+ c = 0x800000;
+ } else if (f.mColor.equalsIgnoreCase("navy")) {
+ c = 0x000080;
+ } else if (f.mColor.equalsIgnoreCase("olive")) {
+ c = 0x808000;
+ } else if (f.mColor.equalsIgnoreCase("purple")) {
+ c = 0x800080;
+ } else if (f.mColor.equalsIgnoreCase("red")) {
+ c = 0xFF0000;
+ } else if (f.mColor.equalsIgnoreCase("silver")) {
+ c = 0xC0C0C0;
+ } else if (f.mColor.equalsIgnoreCase("teal")) {
+ c = 0x008080;
+ } else if (f.mColor.equalsIgnoreCase("white")) {
+ c = 0xFFFFFF;
+ } else if (f.mColor.equalsIgnoreCase("yellow")) {
+ c = 0xFFFF00;
+ } else {
+ try {
+ c = XmlUtils.convertValueToInt(f.mColor, -1);
+ } catch (NumberFormatException nfe) {
+ // Can't understand the color, so just drop it.
+ }
+ }
+
+ if (c != -1) {
+ text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
+ where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ if (f.mFace != null) {
+ text.setSpan(new TypefaceSpan(f.mFace), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void startA(SpannableStringBuilder text, Attributes attributes) {
+ String href = attributes.getValue("", "href");
+
+ int len = text.length();
+ text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endA(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Href.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Href h = (Href) obj;
+
+ if (h.mHref != null) {
+ text.setSpan(new URLSpan(h.mHref), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void endHeader(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Header.class);
+
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ // Back off not to change only the text, not the blank line.
+ while (len > where && text.charAt(len - 1) == '\n') {
+ len--;
+ }
+
+ if (where != len) {
+ Header h = (Header) obj;
+
+ text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ text.setSpan(new StyleSpan(Typeface.BOLD),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ }
+
+ public void startDocument() throws SAXException {
+ }
+
+ public void endDocument() throws SAXException {
+ }
+
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ handleStartTag(localName, attributes);
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ handleEndTag(localName);
+ }
+
+ public void characters(char ch[], int start, int length) throws SAXException {
+ mSpannableStringBuilder.append(CharBuffer.wrap(ch, start, length));
+ }
+
+ public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
+ }
+
+ public void processingInstruction(String target, String data) throws SAXException {
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ }
+
+ private static class Bold { }
+ private static class Italic { }
+ private static class Underline { }
+ private static class Big { }
+ private static class Small { }
+ private static class Monospace { }
+ private static class Blockquote { }
+ private static class Super { }
+ private static class Sub { }
+
+ private static class Font {
+ public String mColor;
+ public String mFace;
+
+ public Font(String color, String face) {
+ mColor = color;
+ mFace = face;
+ }
+ }
+
+ private static class Href {
+ public String mHref;
+
+ public Href(String href) {
+ mHref = href;
+ }
+ }
+
+ private static class Header {
+ private int mLevel;
+
+ public Header(int level) {
+ mLevel = level;
+ }
+ }
+}
diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/text/IClipboard.aidl
new file mode 100644
index 0000000..4deb5c8
--- /dev/null
+++ b/core/java/android/text/IClipboard.aidl
@@ -0,0 +1,42 @@
+/**
+ * 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;
+
+/**
+ * Programming interface to the clipboard, which allows copying and pasting
+ * between applications.
+ * {@hide}
+ */
+interface IClipboard {
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ CharSequence getClipboardText();
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ void setClipboardText(CharSequence text);
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ boolean hasClipboardText();
+}
+
diff --git a/core/java/android/text/InputFilter.java b/core/java/android/text/InputFilter.java
new file mode 100644
index 0000000..e1563ae
--- /dev/null
+++ b/core/java/android/text/InputFilter.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+/**
+ * InputFilters can be attached to {@link Editable}s to constrain the
+ * changes that can be made to them.
+ */
+public interface InputFilter
+{
+ /**
+ * This method is called when the buffer is going to replace the
+ * range <code>dstart &hellip; dend</code> of <code>dest</code>
+ * with the new text from the range <code>start &hellip; end</code>
+ * of <code>source</code>. Return the CharSequence that you would
+ * like to have placed there instead, including an empty string
+ * if appropriate, or <code>null</code> to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text. Also beware that
+ * you should not attempt to make any changes to <code>dest</code>
+ * from this method; you may only examine it for context.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend);
+
+ /**
+ * This filter will capitalize all the lower case letters that are added
+ * through edits.
+ */
+ public static class AllCaps implements InputFilter {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ for (int i = start; i < end; i++) {
+ if (Character.isLowerCase(source.charAt(i))) {
+ char[] v = new char[end - start];
+ TextUtils.getChars(source, start, end, v, 0);
+ String s = new String(v).toUpperCase();
+
+ if (source instanceof Spanned) {
+ SpannableString sp = new SpannableString(s);
+ TextUtils.copySpansFrom((Spanned) source,
+ start, end, null, sp, 0);
+ return sp;
+ } else {
+ return s;
+ }
+ }
+ }
+
+ return null; // keep original
+ }
+ }
+
+ /**
+ * This filter will constrain edits not to make the length of the text
+ * greater than the specified length.
+ */
+ public static class LengthFilter implements InputFilter {
+ public LengthFilter(int max) {
+ mMax = max;
+ }
+
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ int keep = mMax - (dest.length() - (dend - dstart));
+
+ if (keep <= 0) {
+ return "";
+ } else if (keep >= end - start) {
+ return null; // keep original
+ } else {
+ return source.subSequence(start, start + keep);
+ }
+ }
+
+ private int mMax;
+ }
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
new file mode 100644
index 0000000..346db49
--- /dev/null
+++ b/core/java/android/text/Layout.java
@@ -0,0 +1,1745 @@
+/*
+ * 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;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Path;
+import com.android.internal.util.ArrayUtils;
+import android.util.Config;
+
+import junit.framework.Assert;
+import android.text.style.*;
+import android.text.method.TextKeyListener;
+import android.view.KeyEvent;
+
+/**
+ * A base class that manages text layout in visual elements on
+ * the screen.
+ * <p>For text that will be edited, use a {@link DynamicLayout},
+ * which will be updated as the text changes.
+ * For text that will not change, use a {@link StaticLayout}.
+ */
+public abstract class Layout {
+ /**
+ * Return how wide a layout would be necessary to display the
+ * specified text with one line per paragraph.
+ */
+ public static float getDesiredWidth(CharSequence source,
+ TextPaint paint) {
+ return getDesiredWidth(source, 0, source.length(), paint);
+ }
+
+ /**
+ * Return how wide a layout would be necessary to display the
+ * specified text slice with one line per paragraph.
+ */
+ public static float getDesiredWidth(CharSequence source,
+ int start, int end,
+ TextPaint paint) {
+ float need = 0;
+ TextPaint workPaint = new TextPaint();
+
+ int next;
+ for (int i = start; i <= end; i = next) {
+ next = TextUtils.indexOf(source, '\n', i, end);
+
+ if (next < 0)
+ next = end;
+
+ float w = measureText(paint, workPaint,
+ source, i, next, null, true, null);
+
+ if (w > need)
+ need = w;
+
+ next++;
+ }
+
+ return need;
+ }
+
+ /**
+ * Subclasses of Layout use this constructor to set the display text,
+ * width, and other standard properties.
+ */
+ protected Layout(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd) {
+ if (width < 0)
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+
+ mText = text;
+ mPaint = paint;
+ mWorkPaint = new TextPaint();
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingmult;
+ mSpacingAdd = spacingadd;
+ mSpannedText = text instanceof Spanned;
+ }
+
+ /**
+ * Replace constructor properties of this Layout with new ones. Be careful.
+ */
+ /* package */ void replaceWith(CharSequence text, TextPaint paint,
+ int width, Alignment align,
+ float spacingmult, float spacingadd) {
+ if (width < 0) {
+ throw new IllegalArgumentException("Layout: " + width + " < 0");
+ }
+
+ mText = text;
+ mPaint = paint;
+ mWidth = width;
+ mAlignment = align;
+ mSpacingMult = spacingmult;
+ mSpacingAdd = spacingadd;
+ mSpannedText = text instanceof Spanned;
+ }
+
+ /**
+ * Draw this Layout on the specified Canvas.
+ */
+ public void draw(Canvas c) {
+ draw(c, null, null, 0);
+ }
+
+ /**
+ * Draw the specified rectangle from this Layout on the specified Canvas,
+ * with the specified path drawn between the background and the text.
+ */
+ public void draw(Canvas c, Path highlight, Paint highlightpaint,
+ int cursorOffsetVertical) {
+ int dtop, dbottom;
+
+ synchronized (sTempRect) {
+ if (!c.getClipBounds(sTempRect)) {
+ return;
+ }
+
+ dtop = sTempRect.top;
+ dbottom = sTempRect.bottom;
+ }
+
+ TextPaint paint = mPaint;
+
+ int top = 0;
+ // getLineBottom(getLineCount() -1) just calls getLineTop(getLineCount)
+ int bottom = getLineTop(getLineCount());
+
+
+ if (dtop > top) {
+ top = dtop;
+ }
+ if (dbottom < bottom) {
+ bottom = dbottom;
+ }
+
+ int first = getLineForVertical(top);
+ int last = getLineForVertical(bottom);
+
+ int previousLineBottom = getLineTop(first);
+ int previousLineEnd = getLineStart(first);
+
+ CharSequence buf = mText;
+
+ ParagraphStyle[] nospans = ArrayUtils.emptyArray(ParagraphStyle.class);
+ ParagraphStyle[] spans = nospans;
+ int spanend = 0;
+ int textLength = 0;
+ boolean spannedText = mSpannedText;
+
+ if (spannedText) {
+ spanend = 0;
+ textLength = buf.length();
+ for (int i = first; i <= last; i++) {
+ int start = previousLineEnd;
+ int end = getLineStart(i+1);
+ previousLineEnd = end;
+
+ int ltop = previousLineBottom;
+ int lbottom = getLineTop(i+1);
+ previousLineBottom = lbottom;
+ int lbaseline = lbottom - getLineDescent(i);
+
+ if (start >= spanend) {
+ Spanned sp = (Spanned) buf;
+ spanend = sp.nextSpanTransition(start, textLength,
+ LineBackgroundSpan.class);
+ spans = sp.getSpans(start, spanend,
+ LineBackgroundSpan.class);
+ }
+
+ for (int n = 0; n < spans.length; n++) {
+ LineBackgroundSpan back = (LineBackgroundSpan) spans[n];
+
+ back.drawBackground(c, paint, 0, mWidth,
+ ltop, lbaseline, lbottom,
+ buf, start, end,
+ i);
+ }
+ }
+ // reset to their original values
+ spanend = 0;
+ previousLineBottom = getLineTop(first);
+ previousLineEnd = getLineStart(first);
+ spans = nospans;
+ }
+
+ // There can be a highlight even without spans if we are drawing
+ // a non-spanned transformation of a spanned editing buffer.
+ if (highlight != null) {
+ if (cursorOffsetVertical != 0) {
+ c.translate(0, cursorOffsetVertical);
+ }
+
+ c.drawPath(highlight, highlightpaint);
+
+ if (cursorOffsetVertical != 0) {
+ c.translate(0, -cursorOffsetVertical);
+ }
+ }
+
+ Alignment align = mAlignment;
+
+ for (int i = first; i <= last; i++) {
+ int start = previousLineEnd;
+
+ previousLineEnd = getLineStart(i+1);
+ int end = getLineVisibleEnd(i, start, previousLineEnd);
+
+ int ltop = previousLineBottom;
+ int lbottom = getLineTop(i+1);
+ previousLineBottom = lbottom;
+ int lbaseline = lbottom - getLineDescent(i);
+
+ boolean par = false;
+ if (spannedText) {
+ if (start == 0 || buf.charAt(start - 1) == '\n') {
+ par = true;
+ }
+ if (start >= spanend) {
+
+ Spanned sp = (Spanned) buf;
+
+ spanend = sp.nextSpanTransition(start, textLength,
+ ParagraphStyle.class);
+ spans = sp.getSpans(start, spanend, ParagraphStyle.class);
+
+ align = mAlignment;
+
+ for (int n = spans.length-1; n >= 0; n--) {
+ if (spans[n] instanceof AlignmentSpan) {
+ align = ((AlignmentSpan) spans[n]).getAlignment();
+ break;
+ }
+ }
+ }
+ }
+
+ int dir = getParagraphDirection(i);
+ int left = 0;
+ int right = mWidth;
+
+ if (spannedText) {
+ final int length = spans.length;
+ for (int n = 0; n < length; n++) {
+ if (spans[n] instanceof LeadingMarginSpan) {
+ LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ margin.drawLeadingMargin(c, paint, right, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, par, this);
+
+ right -= margin.getLeadingMargin(par);
+ } else {
+ margin.drawLeadingMargin(c, paint, left, dir, ltop,
+ lbaseline, lbottom, buf,
+ start, end, par, this);
+
+ left += margin.getLeadingMargin(par);
+ }
+ }
+ }
+ }
+
+ int x;
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ x = left;
+ } else {
+ x = right;
+ }
+ } else {
+ int max = (int)getLineMax(i, spans, false);
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ x = left + max;
+ } else {
+ x = right - max;
+ }
+ } else {
+ // Alignment.ALIGN_CENTER
+ max = max & ~1;
+ int half = (right - left - max) >> 1;
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ x = right - half;
+ } else {
+ x = left + half;
+ }
+ }
+ }
+
+ Directions directions = getLineDirections(i);
+ boolean hasTab = getLineContainsTab(i);
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT &&
+ !spannedText && !hasTab) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT);
+ Assert.assertNotNull(c);
+ }
+ c.drawText(buf, start, end, x, lbaseline, paint);
+ } else {
+ drawText(c, buf, start, end, dir, directions,
+ x, ltop, lbaseline, lbottom, paint, mWorkPaint,
+ hasTab, spans);
+ }
+ }
+ }
+
+ /**
+ * Return the text that is displayed by this Layout.
+ */
+ public final CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Return the base Paint properties for this layout.
+ * Do NOT change the paint, which may result in funny
+ * drawing for this layout.
+ */
+ public final TextPaint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Return the width of this layout.
+ */
+ public final int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Return the width to which this Layout is ellipsizing, or
+ * {@link #getWidth} if it is not doing anything special.
+ */
+ public int getEllipsizedWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Increase the width of this layout to the specified width.
+ * Be careful to use this only when you know it is appropriate --
+ * it does not cause the text to reflow to use the full new width.
+ */
+ public final void increaseWidthTo(int wid) {
+ if (wid < mWidth) {
+ throw new RuntimeException("attempted to reduce Layout width");
+ }
+
+ mWidth = wid;
+ }
+
+ /**
+ * Return the total height of this layout.
+ */
+ public int getHeight() {
+ return getLineTop(getLineCount()); // same as getLineBottom(getLineCount() - 1);
+ }
+
+ /**
+ * Return the base alignment of this layout.
+ */
+ public final Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ /**
+ * Return what the text height is multiplied by to get the line height.
+ */
+ public final float getSpacingMultiplier() {
+ return mSpacingMult;
+ }
+
+ /**
+ * Return the number of units of leading that are added to each line.
+ */
+ public final float getSpacingAdd() {
+ return mSpacingAdd;
+ }
+
+ /**
+ * Return the number of lines of text in this layout.
+ */
+ public abstract int getLineCount();
+
+ /**
+ * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
+ * If bounds is not null, return the top, left, right, bottom extents
+ * of the specified line in it.
+ * @param line which line to examine (0..getLineCount() - 1)
+ * @param bounds Optional. If not null, it returns the extent of the line
+ * @return the Y-coordinate of the baseline
+ */
+ public int getLineBounds(int line, Rect bounds) {
+ if (bounds != null) {
+ bounds.left = 0; // ???
+ bounds.top = getLineTop(line);
+ bounds.right = mWidth; // ???
+ bounds.bottom = getLineBottom(line);
+ }
+ return getLineBaseline(line);
+ }
+
+ /**
+ * Return the vertical position of the top of the specified line.
+ * If the specified line is one beyond the last line, returns the
+ * bottom of the last line.
+ */
+ public abstract int getLineTop(int line);
+
+ /**
+ * Return the descent of the specified line.
+ */
+ public abstract int getLineDescent(int line);
+
+ /**
+ * Return the text offset of the beginning of the specified line.
+ * If the specified line is one beyond the last line, returns the
+ * end of the last line.
+ */
+ public abstract int getLineStart(int line);
+
+ /**
+ * Returns the primary directionality of the paragraph containing
+ * the specified line.
+ */
+ public abstract int getParagraphDirection(int line);
+
+ /**
+ * Returns whether the specified line contains one or more tabs.
+ */
+ public abstract boolean getLineContainsTab(int line);
+
+ /**
+ * Returns an array of directionalities for the specified line.
+ * The array alternates counts of characters in left-to-right
+ * and right-to-left segments of the line.
+ */
+ public abstract Directions getLineDirections(int line);
+
+ /**
+ * Returns the (negative) number of extra pixels of ascent padding in the
+ * top line of the Layout.
+ */
+ public abstract int getTopPadding();
+
+ /**
+ * Returns the number of extra pixels of descent padding in the
+ * bottom line of the Layout.
+ */
+ public abstract int getBottomPadding();
+
+ /**
+ * Get the primary horizontal position for the specified text offset.
+ * This is the location where a new character would be inserted in
+ * the paragraph's primary direction.
+ */
+ public float getPrimaryHorizontal(int offset) {
+ return getHorizontal(offset, false, true);
+ }
+
+ /**
+ * Get the secondary horizontal position for the specified text offset.
+ * This is the location where a new character would be inserted in
+ * the direction other than the paragraph's primary direction.
+ */
+ public float getSecondaryHorizontal(int offset) {
+ return getHorizontal(offset, true, true);
+ }
+
+ private float getHorizontal(int offset, boolean trailing, boolean alt) {
+ int line = getLineForOffset(offset);
+
+ return getHorizontal(offset, trailing, alt, line);
+ }
+
+ private float getHorizontal(int offset, boolean trailing, boolean alt,
+ int line) {
+ int start = getLineStart(line);
+ int end = getLineVisibleEnd(line);
+ int dir = getParagraphDirection(line);
+ boolean tab = getLineContainsTab(line);
+ Directions directions = getLineDirections(line);
+
+ TabStopSpan[] tabs = null;
+ if (tab && mText instanceof Spanned) {
+ tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ }
+
+ float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end,
+ dir, directions, trailing, alt, tab, tabs);
+
+ if (offset > end) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ wid -= measureText(mPaint, mWorkPaint,
+ mText, end, offset, null, tab, tabs);
+ else
+ wid += measureText(mPaint, mWorkPaint,
+ mText, end, offset, null, tab, tabs);
+ }
+
+ Alignment align = getParagraphAlignment(line);
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return right + wid;
+ else
+ return left + wid;
+ }
+
+ float max = getLineMax(line);
+
+ if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return left + max + wid;
+ else
+ return right - max + wid;
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int imax = ((int) max) & ~1;
+
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return right - (((right - left) - imax) / 2) + wid;
+ else
+ return left + ((right - left) - imax) / 2 + wid;
+ }
+ }
+
+ /**
+ * Get the leftmost position that should be exposed for horizontal
+ * scrolling on the specified line.
+ */
+ public float getLineLeft(int line) {
+ int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return getParagraphRight(line) - getLineMax(line);
+ else
+ return 0;
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return 0;
+ else
+ return mWidth - getLineMax(line);
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+ int max = ((int) getLineMax(line)) & ~1;
+
+ return left + ((right - left) - max) / 2;
+ }
+ }
+
+ /**
+ * Get the rightmost position that should be exposed for horizontal
+ * scrolling on the specified line.
+ */
+ public float getLineRight(int line) {
+ int dir = getParagraphDirection(line);
+ Alignment align = getParagraphAlignment(line);
+
+ if (align == Alignment.ALIGN_NORMAL) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return mWidth;
+ else
+ return getParagraphLeft(line) + getLineMax(line);
+ } else if (align == Alignment.ALIGN_OPPOSITE) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ return getLineMax(line);
+ else
+ return mWidth;
+ } else { /* align == Alignment.ALIGN_CENTER */
+ int left = getParagraphLeft(line);
+ int right = getParagraphRight(line);
+ int max = ((int) getLineMax(line)) & ~1;
+
+ return right - ((right - left) - max) / 2;
+ }
+ }
+
+ /**
+ * Gets the horizontal extent of the specified line, excluding
+ * trailing whitespace.
+ */
+ public float getLineMax(int line) {
+ return getLineMax(line, null, false);
+ }
+
+ /**
+ * Gets the horizontal extent of the specified line, including
+ * trailing whitespace.
+ */
+ public float getLineWidth(int line) {
+ return getLineMax(line, null, true);
+ }
+
+ private float getLineMax(int line, Object[] tabs, boolean full) {
+ int start = getLineStart(line);
+ int end;
+
+ if (full) {
+ end = getLineEnd(line);
+ } else {
+ end = getLineVisibleEnd(line);
+ }
+ boolean tab = getLineContainsTab(line);
+
+ if (tabs == null && tab && mText instanceof Spanned) {
+ tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
+ }
+
+ return measureText(mPaint, mWorkPaint,
+ mText, start, end, null, tab, tabs);
+ }
+
+ /**
+ * Get the line number corresponding to the specified vertical position.
+ * If you ask for a position above 0, you get 0; if you ask for a position
+ * below the bottom of the text, you get the last line.
+ */
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ public int getLineForVertical(int vertical) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineTop(guess) > vertical)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0)
+ return 0;
+ else
+ return low;
+ }
+
+ /**
+ * Get the line number on which the specified text offset appears.
+ * If you ask for a position before 0, you get 0; if you ask for a position
+ * beyond the end of the text, you get the last line.
+ */
+ public int getLineForOffset(int offset) {
+ int high = getLineCount(), low = -1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (getLineStart(guess) > offset)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < 0)
+ return 0;
+ else
+ return low;
+ }
+
+ /**
+ * Get the character offset on the specfied line whose position is
+ * closest to the specified horizontal position.
+ */
+ public int getOffsetForHorizontal(int line, float horiz) {
+ int max = getLineEnd(line) - 1;
+ int min = getLineStart(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line == getLineCount() - 1)
+ max++;
+
+ int best = min;
+ float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
+
+ int here = min;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ int swap = ((i & 1) == 0) ? 1 : -1;
+
+ if (there > max)
+ there = max;
+
+ int high = there - 1 + 1, low = here + 1 - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+ int adguess = getOffsetAtStartOf(guess);
+
+ if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < here + 1)
+ low = here + 1;
+
+ if (low < there) {
+ low = getOffsetAtStartOf(low);
+
+ float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
+
+ int aft = TextUtils.getOffsetAfter(mText, low);
+ if (aft < there) {
+ float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
+
+ if (other < dist) {
+ dist = other;
+ low = aft;
+ }
+ }
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = low;
+ }
+ }
+
+ float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = here;
+ }
+
+ here = there;
+ }
+
+ float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
+
+ if (dist < bestdist) {
+ bestdist = dist;
+ best = max;
+ }
+
+ return best;
+ }
+
+ /**
+ * Return the text offset after the last character on the specified line.
+ */
+ public final int getLineEnd(int line) {
+ return getLineStart(line + 1);
+ }
+
+ /**
+ * Return the text offset after the last visible character (so whitespace
+ * is not counted) on the specified line.
+ */
+ public int getLineVisibleEnd(int line) {
+ return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
+ }
+
+ private int getLineVisibleEnd(int line, int start, int end) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
+ }
+
+ CharSequence text = mText;
+ char ch;
+ if (line == getLineCount() - 1) {
+ return end;
+ }
+
+ for (; end > start; end--) {
+ ch = text.charAt(end - 1);
+
+ if (ch == '\n') {
+ return end - 1;
+ }
+
+ if (ch != ' ' && ch != '\t') {
+ break;
+ }
+
+ }
+
+ return end;
+ }
+
+ /**
+ * Return the vertical position of the bottom of the specified line.
+ */
+ public final int getLineBottom(int line) {
+ return getLineTop(line + 1);
+ }
+
+ /**
+ * Return the vertical position of the baseline of the specified line.
+ */
+ public final int getLineBaseline(int line) {
+ // getLineTop(line+1) == getLineTop(line)
+ return getLineTop(line+1) - getLineDescent(line);
+ }
+
+ /**
+ * Get the ascent of the text on the specified line.
+ * The return value is negative to match the Paint.ascent() convention.
+ */
+ public final int getLineAscent(int line) {
+ // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
+ return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
+ }
+
+ /**
+ * Return the text offset that would be reached by moving left
+ * (possibly onto another line) from the specified offset.
+ */
+ public int getOffsetToLeftOf(int offset) {
+ int line = getLineForOffset(offset);
+ int start = getLineStart(line);
+ int end = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line != getLineCount() - 1)
+ end--;
+
+ float horiz = getPrimaryHorizontal(offset);
+
+ int best = offset;
+ float besth = Integer.MIN_VALUE;
+ int candidate;
+
+ candidate = TextUtils.getOffsetBefore(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ int here = start;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > end)
+ there = end;
+
+ float h = getPrimaryHorizontal(here);
+
+ if (h < horiz && h > besth) {
+ best = here;
+ besth = h;
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, here);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetBefore(mText, there);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h < horiz && h > besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ here = there;
+ }
+
+ float h = getPrimaryHorizontal(end);
+
+ if (h < horiz && h > besth) {
+ best = end;
+ besth = h;
+ }
+
+ if (best != offset)
+ return best;
+
+ int dir = getParagraphDirection(line);
+
+ if (dir > 0) {
+ if (line == 0)
+ return best;
+ else
+ return getOffsetForHorizontal(line - 1, 10000);
+ } else {
+ if (line == getLineCount() - 1)
+ return best;
+ else
+ return getOffsetForHorizontal(line + 1, 10000);
+ }
+ }
+
+ /**
+ * Return the text offset that would be reached by moving right
+ * (possibly onto another line) from the specified offset.
+ */
+ public int getOffsetToRightOf(int offset) {
+ int line = getLineForOffset(offset);
+ int start = getLineStart(line);
+ int end = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (line != getLineCount() - 1)
+ end--;
+
+ float horiz = getPrimaryHorizontal(offset);
+
+ int best = offset;
+ float besth = Integer.MAX_VALUE;
+ int candidate;
+
+ candidate = TextUtils.getOffsetBefore(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, offset);
+ if (candidate >= start && candidate <= end) {
+ float h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ int here = start;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > end)
+ there = end;
+
+ float h = getPrimaryHorizontal(here);
+
+ if (h > horiz && h < besth) {
+ best = here;
+ besth = h;
+ }
+
+ candidate = TextUtils.getOffsetAfter(mText, here);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ candidate = TextUtils.getOffsetBefore(mText, there);
+ if (candidate >= start && candidate <= end) {
+ h = getPrimaryHorizontal(candidate);
+
+ if (h > horiz && h < besth) {
+ best = candidate;
+ besth = h;
+ }
+ }
+
+ here = there;
+ }
+
+ float h = getPrimaryHorizontal(end);
+
+ if (h > horiz && h < besth) {
+ best = end;
+ besth = h;
+ }
+
+ if (best != offset)
+ return best;
+
+ int dir = getParagraphDirection(line);
+
+ if (dir > 0) {
+ if (line == getLineCount() - 1)
+ return best;
+ else
+ return getOffsetForHorizontal(line + 1, -10000);
+ } else {
+ if (line == 0)
+ return best;
+ else
+ return getOffsetForHorizontal(line - 1, -10000);
+ }
+ }
+
+ private int getOffsetAtStartOf(int offset) {
+ if (offset == 0)
+ return 0;
+
+ CharSequence text = mText;
+ char c = text.charAt(offset);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 1);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 1;
+ }
+
+ if (mSpannedText) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = start;
+ }
+ }
+
+ return offset;
+ }
+
+ /**
+ * Fills in the specified Path with a representation of a cursor
+ * at the specified offset. This will often be a vertical line
+ * but can be multiple discontinous lines in text with multiple
+ * directionalities.
+ */
+ public void getCursorPath(int point, Path dest,
+ CharSequence editingBuffer) {
+ dest.reset();
+
+ int line = getLineForOffset(point);
+ int top = getLineTop(line);
+ int bottom = getLineTop(line+1);
+
+ float h1 = getPrimaryHorizontal(point) - 0.5f;
+ float h2 = getSecondaryHorizontal(point) - 0.5f;
+
+ int caps = TextKeyListener.getMetaState(editingBuffer,
+ KeyEvent.META_SHIFT_ON);
+ int fn = TextKeyListener.getMetaState(editingBuffer,
+ KeyEvent.META_ALT_ON);
+ int dist = 0;
+
+ if (caps != 0 || fn != 0) {
+ dist = (bottom - top) >> 2;
+
+ if (fn != 0)
+ top += dist;
+ if (caps != 0)
+ bottom -= dist;
+ }
+
+ if (h1 < 0.5f)
+ h1 = 0.5f;
+ if (h2 < 0.5f)
+ h2 = 0.5f;
+
+ if (h1 == h2) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1, bottom);
+ } else {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1, (top + bottom) >> 1);
+
+ dest.moveTo(h2, (top + bottom) >> 1);
+ dest.lineTo(h2, bottom);
+ }
+
+ if (caps == 2) {
+ dest.moveTo(h2, bottom);
+ dest.lineTo(h2 - dist, bottom + dist);
+ dest.lineTo(h2, bottom);
+ dest.lineTo(h2 + dist, bottom + dist);
+ } else if (caps == 1) {
+ dest.moveTo(h2, bottom);
+ dest.lineTo(h2 - dist, bottom + dist);
+
+ dest.moveTo(h2 - dist, bottom + dist - 0.5f);
+ dest.lineTo(h2 + dist, bottom + dist - 0.5f);
+
+ dest.moveTo(h2 + dist, bottom + dist);
+ dest.lineTo(h2, bottom);
+ }
+
+ if (fn == 2) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+ dest.lineTo(h1, top);
+ dest.lineTo(h1 + dist, top - dist);
+ } else if (fn == 1) {
+ dest.moveTo(h1, top);
+ dest.lineTo(h1 - dist, top - dist);
+
+ dest.moveTo(h1 - dist, top - dist + 0.5f);
+ dest.lineTo(h1 + dist, top - dist + 0.5f);
+
+ dest.moveTo(h1 + dist, top - dist);
+ dest.lineTo(h1, top);
+ }
+ }
+
+ private void addSelection(int line, int start, int end,
+ int top, int bottom, Path dest) {
+ int linestart = getLineStart(line);
+ int lineend = getLineEnd(line);
+ Directions dirs = getLineDirections(line);
+
+ if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
+ lineend--;
+
+ int here = linestart;
+ for (int i = 0; i < dirs.mDirections.length; i++) {
+ int there = here + dirs.mDirections[i];
+ if (there > lineend)
+ there = lineend;
+
+ if (start <= there && end >= here) {
+ int st = Math.max(start, here);
+ int en = Math.min(end, there);
+
+ if (st != en) {
+ float h1 = getHorizontal(st, false, false, line);
+ float h2 = getHorizontal(en, true, false, line);
+
+ dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
+ }
+ }
+
+ here = there;
+ }
+ }
+
+ /**
+ * Fills in the specified Path with a representation of a highlight
+ * between the specified offsets. This will often be a rectangle
+ * or a potentially discontinuous set of rectangles. If the start
+ * and end are the same, the returned path is empty.
+ */
+ public void getSelectionPath(int start, int end, Path dest) {
+ dest.reset();
+
+ if (start == end)
+ return;
+
+ if (end < start) {
+ int temp = end;
+ end = start;
+ start = temp;
+ }
+
+ int startline = getLineForOffset(start);
+ int endline = getLineForOffset(end);
+
+ int top = getLineTop(startline);
+ int bottom = getLineBottom(endline);
+
+ if (startline == endline) {
+ addSelection(startline, start, end, top, bottom, dest);
+ } else {
+ final float width = mWidth;
+
+ addSelection(startline, start, getLineEnd(startline),
+ top, getLineBottom(startline), dest);
+
+ if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
+ dest.addRect(getLineLeft(startline), top,
+ 0, getLineBottom(startline), Path.Direction.CW);
+ else
+ dest.addRect(getLineRight(startline), top,
+ width, getLineBottom(startline), Path.Direction.CW);
+
+ for (int i = startline + 1; i < endline; i++) {
+ top = getLineTop(i);
+ bottom = getLineBottom(i);
+ dest.addRect(0, top, width, bottom, Path.Direction.CW);
+ }
+
+ top = getLineTop(endline);
+ bottom = getLineBottom(endline);
+
+ addSelection(endline, getLineStart(endline), end,
+ top, bottom, dest);
+
+ if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
+ dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
+ else
+ dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
+ }
+ }
+
+ /**
+ * Get the alignment of the specified paragraph, taking into account
+ * markup attached to it.
+ */
+ public final Alignment getParagraphAlignment(int line) {
+ Alignment align = mAlignment;
+
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ AlignmentSpan.class);
+
+ int spanLength = spans.length;
+ if (spanLength > 0) {
+ align = spans[spanLength-1].getAlignment();
+ }
+ }
+
+ return align;
+ }
+
+ /**
+ * Get the left edge of the specified paragraph, inset by left margins.
+ */
+ public final int getParagraphLeft(int line) {
+ int dir = getParagraphDirection(line);
+
+ int left = 0;
+
+ boolean par = false;
+ int off = getLineStart(line);
+ if (off == 0 || mText.charAt(off - 1) == '\n')
+ par = true;
+
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ LeadingMarginSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ left += spans[i].getLeadingMargin(par);
+ }
+ }
+ }
+
+ return left;
+ }
+
+ /**
+ * Get the right edge of the specified paragraph, inset by right margins.
+ */
+ public final int getParagraphRight(int line) {
+ int dir = getParagraphDirection(line);
+
+ int right = mWidth;
+
+ boolean par = false;
+ int off = getLineStart(line);
+ if (off == 0 || mText.charAt(off - 1) == '\n')
+ par = true;
+
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ if (mSpannedText) {
+ Spanned sp = (Spanned) mText;
+ LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
+ getLineEnd(line),
+ LeadingMarginSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ right -= spans[i].getLeadingMargin(par);
+ }
+ }
+ }
+
+ return right;
+ }
+
+ private static void drawText(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, Directions directions,
+ float x, int top, int y, int bottom,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean hasTabs, Object[] parspans) {
+ char[] buf;
+ if (!hasTabs) {
+ if (directions == DIRS_ALL_LEFT_TO_RIGHT) {
+ if (Config.DEBUG) {
+ Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir);
+ }
+ Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false);
+ return;
+ }
+ buf = null;
+ } else {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ float h = 0;
+
+ int here = 0;
+ for (int i = 0; i < directions.mDirections.length; i++) {
+ int there = here + directions.mDirections[i];
+ if (there > end - start)
+ there = end - start;
+
+ int segstart = here;
+ for (int j = hasTabs ? here : there; j <= there; j++) {
+ if (j == there || buf[j] == '\t') {
+ h += Styled.drawText(canvas, text,
+ start + segstart, start + j,
+ dir, (i & 1) != 0, x + h,
+ top, y, bottom, paint, workPaint,
+ start + j != end);
+
+ if (j != there && buf[j] == '\t')
+ h = dir * nextTab(text, start, end, h * dir, parspans);
+
+ segstart = j + 1;
+ }
+ }
+
+ here = there;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+ }
+
+ private static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text,
+ int start, int offset, int end,
+ int dir, Directions directions,
+ boolean trailing, boolean alt,
+ boolean hasTabs, Object[] tabs) {
+ char[] buf = null;
+
+ if (hasTabs) {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ float h = 0;
+
+ if (alt) {
+ if (dir == DIR_RIGHT_TO_LEFT)
+ trailing = !trailing;
+ }
+
+ int here = 0;
+ for (int i = 0; i < directions.mDirections.length; i++) {
+ if (alt)
+ trailing = !trailing;
+
+ int there = here + directions.mDirections[i];
+ if (there > end - start)
+ there = end - start;
+
+ int segstart = here;
+ for (int j = hasTabs ? here : there; j <= there; j++) {
+ if (j == there || buf[j] == '\t') {
+ float segw;
+
+ if (offset < start + j ||
+ (trailing && offset <= start + j)) {
+ if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) {
+ h += Styled.measureText(paint, workPaint, text,
+ start + segstart, offset,
+ null);
+ return h;
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) {
+ h -= Styled.measureText(paint, workPaint, text,
+ start + segstart, offset,
+ null);
+ return h;
+ }
+ }
+
+ segw = Styled.measureText(paint, workPaint, text,
+ start + segstart, start + j,
+ null);
+
+ if (offset < start + j ||
+ (trailing && offset <= start + j)) {
+ if (dir == DIR_LEFT_TO_RIGHT) {
+ h += segw - Styled.measureText(paint, workPaint,
+ text,
+ start + segstart,
+ offset, null);
+ return h;
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT) {
+ h -= segw - Styled.measureText(paint, workPaint,
+ text,
+ start + segstart,
+ offset, null);
+ return h;
+ }
+ }
+
+ if (dir == DIR_RIGHT_TO_LEFT)
+ h -= segw;
+ else
+ h += segw;
+
+ if (j != there && buf[j] == '\t') {
+ if (offset == start + j)
+ return h;
+
+ h = dir * nextTab(text, start, end, h * dir, tabs);
+ }
+
+ segstart = j + 1;
+ }
+ }
+
+ here = there;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+
+ return h;
+ }
+
+ /* package */ static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm,
+ boolean hasTabs, Object[] tabs) {
+ char[] buf = null;
+
+ if (hasTabs) {
+ buf = TextUtils.obtain(end - start);
+ TextUtils.getChars(text, start, end, buf, 0);
+ }
+
+ int len = end - start;
+
+ int here = 0;
+ float h = 0;
+ int ab = 0, be = 0;
+ int top = 0, bot = 0;
+
+ if (fm != null) {
+ fm.ascent = 0;
+ fm.descent = 0;
+ }
+
+ for (int i = hasTabs ? 0 : len; i <= len; i++) {
+ if (i == len || buf[i] == '\t') {
+ workPaint.baselineShift = 0;
+
+ h += Styled.measureText(paint, workPaint, text,
+ start + here, start + i,
+ fm);
+
+ if (fm != null) {
+ if (workPaint.baselineShift < 0) {
+ fm.ascent += workPaint.baselineShift;
+ fm.top += workPaint.baselineShift;
+ } else {
+ fm.descent += workPaint.baselineShift;
+ fm.bottom += workPaint.baselineShift;
+ }
+ }
+
+ if (i != len)
+ h = nextTab(text, start, end, h, tabs);
+
+ if (fm != null) {
+ if (fm.ascent < ab) {
+ ab = fm.ascent;
+ }
+ if (fm.descent > be) {
+ be = fm.descent;
+ }
+
+ if (fm.top < top) {
+ top = fm.top;
+ }
+ if (fm.bottom > bot) {
+ bot = fm.bottom;
+ }
+ }
+
+ here = i + 1;
+ }
+ }
+
+ if (fm != null) {
+ fm.ascent = ab;
+ fm.descent = be;
+ fm.top = top;
+ fm.bottom = bot;
+ }
+
+ if (hasTabs)
+ TextUtils.recycle(buf);
+
+ return h;
+ }
+
+ /* package */ static float nextTab(CharSequence text, int start, int end,
+ float h, Object[] tabs) {
+ float nh = Float.MAX_VALUE;
+ boolean alltabs = false;
+
+ if (text instanceof Spanned) {
+ if (tabs == null) {
+ tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
+ alltabs = true;
+ }
+
+ for (int i = 0; i < tabs.length; i++) {
+ if (!alltabs) {
+ if (!(tabs[i] instanceof TabStopSpan))
+ continue;
+ }
+
+ int where = ((TabStopSpan) tabs[i]).getTabStop();
+
+ if (where < nh && where > h)
+ nh = where;
+ }
+
+ if (nh != Float.MAX_VALUE)
+ return nh;
+ }
+
+ return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
+ }
+
+ protected final boolean isSpanned() {
+ return mSpannedText;
+ }
+
+ private void ellipsize(int start, int end, int line,
+ char[] dest, int destoff) {
+ int ellipsisCount = getEllipsisCount(line);
+
+ if (ellipsisCount == 0) {
+ return;
+ }
+
+ int ellipsisStart = getEllipsisStart(line);
+ int linestart = getLineStart(line);
+
+ for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
+ char c;
+
+ if (i == ellipsisStart) {
+ c = '\u2026'; // ellipsis
+ } else {
+ c = '\uFEFF'; // 0-width space
+ }
+
+ int a = i + linestart;
+
+ if (a >= start && a < end) {
+ dest[destoff + a - start] = c;
+ }
+ }
+ }
+
+ /**
+ * Stores information about bidirectional (left-to-right or right-to-left)
+ * text within the layout of a line. TODO: This work is not complete
+ * or correct and will be fleshed out in a later revision.
+ */
+ public static class Directions {
+ private short[] mDirections;
+
+ /* package */ Directions(short[] dirs) {
+ mDirections = dirs;
+ }
+ }
+
+ /**
+ * Return the offset of the first character to be ellipsized away,
+ * relative to the start of the line. (So 0 if the beginning of the
+ * line is ellipsized, not getLineStart().)
+ */
+ public abstract int getEllipsisStart(int line);
+ /**
+ * Returns the number of characters to be ellipsized away, or 0 if
+ * no ellipsis is to take place.
+ */
+ public abstract int getEllipsisCount(int line);
+
+ /* package */ static class Ellipsizer implements CharSequence, GetChars {
+ /* package */ CharSequence mText;
+ /* package */ Layout mLayout;
+ /* package */ int mWidth;
+ /* package */ TextUtils.TruncateAt mMethod;
+
+ public Ellipsizer(CharSequence s) {
+ mText = s;
+ }
+
+ public char charAt(int off) {
+ char[] buf = TextUtils.obtain(1);
+ getChars(off, off + 1, buf, 0);
+ char ret = buf[0];
+
+ TextUtils.recycle(buf);
+ return ret;
+ }
+
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ int line1 = mLayout.getLineForOffset(start);
+ int line2 = mLayout.getLineForOffset(end);
+
+ TextUtils.getChars(mText, start, end, dest, destoff);
+
+ for (int i = line1; i <= line2; i++) {
+ mLayout.ellipsize(start, end, i, dest, destoff);
+ }
+ }
+
+ public int length() {
+ return mText.length();
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+ return new String(s);
+ }
+
+ public String toString() {
+ char[] s = new char[length()];
+ getChars(0, length(), s, 0);
+ return new String(s);
+ }
+
+ }
+
+ /* package */ static class SpannedEllipsizer
+ extends Ellipsizer implements Spanned {
+ private Spanned mSpanned;
+
+ public SpannedEllipsizer(CharSequence display) {
+ super(display);
+ mSpanned = (Spanned) display;
+ }
+
+ 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 limit, Class type) {
+ return mSpanned.nextSpanTransition(start, limit, type);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ char[] s = new char[end - start];
+ getChars(start, end, s, 0);
+
+ SpannableString ss = new SpannableString(new String(s));
+ TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
+ return ss;
+ }
+ }
+
+ private CharSequence mText;
+ private TextPaint mPaint;
+ /* package */ TextPaint mWorkPaint;
+ private int mWidth;
+ private Alignment mAlignment = Alignment.ALIGN_NORMAL;
+ private float mSpacingMult;
+ private float mSpacingAdd;
+ private static Rect sTempRect = new Rect();
+ private boolean mSpannedText;
+
+ public static final int DIR_LEFT_TO_RIGHT = 1;
+ public static final int DIR_RIGHT_TO_LEFT = -1;
+
+ public enum Alignment {
+ ALIGN_NORMAL,
+ ALIGN_OPPOSITE,
+ ALIGN_CENTER,
+ // XXX ALIGN_LEFT,
+ // XXX ALIGN_RIGHT,
+ }
+
+ private static final int TAB_INCREMENT = 20;
+
+ /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
+ new Directions(new short[] { 32767 });
+ /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
+ new Directions(new short[] { 0, 32767 });
+
+}
+
diff --git a/core/java/android/text/LoginFilter.java b/core/java/android/text/LoginFilter.java
new file mode 100644
index 0000000..dd2d77f
--- /dev/null
+++ b/core/java/android/text/LoginFilter.java
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+/**
+ * Abstract class for filtering login-related text (user names and passwords)
+ *
+ */
+public abstract class LoginFilter implements InputFilter {
+ private boolean mAppendInvalid; // whether to append or ignore invalid characters
+ /**
+ * Base constructor for LoginFilter
+ * @param appendInvalid whether or not to append invalid characters.
+ */
+ LoginFilter(boolean appendInvalid) {
+ mAppendInvalid = appendInvalid;
+ }
+
+ /**
+ * Default constructor for LoginFilter doesn't append invalid characters.
+ */
+ LoginFilter() {
+ mAppendInvalid = false;
+ }
+
+ /**
+ * This method is called when the buffer is going to replace the
+ * range <code>dstart &hellip; dend</code> of <code>dest</code>
+ * with the new text from the range <code>start &hellip; end</code>
+ * of <code>source</code>. Returns the CharSequence that we want
+ * placed there instead, including an empty string
+ * if appropriate, or <code>null</code> to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ char[] out = new char[end - start]; // reserve enough space for whole string
+ int outidx = 0;
+ boolean changed = false;
+
+ onStart();
+
+ // Scan through beginning characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ // Scan through changed characters rejecting disallowed chars
+ for (int i = start; i < end; i++) {
+ char c = source.charAt(i);
+ if (isAllowed(c)) {
+ // Character allowed. Add it to the sequence.
+ out[outidx++] = c;
+ } else {
+ if (mAppendInvalid) out[outidx++] = c;
+ else changed = true; // we changed the original string
+ onInvalidCharacter(c);
+ }
+ }
+
+ // Scan through remaining characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = dend; i < dest.length(); i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ onStop();
+
+ return changed ? new String(out, 0, outidx) : null;
+ }
+
+ /**
+ * Called when we start processing filter.
+ */
+ public void onStart() {
+
+ }
+
+ /**
+ * Called whenever we encounter an invalid character.
+ * @param c the invalid character
+ */
+ public void onInvalidCharacter(char c) {
+
+ }
+
+ /**
+ * Called when we're done processing filter
+ */
+ public void onStop() {
+
+ }
+
+ /**
+ * Returns whether or not we allow character c.
+ * Subclasses must override this method.
+ */
+ public abstract boolean isAllowed(char c);
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with GMail
+ * account creation. It prevents the user from entering user names with characters other than
+ * [a-zA-Z0-9.].
+ *
+ */
+ public static class UsernameFilterGMail extends LoginFilter {
+
+ public UsernameFilterGMail() {
+ super(false);
+ }
+
+ public UsernameFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if ('.' == c)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with Google login.
+ * It is slightly less restrictive than the above filter in that it allows [a-zA-Z0-9._-].
+ *
+ */
+ public static class UsernameFilterGeneric extends LoginFilter {
+ private static final String mAllowed = "@_-."; // Additional characters
+
+ public UsernameFilterGeneric() {
+ super(false);
+ }
+
+ public UsernameFilterGeneric(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if (mAllowed.indexOf(c) != -1)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter is compatible with GMail passwords which restricts characters to
+ * the Latin-1 (ISO8859-1) char set.
+ *
+ */
+ public static class PasswordFilterGMail extends LoginFilter {
+
+ public PasswordFilterGMail() {
+ super(false);
+ }
+
+ public PasswordFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ // We should reject anything not in the Latin-1 (ISO8859-1) charset
+ @Override
+ public boolean isAllowed(char c) {
+ if (32 <= c && c <= 127)
+ return true; // standard charset
+ // if (128 <= c && c <= 159) return true; // nonstandard (Windows(TM)(R)) charset
+ if (160 <= c && c <= 255)
+ return true; // extended charset
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/PackedIntVector.java b/core/java/android/text/PackedIntVector.java
new file mode 100644
index 0000000..d87f600
--- /dev/null
+++ b/core/java/android/text/PackedIntVector.java
@@ -0,0 +1,368 @@
+/*
+ * 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;
+
+import com.android.internal.util.ArrayUtils;
+
+
+/**
+ * PackedIntVector stores a two-dimensional array of integers,
+ * optimized for inserting and deleting rows and for
+ * offsetting the values in segments of a given column.
+ */
+class PackedIntVector {
+ private final int mColumns;
+ private int mRows;
+
+ private int mRowGapStart;
+ private int mRowGapLength;
+
+ private int[] mValues;
+ private int[] mValueGap; // starts, followed by lengths
+
+ /**
+ * Creates a new PackedIntVector with the specified width and
+ * a height of 0.
+ *
+ * @param columns the width of the PackedIntVector.
+ */
+ public PackedIntVector(int columns) {
+ mColumns = columns;
+ mRows = 0;
+
+ mRowGapStart = 0;
+ mRowGapLength = mRows;
+
+ mValues = null;
+ mValueGap = new int[2 * columns];
+ }
+
+ /**
+ * Returns the value at the specified row and column.
+ *
+ * @param row the index of the row to return.
+ * @param column the index of the column to return.
+ *
+ * @return the value stored at the specified position.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row >= size()) or the column is out of range
+ * (column &lt; 0 || column >= width()).
+ */
+ public int getValue(int row, int column) {
+ final int columns = mColumns;
+
+ if (((row | column) < 0) || (row >= size()) || (column >= columns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int value = mValues[row * columns + column];
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value += valuegap[column + columns];
+ }
+
+ return value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row >= size()) or the column is out of range
+ * (column &lt; 0 || column >= width()).
+ */
+ public void setValue(int row, int column, int value) {
+ if (((row | column) < 0) || (row >= size()) || (column >= mColumns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ * Private internal version: does not check args.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ */
+ private void setValueInternal(int row, int column, int value) {
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+
+ /**
+ * Increments all values in the specified column whose row >= the
+ * specified row by the specified delta.
+ *
+ * @param startRow the row at which to begin incrementing.
+ * This may be == size(), which case there is no effect.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (startRow &lt; 0 || startRow > size()) or the column
+ * is out of range (column &lt; 0 || column >= width()).
+ */
+ public void adjustValuesBelow(int startRow, int column, int delta) {
+ if (((startRow | column) < 0) || (startRow > size()) ||
+ (column >= width())) {
+ throw new IndexOutOfBoundsException(startRow + ", " + column);
+ }
+
+ if (startRow >= mRowGapStart) {
+ startRow += mRowGapLength;
+ }
+
+ moveValueGapTo(column, startRow);
+ mValueGap[column + mColumns] += delta;
+ }
+
+ /**
+ * Inserts a new row of values at the specified row offset.
+ *
+ * @param row the row above which to insert the new row.
+ * This may be == size(), which case the new row is added
+ * at the end.
+ * @param values the new values to be added. If this is null,
+ * a row of zeroes is added.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row &lt; 0 || row > size()) or if the length of the
+ * values array is too small (values.length < width()).
+ */
+ public void insertAt(int row, int[] values) {
+ if ((row < 0) || (row > size())) {
+ throw new IndexOutOfBoundsException("row " + row);
+ }
+
+ if ((values != null) && (values.length < width())) {
+ throw new IndexOutOfBoundsException("value count " + values.length);
+ }
+
+ moveRowGapTo(row);
+
+ if (mRowGapLength == 0) {
+ growBuffer();
+ }
+
+ mRowGapStart++;
+ mRowGapLength--;
+
+ if (values == null) {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, 0);
+ }
+ } else {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, values[i]);
+ }
+ }
+ }
+
+ /**
+ * Deletes the specified number of rows starting with the specified
+ * row.
+ *
+ * @param row the index of the first row to be deleted.
+ * @param count the number of rows to delete.
+ *
+ * @throws IndexOutOfBoundsException if any of the rows to be deleted
+ * are out of range (row &lt; 0 || count &lt; 0 ||
+ * row + count > size()).
+ */
+ public void deleteAt(int row, int count) {
+ if (((row | count) < 0) || (row + count > size())) {
+ throw new IndexOutOfBoundsException(row + ", " + count);
+ }
+
+ moveRowGapTo(row + count);
+
+ mRowGapStart -= count;
+ mRowGapLength += count;
+
+ // TODO: Reclaim memory when the new height is much smaller
+ // than the allocated size.
+ }
+
+ /**
+ * Returns the number of rows in the PackedIntVector. This number
+ * will change as rows are inserted and deleted.
+ *
+ * @return the number of rows.
+ */
+ public int size() {
+ return mRows - mRowGapLength;
+ }
+
+ /**
+ * Returns the width of the PackedIntVector. This number is set
+ * at construction and will not change.
+ *
+ * @return the number of columns.
+ */
+ public int width() {
+ return mColumns;
+ }
+
+ /**
+ * Grows the value and gap arrays to be large enough to store at least
+ * one more than the current number of rows.
+ */
+ private final void growBuffer() {
+ final int columns = mColumns;
+ int newsize = size() + 1;
+ newsize = ArrayUtils.idealIntArraySize(newsize * columns) / columns;
+ int[] newvalues = new int[newsize * columns];
+
+ final int[] valuegap = mValueGap;
+ final int rowgapstart = mRowGapStart;
+
+ int after = mRows - (rowgapstart + mRowGapLength);
+
+ if (mValues != null) {
+ System.arraycopy(mValues, 0, newvalues, 0, columns * rowgapstart);
+ System.arraycopy(mValues, (mRows - after) * columns,
+ newvalues, (newsize - after) * columns,
+ after * columns);
+ }
+
+ for (int i = 0; i < columns; i++) {
+ if (valuegap[i] >= rowgapstart) {
+ valuegap[i] += newsize - mRows;
+
+ if (valuegap[i] < rowgapstart) {
+ valuegap[i] = rowgapstart;
+ }
+ }
+ }
+
+ mRowGapLength += newsize - mRows;
+ mRows = newsize;
+ mValues = newvalues;
+ }
+
+ /**
+ * Moves the gap in the values of the specified column to begin at
+ * the specified row.
+ */
+ private final void moveValueGapTo(int column, int where) {
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int columns = mColumns;
+
+ if (where == valuegap[column]) {
+ return;
+ } else if (where > valuegap[column]) {
+ for (int i = valuegap[column]; i < where; i++) {
+ values[i * columns + column] += valuegap[column + columns];
+ }
+ } else /* where < valuegap[column] */ {
+ for (int i = where; i < valuegap[column]; i++) {
+ values[i * columns + column] -= valuegap[column + columns];
+ }
+ }
+
+ valuegap[column] = where;
+ }
+
+ /**
+ * Moves the gap in the row indices to begin at the specified row.
+ */
+ private final void moveRowGapTo(int where) {
+ if (where == mRowGapStart) {
+ return;
+ } else if (where > mRowGapStart) {
+ int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength);
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = gapend; i < gapend + moving; i++) {
+ int destrow = i - gapend + mRowGapStart;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ } else /* where < mRowGapStart */ {
+ int moving = mRowGapStart - where;
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = where + moving - 1; i >= where; i--) {
+ int destrow = i - where + gapend - moving;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ }
+
+ mRowGapStart = where;
+ }
+}
diff --git a/core/java/android/text/PackedObjectVector.java b/core/java/android/text/PackedObjectVector.java
new file mode 100644
index 0000000..a29df09
--- /dev/null
+++ b/core/java/android/text/PackedObjectVector.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+import com.android.internal.util.ArrayUtils;
+
+class PackedObjectVector<E>
+{
+ private int mColumns;
+ private int mRows;
+
+ private int mRowGapStart;
+ private int mRowGapLength;
+
+ private Object[] mValues;
+
+ public
+ PackedObjectVector(int columns)
+ {
+ mColumns = columns;
+ mRows = ArrayUtils.idealIntArraySize(0) / mColumns;
+
+ mRowGapStart = 0;
+ mRowGapLength = mRows;
+
+ mValues = new Object[mRows * mColumns];
+ }
+
+ public E
+ getValue(int row, int column)
+ {
+ if (row >= mRowGapStart)
+ row += mRowGapLength;
+
+ Object value = mValues[row * mColumns + column];
+
+ return (E) value;
+ }
+
+ public void
+ setValue(int row, int column, E value)
+ {
+ if (row >= mRowGapStart)
+ row += mRowGapLength;
+
+ mValues[row * mColumns + column] = value;
+ }
+
+ public void
+ insertAt(int row, E[] values)
+ {
+ moveRowGapTo(row);
+
+ if (mRowGapLength == 0)
+ growBuffer();
+
+ mRowGapStart++;
+ mRowGapLength--;
+
+ if (values == null)
+ for (int i = 0; i < mColumns; i++)
+ setValue(row, i, null);
+ else
+ for (int i = 0; i < mColumns; i++)
+ setValue(row, i, values[i]);
+ }
+
+ public void
+ deleteAt(int row, int count)
+ {
+ moveRowGapTo(row + count);
+
+ mRowGapStart -= count;
+ mRowGapLength += count;
+
+ if (mRowGapLength > size() * 2)
+ {
+ // dump();
+ // growBuffer();
+ }
+ }
+
+ public int
+ size()
+ {
+ return mRows - mRowGapLength;
+ }
+
+ public int
+ width()
+ {
+ return mColumns;
+ }
+
+ private void
+ growBuffer()
+ {
+ int newsize = size() + 1;
+ newsize = ArrayUtils.idealIntArraySize(newsize * mColumns) / mColumns;
+ Object[] newvalues = new Object[newsize * mColumns];
+
+ int after = mRows - (mRowGapStart + mRowGapLength);
+
+ System.arraycopy(mValues, 0, newvalues, 0, mColumns * mRowGapStart);
+ System.arraycopy(mValues, (mRows - after) * mColumns, newvalues, (newsize - after) * mColumns, after * mColumns);
+
+ mRowGapLength += newsize - mRows;
+ mRows = newsize;
+ mValues = newvalues;
+ }
+
+ private void
+ moveRowGapTo(int where)
+ {
+ if (where == mRowGapStart)
+ return;
+
+ if (where > mRowGapStart)
+ {
+ int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength);
+
+ for (int i = mRowGapStart + mRowGapLength; i < mRowGapStart + mRowGapLength + moving; i++)
+ {
+ int destrow = i - (mRowGapStart + mRowGapLength) + mRowGapStart;
+
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ mValues[destrow * mColumns + j] = val;
+ }
+ }
+ }
+ else /* where < mRowGapStart */
+ {
+ int moving = mRowGapStart - where;
+
+ for (int i = where + moving - 1; i >= where; i--)
+ {
+ int destrow = i - where + mRowGapStart + mRowGapLength - moving;
+
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ mValues[destrow * mColumns + j] = val;
+ }
+ }
+ }
+
+ mRowGapStart = where;
+ }
+
+ public void // XXX
+ dump()
+ {
+ for (int i = 0; i < mRows; i++)
+ {
+ for (int j = 0; j < mColumns; j++)
+ {
+ Object val = mValues[i * mColumns + j];
+
+ if (i < mRowGapStart || i >= mRowGapStart + mRowGapLength)
+ System.out.print(val + " ");
+ else
+ System.out.print("(" + val + ") ");
+ }
+
+ System.out.print(" << \n");
+ }
+
+ System.out.print("-----\n\n");
+ }
+}
diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java
new file mode 100644
index 0000000..0f4916a
--- /dev/null
+++ b/core/java/android/text/Selection.java
@@ -0,0 +1,426 @@
+/*
+ * 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;
+
+
+/**
+ * Utility class for manipulating cursors and selections in CharSequences.
+ * A cursor is a selection where the start and end are at the same offset.
+ */
+public class Selection {
+ private Selection() { /* cannot be instantiated */ }
+
+ /*
+ * Retrieving the selection
+ */
+
+ /**
+ * Return the offset of the selection anchor or cursor, or -1 if
+ * there is no selection or cursor.
+ */
+ public static final int getSelectionStart(CharSequence text) {
+ if (text instanceof Spanned)
+ return ((Spanned) text).getSpanStart(SELECTION_START);
+ else
+ return -1;
+ }
+
+ /**
+ * Return the offset of the selection edge or cursor, or -1 if
+ * there is no selection or cursor.
+ */
+ public static final int getSelectionEnd(CharSequence text) {
+ if (text instanceof Spanned)
+ return ((Spanned) text).getSpanStart(SELECTION_END);
+ else
+ return -1;
+ }
+
+ /*
+ * Setting the selection
+ */
+
+ // private static int pin(int value, int min, int max) {
+ // return value < min ? 0 : (value > max ? max : value);
+ // }
+
+ /**
+ * Set the selection anchor to <code>start</code> and the selection edge
+ * to <code>stop</code>.
+ */
+ public static void setSelection(Spannable text, int start, int stop) {
+ // int len = text.length();
+ // start = pin(start, 0, len); XXX remove unless we really need it
+ // stop = pin(stop, 0, len);
+
+ int ostart = getSelectionStart(text);
+ int oend = getSelectionEnd(text);
+
+ if (ostart != start || oend != stop) {
+ text.setSpan(SELECTION_START, start, start,
+ Spanned.SPAN_POINT_POINT);
+ text.setSpan(SELECTION_END, stop, stop,
+ Spanned.SPAN_POINT_POINT);
+ }
+ }
+
+ /**
+ * Move the cursor to offset <code>index</code>.
+ */
+ public static final void setSelection(Spannable text, int index) {
+ setSelection(text, index, index);
+ }
+
+ /**
+ * Select the entire text.
+ */
+ public static final void selectAll(Spannable text) {
+ setSelection(text, 0, text.length());
+ }
+
+ /**
+ * Move the selection edge to offset <code>index</code>.
+ */
+ public static final void extendSelection(Spannable text, int index) {
+ if (text.getSpanStart(SELECTION_END) != index)
+ text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
+ }
+
+ /**
+ * Remove the selection or cursor, if any, from the text.
+ */
+ public static final void removeSelection(Spannable text) {
+ text.removeSpan(SELECTION_START);
+ text.removeSpan(SELECTION_END);
+ }
+
+ /*
+ * Moving the selection within the layout
+ */
+
+ /**
+ * Move the cursor to the buffer offset physically above the current
+ * offset, or return false if the cursor is already on the top line.
+ */
+ public static boolean moveUp(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, min);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically below the current
+ * offset, or return false if the cursor is already on the bottom line.
+ */
+ public static boolean moveDown(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, max);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the left of
+ * the current offset, or return false if the cursor is already
+ * at the left edge of the line and there is not another line to move it to.
+ */
+ public static boolean moveLeft(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, -1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the right of
+ * the current offset, or return false if the cursor is already at
+ * at the right edge of the line and there is not another line
+ * to move it to.
+ */
+ public static boolean moveRight(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, 1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically above
+ * the current selection end.
+ */
+ public static boolean extendUp(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != 0) {
+ extendSelection(text, 0);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically below
+ * the current selection end.
+ */
+ public static boolean extendDown(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != text.length()) {
+ extendSelection(text, text.length());
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the left of
+ * the current selection end.
+ */
+ public static boolean extendLeft(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the right of
+ * the current selection end.
+ */
+ public static boolean extendRight(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ public static boolean extendToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean extendToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ setSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ setSelection(text, where);
+ return true;
+ }
+
+ private static int findEdge(Spannable text, Layout layout, int dir) {
+ int pt = getSelectionEnd(text);
+ int line = layout.getLineForOffset(pt);
+ int pdir = layout.getParagraphDirection(line);
+
+ if (dir * pdir < 0) {
+ return layout.getLineStart(line);
+ } else {
+ int end = layout.getLineEnd(line);
+
+ if (line == layout.getLineCount() - 1)
+ return end;
+ else
+ return end - 1;
+ }
+ }
+
+ private static int chooseHorizontal(Layout layout, int direction,
+ int off1, int off2) {
+ int line1 = layout.getLineForOffset(off1);
+ int line2 = layout.getLineForOffset(off2);
+
+ if (line1 == line2) {
+ // same line, so it goes by pure physical direction
+
+ float h1 = layout.getPrimaryHorizontal(off1);
+ float h2 = layout.getPrimaryHorizontal(off2);
+
+ if (direction < 0) {
+ // to left
+
+ if (h1 < h2)
+ return off1;
+ else
+ return off2;
+ } else {
+ // to right
+
+ if (h1 > h2)
+ return off1;
+ else
+ return off2;
+ }
+ } else {
+ // different line, so which line is "left" and which is "right"
+ // depends upon the directionality of the text
+
+ // This only checks at one end, but it's not clear what the
+ // right thing to do is if the ends don't agree. Even if it
+ // is wrong it should still not be too bad.
+ int line = layout.getLineForOffset(off1);
+ int textdir = layout.getParagraphDirection(line);
+
+ if (textdir == direction)
+ return Math.max(off1, off2);
+ else
+ return Math.min(off1, off2);
+ }
+ }
+
+ /*
+ * Public constants
+ */
+
+ public static final Object SELECTION_START = new Object();
+ public static final Object SELECTION_END = new Object();
+}
diff --git a/core/java/android/text/SpanWatcher.java b/core/java/android/text/SpanWatcher.java
new file mode 100644
index 0000000..f99882a
--- /dev/null
+++ b/core/java/android/text/SpanWatcher.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * When an object of this type is attached to a Spannable, its methods
+ * will be called to notify it that other markup objects have been
+ * added, changed, or removed.
+ */
+public interface SpanWatcher {
+ /**
+ * This method is called to notify you that the specified object
+ * has been attached to the specified range of the text.
+ */
+ public void onSpanAdded(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been detached from the specified range of the text.
+ */
+ public void onSpanRemoved(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been relocated from the range <code>ostart&hellip;oend</code>
+ * to the new range <code>nstart&hellip;nend</code> of the text.
+ */
+ public void onSpanChanged(Spannable text, Object what, int ostart, int oend,
+ int nstart, int nend);
+}
diff --git a/core/java/android/text/Spannable.java b/core/java/android/text/Spannable.java
new file mode 100644
index 0000000..ae5d356
--- /dev/null
+++ b/core/java/android/text/Spannable.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * This is the interface for text to which markup objects can be
+ * attached and detached. Not all Spannable classes have mutable text;
+ * see {@link Editable} for that.
+ */
+public interface Spannable
+extends Spanned
+{
+ /**
+ * Attach the specified markup object to the range <code>start&hellip;end</code>
+ * of the text, or move the object to that range if it was already
+ * attached elsewhere. See {@link Spanned} for an explanation of
+ * what the flags mean. The object can be one that has meaning only
+ * within your application, or it can be one that the text system will
+ * use to affect text display or behavior. Some noteworthy ones are
+ * the subclasses of {@link android.text.style.CharacterStyle} and
+ * {@link android.text.style.ParagraphStyle}, and
+ * {@link android.text.TextWatcher} and
+ * {@link android.text.SpanWatcher}.
+ */
+ public void setSpan(Object what, int start, int end, int flags);
+
+ /**
+ * Remove the specified object from the range of text to which it
+ * was attached, if any. It is OK to remove an object that was never
+ * attached in the first place.
+ */
+ public void removeSpan(Object what);
+
+ /**
+ * Factory used by TextView to create new Spannables. You can subclass
+ * it to provide something other than SpannableString.
+ */
+ public static class Factory {
+ private static Spannable.Factory sInstance = new Spannable.Factory();
+
+ /**
+ * Returns the standard Spannable Factory.
+ */
+ public static Spannable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannableString from the specified CharSequence.
+ * You can override this to provide a different kind of Spannable.
+ */
+ public Spannable newSpannable(CharSequence source) {
+ return new SpannableString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/SpannableString.java b/core/java/android/text/SpannableString.java
new file mode 100644
index 0000000..56d0946
--- /dev/null
+++ b/core/java/android/text/SpannableString.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+
+/**
+ * This is the class for text whose content is immutable but to which
+ * markup objects can be attached and detached.
+ * For mutable text, see {@link SpannableStringBuilder}.
+ */
+public class SpannableString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spannable
+{
+ public SpannableString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannableString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public static SpannableString valueOf(CharSequence source) {
+ if (source instanceof SpannableString) {
+ return (SpannableString) source;
+ } else {
+ return new SpannableString(source);
+ }
+ }
+
+ public void setSpan(Object what, int start, int end, int flags) {
+ super.setSpan(what, start, end, flags);
+ }
+
+ public void removeSpan(Object what) {
+ super.removeSpan(what);
+ }
+
+ public final CharSequence subSequence(int start, int end) {
+ return new SpannableString(this, start, end);
+ }
+}
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
new file mode 100644
index 0000000..223ce2f
--- /dev/null
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -0,0 +1,1136 @@
+/*
+ * 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;
+
+import com.android.internal.util.ArrayUtils;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+import java.lang.reflect.Array;
+
+/**
+ * This is the class for text whose content and markup can both be changed.
+ */
+public class SpannableStringBuilder
+implements CharSequence, GetChars, Spannable, Editable, Appendable,
+ GraphicsOperations
+{
+ /**
+ * Create a new SpannableStringBuilder with empty contents
+ */
+ public SpannableStringBuilder() {
+ this("");
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text) {
+ this(text, 0, text.length());
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified slice of the specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text, int start, int end) {
+ int srclen = end - start;
+
+ int len = ArrayUtils.idealCharArraySize(srclen + 1);
+ mText = new char[len];
+ mGapStart = srclen;
+ mGapLength = len - srclen;
+
+ TextUtils.getChars(text, start, end, mText, 0);
+
+ mSpanCount = 0;
+ int alloc = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[alloc];
+ mSpanStarts = new int[alloc];
+ mSpanEnds = new int[alloc];
+ mSpanFlags = new int[alloc];
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]) - start;
+ int en = sp.getSpanEnd(spans[i]) - start;
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < 0)
+ st = 0;
+ if (st > end - start)
+ st = end - start;
+
+ if (en < 0)
+ en = 0;
+ if (en > end - start)
+ en = end - start;
+
+ setSpan(spans[i], st, en, fl);
+ }
+ }
+ }
+
+ public static SpannableStringBuilder valueOf(CharSequence source) {
+ if (source instanceof SpannableStringBuilder) {
+ return (SpannableStringBuilder) source;
+ } else {
+ return new SpannableStringBuilder(source);
+ }
+ }
+
+ /**
+ * Return the char at the specified offset within the buffer.
+ */
+ public char charAt(int where) {
+ int len = length();
+ if (where < 0) {
+ throw new IndexOutOfBoundsException("charAt: " + where + " < 0");
+ } else if (where >= len) {
+ throw new IndexOutOfBoundsException("charAt: " + where +
+ " >= length " + len);
+ }
+
+ if (where >= mGapStart)
+ return mText[where + mGapLength];
+ else
+ return mText[where];
+ }
+
+ /**
+ * Return the number of chars in the buffer.
+ */
+ public int length() {
+ return mText.length - mGapLength;
+ }
+
+ private void resizeFor(int size) {
+ int newlen = ArrayUtils.idealCharArraySize(size + 1);
+ char[] newtext = new char[newlen];
+
+ int after = mText.length - (mGapStart + mGapLength);
+
+ System.arraycopy(mText, 0, newtext, 0, mGapStart);
+ System.arraycopy(mText, mText.length - after,
+ newtext, newlen - after, after);
+
+ for (int i = 0; i < mSpanCount; i++) {
+ if (mSpanStarts[i] > mGapStart)
+ mSpanStarts[i] += newlen - mText.length;
+ if (mSpanEnds[i] > mGapStart)
+ mSpanEnds[i] += newlen - mText.length;
+ }
+
+ int oldlen = mText.length;
+ mText = newtext;
+ mGapLength += mText.length - oldlen;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+ }
+
+ private void moveGapTo(int where) {
+ if (where == mGapStart)
+ return;
+
+ boolean atend = (where == length());
+
+ if (where < mGapStart) {
+ int overlap = mGapStart - where;
+
+ System.arraycopy(mText, where,
+ mText, mGapStart + mGapLength - overlap, overlap);
+ } else /* where > mGapStart */ {
+ int overlap = where - mGapStart;
+
+ System.arraycopy(mText, where + mGapLength - overlap,
+ mText, mGapStart, overlap);
+ }
+
+ // XXX be more clever
+ for (int i = 0; i < mSpanCount; i++) {
+ int start = mSpanStarts[i];
+ int end = mSpanEnds[i];
+
+ if (start > mGapStart)
+ start -= mGapLength;
+ if (start > where)
+ start += mGapLength;
+ else if (start == where) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end -= mGapLength;
+ if (end > where)
+ end += mGapLength;
+ else if (end == where) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ end += mGapLength;
+ }
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ }
+
+ mGapStart = where;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
+ return replace(where, where, tb, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb) {
+ return replace(where, where, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder delete(int start, int end) {
+ SpannableStringBuilder ret = replace(start, end, "", 0, 0);
+
+ if (mGapLength > 2 * length())
+ resizeFor(length());
+
+ return ret; // == this
+ }
+
+ // Documentation from interface
+ public void clear() {
+ replace(0, length(), "", 0, 0);
+ }
+
+ // Documentation from interface
+ public void clearSpans() {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ Object what = mSpans[i];
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanCount = i;
+ mSpans[i] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ }
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text) {
+ int length = length();
+ return replace(length, length, text, 0, text.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text, int start, int end) {
+ int length = length();
+ return replace(length, length, text, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(char text) {
+ return append(String.valueOf(text));
+ }
+
+ private int change(int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ return change(true, start, end, tb, tbstart, tbend);
+ }
+
+ private int change(boolean notify, int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ checkRange("replace", start, end);
+ int ret = tbend - tbstart;
+ TextWatcher[] recipients = null;
+
+ if (notify)
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) {
+ int st = mSpanStarts[i];
+ if (st > mGapStart)
+ st -= mGapLength;
+
+ int en = mSpanEnds[i];
+ if (en > mGapStart)
+ en -= mGapLength;
+
+ int ost = st;
+ int oen = en;
+ int clen = length();
+
+ if (st > start && st <= end) {
+ for (st = end; st < clen; st++)
+ if (st > end && charAt(st - 1) == '\n')
+ break;
+ }
+
+ if (en > start && en <= end) {
+ for (en = end; en < clen; en++)
+ if (en > end && charAt(en - 1) == '\n')
+ break;
+ }
+
+ if (st != ost || en != oen)
+ setSpan(mSpans[i], st, en, mSpanFlags[i]);
+ }
+ }
+
+ moveGapTo(end);
+
+ if (tbend - tbstart >= mGapLength + (end - start))
+ resizeFor(mText.length - mGapLength +
+ tbend - tbstart - (end - start));
+
+ mGapStart += tbend - tbstart - (end - start);
+ mGapLength -= tbend - tbstart - (end - start);
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ TextUtils.getChars(tb, tbstart, tbend, mText, start);
+
+ if (tb instanceof Spanned) {
+ Spanned sp = (Spanned) tb;
+ Object[] spans = sp.getSpans(tbstart, tbend, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+
+ if (st < tbstart)
+ st = tbstart;
+ if (en > tbend)
+ en = tbend;
+
+ if (getSpanStart(spans[i]) < 0) {
+ setSpan(false, spans[i],
+ st - tbstart + start,
+ en - tbstart + start,
+ sp.getSpanFlags(spans[i]));
+ }
+ }
+ }
+
+ // no need for span fixup on pure insertion
+ if (tbend > tbstart && end - start == 0) {
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ boolean atend = (mGapStart + mGapLength == mText.length);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] >= start &&
+ mSpanStarts[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanStarts[i] = mGapStart + mGapLength;
+ else
+ mSpanStarts[i] = start;
+ }
+
+ if (mSpanEnds[i] >= start &&
+ mSpanEnds[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanEnds[i] = mGapStart + mGapLength;
+ else
+ mSpanEnds[i] = start;
+ }
+
+ // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE
+ // XXX send notification on removal
+
+ if (mSpanEnds[i] < mSpanStarts[i]) {
+ System.arraycopy(mSpans, i + 1,
+ mSpans, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanStarts, i + 1,
+ mSpanStarts, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanEnds, i + 1,
+ mSpanEnds, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanFlags, i + 1,
+ mSpanFlags, i, mSpanCount - (i + 1));
+
+ mSpanCount--;
+ }
+ }
+
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
+ return replace(start, end, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(final int start, final int end,
+ CharSequence tb, int tbstart, int tbend) {
+ int filtercount = mFilters.length;
+ for (int i = 0; i < filtercount; i++) {
+ CharSequence repl = mFilters[i].filter(tb, tbstart, tbend,
+ this, start, end);
+
+ if (repl != null) {
+ tb = repl;
+ tbstart = 0;
+ tbend = repl.length();
+ }
+ }
+
+ if (end == start && tbstart == tbend) {
+ return this;
+ }
+
+ if (end == start || tbstart == tbend) {
+ change(start, end, tb, tbstart, tbend);
+ } else {
+ int selstart = Selection.getSelectionStart(this);
+ int selend = Selection.getSelectionEnd(this);
+
+ // XXX just make the span fixups in change() do the right thing
+ // instead of this madness!
+
+ checkRange("replace", start, end);
+ moveGapTo(end);
+ TextWatcher[] recipients;
+
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ int origlen = end - start;
+
+ if (mGapLength < 2)
+ resizeFor(length() + 1);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] == mGapStart)
+ mSpanStarts[i]++;
+
+ if (mSpanEnds[i] == mGapStart)
+ mSpanEnds[i]++;
+ }
+
+ mText[mGapStart] = ' ';
+ mGapStart++;
+ mGapLength--;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ int oldlen = (end + 1) - start;
+
+ int inserted = change(false, start + 1, start + 1,
+ tb, tbstart, tbend);
+ change(false, start, start + 1, "", 0, 0);
+ change(false, start + inserted, start + inserted + oldlen - 1,
+ "", 0, 0);
+
+ /*
+ * Special case to keep the cursor in the same position
+ * if it was somewhere in the middle of the replaced region.
+ * If it was at the start or the end or crossing the whole
+ * replacement, it should already be where it belongs.
+ * TODO: Is there some more general mechanism that could
+ * accomplish this?
+ */
+ if (selstart > start && selstart < end) {
+ long off = selstart - start;
+
+ off = off * inserted / (end - start);
+ selstart = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_START, selstart, selstart,
+ Spanned.SPAN_POINT_POINT);
+ }
+ if (selend > start && selend < end) {
+ long off = selend - start;
+
+ off = off * inserted / (end - start);
+ selend = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_END, selend, selend,
+ Spanned.SPAN_POINT_POINT);
+ }
+
+ sendTextChange(recipients, start, origlen, inserted);
+ sendTextHasChanged(recipients);
+ }
+ return this;
+ }
+
+ /**
+ * Mark the specified range of text with the specified object.
+ * The flags determine how the span will behave when text is
+ * inserted at the start or end of the span's range.
+ */
+ public void setSpan(Object what, int start, int end, int flags) {
+ setSpan(true, what, start, end, flags);
+ }
+
+ private void setSpan(boolean send,
+ Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & START_MASK) == (PARAGRAPH << START_SHIFT)) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary");
+ }
+ }
+
+ if ((flags & END_MASK) == PARAGRAPH) {
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary");
+ }
+ }
+
+ if (start > mGapStart)
+ start += mGapLength;
+ else if (start == mGapStart) {
+ int flag = (flags & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && start == length()))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end += mGapLength;
+ else if (end == mGapStart) {
+ int flag = (flags & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && end == length()))
+ end += mGapLength;
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ mSpanFlags[i] = flags;
+
+ if (send)
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newspans = new Object[newsize];
+ int[] newspanstarts = new int[newsize];
+ int[] newspanends = new int[newsize];
+ int[] newspanflags = new int[newsize];
+
+ System.arraycopy(mSpans, 0, newspans, 0, mSpanCount);
+ System.arraycopy(mSpanStarts, 0, newspanstarts, 0, mSpanCount);
+ System.arraycopy(mSpanEnds, 0, newspanends, 0, mSpanCount);
+ System.arraycopy(mSpanFlags, 0, newspanflags, 0, mSpanCount);
+
+ mSpans = newspans;
+ mSpanStarts = newspanstarts;
+ mSpanEnds = newspanends;
+ mSpanFlags = newspanflags;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanStarts[mSpanCount] = start;
+ mSpanEnds[mSpanCount] = end;
+ mSpanFlags[mSpanCount] = flags;
+ mSpanCount++;
+
+ if (send)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /**
+ * Remove the specified markup object from the buffer.
+ */
+ public void removeSpan(Object what) {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ int count = mSpanCount - (i + 1);
+
+ System.arraycopy(mSpans, i + 1, mSpans, i, count);
+ System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
+ System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
+ System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
+
+ mSpanCount--;
+ mSpans[mSpanCount] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Return the buffer offset of the beginning of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanStarts[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the buffer offset of the end of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanEnds[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the flags of the end of the specified
+ * markup object, or 0 if it is not attached to this buffer.
+ */
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return mSpanFlags[i];
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Return an array of the spans of the specified type that overlap
+ * the specified range of the buffer. The kind may be Object.class to get
+ * a list of all the spans regardless of type.
+ */
+ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+ int spanCount = mSpanCount;
+ Object[] spans = mSpans;
+ int[] starts = mSpanStarts;
+ int[] ends = mSpanEnds;
+ int[] flags = mSpanFlags;
+ int gapstart = mGapStart;
+ int gaplen = mGapLength;
+
+ int count = 0;
+ Object[] ret = null;
+ Object ret1 = null;
+
+ for (int i = 0; i < spanCount; i++) {
+ int spanStart = starts[i];
+ int spanEnd = ends[i];
+
+ if (spanStart > gapstart) {
+ spanStart -= gaplen;
+ }
+ if (spanEnd > gapstart) {
+ spanEnd -= gaplen;
+ }
+
+ if (spanStart > queryEnd) {
+ continue;
+ }
+ if (spanEnd < queryStart) {
+ continue;
+ }
+
+ if (spanStart != spanEnd && queryStart != queryEnd) {
+ if (spanStart == queryEnd)
+ continue;
+ if (spanEnd == queryStart)
+ continue;
+ }
+
+ if (kind != null && !kind.isInstance(spans[i])) {
+ continue;
+ }
+
+ if (count == 0) {
+ ret1 = spans[i];
+ count++;
+ } else {
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
+ ret[0] = ret1;
+ }
+
+ int prio = flags[i] & SPAN_PRIORITY;
+ if (prio != 0) {
+ int j;
+
+ for (j = 0; j < count; j++) {
+ int p = getSpanFlags(ret[j]) & SPAN_PRIORITY;
+
+ if (prio > p) {
+ break;
+ }
+ }
+
+ System.arraycopy(ret, j, ret, j + 1, count - j);
+ ret[j] = spans[i];
+ count++;
+ } else {
+ ret[count++] = spans[i];
+ }
+ }
+ }
+
+ if (count == 0) {
+ return (T[]) ArrayUtils.emptyArray(kind);
+ }
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, 1);
+ ret[0] = ret1;
+ return (T[]) ret;
+ }
+ if (count == ret.length) {
+ return (T[]) ret;
+ }
+
+ Object[] nret = (Object[]) Array.newInstance(kind, count);
+ System.arraycopy(ret, 0, nret, 0, count);
+ return (T[]) nret;
+ }
+
+ /**
+ * Return the next offset after <code>start</code> but less than or
+ * equal to <code>limit</code> where a span of the specified type
+ * begins or ends.
+ */
+ public int nextSpanTransition(int start, int limit, Class kind) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] starts = mSpanStarts;
+ int[] ends = mSpanEnds;
+ int gapstart = mGapStart;
+ int gaplen = mGapLength;
+
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ for (int i = 0; i < count; i++) {
+ int st = starts[i];
+ int en = ends[i];
+
+ if (st > gapstart)
+ st -= gaplen;
+ if (en > gapstart)
+ en -= gaplen;
+
+ if (st > start && st < limit && kind.isInstance(spans[i]))
+ limit = st;
+ if (en > start && en < limit && kind.isInstance(spans[i]))
+ limit = en;
+ }
+
+ return limit;
+ }
+
+ /**
+ * Return a new CharSequence containing a copy of the specified
+ * range of this buffer, including the overlapping spans.
+ */
+ public CharSequence subSequence(int start, int end) {
+ return new SpannableStringBuilder(this, start, end);
+ }
+
+ /**
+ * Copy the specified range of chars from this buffer into the
+ * specified array, beginning at the specified offset.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ checkRange("getChars", start, end);
+
+ if (end <= mGapStart) {
+ System.arraycopy(mText, start, dest, destoff, end - start);
+ } else if (start >= mGapStart) {
+ System.arraycopy(mText, start + mGapLength,
+ dest, destoff, end - start);
+ } else {
+ System.arraycopy(mText, start, dest, destoff, mGapStart - start);
+ System.arraycopy(mText, mGapStart + mGapLength,
+ dest, destoff + (mGapStart - start),
+ end - mGapStart);
+ }
+ }
+
+ /**
+ * Return a String containing a copy of the chars in this buffer.
+ */
+ public String toString() {
+ int len = length();
+ char[] buf = new char[len];
+
+ getChars(0, len, buf, 0);
+ return new String(buf);
+ }
+
+ private TextWatcher[] sendTextWillChange(int start, int before, int after) {
+ TextWatcher[] recip = getSpans(start, start + before, TextWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].beforeTextChanged(this, start, before, after);
+ }
+
+ return recip;
+ }
+
+ private void sendTextChange(TextWatcher[] recip, int start, int before,
+ int after) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onTextChanged(this, start, before, after);
+ }
+ }
+
+ private void sendTextHasChanged(TextWatcher[] recip) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].afterTextChanged(this);
+ }
+ }
+
+ private void sendSpanAdded(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanAdded(this, what, start, end);
+ }
+ }
+
+ private void sendSpanRemoved(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanRemoved(this, what, start, end);
+ }
+ }
+
+ private void sendSpanChanged(Object what, int s, int e, int st, int en) {
+ SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en),
+ SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanChanged(this, what, s, e, st, en);
+ }
+ }
+
+ private static String region(int start, int end) {
+ return "(" + start + " ... " + end + ")";
+ }
+
+ private void checkRange(final String operation, int start, int end) {
+ if (end < start) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " has end before start");
+ }
+
+ int len = length();
+
+ if (start > len || end > len) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " ends beyond length " + len);
+ }
+
+ if (start < 0 || end < 0) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " starts before 0");
+ }
+ }
+
+ private boolean isprint(char c) { // XXX
+ if (c >= ' ' && c <= '~')
+ return true;
+ else
+ return false;
+ }
+
+/*
+ private static final int startFlag(int flag) {
+ return (flag >> 4) & 0x0F;
+ }
+
+ private static final int endFlag(int flag) {
+ return flag & 0x0F;
+ }
+
+ public void dump() { // XXX
+ for (int i = 0; i < mGapStart; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ for (int i = mGapStart; i < mGapStart + mGapLength; i++) {
+ System.out.print('|');
+ System.out.print('(');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(')');
+ }
+
+ for (int i = mGapStart + mGapLength; i < mText.length; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ System.out.print('\n');
+
+ for (int i = 0; i < mText.length + 1; i++) {
+ int found = 0;
+ int wfound = 0;
+
+ for (int j = 0; j < mSpanCount; j++) {
+ if (mSpanStarts[j] == i) {
+ found = 1;
+ wfound = j;
+ break;
+ }
+
+ if (mSpanEnds[j] == i) {
+ found = 2;
+ wfound = j;
+ break;
+ }
+ }
+
+ if (found == 1) {
+ if (startFlag(mSpanFlags[wfound]) == MARK)
+ System.out.print("( ");
+ if (startFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("< ");
+ else
+ System.out.print("[ ");
+ } else if (found == 2) {
+ if (endFlag(mSpanFlags[wfound]) == POINT)
+ System.out.print(") ");
+ if (endFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("> ");
+ else
+ System.out.print("] ");
+ } else {
+ System.out.print(" ");
+ }
+ }
+
+ System.out.print("\n");
+ }
+*/
+
+ /**
+ * Don't call this yourself -- exists for Canvas to use internally.
+ * {@hide}
+ */
+ public void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p) {
+ checkRange("drawText", start, end);
+
+ if (end <= mGapStart) {
+ c.drawText(mText, start, end - start, x, y, p);
+ } else if (start >= mGapStart) {
+ c.drawText(mText, start + mGapLength, end - start, x, y, p);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ c.drawText(buf, 0, end - start, x, y, p);
+ TextUtils.recycle(buf);
+ }
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public float measureText(int start, int end, Paint p) {
+ checkRange("measureText", start, end);
+
+ float ret;
+
+ if (end <= mGapStart) {
+ ret = p.measureText(mText, start, end - start);
+ } else if (start >= mGapStart) {
+ ret = p.measureText(mText, start + mGapLength, end - start);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.measureText(buf, 0, end - start);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p) {
+ checkRange("getTextWidths", start, end);
+
+ int ret;
+
+ if (end <= mGapStart) {
+ ret = p.getTextWidths(mText, start, end - start, widths);
+ } else if (start >= mGapStart) {
+ ret = p.getTextWidths(mText, start + mGapLength, end - start,
+ widths);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.getTextWidths(buf, 0, end - start, widths);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public void setFilters(InputFilter[] filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException();
+ }
+
+ mFilters = filters;
+ }
+
+ // Documentation from interface
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private InputFilter[] mFilters = NO_FILTERS;
+
+ private char[] mText;
+ private int mGapStart;
+ private int mGapLength;
+
+ private Object[] mSpans;
+ private int[] mSpanStarts;
+ private int[] mSpanEnds;
+ private int[] mSpanFlags;
+ private int mSpanCount;
+
+ private static final int MARK = 1;
+ private static final int POINT = 2;
+ private static final int PARAGRAPH = 3;
+
+ private static final int START_MASK = 0xF0;
+ private static final int END_MASK = 0x0F;
+ private static final int START_SHIFT = 4;
+}
diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java
new file mode 100644
index 0000000..0412285
--- /dev/null
+++ b/core/java/android/text/SpannableStringInternal.java
@@ -0,0 +1,372 @@
+/*
+ * 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;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.reflect.Array;
+
+/* package */ abstract class SpannableStringInternal
+{
+ /* package */ SpannableStringInternal(CharSequence source,
+ int start, int end) {
+ if (start == 0 && end == source.length())
+ mText = source.toString();
+ else
+ mText = source.toString().substring(start, end);
+
+ int initial = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[initial];
+ mSpanData = new int[initial * 3];
+
+ if (source instanceof Spanned) {
+ Spanned sp = (Spanned) source;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ setSpan(spans[i], st - start, en - start, fl);
+ }
+ }
+ }
+
+ public final int length() {
+ return mText.length();
+ }
+
+ public final char charAt(int i) {
+ return mText.charAt(i);
+ }
+
+ public final String toString() {
+ return mText;
+ }
+
+ /* subclasses must do subSequence() to preserve type */
+
+ public final void getChars(int start, int end, char[] dest, int off) {
+ mText.getChars(start, end, dest, off);
+ }
+
+ /* package */ void setSpan(Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary" +
+ " (" + start + " follows " + c + ")");
+ }
+
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary" +
+ " (" + end + " follows " + c + ")");
+ }
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ data[i * COLUMNS + START] = start;
+ data[i * COLUMNS + END] = end;
+ data[i * COLUMNS + FLAGS] = flags;
+
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newtags = new Object[newsize];
+ int[] newdata = new int[newsize * 3];
+
+ System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
+ System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);
+
+ mSpans = newtags;
+ mSpanData = newdata;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanData[mSpanCount * COLUMNS + START] = start;
+ mSpanData[mSpanCount * COLUMNS + END] = end;
+ mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
+ mSpanCount++;
+
+ if (this instanceof Spannable)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /* package */ void removeSpan(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ int c = count - (i + 1);
+
+ System.arraycopy(spans, i + 1, spans, i, c);
+ System.arraycopy(data, (i + 1) * COLUMNS,
+ data, i * COLUMNS, c * COLUMNS);
+
+ mSpanCount--;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + START];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + END];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + FLAGS];
+ }
+ }
+
+ return 0;
+ }
+
+ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
+ int count = 0;
+
+ int spanCount = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+ Object[] ret = null;
+ Object ret1 = null;
+
+ for (int i = 0; i < spanCount; i++) {
+ int spanStart = data[i * COLUMNS + START];
+ int spanEnd = data[i * COLUMNS + END];
+
+ if (spanStart > queryEnd) {
+ continue;
+ }
+ if (spanEnd < queryStart) {
+ continue;
+ }
+
+ if (spanStart != spanEnd && queryStart != queryEnd) {
+ if (spanStart == queryEnd) {
+ continue;
+ }
+ if (spanEnd == queryStart) {
+ continue;
+ }
+ }
+
+ if (kind != null && !kind.isInstance(spans[i])) {
+ continue;
+ }
+
+ if (count == 0) {
+ ret1 = spans[i];
+ count++;
+ } else {
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
+ ret[0] = ret1;
+ }
+
+ int prio = data[i * COLUMNS + FLAGS] & Spanned.SPAN_PRIORITY;
+ if (prio != 0) {
+ int j;
+
+ for (j = 0; j < count; j++) {
+ int p = getSpanFlags(ret[j]) & Spanned.SPAN_PRIORITY;
+
+ if (prio > p) {
+ break;
+ }
+ }
+
+ System.arraycopy(ret, j, ret, j + 1, count - j);
+ ret[j] = spans[i];
+ count++;
+ } else {
+ ret[count++] = spans[i];
+ }
+ }
+ }
+
+ if (count == 0) {
+ return (T[]) ArrayUtils.emptyArray(kind);
+ }
+ if (count == 1) {
+ ret = (Object[]) Array.newInstance(kind, 1);
+ ret[0] = ret1;
+ return (T[]) ret;
+ }
+ if (count == ret.length) {
+ return (T[]) ret;
+ }
+
+ Object[] nret = (Object[]) Array.newInstance(kind, count);
+ System.arraycopy(ret, 0, nret, 0, count);
+ return (T[]) nret;
+ }
+
+ public int nextSpanTransition(int start, int limit, Class kind) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ for (int i = 0; i < count; i++) {
+ int st = data[i * COLUMNS + START];
+ int en = data[i * COLUMNS + END];
+
+ if (st > start && st < limit && kind.isInstance(spans[i]))
+ limit = st;
+ if (en > start && en < limit && kind.isInstance(spans[i]))
+ limit = en;
+ }
+
+ return limit;
+ }
+
+ private void sendSpanAdded(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanAdded((Spannable) this, what, start, end);
+ }
+ }
+
+ private void sendSpanRemoved(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanRemoved((Spannable) this, what, start, end);
+ }
+ }
+
+ private void sendSpanChanged(Object what, int s, int e, int st, int en) {
+ SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en),
+ SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanChanged((Spannable) this, what, s, e, st, en);
+ }
+ }
+
+ private static String region(int start, int end) {
+ return "(" + start + " ... " + end + ")";
+ }
+
+ private void checkRange(final String operation, int start, int end) {
+ if (end < start) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " has end before start");
+ }
+
+ int len = length();
+
+ if (start > len || end > len) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " ends beyond length " + len);
+ }
+
+ if (start < 0 || end < 0) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " starts before 0");
+ }
+ }
+
+ private String mText;
+ private Object[] mSpans;
+ private int[] mSpanData;
+ private int mSpanCount;
+
+ /* package */ static final Object[] EMPTY = new Object[0];
+
+ private static final int START = 0;
+ private static final int END = 1;
+ private static final int FLAGS = 2;
+ private static final int COLUMNS = 3;
+}
diff --git a/core/java/android/text/Spanned.java b/core/java/android/text/Spanned.java
new file mode 100644
index 0000000..2b4b4d2
--- /dev/null
+++ b/core/java/android/text/Spanned.java
@@ -0,0 +1,160 @@
+/*
+ * 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;
+
+/**
+ * This is the interface for text that has markup objects attached to
+ * ranges of it. Not all text classes have mutable markup or text;
+ * see {@link Spannable} for mutable markup and {@link Editable} for
+ * mutable text.
+ */
+public interface Spanned
+extends CharSequence
+{
+ /**
+ * 0-length spans with type SPAN_MARK_MARK behave like text marks:
+ * they remain at their original offset when text is inserted
+ * at that offset.
+ */
+ public static final int SPAN_MARK_MARK = 0x11;
+ /**
+ * SPAN_MARK_POINT is a synonym for {@link #SPAN_INCLUSIVE_INCLUSIVE}.
+ */
+ public static final int SPAN_MARK_POINT = 0x12;
+ /**
+ * SPAN_POINT_MARK is a synonym for {@link #SPAN_EXCLUSIVE_EXCLUSIVE}.
+ */
+ public static final int SPAN_POINT_MARK = 0x21;
+
+ /**
+ * 0-length spans with type SPAN_POINT_POINT behave like cursors:
+ * they are pushed forward by the length of the insertion when text
+ * is inserted at their offset.
+ */
+ public static final int SPAN_POINT_POINT = 0x22;
+
+ /**
+ * SPAN_PARAGRAPH behaves like SPAN_INCLUSIVE_EXCLUSIVE
+ * (SPAN_MARK_MARK), except that if either end of the span is
+ * at the end of the buffer, that end behaves like _POINT
+ * instead (so SPAN_INCLUSIVE_INCLUSIVE if it starts in the
+ * middle and ends at the end, or SPAN_EXCLUSIVE_INCLUSIVE
+ * if it both starts and ends at the end).
+ * <p>
+ * Its endpoints must be the start or end of the buffer or
+ * immediately after a \n character, and if the \n
+ * that anchors it is deleted, the endpoint is pulled to the
+ * next \n that follows in the buffer (or to the end of
+ * the buffer).
+ */
+ public static final int SPAN_PARAGRAPH = 0x33;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their starting point but not at their
+ * ending point. When 0-length, they behave like marks.
+ */
+ public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;
+
+ /**
+ * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
+ * to include text inserted at either their starting or ending point.
+ */
+ public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;
+
+ /**
+ * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand
+ * to include text inserted at either their starting or ending point.
+ * They can never have a length of 0 and are automatically removed
+ * from the buffer if all the text they cover is removed.
+ */
+ public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their ending point but not at their
+ * starting point. When 0-length, they behave like points.
+ */
+ public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;
+
+ /**
+ * The bits numbered SPAN_USER_SHIFT and above are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER_SHIFT = 24;
+ /**
+ * The bits specified by the SPAN_USER bitfield are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER = 0xFFFFFFFF << SPAN_USER_SHIFT;
+
+ /**
+ * The bits numbered just above SPAN_PRIORITY_SHIFT determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY_SHIFT = 16;
+ /**
+ * The bits specified by the SPAN_PRIORITY bitmap determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY = 0xFF << SPAN_PRIORITY_SHIFT;
+
+ /**
+ * Return an array of the markup objects attached to the specified
+ * slice of this CharSequence and whose type is the specified type
+ * or a subclass of it. Specify Object.class for the type if you
+ * want all the objects regardless of type.
+ */
+ public <T> T[] getSpans(int start, int end, Class<T> type);
+
+ /**
+ * Return the beginning of the range of text to which the specified
+ * markup object is attached, or -1 if the object is not attached.
+ */
+ public int getSpanStart(Object tag);
+
+ /**
+ * Return the end of the range of text to which the specified
+ * markup object is attached, or -1 if the object is not attached.
+ */
+ public int getSpanEnd(Object tag);
+
+ /**
+ * Return the flags that were specified when {@link Spannable#setSpan} was
+ * used to attach the specified markup object, or 0 if the specified
+ * object has not been attached.
+ */
+ public int getSpanFlags(Object tag);
+
+ /**
+ * Return the first offset greater than or equal to <code>start</code>
+ * where a markup object of class <code>type</code> begins or ends,
+ * or <code>limit</code> if there are no starts or ends greater than or
+ * equal to <code>start</code> but less than <code>limit</code>. Specify
+ * <code>null</code> or Object.class for the type if you want every
+ * transition regardless of type.
+ */
+ public int nextSpanTransition(int start, int limit, Class type);
+}
diff --git a/core/java/android/text/SpannedString.java b/core/java/android/text/SpannedString.java
new file mode 100644
index 0000000..afed221
--- /dev/null
+++ b/core/java/android/text/SpannedString.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+
+/**
+ * This is the class for text whose content and markup are immutable.
+ * For mutable markup, see {@link SpannableString}; for mutable text,
+ * see {@link SpannableStringBuilder}.
+ */
+public final class SpannedString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spanned
+{
+ public SpannedString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannedString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return new SpannedString(this, start, end);
+ }
+
+ public static SpannedString valueOf(CharSequence source) {
+ if (source instanceof SpannedString) {
+ return (SpannedString) source;
+ } else {
+ return new SpannedString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
new file mode 100644
index 0000000..2d18575
--- /dev/null
+++ b/core/java/android/text/StaticLayout.java
@@ -0,0 +1,1118 @@
+/*
+ * 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;
+
+import android.graphics.Paint;
+import com.android.internal.util.ArrayUtils;
+import android.util.Log;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+
+/**
+ * StaticLayout is a Layout for text that will not be edited after it
+ * is laid out. Use {@link DynamicLayout} for text that may change.
+ * <p>This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or would be tempted to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.</p>
+ */
+public class
+StaticLayout
+extends Layout
+{
+ public StaticLayout(CharSequence source, TextPaint paint,
+ int width,
+ Alignment align, float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, 0, source.length(), paint, width, align,
+ spacingmult, spacingadd, includepad);
+ }
+
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad) {
+ this(source, bufstart, bufend, paint, outerwidth, align,
+ spacingmult, spacingadd, includepad, null, 0);
+ }
+
+ public StaticLayout(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad,
+ TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
+ super((ellipsize == null)
+ ? source
+ : (source instanceof Spanned)
+ ? new SpannedEllipsizer(source)
+ : new Ellipsizer(source),
+ paint, outerwidth, align, spacingmult, spacingadd);
+
+ /*
+ * This is annoying, but we can't refer to the layout until
+ * superclass construction is finished, and the superclass
+ * constructor wants the reference to the display text.
+ *
+ * This will break if the superclass constructor ever actually
+ * cares about the content instead of just holding the reference.
+ */
+ if (ellipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = ellipsizedWidth;
+ e.mMethod = ellipsize;
+ mEllipsizedWidth = ellipsizedWidth;
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ } else {
+ mColumns = COLUMNS_NORMAL;
+ mEllipsizedWidth = outerwidth;
+ }
+
+ mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mLineDirections = new Directions[
+ ArrayUtils.idealIntArraySize(2 * mColumns)];
+
+ generate(source, bufstart, bufend, paint, outerwidth, align,
+ spacingmult, spacingadd, includepad, includepad,
+ ellipsize != null, ellipsizedWidth, ellipsize);
+
+ mChdirs = null;
+ mChs = null;
+ mWidths = null;
+ mFontMetricsInt = null;
+ }
+
+ /* package */ StaticLayout(boolean ellipsize) {
+ super(null, null, 0, null, 0, 0);
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
+ mLineDirections = new Directions[
+ ArrayUtils.idealIntArraySize(2 * mColumns)];
+ }
+
+ /* package */ void generate(CharSequence source, int bufstart, int bufend,
+ TextPaint paint, int outerwidth,
+ Alignment align,
+ float spacingmult, float spacingadd,
+ boolean includepad, boolean trackpad,
+ boolean breakOnlyAtSpaces,
+ float ellipsizedWidth, TextUtils.TruncateAt where) {
+ mLineCount = 0;
+
+ int v = 0;
+ boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
+
+ Paint.FontMetricsInt fm = mFontMetricsInt;
+ int[] choosehtv = null;
+
+ int end = TextUtils.indexOf(source, '\n', bufstart, bufend);
+ int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart;
+ boolean first = true;
+
+ if (mChdirs == null) {
+ mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)];
+ mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)];
+ mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)];
+ }
+
+ byte[] chdirs = mChdirs;
+ char[] chs = mChs;
+ float[] widths = mWidths;
+
+ AlteredCharSequence alter = null;
+ Spanned spanned = null;
+
+ if (source instanceof Spanned)
+ spanned = (Spanned) source;
+
+ int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
+
+ for (int start = bufstart; start <= bufend; start = end) {
+ if (first)
+ first = false;
+ else
+ end = TextUtils.indexOf(source, '\n', start, bufend);
+
+ if (end < 0)
+ end = bufend;
+ else
+ end++;
+
+ int firstwidth = outerwidth;
+ int restwidth = outerwidth;
+
+ LineHeightSpan[] chooseht = null;
+
+ if (spanned != null) {
+ LeadingMarginSpan[] sp;
+
+ sp = spanned.getSpans(start, end, LeadingMarginSpan.class);
+ for (int i = 0; i < sp.length; i++) {
+ firstwidth -= sp[i].getLeadingMargin(true);
+ restwidth -= sp[i].getLeadingMargin(false);
+ }
+
+ chooseht = spanned.getSpans(start, end, LineHeightSpan.class);
+
+ if (chooseht.length != 0) {
+ if (choosehtv == null ||
+ choosehtv.length < chooseht.length) {
+ choosehtv = new int[ArrayUtils.idealIntArraySize(
+ chooseht.length)];
+ }
+
+ for (int i = 0; i < chooseht.length; i++) {
+ int o = spanned.getSpanStart(chooseht[i]);
+
+ if (o < start) {
+ // starts in this layout, before the
+ // current paragraph
+
+ choosehtv[i] = getLineTop(getLineForOffset(o));
+ } else {
+ // starts in this paragraph
+
+ choosehtv[i] = v;
+ }
+ }
+ }
+ }
+
+ if (end - start > chdirs.length) {
+ chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)];
+ mChdirs = chdirs;
+ }
+ if (end - start > chs.length) {
+ chs = new char[ArrayUtils.idealCharArraySize(end - start)];
+ mChs = chs;
+ }
+ if ((end - start) * 2 > widths.length) {
+ widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)];
+ mWidths = widths;
+ }
+
+ TextUtils.getChars(source, start, end, chs, 0);
+ final int n = end - start;
+
+ boolean easy = true;
+ boolean altered = false;
+ int dir = DEFAULT_DIR; // XXX
+
+ for (int i = 0; i < n; i++) {
+ if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
+ easy = false;
+ break;
+ }
+ }
+
+ if (!easy) {
+ AndroidCharacter.getDirectionalities(chs, chdirs, end - start);
+
+ /*
+ * Determine primary paragraph direction
+ */
+
+ for (int j = start; j < end; j++) {
+ int d = chdirs[j - start];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT) {
+ dir = DIR_LEFT_TO_RIGHT;
+ break;
+ }
+ if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ dir = DIR_RIGHT_TO_LEFT;
+ break;
+ }
+ }
+
+ /*
+ * XXX Explicit overrides should go here
+ */
+
+ /*
+ * Weak type resolution
+ */
+
+ final byte SOR = dir == DIR_LEFT_TO_RIGHT ?
+ Character.DIRECTIONALITY_LEFT_TO_RIGHT :
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+
+ // dump(chdirs, n, "initial");
+
+ // W1 non spacing marks
+ for (int j = 0; j < n; j++) {
+ if (chdirs[j] == Character.NON_SPACING_MARK) {
+ if (j == 0)
+ chdirs[j] = SOR;
+ else
+ chdirs[j] = chdirs[j - 1];
+ }
+ }
+
+ // dump(chdirs, n, "W1");
+
+ // W2 european numbers
+ byte cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ cur = d;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER) {
+ if (cur ==
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
+ }
+ }
+
+ // dump(chdirs, n, "W2");
+
+ // W3 arabic letters
+ for (int j = 0; j < n; j++) {
+ if (chdirs[j] == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC)
+ chdirs[j] = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ }
+
+ // dump(chdirs, n, "W3");
+
+ // W4 single separator between numbers
+ for (int j = 1; j < n - 1; j++) {
+ byte d = chdirs[j];
+ byte prev = chdirs[j - 1];
+ byte next = chdirs[j + 1];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR) {
+ if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
+ next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ } else if (d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR) {
+ if (prev == Character.DIRECTIONALITY_EUROPEAN_NUMBER &&
+ next == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ if (prev == Character.DIRECTIONALITY_ARABIC_NUMBER &&
+ next == Character.DIRECTIONALITY_ARABIC_NUMBER)
+ chdirs[j] = Character.DIRECTIONALITY_ARABIC_NUMBER;
+ }
+ }
+
+ // dump(chdirs, n, "W4");
+
+ // W5 european number terminators
+ boolean adjacent = false;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ adjacent = true;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR && adjacent)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ else
+ adjacent = false;
+ }
+
+ //dump(chdirs, n, "W5");
+
+ // W5 european number terminators part 2,
+ // W6 separators and terminators
+ adjacent = false;
+ for (int j = n - 1; j >= 0; j--) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ adjacent = true;
+ else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR) {
+ if (adjacent)
+ chdirs[j] = Character.DIRECTIONALITY_EUROPEAN_NUMBER;
+ else
+ chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
+ }
+ else {
+ adjacent = false;
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
+ d == Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR ||
+ d == Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR ||
+ d == Character.DIRECTIONALITY_SEGMENT_SEPARATOR)
+ chdirs[j] = Character.DIRECTIONALITY_OTHER_NEUTRALS;
+ }
+ }
+
+ // dump(chdirs, n, "W6");
+
+ // W7 strong direction of european numbers
+ cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == SOR ||
+ d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT)
+ cur = d;
+
+ if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER)
+ chdirs[j] = cur;
+ }
+
+ // dump(chdirs, n, "W7");
+
+ // N1, N2 neutrals
+ cur = SOR;
+ for (int j = 0; j < n; j++) {
+ byte d = chdirs[j];
+
+ if (d == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ d == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ cur = d;
+ } else if (d == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
+ d == Character.DIRECTIONALITY_ARABIC_NUMBER) {
+ cur = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ } else {
+ byte dd = SOR;
+ int k;
+
+ for (k = j + 1; k < n; k++) {
+ dd = chdirs[k];
+
+ if (dd == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
+ dd == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ break;
+ }
+ if (dd == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
+ dd == Character.DIRECTIONALITY_ARABIC_NUMBER) {
+ dd = Character.DIRECTIONALITY_RIGHT_TO_LEFT;
+ break;
+ }
+ }
+
+ for (int y = j; y < k; y++) {
+ if (dd == cur)
+ chdirs[y] = cur;
+ else
+ chdirs[y] = SOR;
+ }
+
+ j = k - 1;
+ }
+ }
+
+ // dump(chdirs, n, "final");
+
+ // extra: enforce that all tabs go the primary direction
+
+ for (int j = 0; j < n; j++) {
+ if (chs[j] == '\t')
+ chdirs[j] = SOR;
+ }
+
+ // extra: enforce that object replacements go to the
+ // primary direction
+ // and that none of the underlying characters are treated
+ // as viable breakpoints
+
+ if (source instanceof Spanned) {
+ Spanned sp = (Spanned) source;
+ ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class);
+
+ for (int y = 0; y < spans.length; y++) {
+ int a = sp.getSpanStart(spans[y]);
+ int b = sp.getSpanEnd(spans[y]);
+
+ for (int x = a; x < b; x++) {
+ chdirs[x - start] = SOR;
+ chs[x - start] = '\uFFFC';
+ }
+ }
+ }
+
+ // Do mirroring for right-to-left segments
+
+ for (int i = 0; i < n; i++) {
+ if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
+ int j;
+
+ for (j = i; j < n; j++) {
+ if (chdirs[j] !=
+ Character.DIRECTIONALITY_RIGHT_TO_LEFT)
+ break;
+ }
+
+ if (AndroidCharacter.mirror(chs, i, j - i))
+ altered = true;
+
+ i = j - 1;
+ }
+ }
+ }
+
+ CharSequence sub;
+
+ if (altered) {
+ if (alter == null)
+ alter = AlteredCharSequence.make(source, chs, start, end);
+ else
+ alter.update(chs, start, end);
+
+ sub = alter;
+ } else {
+ sub = source;
+ }
+
+ int width = firstwidth;
+
+ float w = 0;
+ int here = start;
+
+ int ok = start;
+ float okwidth = w;
+ int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0;
+
+ int fit = start;
+ float fitwidth = w;
+ int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0;
+
+ boolean tab = false;
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ if (spanned == null)
+ next = end;
+ else
+ next = spanned.nextSpanTransition(i, end,
+ MetricAffectingSpan.
+ class);
+
+ if (spanned == null) {
+ paint.getTextWidths(sub, i, next, widths);
+ System.arraycopy(widths, 0, widths,
+ end - start + (i - start), next - i);
+
+ paint.getFontMetricsInt(fm);
+ } else {
+ mWorkPaint.baselineShift = 0;
+
+ Styled.getTextWidths(paint, mWorkPaint,
+ spanned, i, next,
+ widths, fm);
+ System.arraycopy(widths, 0, widths,
+ end - start + (i - start), next - i);
+
+ if (mWorkPaint.baselineShift < 0) {
+ fm.ascent += mWorkPaint.baselineShift;
+ fm.top += mWorkPaint.baselineShift;
+ } else {
+ fm.descent += mWorkPaint.baselineShift;
+ fm.bottom += mWorkPaint.baselineShift;
+ }
+ }
+
+ int fmtop = fm.top;
+ int fmbottom = fm.bottom;
+ int fmascent = fm.ascent;
+ int fmdescent = fm.descent;
+
+ if (false) {
+ StringBuilder sb = new StringBuilder();
+ for (int j = i; j < next; j++) {
+ sb.append(widths[j - start + (end - start)]);
+ sb.append(' ');
+ }
+
+ Log.e("text", sb.toString());
+ }
+
+ for (int j = i; j < next; j++) {
+ char c = chs[j - start];
+ float before = w;
+
+ switch (c) {
+ case '\n':
+ break;
+
+ case '\t':
+ w = Layout.nextTab(sub, start, end, w, null);
+ tab = true;
+ break;
+
+ default:
+ w += widths[j - start + (end - start)];
+ }
+
+ // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
+
+ if (w <= width) {
+ fitwidth = w;
+ fit = j + 1;
+
+ if (fmtop < fittop)
+ fittop = fmtop;
+ if (fmascent < fitascent)
+ fitascent = fmascent;
+ if (fmdescent > fitdescent)
+ fitdescent = fmdescent;
+ if (fmbottom > fitbottom)
+ fitbottom = fmbottom;
+
+ if (c == ' ' || c == '\t') {
+ okwidth = w;
+ ok = j + 1;
+
+ if (fittop < oktop)
+ oktop = fittop;
+ if (fitascent < okascent)
+ okascent = fitascent;
+ if (fitdescent > okdescent)
+ okdescent = fitdescent;
+ if (fitbottom > okbottom)
+ okbottom = fitbottom;
+ }
+ } else if (breakOnlyAtSpaces) {
+ if (ok != here) {
+ // Log.e("text", "output ok " + here + " to " +ok);
+ v = out(source,
+ here, ok,
+ okascent, okdescent, oktop, okbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ ok == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, okwidth,
+ paint);
+
+ here = ok;
+ } else {
+ // Act like it fit even though it didn't.
+
+ fitwidth = w;
+ fit = j + 1;
+
+ if (fmtop < fittop)
+ fittop = fmtop;
+ if (fmascent < fitascent)
+ fitascent = fmascent;
+ if (fmdescent > fitdescent)
+ fitdescent = fmdescent;
+ if (fmbottom > fitbottom)
+ fitbottom = fmbottom;
+ }
+ } else {
+ if (ok != here) {
+ // Log.e("text", "output ok " + here + " to " +ok);
+ v = out(source,
+ here, ok,
+ okascent, okdescent, oktop, okbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ ok == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, okwidth,
+ paint);
+
+ here = ok;
+ } else if (fit != here) {
+ // Log.e("text", "output fit " + here + " to " +fit);
+ v = out(source,
+ here, fit,
+ fitascent, fitdescent,
+ fittop, fitbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ fit == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, fitwidth,
+ paint);
+
+ here = fit;
+ } else {
+ // Log.e("text", "output one " + here + " to " +(here + 1));
+ measureText(paint, mWorkPaint,
+ source, here, here + 1, fm, tab,
+ null);
+
+ v = out(source,
+ here, here+1,
+ fm.ascent, fm.descent,
+ fm.top, fm.bottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ here + 1 == bufend, includepad,
+ trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth,
+ widths[here - start], paint);
+
+ here = here + 1;
+ }
+
+ if (here < i) {
+ j = next = here; // must remeasure
+ } else {
+ j = here - 1; // continue looping
+ }
+
+ ok = fit = here;
+ w = 0;
+ fitascent = fitdescent = fittop = fitbottom = 0;
+ okascent = okdescent = oktop = okbottom = 0;
+
+ width = restwidth;
+ }
+ }
+ }
+
+ if (end != here) {
+ if ((fittop | fitbottom | fitdescent | fitascent) == 0) {
+ paint.getFontMetricsInt(fm);
+
+ fittop = fm.top;
+ fitbottom = fm.bottom;
+ fitascent = fm.ascent;
+ fitdescent = fm.descent;
+ }
+
+ // Log.e("text", "output rest " + here + " to " + end);
+
+ v = out(source,
+ here, end, fitascent, fitdescent,
+ fittop, fitbottom,
+ v,
+ spacingmult, spacingadd, chooseht,
+ choosehtv, fm, tab,
+ needMultiply, start, chdirs, dir, easy,
+ end == bufend, includepad, trackpad,
+ widths, start, end - start,
+ where, ellipsizedWidth, w, paint);
+ }
+
+ start = end;
+
+ if (end == bufend)
+ break;
+ }
+
+ if (bufend == bufstart || source.charAt(bufend - 1) == '\n') {
+ // Log.e("text", "output last " + bufend);
+
+ paint.getFontMetricsInt(fm);
+
+ v = out(source,
+ bufend, bufend, fm.ascent, fm.descent,
+ fm.top, fm.bottom,
+ v,
+ spacingmult, spacingadd, null,
+ null, fm, false,
+ needMultiply, bufend, chdirs, DEFAULT_DIR, true,
+ true, includepad, trackpad,
+ widths, bufstart, 0,
+ where, ellipsizedWidth, 0, paint);
+ }
+ }
+
+/*
+ private static void dump(byte[] data, int count, String label) {
+ if (false) {
+ System.out.print(label);
+
+ for (int i = 0; i < count; i++)
+ System.out.print(" " + data[i]);
+
+ System.out.println();
+ }
+ }
+*/
+
+ private static int getFit(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text, int start, int end,
+ float wid) {
+ int high = end + 1, low = start - 1, guess;
+
+ while (high - low > 1) {
+ guess = (high + low) / 2;
+
+ if (measureText(paint, workPaint,
+ text, start, guess, null, true, null) > wid)
+ high = guess;
+ else
+ low = guess;
+ }
+
+ if (low < start)
+ return start;
+ else
+ return low;
+ }
+
+ private int out(CharSequence text, int start, int end,
+ int above, int below, int top, int bottom, int v,
+ float spacingmult, float spacingadd,
+ LineHeightSpan[] chooseht, int[] choosehtv,
+ Paint.FontMetricsInt fm, boolean tab,
+ boolean needMultiply, int pstart, byte[] chdirs,
+ int dir, boolean easy, boolean last,
+ boolean includepad, boolean trackpad,
+ float[] widths, int widstart, int widoff,
+ TextUtils.TruncateAt ellipsize, float ellipsiswidth,
+ float textwidth, TextPaint paint) {
+ int j = mLineCount;
+ int off = j * mColumns;
+ int want = off + mColumns + TOP;
+ int[] lines = mLines;
+
+ // Log.e("text", "line " + start + " to " + end + (last ? "===" : ""));
+
+ if (want >= lines.length) {
+ int nlen = ArrayUtils.idealIntArraySize(want + 1);
+ int[] grow = new int[nlen];
+ System.arraycopy(lines, 0, grow, 0, lines.length);
+ mLines = grow;
+ lines = grow;
+
+ Directions[] grow2 = new Directions[nlen];
+ System.arraycopy(mLineDirections, 0, grow2, 0,
+ mLineDirections.length);
+ mLineDirections = grow2;
+ }
+
+ if (chooseht != null) {
+ fm.ascent = above;
+ fm.descent = below;
+ fm.top = top;
+ fm.bottom = bottom;
+
+ for (int i = 0; i < chooseht.length; i++) {
+ chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm);
+ }
+
+ above = fm.ascent;
+ below = fm.descent;
+ top = fm.top;
+ bottom = fm.bottom;
+ }
+
+ if (j == 0) {
+ if (trackpad) {
+ mTopPadding = top - above;
+ }
+
+ if (includepad) {
+ above = top;
+ }
+ }
+ if (last) {
+ if (trackpad) {
+ mBottomPadding = bottom - below;
+ }
+
+ if (includepad) {
+ below = bottom;
+ }
+ }
+
+ int extra;
+
+ if (needMultiply) {
+ extra = (int) ((below - above) * (spacingmult - 1)
+ + spacingadd + 0.5);
+ } else {
+ extra = 0;
+ }
+
+ lines[off + START] = start;
+ lines[off + TOP] = v;
+ lines[off + DESCENT] = below + extra;
+
+ v += (below - above) + extra;
+ lines[off + mColumns + START] = end;
+ lines[off + mColumns + TOP] = v;
+
+ if (tab)
+ lines[off + TAB] |= TAB_MASK;
+
+ {
+ lines[off + DIR] |= dir << DIR_SHIFT;
+
+ int cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
+ int count = 0;
+
+ if (!easy) {
+ for (int k = start; k < end; k++) {
+ if (chdirs[k - pstart] != cur) {
+ count++;
+ cur = chdirs[k - pstart];
+ }
+ }
+ }
+
+ Directions linedirs;
+
+ if (count == 0) {
+ linedirs = DIRS_ALL_LEFT_TO_RIGHT;
+ } else {
+ short[] ld = new short[count + 1];
+
+ cur = Character.DIRECTIONALITY_LEFT_TO_RIGHT;
+ count = 0;
+ int here = start;
+
+ for (int k = start; k < end; k++) {
+ if (chdirs[k - pstart] != cur) {
+ // XXX check to make sure we don't
+ // overflow short
+ ld[count++] = (short) (k - here);
+ cur = chdirs[k - pstart];
+ here = k;
+ }
+ }
+
+ ld[count] = (short) (end - here);
+
+ if (count == 1 && ld[0] == 0) {
+ linedirs = DIRS_ALL_RIGHT_TO_LEFT;
+ } else {
+ linedirs = new Directions(ld);
+ }
+ }
+
+ mLineDirections[j] = linedirs;
+
+ if (ellipsize != null) {
+ calculateEllipsis(start, end, widths, widstart, widoff,
+ ellipsiswidth, ellipsize, j,
+ textwidth, paint);
+ }
+ }
+
+ mLineCount++;
+ return v;
+ }
+
+ private void calculateEllipsis(int linestart, int lineend,
+ float[] widths, int widstart, int widoff,
+ float avail, TextUtils.TruncateAt where,
+ int line, float textwidth, TextPaint paint) {
+ int len = lineend - linestart;
+
+ if (textwidth <= avail) {
+ // Everything fits!
+ mLines[mColumns * line + ELLIPSIS_START] = 0;
+ mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
+ return;
+ }
+
+ float ellipsiswid = paint.measureText("\u2026");
+ int ellipsisStart, ellipsisCount;
+
+ if (where == TextUtils.TruncateAt.START) {
+ float sum = 0;
+ int i;
+
+ for (i = len; i >= 0; i--) {
+ float w = widths[i - 1 + linestart - widstart + widoff];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ ellipsisStart = 0;
+ ellipsisCount = i;
+ } else if (where == TextUtils.TruncateAt.END) {
+ float sum = 0;
+ int i;
+
+ for (i = 0; i < len; i++) {
+ float w = widths[i + linestart - widstart + widoff];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ ellipsisStart = i;
+ ellipsisCount = len - i;
+ } else /* where = TextUtils.TruncateAt.MIDDLE */ {
+ float lsum = 0, rsum = 0;
+ int left = 0, right = len;
+
+ float ravail = (avail - ellipsiswid) / 2;
+ for (right = len; right >= 0; right--) {
+ float w = widths[right - 1 + linestart - widstart + widoff];
+
+ if (w + rsum > ravail) {
+ break;
+ }
+
+ rsum += w;
+ }
+
+ float lavail = avail - ellipsiswid - rsum;
+ for (left = 0; left < right; left++) {
+ float w = widths[left + linestart - widstart + widoff];
+
+ if (w + lsum > lavail) {
+ break;
+ }
+
+ lsum += w;
+ }
+
+ ellipsisStart = left;
+ ellipsisCount = right - left;
+ }
+
+ mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
+ mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
+ }
+
+ // Override the baseclass so we can directly access our members,
+ // rather than relying on member functions.
+ // The logic mirrors that of Layout.getLineForVertical
+ // FIXME: It may be faster to do a linear search for layouts without many lines.
+ public int getLineForVertical(int vertical) {
+ int high = mLineCount;
+ int low = -1;
+ int guess;
+ int[] lines = mLines;
+ while (high - low > 1) {
+ guess = (high + low) >> 1;
+ if (lines[mColumns * guess + TOP] > vertical){
+ high = guess;
+ } else {
+ low = guess;
+ }
+ }
+ if (low < 0) {
+ return 0;
+ } else {
+ return low;
+ }
+ }
+
+ public int getLineCount() {
+ return mLineCount;
+ }
+
+ public int getLineTop(int line) {
+ return mLines[mColumns * line + TOP];
+ }
+
+ public int getLineDescent(int line) {
+ return mLines[mColumns * line + DESCENT];
+ }
+
+ public int getLineStart(int line) {
+ return mLines[mColumns * line + START] & START_MASK;
+ }
+
+ public int getParagraphDirection(int line) {
+ return mLines[mColumns * line + DIR] >> DIR_SHIFT;
+ }
+
+ public boolean getLineContainsTab(int line) {
+ return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
+ }
+
+ public final Directions getLineDirections(int line) {
+ return mLineDirections[line];
+ }
+
+ public int getTopPadding() {
+ return mTopPadding;
+ }
+
+ public int getBottomPadding() {
+ return mBottomPadding;
+ }
+
+ @Override
+ public int getEllipsisCount(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_COUNT];
+ }
+
+ @Override
+ public int getEllipsisStart(int line) {
+ if (mColumns < COLUMNS_ELLIPSIZE) {
+ return 0;
+ }
+
+ return mLines[mColumns * line + ELLIPSIS_START];
+ }
+
+ @Override
+ public int getEllipsizedWidth() {
+ return mEllipsizedWidth;
+ }
+
+ private int mLineCount;
+ private int mTopPadding, mBottomPadding;
+ private int mColumns;
+ private int mEllipsizedWidth;
+
+ private static final int COLUMNS_NORMAL = 3;
+ private static final int COLUMNS_ELLIPSIZE = 5;
+ private static final int START = 0;
+ private static final int DIR = START;
+ private static final int TAB = START;
+ private static final int TOP = 1;
+ private static final int DESCENT = 2;
+ private static final int ELLIPSIS_START = 3;
+ private static final int ELLIPSIS_COUNT = 4;
+
+ private int[] mLines;
+ private Directions[] mLineDirections;
+
+ private static final int START_MASK = 0x1FFFFFFF;
+ private static final int DIR_MASK = 0xC0000000;
+ private static final int DIR_SHIFT = 30;
+ private static final int TAB_MASK = 0x20000000;
+
+ private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
+
+ /*
+ * These are reused across calls to generate()
+ */
+ private byte[] mChdirs;
+ private char[] mChs;
+ private float[] mWidths;
+ private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
+}
diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java
new file mode 100644
index 0000000..05c27ea
--- /dev/null
+++ b/core/java/android/text/Styled.java
@@ -0,0 +1,298 @@
+/*
+ * 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;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.MaskFilter;
+import android.graphics.Rasterizer;
+import android.graphics.LayerRasterizer;
+import android.text.style.*;
+
+/* package */ class Styled
+{
+ private static float each(Canvas canvas,
+ Spanned text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ Paint.FontMetricsInt fm,
+ TextPaint realPaint,
+ TextPaint paint,
+ boolean needwid) {
+
+ boolean havewid = false;
+ float ret = 0;
+ CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class);
+
+ ReplacementSpan replacement = null;
+
+ realPaint.bgColor = 0;
+ realPaint.baselineShift = 0;
+ paint.set(realPaint);
+
+ if (spans.length > 0) {
+ for (int i = 0; i < spans.length; i++) {
+ CharacterStyle span = spans[i];
+
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ }
+ else {
+ span.updateDrawState(paint);
+ }
+ }
+ }
+
+ if (replacement == null) {
+ CharSequence tmp;
+ int tmpstart, tmpend;
+
+ if (reverse) {
+ tmp = TextUtils.getReverse(text, start, end);
+ tmpstart = 0;
+ tmpend = end - start;
+ } else {
+ tmp = text;
+ tmpstart = start;
+ tmpend = end;
+ }
+
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ if (canvas != null) {
+ if (paint.bgColor != 0) {
+ int c = paint.getColor();
+ Paint.Style s = paint.getStyle();
+ paint.setColor(paint.bgColor);
+ paint.setStyle(Paint.Style.FILL);
+
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ canvas.drawRect(x - ret, top, x, bottom, paint);
+ else
+ canvas.drawRect(x, top, x + ret, bottom, paint);
+
+ paint.setStyle(s);
+ paint.setColor(c);
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT) {
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+
+ canvas.drawText(tmp, tmpstart, tmpend,
+ x - ret, y + paint.baselineShift, paint);
+ } else {
+ if (needwid) {
+ if (!havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+ }
+
+ canvas.drawText(tmp, tmpstart, tmpend,
+ x, y + paint.baselineShift, paint);
+ }
+ } else {
+ if (needwid && !havewid) {
+ ret = paint.measureText(tmp, tmpstart, tmpend);
+ havewid = true;
+ }
+ }
+ } else {
+ ret = replacement.getSize(paint, text, start, end, fm);
+
+ if (canvas != null) {
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ replacement.draw(canvas, text, start, end,
+ x - ret, top, y, bottom, paint);
+ else
+ replacement.draw(canvas, text, start, end,
+ x, top, y, bottom, paint);
+ }
+ }
+
+ if (dir == Layout.DIR_RIGHT_TO_LEFT)
+ return -ret;
+ else
+ return ret;
+ }
+
+ public static int getTextWidths(TextPaint realPaint,
+ TextPaint paint,
+ Spanned text, int start, int end,
+ float[] widths, Paint.FontMetricsInt fm) {
+
+ MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class);
+
+ ReplacementSpan replacement = null;
+ paint.set(realPaint);
+
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ }
+ else {
+ span.updateMeasureState(paint);
+ }
+ }
+
+ if (replacement == null) {
+ paint.getFontMetricsInt(fm);
+ paint.getTextWidths(text, start, end, widths);
+ } else {
+ int wid = replacement.getSize(paint, text, start, end, fm);
+
+ if (end > start) {
+ widths[0] = wid;
+
+ for (int i = start + 1; i < end; i++)
+ widths[i - start] = 0;
+ }
+ }
+ return end - start;
+ }
+
+ private static float foreach(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ Paint.FontMetricsInt fm,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean needwid) {
+ if (! (text instanceof Spanned)) {
+ float ret = 0;
+
+ if (reverse) {
+ CharSequence tmp = TextUtils.getReverse(text, start, end);
+ int tmpend = end - start;
+
+ if (canvas != null || needwid)
+ ret = paint.measureText(tmp, 0, tmpend);
+
+ if (canvas != null)
+ canvas.drawText(tmp, 0, tmpend,
+ x - ret, y, paint);
+ } else {
+ if (needwid)
+ ret = paint.measureText(text, start, end);
+
+ if (canvas != null)
+ canvas.drawText(text, start, end, x, y, paint);
+ }
+
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ return ret * dir; //Layout.DIR_RIGHT_TO_LEFT == -1
+ }
+
+ float ox = x;
+ int asc = 0, desc = 0;
+ int ftop = 0, fbot = 0;
+
+ Spanned sp = (Spanned) text;
+ Class division;
+
+ if (canvas == null)
+ division = MetricAffectingSpan.class;
+ else
+ division = CharacterStyle.class;
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = sp.nextSpanTransition(i, end, division);
+
+ x += each(canvas, sp, i, next, dir, reverse,
+ x, top, y, bottom, fm, paint, workPaint,
+ needwid || next != end);
+
+ if (fm != null) {
+ if (fm.ascent < asc)
+ asc = fm.ascent;
+ if (fm.descent > desc)
+ desc = fm.descent;
+
+ if (fm.top < ftop)
+ ftop = fm.top;
+ if (fm.bottom > fbot)
+ fbot = fm.bottom;
+ }
+ }
+
+ if (fm != null) {
+ if (start == end) {
+ paint.getFontMetricsInt(fm);
+ } else {
+ fm.ascent = asc;
+ fm.descent = desc;
+ fm.top = ftop;
+ fm.bottom = fbot;
+ }
+ }
+
+ return x - ox;
+ }
+
+ public static float drawText(Canvas canvas,
+ CharSequence text, int start, int end,
+ int dir, boolean reverse,
+ float x, int top, int y, int bottom,
+ TextPaint paint,
+ TextPaint workPaint,
+ boolean needwid) {
+ if ((dir == Layout.DIR_RIGHT_TO_LEFT && !reverse)||(reverse && dir == Layout.DIR_LEFT_TO_RIGHT)) {
+ float ch = foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT,
+ false, 0, 0, 0, 0, null, paint, workPaint,
+ true);
+
+ ch *= dir; // DIR_RIGHT_TO_LEFT == -1
+ foreach(canvas, text, start, end, -dir,
+ reverse, x + ch, top, y, bottom, null, paint,
+ workPaint, true);
+
+ return ch;
+ }
+
+ return foreach(canvas, text, start, end, dir, reverse,
+ x, top, y, bottom, null, paint, workPaint,
+ needwid);
+ }
+
+ public static float measureText(TextPaint paint,
+ TextPaint workPaint,
+ CharSequence text, int start, int end,
+ Paint.FontMetricsInt fm) {
+ return foreach(null, text, start, end,
+ Layout.DIR_LEFT_TO_RIGHT, false,
+ 0, 0, 0, 0, fm, paint, workPaint, true);
+ }
+}
diff --git a/core/java/android/text/TextPaint.java b/core/java/android/text/TextPaint.java
new file mode 100644
index 0000000..f13820d
--- /dev/null
+++ b/core/java/android/text/TextPaint.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import android.graphics.Paint;
+
+/**
+ * TextPaint is an extension of Paint that leaves room for some extra
+ * data used during text measuring and drawing.
+ */
+public class TextPaint extends Paint {
+ public int bgColor;
+ public int baselineShift;
+ public int linkColor;
+ public int[] drawableState;
+
+ public TextPaint() {
+ super();
+ }
+
+ public TextPaint(int flags) {
+ super(flags);
+ }
+
+ public TextPaint(Paint p) {
+ super(p);
+ }
+
+ /**
+ * Copy the fields from tp into this TextPaint, including the
+ * fields inherited from Paint.
+ */
+ public void set(TextPaint tp) {
+ super.set(tp);
+
+ bgColor = tp.bgColor;
+ baselineShift = tp.baselineShift;
+ linkColor = tp.linkColor;
+ drawableState = tp.drawableState;
+ }
+}
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
new file mode 100644
index 0000000..e791aaf
--- /dev/null
+++ b/core/java/android/text/TextUtils.java
@@ -0,0 +1,1570 @@
+/*
+ * 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;
+
+import com.android.internal.R;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.AlignmentSpan;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.BulletSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.ReplacementSpan;
+import android.text.style.ScaleXSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.regex.Pattern;
+import java.util.Iterator;
+
+public class TextUtils
+{
+ private TextUtils() { /* cannot be instantiated */ }
+
+ private static String[] EMPTY_STRING_ARRAY = new String[]{};
+
+ public static void getChars(CharSequence s, int start, int end,
+ char[] dest, int destoff) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ ((String) s).getChars(start, end, dest, destoff);
+ else if (c == StringBuffer.class)
+ ((StringBuffer) s).getChars(start, end, dest, destoff);
+ else if (c == StringBuilder.class)
+ ((StringBuilder) s).getChars(start, end, dest, destoff);
+ else if (s instanceof GetChars)
+ ((GetChars) s).getChars(start, end, dest, destoff);
+ else {
+ for (int i = start; i < end; i++)
+ dest[destoff++] = s.charAt(i);
+ }
+ }
+
+ public static int indexOf(CharSequence s, char ch) {
+ return indexOf(s, ch, 0);
+ }
+
+ public static int indexOf(CharSequence s, char ch, int start) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ return ((String) s).indexOf(ch, start);
+
+ return indexOf(s, ch, start, s.length());
+ }
+
+ public static int indexOf(CharSequence s, char ch, int start, int end) {
+ Class c = s.getClass();
+
+ if (s instanceof GetChars || c == StringBuffer.class ||
+ c == StringBuilder.class || c == String.class) {
+ final int INDEX_INCREMENT = 500;
+ char[] temp = obtain(INDEX_INCREMENT);
+
+ while (start < end) {
+ int segend = start + INDEX_INCREMENT;
+ if (segend > end)
+ segend = end;
+
+ getChars(s, start, segend, temp, 0);
+
+ int count = segend - start;
+ for (int i = 0; i < count; i++) {
+ if (temp[i] == ch) {
+ recycle(temp);
+ return i + start;
+ }
+ }
+
+ start = segend;
+ }
+
+ recycle(temp);
+ return -1;
+ }
+
+ for (int i = start; i < end; i++)
+ if (s.charAt(i) == ch)
+ return i;
+
+ return -1;
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch) {
+ return lastIndexOf(s, ch, s.length() - 1);
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch, int last) {
+ Class c = s.getClass();
+
+ if (c == String.class)
+ return ((String) s).lastIndexOf(ch, last);
+
+ return lastIndexOf(s, ch, 0, last);
+ }
+
+ public static int lastIndexOf(CharSequence s, char ch,
+ int start, int last) {
+ if (last < 0)
+ return -1;
+ if (last >= s.length())
+ last = s.length() - 1;
+
+ int end = last + 1;
+
+ Class c = s.getClass();
+
+ if (s instanceof GetChars || c == StringBuffer.class ||
+ c == StringBuilder.class || c == String.class) {
+ final int INDEX_INCREMENT = 500;
+ char[] temp = obtain(INDEX_INCREMENT);
+
+ while (start < end) {
+ int segstart = end - INDEX_INCREMENT;
+ if (segstart < start)
+ segstart = start;
+
+ getChars(s, segstart, end, temp, 0);
+
+ int count = end - segstart;
+ for (int i = count - 1; i >= 0; i--) {
+ if (temp[i] == ch) {
+ recycle(temp);
+ return i + segstart;
+ }
+ }
+
+ end = segstart;
+ }
+
+ recycle(temp);
+ return -1;
+ }
+
+ for (int i = end - 1; i >= start; i--)
+ if (s.charAt(i) == ch)
+ return i;
+
+ return -1;
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle) {
+ return indexOf(s, needle, 0, s.length());
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle, int start) {
+ return indexOf(s, needle, start, s.length());
+ }
+
+ public static int indexOf(CharSequence s, CharSequence needle,
+ int start, int end) {
+ int nlen = needle.length();
+ if (nlen == 0)
+ return start;
+
+ char c = needle.charAt(0);
+
+ for (;;) {
+ start = indexOf(s, c, start);
+ if (start > end - nlen) {
+ break;
+ }
+
+ if (start < 0) {
+ return -1;
+ }
+
+ if (regionMatches(s, start, needle, 0, nlen)) {
+ return start;
+ }
+
+ start++;
+ }
+ return -1;
+ }
+
+ public static boolean regionMatches(CharSequence one, int toffset,
+ CharSequence two, int ooffset,
+ int len) {
+ char[] temp = obtain(2 * len);
+
+ getChars(one, toffset, toffset + len, temp, 0);
+ getChars(two, ooffset, ooffset + len, temp, len);
+
+ boolean match = true;
+ for (int i = 0; i < len; i++) {
+ if (temp[i] != temp[i + len]) {
+ match = false;
+ break;
+ }
+ }
+
+ recycle(temp);
+ return match;
+ }
+
+ public static String substring(CharSequence source, int start, int end) {
+ if (source instanceof String)
+ return ((String) source).substring(start, end);
+ if (source instanceof StringBuilder)
+ return ((StringBuilder) source).substring(start, end);
+ if (source instanceof StringBuffer)
+ return ((StringBuffer) source).substring(start, end);
+
+ char[] temp = obtain(end - start);
+ getChars(source, start, end, temp, 0);
+ String ret = new String(temp, 0, end - start);
+ recycle(temp);
+
+ return ret;
+ }
+
+ /**
+ * Returns a string containing the tokens joined by delimiters.
+ * @param tokens an array objects to be joined. Strings will be formed from
+ * the objects by calling object.toString().
+ */
+ public static String join(CharSequence delimiter, Object[] tokens) {
+ StringBuilder sb = new StringBuilder();
+ boolean firstTime = true;
+ for (Object token: tokens) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(delimiter);
+ }
+ sb.append(token);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a string containing the tokens joined by delimiters.
+ * @param tokens an array objects to be joined. Strings will be formed from
+ * the objects by calling object.toString().
+ */
+ public static String join(CharSequence delimiter, Iterable tokens) {
+ StringBuilder sb = new StringBuilder();
+ boolean firstTime = true;
+ for (Object token: tokens) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(delimiter);
+ }
+ sb.append(token);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * String.split() returns [''] when the string to be split is empty. This returns []. This does
+ * not remove any empty strings from the result. For example split("a,", "," ) returns {"a", ""}.
+ *
+ * @param text the string to split
+ * @param expression the regular expression to match
+ * @return an array of strings. The array will be empty if text is empty
+ *
+ * @throws NullPointerException if expression or text is null
+ */
+ public static String[] split(String text, String expression) {
+ if (text.length() == 0) {
+ return EMPTY_STRING_ARRAY;
+ } else {
+ return text.split(expression, -1);
+ }
+ }
+
+ /**
+ * Splits a string on a pattern. String.split() returns [''] when the string to be
+ * split is empty. This returns []. This does not remove any empty strings from the result.
+ * @param text the string to split
+ * @param pattern the regular expression to match
+ * @return an array of strings. The array will be empty if text is empty
+ *
+ * @throws NullPointerException if expression or text is null
+ */
+ public static String[] split(String text, Pattern pattern) {
+ if (text.length() == 0) {
+ return EMPTY_STRING_ARRAY;
+ } else {
+ return pattern.split(text, -1);
+ }
+ }
+
+ /**
+ * An interface for splitting strings according to rules that are opaque to the user of this
+ * interface. This also has less overhead than split, which uses regular expressions and
+ * allocates an array to hold the results.
+ *
+ * <p>The most efficient way to use this class is:
+ *
+ * <pre>
+ * // Once
+ * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
+ *
+ * // Once per string to split
+ * splitter.setString(string);
+ * for (String s : splitter) {
+ * ...
+ * }
+ * </pre>
+ */
+ public interface StringSplitter extends Iterable<String> {
+ public void setString(String string);
+ }
+
+ /**
+ * A simple string splitter.
+ *
+ * <p>If the final character in the string to split is the delimiter then no empty string will
+ * be returned for the empty string after that delimeter. That is, splitting <tt>"a,b,"</tt> on
+ * comma will return <tt>"a", "b"</tt>, not <tt>"a", "b", ""</tt>.
+ */
+ public static class SimpleStringSplitter implements StringSplitter, Iterator<String> {
+ private String mString;
+ private char mDelimiter;
+ private int mPosition;
+ private int mLength;
+
+ /**
+ * Initializes the splitter. setString may be called later.
+ * @param delimiter the delimeter on which to split
+ */
+ public SimpleStringSplitter(char delimiter) {
+ mDelimiter = delimiter;
+ }
+
+ /**
+ * Sets the string to split
+ * @param string the string to split
+ */
+ public void setString(String string) {
+ mString = string;
+ mPosition = 0;
+ mLength = mString.length();
+ }
+
+ public Iterator<String> iterator() {
+ return this;
+ }
+
+ public boolean hasNext() {
+ return mPosition < mLength;
+ }
+
+ public String next() {
+ int end = mString.indexOf(mDelimiter, mPosition);
+ if (end == -1) {
+ end = mLength;
+ }
+ String nextString = mString.substring(mPosition, end);
+ mPosition = end + 1; // Skip the delimiter.
+ return nextString;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public static CharSequence stringOrSpannedString(CharSequence source) {
+ if (source == null)
+ return null;
+ if (source instanceof SpannedString)
+ return source;
+ if (source instanceof Spanned)
+ return new SpannedString(source);
+
+ return source.toString();
+ }
+
+ /**
+ * Returns true if the string is null or 0-length.
+ * @param str the string to be examined
+ * @return true if str is null or zero length
+ */
+ public static boolean isEmpty(CharSequence str) {
+ if (str == null || str.length() == 0)
+ return true;
+ else
+ return false;
+ }
+
+ /**
+ * Returns the length that the specified CharSequence would have if
+ * spaces and control characters were trimmed from the start and end,
+ * as by {@link String#trim}.
+ */
+ public static int getTrimmedLength(CharSequence s) {
+ int len = s.length();
+
+ int start = 0;
+ while (start < len && s.charAt(start) <= ' ') {
+ start++;
+ }
+
+ int end = len;
+ while (end > start && s.charAt(end - 1) <= ' ') {
+ end--;
+ }
+
+ return end - start;
+ }
+
+ /**
+ * Returns true if a and b are equal, including if they are both null.
+ *
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return true if a and b are equal
+ */
+ public static boolean equals(CharSequence a, CharSequence b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ // XXX currently this only reverses chars, not spans
+ public static CharSequence getReverse(CharSequence source,
+ int start, int end) {
+ return new Reverser(source, start, end);
+ }
+
+ private static class Reverser
+ implements CharSequence, GetChars
+ {
+ public Reverser(CharSequence source, int start, int end) {
+ mSource = source;
+ mStart = start;
+ mEnd = end;
+ }
+
+ public int length() {
+ return mEnd - mStart;
+ }
+
+ 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 char charAt(int off) {
+ return AndroidCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
+ }
+
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ TextUtils.getChars(mSource, start + mStart, end + mStart,
+ dest, destoff);
+ AndroidCharacter.mirror(dest, 0, end - start);
+
+ int len = end - start;
+ int n = (end - start) / 2;
+ for (int i = 0; i < n; i++) {
+ char tmp = dest[destoff + i];
+
+ dest[destoff + i] = dest[destoff + len - i - 1];
+ dest[destoff + len - i - 1] = tmp;
+ }
+ }
+
+ private CharSequence mSource;
+ private int mStart;
+ private int mEnd;
+ }
+
+ private static final int ALIGNMENT_SPAN = 1;
+ private static final int FOREGROUND_COLOR_SPAN = 2;
+ private static final int RELATIVE_SIZE_SPAN = 3;
+ private static final int SCALE_X_SPAN = 4;
+ private static final int STRIKETHROUGH_SPAN = 5;
+ private static final int UNDERLINE_SPAN = 6;
+ private static final int STYLE_SPAN = 7;
+ private static final int BULLET_SPAN = 8;
+ private static final int QUOTE_SPAN = 9;
+ private static final int LEADING_MARGIN_SPAN = 10;
+ private static final int URL_SPAN = 11;
+ private static final int BACKGROUND_COLOR_SPAN = 12;
+ private static final int TYPEFACE_SPAN = 13;
+ private static final int SUPERSCRIPT_SPAN = 14;
+ private static final int SUBSCRIPT_SPAN = 15;
+ private static final int ABSOLUTE_SIZE_SPAN = 16;
+ private static final int TEXT_APPEARANCE_SPAN = 17;
+ private static final int ANNOTATION = 18;
+
+ /**
+ * Flatten a CharSequence and whatever styles can be copied across processes
+ * into the parcel.
+ */
+ public static void writeToParcel(CharSequence cs, Parcel p,
+ int parcelableFlags) {
+ if (cs instanceof Spanned) {
+ p.writeInt(0);
+ p.writeString(cs.toString());
+
+ Spanned sp = (Spanned) cs;
+ Object[] os = sp.getSpans(0, cs.length(), Object.class);
+
+ // note to people adding to this: check more specific types
+ // before more generic types. also notice that it uses
+ // "if" instead of "else if" where there are interfaces
+ // so one object can be several.
+
+ for (int i = 0; i < os.length; i++) {
+ Object o = os[i];
+ Object prop = os[i];
+
+ if (prop instanceof CharacterStyle) {
+ prop = ((CharacterStyle) prop).getUnderlying();
+ }
+
+ if (prop instanceof AlignmentSpan) {
+ p.writeInt(ALIGNMENT_SPAN);
+ p.writeString(((AlignmentSpan) prop).getAlignment().name());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof ForegroundColorSpan) {
+ p.writeInt(FOREGROUND_COLOR_SPAN);
+ p.writeInt(((ForegroundColorSpan) prop).getForegroundColor());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof RelativeSizeSpan) {
+ p.writeInt(RELATIVE_SIZE_SPAN);
+ p.writeFloat(((RelativeSizeSpan) prop).getSizeChange());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof ScaleXSpan) {
+ p.writeInt(SCALE_X_SPAN);
+ p.writeFloat(((ScaleXSpan) prop).getScaleX());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof StrikethroughSpan) {
+ p.writeInt(STRIKETHROUGH_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof UnderlineSpan) {
+ p.writeInt(UNDERLINE_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof StyleSpan) {
+ p.writeInt(STYLE_SPAN);
+ p.writeInt(((StyleSpan) prop).getStyle());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof LeadingMarginSpan) {
+ if (prop instanceof BulletSpan) {
+ p.writeInt(BULLET_SPAN);
+ writeWhere(p, sp, o);
+ } else if (prop instanceof QuoteSpan) {
+ p.writeInt(QUOTE_SPAN);
+ p.writeInt(((QuoteSpan) prop).getColor());
+ writeWhere(p, sp, o);
+ } else {
+ p.writeInt(LEADING_MARGIN_SPAN);
+ p.writeInt(((LeadingMarginSpan) prop).
+ getLeadingMargin(true));
+ p.writeInt(((LeadingMarginSpan) prop).
+ getLeadingMargin(false));
+ writeWhere(p, sp, o);
+ }
+ }
+
+ if (prop instanceof URLSpan) {
+ p.writeInt(URL_SPAN);
+ p.writeString(((URLSpan) prop).getURL());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof BackgroundColorSpan) {
+ p.writeInt(BACKGROUND_COLOR_SPAN);
+ p.writeInt(((BackgroundColorSpan) prop).getBackgroundColor());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof TypefaceSpan) {
+ p.writeInt(TYPEFACE_SPAN);
+ p.writeString(((TypefaceSpan) prop).getFamily());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof SuperscriptSpan) {
+ p.writeInt(SUPERSCRIPT_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof SubscriptSpan) {
+ p.writeInt(SUBSCRIPT_SPAN);
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof AbsoluteSizeSpan) {
+ p.writeInt(ABSOLUTE_SIZE_SPAN);
+ p.writeInt(((AbsoluteSizeSpan) prop).getSize());
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof TextAppearanceSpan) {
+ TextAppearanceSpan tas = (TextAppearanceSpan) prop;
+ p.writeInt(TEXT_APPEARANCE_SPAN);
+
+ String tf = tas.getFamily();
+ if (tf != null) {
+ p.writeInt(1);
+ p.writeString(tf);
+ } else {
+ p.writeInt(0);
+ }
+
+ p.writeInt(tas.getTextSize());
+ p.writeInt(tas.getTextStyle());
+
+ ColorStateList csl = tas.getTextColor();
+ if (csl == null) {
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ csl.writeToParcel(p, parcelableFlags);
+ }
+
+ csl = tas.getLinkTextColor();
+ if (csl == null) {
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ csl.writeToParcel(p, parcelableFlags);
+ }
+
+ writeWhere(p, sp, o);
+ }
+
+ if (prop instanceof Annotation) {
+ p.writeInt(ANNOTATION);
+ p.writeString(((Annotation) prop).getKey());
+ p.writeString(((Annotation) prop).getValue());
+ writeWhere(p, sp, o);
+ }
+ }
+
+ p.writeInt(0);
+ } else {
+ p.writeInt(1);
+ if (cs != null) {
+ p.writeString(cs.toString());
+ } else {
+ p.writeString(null);
+ }
+ }
+ }
+
+ private static void writeWhere(Parcel p, Spanned sp, Object o) {
+ p.writeInt(sp.getSpanStart(o));
+ p.writeInt(sp.getSpanEnd(o));
+ p.writeInt(sp.getSpanFlags(o));
+ }
+
+ public static final Parcelable.Creator<CharSequence> CHAR_SEQUENCE_CREATOR
+ = new Parcelable.Creator<CharSequence>()
+ {
+ /**
+ * Read and return a new CharSequence, possibly with styles,
+ * from the parcel.
+ */
+ public CharSequence createFromParcel(Parcel p) {
+ int kind = p.readInt();
+
+ if (kind == 1)
+ return p.readString();
+
+ SpannableString sp = new SpannableString(p.readString());
+
+ while (true) {
+ kind = p.readInt();
+
+ if (kind == 0)
+ break;
+
+ switch (kind) {
+ case ALIGNMENT_SPAN:
+ readSpan(p, sp, new AlignmentSpan.Standard(
+ Layout.Alignment.valueOf(p.readString())));
+ break;
+
+ case FOREGROUND_COLOR_SPAN:
+ readSpan(p, sp, new ForegroundColorSpan(p.readInt()));
+ break;
+
+ case RELATIVE_SIZE_SPAN:
+ readSpan(p, sp, new RelativeSizeSpan(p.readFloat()));
+ break;
+
+ case SCALE_X_SPAN:
+ readSpan(p, sp, new ScaleXSpan(p.readFloat()));
+ break;
+
+ case STRIKETHROUGH_SPAN:
+ readSpan(p, sp, new StrikethroughSpan());
+ break;
+
+ case UNDERLINE_SPAN:
+ readSpan(p, sp, new UnderlineSpan());
+ break;
+
+ case STYLE_SPAN:
+ readSpan(p, sp, new StyleSpan(p.readInt()));
+ break;
+
+ case BULLET_SPAN:
+ readSpan(p, sp, new BulletSpan());
+ break;
+
+ case QUOTE_SPAN:
+ readSpan(p, sp, new QuoteSpan(p.readInt()));
+ break;
+
+ case LEADING_MARGIN_SPAN:
+ readSpan(p, sp, new LeadingMarginSpan.Standard(p.readInt(),
+ p.readInt()));
+ break;
+
+ case URL_SPAN:
+ readSpan(p, sp, new URLSpan(p.readString()));
+ break;
+
+ case BACKGROUND_COLOR_SPAN:
+ readSpan(p, sp, new BackgroundColorSpan(p.readInt()));
+ break;
+
+ case TYPEFACE_SPAN:
+ readSpan(p, sp, new TypefaceSpan(p.readString()));
+ break;
+
+ case SUPERSCRIPT_SPAN:
+ readSpan(p, sp, new SuperscriptSpan());
+ break;
+
+ case SUBSCRIPT_SPAN:
+ readSpan(p, sp, new SubscriptSpan());
+ break;
+
+ case ABSOLUTE_SIZE_SPAN:
+ readSpan(p, sp, new AbsoluteSizeSpan(p.readInt()));
+ break;
+
+ case TEXT_APPEARANCE_SPAN:
+ readSpan(p, sp, new TextAppearanceSpan(
+ p.readInt() != 0
+ ? p.readString()
+ : null,
+ p.readInt(),
+ p.readInt(),
+ p.readInt() != 0
+ ? ColorStateList.CREATOR.createFromParcel(p)
+ : null,
+ p.readInt() != 0
+ ? ColorStateList.CREATOR.createFromParcel(p)
+ : null));
+ break;
+
+ case ANNOTATION:
+ readSpan(p, sp,
+ new Annotation(p.readString(), p.readString()));
+ break;
+
+ default:
+ throw new RuntimeException("bogus span encoding " + kind);
+ }
+ }
+
+ return sp;
+ }
+
+ public CharSequence[] newArray(int size)
+ {
+ return new CharSequence[size];
+ }
+ };
+
+ /**
+ * Return a new CharSequence in which each of the source strings is
+ * replaced by the corresponding element of the destinations.
+ */
+ public static CharSequence replace(CharSequence template,
+ String[] sources,
+ CharSequence[] destinations) {
+ SpannableStringBuilder tb = new SpannableStringBuilder(template);
+
+ for (int i = 0; i < sources.length; i++) {
+ int where = indexOf(tb, sources[i]);
+
+ if (where >= 0)
+ tb.setSpan(sources[i], where, where + sources[i].length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ for (int i = 0; i < sources.length; i++) {
+ int start = tb.getSpanStart(sources[i]);
+ int end = tb.getSpanEnd(sources[i]);
+
+ if (start >= 0) {
+ tb.replace(start, end, destinations[i]);
+ }
+ }
+
+ return tb;
+ }
+
+ /**
+ * Replace instances of "^1", "^2", etc. in the
+ * <code>template</code> CharSequence with the corresponding
+ * <code>values</code>. "^^" is used to produce a single caret in
+ * the output. Only up to 9 replacement values are supported,
+ * "^10" will be produce the first replacement value followed by a
+ * '0'.
+ *
+ * @param template the input text containing "^1"-style
+ * placeholder values. This object is not modified; a copy is
+ * returned.
+ *
+ * @param values CharSequences substituted into the template. The
+ * first is substituted for "^1", the second for "^2", and so on.
+ *
+ * @return the new CharSequence produced by doing the replacement
+ *
+ * @throws IllegalArgumentException if the template requests a
+ * value that was not provided, or if more than 9 values are
+ * provided.
+ */
+ public static CharSequence expandTemplate(CharSequence template,
+ CharSequence... values) {
+ if (values.length > 9) {
+ throw new IllegalArgumentException("max of 9 values are supported");
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder(template);
+
+ try {
+ int i = 0;
+ while (i < ssb.length()) {
+ if (ssb.charAt(i) == '^') {
+ char next = ssb.charAt(i+1);
+ if (next == '^') {
+ ssb.delete(i+1, i+2);
+ ++i;
+ continue;
+ } else if (Character.isDigit(next)) {
+ int which = Character.getNumericValue(next) - 1;
+ if (which < 0) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1));
+ }
+ if (which >= values.length) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1) +
+ "; only " + values.length + " provided");
+ }
+ ssb.replace(i, i+2, values[which]);
+ i += values[which].length();
+ continue;
+ }
+ }
+ ++i;
+ }
+ } catch (IndexOutOfBoundsException ignore) {
+ // happens when ^ is the last character in the string.
+ }
+ return ssb;
+ }
+
+ public static int getOffsetBefore(CharSequence text, int offset) {
+ if (offset == 0)
+ return 0;
+ if (offset == 1)
+ return 0;
+
+ char c = text.charAt(offset - 1);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 2);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 2;
+ else
+ offset -= 1;
+ } else {
+ offset -= 1;
+ }
+
+ if (text instanceof Spanned) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = start;
+ }
+ }
+
+ return offset;
+ }
+
+ public static int getOffsetAfter(CharSequence text, int offset) {
+ int len = text.length();
+
+ if (offset == len)
+ return len;
+ if (offset == len - 1)
+ return len;
+
+ char c = text.charAt(offset);
+
+ if (c >= '\uD800' && c <= '\uDBFF') {
+ char c1 = text.charAt(offset + 1);
+
+ if (c1 >= '\uDC00' && c1 <= '\uDFFF')
+ offset += 2;
+ else
+ offset += 1;
+ } else {
+ offset += 1;
+ }
+
+ if (text instanceof Spanned) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = end;
+ }
+ }
+
+ return offset;
+ }
+
+ private static void readSpan(Parcel p, Spannable sp, Object o) {
+ sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
+ }
+
+ public static void copySpansFrom(Spanned source, int start, int end,
+ Class kind,
+ Spannable dest, int destoff) {
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ Object[] spans = source.getSpans(start, end, kind);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = source.getSpanStart(spans[i]);
+ int en = source.getSpanEnd(spans[i]);
+ int fl = source.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
+ fl);
+ }
+ }
+
+ public enum TruncateAt {
+ START,
+ MIDDLE,
+ END,
+ }
+
+ public interface EllipsizeCallback {
+ /**
+ * This method is called to report that the specified region of
+ * text was ellipsized away by a call to {@link #ellipsize}.
+ */
+ public void ellipsized(int start, int end);
+ }
+
+ private static String sEllipsis = null;
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a truncated
+ * copy with ellipsis character added at the specified edge or center.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where) {
+ return ellipsize(text, p, avail, where, false, null);
+ }
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a copy with ellipsis character added
+ * at the specified edge or center.
+ * If <code>preserveLength</code> is specified, the returned copy
+ * will be padded with zero-width spaces to preserve the original
+ * length and offsets instead of truncating.
+ * If <code>callback</code> is non-null, it will be called to
+ * report the start and end of the ellipsized range.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where,
+ boolean preserveLength,
+ EllipsizeCallback callback) {
+ if (sEllipsis == null) {
+ Resources r = Resources.getSystem();
+ sEllipsis = r.getString(R.string.ellipsis);
+ }
+
+ int len = text.length();
+
+ // Use Paint.breakText() for the non-Spanned case to avoid having
+ // to allocate memory and accumulate the character widths ourselves.
+
+ if (!(text instanceof Spanned)) {
+ float wid = p.measureText(text, 0, len);
+
+ if (wid <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+ return ret;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ int fit = p.breakText(text, 0, len, false,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(0, len - fit);
+ }
+
+ if (preserveLength) {
+ return blank(text, 0, len - fit);
+ } else {
+ return sEllipsis + text.toString().substring(len - fit, len);
+ }
+ } else if (where == TruncateAt.END) {
+ int fit = p.breakText(text, 0, len, true,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(fit, len);
+ }
+
+ if (preserveLength) {
+ return blank(text, fit, len);
+ } else {
+ return text.toString().substring(0, fit) + sEllipsis;
+ }
+ } else /* where == TruncateAt.MIDDLE */ {
+ int right = p.breakText(text, 0, len, false,
+ (avail - ellipsiswid) / 2, null);
+ float used = p.measureText(text, len - right, len);
+ int left = p.breakText(text, 0, len - right, true,
+ avail - ellipsiswid - used, null);
+
+ if (callback != null) {
+ callback.ellipsized(left, len - right);
+ }
+
+ if (preserveLength) {
+ return blank(text, left, len - right);
+ } else {
+ String s = text.toString();
+ return s.substring(0, left) + sEllipsis +
+ s.substring(len - right, len);
+ }
+ }
+ }
+
+ // But do the Spanned cases by hand, because it's such a pain
+ // to iterate the span transitions backwards and getTextWidths()
+ // will give us the information we need.
+
+ // getTextWidths() always writes into the start of the array,
+ // so measure each span into the first half and then copy the
+ // results into the second half to use later.
+
+ float[] wid = new float[len * 2];
+ TextPaint temppaint = new TextPaint();
+ Spanned sp = (Spanned) text;
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ float sum = 0;
+ for (int i = 0; i < len; i++) {
+ sum += wid[len + i];
+ }
+
+ if (sum <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ SpannableString ss = new SpannableString(new String(buf, 0, len));
+ recycle(buf);
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ sum = 0;
+ int i;
+
+ for (i = len; i >= 0; i--) {
+ float w = wid[len + i - 1];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(0, i);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, 0, i));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(1, text, i, len);
+
+ return out;
+ }
+ } else if (where == TruncateAt.END) {
+ sum = 0;
+ int i;
+
+ for (i = 0; i < len; i++) {
+ float w = wid[len + i];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(i, len);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, i, len));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, i);
+
+ return out;
+ }
+ } else /* where = TruncateAt.MIDDLE */ {
+ float lsum = 0, rsum = 0;
+ int left = 0, right = len;
+
+ float ravail = (avail - ellipsiswid) / 2;
+ for (right = len; right >= 0; right--) {
+ float w = wid[len + right - 1];
+
+ if (w + rsum > ravail) {
+ break;
+ }
+
+ rsum += w;
+ }
+
+ float lavail = avail - ellipsiswid - rsum;
+ for (left = 0; left < right; left++) {
+ float w = wid[len + left];
+
+ if (w + lsum > lavail) {
+ break;
+ }
+
+ lsum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(left, right);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, left, right));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, left);
+ out.insert(out.length(), text, right, len);
+
+ return out;
+ }
+ }
+ }
+
+ private static String blank(CharSequence source, int start, int end) {
+ int len = source.length();
+ char[] buf = obtain(len);
+
+ if (start != 0) {
+ getChars(source, 0, start, buf, 0);
+ }
+ if (end != len) {
+ getChars(source, end, len, buf, end);
+ }
+
+ if (start != end) {
+ buf[start] = '\u2026';
+
+ for (int i = start + 1; i < end; i++) {
+ buf[i] = '\uFEFF';
+ }
+ }
+
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+
+ return ret;
+ }
+
+ /**
+ * Converts a CharSequence of the comma-separated form "Andy, Bob,
+ * Charles, David" that is too wide to fit into the specified width
+ * into one like "Andy, Bob, 2 more".
+ *
+ * @param text the text to truncate
+ * @param p the Paint with which to measure the text
+ * @param avail the horizontal width available for the text
+ * @param oneMore the string for "1 more" in the current locale
+ * @param more the string for "%d more" in the current locale
+ */
+ public static CharSequence commaEllipsize(CharSequence text,
+ TextPaint p, float avail,
+ String oneMore,
+ String more) {
+ int len = text.length();
+ char[] buf = new char[len];
+ TextUtils.getChars(text, 0, len, buf, 0);
+
+ int commaCount = 0;
+ for (int i = 0; i < len; i++) {
+ if (buf[i] == ',') {
+ commaCount++;
+ }
+ }
+
+ float[] wid;
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ TextPaint temppaint = new TextPaint();
+ wid = new float[len * 2];
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ System.arraycopy(wid, len, wid, 0, len);
+ } else {
+ wid = new float[len];
+ p.getTextWidths(text, 0, len, wid);
+ }
+
+ int ok = 0;
+ int okRemaining = commaCount + 1;
+ String okFormat = "";
+
+ int w = 0;
+ int count = 0;
+
+ for (int i = 0; i < len; i++) {
+ w += wid[i];
+
+ if (buf[i] == ',') {
+ count++;
+
+ int remaining = commaCount - count + 1;
+ float moreWid;
+ String format;
+
+ if (remaining == 1) {
+ format = " " + oneMore;
+ } else {
+ format = " " + String.format(more, remaining);
+ }
+
+ moreWid = p.measureText(format);
+
+ if (w + moreWid <= avail) {
+ ok = i + 1;
+ okRemaining = remaining;
+ okFormat = format;
+ }
+ }
+ }
+
+ if (w <= avail) {
+ return text;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
+ out.insert(0, text, 0, ok);
+ return out;
+ }
+ }
+
+ /* package */ static char[] obtain(int len) {
+ char[] buf;
+
+ synchronized (sLock) {
+ buf = sTemp;
+ sTemp = null;
+ }
+
+ if (buf == null || buf.length < len)
+ buf = new char[ArrayUtils.idealCharArraySize(len)];
+
+ return buf;
+ }
+
+ /* package */ static void recycle(char[] temp) {
+ if (temp.length > 1000)
+ return;
+
+ synchronized (sLock) {
+ sTemp = temp;
+ }
+ }
+
+ /**
+ * Html-encode the string.
+ * @param s the string to be encoded
+ * @return the encoded string
+ */
+ public static String htmlEncode(String s) {
+ StringBuilder sb = new StringBuilder();
+ char c;
+ for (int i = 0; i < s.length(); i++) {
+ c = s.charAt(i);
+ switch (c) {
+ case '<':
+ sb.append("&lt;"); //$NON-NLS-1$
+ break;
+ case '>':
+ sb.append("&gt;"); //$NON-NLS-1$
+ break;
+ case '&':
+ sb.append("&amp;"); //$NON-NLS-1$
+ break;
+ case '\\':
+ sb.append("&apos;"); //$NON-NLS-1$
+ break;
+ case '"':
+ sb.append("&quot;"); //$NON-NLS-1$
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a CharSequence concatenating the specified CharSequences,
+ * retaining their spans if any.
+ */
+ public static CharSequence concat(CharSequence... text) {
+ if (text.length == 0) {
+ return "";
+ }
+
+ if (text.length == 1) {
+ return text[0];
+ }
+
+ boolean spanned = false;
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] instanceof Spanned) {
+ spanned = true;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < text.length; i++) {
+ sb.append(text[i]);
+ }
+
+ if (!spanned) {
+ return sb.toString();
+ }
+
+ SpannableString ss = new SpannableString(sb);
+ int off = 0;
+ for (int i = 0; i < text.length; i++) {
+ int len = text[i].length();
+
+ if (text[i] instanceof Spanned) {
+ copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
+ }
+
+ off += len;
+ }
+
+ return new SpannedString(ss);
+ }
+
+ /**
+ * Returns whether the given CharSequence contains any printable characters.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ final int len = str.length();
+ for (int i=0; i<len; i++) {
+ int gc = Character.getType(str.charAt(i));
+ if (gc != Character.CONTROL
+ && gc != Character.FORMAT
+ && gc != Character.SURROGATE
+ && gc != Character.UNASSIGNED
+ && gc != Character.LINE_SEPARATOR
+ && gc != Character.PARAGRAPH_SEPARATOR
+ && gc != Character.SPACE_SEPARATOR) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether this character is a printable character.
+ */
+ public static boolean isGraphic(char c) {
+ int gc = Character.getType(c);
+ return gc != Character.CONTROL
+ && gc != Character.FORMAT
+ && gc != Character.SURROGATE
+ && gc != Character.UNASSIGNED
+ && gc != Character.LINE_SEPARATOR
+ && gc != Character.PARAGRAPH_SEPARATOR
+ && gc != Character.SPACE_SEPARATOR;
+ }
+
+ /**
+ * Returns whether the given CharSequence contains only digits.
+ */
+ public static boolean isDigitsOnly(CharSequence str) {
+ final int len = str.length();
+ for (int i = 0; i < len; i++) {
+ if (!Character.isDigit(str.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Object sLock = new Object();
+ private static char[] sTemp = null;
+}
diff --git a/core/java/android/text/TextWatcher.java b/core/java/android/text/TextWatcher.java
new file mode 100644
index 0000000..7456b28
--- /dev/null
+++ b/core/java/android/text/TextWatcher.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+/**
+ * When an object of a type is attached to an Editable, its methods will
+ * be called when the text is changed.
+ */
+public interface TextWatcher {
+ /**
+ * This method is called to notify you that, within <code>s</code>,
+ * the <code>count</code> characters beginning at <code>start</code>
+ * are about to be replaced by new text with length <code>after</code>.
+ * It is an error to attempt to make changes to <code>s</code> from
+ * this callback.
+ */
+ public void beforeTextChanged(CharSequence s, int start,
+ int count, int after);
+ /**
+ * This method is called to notify you that, within <code>s</code>,
+ * the <code>count</code> characters beginning at <code>start</code>
+ * have just replaced old text that had length <code>before</code>.
+ * It is an error to attempt to make changes to <code>s</code> from
+ * this callback.
+ */
+ public void onTextChanged(CharSequence s, int start, int before, int count);
+
+ /**
+ * This method is called to notify you that, somewhere within
+ * <code>s</code>, the text has been changed.
+ * It is legitimate to make further changes to <code>s</code> from
+ * this callback, but be careful not to get yourself into an infinite
+ * loop, because any changes you make will cause this method to be
+ * called again recursively.
+ * (You are not told where the change took place because other
+ * afterTextChanged() methods may already have made other changes
+ * and invalidated the offsets. But if you need to know here,
+ * you can use {@link Spannable#setSpan} in {@link #onTextChanged}
+ * to mark your place and then look up from here where the span
+ * ended up.
+ */
+ public void afterTextChanged(Editable s);
+}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
new file mode 100644
index 0000000..ac2e499
--- /dev/null
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.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.view.KeyEvent;
+import android.text.*;
+import android.widget.TextView;
+import android.view.View;
+import android.view.MotionEvent;
+
+// XXX this doesn't extend MetaKeyKeyListener because the signatures
+// don't match. Need to figure that out. Meanwhile the meta keys
+// won't work in fields that don't take input.
+
+public class
+ArrowKeyMovementMethod
+implements MovementMethod
+{
+ private boolean up(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.extendUp(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.moveUp(buffer, layout);
+ }
+ }
+ }
+
+ private boolean down(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.extendDown(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.moveDown(buffer, layout);
+ }
+ }
+ }
+
+ private boolean left(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToLeftEdge(buffer, layout);
+ } else {
+ return Selection.extendLeft(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToLeftEdge(buffer, layout);
+ } else {
+ return Selection.moveLeft(buffer, layout);
+ }
+ }
+ }
+
+ private boolean right(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToRightEdge(buffer, layout);
+ } else {
+ return Selection.extendRight(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToRightEdge(buffer, layout);
+ } else {
+ return Selection.moveRight(buffer, layout);
+ }
+ }
+ }
+
+ public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ handled |= up(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ handled |= down(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled |= left(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled |= right(widget, buffer);
+ break;
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onTrackballEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = false;
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ for (; y < 0; y++) {
+ handled |= up(widget, buffer);
+ }
+ for (; y > 0; y--) {
+ handled |= down(widget, buffer);
+ }
+
+ for (; x < 0; x++) {
+ handled |= left(widget, buffer);
+ }
+ for (; x > 0; x--) {
+ handled |= right(widget, buffer);
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = Touch.onTouchEvent(widget, buffer, event);
+
+ if (widget.isFocused()) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ 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);
+
+ boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0;
+
+ if (cap) {
+ Selection.extendSelection(buffer, off);
+ } else {
+ Selection.setSelection(buffer, off);
+ }
+
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ public boolean canSelectArbitrarily() {
+ return true;
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.setSelection(text, 0);
+ }
+
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
+ Layout layout = view.getLayout();
+
+ if (layout == null) {
+ /*
+ * This shouldn't be null, but do something sensible if it is.
+ */
+ Selection.setSelection(text, text.length());
+ } else {
+ /*
+ * Put the cursor at the end of the first line, which is
+ * either the last offset if there is only one line, or the
+ * offset before the first character of the second line
+ * if there is more than one line.
+ */
+ if (layout.getLineCount() == 1) {
+ Selection.setSelection(text, text.length());
+ } else {
+ Selection.setSelection(text, layout.getLineStart(1) - 1);
+ }
+ }
+ } else {
+ Selection.setSelection(text, text.length());
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new ArrowKeyMovementMethod();
+
+ return sInstance;
+ }
+
+ private static ArrowKeyMovementMethod sInstance;
+}
diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java
new file mode 100644
index 0000000..3e92b7b
--- /dev/null
+++ b/core/java/android/text/method/BaseKeyListener.java
@@ -0,0 +1,112 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.util.Log;
+import android.text.*;
+import android.widget.TextView;
+
+public abstract class BaseKeyListener
+extends MetaKeyKeyListener
+implements KeyListener {
+ /* package */ static final Object OLD_SEL_START = new Object();
+
+ /**
+ * Performs the action that happens when you press the DEL key in
+ * a TextView. If there is a selection, deletes the selection;
+ * otherwise, DEL alone 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) {
+ int selStart, selEnd;
+ boolean result = true;
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ if (selStart != selEnd) {
+ content.delete(selStart, selEnd);
+ } else if (altBackspace(view, content, keyCode, event)) {
+ result = true;
+ } else {
+ int to = TextUtils.getOffsetBefore(content, selEnd);
+
+ if (to != selEnd) {
+ content.delete(Math.min(to, selEnd), Math.max(to, selEnd));
+ }
+ else {
+ result = false;
+ }
+ }
+
+ if (result)
+ adjustMetaAfterKeypress(content);
+
+ return result;
+ }
+
+ private boolean altBackspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ if (getMetaState(content, META_ALT_ON) != 1) {
+ return false;
+ }
+
+ if (!(view instanceof TextView)) {
+ return false;
+ }
+
+ Layout layout = ((TextView) view).getLayout();
+
+ if (layout == null) {
+ return false;
+ }
+
+ int l = layout.getLineForOffset(Selection.getSelectionStart(content));
+ int start = layout.getLineStart(l);
+ int end = layout.getLineEnd(l);
+
+ if (end == start) {
+ return false;
+ }
+
+ content.delete(start, end);
+ return true;
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ backspace(view, content, keyCode, event);
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+}
+
diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java
new file mode 100644
index 0000000..d787132
--- /dev/null
+++ b/core/java/android/text/method/CharacterPickerDialog.java
@@ -0,0 +1,134 @@
+/*
+ * 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 com.android.internal.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.*;
+import android.view.LayoutInflater;
+import android.view.View.OnClickListener;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.TextView;
+
+/**
+ * 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;
+
+ /**
+ * 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);
+
+ 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_PANEL;
+
+ setTitle(R.string.select_character);
+ setContentView(R.layout.character_picker);
+
+ GridView grid = (GridView) findViewById(R.id.characterPicker);
+ grid.setAdapter(new OptionsAdapter(getContext()));
+ grid.setOnItemClickListener(this);
+
+ findViewById(R.id.cancel).setOnClickListener(this);
+ }
+
+ /**
+ * Handles clicks on the character buttons.
+ */
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ int selEnd = Selection.getSelectionEnd(mText);
+ String result = String.valueOf(mOptions.charAt(position));
+
+ if (mInsert || selEnd == 0) {
+ mText.insert(selEnd, result);
+ } else {
+ mText.replace(selEnd - 1, selEnd, result);
+ }
+
+ dismiss();
+ }
+
+ /**
+ * Handles clicks on the Cancel button.
+ */
+ public void onClick(View v) {
+ dismiss();
+ }
+
+ private class OptionsAdapter extends BaseAdapter {
+ private Context mContext;
+
+ public OptionsAdapter(Context context) {
+ super();
+ mContext = context;
+ }
+
+ 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)));
+ 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/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java
new file mode 100644
index 0000000..0ca0332
--- /dev/null
+++ b/core/java/android/text/method/DateKeyListener.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;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates in a text field.
+ */
+public class DateKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * 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',
+ '/', '-', '.'
+ };
+
+ private static DateKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java
new file mode 100644
index 0000000..304d326
--- /dev/null
+++ b/core/java/android/text/method/DateTimeKeyListener.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;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates and times in the same text field.
+ */
+public class DateTimeKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateTimeKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateTimeKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * 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', 'a', 'm',
+ 'p', ':', '/', '-', ' '
+ };
+
+ private static DateTimeKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java
new file mode 100644
index 0000000..e805ad7
--- /dev/null
+++ b/core/java/android/text/method/DialerKeyListener.java
@@ -0,0 +1,109 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.KeyCharacterMap.KeyData;
+import android.util.SparseIntArray;
+import android.text.Spannable;
+
+/**
+ * For dialing-only text entry
+ */
+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;
+ }
+
+ /**
+ * 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);
+ 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 & (KeyEvent.META_ALT_ON | KeyEvent.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/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java
new file mode 100644
index 0000000..99a3f1a
--- /dev/null
+++ b/core/java/android/text/method/DigitsKeyListener.java
@@ -0,0 +1,206 @@
+/*
+ * 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.Spanned;
+import android.text.SpannableStringBuilder;
+import android.view.KeyEvent;
+
+
+/**
+ * For digits-only text entry
+ */
+public class DigitsKeyListener extends NumberKeyListener
+{
+ private char[] mAccepted;
+ private boolean mSign;
+ private boolean mDecimal;
+
+ private static final int SIGN = 1;
+ private static final int DECIMAL = 2;
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return mAccepted;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ private static final char[][] CHARACTERS = new char[][] {
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.' },
+ };
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public DigitsKeyListener() {
+ this(false, false);
+ }
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public DigitsKeyListener(boolean sign, boolean decimal) {
+ mSign = sign;
+ mDecimal = decimal;
+
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+ mAccepted = CHARACTERS[kind];
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public static DigitsKeyListener getInstance() {
+ return getInstance(false, false);
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+
+ if (sInstance[kind] != null)
+ return sInstance[kind];
+
+ sInstance[kind] = new DigitsKeyListener(sign, decimal);
+ return sInstance[kind];
+ }
+
+ /**
+ * 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.
+ */
+ public static DigitsKeyListener getInstance(String accepted) {
+ // TODO: do we need a cache of these to avoid allocating?
+
+ DigitsKeyListener dim = new DigitsKeyListener();
+
+ dim.mAccepted = new char[accepted.length()];
+ accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
+
+ return dim;
+ }
+
+ @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 '-' or '.' characters.
+ */
+
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ sign = i;
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+ for (int i = dend; i < dlen; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ return ""; // Nothing can be inserted in front of a '-'.
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+
+ /*
+ * If it does, we must strip them out from the source.
+ * In addition, '-' must be the very first character,
+ * and nothing can be inserted before an existing '-'.
+ * 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 (c == '-') {
+ if (i != start || dstart != 0) {
+ strip = true;
+ } else if (sign >= 0) {
+ strip = true;
+ } else {
+ sign = i;
+ }
+ } else if (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;
+ }
+ }
+
+ private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
+}
diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java
new file mode 100644
index 0000000..ce18692
--- /dev/null
+++ b/core/java/android/text/method/HideReturnsTransformationMethod.java
@@ -0,0 +1,59 @@
+/*
+ * 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.GetChars;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * 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/core/java/android/text/method/KeyListener.java b/core/java/android/text/method/KeyListener.java
new file mode 100644
index 0000000..05ab72d
--- /dev/null
+++ b/core/java/android/text/method/KeyListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.text.*;
+import android.widget.TextView;
+
+public interface KeyListener
+{
+ /**
+ * If the key listener wants to handle this key, return true,
+ * otherwise return false and the caller (i.e. 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. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyUp(View view, Editable text,
+ int keyCode, KeyEvent event);
+}
diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java
new file mode 100644
index 0000000..92ac531
--- /dev/null
+++ b/core/java/android/text/method/LinkMovementMethod.java
@@ -0,0 +1,256 @@
+/*
+ * 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.content.Intent;
+import android.net.Uri;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+import android.text.style.*;
+import android.view.View;
+import android.widget.TextView;
+
+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 onKeyDown(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.getRepeatCount() == 0) {
+ if (action(CLICK, widget, buffer)) {
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(widget, buffer, keyCode, 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) {
+ boolean handled = false;
+
+ 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;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ 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[] link = buffer.getSpans(off, off, ClickableSpan.class);
+
+ if (link.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ link[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
+ buffer.getSpanStart(link[0]),
+ buffer.getSpanEnd(link[0]));
+ }
+
+ return true;
+ } else {
+ Selection.removeSelection(buffer);
+ }
+ }
+
+ return super.onTouchEvent(widget, buffer, event);
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.removeSelection(text);
+ text.removeSpan(FROM_BELOW);
+ }
+
+ 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 Object();
+}
diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java
new file mode 100644
index 0000000..2d75b87
--- /dev/null
+++ b/core/java/android/text/method/MetaKeyKeyListener.java
@@ -0,0 +1,250 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.View;
+import android.text.*;
+
+/**
+ * This base class encapsulates the behavior for handling the meta keys
+ * (caps, fn, sym). Key listener that care about meta state should
+ * inherit from it; you should not instantiate this class directly in a client.
+ */
+
+public abstract class MetaKeyKeyListener {
+ public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON;
+ public static final int META_ALT_ON = KeyEvent.META_ALT_ON;
+ public static final int META_SYM_ON = KeyEvent.META_SYM_ON;
+
+ public static final int META_CAP_LOCKED = KeyEvent.META_SHIFT_ON << 8;
+ public static final int META_ALT_LOCKED = KeyEvent.META_ALT_ON << 8;
+ public static final int META_SYM_LOCKED = KeyEvent.META_SYM_ON << 8;
+
+ private static final Object CAP = new Object();
+ private static final Object ALT = new Object();
+ private static final Object SYM = new Object();
+
+ /**
+ * Resets all meta state to inactive.
+ */
+ public static void resetMetaState(Spannable text) {
+ text.removeSpan(CAP);
+ text.removeSpan(ALT);
+ text.removeSpan(SYM);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Gets the state of a particular meta key.
+ *
+ * @param meta META_SHIFT_ON, META_ALT_ON, or 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, 1, 2);
+
+ case META_ALT_ON:
+ return getActive(text, ALT, 1, 2);
+
+ case META_SYM_ON:
+ return getActive(text, SYM, 1, 2);
+
+ default:
+ return 0;
+ }
+ }
+
+ 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 meta state in the specified text.
+ */
+ public static boolean isMetaTracker(CharSequence text, Object what) {
+ return what == CAP || what == ALT || what == SYM;
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ /**
+ * 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);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ release(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ release(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void release(Editable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == USED)
+ content.removeSpan(what);
+ else if (current == PRESSED)
+ content.setSpan(what, 0, 0, RELEASED);
+ }
+
+ /**
+ * 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/core/java/android/text/method/MovementMethod.java b/core/java/android/text/method/MovementMethod.java
new file mode 100644
index 0000000..9e37e59
--- /dev/null
+++ b/core/java/android/text/method/MovementMethod.java
@@ -0,0 +1,43 @@
+/*
+ * 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.widget.TextView;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+
+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);
+ 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);
+
+ /**
+ * 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/core/java/android/text/method/MultiTapKeyListener.java b/core/java/android/text/method/MultiTapKeyListener.java
new file mode 100644
index 0000000..7137d40
--- /dev/null
+++ b/core/java/android/text/method/MultiTapKeyListener.java
@@ -0,0 +1,285 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.*;
+import android.text.method.TextKeyListener.Capitalize;
+import android.widget.TextView;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+/**
+ * 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.
+ */
+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 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);
+ Timeout t = new Timeout(content);
+
+ return true;
+ }
+ if (Character.isUpperCase(current)) {
+ content.replace(selStart, selEnd,
+ String.valueOf(current).toLowerCase());
+ removeTimeouts(content);
+ Timeout t = new Timeout(content);
+
+ 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);
+ Timeout t = new Timeout(content);
+
+ 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);
+ Timeout t = new Timeout(content);
+
+ // 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/core/java/android/text/method/NumberKeyListener.java b/core/java/android/text/method/NumberKeyListener.java
new file mode 100644
index 0000000..348b658
--- /dev/null
+++ b/core/java/android/text/method/NumberKeyListener.java
@@ -0,0 +1,132 @@
+/*
+ * 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.view.KeyEvent;
+import android.view.View;
+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.util.SparseIntArray;
+
+/**
+ * For numeric text entry
+ */
+public abstract class NumberKeyListener extends BaseKeyListener
+ implements InputFilter
+{
+ /**
+ * You can say which characters you can accept.
+ */
+ protected abstract char[] getAcceptedChars();
+
+ protected int lookup(KeyEvent event, Spannable content) {
+ return event.getMatch(getAcceptedChars(), getMetaState(content));
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+}
diff --git a/core/java/android/text/method/PasswordTransformationMethod.java b/core/java/android/text/method/PasswordTransformationMethod.java
new file mode 100644
index 0000000..edaa836
--- /dev/null
+++ b/core/java/android/text/method/PasswordTransformationMethod.java
@@ -0,0 +1,260 @@
+/*
+ * 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.graphics.Rect;
+import android.view.View;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.Spannable;
+import android.text.style.UpdateLayout;
+
+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]);
+ }
+
+ 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) {
+ Visible[] old = sp.getSpans(0, sp.length(), Visible.class);
+ for (int i = 0; i < old.length; i++) {
+ sp.removeSpan(old[i]);
+ }
+
+ 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;
+
+ 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> {
+ public ViewReference(View v) {
+ super(v);
+ }
+ }
+
+ private static PasswordTransformationMethod sInstance;
+ private static char DOT = '\u2022';
+}
diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java
new file mode 100644
index 0000000..ae7ba8f
--- /dev/null
+++ b/core/java/android/text/method/QwertyKeyListener.java
@@ -0,0 +1,444 @@
+/*
+ * 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.Message;
+import android.os.Handler;
+import android.text.*;
+import android.text.method.TextKeyListener.Capitalize;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.HashMap;
+
+/**
+ * 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.
+ */
+public class QwertyKeyListener extends BaseKeyListener {
+ private static QwertyKeyListener[] sInstance =
+ new QwertyKeyListener[Capitalize.values().length * 2];
+
+ public QwertyKeyListener(Capitalize cap, boolean autotext) {
+ mAutoCap = cap;
+ mAutoText = autotext;
+ }
+
+ /**
+ * 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];
+ }
+
+ 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));
+
+ 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;
+ }
+ }
+
+ 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 == '"' || i == ')' || i == ']') &&
+ 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);
+
+ if (Character.isLetter(c)) {
+ content.replace(selEnd - 2, selEnd - 1, ".");
+ }
+ }
+ }
+ }
+
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_DEL && 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]);
+ 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);
+
+ 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('!', "\u00A1");
+ PICKER_SETS.put('<', "\u00AB");
+ PICKER_SETS.put('>', "\u00BB");
+ PICKER_SETS.put('?', "\u00BF");
+ PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5");
+ PICKER_SETS.put('C', "\u00C7");
+ PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB");
+ PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF");
+ PICKER_SETS.put('N', "\u00D1");
+ PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6");
+ PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC");
+ PICKER_SETS.put('Y', "\u00DD\u0178");
+ PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5");
+ PICKER_SETS.put('c', "\u00E7");
+ PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB");
+ PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF");
+ PICKER_SETS.put('n', "\u00F1");
+ PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6");
+ PICKER_SETS.put('s', "\u00A7\u00DF");
+ PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC");
+ PICKER_SETS.put('y', "\u00FD\u00FF");
+ PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT,
+ "\u2026\u00A5\u2022\u00AE\u00A9\u00B1");
+ };
+
+ 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
+ {
+ public Replaced(char[] text) {
+ mText = text;
+ }
+
+ private char[] mText;
+ }
+
+ private Capitalize mAutoCap;
+ private boolean mAutoText;
+}
+
diff --git a/core/java/android/text/method/ReplacementTransformationMethod.java b/core/java/android/text/method/ReplacementTransformationMethod.java
new file mode 100644
index 0000000..d6f879a
--- /dev/null
+++ b/core/java/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/core/java/android/text/method/ScrollingMovementMethod.java b/core/java/android/text/method/ScrollingMovementMethod.java
new file mode 100644
index 0000000..0438e1e
--- /dev/null
+++ b/core/java/android/text/method/ScrollingMovementMethod.java
@@ -0,0 +1,234 @@
+/*
+ * 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.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+import android.widget.TextView;
+import android.view.View;
+
+public class
+ScrollingMovementMethod
+implements MovementMethod
+{
+ /**
+ * Scrolls the text to the left if possible.
+ */
+ protected boolean left(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int scrolly = widget.getScrollY();
+ int scr = widget.getScrollX();
+ int em = Math.round(layout.getPaint().getFontSpacing());
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(scrolly);
+ int bottom = layout.getLineForVertical(scrolly + widget.getHeight() -
+ padding);
+ int left = Integer.MAX_VALUE;
+
+ for (int i = top; i <= bottom; i++) {
+ left = (int) Math.min(left, layout.getLineLeft(i));
+ }
+
+ if (scr > left) {
+ int s = Math.max(scr - em, left);
+ widget.scrollTo(s, widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text to the right if possible.
+ */
+ protected boolean right(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int scrolly = widget.getScrollY();
+ int scr = widget.getScrollX();
+ int em = Math.round(layout.getPaint().getFontSpacing());
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(scrolly);
+ int bottom = layout.getLineForVertical(scrolly + widget.getHeight() -
+ padding);
+ int right = 0;
+
+ for (int i = top; i <= bottom; i++) {
+ right = (int) Math.max(right, layout.getLineRight(i));
+ }
+
+ padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
+ if (scr < right - (widget.getWidth() - padding)) {
+ int s = Math.min(scr + em, right - (widget.getWidth() - padding));
+ widget.scrollTo(s, widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text up if possible.
+ */
+ protected boolean up(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int areatop = widget.getScrollY();
+ int line = layout.getLineForVertical(areatop);
+ int linetop = layout.getLineTop(line);
+
+ // If the top line is partially visible, bring it all the way
+ // into view; otherwise, bring the previous line into view.
+ if (areatop == linetop)
+ line--;
+
+ if (line >= 0) {
+ Touch.scrollTo(widget, layout,
+ widget.getScrollX(), layout.getLineTop(line));
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Scrolls the text down if possible.
+ */
+ protected boolean down(TextView widget, Spannable buffer) {
+ Layout layout = widget.getLayout();
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+
+ int areabot = widget.getScrollY() + widget.getHeight() - padding;
+ int line = layout.getLineForVertical(areabot);
+
+ if (layout.getLineTop(line+1) < areabot + 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.
+
+ line++;
+ }
+
+ if (line <= layout.getLineCount() - 1) {
+ widget.scrollTo(widget.getScrollX(), layout.getLineTop(line+1) -
+ (widget.getHeight() - padding));
+ Touch.scrollTo(widget, layout,
+ widget.getScrollX(), widget.getScrollY());
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled |= left(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled |= right(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ handled |= up(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ handled |= down(widget, buffer);
+ break;
+ }
+
+ return handled;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ return Touch.onTouchEvent(widget, buffer, event);
+ }
+
+ public boolean onTrackballEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = false;
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ for (; y < 0; y++) {
+ handled |= up(widget, buffer);
+ }
+ for (; y > 0; y--) {
+ handled |= down(widget, buffer);
+ }
+
+ for (; x < 0; x++) {
+ handled |= left(widget, buffer);
+ }
+ for (; x > 0; x--) {
+ handled |= right(widget, buffer);
+ }
+
+ return handled;
+ }
+
+ public void initialize(TextView widget, Spannable text) { }
+
+ public boolean canSelectArbitrarily() {
+ return false;
+ }
+
+ 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/core/java/android/text/method/SingleLineTransformationMethod.java b/core/java/android/text/method/SingleLineTransformationMethod.java
new file mode 100644
index 0000000..a4fcf15
--- /dev/null
+++ b/core/java/android/text/method/SingleLineTransformationMethod.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.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 any newline characters (\n) to be
+ * displayed as spaces instead of causing line breaks.
+ */
+public class SingleLineTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\n' };
+ private static char[] REPLACEMENT = new char[] { ' ' };
+
+ /**
+ * The character to be replaced is \n.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character \n is replaced with is 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/core/java/android/text/method/TextKeyListener.java b/core/java/android/text/method/TextKeyListener.java
new file mode 100644
index 0000000..012e41d
--- /dev/null
+++ b/core/java/android/text/method/TextKeyListener.java
@@ -0,0 +1,339 @@
+/*
+ * 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.*;
+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.
+ */
+public class TextKeyListener extends BaseKeyListener implements SpanWatcher {
+ private static TextKeyListener[] sInstance =
+ new TextKeyListener[Capitalize.values().length * 2];
+
+ /* package */ static final Object ACTIVE = new Object();
+ /* package */ static final Object CAPPED = new Object();
+ /* package */ static final Object INHIBIT_REPLACEMENT = new Object();
+ /* package */ static final Object LAST_TYPED = new Object();
+
+ 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;
+ }
+
+ // Back over allowed opening punctuation.
+
+ for (i = off; i > 0; i--) {
+ c = cs.charAt(i - 1);
+
+ if (c != '"' && c != '(' && c != '[' && c != '\'') {
+ break;
+ }
+ }
+
+ // Start of paragraph, with optional whitespace.
+
+ int j = i;
+ while (j > 0 && ((c = cs.charAt(j - 1)) == ' ' || c == '\t')) {
+ j--;
+ }
+ if (j == 0 || cs.charAt(j - 1) == '\n') {
+ return true;
+ }
+
+ // Or start of word if we are that style.
+
+ if (cap == Capitalize.WORDS) {
+ return i != j;
+ }
+
+ // There must be a space if not the start of paragraph.
+
+ if (i == j) {
+ return false;
+ }
+
+ // Back over allowed closing punctuation.
+
+ for (; j > 0; j--) {
+ c = cs.charAt(j - 1);
+
+ if (c != '"' && c != ')' && c != ']' && c != '\'') {
+ break;
+ }
+ }
+
+ if (j > 0) {
+ c = cs.charAt(j - 1);
+
+ if (c == '.' || c == '?' || c == '!') {
+ // Do not capitalize if the word ends with a period but
+ // also contains a period, in which case it is an abbreviation.
+
+ if (c == '.') {
+ for (int k = j - 2; k >= 0; k--) {
+ c = cs.charAt(k);
+
+ if (c == '.') {
+ return false;
+ }
+
+ if (!Character.isLetter(c)) {
+ break;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @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);
+ }
+
+ /**
+ * 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 = KeyCharacterMap.load(event.getKeyboardDevice());
+ int kind = kmap.getKeyboardType();
+
+ if (kind == KeyCharacterMap.ALPHA) {
+ return QwertyKeyListener.getInstance(mAutoText, mAutoCap);
+ } else if (kind == KeyCharacterMap.NUMERIC) {
+ return MultiTapKeyListener.getInstance(mAutoText, mAutoCap);
+ }
+
+ return NullKeyListener.getInstance();
+ }
+
+ public enum Capitalize {
+ NONE, SENTENCES, WORDS, CHARACTERS,
+ }
+
+ private static class NullKeyListener implements KeyListener
+ {
+ 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 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);
+ 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/core/java/android/text/method/TimeKeyListener.java b/core/java/android/text/method/TimeKeyListener.java
new file mode 100644
index 0000000..9ba1fe6
--- /dev/null
+++ b/core/java/android/text/method/TimeKeyListener.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;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering times in a text field.
+ */
+public class TimeKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static TimeKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new TimeKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * 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', 'a', 'm',
+ 'p', ':'
+ };
+
+ private static TimeKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java
new file mode 100644
index 0000000..bd01728
--- /dev/null
+++ b/core/java/android/text/method/Touch.java
@@ -0,0 +1,138 @@
+/*
+ * 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.Spannable;
+import android.view.MotionEvent;
+import android.view.View;
+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) {
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int top = layout.getLineForVertical(y);
+ int bottom = layout.getLineForVertical(y + widget.getHeight() -
+ padding);
+
+ int left = Integer.MAX_VALUE;
+ int 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));
+ }
+
+ padding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight();
+ x = Math.min(x, right - (widget.getWidth() - padding));
+ 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.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ buffer.setSpan(new DragState(event.getX(), event.getY()),
+ 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.getTouchSlop();
+
+ 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;
+
+ float dx = ds[0].mX - event.getX();
+ float 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);
+
+ scrollTo(widget, layout, nx, ny);
+ widget.cancelLongPress();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static class DragState {
+ public float mX;
+ public float mY;
+ public boolean mFarEnough;
+ public boolean mUsed;
+
+ public DragState(float x, float y) {
+ mX = x;
+ mY = y;
+ }
+ }
+}
diff --git a/core/java/android/text/method/TransformationMethod.java b/core/java/android/text/method/TransformationMethod.java
new file mode 100644
index 0000000..9f51c2a
--- /dev/null
+++ b/core/java/android/text/method/TransformationMethod.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+import android.widget.TextView;
+
+/**
+ * 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/core/java/android/text/method/package.html b/core/java/android/text/method/package.html
new file mode 100644
index 0000000..93698b8
--- /dev/null
+++ b/core/java/android/text/method/package.html
@@ -0,0 +1,21 @@
+<html>
+<body>
+
+<p>Provides classes that monitor or modify keypad input.</p>
+<p>You can use these classes to modify the type of keypad entry
+for your application, or decipher the keypresses entered for your specific
+entry method. For example:</p>
+<pre>
+// Set the text to password display style:
+EditText txtView = (EditText)findViewById(R.id.text);
+txtView.setTransformationMethod(PasswordTransformationMethod.getInstance());
+
+//Set the input style to numbers, rather than qwerty keyboard style.
+txtView.setInputMethod(DigitsInputMethod.getInstance());
+
+// Find out whether the caps lock is on.
+// 0 is no, 1 is yes, 2 is caps lock on.
+int active = MultiTapInputMethod.getCapsActive(txtView.getText());
+</pre>
+</body>
+</html>
diff --git a/core/java/android/text/package.html b/core/java/android/text/package.html
new file mode 100644
index 0000000..162dcd8
--- /dev/null
+++ b/core/java/android/text/package.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+
+<p>Provides classes used to render or track text and text spans on the screen.</p>
+<p>You can use these classes to design your own widgets that manage text,
+to handle arbitrary text spans for changes, or to handle drawing yourself
+for an existing widget.</p>
+<p>The Span&hellip; interfaces and classes are used to create or manage spans of
+text in a View item. You can use these to style the text or background, or to
+listen for changes. If creating your own widget, extend DynamicLayout, to manages
+the actual wrapping and drawing of your text.
+</body>
+</html>
diff --git a/core/java/android/text/style/AbsoluteSizeSpan.java b/core/java/android/text/style/AbsoluteSizeSpan.java
new file mode 100644
index 0000000..8f6ed5a
--- /dev/null
+++ b/core/java/android/text/style/AbsoluteSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class AbsoluteSizeSpan extends MetricAffectingSpan {
+
+ private int mSize;
+
+ public AbsoluteSizeSpan(int size) {
+ mSize = size;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+}
diff --git a/core/java/android/text/style/AlignmentSpan.java b/core/java/android/text/style/AlignmentSpan.java
new file mode 100644
index 0000000..d51edcc
--- /dev/null
+++ b/core/java/android/text/style/AlignmentSpan.java
@@ -0,0 +1,39 @@
+/*
+ * 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.style;
+
+import android.text.Layout;
+
+public interface AlignmentSpan
+extends ParagraphStyle
+{
+ public Layout.Alignment getAlignment();
+
+ public static class Standard
+ implements AlignmentSpan
+ {
+ public Standard(Layout.Alignment align) {
+ mAlignment = align;
+ }
+
+ public Layout.Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ private Layout.Alignment mAlignment;
+ }
+}
diff --git a/core/java/android/text/style/BackgroundColorSpan.java b/core/java/android/text/style/BackgroundColorSpan.java
new file mode 100644
index 0000000..be6ef77
--- /dev/null
+++ b/core/java/android/text/style/BackgroundColorSpan.java
@@ -0,0 +1,37 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class BackgroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public BackgroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getBackgroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.bgColor = mColor;
+ }
+}
diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java
new file mode 100644
index 0000000..70c4d33
--- /dev/null
+++ b/core/java/android/text/style/BulletSpan.java
@@ -0,0 +1,76 @@
+/*
+ * 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.style;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.Spanned;
+
+public class BulletSpan implements LeadingMarginSpan {
+
+ public BulletSpan() {
+ mGapWidth = STANDARD_GAP_WIDTH;
+ }
+
+ public BulletSpan(int gapWidth) {
+ mGapWidth = gapWidth;
+ }
+
+ public BulletSpan(int gapWidth, int color) {
+ mGapWidth = gapWidth;
+ mWantColor = true;
+ mColor = color;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return 2 * BULLET_RADIUS + mGapWidth;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout l) {
+ if (((Spanned) text).getSpanStart(this) == start) {
+ Paint.Style style = p.getStyle();
+ int oldcolor = 0;
+
+ if (mWantColor) {
+ oldcolor = p.getColor();
+ p.setColor(mColor);
+ }
+
+ p.setStyle(Paint.Style.FILL);
+
+ c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f,
+ BULLET_RADIUS, p);
+
+ if (mWantColor) {
+ p.setColor(oldcolor);
+ }
+
+ p.setStyle(style);
+ }
+ }
+
+ private int mGapWidth;
+ private boolean mWantColor;
+ private int mColor;
+
+ private static final int BULLET_RADIUS = 3;
+ public static final int STANDARD_GAP_WIDTH = 2;
+}
diff --git a/core/java/android/text/style/CharacterStyle.java b/core/java/android/text/style/CharacterStyle.java
new file mode 100644
index 0000000..7620d29
--- /dev/null
+++ b/core/java/android/text/style/CharacterStyle.java
@@ -0,0 +1,87 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting extend this
+ * class. Most also extend {@link MetricAffectingSpan}.
+ */
+public abstract class CharacterStyle {
+ public abstract void updateDrawState(TextPaint tp);
+
+ /**
+ * A given CharacterStyle can only applied to a single region of a given
+ * Spanned. If you need to attach the same CharacterStyle to multiple
+ * regions, you can use this method to wrap it with a new object that
+ * will have the same effect but be a distinct object so that it can
+ * also be attached without conflict.
+ */
+ public static CharacterStyle wrap(CharacterStyle cs) {
+ if (cs instanceof MetricAffectingSpan) {
+ return new MetricAffectingSpan.Passthrough((MetricAffectingSpan) cs);
+ } else {
+ return new Passthrough(cs);
+ }
+ }
+
+ /**
+ * Returns "this" for most CharacterStyles, but for CharacterStyles
+ * that were generated by {@link #wrap}, returns the underlying
+ * CharacterStyle.
+ */
+ public CharacterStyle getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough CharacterStyle is one that
+ * passes {@link #updateDrawState} calls through to the
+ * specified CharacterStyle while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified CharacterStyle is already attached.
+ */
+ private static class Passthrough extends CharacterStyle {
+ private CharacterStyle mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied CharacterStyle.
+ */
+ public Passthrough(CharacterStyle cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying CharacterStyle.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Returns the CharacterStyle underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public CharacterStyle getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ClickableSpan.java b/core/java/android/text/style/ClickableSpan.java
new file mode 100644
index 0000000..a232ed7
--- /dev/null
+++ b/core/java/android/text/style/ClickableSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+import android.view.View;
+
+/**
+ * If an object of this type is attached to the text of a TextView
+ * with a movement method of LinkMovementMethod, the affected spans of
+ * text can be selected. If clicked, the {@link #onClick} method will
+ * be called.
+ */
+public abstract class ClickableSpan extends CharacterStyle {
+
+ /**
+ * Performs the click action associated with this span.
+ */
+ public abstract void onClick(View widget);
+
+ /**
+ * Makes the text underlined and in the link color.
+ */
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(ds.linkColor);
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java
new file mode 100644
index 0000000..3c471a5
--- /dev/null
+++ b/core/java/android/text/style/DrawableMarginSpan.java
@@ -0,0 +1,79 @@
+/*
+ * 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.style;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class DrawableMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public DrawableMarginSpan(Drawable b) {
+ mDrawable = b;
+ }
+
+ public DrawableMarginSpan(Drawable b, int pad) {
+ mDrawable = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mDrawable.getIntrinsicWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int ix = (int)x;
+ int itop = (int)layout.getLineTop(layout.getLineForOffset(st));
+
+ int dw = mDrawable.getIntrinsicWidth();
+ int dh = mDrawable.getIntrinsicHeight();
+
+ if (dir < 0)
+ x -= dw;
+
+ // XXX What to do about Paint?
+ mDrawable.setBounds(ix, itop, ix+dw, itop+dh);
+ mDrawable.draw(c);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mDrawable.getIntrinsicHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Drawable mDrawable;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java
new file mode 100644
index 0000000..3bcc335
--- /dev/null
+++ b/core/java/android/text/style/DynamicDrawableSpan.java
@@ -0,0 +1,82 @@
+/*
+ * 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.style;
+
+import java.lang.ref.WeakReference;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+/**
+ *
+ */
+public abstract class DynamicDrawableSpan
+extends ReplacementSpan
+{
+ /**
+ * Your subclass must implement this method to provide the bitmap
+ * to be drawn. The dimensions of the bitmap must be the same
+ * from each call to the next.
+ */
+ public abstract Drawable getDrawable();
+
+ public int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm) {
+ Drawable b = getCachedDrawable();
+
+ if (fm != null) {
+ fm.ascent = -b.getIntrinsicHeight();
+ fm.descent = 0;
+
+ fm.top = fm.ascent;
+ fm.bottom = 0;
+ }
+
+ return b.getIntrinsicWidth();
+ }
+
+ public void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint) {
+ Drawable b = getCachedDrawable();
+ canvas.save();
+
+ canvas.translate(x, bottom-b.getIntrinsicHeight());;
+ b.draw(canvas);
+ canvas.restore();
+ }
+
+ private Drawable getCachedDrawable() {
+ WeakReference wr = mDrawableRef;
+ Drawable b = null;
+
+ if (wr != null)
+ b = (Drawable) wr.get();
+
+ if (b == null) {
+ b = getDrawable();
+ mDrawableRef = new WeakReference(b);
+ }
+
+ return b;
+ }
+
+ private WeakReference mDrawableRef;
+}
+
diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java
new file mode 100644
index 0000000..5cccd9c
--- /dev/null
+++ b/core/java/android/text/style/ForegroundColorSpan.java
@@ -0,0 +1,38 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ForegroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public ForegroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getForegroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(mColor);
+ }
+}
diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java
new file mode 100644
index 0000000..c786a17
--- /dev/null
+++ b/core/java/android/text/style/IconMarginSpan.java
@@ -0,0 +1,73 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class IconMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public IconMarginSpan(Bitmap b) {
+ mBitmap = b;
+ }
+
+ public IconMarginSpan(Bitmap b, int pad) {
+ mBitmap = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mBitmap.getWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int itop = layout.getLineTop(layout.getLineForOffset(st));
+
+ if (dir < 0)
+ x -= mBitmap.getWidth();
+
+ c.drawBitmap(mBitmap, x, itop, p);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mBitmap.getHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Bitmap mBitmap;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java
new file mode 100644
index 0000000..de067b1
--- /dev/null
+++ b/core/java/android/text/style/ImageSpan.java
@@ -0,0 +1,99 @@
+/*
+ * 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.style;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.InputStream;
+
+public class ImageSpan extends DynamicDrawableSpan {
+ private Drawable mDrawable;
+ private Uri mContentUri;
+ private int mResourceId;
+ private Context mContext;
+ private String mSource;
+
+
+ public ImageSpan(Bitmap b) {
+ mDrawable = new BitmapDrawable(b);
+ mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(),
+ mDrawable.getIntrinsicHeight());
+ }
+
+ public ImageSpan(Drawable d) {
+ mDrawable = d;
+ }
+
+ public ImageSpan(Drawable d, String source) {
+ mDrawable = d;
+ mSource = source;
+ }
+
+ public ImageSpan(Context context, Uri uri) {
+ mContext = context;
+ mContentUri = uri;
+ }
+
+ public ImageSpan(Context context, int resourceId) {
+ mContext = context;
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public Drawable getDrawable() {
+ Drawable drawable = null;
+
+ if (mDrawable != null) {
+ drawable = mDrawable;
+ } else if (mContentUri != null) {
+ Bitmap bitmap = null;
+ try {
+ InputStream is = mContext.getContentResolver().openInputStream(
+ mContentUri);
+ bitmap = BitmapFactory.decodeStream(is);
+ drawable = new BitmapDrawable(bitmap);
+ is.close();
+ } catch (Exception e) {
+ Log.e("sms", "Failed to loaded content " + mContentUri, e);
+ }
+ } else {
+ try {
+ drawable = mContext.getResources().getDrawable(mResourceId);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+ } catch (Exception e) {
+ Log.e("sms", "Unable to find resource: " + mResourceId);
+ }
+ }
+
+ return drawable;
+ }
+
+ /**
+ * Returns the source string that was saved during construction.
+ */
+ public String getSource() {
+ return mSource;
+ }
+
+}
diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java
new file mode 100644
index 0000000..85a27dc
--- /dev/null
+++ b/core/java/android/text/style/LeadingMarginSpan.java
@@ -0,0 +1,59 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LeadingMarginSpan
+extends ParagraphStyle
+{
+ public int getLeadingMargin(boolean first);
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout);
+
+ public static class Standard
+ implements LeadingMarginSpan
+ {
+ public Standard(int first, int rest) {
+ mFirst = first;
+ mRest = rest;
+ }
+
+ public Standard(int every) {
+ this(every, every);
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return first ? mFirst : mRest;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ ;
+ }
+
+ private int mFirst, mRest;
+ }
+}
diff --git a/core/java/android/text/style/LineBackgroundSpan.java b/core/java/android/text/style/LineBackgroundSpan.java
new file mode 100644
index 0000000..854aeaf
--- /dev/null
+++ b/core/java/android/text/style/LineBackgroundSpan.java
@@ -0,0 +1,30 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+public interface LineBackgroundSpan
+extends ParagraphStyle
+{
+ public void drawBackground(Canvas c, Paint p,
+ int left, int right,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ int lnum);
+}
diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java
new file mode 100644
index 0000000..c0ef97c
--- /dev/null
+++ b/core/java/android/text/style/LineHeightSpan.java
@@ -0,0 +1,29 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LineHeightSpan
+extends ParagraphStyle, WrapTogetherSpan
+{
+ public void chooseHeight(CharSequence text, int start, int end,
+ int spanstartv, int v,
+ Paint.FontMetricsInt fm);
+}
diff --git a/core/java/android/text/style/MaskFilterSpan.java b/core/java/android/text/style/MaskFilterSpan.java
new file mode 100644
index 0000000..781bcec
--- /dev/null
+++ b/core/java/android/text/style/MaskFilterSpan.java
@@ -0,0 +1,39 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.MaskFilter;
+import android.text.TextPaint;
+
+public class MaskFilterSpan extends CharacterStyle {
+
+ private MaskFilter mFilter;
+
+ public MaskFilterSpan(MaskFilter filter) {
+ mFilter = filter;
+ }
+
+ public MaskFilter getMaskFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setMaskFilter(mFilter);
+ }
+}
diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java
new file mode 100644
index 0000000..92558eb
--- /dev/null
+++ b/core/java/android/text/style/MetricAffectingSpan.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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * changes the width or height of characters extend this class.
+ */
+public abstract class MetricAffectingSpan
+extends CharacterStyle
+implements UpdateLayout {
+
+ public abstract void updateMeasureState(TextPaint p);
+
+ /**
+ * Returns "this" for most MetricAffectingSpans, but for
+ * MetricAffectingSpans that were generated by {@link #wrap},
+ * returns the underlying MetricAffectingSpan.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough MetricAffectingSpan is one that
+ * passes {@link #updateDrawState} and {@link #updateMeasureState}
+ * calls through to the specified MetricAffectingSpan
+ * while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified MetricAffectingSpan is already attached.
+ */
+ /* package */ static class Passthrough extends MetricAffectingSpan {
+ private MetricAffectingSpan mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied MetricAffectingSpan.
+ */
+ public Passthrough(MetricAffectingSpan cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Passes updateMeasureState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ mStyle.updateMeasureState(tp);
+ }
+
+ /**
+ * Returns the MetricAffectingSpan underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ParagraphStyle.java b/core/java/android/text/style/ParagraphStyle.java
new file mode 100644
index 0000000..423156e
--- /dev/null
+++ b/core/java/android/text/style/ParagraphStyle.java
@@ -0,0 +1,26 @@
+/*
+ * 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.style;
+
+/**
+ * The classes that affect paragraph-level text formatting implement
+ * this interface.
+ */
+public interface ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/QuoteSpan.java b/core/java/android/text/style/QuoteSpan.java
new file mode 100644
index 0000000..3f4a32f
--- /dev/null
+++ b/core/java/android/text/style/QuoteSpan.java
@@ -0,0 +1,64 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Layout;
+
+public class QuoteSpan
+implements LeadingMarginSpan
+{
+ private static final int STRIPE_WIDTH = 2;
+ private static final int GAP_WIDTH = 2;
+
+ private int mColor = 0xff0000ff;
+
+ public QuoteSpan() {
+ super();
+ }
+
+ public QuoteSpan(int color) {
+ this();
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return STRIPE_WIDTH + GAP_WIDTH;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(mColor);
+
+ c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/core/java/android/text/style/RasterizerSpan.java b/core/java/android/text/style/RasterizerSpan.java
new file mode 100644
index 0000000..193c700
--- /dev/null
+++ b/core/java/android/text/style/RasterizerSpan.java
@@ -0,0 +1,39 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Rasterizer;
+import android.text.TextPaint;
+
+public class RasterizerSpan extends CharacterStyle {
+
+ private Rasterizer mRasterizer;
+
+ public RasterizerSpan(Rasterizer r) {
+ mRasterizer = r;
+ }
+
+ public Rasterizer getRasterizer() {
+ return mRasterizer;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setRasterizer(mRasterizer);
+ }
+}
diff --git a/core/java/android/text/style/RelativeSizeSpan.java b/core/java/android/text/style/RelativeSizeSpan.java
new file mode 100644
index 0000000..a8ad076
--- /dev/null
+++ b/core/java/android/text/style/RelativeSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class RelativeSizeSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public RelativeSizeSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getSizeChange() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/ReplacementSpan.java b/core/java/android/text/style/ReplacementSpan.java
new file mode 100644
index 0000000..26c725f
--- /dev/null
+++ b/core/java/android/text/style/ReplacementSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.TextPaint;
+
+public abstract class ReplacementSpan extends MetricAffectingSpan {
+
+ public abstract int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm);
+ public abstract void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint);
+
+ /**
+ * This method does nothing, since ReplacementSpans are measured
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateMeasureState(TextPaint p) { }
+
+ /**
+ * This method does nothing, since ReplacementSpans are drawn
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateDrawState(TextPaint ds) { }
+}
diff --git a/core/java/android/text/style/ScaleXSpan.java b/core/java/android/text/style/ScaleXSpan.java
new file mode 100644
index 0000000..ac9e35d
--- /dev/null
+++ b/core/java/android/text/style/ScaleXSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ScaleXSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public ScaleXSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getScaleX() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/StrikethroughSpan.java b/core/java/android/text/style/StrikethroughSpan.java
new file mode 100644
index 0000000..01ae38c
--- /dev/null
+++ b/core/java/android/text/style/StrikethroughSpan.java
@@ -0,0 +1,27 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class StrikethroughSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setStrikeThruText(true);
+ }
+}
diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java
new file mode 100644
index 0000000..cc8b06c
--- /dev/null
+++ b/core/java/android/text/style/StyleSpan.java
@@ -0,0 +1,93 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ *
+ * Describes a style in a span.
+ * Note that styles are cumulative -- if both bold and italic are set in
+ * separate spans, or if the base style is bold and a span calls for italic,
+ * you get bold italic. You can't turn off a style from the base style.
+ *
+ */
+public class StyleSpan extends MetricAffectingSpan {
+
+ private int mStyle;
+
+ /**
+ *
+ * @param style An integer constant describing the style for this span. Examples
+ * include bold, italic, and normal. Values are constants defined
+ * in {@link android.graphics.Typeface}.
+ */
+ public StyleSpan(int style) {
+ mStyle = style;
+ }
+
+ /**
+ * Returns the style constant defined in {@link android.graphics.Typeface}.
+ */
+ public int getStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mStyle);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mStyle);
+ }
+
+ private static void apply(Paint paint, int style) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ int want = oldStyle | style;
+
+ Typeface tf;
+ if (old == null) {
+ tf = Typeface.defaultFromStyle(want);
+ } else {
+ tf = Typeface.create(old, want);
+ }
+
+ int fake = want & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/SubscriptSpan.java b/core/java/android/text/style/SubscriptSpan.java
new file mode 100644
index 0000000..78d6ba9
--- /dev/null
+++ b/core/java/android/text/style/SubscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class SubscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/SuperscriptSpan.java b/core/java/android/text/style/SuperscriptSpan.java
new file mode 100644
index 0000000..79be4de
--- /dev/null
+++ b/core/java/android/text/style/SuperscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class SuperscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/TabStopSpan.java b/core/java/android/text/style/TabStopSpan.java
new file mode 100644
index 0000000..e5b7644
--- /dev/null
+++ b/core/java/android/text/style/TabStopSpan.java
@@ -0,0 +1,37 @@
+/*
+ * 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.style;
+
+public interface TabStopSpan
+extends ParagraphStyle
+{
+ public int getTabStop();
+
+ public static class Standard
+ implements TabStopSpan
+ {
+ public Standard(int where) {
+ mTab = where;
+ }
+
+ public int getTabStop() {
+ return mTab;
+ }
+
+ private int mTab;
+ }
+}
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
new file mode 100644
index 0000000..c4ec976
--- /dev/null
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -0,0 +1,198 @@
+/*
+ * 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.style;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Sets the text color, size, style, and typeface to match a TextAppearance
+ * resource.
+ */
+public class TextAppearanceSpan extends MetricAffectingSpan {
+ private String mTypeface;
+ private int mStyle;
+ private int mTextSize;
+ private ColorStateList mTextColor;
+ private ColorStateList mTextColorLink;
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance. The <code>appearance</code> should be, for example,
+ * <code>android.R.style.TextAppearance_Small</code>.
+ */
+ public TextAppearanceSpan(Context context, int appearance) {
+ this(context, appearance, -1);
+ }
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance, and the specified text color resource
+ * to determine the color. The <code>appearance</code> should be,
+ * for example, <code>android.R.style.TextAppearance_Small</code>,
+ * and the <code>colorList</code> should be, for example,
+ * <code>android.R.styleable.Theme_textColorDim</code>.
+ */
+ public TextAppearanceSpan(Context context, int appearance,
+ int colorList) {
+ TypedArray a =
+ context.obtainStyledAttributes(appearance,
+ com.android.internal.R.styleable.TextAppearance);
+
+ mTextColor = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ mTextColorLink = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorLink);
+ mTextSize = a.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, -1);
+
+ mStyle = a.getInt(com.android.internal.R.styleable.TextAppearance_textStyle, 0);
+ int tf = a.getInt(com.android.internal.R.styleable.TextAppearance_typeface, 0);
+
+ switch (tf) {
+ case 1:
+ mTypeface = "sans";
+ break;
+
+ case 2:
+ mTypeface = "serif";
+ break;
+
+ case 3:
+ mTypeface = "monospace";
+ break;
+ }
+
+ a.recycle();
+
+ if (colorList >= 0) {
+ a = context.obtainStyledAttributes(com.android.internal.R.style.Theme,
+ com.android.internal.R.styleable.Theme);
+
+ mTextColor = a.getColorStateList(colorList);
+ a.recycle();
+ }
+ }
+
+ /**
+ * Makes text be drawn with the specified typeface, size, style,
+ * and colors.
+ */
+ public TextAppearanceSpan(String family, int style, int size,
+ ColorStateList color, ColorStateList linkColor) {
+ mTypeface = family;
+ mStyle = style;
+ mTextSize = size;
+ mTextColor = color;
+ mTextColorLink = linkColor;
+ }
+
+ /**
+ * Returns the typeface family specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public String getFamily() {
+ return mTypeface;
+ }
+
+ /**
+ * Returns the text color specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public ColorStateList getTextColor() {
+ return mTextColor;
+ }
+
+ /**
+ * Returns the link color specified by this span, or <code>null</code>
+ * if it does not specify one.
+ */
+ public ColorStateList getLinkTextColor() {
+ return mTextColorLink;
+ }
+
+ /**
+ * Returns the text size specified by this span, or <code>-1</code>
+ * if it does not specify one.
+ */
+ public int getTextSize() {
+ return mTextSize;
+ }
+
+ /**
+ * Returns the text style specified by this span, or <code>0</code>
+ * if it does not specify one.
+ */
+ public int getTextStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ updateMeasureState(ds);
+
+ if (mTextColor != null) {
+ ds.setColor(mTextColor.getColorForState(ds.drawableState, 0));
+ }
+
+ if (mTextColorLink != null) {
+ ds.linkColor = mTextColor.getColorForState(ds.drawableState, 0);
+ }
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ if (mTypeface != null || mStyle != 0) {
+ Typeface tf = ds.getTypeface();
+ int style = 0;
+
+ if (tf != null) {
+ style = tf.getStyle();
+ }
+
+ style |= mStyle;
+
+ if (mTypeface != null) {
+ tf = Typeface.create(mTypeface, style);
+ } else if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ int fake = style & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ ds.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ ds.setTextSkewX(-0.25f);
+ }
+
+ ds.setTypeface(tf);
+ }
+
+ if (mTextSize > 0) {
+ ds.setTextSize(mTextSize);
+ }
+ }
+}
diff --git a/core/java/android/text/style/TypefaceSpan.java b/core/java/android/text/style/TypefaceSpan.java
new file mode 100644
index 0000000..7519ac2
--- /dev/null
+++ b/core/java/android/text/style/TypefaceSpan.java
@@ -0,0 +1,77 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Changes the typeface family of the text to which the span is attached.
+ */
+public class TypefaceSpan extends MetricAffectingSpan {
+ private String mFamily;
+
+ /**
+ * @param family The font family for this typeface. Examples include
+ * "monospace", "serif", and "sans-serif".
+ */
+ public TypefaceSpan(String family) {
+ mFamily = family;
+ }
+
+ /**
+ * Returns the font family name.
+ */
+ public String getFamily() {
+ return mFamily;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mFamily);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mFamily);
+ }
+
+ private static void apply(Paint paint, String family) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ Typeface tf = Typeface.create(family, oldStyle);
+ int fake = oldStyle & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/URLSpan.java b/core/java/android/text/style/URLSpan.java
new file mode 100644
index 0000000..79809b5
--- /dev/null
+++ b/core/java/android/text/style/URLSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextPaint;
+import android.view.View;
+
+public class URLSpan extends ClickableSpan {
+
+ private String mURL;
+
+ public URLSpan(String url) {
+ mURL = url;
+ }
+
+ public String getURL() {
+ return mURL;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ Uri uri = Uri.parse(getURL());
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ widget.getContext().startActivity(intent);
+ }
+}
diff --git a/core/java/android/text/style/UnderlineSpan.java b/core/java/android/text/style/UnderlineSpan.java
new file mode 100644
index 0000000..5dce0f2
--- /dev/null
+++ b/core/java/android/text/style/UnderlineSpan.java
@@ -0,0 +1,27 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class UnderlineSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/UpdateLayout.java b/core/java/android/text/style/UpdateLayout.java
new file mode 100644
index 0000000..211685a
--- /dev/null
+++ b/core/java/android/text/style/UpdateLayout.java
@@ -0,0 +1,24 @@
+/*
+ * 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.style;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * triggers a text layout update when one is added or remove must implement
+ * this interface.
+ */
+public interface UpdateLayout { }
diff --git a/core/java/android/text/style/WrapTogetherSpan.java b/core/java/android/text/style/WrapTogetherSpan.java
new file mode 100644
index 0000000..11721a8
--- /dev/null
+++ b/core/java/android/text/style/WrapTogetherSpan.java
@@ -0,0 +1,23 @@
+/*
+ * 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.style;
+
+public interface WrapTogetherSpan
+extends ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/package.html b/core/java/android/text/style/package.html
new file mode 100644
index 0000000..0a8520c
--- /dev/null
+++ b/core/java/android/text/style/package.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+
+<p>Provides classes used to view or change the style of a span of text in a View object.
+The classes with a subclass Standard are passed in to {@link android.text.SpannableString#setSpan(java.lang.Object, int, int, int)
+SpannableString.setSpan()} or {@link android.text.SpannableStringBuilder#setSpan(java.lang.Object, int, int, int)
+SpannableStringBuilder.setSpan()} to add a new styled span to a string in a View object.
+
+</body>
+</html>
diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java
new file mode 100644
index 0000000..79ecfbd
--- /dev/null
+++ b/core/java/android/text/util/Linkify.java
@@ -0,0 +1,533 @@
+/*
+ * 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.util;
+
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.text.style.URLSpan;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.webkit.WebView;
+import android.widget.TextView;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Linkify take a piece of text and a regular expression and turns all of the
+ * regex matches in the text into clickable links. This is particularly
+ * useful for matching things like email addresses, web urls, etc. and making
+ * them actionable.
+ *
+ * Alone with the pattern that is to be matched, a url scheme prefix is also
+ * required. Any pattern match that does not begin with the supplied scheme
+ * will have the scheme prepended to the matched text when the clickable url
+ * is created. For instance, if you are matching web urls you would supply
+ * the scheme <code>http://</code>. If the pattern matches example.com, which
+ * does not have a url scheme prefix, the supplied scheme will be prepended to
+ * create <code>http://example.com</code> when the clickable url link is
+ * created.
+ */
+
+public class Linkify {
+ /**
+ * Bit field indicating that web URLs should be matched in methods that
+ * take an options mask
+ */
+ public static final int WEB_URLS = 0x01;
+
+ /**
+ * Bit field indicating that email addresses should be matched in methods
+ * that take an options mask
+ */
+ public static final int EMAIL_ADDRESSES = 0x02;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int PHONE_NUMBERS = 0x04;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int MAP_ADDRESSES = 0x08;
+
+ /**
+ * Bit mask indicating that all available patterns should be matched in
+ * methods that take an options mask
+ */
+ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS
+ | MAP_ADDRESSES;
+
+ /**
+ * Don't treat anything with fewer than this many digits as a
+ * phone number.
+ */
+ private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
+
+ /**
+ * Filters out web URL matches that occur after an at-sign (@). This is
+ * to prevent turning the domain name in an email address into a web link.
+ */
+ public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ if (start == 0) {
+ return true;
+ }
+
+ if (s.charAt(start - 1) == '@') {
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ /**
+ * Filters out URL matches that don't have enough digits to be a
+ * phone number.
+ */
+ public static final MatchFilter sPhoneNumberMatchFilter =
+ new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ int digitCount = 0;
+
+ for (int i = start; i < end; i++) {
+ if (Character.isDigit(s.charAt(i))) {
+ digitCount++;
+ if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ /**
+ * Transforms matched phone number text into something suitable
+ * to be used in a tel: URL. It does this by removing everything
+ * but the digits and plus signs. For instance:
+ * &apos;+1 (919) 555-1212&apos;
+ * becomes &apos;+19195551212&apos;
+ */
+ public static final TransformFilter sPhoneNumberTransformFilter =
+ new TransformFilter() {
+ public final String transformUrl(final Matcher match, String url) {
+ return Regex.digitsAndPlusOnly(match);
+ }
+ };
+
+ /**
+ * MatchFilter enables client code to have more control over
+ * what is allowed to match and become a link, and what is not.
+ *
+ * For example: when matching web urls you would like things like
+ * http://www.example.com to match, as well as just example.com itelf.
+ * However, you would not want to match against the domain in
+ * support@example.com. So, when matching against a web url pattern you
+ * might also include a MatchFilter that disallows the match if it is
+ * immediately preceded by an at-sign (@).
+ */
+ public interface MatchFilter {
+ /**
+ * Examines the character span matched by the pattern and determines
+ * if the match should be turned into an actionable link.
+ *
+ * @param s The body of text against which the pattern
+ * was matched
+ * @param start The index of the first character in s that was
+ * matched by the pattern - inclusive
+ * @param end The index of the last character in s that was
+ * matched - exclusive
+ *
+ * @return Whether this match should be turned into a link
+ */
+ boolean acceptMatch(CharSequence s, int start, int end);
+ }
+
+ /**
+ * TransformFilter enables client code to have more control over
+ * how matched patterns are represented as URLs.
+ *
+ * For example: when converting a phone number such as (919) 555-1212
+ * into a tel: URL the parentheses, white space, and hyphen need to be
+ * removed to produce tel:9195551212.
+ */
+ public interface TransformFilter {
+ /**
+ * Examines the matched text and either passes it through or uses the
+ * data in the Matcher state to produce a replacement.
+ *
+ * @param match The regex matcher state that found this URL text
+ * @param url The text that was matched
+ *
+ * @return The transformed form of the URL
+ */
+ String transformUrl(final Matcher match, String url);
+ }
+
+ /**
+ * Scans the text of the provided Spannable and turns all occurrences
+ * of the link types indicated in the mask into clickable links.
+ * If the mask is nonzero, it also removes any existing URLSpans
+ * attached to the Spannable, to avoid problems if you call it
+ * repeatedly on the same text.
+ */
+ public static final boolean addLinks(Spannable text, int mask) {
+ if (mask == 0) {
+ return false;
+ }
+
+ URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
+
+ for (int i = old.length - 1; i >= 0; i--) {
+ text.removeSpan(old[i]);
+ }
+
+ ArrayList<LinkSpec> links = new ArrayList<LinkSpec>();
+
+ if ((mask & WEB_URLS) != 0) {
+ gatherLinks(links, text, Regex.WEB_URL_PATTERN,
+ new String[] { "http://", "https://" },
+ sUrlMatchFilter, null);
+ }
+
+ if ((mask & EMAIL_ADDRESSES) != 0) {
+ gatherLinks(links, text, Regex.EMAIL_ADDRESS_PATTERN,
+ new String[] { "mailto:" },
+ null, null);
+ }
+
+ if ((mask & PHONE_NUMBERS) != 0) {
+ gatherLinks(links, text, Regex.PHONE_PATTERN,
+ new String[] { "tel:" },
+ sPhoneNumberMatchFilter, sPhoneNumberTransformFilter);
+ }
+
+ if ((mask & MAP_ADDRESSES) != 0) {
+ gatherMapLinks(links, text);
+ }
+
+ pruneOverlaps(links);
+
+ if (links.size() == 0) {
+ return false;
+ }
+
+ for (LinkSpec link: links) {
+ applyLink(link.url, link.start, link.end, text);
+ }
+
+ return true;
+ }
+
+ /**
+ * Scans the text of the provided TextView and turns all occurrences of
+ * the link types indicated in the mask into clickable links. If matches
+ * are found the movement method for the TextView is set to
+ * LinkMovementMethod.
+ */
+ public static final boolean addLinks(TextView text, int mask) {
+ if (mask == 0) {
+ return false;
+ }
+
+ CharSequence t = text.getText();
+
+ if (t instanceof Spannable) {
+ if (addLinks((Spannable) t, mask)) {
+ addLinkMovementMethod(text);
+ return true;
+ }
+
+ return false;
+ } else {
+ SpannableString s = SpannableString.valueOf(t);
+
+ if (addLinks(s, mask)) {
+ addLinkMovementMethod(text);
+ text.setText(s);
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private static final void addLinkMovementMethod(TextView t) {
+ MovementMethod m = t.getMovementMethod();
+
+ if ((m == null) || !(m instanceof LinkMovementMethod)) {
+ if (t.getLinksClickable()) {
+ t.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ /**
+ * Applies a regex to the text of a TextView turning the matches into
+ * links. If links are found then UrlSpans are applied to the link
+ * text match areas, and the movement method for the text is changed
+ * to LinkMovementMethod.
+ *
+ * @param text TextView whose text is to be marked-up with links
+ * @param pattern Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final void addLinks(TextView text, Pattern pattern,
+ String scheme) {
+ addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to the text of a TextView turning the matches into
+ * links. If links are found then UrlSpans are applied to the link
+ * text match areas, and the movement method for the text is changed
+ * to LinkMovementMethod.
+ *
+ * @param text TextView whose text is to be marked-up with links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final void addLinks(TextView text, Pattern p, String scheme,
+ MatchFilter matchFilter, TransformFilter transformFilter) {
+ SpannableString s = SpannableString.valueOf(text.getText());
+
+ if (addLinks(s, p, scheme, matchFilter, transformFilter)) {
+ text.setText(s);
+ addLinkMovementMethod(text);
+ }
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param text Spannable whose text is to be marked-up with
+ * links
+ * @param pattern Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final boolean addLinks(Spannable text, Pattern pattern,
+ String scheme) {
+ return addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param s Spannable whose text is to be marked-up with
+ * links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg <code>http://</code> to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final boolean addLinks(Spannable s, Pattern p,
+ String scheme, MatchFilter matchFilter,
+ TransformFilter transformFilter) {
+ boolean hasMatches = false;
+ String prefix = (scheme == null) ? "" : scheme.toLowerCase();
+ Matcher m = p.matcher(s);
+
+ while (m.find()) {
+ int start = m.start();
+ int end = m.end();
+ boolean allowed = true;
+
+ if (matchFilter != null) {
+ allowed = matchFilter.acceptMatch(s, start, end);
+ }
+
+ if (allowed) {
+ String url = makeUrl(m.group(0), new String[] { prefix },
+ m, transformFilter);
+
+ applyLink(url, start, end, s);
+ hasMatches = true;
+ }
+ }
+
+ return hasMatches;
+ }
+
+ private static final void applyLink(String url, int start, int end,
+ Spannable text) {
+ URLSpan span = new URLSpan(url);
+
+ text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static final String makeUrl(String url, String[] prefixes,
+ Matcher m, TransformFilter filter) {
+ if (filter != null) {
+ url = filter.transformUrl(m, url);
+ }
+
+ boolean hasPrefix = false;
+ for (int i = 0; i < prefixes.length; i++) {
+ if (url.regionMatches(true, 0, prefixes[i], 0,
+ prefixes[i].length())) {
+ hasPrefix = true;
+ break;
+ }
+ }
+ if (!hasPrefix) {
+ url = prefixes[0] + url;
+ }
+
+ return url;
+ }
+
+ private static final void gatherLinks(ArrayList<LinkSpec> links,
+ Spannable s, Pattern pattern, String[] schemes,
+ MatchFilter matchFilter, TransformFilter transformFilter) {
+ Matcher m = pattern.matcher(s);
+
+ while (m.find()) {
+ int start = m.start();
+ int end = m.end();
+
+ if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
+ LinkSpec spec = new LinkSpec();
+ String url = makeUrl(m.group(0), schemes, m, transformFilter);
+
+ spec.url = url;
+ spec.start = start;
+ spec.end = end;
+
+ links.add(spec);
+ }
+ }
+ }
+
+ private static final void gatherMapLinks(ArrayList<LinkSpec> links,
+ Spannable s) {
+ String string = s.toString();
+ String address;
+ int base = 0;
+ while ((address = WebView.findAddress(string)) != null) {
+ int start = string.indexOf(address);
+ if (start < 0) {
+ break;
+ }
+ LinkSpec spec = new LinkSpec();
+ int length = address.length();
+ int end = start + length;
+ spec.start = base + start;
+ spec.end = base + end;
+ string = string.substring(end);
+ base += end;
+
+ String encodedAddress = null;
+ try {
+ encodedAddress = URLEncoder.encode(address,"UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ continue;
+ }
+ spec.url = "geo:0,0?q=" + encodedAddress;
+ links.add(spec);
+ }
+ }
+
+ private static final void pruneOverlaps(ArrayList<LinkSpec> links) {
+ Comparator<LinkSpec> c = new Comparator<LinkSpec>() {
+ public final int compare(LinkSpec a, LinkSpec b) {
+ if (a.start < b.start) {
+ return -1;
+ }
+
+ if (a.start > b.start) {
+ return 1;
+ }
+
+ if (a.end < b.end) {
+ return 1;
+ }
+
+ if (a.end > b.end) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ public final boolean equals(Object o) {
+ return false;
+ }
+ };
+
+ Collections.sort(links, c);
+
+ int len = links.size();
+ int i = 0;
+
+ while (i < len - 1) {
+ LinkSpec a = links.get(i);
+ LinkSpec b = links.get(i + 1);
+ int remove = -1;
+
+ if ((a.start <= b.start) && (a.end > b.start)) {
+ if (b.end <= a.end) {
+ remove = i + 1;
+ } else if ((a.end - a.start) > (b.end - b.start)) {
+ remove = i + 1;
+ } else if ((a.end - a.start) < (b.end - b.start)) {
+ remove = i;
+ }
+
+ if (remove != -1) {
+ links.remove(remove);
+ len--;
+ continue;
+ }
+
+ }
+
+ i++;
+ }
+ }
+}
+
+class LinkSpec {
+ String url;
+ int start;
+ int end;
+}
diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java
new file mode 100644
index 0000000..55ad140
--- /dev/null
+++ b/core/java/android/text/util/Regex.java
@@ -0,0 +1,192 @@
+/*
+ * 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.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @hide
+ */
+public class Regex {
+ /**
+ * Regular expression pattern to match all IANA top-level domains.
+ * List accurate as of 2007/06/15. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py
+ */
+ public static final Pattern TOP_LEVEL_DOMAIN_PATTERN
+ = Pattern.compile(
+ "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(biz|b[abdefghijmnorstvwyz])"
+ + "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(info|int|i[delmnoqrst])"
+ + "|(jobs|j[emop])"
+ + "|k[eghimnrwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(mil|mobi|museum|m[acdghklmnopqrstuvwxyz])"
+ + "|(name|net|n[acefgilopruz])"
+ + "|(org|om)"
+ + "|(pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eouw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agkmsyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|y[etu]"
+ + "|z[amw])");
+
+ /**
+ * Regular expression pattern to match RFC 1738 URLs
+ * List accurate as of 2007/06/15. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py
+ */
+ public static final Pattern WEB_URL_PATTERN
+ = Pattern.compile(
+ "((?:(http|https):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2}))+)?\\@)?)?"
+ + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+" // named host
+ + "(?:" // plus top level domain
+ + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(?:biz|b[abdefghijmnorstvwyz])"
+ + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(?:edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(?:gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(?:info|int|i[delmnoqrst])"
+ + "|(?:jobs|j[emop])"
+ + "|k[eghimnrwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(?:mil|mobi|museum|m[acdghklmnopqrstuvwxyz])"
+ + "|(?:name|net|n[acefgilopruz])"
+ + "|(?:org|om)"
+ + "|(?:pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eouw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agkmsyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|y[etu]"
+ + "|z[amw]))"
+ + "|(?:(?:25[0-5]|2[0-4]" // or ip address
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
+ + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9])))"
+ + "(?:\\:\\d{1,5})?)" // plus option port number
+ + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params
+ + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?"
+ + "\\b"); // and finally, a word boundary this is to stop foo.sure from matching as foo.su
+
+ public static final Pattern IP_ADDRESS_PATTERN
+ = Pattern.compile(
+ "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9]))");
+
+ public static final Pattern DOMAIN_NAME_PATTERN
+ = Pattern.compile(
+ "(((([a-zA-Z0-9][a-zA-Z0-9\\-]*)*[a-zA-Z0-9]\\.)+"
+ + TOP_LEVEL_DOMAIN_PATTERN + ")|"
+ + IP_ADDRESS_PATTERN + ")");
+
+ public static final Pattern EMAIL_ADDRESS_PATTERN
+ = Pattern.compile(
+ "[a-zA-Z0-9\\+\\.\\_\\%\\-]+" +
+ "\\@" +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]*" +
+ "(" +
+ "\\." +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]*" +
+ ")+"
+ );
+
+ /**
+ * This pattern is intended for searching for things that look like they
+ * might be phone numbers in arbitrary text, not for validating whether
+ * something is in fact a phone number. It will miss many things that
+ * are legitimate phone numbers.
+ */
+ public static final Pattern PHONE_PATTERN
+ = Pattern.compile(
+ "(?:\\+[0-9]+)|(?:[0-9()][0-9()\\- \\.][0-9()\\- \\.]+[0-9])");
+
+ /**
+ * Convenience method to take all of the non-null matching groups in a
+ * regex Matcher and return them as a concatenated string.
+ *
+ * @param matcher The Matcher object from which grouped text will
+ * be extracted
+ *
+ * @return A String comprising all of the non-null matched
+ * groups concatenated together
+ */
+ public static final String concatGroups(Matcher matcher) {
+ StringBuilder b = new StringBuilder();
+ final int numGroups = matcher.groupCount();
+
+ for (int i = 1; i <= numGroups; i++) {
+ String s = matcher.group(i);
+
+ System.err.println("Group(" + i + ") : " + s);
+
+ if (s != null) {
+ b.append(s);
+ }
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Convenience method to return only the digits and plus signs
+ * in the matching string.
+ *
+ * @param matcher The Matcher object from which digits and plus will
+ * be extracted
+ *
+ * @return A String comprising all of the digits and plus in
+ * the match
+ */
+ public static final String digitsAndPlusOnly(Matcher matcher) {
+ StringBuilder buffer = new StringBuilder();
+ String matchingRegion = matcher.group();
+
+ for (int i = 0, size = matchingRegion.length(); i < size; i++) {
+ char character = matchingRegion.charAt(i);
+
+ if (character == '+' || Character.isDigit(character)) {
+ buffer.append(character);
+ }
+ }
+ return buffer.toString();
+ }
+}
diff --git a/core/java/android/text/util/Rfc822Token.java b/core/java/android/text/util/Rfc822Token.java
new file mode 100644
index 0000000..e6472df
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Token.java
@@ -0,0 +1,172 @@
+/*
+ * 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.util;
+
+/**
+ * This class stores an RFC 822-like name, address, and comment,
+ * and provides methods to convert them to quoted strings.
+ */
+public class Rfc822Token {
+ private String mName, mAddress, mComment;
+
+ /**
+ * Creates a new Rfc822Token with the specified name, address,
+ * and comment.
+ */
+ public Rfc822Token(String name, String address, String comment) {
+ mName = name;
+ mAddress = address;
+ mComment = comment;
+ }
+
+ /**
+ * Returns the name part.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the address part.
+ */
+ public String getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * Returns the comment part.
+ */
+ public String getComment() {
+ return mComment;
+ }
+
+ /**
+ * Changes the name to the specified name.
+ */
+ public void setName(String name) {
+ mName = name;
+ }
+
+ /**
+ * Changes the address to the specified address.
+ */
+ public void setAddress(String address) {
+ mAddress = address;
+ }
+
+ /**
+ * Changes the comment to the specified comment.
+ */
+ public void setComment(String comment) {
+ mComment = comment;
+ }
+
+ /**
+ * Returns the name (with quoting added if necessary),
+ * the comment (in parentheses), and the address (in angle brackets).
+ * This should be suitable for inclusion in an RFC 822 address list.
+ */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ if (mName != null && mName.length() != 0) {
+ sb.append(quoteNameIfNecessary(mName));
+ sb.append(' ');
+ }
+
+ if (mComment != null && mComment.length() != 0) {
+ sb.append('(');
+ sb.append(quoteComment(mComment));
+ sb.append(") ");
+ }
+
+ if (mAddress != null && mAddress.length() != 0) {
+ sb.append('<');
+ sb.append(mAddress);
+ sb.append('>');
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the name, conservatively quoting it if there are any
+ * characters that are likely to cause trouble outside of a
+ * quoted string, or returning it literally if it seems safe.
+ */
+ public static String quoteNameIfNecessary(String name) {
+ int len = name.length();
+
+ for (int i = 0; i < len; i++) {
+ char c = name.charAt(i);
+
+ if (! ((c >= 'A' && i <= 'Z') ||
+ (c >= 'a' && c <= 'z') ||
+ (c == ' ') ||
+ (c >= '0' && c <= '9'))) {
+ return '"' + quoteName(name) + '"';
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Returns the name, with internal backslashes and quotation marks
+ * preceded by backslashes. The outer quote marks themselves are not
+ * added by this method.
+ */
+ public static String quoteName(String name) {
+ StringBuilder sb = new StringBuilder();
+
+ int len = name.length();
+ for (int i = 0; i < len; i++) {
+ char c = name.charAt(i);
+
+ if (c == '\\' || c == '"') {
+ sb.append('\\');
+ }
+
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the comment, with internal backslashes and parentheses
+ * preceded by backslashes. The outer parentheses themselves are
+ * not added by this method.
+ */
+ public static String quoteComment(String comment) {
+ int len = comment.length();
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < len; i++) {
+ char c = comment.charAt(i);
+
+ if (c == '(' || c == ')' || c == '\\') {
+ sb.append('\\');
+ }
+
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+}
+
diff --git a/core/java/android/text/util/Rfc822Tokenizer.java b/core/java/android/text/util/Rfc822Tokenizer.java
new file mode 100644
index 0000000..d4e78b0
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Tokenizer.java
@@ -0,0 +1,292 @@
+/*
+ * 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.util;
+
+import android.widget.MultiAutoCompleteTextView;
+
+import java.util.ArrayList;
+
+/**
+ * This class works as a Tokenizer for MultiAutoCompleteTextView for
+ * address list fields, and also provides a method for converting
+ * a string of addresses (such as might be typed into such a field)
+ * into a series of Rfc822Tokens.
+ */
+public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer {
+ /**
+ * This constructor will try to take a string like
+ * "Foo Bar (something) &lt;foo\@google.com&gt;,
+ * blah\@google.com (something)"
+ * and convert it into one or more Rfc822Tokens.
+ * It does *not* decode MIME encoded-words; charset conversion
+ * must already have taken place if necessary.
+ * It will try to be tolerant of broken syntax instead of
+ * returning an error.
+ */
+ public static Rfc822Token[] tokenize(CharSequence text) {
+ ArrayList<Rfc822Token> out = new ArrayList<Rfc822Token>();
+ StringBuilder name = new StringBuilder();
+ StringBuilder address = new StringBuilder();
+ StringBuilder comment = new StringBuilder();
+
+ int i = 0;
+ int cursor = text.length();
+
+ while (i < cursor) {
+ char c = text.charAt(i);
+
+ if (c == ',' || c == ';') {
+ i++;
+
+ while (i < cursor && text.charAt(i) == ' ') {
+ i++;
+ }
+
+ crunch(name);
+
+ if (address.length() > 0) {
+ out.add(new Rfc822Token(name.toString(),
+ address.toString(),
+ comment.toString()));
+ } else if (name.length() > 0) {
+ out.add(new Rfc822Token(null,
+ name.toString(),
+ comment.toString()));
+ }
+
+ name.setLength(0);
+ address.setLength(0);
+ address.setLength(0);
+ } else if (c == '"') {
+ i++;
+
+ while (i < cursor) {
+ c = text.charAt(i);
+
+ if (c == '"') {
+ i++;
+ break;
+ } else if (c == '\\') {
+ name.append(text.charAt(i + 1));
+ i += 2;
+ } else {
+ name.append(c);
+ i++;
+ }
+ }
+ } else if (c == '(') {
+ int level = 1;
+ i++;
+
+ while (i < cursor && level > 0) {
+ c = text.charAt(i);
+
+ if (c == ')') {
+ if (level > 1) {
+ comment.append(c);
+ }
+
+ level--;
+ i++;
+ } else if (c == '(') {
+ comment.append(c);
+ level++;
+ i++;
+ } else if (c == '\\') {
+ comment.append(text.charAt(i + 1));
+ i += 2;
+ } else {
+ comment.append(c);
+ i++;
+ }
+ }
+ } else if (c == '<') {
+ i++;
+
+ while (i < cursor) {
+ c = text.charAt(i);
+
+ if (c == '>') {
+ i++;
+ break;
+ } else {
+ address.append(c);
+ i++;
+ }
+ }
+ } else if (c == ' ') {
+ name.append('\0');
+ i++;
+ } else {
+ name.append(c);
+ i++;
+ }
+ }
+
+ crunch(name);
+
+ if (address.length() > 0) {
+ out.add(new Rfc822Token(name.toString(),
+ address.toString(),
+ comment.toString()));
+ } else if (name.length() > 0) {
+ out.add(new Rfc822Token(null,
+ name.toString(),
+ comment.toString()));
+ }
+
+ return out.toArray(new Rfc822Token[out.size()]);
+ }
+
+ private static void crunch(StringBuilder sb) {
+ int i = 0;
+ int len = sb.length();
+
+ while (i < len) {
+ char c = sb.charAt(i);
+
+ if (c == '\0') {
+ if (i == 0 || i == len - 1 ||
+ sb.charAt(i - 1) == ' ' ||
+ sb.charAt(i - 1) == '\0' ||
+ sb.charAt(i + 1) == ' ' ||
+ sb.charAt(i + 1) == '\0') {
+ sb.deleteCharAt(i);
+ len--;
+ } else {
+ i++;
+ }
+ } else {
+ i++;
+ }
+ }
+
+ for (i = 0; i < len; i++) {
+ if (sb.charAt(i) == '\0') {
+ sb.setCharAt(i, ' ');
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int findTokenStart(CharSequence text, int cursor) {
+ /*
+ * It's hard to search backward, so search forward until
+ * we reach the cursor.
+ */
+
+ int best = 0;
+ int i = 0;
+
+ while (i < cursor) {
+ i = findTokenEnd(text, i);
+
+ if (i < cursor) {
+ i++; // Skip terminating punctuation
+
+ while (i < cursor && text.charAt(i) == ' ') {
+ i++;
+ }
+
+ if (i < cursor) {
+ best = i;
+ }
+ }
+ }
+
+ return best;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int findTokenEnd(CharSequence text, int cursor) {
+ int len = text.length();
+ int i = cursor;
+
+ while (i < len) {
+ char c = text.charAt(i);
+
+ if (c == ',' || c == ';') {
+ return i;
+ } else if (c == '"') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '"') {
+ i++;
+ break;
+ } else if (c == '\\') {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '(') {
+ int level = 1;
+ i++;
+
+ while (i < len && level > 0) {
+ c = text.charAt(i);
+
+ if (c == ')') {
+ level--;
+ i++;
+ } else if (c == '(') {
+ level++;
+ i++;
+ } else if (c == '\\') {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '<') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '>') {
+ i++;
+ break;
+ } else {
+ i++;
+ }
+ }
+ } else {
+ i++;
+ }
+ }
+
+ return i;
+ }
+
+ /**
+ * Terminates the specified address with a comma and space.
+ * This assumes that the specified text already has valid syntax.
+ * The Adapter subclass's convertToString() method must make that
+ * guarantee.
+ */
+ public CharSequence terminateToken(CharSequence text) {
+ return text + ", ";
+ }
+}
diff --git a/core/java/android/text/util/Rfc822Validator.java b/core/java/android/text/util/Rfc822Validator.java
new file mode 100644
index 0000000..9f03bb0
--- /dev/null
+++ b/core/java/android/text/util/Rfc822Validator.java
@@ -0,0 +1,128 @@
+/*
+ * 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.util;
+
+import android.widget.AutoCompleteTextView;
+
+import java.util.regex.Pattern;
+
+/**
+ * This class works as a Validator for AutoCompleteTextView for
+ * email addresses. If a token does not appear to be a valid address,
+ * it is trimmed of characters that cannot legitimately appear in one
+ * and has the specified domain name added. It is meant for use with
+ * {@link Rfc822Token} and {@link Rfc822Tokenizer}.
+ *
+ * @deprecated In the future make sure we don't quietly alter the user's
+ * text in ways they did not intend. Meanwhile, hide this
+ * class from the public API because it does not even have
+ * a full understanding of the syntax it claims to correct.
+ * @hide
+ */
+public class Rfc822Validator implements AutoCompleteTextView.Validator {
+ /*
+ * Regex.EMAIL_ADDRESS_PATTERN hardcodes the TLD that we accept, but we
+ * want to make sure we will keep accepting email addresses with TLD's
+ * that don't exist at the time of this writing, so this regexp relaxes
+ * that constraint by accepting any kind of top level domain, not just
+ * ".com", ".fr", etc...
+ */
+ private static final Pattern EMAIL_ADDRESS_PATTERN =
+ Pattern.compile("[^\\s@]+@[^\\s@]+\\.[a-zA-z][a-zA-Z][a-zA-Z]*");
+
+ private String mDomain;
+
+ /**
+ * Constructs a new validator that uses the specified domain name as
+ * the default when none is specified.
+ */
+ public Rfc822Validator(String domain) {
+ mDomain = domain;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isValid(CharSequence text) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text);
+
+ return tokens.length == 1 &&
+ EMAIL_ADDRESS_PATTERN.
+ matcher(tokens[0].getAddress()).matches();
+ }
+
+ /**
+ * @return a string in which all the characters that are illegal for the username
+ * part of the email address have been removed.
+ */
+ private String removeIllegalCharacters(String s) {
+ StringBuilder result = new StringBuilder();
+ int length = s.length();
+ for (int i = 0; i < length; i++) {
+ char c = s.charAt(i);
+
+ /*
+ * An RFC822 atom can contain any ASCII printing character
+ * except for periods and any of the following punctuation.
+ * A local-part can contain multiple atoms, concatenated by
+ * periods, so do allow periods here.
+ */
+
+ if (c <= ' ' || c > '~') {
+ continue;
+ }
+
+ if (c == '(' || c == ')' || c == '<' || c == '>' ||
+ c == '@' || c == ',' || c == ';' || c == ':' ||
+ c == '\\' || c == '"' || c == '[' || c == ']') {
+ continue;
+ }
+
+ result.append(c);
+ }
+ return result.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public CharSequence fixText(CharSequence cs) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs);
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < tokens.length; i++) {
+ String text = tokens[i].getAddress();
+ int index = text.indexOf('@');
+ if (index < 0) {
+ // If there is no @, just append the domain of the account
+ tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain);
+ } else {
+ // Otherwise, remove everything right of the '@' and append the domain
+ // ("a@b" becomes "a@gmail.com").
+ String fix = removeIllegalCharacters(text.substring(0, index));
+ tokens[i].setAddress(fix + "@" + mDomain);
+ }
+
+ sb.append(tokens[i].toString());
+ if (i + 1 < tokens.length) {
+ sb.append(", ");
+ }
+ }
+
+ return sb;
+ }
+}
diff --git a/core/java/android/text/util/package.html b/core/java/android/text/util/package.html
new file mode 100644
index 0000000..d9312aa2
--- /dev/null
+++ b/core/java/android/text/util/package.html
@@ -0,0 +1,6 @@
+<HTML>
+<BODY>
+Utilities for converting identifiable text strings into clickable links
+and creating RFC 822-type message (SMTP) tokens.
+</BODY>
+</HTML> \ No newline at end of file