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