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.text.TextUtils;
21import android.util.Log;
22import android.util.SparseIntArray;
23import android.view.inputmethod.EditorInfo;
24
25import com.android.inputmethod.keyboard.Key;
26import com.android.inputmethod.keyboard.Keyboard;
27import com.android.inputmethod.keyboard.KeyboardId;
28import com.android.inputmethod.latin.Constants;
29import com.android.inputmethod.latin.R;
30import com.android.inputmethod.latin.utils.CollectionUtils;
31
32import java.util.HashMap;
33
34public 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