• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.cts;
18 
19 import java.util.ArrayList;
20 
21 import android.test.AndroidTestCase;
22 import android.text.SpanWatcher;
23 import android.text.Spannable;
24 import android.text.SpannableStringBuilder;
25 import android.text.Spanned;
26 
27 /**
28  * Test {@link SpannableStringBuilder}.
29  */
30 public class SpannableStringBuilderSpanTest extends AndroidTestCase {
31 
32     private static final boolean DEBUG = false;
33 
34     private SpanSet mSpanSet = new SpanSet();
35     private SpanSet mReplacementSpanSet = new SpanSet();
36     private int testCounter;
37 
testReplaceWithSpans()38     public void testReplaceWithSpans() {
39         testCounter = 0;
40         String originals[] = { "", "A", "here", "Well, hello there" };
41         String replacements[] = { "", "X", "test", "longer replacement" };
42 
43         for (String original: originals) {
44             for (String replacement: replacements) {
45                 replace(original, replacement);
46             }
47         }
48     }
49 
replace(String original, String replacement)50     private void replace(String original, String replacement) {
51         PositionSet positionSet = new PositionSet(4);
52         positionSet.addPosition(0);
53         positionSet.addPosition(original.length() / 3);
54         positionSet.addPosition(2 * original.length() / 3);
55         positionSet.addPosition(original.length());
56 
57         PositionSet replPositionSet = new PositionSet(4);
58         replPositionSet.addPosition(0);
59         replPositionSet.addPosition(replacement.length() / 3);
60         replPositionSet.addPosition(2 * replacement.length() / 3);
61         replPositionSet.addPosition(replacement.length());
62 
63         for (int s = 0; s < positionSet.size(); s++) {
64             for (int e = s; e < positionSet.size(); e++) {
65                 for (int rs = 0; rs < replPositionSet.size(); rs++) {
66                     for (int re = rs; re < replPositionSet.size(); re++) {
67                         replaceWithRange(original,
68                                 positionSet.getPosition(s), positionSet.getPosition(e),
69                                 replacement,
70                                 replPositionSet.getPosition(rs), replPositionSet.getPosition(re));
71                     }
72                 }
73             }
74         }
75     }
76 
replaceWithRange(String original, int replaceStart, int replaceEnd, String replacement, int replacementStart, int replacementEnd)77     private void replaceWithRange(String original, int replaceStart, int replaceEnd,
78             String replacement, int replacementStart, int replacementEnd) {
79         int flags[] = { Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, Spanned.SPAN_INCLUSIVE_INCLUSIVE,
80                 Spanned.SPAN_EXCLUSIVE_INCLUSIVE, Spanned.SPAN_INCLUSIVE_EXCLUSIVE };
81 
82 
83         for (int flag: flags) {
84             replaceWithSpanFlag(original, replaceStart, replaceEnd,
85                     replacement, replacementStart, replacementEnd, flag);
86         }
87     }
88 
replaceWithSpanFlag(String original, int replaceStart, int replaceEnd, String replacement, int replacementStart, int replacementEnd, int flag)89     private void replaceWithSpanFlag(String original, int replaceStart, int replaceEnd,
90             String replacement, int replacementStart, int replacementEnd, int flag) {
91 
92         testCounter++;
93         int debugTestNumber = -1;
94         if (debugTestNumber >= 0 && testCounter != debugTestNumber) return;
95 
96         String subReplacement = replacement.substring(replacementStart, replacementEnd);
97         String expected = original.substring(0, replaceStart) +
98                 subReplacement + original.substring(replaceEnd, original.length());
99         if (DEBUG) System.out.println("#" + testCounter + ", replace \"" + original + "\" [" +
100                 replaceStart + " " + replaceEnd + "] by \"" + subReplacement + "\" -> \"" +
101                 expected + "\", flag=" + flag);
102 
103         SpannableStringBuilder originalSpannable = new SpannableStringBuilder(original);
104         Spannable replacementSpannable = new SpannableStringBuilder(replacement);
105 
106         mSpanSet.initSpans(originalSpannable, replaceStart, replaceEnd, flag);
107         mReplacementSpanSet.initSpans(replacementSpannable, replacementStart, replacementEnd, flag);
108 
109         originalSpannable.replace(replaceStart, replaceEnd, replacementSpannable,
110                 replacementStart, replacementEnd);
111 
112         assertEquals(expected, originalSpannable.toString());
113 
114         checkSpanPositions(originalSpannable, replaceStart, replaceEnd, subReplacement.length(),
115                 flag);
116         checkReplacementSpanPositions(originalSpannable, replaceStart, replacementSpannable,
117                 replacementStart, replacementEnd, flag);
118     }
119 
checkSpanPositions(Spannable spannable, int replaceStart, int replaceEnd, int replacementLength, int flag)120     private void checkSpanPositions(Spannable spannable, int replaceStart, int replaceEnd,
121             int replacementLength, int flag) {
122         int count = 0;
123         int replacedLength = replaceEnd - replaceStart;
124         int delta = replacementLength - replacedLength;
125         boolean textIsReplaced = replacedLength > 0 && replacementLength > 0;
126         for (int s = 0; s < mSpanSet.mPositionSet.size(); s++) {
127             for (int e = s; e < mSpanSet.mPositionSet.size(); e++) {
128                 Object span = mSpanSet.mSpans[count];
129                 int originalStart = mSpanSet.mPositionSet.getPosition(s);
130                 int originalEnd = mSpanSet.mPositionSet.getPosition(e);
131                 int start = spannable.getSpanStart(span);
132                 int end = spannable.getSpanEnd(span);
133                 int startStyle = mSpanSet.mSpanStartPositionStyle[count];
134                 int endStyle = mSpanSet.mSpanEndPositionStyle[count];
135                 count++;
136 
137                 if (!isValidSpan(originalStart, originalEnd, flag)) continue;
138 
139                 if (DEBUG) System.out.println("  " + originalStart + "," + originalEnd + " -> " +
140                         start + "," + end + " | " + startStyle + " " + endStyle +
141                         " delta=" + delta);
142 
143                 // This is the exception to the following generic code where we need to consider
144                 // both the start and end styles.
145                 if (startStyle == SpanSet.INSIDE && endStyle == SpanSet.INSIDE &&
146                         flag == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE &&
147                         (replacementLength == 0 || originalStart > replaceStart ||
148                         originalEnd < replaceEnd)) {
149                     // 0-length spans should have been removed
150                     assertEquals(-1, start);
151                     assertEquals(-1, end);
152                     mSpanSet.mRecorder.assertRemoved(span, originalStart, originalEnd);
153                     continue;
154                 }
155 
156                 switch (startStyle) {
157                     case SpanSet.BEFORE:
158                         assertEquals(originalStart, start);
159                         break;
160                     case SpanSet.INSIDE:
161                         switch (flag) {
162                             case Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:
163                             case Spanned.SPAN_EXCLUSIVE_INCLUSIVE:
164                                 // start is POINT
165                                 if (originalStart == replaceStart && textIsReplaced) {
166                                     assertEquals(replaceStart, start);
167                                 } else {
168                                     assertEquals(replaceStart + replacementLength, start);
169                                 }
170                                 break;
171                             case Spanned.SPAN_INCLUSIVE_INCLUSIVE:
172                             case Spanned.SPAN_INCLUSIVE_EXCLUSIVE:
173                                 // start is MARK
174                                 if (originalStart == replaceEnd && textIsReplaced) {
175                                     assertEquals(replaceStart + replacementLength, start);
176                                 } else {
177                                     assertEquals(replaceStart, start);
178                                 }
179                                 break;
180                             case Spanned.SPAN_PARAGRAPH:
181                                 fail("TODO");
182                                 break;
183                         }
184                         break;
185                     case SpanSet.AFTER:
186                         assertEquals(originalStart + delta, start);
187                         break;
188                 }
189 
190                 switch (endStyle) {
191                     case SpanSet.BEFORE:
192                         assertEquals(originalEnd, end);
193                         break;
194                     case SpanSet.INSIDE:
195                         switch (flag) {
196                             case Spanned.SPAN_EXCLUSIVE_EXCLUSIVE:
197                             case Spanned.SPAN_INCLUSIVE_EXCLUSIVE:
198                                 // end is MARK
199                                 if (originalEnd == replaceEnd && textIsReplaced) {
200                                     assertEquals(replaceStart + replacementLength, end);
201                                 } else {
202                                     assertEquals(replaceStart, end);
203                                 }
204                                 break;
205                             case Spanned.SPAN_INCLUSIVE_INCLUSIVE:
206                             case Spanned.SPAN_EXCLUSIVE_INCLUSIVE:
207                                 // end is POINT
208                                 if (originalEnd == replaceStart && textIsReplaced) {
209                                     assertEquals(replaceStart, end);
210                                 } else {
211                                     assertEquals(replaceStart + replacementLength, end);
212                                 }
213                                 break;
214                             case Spanned.SPAN_PARAGRAPH:
215                                 fail("TODO");
216                                 break;
217                         }
218                         break;
219                     case SpanSet.AFTER:
220                         assertEquals(originalEnd + delta, end);
221                         break;
222                 }
223 
224                 if (start != originalStart || end != originalEnd) {
225                     mSpanSet.mRecorder.assertChanged(span, originalStart, originalEnd, start, end);
226                 } else {
227                     mSpanSet.mRecorder.assertUnmodified(span);
228                 }
229             }
230         }
231     }
232 
checkReplacementSpanPositions(Spannable originalSpannable, int replaceStart, Spannable replacementSpannable, int replStart, int replEnd, int flag)233     private void checkReplacementSpanPositions(Spannable originalSpannable, int replaceStart,
234             Spannable replacementSpannable, int replStart, int replEnd, int flag) {
235 
236         // Get all spans overlapping the replacement substring region
237         Object[] addedSpans = replacementSpannable.getSpans(replStart, replEnd, Object.class);
238 
239         int count = 0;
240         for (int s = 0; s < mReplacementSpanSet.mPositionSet.size(); s++) {
241             for (int e = s; e < mReplacementSpanSet.mPositionSet.size(); e++) {
242                 Object span = mReplacementSpanSet.mSpans[count];
243                 int originalStart = mReplacementSpanSet.mPositionSet.getPosition(s);
244                 int originalEnd = mReplacementSpanSet.mPositionSet.getPosition(e);
245                 int start = originalSpannable.getSpanStart(span);
246                 int end = originalSpannable.getSpanEnd(span);
247                 count++;
248 
249                 if (!isValidSpan(originalStart, originalEnd, flag)) continue;
250 
251                 if (DEBUG) System.out.println("  replacement " + originalStart + "," + originalEnd +
252                         " -> " + start + "," + end);
253 
254                 // There should be no change reported to the replacement string spanWatcher
255                 mReplacementSpanSet.mRecorder.assertUnmodified(span);
256 
257                 boolean shouldBeAdded = false;
258                 for (int i = 0; i < addedSpans.length; i++) {
259                     if (addedSpans[i] == span) {
260                         shouldBeAdded = true;
261                         break;
262                     }
263                 }
264 
265                 if (shouldBeAdded) {
266                     int newStart = Math.max(0, originalStart - replStart) + replaceStart;
267                     int newEnd = Math.min(originalEnd, replEnd) - replStart + replaceStart;
268                     if (isValidSpan(newStart, newEnd, flag)) {
269                         assertEquals(start, newStart);
270                         assertEquals(end, newEnd);
271                         mSpanSet.mRecorder.assertAdded(span, start, end);
272                         continue;
273                     }
274                 }
275 
276                 mSpanSet.mRecorder.assertUnmodified(span);
277             }
278         }
279     }
280 
isValidSpan(int start, int end, int flag)281     private static boolean isValidSpan(int start, int end, int flag) {
282         // Zero length SPAN_EXCLUSIVE_EXCLUSIVE are not allowed
283         if (flag == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE && start == end) return false;
284         return true;
285     }
286 
287     private static class PositionSet {
288         private int[] mPositions;
289         private int mSize;
290 
PositionSet(int capacity)291         PositionSet(int capacity) {
292             mPositions = new int[capacity];
293             mSize = 0;
294         }
295 
addPosition(int position)296         void addPosition(int position) {
297             if (mSize == 0 || position > mPositions[mSize - 1]) {
298                 mPositions[mSize] = position;
299                 mSize++;
300             }
301         }
302 
clear()303         void clear() {
304             mSize = 0;
305         }
306 
size()307         int size() {
308             return mSize;
309         }
310 
getPosition(int index)311         int getPosition(int index) {
312             return mPositions[index];
313         }
314     }
315 
316     private static class SpanSet {
317         private static final int NB_POSITIONS = 8;
318 
319         static final int BEFORE = 0;
320         static final int INSIDE = 1;
321         static final int AFTER = 2;
322 
323         private PositionSet mPositionSet;
324         private Object[] mSpans;
325         private int[] mSpanStartPositionStyle;
326         private int[] mSpanEndPositionStyle;
327         private SpanWatcherRecorder mRecorder;
328 
SpanSet()329         SpanSet() {
330             mPositionSet = new PositionSet(NB_POSITIONS);
331             int nbSpans = (NB_POSITIONS * (NB_POSITIONS + 1)) / 2;
332             mSpanStartPositionStyle = new int[nbSpans];
333             mSpanEndPositionStyle = new int[nbSpans];
334             mSpans = new Object[nbSpans];
335             for (int i = 0; i < nbSpans; i++) {
336                 mSpans[i] = new Object();
337             }
338             mRecorder = new SpanWatcherRecorder();
339         }
340 
getPositionStyle(int position, int replaceStart, int replaceEnd)341         static int getPositionStyle(int position, int replaceStart, int replaceEnd) {
342             if (position < replaceStart) return BEFORE;
343             else if (position <= replaceEnd) return INSIDE;
344             else return AFTER;
345         }
346 
347         /**
348          * Creates spans for all the possible interval cases. On short strings, or when the
349          * replaced region is at the beginning/end of the text, some of these spans may have an
350          * identical range
351          */
initSpans(Spannable spannable, int rangeStart, int rangeEnd, int flag)352         void initSpans(Spannable spannable, int rangeStart, int rangeEnd, int flag) {
353             mPositionSet.clear();
354             mPositionSet.addPosition(0);
355             mPositionSet.addPosition(rangeStart / 2);
356             mPositionSet.addPosition(rangeStart);
357             mPositionSet.addPosition((2 * rangeStart + rangeEnd) / 3);
358             mPositionSet.addPosition((rangeStart + 2 * rangeEnd) / 3);
359             mPositionSet.addPosition(rangeEnd);
360             mPositionSet.addPosition((rangeEnd + spannable.length()) / 2);
361             mPositionSet.addPosition(spannable.length());
362 
363             int count = 0;
364             for (int s = 0; s < mPositionSet.size(); s++) {
365                 for (int e = s; e < mPositionSet.size(); e++) {
366                     int start = mPositionSet.getPosition(s);
367                     int end = mPositionSet.getPosition(e);
368                     if (isValidSpan(start, end, flag)) {
369                         spannable.setSpan(mSpans[count], start, end, flag);
370                     }
371                     mSpanStartPositionStyle[count] = getPositionStyle(start, rangeStart, rangeEnd);
372                     mSpanEndPositionStyle[count] = getPositionStyle(end, rangeStart, rangeEnd);
373                     count++;
374                 }
375             }
376 
377             // Must be done after all the spans were added, to not record these additions
378             spannable.setSpan(mRecorder, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
379             mRecorder.reset(spannable);
380         }
381     }
382 
383     private static class SpanWatcherRecorder implements SpanWatcher {
384         private ArrayList<AddedRemoved> mAdded = new ArrayList<AddedRemoved>();
385         private ArrayList<AddedRemoved> mRemoved = new ArrayList<AddedRemoved>();
386         private ArrayList<Changed> mChanged = new ArrayList<Changed>();
387 
388         private Spannable mSpannable;
389 
390         private class AddedRemoved {
391             Object span;
392             int start;
393             int end;
394 
AddedRemoved(Object span, int start, int end)395             public AddedRemoved(Object span, int start, int end) {
396                 this.span = span;
397                 this.start = start;
398                 this.end = end;
399             }
400         }
401 
402         private class Changed {
403             Object span;
404             int oldStart;
405             int oldEnd;
406             int newStart;
407             int newEnd;
408 
Changed(Object span, int oldStart, int oldEnd, int newStart, int newEnd)409             public Changed(Object span, int oldStart, int oldEnd, int newStart, int newEnd) {
410                 this.span = span;
411                 this.oldStart = oldStart;
412                 this.oldEnd = oldEnd;
413                 this.newStart = newStart;
414                 this.newEnd = newEnd;
415             }
416         }
417 
reset(Spannable spannable)418         public void reset(Spannable spannable) {
419             mSpannable = spannable;
420             mAdded.clear();
421             mRemoved.clear();
422             mChanged.clear();
423         }
424 
425         @Override
onSpanAdded(Spannable text, Object span, int start, int end)426         public void onSpanAdded(Spannable text, Object span, int start, int end) {
427             if (text == mSpannable) mAdded.add(new AddedRemoved(span, start, end));
428         }
429 
430         @Override
onSpanRemoved(Spannable text, Object span, int start, int end)431         public void onSpanRemoved(Spannable text, Object span, int start, int end) {
432             if (text == mSpannable) mRemoved.add(new AddedRemoved(span, start, end));
433         }
434 
435         @Override
onSpanChanged(Spannable text, Object span, int ostart, int oend, int nstart, int nend)436         public void onSpanChanged(Spannable text, Object span, int ostart, int oend, int nstart,
437                 int nend) {
438             if (text == mSpannable) mChanged.add(new Changed(span, ostart, oend, nstart, nend));
439         }
440 
assertUnmodified(Object span)441         public void assertUnmodified(Object span) {
442             for (AddedRemoved added: mAdded) {
443                 if (added.span == span)
444                     fail("Span " + span + " was added and not unmodified");
445             }
446             for (AddedRemoved removed: mRemoved) {
447                 if (removed.span == span)
448                     fail("Span " + span + " was removed and not unmodified");
449             }
450             for (Changed changed: mChanged) {
451                 if (changed.span == span)
452                     fail("Span " + span + " was changed and not unmodified");
453             }
454         }
455 
assertChanged(Object span, int oldStart, int oldEnd, int newStart, int newEnd)456         public void assertChanged(Object span, int oldStart, int oldEnd, int newStart, int newEnd) {
457             for (Changed changed : mChanged) {
458                 if (changed.span == span) {
459                     assertEquals(changed.newStart, newStart);
460                     assertEquals(changed.newEnd, newEnd);
461                     // TODO previous range is not correctly sent in case a bound was inside the
462                     // affected range. See SpannableStringBuilder#sendToSpanWatchers limitation
463                     //assertEquals(changed.oldStart, oldStart);
464                     //assertEquals(changed.oldEnd, oldEnd);
465                     return;
466                 }
467             }
468             fail("Span " + span + " was not changed");
469         }
470 
assertAdded(Object span, int start, int end)471         public void assertAdded(Object span, int start, int end) {
472             for (AddedRemoved added : mAdded) {
473                 if (added.span == span) {
474                     assertEquals(added.start, start);
475                     assertEquals(added.end, end);
476                     return;
477                 }
478             }
479             fail("Span " + span + " was not added");
480         }
481 
assertRemoved(Object span, int start, int end)482         public void assertRemoved(Object span, int start, int end) {
483             for (AddedRemoved removed : mRemoved) {
484                 if (removed.span == span) {
485                     assertEquals(removed.start, start);
486                     assertEquals(removed.end, end);
487                     return;
488                 }
489             }
490             fail("Span " + span + " was not removed");
491         }
492     }
493 
494     // TODO Thoroughly test the SPAN_PARAGRAPH span flag.
495 }
496