• 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.keyboard;
18 
19 import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
20 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
21 import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
22 import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY;
23 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
24 
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.content.res.XmlResourceParser;
30 import android.text.InputType;
31 import android.text.TextUtils;
32 import android.util.DisplayMetrics;
33 import android.util.Log;
34 import android.util.SparseArray;
35 import android.util.Xml;
36 import android.view.inputmethod.EditorInfo;
37 import android.view.inputmethod.InputMethodSubtype;
38 
39 import com.android.inputmethod.compat.EditorInfoCompatUtils;
40 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
41 import com.android.inputmethod.keyboard.internal.KeyboardParams;
42 import com.android.inputmethod.keyboard.internal.KeysCache;
43 import com.android.inputmethod.latin.AdditionalSubtype;
44 import com.android.inputmethod.latin.CollectionUtils;
45 import com.android.inputmethod.latin.InputAttributes;
46 import com.android.inputmethod.latin.InputTypeUtils;
47 import com.android.inputmethod.latin.LatinImeLogger;
48 import com.android.inputmethod.latin.R;
49 import com.android.inputmethod.latin.ResourceUtils;
50 import com.android.inputmethod.latin.SubtypeLocale;
51 import com.android.inputmethod.latin.SubtypeSwitcher;
52 import com.android.inputmethod.latin.XmlParseUtils;
53 
54 import org.xmlpull.v1.XmlPullParser;
55 import org.xmlpull.v1.XmlPullParserException;
56 
57 import java.io.IOException;
58 import java.lang.ref.SoftReference;
59 import java.util.HashMap;
60 
61 /**
62  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
63  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
64  * {@link KeyboardLayoutSet} are related to each other.
65  * A {@link KeyboardLayoutSet} needs to be created for each
66  * {@link android.view.inputmethod.EditorInfo}.
67  */
68 public final class KeyboardLayoutSet {
69     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
70     private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
71 
72     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
73     private static final String TAG_ELEMENT = "Element";
74 
75     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
76     private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
77     private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
78 
79     private final Context mContext;
80     private final Params mParams;
81 
82     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
83             CollectionUtils.newHashMap();
84     private static final KeysCache sKeysCache = new KeysCache();
85 
86     @SuppressWarnings("serial")
87     public static final class KeyboardLayoutSetException extends RuntimeException {
88         public final KeyboardId mKeyboardId;
89 
KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId)90         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
91             super(cause);
92             mKeyboardId = keyboardId;
93         }
94     }
95 
96     private static final class ElementParams {
97         int mKeyboardXmlId;
98         boolean mProximityCharsCorrectionEnabled;
ElementParams()99         public ElementParams() {}
100     }
101 
102     public static final class Params {
103         String mKeyboardLayoutSetName;
104         int mMode;
105         EditorInfo mEditorInfo;
106         boolean mDisableTouchPositionCorrectionDataForTest;
107         boolean mVoiceKeyEnabled;
108         boolean mVoiceKeyOnMain;
109         boolean mNoSettingsKey;
110         boolean mLanguageSwitchKeyEnabled;
111         InputMethodSubtype mSubtype;
112         int mOrientation;
113         int mKeyboardWidth;
114         int mKeyboardHeight;
115         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
116         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
117                 CollectionUtils.newSparseArray();
118     }
119 
clearKeyboardCache()120     public static void clearKeyboardCache() {
121         sKeyboardCache.clear();
122         sKeysCache.clear();
123     }
124 
KeyboardLayoutSet(final Context context, final Params params)125     KeyboardLayoutSet(final Context context, final Params params) {
126         mContext = context;
127         mParams = params;
128     }
129 
getKeyboard(final int baseKeyboardLayoutSetElementId)130     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
131         final int keyboardLayoutSetElementId;
132         switch (mParams.mMode) {
133         case KeyboardId.MODE_PHONE:
134             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
135                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
136             } else {
137                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
138             }
139             break;
140         case KeyboardId.MODE_NUMBER:
141         case KeyboardId.MODE_DATE:
142         case KeyboardId.MODE_TIME:
143         case KeyboardId.MODE_DATETIME:
144             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
145             break;
146         default:
147             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
148             break;
149         }
150 
151         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
152                 keyboardLayoutSetElementId);
153         if (elementParams == null) {
154             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
155                     KeyboardId.ELEMENT_ALPHABET);
156         }
157         // Note: The keyboard for each shift state, and mode are represented as an elementName
158         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
159         // specified as an elementKeyboard attribute in the file.
160         // The KeyboardId is an internal key for a Keyboard object.
161         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
162         try {
163             return getKeyboard(elementParams, id);
164         } catch (RuntimeException e) {
165             throw new KeyboardLayoutSetException(e, id);
166         }
167     }
168 
getKeyboard(final ElementParams elementParams, final KeyboardId id)169     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
170         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
171         Keyboard keyboard = (ref == null) ? null : ref.get();
172         if (keyboard == null) {
173             final KeyboardBuilder<KeyboardParams> builder =
174                     new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
175             if (id.isAlphabetKeyboard()) {
176                 builder.setAutoGenerate(sKeysCache);
177             }
178             final int keyboardXmlId = elementParams.mKeyboardXmlId;
179             builder.load(keyboardXmlId, id);
180             if (mParams.mDisableTouchPositionCorrectionDataForTest) {
181                 builder.disableTouchPositionCorrectionDataForTest();
182             }
183             builder.setProximityCharsCorrectionEnabled(
184                     elementParams.mProximityCharsCorrectionEnabled);
185             keyboard = builder.build();
186             sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
187 
188             if (DEBUG_CACHE) {
189                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
190                         + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
191             }
192         } else if (DEBUG_CACHE) {
193             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
194         }
195 
196         return keyboard;
197     }
198 
199     public static final class Builder {
200         private final Context mContext;
201         private final String mPackageName;
202         private final Resources mResources;
203         private final EditorInfo mEditorInfo;
204 
205         private final Params mParams = new Params();
206 
207         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
208 
Builder(final Context context, final EditorInfo editorInfo)209         public Builder(final Context context, final EditorInfo editorInfo) {
210             mContext = context;
211             mPackageName = context.getPackageName();
212             mResources = context.getResources();
213             mEditorInfo = editorInfo;
214             final Params params = mParams;
215 
216             params.mMode = getKeyboardMode(editorInfo);
217             params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
218             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
219                     mPackageName, NO_SETTINGS_KEY, mEditorInfo);
220         }
221 
setScreenGeometry(final int widthPixels, final int heightPixels)222         public Builder setScreenGeometry(final int widthPixels, final int heightPixels) {
223             final Params params = mParams;
224             params.mOrientation = (heightPixels > widthPixels)
225                     ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
226             setDefaultKeyboardSize(widthPixels, heightPixels);
227             return this;
228         }
229 
setDefaultKeyboardSize(final int widthPixels, final int heightPixels)230         private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
231             final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
232                     mResources, R.array.keyboard_heights);
233             final float keyboardHeight;
234             if (TextUtils.isEmpty(keyboardHeightString)) {
235                 keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
236             } else {
237                 keyboardHeight = Float.parseFloat(keyboardHeightString)
238                         * mResources.getDisplayMetrics().density;
239             }
240             final float maxKeyboardHeight = mResources.getFraction(
241                     R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
242             float minKeyboardHeight = mResources.getFraction(
243                     R.fraction.minKeyboardHeight, heightPixels, heightPixels);
244             if (minKeyboardHeight < 0.0f) {
245                 // Specified fraction was negative, so it should be calculated against display
246                 // width.
247                 minKeyboardHeight = -mResources.getFraction(
248                         R.fraction.minKeyboardHeight, widthPixels, widthPixels);
249             }
250             // Keyboard height will not exceed maxKeyboardHeight and will not be less than
251             // minKeyboardHeight.
252             mParams.mKeyboardHeight = (int)Math.max(
253                     Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
254             mParams.mKeyboardWidth = widthPixels;
255         }
256 
setSubtype(final InputMethodSubtype subtype)257         public Builder setSubtype(final InputMethodSubtype subtype) {
258             final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
259             @SuppressWarnings("deprecation")
260             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
261                     mPackageName, FORCE_ASCII, mEditorInfo);
262             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
263                     mParams.mEditorInfo.imeOptions)
264                     || deprecatedForceAscii;
265             final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
266                     ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
267                     : subtype;
268             mParams.mSubtype = keyboardSubtype;
269             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
270                     + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype);
271             return this;
272         }
273 
setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, final boolean languageSwitchKeyEnabled)274         public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
275                 final boolean languageSwitchKeyEnabled) {
276             @SuppressWarnings("deprecation")
277             final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
278                     null, NO_MICROPHONE_COMPAT, mEditorInfo);
279             final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
280                     mPackageName, NO_MICROPHONE, mEditorInfo)
281                     || deprecatedNoMicrophone;
282             mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
283             mParams.mVoiceKeyOnMain = voiceKeyOnMain;
284             mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
285             return this;
286         }
287 
disableTouchPositionCorrectionData()288         public void disableTouchPositionCorrectionData() {
289             mParams.mDisableTouchPositionCorrectionDataForTest = true;
290         }
291 
build()292         public KeyboardLayoutSet build() {
293             if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
294                 throw new RuntimeException("Screen geometry is not specified");
295             if (mParams.mSubtype == null)
296                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
297             final String packageName = mResources.getResourcePackageName(
298                     R.xml.keyboard_layout_set_qwerty);
299             final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
300             final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
301             try {
302                 parseKeyboardLayoutSet(mResources, xmlId);
303             } catch (final IOException e) {
304                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
305             } catch (final XmlPullParserException e) {
306                 throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e);
307             }
308             return new KeyboardLayoutSet(mContext, mParams);
309         }
310 
parseKeyboardLayoutSet(final Resources res, final int resId)311         private void parseKeyboardLayoutSet(final Resources res, final int resId)
312                 throws XmlPullParserException, IOException {
313             final XmlResourceParser parser = res.getXml(resId);
314             try {
315                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
316                     final int event = parser.next();
317                     if (event == XmlPullParser.START_TAG) {
318                         final String tag = parser.getName();
319                         if (TAG_KEYBOARD_SET.equals(tag)) {
320                             parseKeyboardLayoutSetContent(parser);
321                         } else {
322                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
323                         }
324                     }
325                 }
326             } finally {
327                 parser.close();
328             }
329         }
330 
parseKeyboardLayoutSetContent(final XmlPullParser parser)331         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
332                 throws XmlPullParserException, IOException {
333             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
334                 final int event = parser.next();
335                 if (event == XmlPullParser.START_TAG) {
336                     final String tag = parser.getName();
337                     if (TAG_ELEMENT.equals(tag)) {
338                         parseKeyboardLayoutSetElement(parser);
339                     } else {
340                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
341                     }
342                 } else if (event == XmlPullParser.END_TAG) {
343                     final String tag = parser.getName();
344                     if (TAG_KEYBOARD_SET.equals(tag)) {
345                         break;
346                     } else {
347                         throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
348                     }
349                 }
350             }
351         }
352 
parseKeyboardLayoutSetElement(final XmlPullParser parser)353         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
354                 throws XmlPullParserException, IOException {
355             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
356                     R.styleable.KeyboardLayoutSet_Element);
357             try {
358                 XmlParseUtils.checkAttributeExists(a,
359                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
360                         TAG_ELEMENT, parser);
361                 XmlParseUtils.checkAttributeExists(a,
362                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
363                         TAG_ELEMENT, parser);
364                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
365 
366                 final ElementParams elementParams = new ElementParams();
367                 final int elementName = a.getInt(
368                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
369                 elementParams.mKeyboardXmlId = a.getResourceId(
370                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
371                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
372                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
373                         false);
374                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
375             } finally {
376                 a.recycle();
377             }
378         }
379 
getKeyboardMode(final EditorInfo editorInfo)380         private static int getKeyboardMode(final EditorInfo editorInfo) {
381             if (editorInfo == null)
382                 return KeyboardId.MODE_TEXT;
383 
384             final int inputType = editorInfo.inputType;
385             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
386 
387             switch (inputType & InputType.TYPE_MASK_CLASS) {
388             case InputType.TYPE_CLASS_NUMBER:
389                 return KeyboardId.MODE_NUMBER;
390             case InputType.TYPE_CLASS_DATETIME:
391                 switch (variation) {
392                 case InputType.TYPE_DATETIME_VARIATION_DATE:
393                     return KeyboardId.MODE_DATE;
394                 case InputType.TYPE_DATETIME_VARIATION_TIME:
395                     return KeyboardId.MODE_TIME;
396                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
397                     return KeyboardId.MODE_DATETIME;
398                 }
399             case InputType.TYPE_CLASS_PHONE:
400                 return KeyboardId.MODE_PHONE;
401             case InputType.TYPE_CLASS_TEXT:
402                 if (InputTypeUtils.isEmailVariation(variation)) {
403                     return KeyboardId.MODE_EMAIL;
404                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
405                     return KeyboardId.MODE_URL;
406                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
407                     return KeyboardId.MODE_IM;
408                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
409                     return KeyboardId.MODE_TEXT;
410                 } else {
411                     return KeyboardId.MODE_TEXT;
412                 }
413             default:
414                 return KeyboardId.MODE_TEXT;
415             }
416         }
417     }
418 
createKeyboardSetForSpellChecker(final Context context, final String locale, final String layout)419     public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
420             final String locale, final String layout) {
421         final InputMethodSubtype subtype =
422                 AdditionalSubtype.createAdditionalSubtype(locale, layout, null);
423         return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
424                 SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
425     }
426 
createKeyboardSetForTest(final Context context, final InputMethodSubtype subtype, final int orientation, final boolean testCasesHaveTouchCoordinates)427     public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
428             final InputMethodSubtype subtype, final int orientation,
429             final boolean testCasesHaveTouchCoordinates) {
430         final DisplayMetrics dm = context.getResources().getDisplayMetrics();
431         final int width;
432         final int height;
433         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
434             width = Math.max(dm.widthPixels, dm.heightPixels);
435             height = Math.min(dm.widthPixels, dm.heightPixels);
436         } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
437             width = Math.min(dm.widthPixels, dm.heightPixels);
438             height = Math.max(dm.widthPixels, dm.heightPixels);
439         } else {
440             throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
441                     + "ORIENTATION_PORTRAIT: orientation=" + orientation);
442         }
443         return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates);
444     }
445 
createKeyboardSet(final Context context, final InputMethodSubtype subtype, final int width, final int height, final boolean testCasesHaveTouchCoordinates)446     private static KeyboardLayoutSet createKeyboardSet(final Context context,
447             final InputMethodSubtype subtype, final int width, final int height,
448             final boolean testCasesHaveTouchCoordinates) {
449         final EditorInfo editorInfo = new EditorInfo();
450         editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
451         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
452                 context, editorInfo);
453         builder.setScreenGeometry(width, height);
454         builder.setSubtype(subtype);
455         if (!testCasesHaveTouchCoordinates) {
456             // For spell checker and tests
457             builder.disableTouchPositionCorrectionData();
458         }
459         return builder.build();
460     }
461 }
462