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