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