Copy only the spans we are interested in.

The PARAGRAPH type of span is dangerous, as concatenating
CharSequences that contain it may crash. We also don't use
other spans than SuggestionSpans, so we don't copy them.

Bug: 10622489
Change-Id: If4e44eca3cdc5bb02cf2e0c8c44ecd4bf27fae57
main
Jean Chalard 2013-09-12 19:11:59 +09:00
parent 3ca9c5cbec
commit 3d68b06662
3 changed files with 129 additions and 2 deletions

View File

@ -607,8 +607,11 @@ public final class RichInputConnection {
}
}
return new TextRange(TextUtils.concat(before, after), startIndexInBefore,
before.length() + endIndexInAfter, before.length());
// We don't use TextUtils#concat because it copies all spans without respect to their
// nature. If the text includes a PARAGRAPH span and it has been split, then
// TextUtils#concat will crash when it tries to concat both sides of it.
return new TextRange(StringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
startIndexInBefore, before.length() + endIndexInAfter, before.length());
}
public boolean isCursorTouchingWord(final SettingsValues settingsValues) {

View File

@ -20,7 +20,12 @@ import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.settings.SettingsValues;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import android.util.JsonReader;
import android.util.JsonWriter;
import android.util.Log;
@ -462,4 +467,88 @@ public final class StringUtils {
}
return "";
}
/**
* Copies the spans from the region <code>start...end</code> in
* <code>source</code> to the region
* <code>destoff...destoff+end-start</code> in <code>dest</code>.
* Spans in <code>source</code> that begin before <code>start</code>
* or end after <code>end</code> but overlap this range are trimmed
* as if they began at <code>start</code> or ended at <code>end</code>.
* Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied.
*
* This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the
* kind of span that is copied.
*
* @throws IndexOutOfBoundsException if any of the copied spans
* are out of range in <code>dest</code>.
*/
public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
Spannable dest, int destoff) {
Object[] spans = source.getSpans(start, end, SuggestionSpan.class);
for (int i = 0; i < spans.length; i++) {
int fl = source.getSpanFlags(spans[i]);
if (0 != (fl & Spannable.SPAN_PARAGRAPH)) continue;
int st = source.getSpanStart(spans[i]);
int en = source.getSpanEnd(spans[i]);
if (st < start)
st = start;
if (en > end)
en = end;
dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
fl);
}
}
/**
* Returns a CharSequence concatenating the specified CharSequences, retaining their
* SuggestionSpans that don't have the PARAGRAPH flag, but not other spans.
*
* This code is almost entirely taken from {@link TextUtils#concat(CharSequence...)}, except
* it calls copyNonParagraphSuggestionSpansFrom instead of {@link TextUtils#copySpansFrom}.
*/
public static CharSequence concatWithNonParagraphSuggestionSpansOnly(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) {
copyNonParagraphSuggestionSpansFrom((Spanned) text[i], 0, len, ss, off);
}
off += len;
}
return new SpannedString(ss);
}
}

View File

@ -20,6 +20,11 @@ import com.android.inputmethod.latin.settings.SettingsValues;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.style.SuggestionSpan;
import android.text.style.URLSpan;
import android.text.SpannableStringBuilder;
import android.text.Spannable;
import android.text.Spanned;
import java.util.Arrays;
import java.util.List;
@ -280,4 +285,34 @@ public class StringUtilsTests extends AndroidTestCase {
assertEquals(objs[i], newObjArray.get(i));
}
}
public void testConcatWithSuggestionSpansOnly() {
SpannableStringBuilder s = new SpannableStringBuilder("test string\ntest string\n"
+ "test string\ntest string\ntest string\ntest string\ntest string\ntest string\n"
+ "test string\ntest string\n");
final int N = 10;
for (int i = 0; i < N; ++i) {
// Put a PARAGRAPH-flagged span that should not be found in the result.
s.setSpan(new SuggestionSpan(getContext(),
new String[] {"" + i}, Spannable.SPAN_PARAGRAPH),
i * 12, i * 12 + 12, Spannable.SPAN_PARAGRAPH);
// Put a normal suggestion span that should be found in the result.
s.setSpan(new SuggestionSpan(getContext(), new String[] {"" + i}, 0), i, i * 2, 0);
// Put a URL span than should not be found in the result.
s.setSpan(new URLSpan("http://a"), i, i * 2, 0);
}
final CharSequence a = s.subSequence(0, 15);
final CharSequence b = s.subSequence(15, s.length());
final Spanned result =
(Spanned)StringUtils.concatWithNonParagraphSuggestionSpansOnly(a, b);
Object[] spans = result.getSpans(0, result.length(), SuggestionSpan.class);
for (int i = 0; i < spans.length; i++) {
final int flags = result.getSpanFlags(spans[i]);
assertEquals("Should not find a span with PARAGRAPH flag",
flags & Spannable.SPAN_PARAGRAPH, 0);
assertTrue("Should be a SuggestionSpan", spans[i] instanceof SuggestionSpan);
}
}
}