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