Home | History | Annotate | Download | only in cts
      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 
     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 
     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 
     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 
     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 
    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 
    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 
    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 
    291         PositionSet(int capacity) {
    292             mPositions = new int[capacity];
    293             mSize = 0;
    294         }
    295 
    296         void addPosition(int position) {
    297             if (mSize == 0 || position > mPositions[mSize - 1]) {
    298                 mPositions[mSize] = position;
    299                 mSize++;
    300             }
    301         }
    302 
    303         void clear() {
    304             mSize = 0;
    305         }
    306 
    307         int size() {
    308             return mSize;
    309         }
    310 
    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 
    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 
    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          */
    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 
    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 
    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 
    418         public void reset(Spannable spannable) {
    419             mSpannable = spannable;
    420             mAdded.clear();
    421             mRemoved.clear();
    422             mChanged.clear();
    423         }
    424 
    425         @Override
    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
    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
    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 
    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 
    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 
    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 
    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