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.SharedPreferences; 21 import android.text.TextUtils; 22 23 import com.android.inputmethod.keyboard.Key; 24 import com.android.inputmethod.keyboard.Keyboard; 25 import com.android.inputmethod.keyboard.KeyboardId; 26 import com.android.inputmethod.latin.R; 27 28 import java.util.HashMap; 29 30 public class KeyCodeDescriptionMapper { 31 // The resource ID of the string spoken for obscured keys 32 private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; 33 34 private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); 35 36 // Map of key labels to spoken description resource IDs 37 private final HashMap<CharSequence, Integer> mKeyLabelMap; 38 39 // Map of key codes to spoken description resource IDs 40 private final HashMap<Integer, Integer> mKeyCodeMap; 41 42 // Map of shifted key codes to spoken description resource IDs 43 private final HashMap<Integer, Integer> mShiftedKeyCodeMap; 44 45 // Map of shift-locked key codes to spoken description resource IDs 46 private final HashMap<Integer, Integer> mShiftLockedKeyCodeMap; 47 48 public static void init(Context context, SharedPreferences prefs) { 49 sInstance.initInternal(context, prefs); 50 } 51 52 public static KeyCodeDescriptionMapper getInstance() { 53 return sInstance; 54 } 55 56 private KeyCodeDescriptionMapper() { 57 mKeyLabelMap = new HashMap<CharSequence, Integer>(); 58 mKeyCodeMap = new HashMap<Integer, Integer>(); 59 mShiftedKeyCodeMap = new HashMap<Integer, Integer>(); 60 mShiftLockedKeyCodeMap = new HashMap<Integer, Integer>(); 61 } 62 63 private void initInternal(Context context, SharedPreferences prefs) { 64 // Manual label substitutions for key labels with no string resource 65 mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); 66 67 // Symbols that most TTS engines can't speak 68 mKeyCodeMap.put((int) '.', R.string.spoken_description_period); 69 mKeyCodeMap.put((int) ',', R.string.spoken_description_comma); 70 mKeyCodeMap.put((int) '(', R.string.spoken_description_left_parenthesis); 71 mKeyCodeMap.put((int) ')', R.string.spoken_description_right_parenthesis); 72 mKeyCodeMap.put((int) ':', R.string.spoken_description_colon); 73 mKeyCodeMap.put((int) ';', R.string.spoken_description_semicolon); 74 mKeyCodeMap.put((int) '!', R.string.spoken_description_exclamation_mark); 75 mKeyCodeMap.put((int) '?', R.string.spoken_description_question_mark); 76 mKeyCodeMap.put((int) '\"', R.string.spoken_description_double_quote); 77 mKeyCodeMap.put((int) '\'', R.string.spoken_description_single_quote); 78 mKeyCodeMap.put((int) '*', R.string.spoken_description_star); 79 mKeyCodeMap.put((int) '#', R.string.spoken_description_pound); 80 mKeyCodeMap.put((int) ' ', R.string.spoken_description_space); 81 82 // Non-ASCII symbols (must use escape codes!) 83 mKeyCodeMap.put((int) '\u2022', R.string.spoken_description_dot); 84 mKeyCodeMap.put((int) '\u221A', R.string.spoken_description_square_root); 85 mKeyCodeMap.put((int) '\u03C0', R.string.spoken_description_pi); 86 mKeyCodeMap.put((int) '\u0394', R.string.spoken_description_delta); 87 mKeyCodeMap.put((int) '\u2122', R.string.spoken_description_trademark); 88 mKeyCodeMap.put((int) '\u2105', R.string.spoken_description_care_of); 89 mKeyCodeMap.put((int) '\u2026', R.string.spoken_description_ellipsis); 90 mKeyCodeMap.put((int) '\u201E', R.string.spoken_description_low_double_quote); 91 mKeyCodeMap.put((int) '\uFF0A', R.string.spoken_description_star); 92 93 // Special non-character codes defined in Keyboard 94 mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete); 95 mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return); 96 mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings); 97 mKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift); 98 mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic); 99 mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); 100 mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab); 101 102 // Shifted versions of non-character codes defined in Keyboard 103 mShiftedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift_shifted); 104 105 // Shift-locked versions of non-character codes defined in Keyboard 106 mShiftLockedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_caps_lock); 107 } 108 109 /** 110 * Returns the localized description of the action performed by a specified 111 * key based on the current keyboard state. 112 * <p> 113 * The order of precedence for key descriptions is: 114 * <ol> 115 * <li>Manually-defined based on the key label</li> 116 * <li>Automatic or manually-defined based on the key code</li> 117 * <li>Automatically based on the key label</li> 118 * <li>{code null} for keys with no label or key code defined</li> 119 * </p> 120 * 121 * @param context The package's context. 122 * @param keyboard The keyboard on which the key resides. 123 * @param key The key from which to obtain a description. 124 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 125 * @return a character sequence describing the action performed by pressing 126 * the key 127 */ 128 public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key, 129 boolean shouldObscure) { 130 if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 131 final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard); 132 if (description != null) 133 return description; 134 } 135 136 if (!TextUtils.isEmpty(key.mLabel)) { 137 final String label = key.mLabel.toString().trim(); 138 139 if (mKeyLabelMap.containsKey(label)) { 140 return context.getString(mKeyLabelMap.get(label)); 141 } else if (label.length() == 1 142 || (keyboard.isManualTemporaryUpperCase() && !TextUtils 143 .isEmpty(key.mHintLabel))) { 144 return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); 145 } else { 146 return label; 147 } 148 } else if (key.mCode != Keyboard.CODE_DUMMY) { 149 return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); 150 } 151 152 return null; 153 } 154 155 /** 156 * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL 157 * key or {@code null} if there is not a description provided for the 158 * current keyboard context. 159 * 160 * @param context The package's context. 161 * @param keyboard The keyboard on which the key resides. 162 * @return a character sequence describing the action performed by pressing 163 * the key 164 */ 165 private CharSequence getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) { 166 final KeyboardId id = keyboard.mId; 167 168 if (id.isAlphabetKeyboard()) { 169 return context.getString(R.string.spoken_description_to_symbol); 170 } else if (id.isSymbolsKeyboard()) { 171 return context.getString(R.string.spoken_description_to_alpha); 172 } else if (id.isPhoneShiftKeyboard()) { 173 return context.getString(R.string.spoken_description_to_numeric); 174 } else if (id.isPhoneKeyboard()) { 175 return context.getString(R.string.spoken_description_to_symbol); 176 } else { 177 return null; 178 } 179 } 180 181 /** 182 * Returns the keycode for the specified key given the current keyboard 183 * state. 184 * 185 * @param keyboard The keyboard on which the key resides. 186 * @param key The key from which to obtain a key code. 187 * @return the key code for the specified key 188 */ 189 private int getCorrectKeyCode(Keyboard keyboard, Key key) { 190 if (keyboard.isManualTemporaryUpperCase() && !TextUtils.isEmpty(key.mHintLabel)) { 191 return key.mHintLabel.charAt(0); 192 } else { 193 return key.mCode; 194 } 195 } 196 197 /** 198 * Returns a localized character sequence describing what will happen when 199 * the specified key is pressed based on its key code. 200 * <p> 201 * The order of precedence for key code descriptions is: 202 * <ol> 203 * <li>Manually-defined shift-locked description</li> 204 * <li>Manually-defined shifted description</li> 205 * <li>Manually-defined normal description</li> 206 * <li>Automatic based on the character represented by the key code</li> 207 * <li>Fall-back for undefined or control characters</li> 208 * </ol> 209 * </p> 210 * 211 * @param context The package's context. 212 * @param keyboard The keyboard on which the key resides. 213 * @param key The key from which to obtain a description. 214 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 215 * @return a character sequence describing the action performed by pressing 216 * the key 217 */ 218 private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key, 219 boolean shouldObscure) { 220 final int code = getCorrectKeyCode(keyboard, key); 221 222 if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) { 223 return context.getString(mShiftLockedKeyCodeMap.get(code)); 224 } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) { 225 return context.getString(mShiftedKeyCodeMap.get(code)); 226 } 227 228 // If the key description should be obscured, now is the time to do it. 229 final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); 230 if (shouldObscure && isDefinedNonCtrl) { 231 return context.getString(OBSCURED_KEY_RES_ID); 232 } 233 234 if (mKeyCodeMap.containsKey(code)) { 235 return context.getString(mKeyCodeMap.get(code)); 236 } else if (isDefinedNonCtrl) { 237 return Character.toString((char) code); 238 } else { 239 return context.getString(R.string.spoken_description_unknown, code); 240 } 241 } 242 } 243