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
17package android.text.method;
18
19import android.graphics.Canvas;
20import android.graphics.Paint;
21import android.text.Editable;
22import android.text.Spannable;
23import android.text.SpannableString;
24import android.text.style.ReplacementSpan;
25
26import 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 */
48public 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
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 {
66        public int getSize(Paint paint, CharSequence text, int start, int end,
67                Paint.FontMetricsInt fm) {
68            return 0;
69        }
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.
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.
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
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