Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2011 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.accessibility;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.text.TextUtils;
     22 import android.util.Log;
     23 import android.util.SparseIntArray;
     24 import android.view.inputmethod.EditorInfo;
     25 
     26 import com.android.inputmethod.keyboard.Key;
     27 import com.android.inputmethod.keyboard.Keyboard;
     28 import com.android.inputmethod.keyboard.KeyboardId;
     29 import com.android.inputmethod.latin.R;
     30 import com.android.inputmethod.latin.common.Constants;
     31 import com.android.inputmethod.latin.common.StringUtils;
     32 
     33 import java.util.Locale;
     34 
     35 final class KeyCodeDescriptionMapper {
     36     private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
     37     private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
     38     private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
     39     private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
     40     private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon";
     41     private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X";
     42 
     43     // The resource ID of the string spoken for obscured keys
     44     private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
     45 
     46     private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
     47 
     48     public static KeyCodeDescriptionMapper getInstance() {
     49         return sInstance;
     50     }
     51 
     52     // Sparse array of spoken description resource IDs indexed by key codes
     53     private final SparseIntArray mKeyCodeMap = new SparseIntArray();
     54 
     55     private KeyCodeDescriptionMapper() {
     56         // Special non-character codes defined in Keyboard
     57         mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space);
     58         mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete);
     59         mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return);
     60         mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings);
     61         mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
     62         mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
     63         mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
     64         mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
     65         mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
     66                 R.string.spoken_description_language_switch);
     67         mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
     68         mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
     69                 R.string.spoken_description_action_previous);
     70         mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji);
     71         // Because the upper-case and lower-case mappings of the following letters is depending on
     72         // the locale, the upper case descriptions should be defined here. The lower case
     73         // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
     74         // U+0049: "I" LATIN CAPITAL LETTER I
     75         // U+0069: "i" LATIN SMALL LETTER I
     76         // U+0130: "" LATIN CAPITAL LETTER I WITH DOT ABOVE
     77         // U+0131: "" LATIN SMALL LETTER DOTLESS I
     78         mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049);
     79         mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130);
     80     }
     81 
     82     /**
     83      * Returns the localized description of the action performed by a specified
     84      * key based on the current keyboard state.
     85      *
     86      * @param context The package's context.
     87      * @param keyboard The keyboard on which the key resides.
     88      * @param key The key from which to obtain a description.
     89      * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
     90      * @return a character sequence describing the action performed by pressing the key
     91      */
     92     public String getDescriptionForKey(final Context context, final Keyboard keyboard,
     93             final Key key, final boolean shouldObscure) {
     94         final int code = key.getCode();
     95 
     96         if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
     97             final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
     98             if (description != null) {
     99                 return description;
    100             }
    101         }
    102 
    103         if (code == Constants.CODE_SHIFT) {
    104             return getDescriptionForShiftKey(context, keyboard);
    105         }
    106 
    107         if (code == Constants.CODE_ENTER) {
    108             // The following function returns the correct description in all action and
    109             // regular enter cases, taking care of all modes.
    110             return getDescriptionForActionKey(context, keyboard, key);
    111         }
    112 
    113         if (code == Constants.CODE_OUTPUT_TEXT) {
    114             final String outputText = key.getOutputText();
    115             final String description = getSpokenEmoticonDescription(context, outputText);
    116             return TextUtils.isEmpty(description) ? outputText : description;
    117         }
    118 
    119         // Just attempt to speak the description.
    120         if (code != Constants.CODE_UNSPECIFIED) {
    121             // If the key description should be obscured, now is the time to do it.
    122             final boolean isDefinedNonCtrl = Character.isDefined(code)
    123                     && !Character.isISOControl(code);
    124             if (shouldObscure && isDefinedNonCtrl) {
    125                 return context.getString(OBSCURED_KEY_RES_ID);
    126             }
    127             final String description = getDescriptionForCodePoint(context, code);
    128             if (description != null) {
    129                 return description;
    130             }
    131             if (!TextUtils.isEmpty(key.getLabel())) {
    132                 return key.getLabel();
    133             }
    134             return context.getString(R.string.spoken_description_unknown);
    135         }
    136         return null;
    137     }
    138 
    139     /**
    140      * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
    141      * key or {@code null} if there is not a description provided for the
    142      * current keyboard context.
    143      *
    144      * @param context The package's context.
    145      * @param keyboard The keyboard on which the key resides.
    146      * @return a character sequence describing the action performed by pressing the key
    147      */
    148     private static String getDescriptionForSwitchAlphaSymbol(final Context context,
    149             final Keyboard keyboard) {
    150         final KeyboardId keyboardId = keyboard.mId;
    151         final int elementId = keyboardId.mElementId;
    152         final int resId;
    153 
    154         switch (elementId) {
    155         case KeyboardId.ELEMENT_ALPHABET:
    156         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
    157         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
    158         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
    159         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
    160             resId = R.string.spoken_description_to_symbol;
    161             break;
    162         case KeyboardId.ELEMENT_SYMBOLS:
    163         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
    164             resId = R.string.spoken_description_to_alpha;
    165             break;
    166         case KeyboardId.ELEMENT_PHONE:
    167             resId = R.string.spoken_description_to_symbol;
    168             break;
    169         case KeyboardId.ELEMENT_PHONE_SYMBOLS:
    170             resId = R.string.spoken_description_to_numeric;
    171             break;
    172         default:
    173             Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
    174             return null;
    175         }
    176         return context.getString(resId);
    177     }
    178 
    179     /**
    180      * Returns a context-sensitive description of the "Shift" key.
    181      *
    182      * @param context The package's context.
    183      * @param keyboard The keyboard on which the key resides.
    184      * @return A context-sensitive description of the "Shift" key.
    185      */
    186     private static String getDescriptionForShiftKey(final Context context,
    187             final Keyboard keyboard) {
    188         final KeyboardId keyboardId = keyboard.mId;
    189         final int elementId = keyboardId.mElementId;
    190         final int resId;
    191 
    192         switch (elementId) {
    193         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
    194         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
    195             resId = R.string.spoken_description_caps_lock;
    196             break;
    197         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
    198         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
    199             resId = R.string.spoken_description_shift_shifted;
    200             break;
    201         case KeyboardId.ELEMENT_SYMBOLS:
    202             resId = R.string.spoken_description_symbols_shift;
    203             break;
    204         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
    205             resId = R.string.spoken_description_symbols_shift_shifted;
    206             break;
    207         default:
    208             resId = R.string.spoken_description_shift;
    209         }
    210         return context.getString(resId);
    211     }
    212 
    213     /**
    214      * Returns a context-sensitive description of the "Enter" action key.
    215      *
    216      * @param context The package's context.
    217      * @param keyboard The keyboard on which the key resides.
    218      * @param key The key to describe.
    219      * @return Returns a context-sensitive description of the "Enter" action key.
    220      */
    221     private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
    222             final Key key) {
    223         final KeyboardId keyboardId = keyboard.mId;
    224         final int actionId = keyboardId.imeAction();
    225         final int resId;
    226 
    227         // Always use the label, if available.
    228         if (!TextUtils.isEmpty(key.getLabel())) {
    229             return key.getLabel().trim();
    230         }
    231 
    232         // Otherwise, use the action ID.
    233         switch (actionId) {
    234         case EditorInfo.IME_ACTION_SEARCH:
    235             resId = R.string.spoken_description_search;
    236             break;
    237         case EditorInfo.IME_ACTION_GO:
    238             resId = R.string.label_go_key;
    239             break;
    240         case EditorInfo.IME_ACTION_SEND:
    241             resId = R.string.label_send_key;
    242             break;
    243         case EditorInfo.IME_ACTION_NEXT:
    244             resId = R.string.label_next_key;
    245             break;
    246         case EditorInfo.IME_ACTION_DONE:
    247             resId = R.string.label_done_key;
    248             break;
    249         case EditorInfo.IME_ACTION_PREVIOUS:
    250             resId = R.string.label_previous_key;
    251             break;
    252         default:
    253             resId = R.string.spoken_description_return;
    254         }
    255         return context.getString(resId);
    256     }
    257 
    258     /**
    259      * Returns a localized character sequence describing what will happen when
    260      * the specified key is pressed based on its key code point.
    261      *
    262      * @param context The package's context.
    263      * @param codePoint The code point from which to obtain a description.
    264      * @return a character sequence describing the code point.
    265      */
    266     public String getDescriptionForCodePoint(final Context context, final int codePoint) {
    267         // If the key description should be obscured, now is the time to do it.
    268         final int index = mKeyCodeMap.indexOfKey(codePoint);
    269         if (index >= 0) {
    270             return context.getString(mKeyCodeMap.valueAt(index));
    271         }
    272         final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
    273         if (accentedLetter != null) {
    274             return accentedLetter;
    275         }
    276         // Here, <code>code</code> may be a base (non-accented) letter.
    277         final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
    278         if (unsupportedSymbol != null) {
    279             return unsupportedSymbol;
    280         }
    281         final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
    282         if (emojiDescription != null) {
    283             return emojiDescription;
    284         }
    285         if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
    286             return StringUtils.newSingleCodePointString(codePoint);
    287         }
    288         return null;
    289     }
    290 
    291     // TODO: Remove this method once TTS supports those accented letters' verbalization.
    292     private String getSpokenAccentedLetterDescription(final Context context, final int code) {
    293         final boolean isUpperCase = Character.isUpperCase(code);
    294         final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
    295         final int baseIndex = mKeyCodeMap.indexOfKey(baseCode);
    296         final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex)
    297                 : getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT);
    298         if (resId == 0) {
    299             return null;
    300         }
    301         final String spokenText = context.getString(resId);
    302         return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText)
    303                 : spokenText;
    304     }
    305 
    306     // TODO: Remove this method once TTS supports those symbols' verbalization.
    307     private String getSpokenSymbolDescription(final Context context, final int code) {
    308         final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
    309         if (resId == 0) {
    310             return null;
    311         }
    312         final String spokenText = context.getString(resId);
    313         if (!TextUtils.isEmpty(spokenText)) {
    314             return spokenText;
    315         }
    316         // If a translated description is empty, fall back to unknown symbol description.
    317         return context.getString(R.string.spoken_symbol_unknown);
    318     }
    319 
    320     // TODO: Remove this method once TTS supports emoji verbalization.
    321     private String getSpokenEmojiDescription(final Context context, final int code) {
    322         final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
    323         if (resId == 0) {
    324             return null;
    325         }
    326         final String spokenText = context.getString(resId);
    327         if (!TextUtils.isEmpty(spokenText)) {
    328             return spokenText;
    329         }
    330         // If a translated description is empty, fall back to unknown emoji description.
    331         return context.getString(R.string.spoken_emoji_unknown);
    332     }
    333 
    334     private int getSpokenDescriptionId(final Context context, final int code,
    335             final String resourceNameFormat) {
    336         final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
    337         final Resources resources = context.getResources();
    338         // Note that the resource package name may differ from the context package name.
    339         final String resourcePackageName = resources.getResourcePackageName(
    340                 R.string.spoken_description_unknown);
    341         final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
    342         if (resId != 0) {
    343             mKeyCodeMap.append(code, resId);
    344         }
    345         return resId;
    346     }
    347 
    348     // TODO: Remove this method once TTS supports emoticon verbalization.
    349     private static String getSpokenEmoticonDescription(final Context context,
    350             final String outputText) {
    351         final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX);
    352         final int textLength = outputText.length();
    353         for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) {
    354             final int codePoint = outputText.codePointAt(index);
    355             sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint));
    356         }
    357         final String resourceName = sb.toString();
    358         final Resources resources = context.getResources();
    359         // Note that the resource package name may differ from the context package name.
    360         final String resourcePackageName = resources.getResourcePackageName(
    361                 R.string.spoken_description_unknown);
    362         final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
    363         return (resId == 0) ? null : resources.getString(resId);
    364     }
    365 }
    366