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.util.SparseIntArray; 23 import android.view.inputmethod.EditorInfo; 24 25 import com.android.inputmethod.keyboard.Key; 26 import com.android.inputmethod.keyboard.Keyboard; 27 import com.android.inputmethod.keyboard.KeyboardId; 28 import com.android.inputmethod.latin.Constants; 29 import com.android.inputmethod.latin.R; 30 import com.android.inputmethod.latin.utils.CollectionUtils; 31 32 import java.util.HashMap; 33 34 public final class KeyCodeDescriptionMapper { 35 private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); 36 37 // The resource ID of the string spoken for obscured keys 38 private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; 39 40 private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); 41 42 // Map of key labels to spoken description resource IDs 43 private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap(); 44 45 // Sparse array of spoken description resource IDs indexed by key codes 46 private final SparseIntArray mKeyCodeMap; 47 48 public static void init() { 49 sInstance.initInternal(); 50 } 51 52 public static KeyCodeDescriptionMapper getInstance() { 53 return sInstance; 54 } 55 56 private KeyCodeDescriptionMapper() { 57 mKeyCodeMap = new SparseIntArray(); 58 } 59 60 private void initInternal() { 61 // Manual label substitutions for key labels with no string resource 62 mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); 63 64 // Special non-character codes defined in Keyboard 65 mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); 66 mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); 67 mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return); 68 mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings); 69 mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift); 70 mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic); 71 mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); 72 mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab); 73 mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH, 74 R.string.spoken_description_language_switch); 75 mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); 76 mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, 77 R.string.spoken_description_action_previous); 78 } 79 80 /** 81 * Returns the localized description of the action performed by a specified 82 * key based on the current keyboard state. 83 * <p> 84 * The order of precedence for key descriptions is: 85 * <ol> 86 * <li>Manually-defined based on the key label</li> 87 * <li>Automatic or manually-defined based on the key code</li> 88 * <li>Automatically based on the key label</li> 89 * <li>{code null} for keys with no label or key code defined</li> 90 * </p> 91 * 92 * @param context The package's context. 93 * @param keyboard The keyboard on which the key resides. 94 * @param key The key from which to obtain a description. 95 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 96 * @return a character sequence describing the action performed by pressing the key 97 */ 98 public String getDescriptionForKey(final Context context, final Keyboard keyboard, 99 final Key key, final boolean shouldObscure) { 100 final int code = key.getCode(); 101 102 if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { 103 final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard); 104 if (description != null) { 105 return description; 106 } 107 } 108 109 if (code == Constants.CODE_SHIFT) { 110 return getDescriptionForShiftKey(context, keyboard); 111 } 112 113 if (code == Constants.CODE_ENTER) { 114 // The following function returns the correct description in all action and 115 // regular enter cases, taking care of all modes. 116 return getDescriptionForActionKey(context, keyboard, key); 117 } 118 119 if (!TextUtils.isEmpty(key.getLabel())) { 120 final String label = key.getLabel().trim(); 121 122 // First, attempt to map the label to a pre-defined description. 123 if (mKeyLabelMap.containsKey(label)) { 124 return context.getString(mKeyLabelMap.get(label)); 125 } 126 } 127 128 // Just attempt to speak the description. 129 if (key.getCode() != Constants.CODE_UNSPECIFIED) { 130 return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); 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 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 String getDescriptionForShiftKey(final Context context, final Keyboard keyboard) { 183 final KeyboardId keyboardId = keyboard.mId; 184 final int elementId = keyboardId.mElementId; 185 final int resId; 186 187 switch (elementId) { 188 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 189 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 190 resId = R.string.spoken_description_caps_lock; 191 break; 192 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 193 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 194 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 195 resId = R.string.spoken_description_shift_shifted; 196 break; 197 default: 198 resId = R.string.spoken_description_shift; 199 } 200 return context.getString(resId); 201 } 202 203 /** 204 * Returns a context-sensitive description of the "Enter" action key. 205 * 206 * @param context The package's context. 207 * @param keyboard The keyboard on which the key resides. 208 * @param key The key to describe. 209 * @return Returns a context-sensitive description of the "Enter" action key. 210 */ 211 private String getDescriptionForActionKey(final Context context, final Keyboard keyboard, 212 final Key key) { 213 final KeyboardId keyboardId = keyboard.mId; 214 final int actionId = keyboardId.imeAction(); 215 final int resId; 216 217 // Always use the label, if available. 218 if (!TextUtils.isEmpty(key.getLabel())) { 219 return key.getLabel().trim(); 220 } 221 222 // Otherwise, use the action ID. 223 switch (actionId) { 224 case EditorInfo.IME_ACTION_SEARCH: 225 resId = R.string.spoken_description_search; 226 break; 227 case EditorInfo.IME_ACTION_GO: 228 resId = R.string.label_go_key; 229 break; 230 case EditorInfo.IME_ACTION_SEND: 231 resId = R.string.label_send_key; 232 break; 233 case EditorInfo.IME_ACTION_NEXT: 234 resId = R.string.label_next_key; 235 break; 236 case EditorInfo.IME_ACTION_DONE: 237 resId = R.string.label_done_key; 238 break; 239 case EditorInfo.IME_ACTION_PREVIOUS: 240 resId = R.string.label_previous_key; 241 break; 242 default: 243 resId = R.string.spoken_description_return; 244 } 245 return context.getString(resId); 246 } 247 248 /** 249 * Returns a localized character sequence describing what will happen when 250 * the specified key is pressed based on its key code. 251 * <p> 252 * The order of precedence for key code descriptions is: 253 * <ol> 254 * <li>Manually-defined shift-locked description</li> 255 * <li>Manually-defined shifted description</li> 256 * <li>Manually-defined normal description</li> 257 * <li>Automatic based on the character represented by the key code</li> 258 * <li>Fall-back for undefined or control characters</li> 259 * </ol> 260 * </p> 261 * 262 * @param context The package's context. 263 * @param keyboard The keyboard on which the key resides. 264 * @param key The key from which to obtain a description. 265 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 266 * @return a character sequence describing the action performed by pressing the key 267 */ 268 private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard, 269 final Key key, final boolean shouldObscure) { 270 final int code = key.getCode(); 271 272 // If the key description should be obscured, now is the time to do it. 273 final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); 274 if (shouldObscure && isDefinedNonCtrl) { 275 return context.getString(OBSCURED_KEY_RES_ID); 276 } 277 if (mKeyCodeMap.indexOfKey(code) >= 0) { 278 return context.getString(mKeyCodeMap.get(code)); 279 } 280 if (isDefinedNonCtrl) { 281 return Character.toString((char) code); 282 } 283 if (!TextUtils.isEmpty(key.getLabel())) { 284 return key.getLabel(); 285 } 286 return context.getString(R.string.spoken_description_unknown, code); 287 } 288 } 289