Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2012 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 com.android.inputmethod.keyboard.internal;
     18 
     19 import android.text.TextUtils;
     20 import android.util.SparseIntArray;
     21 
     22 import com.android.inputmethod.compat.CharacterCompat;
     23 import com.android.inputmethod.keyboard.Key;
     24 import com.android.inputmethod.latin.common.CollectionUtils;
     25 import com.android.inputmethod.latin.common.Constants;
     26 import com.android.inputmethod.latin.common.StringUtils;
     27 
     28 import java.util.ArrayList;
     29 import java.util.HashSet;
     30 import java.util.Locale;
     31 
     32 import javax.annotation.Nonnull;
     33 import javax.annotation.Nullable;
     34 
     35 /**
     36  * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
     37  *
     38  * The more keys specification is comma separated "key specification" each of which represents one
     39  * "more key".
     40  * The key specification might have label or string resource reference in it. These references are
     41  * expanded before parsing comma.
     42  * Special character, comma ',' backslash '\' can be escaped by '\' character.
     43  * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
     44  * as well.
     45  */
     46 // TODO: Should extend the key specification object.
     47 public final class MoreKeySpec {
     48     public final int mCode;
     49     @Nullable
     50     public final String mLabel;
     51     @Nullable
     52     public final String mOutputText;
     53     public final int mIconId;
     54 
     55     public MoreKeySpec(@Nonnull final String moreKeySpec, boolean needsToUpperCase,
     56             @Nonnull final Locale locale) {
     57         if (moreKeySpec.isEmpty()) {
     58             throw new KeySpecParser.KeySpecParserError("Empty more key spec");
     59         }
     60         final String label = KeySpecParser.getLabel(moreKeySpec);
     61         mLabel = needsToUpperCase ? StringUtils.toTitleCaseOfKeyLabel(label, locale) : label;
     62         final int codeInSpec = KeySpecParser.getCode(moreKeySpec);
     63         final int code = needsToUpperCase ? StringUtils.toTitleCaseOfKeyCode(codeInSpec, locale)
     64                 : codeInSpec;
     65         if (code == Constants.CODE_UNSPECIFIED) {
     66             // Some letter, for example German Eszett (U+00DF: ""), has multiple characters
     67             // upper case representation ("SS").
     68             mCode = Constants.CODE_OUTPUT_TEXT;
     69             mOutputText = mLabel;
     70         } else {
     71             mCode = code;
     72             final String outputText = KeySpecParser.getOutputText(moreKeySpec);
     73             mOutputText = needsToUpperCase
     74                     ? StringUtils.toTitleCaseOfKeyLabel(outputText, locale) : outputText;
     75         }
     76         mIconId = KeySpecParser.getIconId(moreKeySpec);
     77     }
     78 
     79     @Nonnull
     80     public Key buildKey(final int x, final int y, final int labelFlags,
     81             @Nonnull final KeyboardParams params) {
     82         return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
     83                 Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
     84                 params.mHorizontalGap, params.mVerticalGap);
     85     }
     86 
     87     @Override
     88     public int hashCode() {
     89         int hashCode = 1;
     90         hashCode = 31 + mCode;
     91         hashCode = hashCode * 31 + mIconId;
     92         final String label = mLabel;
     93         hashCode = hashCode * 31 + (label == null ? 0 : label.hashCode());
     94         final String outputText = mOutputText;
     95         hashCode = hashCode * 31 + (outputText == null ? 0 : outputText.hashCode());
     96         return hashCode;
     97     }
     98 
     99     @Override
    100     public boolean equals(final Object o) {
    101         if (this == o) {
    102             return true;
    103         }
    104         if (o instanceof MoreKeySpec) {
    105             final MoreKeySpec other = (MoreKeySpec)o;
    106             return mCode == other.mCode
    107                     && mIconId == other.mIconId
    108                     && TextUtils.equals(mLabel, other.mLabel)
    109                     && TextUtils.equals(mOutputText, other.mOutputText);
    110         }
    111         return false;
    112     }
    113 
    114     @Override
    115     public String toString() {
    116         final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
    117                 : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
    118         final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
    119                 : Constants.printableCode(mCode));
    120         if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
    121             return output;
    122         }
    123         return label + "|" + output;
    124     }
    125 
    126     public static class LettersOnBaseLayout {
    127         private final SparseIntArray mCodes = new SparseIntArray();
    128         private final HashSet<String> mTexts = new HashSet<>();
    129 
    130         public void addLetter(@Nonnull final Key key) {
    131             final int code = key.getCode();
    132             if (CharacterCompat.isAlphabetic(code)) {
    133                 mCodes.put(code, 0);
    134             } else if (code == Constants.CODE_OUTPUT_TEXT) {
    135                 mTexts.add(key.getOutputText());
    136             }
    137         }
    138 
    139         public boolean contains(@Nonnull final MoreKeySpec moreKey) {
    140             final int code = moreKey.mCode;
    141             if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
    142                 return true;
    143             } else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
    144                 return true;
    145             }
    146             return false;
    147         }
    148     }
    149 
    150     @Nullable
    151     public static MoreKeySpec[] removeRedundantMoreKeys(@Nullable final MoreKeySpec[] moreKeys,
    152             @Nonnull final LettersOnBaseLayout lettersOnBaseLayout) {
    153         if (moreKeys == null) {
    154             return null;
    155         }
    156         final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
    157         for (final MoreKeySpec moreKey : moreKeys) {
    158             if (!lettersOnBaseLayout.contains(moreKey)) {
    159                 filteredMoreKeys.add(moreKey);
    160             }
    161         }
    162         final int size = filteredMoreKeys.size();
    163         if (size == moreKeys.length) {
    164             return moreKeys;
    165         }
    166         if (size == 0) {
    167             return null;
    168         }
    169         return filteredMoreKeys.toArray(new MoreKeySpec[size]);
    170     }
    171 
    172     // Constants for parsing.
    173     private static final char COMMA = Constants.CODE_COMMA;
    174     private static final char BACKSLASH = Constants.CODE_BACKSLASH;
    175     private static final String ADDITIONAL_MORE_KEY_MARKER =
    176             StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
    177 
    178     /**
    179      * Split the text containing multiple key specifications separated by commas into an array of
    180      * key specifications.
    181      * A key specification can contain a character escaped by the backslash character, including a
    182      * comma character.
    183      * Note that an empty key specification will be eliminated from the result array.
    184      *
    185      * @param text the text containing multiple key specifications.
    186      * @return an array of key specification text. Null if the specified <code>text</code> is empty
    187      * or has no key specifications.
    188      */
    189     @Nullable
    190     public static String[] splitKeySpecs(@Nullable final String text) {
    191         if (TextUtils.isEmpty(text)) {
    192             return null;
    193         }
    194         final int size = text.length();
    195         // Optimization for one-letter key specification.
    196         if (size == 1) {
    197             return text.charAt(0) == COMMA ? null : new String[] { text };
    198         }
    199 
    200         ArrayList<String> list = null;
    201         int start = 0;
    202         // The characters in question in this loop are COMMA and BACKSLASH. These characters never
    203         // match any high or low surrogate character. So it is OK to iterate through with char
    204         // index.
    205         for (int pos = 0; pos < size; pos++) {
    206             final char c = text.charAt(pos);
    207             if (c == COMMA) {
    208                 // Skip empty entry.
    209                 if (pos - start > 0) {
    210                     if (list == null) {
    211                         list = new ArrayList<>();
    212                     }
    213                     list.add(text.substring(start, pos));
    214                 }
    215                 // Skip comma
    216                 start = pos + 1;
    217             } else if (c == BACKSLASH) {
    218                 // Skip escape character and escaped character.
    219                 pos++;
    220             }
    221         }
    222         final String remain = (size - start > 0) ? text.substring(start) : null;
    223         if (list == null) {
    224             return remain != null ? new String[] { remain } : null;
    225         }
    226         if (remain != null) {
    227             list.add(remain);
    228         }
    229         return list.toArray(new String[list.size()]);
    230     }
    231 
    232     @Nonnull
    233     private static final String[] EMPTY_STRING_ARRAY = new String[0];
    234 
    235     @Nonnull
    236     private static String[] filterOutEmptyString(@Nullable final String[] array) {
    237         if (array == null) {
    238             return EMPTY_STRING_ARRAY;
    239         }
    240         ArrayList<String> out = null;
    241         for (int i = 0; i < array.length; i++) {
    242             final String entry = array[i];
    243             if (TextUtils.isEmpty(entry)) {
    244                 if (out == null) {
    245                     out = CollectionUtils.arrayAsList(array, 0, i);
    246                 }
    247             } else if (out != null) {
    248                 out.add(entry);
    249             }
    250         }
    251         if (out == null) {
    252             return array;
    253         }
    254         return out.toArray(new String[out.size()]);
    255     }
    256 
    257     public static String[] insertAdditionalMoreKeys(@Nullable final String[] moreKeySpecs,
    258             @Nullable final String[] additionalMoreKeySpecs) {
    259         final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
    260         final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
    261         final int moreKeysCount = moreKeys.length;
    262         final int additionalCount = additionalMoreKeys.length;
    263         ArrayList<String> out = null;
    264         int additionalIndex = 0;
    265         for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
    266             final String moreKeySpec = moreKeys[moreKeyIndex];
    267             if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
    268                 if (additionalIndex < additionalCount) {
    269                     // Replace '%' marker with additional more key specification.
    270                     final String additionalMoreKey = additionalMoreKeys[additionalIndex];
    271                     if (out != null) {
    272                         out.add(additionalMoreKey);
    273                     } else {
    274                         moreKeys[moreKeyIndex] = additionalMoreKey;
    275                     }
    276                     additionalIndex++;
    277                 } else {
    278                     // Filter out excessive '%' marker.
    279                     if (out == null) {
    280                         out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
    281                     }
    282                 }
    283             } else {
    284                 if (out != null) {
    285                     out.add(moreKeySpec);
    286                 }
    287             }
    288         }
    289         if (additionalCount > 0 && additionalIndex == 0) {
    290             // No '%' marker is found in more keys.
    291             // Insert all additional more keys to the head of more keys.
    292             out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
    293             for (int i = 0; i < moreKeysCount; i++) {
    294                 out.add(moreKeys[i]);
    295             }
    296         } else if (additionalIndex < additionalCount) {
    297             // The number of '%' markers are less than additional more keys.
    298             // Append remained additional more keys to the tail of more keys.
    299             out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
    300             for (int i = additionalIndex; i < additionalCount; i++) {
    301                 out.add(additionalMoreKeys[additionalIndex]);
    302             }
    303         }
    304         if (out == null && moreKeysCount > 0) {
    305             return moreKeys;
    306         } else if (out != null && out.size() > 0) {
    307             return out.toArray(new String[out.size()]);
    308         } else {
    309             return null;
    310         }
    311     }
    312 
    313     public static int getIntValue(@Nullable final String[] moreKeys, final String key,
    314             final int defaultValue) {
    315         if (moreKeys == null) {
    316             return defaultValue;
    317         }
    318         final int keyLen = key.length();
    319         boolean foundValue = false;
    320         int value = defaultValue;
    321         for (int i = 0; i < moreKeys.length; i++) {
    322             final String moreKeySpec = moreKeys[i];
    323             if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
    324                 continue;
    325             }
    326             moreKeys[i] = null;
    327             try {
    328                 if (!foundValue) {
    329                     value = Integer.parseInt(moreKeySpec.substring(keyLen));
    330                     foundValue = true;
    331                 }
    332             } catch (NumberFormatException e) {
    333                 throw new RuntimeException(
    334                         "integer should follow after " + key + ": " + moreKeySpec);
    335             }
    336         }
    337         return value;
    338     }
    339 
    340     public static boolean getBooleanValue(@Nullable final String[] moreKeys, final String key) {
    341         if (moreKeys == null) {
    342             return false;
    343         }
    344         boolean value = false;
    345         for (int i = 0; i < moreKeys.length; i++) {
    346             final String moreKeySpec = moreKeys[i];
    347             if (moreKeySpec == null || !moreKeySpec.equals(key)) {
    348                 continue;
    349             }
    350             moreKeys[i] = null;
    351             value = true;
    352         }
    353         return value;
    354     }
    355 }
    356