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