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