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