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