• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.res.Resources;
21 import android.text.TextUtils;
22 import android.util.Log;
23 import android.util.SparseIntArray;
24 import android.view.inputmethod.EditorInfo;
25 
26 import com.android.inputmethod.keyboard.Key;
27 import com.android.inputmethod.keyboard.Keyboard;
28 import com.android.inputmethod.keyboard.KeyboardId;
29 import com.android.inputmethod.latin.R;
30 import com.android.inputmethod.latin.common.Constants;
31 import com.android.inputmethod.latin.common.StringUtils;
32 
33 import java.util.Locale;
34 
35 final class KeyCodeDescriptionMapper {
36     private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
37     private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
38     private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
39     private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
40     private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon";
41     private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X";
42 
43     // The resource ID of the string spoken for obscured keys
44     private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
45 
46     private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
47 
getInstance()48     public static KeyCodeDescriptionMapper getInstance() {
49         return sInstance;
50     }
51 
52     // Sparse array of spoken description resource IDs indexed by key codes
53     private final SparseIntArray mKeyCodeMap = new SparseIntArray();
54 
KeyCodeDescriptionMapper()55     private KeyCodeDescriptionMapper() {
56         // Special non-character codes defined in Keyboard
57         mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space);
58         mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete);
59         mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return);
60         mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings);
61         mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
62         mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
63         mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
64         mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
65         mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
66                 R.string.spoken_description_language_switch);
67         mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
68         mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
69                 R.string.spoken_description_action_previous);
70         mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji);
71         // Because the upper-case and lower-case mappings of the following letters is depending on
72         // the locale, the upper case descriptions should be defined here. The lower case
73         // descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
74         // U+0049: "I" LATIN CAPITAL LETTER I
75         // U+0069: "i" LATIN SMALL LETTER I
76         // U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
77         // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
78         mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049);
79         mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130);
80     }
81 
82     /**
83      * Returns the localized description of the action performed by a specified
84      * key based on the current keyboard state.
85      *
86      * @param context The package's context.
87      * @param keyboard The keyboard on which the key resides.
88      * @param key The key from which to obtain a description.
89      * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
90      * @return a character sequence describing the action performed by pressing the key
91      */
getDescriptionForKey(final Context context, final Keyboard keyboard, final Key key, final boolean shouldObscure)92     public String getDescriptionForKey(final Context context, final Keyboard keyboard,
93             final Key key, final boolean shouldObscure) {
94         final int code = key.getCode();
95 
96         if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
97             final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
98             if (description != null) {
99                 return description;
100             }
101         }
102 
103         if (code == Constants.CODE_SHIFT) {
104             return getDescriptionForShiftKey(context, keyboard);
105         }
106 
107         if (code == Constants.CODE_ENTER) {
108             // The following function returns the correct description in all action and
109             // regular enter cases, taking care of all modes.
110             return getDescriptionForActionKey(context, keyboard, key);
111         }
112 
113         if (code == Constants.CODE_OUTPUT_TEXT) {
114             final String outputText = key.getOutputText();
115             final String description = getSpokenEmoticonDescription(context, outputText);
116             return TextUtils.isEmpty(description) ? outputText : description;
117         }
118 
119         // Just attempt to speak the description.
120         if (code != Constants.CODE_UNSPECIFIED) {
121             // If the key description should be obscured, now is the time to do it.
122             final boolean isDefinedNonCtrl = Character.isDefined(code)
123                     && !Character.isISOControl(code);
124             if (shouldObscure && isDefinedNonCtrl) {
125                 return context.getString(OBSCURED_KEY_RES_ID);
126             }
127             final String description = getDescriptionForCodePoint(context, code);
128             if (description != null) {
129                 return description;
130             }
131             if (!TextUtils.isEmpty(key.getLabel())) {
132                 return key.getLabel();
133             }
134             return context.getString(R.string.spoken_description_unknown);
135         }
136         return null;
137     }
138 
139     /**
140      * Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
141      * key or {@code null} if there is not a description provided for the
142      * current keyboard context.
143      *
144      * @param context The package's context.
145      * @param keyboard The keyboard on which the key resides.
146      * @return a character sequence describing the action performed by pressing the key
147      */
getDescriptionForSwitchAlphaSymbol(final Context context, final Keyboard keyboard)148     private static String getDescriptionForSwitchAlphaSymbol(final Context context,
149             final Keyboard keyboard) {
150         final KeyboardId keyboardId = keyboard.mId;
151         final int elementId = keyboardId.mElementId;
152         final int resId;
153 
154         switch (elementId) {
155         case KeyboardId.ELEMENT_ALPHABET:
156         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
157         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
158         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
159         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
160             resId = R.string.spoken_description_to_symbol;
161             break;
162         case KeyboardId.ELEMENT_SYMBOLS:
163         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
164             resId = R.string.spoken_description_to_alpha;
165             break;
166         case KeyboardId.ELEMENT_PHONE:
167             resId = R.string.spoken_description_to_symbol;
168             break;
169         case KeyboardId.ELEMENT_PHONE_SYMBOLS:
170             resId = R.string.spoken_description_to_numeric;
171             break;
172         default:
173             Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
174             return null;
175         }
176         return context.getString(resId);
177     }
178 
179     /**
180      * Returns a context-sensitive description of the "Shift" key.
181      *
182      * @param context The package's context.
183      * @param keyboard The keyboard on which the key resides.
184      * @return A context-sensitive description of the "Shift" key.
185      */
getDescriptionForShiftKey(final Context context, final Keyboard keyboard)186     private static String getDescriptionForShiftKey(final Context context,
187             final Keyboard keyboard) {
188         final KeyboardId keyboardId = keyboard.mId;
189         final int elementId = keyboardId.mElementId;
190         final int resId;
191 
192         switch (elementId) {
193         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
194         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
195             resId = R.string.spoken_description_caps_lock;
196             break;
197         case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
198         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
199             resId = R.string.spoken_description_shift_shifted;
200             break;
201         case KeyboardId.ELEMENT_SYMBOLS:
202             resId = R.string.spoken_description_symbols_shift;
203             break;
204         case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
205             resId = R.string.spoken_description_symbols_shift_shifted;
206             break;
207         default:
208             resId = R.string.spoken_description_shift;
209         }
210         return context.getString(resId);
211     }
212 
213     /**
214      * Returns a context-sensitive description of the "Enter" action key.
215      *
216      * @param context The package's context.
217      * @param keyboard The keyboard on which the key resides.
218      * @param key The key to describe.
219      * @return Returns a context-sensitive description of the "Enter" action key.
220      */
getDescriptionForActionKey(final Context context, final Keyboard keyboard, final Key key)221     private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
222             final Key key) {
223         final KeyboardId keyboardId = keyboard.mId;
224         final int actionId = keyboardId.imeAction();
225         final int resId;
226 
227         // Always use the label, if available.
228         if (!TextUtils.isEmpty(key.getLabel())) {
229             return key.getLabel().trim();
230         }
231 
232         // Otherwise, use the action ID.
233         switch (actionId) {
234         case EditorInfo.IME_ACTION_SEARCH:
235             resId = R.string.spoken_description_search;
236             break;
237         case EditorInfo.IME_ACTION_GO:
238             resId = R.string.label_go_key;
239             break;
240         case EditorInfo.IME_ACTION_SEND:
241             resId = R.string.label_send_key;
242             break;
243         case EditorInfo.IME_ACTION_NEXT:
244             resId = R.string.label_next_key;
245             break;
246         case EditorInfo.IME_ACTION_DONE:
247             resId = R.string.label_done_key;
248             break;
249         case EditorInfo.IME_ACTION_PREVIOUS:
250             resId = R.string.label_previous_key;
251             break;
252         default:
253             resId = R.string.spoken_description_return;
254         }
255         return context.getString(resId);
256     }
257 
258     /**
259      * Returns a localized character sequence describing what will happen when
260      * the specified key is pressed based on its key code point.
261      *
262      * @param context The package's context.
263      * @param codePoint The code point from which to obtain a description.
264      * @return a character sequence describing the code point.
265      */
getDescriptionForCodePoint(final Context context, final int codePoint)266     public String getDescriptionForCodePoint(final Context context, final int codePoint) {
267         // If the key description should be obscured, now is the time to do it.
268         final int index = mKeyCodeMap.indexOfKey(codePoint);
269         if (index >= 0) {
270             return context.getString(mKeyCodeMap.valueAt(index));
271         }
272         final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
273         if (accentedLetter != null) {
274             return accentedLetter;
275         }
276         // Here, <code>code</code> may be a base (non-accented) letter.
277         final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
278         if (unsupportedSymbol != null) {
279             return unsupportedSymbol;
280         }
281         final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
282         if (emojiDescription != null) {
283             return emojiDescription;
284         }
285         if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
286             return StringUtils.newSingleCodePointString(codePoint);
287         }
288         return null;
289     }
290 
291     // TODO: Remove this method once TTS supports those accented letters' verbalization.
getSpokenAccentedLetterDescription(final Context context, final int code)292     private String getSpokenAccentedLetterDescription(final Context context, final int code) {
293         final boolean isUpperCase = Character.isUpperCase(code);
294         final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
295         final int baseIndex = mKeyCodeMap.indexOfKey(baseCode);
296         final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex)
297                 : getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT);
298         if (resId == 0) {
299             return null;
300         }
301         final String spokenText = context.getString(resId);
302         return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText)
303                 : spokenText;
304     }
305 
306     // TODO: Remove this method once TTS supports those symbols' verbalization.
getSpokenSymbolDescription(final Context context, final int code)307     private String getSpokenSymbolDescription(final Context context, final int code) {
308         final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
309         if (resId == 0) {
310             return null;
311         }
312         final String spokenText = context.getString(resId);
313         if (!TextUtils.isEmpty(spokenText)) {
314             return spokenText;
315         }
316         // If a translated description is empty, fall back to unknown symbol description.
317         return context.getString(R.string.spoken_symbol_unknown);
318     }
319 
320     // TODO: Remove this method once TTS supports emoji verbalization.
getSpokenEmojiDescription(final Context context, final int code)321     private String getSpokenEmojiDescription(final Context context, final int code) {
322         final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
323         if (resId == 0) {
324             return null;
325         }
326         final String spokenText = context.getString(resId);
327         if (!TextUtils.isEmpty(spokenText)) {
328             return spokenText;
329         }
330         // If a translated description is empty, fall back to unknown emoji description.
331         return context.getString(R.string.spoken_emoji_unknown);
332     }
333 
getSpokenDescriptionId(final Context context, final int code, final String resourceNameFormat)334     private int getSpokenDescriptionId(final Context context, final int code,
335             final String resourceNameFormat) {
336         final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
337         final Resources resources = context.getResources();
338         // Note that the resource package name may differ from the context package name.
339         final String resourcePackageName = resources.getResourcePackageName(
340                 R.string.spoken_description_unknown);
341         final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
342         if (resId != 0) {
343             mKeyCodeMap.append(code, resId);
344         }
345         return resId;
346     }
347 
348     // TODO: Remove this method once TTS supports emoticon verbalization.
getSpokenEmoticonDescription(final Context context, final String outputText)349     private static String getSpokenEmoticonDescription(final Context context,
350             final String outputText) {
351         final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX);
352         final int textLength = outputText.length();
353         for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) {
354             final int codePoint = outputText.codePointAt(index);
355             sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint));
356         }
357         final String resourceName = sb.toString();
358         final Resources resources = context.getResources();
359         // Note that the resource package name may differ from the context package name.
360         final String resourcePackageName = resources.getResourcePackageName(
361                 R.string.spoken_description_unknown);
362         final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
363         return (resId == 0) ? null : resources.getString(resId);
364     }
365 }
366