/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.accessibility; import android.content.Context; import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.StringUtils; import java.util.Locale; final class KeyCodeDescriptionMapper { private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"; private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"; private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"; private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon"; private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X"; // The resource ID of the string spoken for obscured keys private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); public static KeyCodeDescriptionMapper getInstance() { return sInstance; } // Sparse array of spoken description resource IDs indexed by key codes private final SparseIntArray mKeyCodeMap = new SparseIntArray(); private KeyCodeDescriptionMapper() { // Special non-character codes defined in Keyboard mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return); mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings); mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift); mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic); mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab); mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH, R.string.spoken_description_language_switch); mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous); mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji); // Because the upper-case and lower-case mappings of the following letters is depending on // the locale, the upper case descriptions should be defined here. The lower case // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}. // U+0049: "I" LATIN CAPITAL LETTER I // U+0069: "i" LATIN SMALL LETTER I // U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE // U+0131: "ı" LATIN SMALL LETTER DOTLESS I mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049); mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130); } /** * Returns the localized description of the action performed by a specified * key based on the current keyboard state. * * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @param key The key from which to obtain a description. * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. * @return a character sequence describing the action performed by pressing the key */ public String getDescriptionForKey(final Context context, final Keyboard keyboard, final Key key, final boolean shouldObscure) { final int code = key.getCode(); if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard); if (description != null) { return description; } } if (code == Constants.CODE_SHIFT) { return getDescriptionForShiftKey(context, keyboard); } if (code == Constants.CODE_ENTER) { // The following function returns the correct description in all action and // regular enter cases, taking care of all modes. return getDescriptionForActionKey(context, keyboard, key); } if (code == Constants.CODE_OUTPUT_TEXT) { final String outputText = key.getOutputText(); final String description = getSpokenEmoticonDescription(context, outputText); return TextUtils.isEmpty(description) ? outputText : description; } // Just attempt to speak the description. if (code != Constants.CODE_UNSPECIFIED) { // If the key description should be obscured, now is the time to do it. final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); if (shouldObscure && isDefinedNonCtrl) { return context.getString(OBSCURED_KEY_RES_ID); } final String description = getDescriptionForCodePoint(context, code); if (description != null) { return description; } if (!TextUtils.isEmpty(key.getLabel())) { return key.getLabel(); } return context.getString(R.string.spoken_description_unknown); } return null; } /** * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL * key or {@code null} if there is not a description provided for the * current keyboard context. * * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @return a character sequence describing the action performed by pressing the key */ private static String getDescriptionForSwitchAlphaSymbol(final Context context, final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; final int resId; switch (elementId) { case KeyboardId.ELEMENT_ALPHABET: case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: resId = R.string.spoken_description_to_symbol; break; case KeyboardId.ELEMENT_SYMBOLS: case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: resId = R.string.spoken_description_to_alpha; break; case KeyboardId.ELEMENT_PHONE: resId = R.string.spoken_description_to_symbol; break; case KeyboardId.ELEMENT_PHONE_SYMBOLS: resId = R.string.spoken_description_to_numeric; break; default: Log.e(TAG, "Missing description for keyboard element ID:" + elementId); return null; } return context.getString(resId); } /** * Returns a context-sensitive description of the "Shift" key. * * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @return A context-sensitive description of the "Shift" key. */ private static String getDescriptionForShiftKey(final Context context, final Keyboard keyboard) { final KeyboardId keyboardId = keyboard.mId; final int elementId = keyboardId.mElementId; final int resId; switch (elementId) { case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: resId = R.string.spoken_description_caps_lock; break; case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: resId = R.string.spoken_description_shift_shifted; break; case KeyboardId.ELEMENT_SYMBOLS: resId = R.string.spoken_description_symbols_shift; break; case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: resId = R.string.spoken_description_symbols_shift_shifted; break; default: resId = R.string.spoken_description_shift; } return context.getString(resId); } /** * Returns a context-sensitive description of the "Enter" action key. * * @param context The package's context. * @param keyboard The keyboard on which the key resides. * @param key The key to describe. * @return Returns a context-sensitive description of the "Enter" action key. */ private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard, final Key key) { final KeyboardId keyboardId = keyboard.mId; final int actionId = keyboardId.imeAction(); final int resId; // Always use the label, if available. if (!TextUtils.isEmpty(key.getLabel())) { return key.getLabel().trim(); } // Otherwise, use the action ID. switch (actionId) { case EditorInfo.IME_ACTION_SEARCH: resId = R.string.spoken_description_search; break; case EditorInfo.IME_ACTION_GO: resId = R.string.label_go_key; break; case EditorInfo.IME_ACTION_SEND: resId = R.string.label_send_key; break; case EditorInfo.IME_ACTION_NEXT: resId = R.string.label_next_key; break; case EditorInfo.IME_ACTION_DONE: resId = R.string.label_done_key; break; case EditorInfo.IME_ACTION_PREVIOUS: resId = R.string.label_previous_key; break; default: resId = R.string.spoken_description_return; } return context.getString(resId); } /** * Returns a localized character sequence describing what will happen when * the specified key is pressed based on its key code point. * * @param context The package's context. * @param codePoint The code point from which to obtain a description. * @return a character sequence describing the code point. */ public String getDescriptionForCodePoint(final Context context, final int codePoint) { // If the key description should be obscured, now is the time to do it. final int index = mKeyCodeMap.indexOfKey(codePoint); if (index >= 0) { return context.getString(mKeyCodeMap.valueAt(index)); } final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint); if (accentedLetter != null) { return accentedLetter; } // Here, code may be a base (non-accented) letter. final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint); if (unsupportedSymbol != null) { return unsupportedSymbol; } final String emojiDescription = getSpokenEmojiDescription(context, codePoint); if (emojiDescription != null) { return emojiDescription; } if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) { return StringUtils.newSingleCodePointString(codePoint); } return null; } // TODO: Remove this method once TTS supports those accented letters' verbalization. private String getSpokenAccentedLetterDescription(final Context context, final int code) { final boolean isUpperCase = Character.isUpperCase(code); final int baseCode = isUpperCase ? Character.toLowerCase(code) : code; final int baseIndex = mKeyCodeMap.indexOfKey(baseCode); final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex) : getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT); if (resId == 0) { return null; } final String spokenText = context.getString(resId); return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText) : spokenText; } // TODO: Remove this method once TTS supports those symbols' verbalization. private String getSpokenSymbolDescription(final Context context, final int code) { final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT); if (resId == 0) { return null; } final String spokenText = context.getString(resId); if (!TextUtils.isEmpty(spokenText)) { return spokenText; } // If a translated description is empty, fall back to unknown symbol description. return context.getString(R.string.spoken_symbol_unknown); } // TODO: Remove this method once TTS supports emoji verbalization. private String getSpokenEmojiDescription(final Context context, final int code) { final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT); if (resId == 0) { return null; } final String spokenText = context.getString(resId); if (!TextUtils.isEmpty(spokenText)) { return spokenText; } // If a translated description is empty, fall back to unknown emoji description. return context.getString(R.string.spoken_emoji_unknown); } private int getSpokenDescriptionId(final Context context, final int code, final String resourceNameFormat) { final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code); final Resources resources = context.getResources(); // Note that the resource package name may differ from the context package name. final String resourcePackageName = resources.getResourcePackageName( R.string.spoken_description_unknown); final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName); if (resId != 0) { mKeyCodeMap.append(code, resId); } return resId; } // TODO: Remove this method once TTS supports emoticon verbalization. private static String getSpokenEmoticonDescription(final Context context, final String outputText) { final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX); final int textLength = outputText.length(); for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) { final int codePoint = outputText.codePointAt(index); sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint)); } final String resourceName = sb.toString(); final Resources resources = context.getResources(); // Note that the resource package name may differ from the context package name. final String resourcePackageName = resources.getResourcePackageName( R.string.spoken_description_unknown); final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName); return (resId == 0) ? null : resources.getString(resId); } }