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