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.text.TextUtils; 21 import android.util.Log; 22 import android.view.inputmethod.EditorInfo; 23 24 import com.android.inputmethod.keyboard.Key; 25 import com.android.inputmethod.keyboard.Keyboard; 26 import com.android.inputmethod.keyboard.KeyboardId; 27 import com.android.inputmethod.latin.R; 28 29 import java.util.HashMap; 30 31 public class KeyCodeDescriptionMapper { 32 private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); 33 34 // The resource ID of the string spoken for obscured keys 35 private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; 36 37 private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); 38 39 // Map of key labels to spoken description resource IDs 40 private final HashMap<CharSequence, Integer> mKeyLabelMap; 41 42 // Map of key codes to spoken description resource IDs 43 private final HashMap<Integer, Integer> mKeyCodeMap; 44 45 public static void init() { 46 sInstance.initInternal(); 47 } 48 49 public static KeyCodeDescriptionMapper getInstance() { 50 return sInstance; 51 } 52 53 private KeyCodeDescriptionMapper() { 54 mKeyLabelMap = new HashMap<CharSequence, Integer>(); 55 mKeyCodeMap = new HashMap<Integer, Integer>(); 56 } 57 58 private void initInternal() { 59 // Manual label substitutions for key labels with no string resource 60 mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); 61 62 // Symbols that most TTS engines can't speak 63 mKeyCodeMap.put((int) ' ', R.string.spoken_description_space); 64 65 // Special non-character codes defined in Keyboard 66 mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete); 67 mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return); 68 mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings); 69 mKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift); 70 mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic); 71 mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); 72 mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab); 73 } 74 75 /** 76 * Returns the localized description of the action performed by a specified 77 * key based on the current keyboard state. 78 * <p> 79 * The order of precedence for key descriptions is: 80 * <ol> 81 * <li>Manually-defined based on the key label</li> 82 * <li>Automatic or manually-defined based on the key code</li> 83 * <li>Automatically based on the key label</li> 84 * <li>{code null} for keys with no label or key code defined</li> 85 * </p> 86 * 87 * @param context The package's context. 88 * @param keyboard The keyboard on which the key resides. 89 * @param key The key from which to obtain a description. 90 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 91 * @return a character sequence describing the action performed by pressing 92 * the key 93 */ 94 public String getDescriptionForKey(Context context, Keyboard keyboard, Key key, 95 boolean shouldObscure) { 96 final int code = key.mCode; 97 98 if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 99 final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard); 100 if (description != null) 101 return description; 102 } 103 104 if (code == Keyboard.CODE_SHIFT) { 105 return getDescriptionForShiftKey(context, keyboard); 106 } 107 108 if (code == Keyboard.CODE_ACTION_ENTER) { 109 return getDescriptionForActionKey(context, keyboard, key); 110 } 111 112 if (!TextUtils.isEmpty(key.mLabel)) { 113 final String label = key.mLabel.toString().trim(); 114 115 // First, attempt to map the label to a pre-defined description. 116 if (mKeyLabelMap.containsKey(label)) { 117 return context.getString(mKeyLabelMap.get(label)); 118 } 119 } 120 121 // Just attempt to speak the description. 122 if (key.mCode != Keyboard.CODE_UNSPECIFIED) { 123 return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); 124 } 125 126 return null; 127 } 128 129 /** 130 * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL 131 * key or {@code null} if there is not a description provided for the 132 * current keyboard context. 133 * 134 * @param context The package's context. 135 * @param keyboard The keyboard on which the key resides. 136 * @return a character sequence describing the action performed by pressing 137 * the key 138 */ 139 private String getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) { 140 final KeyboardId keyboardId = keyboard.mId; 141 final int elementId = keyboardId.mElementId; 142 final int resId; 143 144 switch (elementId) { 145 case KeyboardId.ELEMENT_ALPHABET: 146 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 147 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 148 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 149 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 150 resId = R.string.spoken_description_to_symbol; 151 break; 152 case KeyboardId.ELEMENT_SYMBOLS: 153 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 154 resId = R.string.spoken_description_to_alpha; 155 break; 156 case KeyboardId.ELEMENT_PHONE: 157 resId = R.string.spoken_description_to_symbol; 158 break; 159 case KeyboardId.ELEMENT_PHONE_SYMBOLS: 160 resId = R.string.spoken_description_to_numeric; 161 break; 162 default: 163 Log.e(TAG, "Missing description for keyboard element ID:" + elementId); 164 return null; 165 } 166 167 return context.getString(resId); 168 } 169 170 /** 171 * Returns a context-sensitive description of the "Shift" key. 172 * 173 * @param context The package's context. 174 * @param keyboard The keyboard on which the key resides. 175 * @return A context-sensitive description of the "Shift" key. 176 */ 177 private String getDescriptionForShiftKey(Context context, Keyboard keyboard) { 178 final KeyboardId keyboardId = keyboard.mId; 179 final int elementId = keyboardId.mElementId; 180 final int resId; 181 182 switch (elementId) { 183 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 184 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 185 resId = R.string.spoken_description_caps_lock; 186 break; 187 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 188 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 189 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 190 resId = R.string.spoken_description_shift_shifted; 191 break; 192 default: 193 resId = R.string.spoken_description_shift; 194 } 195 196 return context.getString(resId); 197 } 198 199 /** 200 * Returns a context-sensitive description of the "Enter" action key. 201 * 202 * @param context The package's context. 203 * @param keyboard The keyboard on which the key resides. 204 * @param key The key to describe. 205 * @return Returns a context-sensitive description of the "Enter" action 206 * key. 207 */ 208 private String getDescriptionForActionKey(Context context, Keyboard keyboard, Key key) { 209 final KeyboardId keyboardId = keyboard.mId; 210 final int actionId = keyboardId.imeActionId(); 211 final int resId; 212 213 // Always use the label, if available. 214 if (!TextUtils.isEmpty(key.mLabel)) { 215 return key.mLabel.toString().trim(); 216 } 217 218 // Otherwise, use the action ID. 219 switch (actionId) { 220 case EditorInfo.IME_ACTION_SEARCH: 221 resId = R.string.spoken_description_search; 222 break; 223 case EditorInfo.IME_ACTION_GO: 224 resId = R.string.label_go_key; 225 break; 226 case EditorInfo.IME_ACTION_SEND: 227 resId = R.string.label_send_key; 228 break; 229 case EditorInfo.IME_ACTION_NEXT: 230 resId = R.string.label_next_key; 231 break; 232 case EditorInfo.IME_ACTION_DONE: 233 resId = R.string.label_done_key; 234 break; 235 case EditorInfo.IME_ACTION_PREVIOUS: 236 resId = R.string.label_previous_key; 237 break; 238 default: 239 resId = R.string.spoken_description_return; 240 } 241 242 return context.getString(resId); 243 } 244 245 /** 246 * Returns a localized character sequence describing what will happen when 247 * the specified key is pressed based on its key code. 248 * <p> 249 * The order of precedence for key code descriptions is: 250 * <ol> 251 * <li>Manually-defined shift-locked description</li> 252 * <li>Manually-defined shifted description</li> 253 * <li>Manually-defined normal description</li> 254 * <li>Automatic based on the character represented by the key code</li> 255 * <li>Fall-back for undefined or control characters</li> 256 * </ol> 257 * </p> 258 * 259 * @param context The package's context. 260 * @param keyboard The keyboard on which the key resides. 261 * @param key The key from which to obtain a description. 262 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 263 * @return a character sequence describing the action performed by pressing 264 * the key 265 */ 266 private String getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key, 267 boolean shouldObscure) { 268 final int code = key.mCode; 269 270 // If the key description should be obscured, now is the time to do it. 271 final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); 272 if (shouldObscure && isDefinedNonCtrl) { 273 return context.getString(OBSCURED_KEY_RES_ID); 274 } 275 276 if (mKeyCodeMap.containsKey(code)) { 277 return context.getString(mKeyCodeMap.get(code)); 278 } else if (isDefinedNonCtrl) { 279 return Character.toString((char) code); 280 } else if (!TextUtils.isEmpty(key.mLabel)) { 281 return key.mLabel; 282 } else { 283 return context.getString(R.string.spoken_description_unknown, code); 284 } 285 } 286 } 287