1 /* 2 * Copyright (C) 2016 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.method.cts; 18 19 import android.graphics.Canvas; 20 import android.graphics.Paint; 21 import android.text.Editable; 22 import android.text.Spannable; 23 import android.text.SpannableString; 24 import android.text.style.ReplacementSpan; 25 26 import junit.framework.Assert; 27 28 /** 29 * Represents an editor state. 30 * 31 * The editor state can be specified by following string format. 32 * - Components are separated by space(U+0020). 33 * - Single-quoted string for printable ASCII characters, e.g. 'a', '123'. 34 * - U+XXXX form can be used for a Unicode code point. 35 * - Components inside '[' and ']' are in selection. 36 * - Components inside '(' and ')' are in ReplacementSpan. 37 * - '|' is for specifying cursor position. 38 * 39 * Selection and cursor can not be specified at the same time. 40 * 41 * Example: 42 * - "'Hello,' | U+0020 'world!'" means "Hello, world!" is displayed and the cursor position 43 * is 6. 44 * - "'abc' [ 'def' ] 'ghi'" means "abcdefghi" is displayed and "def" is selected. 45 * - "U+1F441 | ( U+1F441 U+1F441 )" means three U+1F441 characters are displayed and 46 * ReplacementSpan is set from offset 2 to 6. 47 */ 48 public class EditorState { 49 private static final String REPLACEMENT_SPAN_START = "("; 50 private static final String REPLACEMENT_SPAN_END = ")"; 51 private static final String SELECTION_START = "["; 52 private static final String SELECTION_END = "]"; 53 private static final String CURSOR = "|"; 54 55 public Editable mText; 56 public int mSelectionStart = -1; 57 public int mSelectionEnd = -1; 58 EditorState()59 public EditorState() { 60 } 61 62 /** 63 * A mocked {@link android.text.style.ReplacementSpan} for testing purpose. 64 */ 65 private static class MockReplacementSpan extends ReplacementSpan { getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)66 public int getSize(Paint paint, CharSequence text, int start, int end, 67 Paint.FontMetricsInt fm) { 68 return 0; 69 } draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)70 public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, 71 int y, int bottom, Paint paint) { 72 } 73 } 74 75 // Returns true if the code point is ASCII and graph. isGraphicAscii(int codePoint)76 private boolean isGraphicAscii(int codePoint) { 77 return 0x20 < codePoint && codePoint < 0x7F; 78 } 79 80 // Setup editor state with string. Please see class description for string format. setByString(String string)81 public void setByString(String string) { 82 final StringBuilder sb = new StringBuilder(); 83 int replacementSpanStart = -1; 84 int replacementSpanEnd = -1; 85 mSelectionStart = -1; 86 mSelectionEnd = -1; 87 88 final String[] tokens = string.split(" +"); 89 for (String token : tokens) { 90 if (token.startsWith("'") && token.endsWith("'")) { 91 for (int i = 1; i < token.length() - 1; ++i) { 92 final char ch = token.charAt(1); 93 if (!isGraphicAscii(ch)) { 94 throw new IllegalArgumentException( 95 "Only printable characters can be in single quote. " + 96 "Use U+" + Integer.toHexString(ch).toUpperCase() + " instead"); 97 } 98 } 99 sb.append(token.substring(1, token.length() - 1)); 100 } else if (token.startsWith("U+")) { 101 final int codePoint = Integer.parseInt(token.substring(2), 16); 102 if (codePoint < 0 || 0x10FFFF < codePoint) { 103 throw new IllegalArgumentException("Invalid code point is specified:" + token); 104 } 105 sb.append(Character.toChars(codePoint)); 106 } else if (token.equals(CURSOR)) { 107 if (mSelectionStart != -1 || mSelectionEnd != -1) { 108 throw new IllegalArgumentException( 109 "Two or more cursor/selection positions are specified."); 110 } 111 mSelectionStart = mSelectionEnd = sb.length(); 112 } else if (token.equals(SELECTION_START)) { 113 if (mSelectionStart != -1) { 114 throw new IllegalArgumentException( 115 "Two or more cursor/selection positions are specified."); 116 } 117 mSelectionStart = sb.length(); 118 } else if (token.equals(SELECTION_END)) { 119 if (mSelectionEnd != -1) { 120 throw new IllegalArgumentException( 121 "Two or more cursor/selection positions are specified."); 122 } 123 mSelectionEnd = sb.length(); 124 } else if (token.equals(REPLACEMENT_SPAN_START)) { 125 if (replacementSpanStart != -1) { 126 throw new IllegalArgumentException( 127 "Only one replacement span is supported"); 128 } 129 replacementSpanStart = sb.length(); 130 } else if (token.equals(REPLACEMENT_SPAN_END)) { 131 if (replacementSpanEnd != -1) { 132 throw new IllegalArgumentException( 133 "Only one replacement span is supported"); 134 } 135 replacementSpanEnd = sb.length(); 136 } else { 137 throw new IllegalArgumentException("Unknown or invalid token: " + token); 138 } 139 } 140 141 if (mSelectionStart == -1 || mSelectionEnd == -1) { 142 if (mSelectionEnd != -1) { 143 throw new IllegalArgumentException( 144 "Selection start position doesn't exist."); 145 } else if (mSelectionStart != -1) { 146 throw new IllegalArgumentException( 147 "Selection end position doesn't exist."); 148 } else { 149 throw new IllegalArgumentException( 150 "At least cursor position or selection range must be specified."); 151 } 152 } else if (mSelectionStart > mSelectionEnd) { 153 throw new IllegalArgumentException( 154 "Selection start position appears after end position."); 155 } 156 157 final Spannable spannable = new SpannableString(sb.toString()); 158 159 if (replacementSpanStart != -1 || replacementSpanEnd != -1) { 160 if (replacementSpanStart == -1) { 161 throw new IllegalArgumentException( 162 "ReplacementSpan start position doesn't exist."); 163 } 164 if (replacementSpanEnd == -1) { 165 throw new IllegalArgumentException( 166 "ReplacementSpan end position doesn't exist."); 167 } 168 if (replacementSpanStart > replacementSpanEnd) { 169 throw new IllegalArgumentException( 170 "ReplacementSpan start position appears after end position."); 171 } 172 spannable.setSpan(new MockReplacementSpan(), replacementSpanStart, replacementSpanEnd, 173 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 174 } 175 mText = Editable.Factory.getInstance().newEditable(spannable); 176 } 177 assertEquals(String string)178 public void assertEquals(String string) { 179 EditorState expected = new EditorState(); 180 expected.setByString(string); 181 182 Assert.assertEquals(expected.mText.toString(), mText.toString()); 183 Assert.assertEquals(expected.mSelectionStart, mSelectionStart); 184 Assert.assertEquals(expected.mSelectionEnd, mSelectionEnd); 185 } 186 } 187 188