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 
EditorState()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 {
getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)66         public int getSize(Paint paint, CharSequence text, int start, int end,
67                 Paint.FontMetricsInt fm) {
68             return 0;
69         }
draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)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.
isGraphicAscii(int codePoint)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.
setByString(String string)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 
assertEquals(String string)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