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 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 { 66 public int getSize(Paint paint, CharSequence text, int start, int end, 67 Paint.FontMetricsInt fm) { 68 return 0; 69 } 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. 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. 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 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