KeyCodeDescriptionMapper.java revision 305778b53a5e7c865cae4010e657d00bb9bf5075
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 17package com.android.inputmethod.accessibility; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.text.TextUtils; 22import android.util.Log; 23import android.util.SparseIntArray; 24import android.view.inputmethod.EditorInfo; 25 26import com.android.inputmethod.keyboard.Key; 27import com.android.inputmethod.keyboard.Keyboard; 28import com.android.inputmethod.keyboard.KeyboardId; 29import com.android.inputmethod.latin.Constants; 30import com.android.inputmethod.latin.R; 31 32import java.util.Locale; 33 34public final class KeyCodeDescriptionMapper { 35 private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName(); 36 private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"; 37 38 // The resource ID of the string spoken for obscured keys 39 private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; 40 41 private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper(); 42 43 // Sparse array of spoken description resource IDs indexed by key codes 44 private final SparseIntArray mKeyCodeMap; 45 46 public static void init() { 47 sInstance.initInternal(); 48 } 49 50 public static KeyCodeDescriptionMapper getInstance() { 51 return sInstance; 52 } 53 54 private KeyCodeDescriptionMapper() { 55 mKeyCodeMap = new SparseIntArray(); 56 } 57 58 private void initInternal() { 59 // Special non-character codes defined in Keyboard 60 mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); 61 mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); 62 mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return); 63 mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings); 64 mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift); 65 mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic); 66 mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); 67 mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab); 68 mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH, 69 R.string.spoken_description_language_switch); 70 mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); 71 mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, 72 R.string.spoken_description_action_previous); 73 mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji); 74 } 75 76 /** 77 * Returns the localized description of the action performed by a specified 78 * key based on the current keyboard state. 79 * <p> 80 * The order of precedence for key descriptions is: 81 * <ol> 82 * <li>Manually-defined based on the key label</li> 83 * <li>Automatic or manually-defined based on the key code</li> 84 * <li>Automatically based on the key label</li> 85 * <li>{code null} for keys with no label or key code defined</li> 86 * </p> 87 * 88 * @param context The package's context. 89 * @param keyboard The keyboard on which the key resides. 90 * @param key The key from which to obtain a description. 91 * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured. 92 * @return a character sequence describing the action performed by pressing the key 93 */ 94 public String getDescriptionForKey(final Context context, final Keyboard keyboard, 95 final Key key, final boolean shouldObscure) { 96 final int code = key.getCode(); 97 98 if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { 99 final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard); 100 if (description != null) { 101 return description; 102 } 103 } 104 105 if (code == Constants.CODE_SHIFT) { 106 return getDescriptionForShiftKey(context, keyboard); 107 } 108 109 if (code == Constants.CODE_ENTER) { 110 // The following function returns the correct description in all action and 111 // regular enter cases, taking care of all modes. 112 return getDescriptionForActionKey(context, keyboard, key); 113 } 114 115 if (code == Constants.CODE_OUTPUT_TEXT) { 116 return key.getOutputText(); 117 } 118 119 // Just attempt to speak the description. 120 if (code != Constants.CODE_UNSPECIFIED) { 121 return getDescriptionForKeyCode(context, keyboard, key, shouldObscure); 122 } 123 return null; 124 } 125 126 /** 127 * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL 128 * key or {@code null} if there is not a description provided for the 129 * current keyboard context. 130 * 131 * @param context The package's context. 132 * @param keyboard The keyboard on which the key resides. 133 * @return a character sequence describing the action performed by pressing the key 134 */ 135 private static String getDescriptionForSwitchAlphaSymbol(final Context context, 136 final Keyboard keyboard) { 137 final KeyboardId keyboardId = keyboard.mId; 138 final int elementId = keyboardId.mElementId; 139 final int resId; 140 141 switch (elementId) { 142 case KeyboardId.ELEMENT_ALPHABET: 143 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 144 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 145 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 146 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 147 resId = R.string.spoken_description_to_symbol; 148 break; 149 case KeyboardId.ELEMENT_SYMBOLS: 150 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 151 resId = R.string.spoken_description_to_alpha; 152 break; 153 case KeyboardId.ELEMENT_PHONE: 154 resId = R.string.spoken_description_to_symbol; 155 break; 156 case KeyboardId.ELEMENT_PHONE_SYMBOLS: 157 resId = R.string.spoken_description_to_numeric; 158 break; 159 default: 160 Log.e(TAG, "Missing description for keyboard element ID:" + elementId); 161 return null; 162 } 163 return context.getString(resId); 164 } 165 166 /** 167 * Returns a context-sensitive description of the "Shift" key. 168 * 169 * @param context The package's context. 170 * @param keyboard The keyboard on which the key resides. 171 * @return A context-sensitive description of the "Shift" key. 172 */ 173 private static String getDescriptionForShiftKey(final Context context, 174 final Keyboard keyboard) { 175 final KeyboardId keyboardId = keyboard.mId; 176 final int elementId = keyboardId.mElementId; 177 final int resId; 178 179 switch (elementId) { 180 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 181 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 182 resId = R.string.spoken_description_caps_lock; 183 break; 184 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 185 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 186 resId = R.string.spoken_description_shift_shifted; 187 break; 188 case KeyboardId.ELEMENT_SYMBOLS: 189 resId = R.string.spoken_description_symbols_shift; 190 break; 191 case KeyboardId.ELEMENT_SYMBOLS_SHIFTED: 192 resId = R.string.spoken_description_symbols_shift_shifted; 193 break; 194 default: 195 resId = R.string.spoken_description_shift; 196 } 197 return context.getString(resId); 198 } 199 200 /** 201 * Returns a context-sensitive description of the "Enter" action key. 202 * 203 * @param context The package's context. 204 * @param keyboard The keyboard on which the key resides. 205 * @param key The key to describe. 206 * @return Returns a context-sensitive description of the "Enter" action key. 207 */ 208 private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard, 209 final Key key) { 210 final KeyboardId keyboardId = keyboard.mId; 211 final int actionId = keyboardId.imeAction(); 212 final int resId; 213 214 // Always use the label, if available. 215 if (!TextUtils.isEmpty(key.getLabel())) { 216 return key.getLabel().trim(); 217 } 218 219 // Otherwise, use the action ID. 220 switch (actionId) { 221 case EditorInfo.IME_ACTION_SEARCH: 222 resId = R.string.spoken_description_search; 223 break; 224 case EditorInfo.IME_ACTION_GO: 225 resId = R.string.label_go_key; 226 break; 227 case EditorInfo.IME_ACTION_SEND: 228 resId = R.string.label_send_key; 229 break; 230 case EditorInfo.IME_ACTION_NEXT: 231 resId = R.string.label_next_key; 232 break; 233 case EditorInfo.IME_ACTION_DONE: 234 resId = R.string.label_done_key; 235 break; 236 case EditorInfo.IME_ACTION_PREVIOUS: 237 resId = R.string.label_previous_key; 238 break; 239 default: 240 resId = R.string.spoken_description_return; 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 the key 264 */ 265 private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard, 266 final Key key, final boolean shouldObscure) { 267 final int code = key.getCode(); 268 269 // If the key description should be obscured, now is the time to do it. 270 final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code); 271 if (shouldObscure && isDefinedNonCtrl) { 272 return context.getString(OBSCURED_KEY_RES_ID); 273 } 274 if (mKeyCodeMap.indexOfKey(code) >= 0) { 275 return context.getString(mKeyCodeMap.get(code)); 276 } 277 final int spokenEmojiId = getSpokenDescriptionId( 278 context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT); 279 if (spokenEmojiId != 0) { 280 final String spokenEmoji = context.getString(spokenEmojiId); 281 mKeyCodeMap.append(code, spokenEmojiId); 282 return spokenEmoji; 283 } 284 if (isDefinedNonCtrl) { 285 return Character.toString((char) code); 286 } 287 if (!TextUtils.isEmpty(key.getLabel())) { 288 return key.getLabel(); 289 } 290 return context.getString(R.string.spoken_description_unknown, code); 291 } 292 293 private static int getSpokenDescriptionId(final Context context, final int code, 294 final String resourceNameFormat) { 295 final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code); 296 final Resources resources = context.getResources(); 297 final String packageName = resources.getResourcePackageName( 298 R.string.spoken_description_unknown); 299 return resources.getIdentifier(resourceName, "string", packageName); 300 } 301} 302