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