Home | History | Annotate | Download | only in method
      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 
     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