blob: 352767c57613edbba1062c0e3c5245751deab28b [file] [log] [blame]
/*
* 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.cts;
import java.util.ArrayList;
import android.test.AndroidTestCase;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
/**
* Test {@link SpannableStringBuilder}.
*/
public class SpannableStringBuilderSpanTest extends AndroidTestCase {
private static final boolean DEBUG = false;
private SpanSet mSpanSet = new SpanSet();
private SpanSet mReplacementSpanSet = new SpanSet();
private int testCounter;
public void testReplaceWithSpans() {
testCounter = 0;
String originals[] = { "", "A", "here", "Well, hello there" };
String replacements[] = { "", "X", "test", "longer replacement" };
for (String original: originals) {
for (String replacement: replacements) {
replace(original, replacement);
}
}
}
private void replace(String original, String replacement) {
PositionSet positionSet = new PositionSet(4);
positionSet.addPosition(0);
positionSet.addPosition(original.length() / 3);
positionSet.addPosition(2 * original.length() / 3);
positionSet.addPosition(original.length());
PositionSet replPositionSet = new PositionSet(4);
replPositionSet.addPosition(0);
replPositionSet.addPosition(replacement.length() / 3);
replPositionSet.addPosition(2 * replacement.length() / 3);
replPositionSet.addPosition(replacement.length());
for (int s = 0; s < positionSet.size(); s++) {
for (int e = s; e < positionSet.size(); e++) {
for (int rs = 0; rs < replPositionSet.size(); rs++) {
for (int re = rs; re < replPositionSet.size(); re++) {
replaceWithRange(original,
positionSet.getPosition(s), positionSet.getPosition(e),
replacement,
replPositionSet.getPosition(rs), replPositionSet.getPosition(re));
}
}
}
}
}
private void replaceWithRange(String original, int replaceStart, int replaceEnd,
String replacement, int replacementStart, int replacementEnd) {
int flags[] = { Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, Spanned.SPAN_INCLUSIVE_INCLUSIVE,
Spanned.SPAN_EXCLUSIVE_INCLUSIVE, Spanned.SPAN_INCLUSIVE_EXCLUSIVE };
for (int flag: flags) {
replaceWithSpanFlag(original, replaceStart, replaceEnd,
replacement, replacementStart, replacementEnd, flag);
}
}
private void replaceWithSpanFlag(String original, int replaceStart, int replaceEnd,
String replacement, int replacementStart, int replacementEnd, int flag) {
testCounter++;
int debugTestNumber = -1;
if (debugTestNumber >= 0 && testCounter != debugTestNumber) return;
String subReplacement = replacement.substring(replacementStart, replacementEnd);
String expected = original.substring(0, replaceStart) +
subReplacement + original.substring(replaceEnd, original.length());
if (DEBUG) System.out.println("#" + testCounter + ", replace \"" + original + "\" [" +
replaceStart + " " + replaceEnd + "] by \"" + subReplacement + "\" -> \"" +
expected + "\", flag=" + flag);
SpannableStringBuilder originalSpannable = new SpannableStringBuilder(original);
Spannable replacementSpannable = new SpannableStringBuilder(replacement);
mSpanSet.initSpans(originalSpannable, replaceStart, replaceEnd, flag);
mReplacementSpanSet.initSpans(replacementSpannable, replacementStart, replacementEnd, flag);
originalSpannable.replace(replaceStart, replaceEnd, replacementSpannable,
replacementStart, replacementEnd);
assertEquals(expected, originalSpannable.toString());
checkSpanPositions(originalSpannable, replaceStart, replaceEnd, subReplacement.length(),
flag);
checkReplacementSpanPositions(originalSpannable, replaceStart, replacementSpannable,
replacementStart, replacementEnd, flag);
}
private void checkSpanPositions(Spannable spannable, int replaceStart, int replaceEnd,
int replacementLength, int flag) {
int count = 0;
int replacedLength = replaceEnd - replaceStart;
int delta = replacementLength - replacedLength;
boolean textIsReplaced = replacedLength > 0 && replacementLength > 0;
for (int s = 0; s < mSpanSet.mPositionSet.size(); s++) {
for (int e = s; e < mSpanSet.mPositionSet.size(); e++) {
Object span = mSpanSet.mSpans[count];
int originalStart = mSpanSet.mPositionSet.getPosition(s);
int originalEnd = mSpanSet.mPositionSet.getPosition(e);
int start = spannable.getSpanStart(span);
int end = spannable.getSpanEnd(span);
int startStyle = mSpanSet.mSpanStartPositionStyle[count];
int endStyle = mSpanSet.mSpanEndPositionStyle[count];
count++;
if (!isValidSpan(originalStart, originalEnd, flag)) continue;
if (DEBUG) System.out.println(" " + originalStart + "," + originalEnd + " -> " +
start + "," + end + " | " + startStyle + " " + endStyle +
" delta=" + delta);
// This is the exception to the following generic code where we need to consider
// both the start and end styles.
if (startStyle == SpanSet.INSIDE && endStyle == SpanSet.INSIDE &&
flag == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE &&
(replacementLength == 0 || originalStart > replaceStart ||
originalEnd < replaceEnd)) {
// 0-length spans should have been removed
assertEquals(-1, start);
assertEquals(-1, end);
mSpanSet.mRecorder.assertRemoved(span, originalStart, originalEnd);
continue;
}
switch (startStyle) {
case SpanSet.BEFORE:
assertEquals(originalStart, start);
break;
case SpanSet.INSIDE:
switch (flag) {
case Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:
case Spanned.SPAN_EXCLUSIVE_INCLUSIVE:
// start is POINT
if (originalStart == replaceStart && textIsReplaced) {
assertEquals(replaceStart, start);
} else {
assertEquals(replaceStart + replacementLength, start);
}
break;
case Spanned.SPAN_INCLUSIVE_INCLUSIVE:
case Spanned.SPAN_INCLUSIVE_EXCLUSIVE:
// start is MARK
if (originalStart == replaceEnd && textIsReplaced) {
assertEquals(replaceStart + replacementLength, start);
} else {
assertEquals(replaceStart, start);
}
break;
case Spanned.SPAN_PARAGRAPH:
fail("TODO");
break;
}
break;
case SpanSet.AFTER:
assertEquals(originalStart + delta, start);
break;
}
switch (endStyle) {
case SpanSet.BEFORE:
assertEquals(originalEnd, end);
break;
case SpanSet.INSIDE:
switch (flag) {
case Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:
case Spanned.SPAN_INCLUSIVE_EXCLUSIVE:
// end is MARK
if (originalEnd == replaceEnd && textIsReplaced) {
assertEquals(replaceStart + replacementLength, end);
} else {
assertEquals(replaceStart, end);
}
break;
case Spanned.SPAN_INCLUSIVE_INCLUSIVE:
case Spanned.SPAN_EXCLUSIVE_INCLUSIVE:
// end is POINT
if (originalEnd == replaceStart && textIsReplaced) {
assertEquals(replaceStart, end);
} else {
assertEquals(replaceStart + replacementLength, end);
}
break;
case Spanned.SPAN_PARAGRAPH:
fail("TODO");
break;
}
break;
case SpanSet.AFTER:
assertEquals(originalEnd + delta, end);
break;
}
if (start != originalStart || end != originalEnd) {
mSpanSet.mRecorder.assertChanged(span, originalStart, originalEnd, start, end);
} else {
mSpanSet.mRecorder.assertUnmodified(span);
}
}
}
}
private void checkReplacementSpanPositions(Spannable originalSpannable, int replaceStart,
Spannable replacementSpannable, int replStart, int replEnd, int flag) {
// Get all spans overlapping the replacement substring region
Object[] addedSpans = replacementSpannable.getSpans(replStart, replEnd, Object.class);
int count = 0;
for (int s = 0; s < mReplacementSpanSet.mPositionSet.size(); s++) {
for (int e = s; e < mReplacementSpanSet.mPositionSet.size(); e++) {
Object span = mReplacementSpanSet.mSpans[count];
int originalStart = mReplacementSpanSet.mPositionSet.getPosition(s);
int originalEnd = mReplacementSpanSet.mPositionSet.getPosition(e);
int start = originalSpannable.getSpanStart(span);
int end = originalSpannable.getSpanEnd(span);
count++;
if (!isValidSpan(originalStart, originalEnd, flag)) continue;
if (DEBUG) System.out.println(" replacement " + originalStart + "," + originalEnd +
" -> " + start + "," + end);
// There should be no change reported to the replacement string spanWatcher
mReplacementSpanSet.mRecorder.assertUnmodified(span);
boolean shouldBeAdded = false;
for (int i = 0; i < addedSpans.length; i++) {
if (addedSpans[i] == span) {
shouldBeAdded = true;
break;
}
}
if (shouldBeAdded) {
int newStart = Math.max(0, originalStart - replStart) + replaceStart;
int newEnd = Math.min(originalEnd, replEnd) - replStart + replaceStart;
if (isValidSpan(newStart, newEnd, flag)) {
assertEquals(start, newStart);
assertEquals(end, newEnd);
mSpanSet.mRecorder.assertAdded(span, start, end);
continue;
}
}
mSpanSet.mRecorder.assertUnmodified(span);
}
}
}
private static boolean isValidSpan(int start, int end, int flag) {
// Zero length SPAN_EXCLUSIVE_EXCLUSIVE are not allowed
if (flag == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE && start == end) return false;
return true;
}
private static class PositionSet {
private int[] mPositions;
private int mSize;
PositionSet(int capacity) {
mPositions = new int[capacity];
mSize = 0;
}
void addPosition(int position) {
if (mSize == 0 || position > mPositions[mSize - 1]) {
mPositions[mSize] = position;
mSize++;
}
}
void clear() {
mSize = 0;
}
int size() {
return mSize;
}
int getPosition(int index) {
return mPositions[index];
}
}
private static class SpanSet {
private static final int NB_POSITIONS = 8;
static final int BEFORE = 0;
static final int INSIDE = 1;
static final int AFTER = 2;
private PositionSet mPositionSet;
private Object[] mSpans;
private int[] mSpanStartPositionStyle;
private int[] mSpanEndPositionStyle;
private SpanWatcherRecorder mRecorder;
SpanSet() {
mPositionSet = new PositionSet(NB_POSITIONS);
int nbSpans = (NB_POSITIONS * (NB_POSITIONS + 1)) / 2;
mSpanStartPositionStyle = new int[nbSpans];
mSpanEndPositionStyle = new int[nbSpans];
mSpans = new Object[nbSpans];
for (int i = 0; i < nbSpans; i++) {
mSpans[i] = new Object();
}
mRecorder = new SpanWatcherRecorder();
}
static int getPositionStyle(int position, int replaceStart, int replaceEnd) {
if (position < replaceStart) return BEFORE;
else if (position <= replaceEnd) return INSIDE;
else return AFTER;
}
/**
* Creates spans for all the possible interval cases. On short strings, or when the
* replaced region is at the beginning/end of the text, some of these spans may have an
* identical range
*/
void initSpans(Spannable spannable, int rangeStart, int rangeEnd, int flag) {
mPositionSet.clear();
mPositionSet.addPosition(0);
mPositionSet.addPosition(rangeStart / 2);
mPositionSet.addPosition(rangeStart);
mPositionSet.addPosition((2 * rangeStart + rangeEnd) / 3);
mPositionSet.addPosition((rangeStart + 2 * rangeEnd) / 3);
mPositionSet.addPosition(rangeEnd);
mPositionSet.addPosition((rangeEnd + spannable.length()) / 2);
mPositionSet.addPosition(spannable.length());
int count = 0;
for (int s = 0; s < mPositionSet.size(); s++) {
for (int e = s; e < mPositionSet.size(); e++) {
int start = mPositionSet.getPosition(s);
int end = mPositionSet.getPosition(e);
if (isValidSpan(start, end, flag)) {
spannable.setSpan(mSpans[count], start, end, flag);
}
mSpanStartPositionStyle[count] = getPositionStyle(start, rangeStart, rangeEnd);
mSpanEndPositionStyle[count] = getPositionStyle(end, rangeStart, rangeEnd);
count++;
}
}
// Must be done after all the spans were added, to not record these additions
spannable.setSpan(mRecorder, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
mRecorder.reset(spannable);
}
}
private static class SpanWatcherRecorder implements SpanWatcher {
private ArrayList<AddedRemoved> mAdded = new ArrayList<AddedRemoved>();
private ArrayList<AddedRemoved> mRemoved = new ArrayList<AddedRemoved>();
private ArrayList<Changed> mChanged = new ArrayList<Changed>();
private Spannable mSpannable;
private class AddedRemoved {
Object span;
int start;
int end;
public AddedRemoved(Object span, int start, int end) {
this.span = span;
this.start = start;
this.end = end;
}
}
private class Changed {
Object span;
int oldStart;
int oldEnd;
int newStart;
int newEnd;
public Changed(Object span, int oldStart, int oldEnd, int newStart, int newEnd) {
this.span = span;
this.oldStart = oldStart;
this.oldEnd = oldEnd;
this.newStart = newStart;
this.newEnd = newEnd;
}
}
public void reset(Spannable spannable) {
mSpannable = spannable;
mAdded.clear();
mRemoved.clear();
mChanged.clear();
}
@Override
public void onSpanAdded(Spannable text, Object span, int start, int end) {
if (text == mSpannable) mAdded.add(new AddedRemoved(span, start, end));
}
@Override
public void onSpanRemoved(Spannable text, Object span, int start, int end) {
if (text == mSpannable) mRemoved.add(new AddedRemoved(span, start, end));
}
@Override
public void onSpanChanged(Spannable text, Object span, int ostart, int oend, int nstart,
int nend) {
if (text == mSpannable) mChanged.add(new Changed(span, ostart, oend, nstart, nend));
}
public void assertUnmodified(Object span) {
for (AddedRemoved added: mAdded) {
if (added.span == span)
fail("Span " + span + " was added and not unmodified");
}
for (AddedRemoved removed: mRemoved) {
if (removed.span == span)
fail("Span " + span + " was removed and not unmodified");
}
for (Changed changed: mChanged) {
if (changed.span == span)
fail("Span " + span + " was changed and not unmodified");
}
}
public void assertChanged(Object span, int oldStart, int oldEnd, int newStart, int newEnd) {
for (Changed changed : mChanged) {
if (changed.span == span) {
assertEquals(changed.newStart, newStart);
assertEquals(changed.newEnd, newEnd);
// TODO previous range is not correctly sent in case a bound was inside the
// affected range. See SpannableStringBuilder#sendToSpanWatchers limitation
//assertEquals(changed.oldStart, oldStart);
//assertEquals(changed.oldEnd, oldEnd);
return;
}
}
fail("Span " + span + " was not changed");
}
public void assertAdded(Object span, int start, int end) {
for (AddedRemoved added : mAdded) {
if (added.span == span) {
assertEquals(added.start, start);
assertEquals(added.end, end);
return;
}
}
fail("Span " + span + " was not added");
}
public void assertRemoved(Object span, int start, int end) {
for (AddedRemoved removed : mRemoved) {
if (removed.span == span) {
assertEquals(removed.start, start);
assertEquals(removed.end, end);
return;
}
}
fail("Span " + span + " was not removed");
}
}
// TODO Thoroughly test the SPAN_PARAGRAPH span flag.
}