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