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 static org.mockito.Matchers.any;
20import static org.mockito.Mockito.doNothing;
21import static org.mockito.Mockito.mock;
22import static org.mockito.Mockito.when;
23
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.text.Editable;
27import android.text.Spannable;
28import android.text.SpannableString;
29import android.text.style.ReplacementSpan;
30
31import 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 */
53public 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
64    public EditorState() {
65    }
66
67    // Returns true if the code point is ASCII and graph.
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.
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
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