1 /*
2  * Copyright (C) 2008-2009 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.inputmethodservice;
18 
19 import org.xmlpull.v1.XmlPullParserException;
20 
21 import android.annotation.XmlRes;
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.content.res.XmlResourceParser;
26 import android.graphics.drawable.Drawable;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.util.TypedValue;
30 import android.util.Xml;
31 import android.util.DisplayMetrics;
32 
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.StringTokenizer;
37 
38 
39 /**
40  * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
41  * consists of rows of keys.
42  * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
43  * <pre>
44  * &lt;Keyboard
45  *         android:keyWidth="%10p"
46  *         android:keyHeight="50px"
47  *         android:horizontalGap="2px"
48  *         android:verticalGap="2px" &gt;
49  *     &lt;Row android:keyWidth="32px" &gt;
50  *         &lt;Key android:keyLabel="A" /&gt;
51  *         ...
52  *     &lt;/Row&gt;
53  *     ...
54  * &lt;/Keyboard&gt;
55  * </pre>
56  * @attr ref android.R.styleable#Keyboard_keyWidth
57  * @attr ref android.R.styleable#Keyboard_keyHeight
58  * @attr ref android.R.styleable#Keyboard_horizontalGap
59  * @attr ref android.R.styleable#Keyboard_verticalGap
60  */
61 public class Keyboard {
62 
63     static final String TAG = "Keyboard";
64 
65     // Keyboard XML Tags
66     private static final String TAG_KEYBOARD = "Keyboard";
67     private static final String TAG_ROW = "Row";
68     private static final String TAG_KEY = "Key";
69 
70     public static final int EDGE_LEFT = 0x01;
71     public static final int EDGE_RIGHT = 0x02;
72     public static final int EDGE_TOP = 0x04;
73     public static final int EDGE_BOTTOM = 0x08;
74 
75     public static final int KEYCODE_SHIFT = -1;
76     public static final int KEYCODE_MODE_CHANGE = -2;
77     public static final int KEYCODE_CANCEL = -3;
78     public static final int KEYCODE_DONE = -4;
79     public static final int KEYCODE_DELETE = -5;
80     public static final int KEYCODE_ALT = -6;
81 
82     /** Keyboard label **/
83     private CharSequence mLabel;
84 
85     /** Horizontal gap default for all rows */
86     private int mDefaultHorizontalGap;
87 
88     /** Default key width */
89     private int mDefaultWidth;
90 
91     /** Default key height */
92     private int mDefaultHeight;
93 
94     /** Default gap between rows */
95     private int mDefaultVerticalGap;
96 
97     /** Is the keyboard in the shifted state */
98     private boolean mShifted;
99 
100     /** Key instance for the shift key, if present */
101     private Key[] mShiftKeys = { null, null };
102 
103     /** Key index for the shift key, if present */
104     private int[] mShiftKeyIndices = {-1, -1};
105 
106     /** Current key width, while loading the keyboard */
107     private int mKeyWidth;
108 
109     /** Current key height, while loading the keyboard */
110     private int mKeyHeight;
111 
112     /** Total height of the keyboard, including the padding and keys */
113     private int mTotalHeight;
114 
115     /**
116      * Total width of the keyboard, including left side gaps and keys, but not any gaps on the
117      * right side.
118      */
119     private int mTotalWidth;
120 
121     /** List of keys in this keyboard */
122     private List<Key> mKeys;
123 
124     /** List of modifier keys such as Shift & Alt, if any */
125     private List<Key> mModifierKeys;
126 
127     /** Width of the screen available to fit the keyboard */
128     private int mDisplayWidth;
129 
130     /** Height of the screen */
131     private int mDisplayHeight;
132 
133     /** Keyboard mode, or zero, if none.  */
134     private int mKeyboardMode;
135 
136     // Variables for pre-computing nearest keys.
137 
138     private static final int GRID_WIDTH = 10;
139     private static final int GRID_HEIGHT = 5;
140     private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
141     private int mCellWidth;
142     private int mCellHeight;
143     private int[][] mGridNeighbors;
144     private int mProximityThreshold;
145     /** Number of key widths from current touch point to search for nearest keys. */
146     private static float SEARCH_DISTANCE = 1.8f;
147 
148     private ArrayList<Row> rows = new ArrayList<Row>();
149 
150     /**
151      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
152      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
153      * defines.
154      * @attr ref android.R.styleable#Keyboard_keyWidth
155      * @attr ref android.R.styleable#Keyboard_keyHeight
156      * @attr ref android.R.styleable#Keyboard_horizontalGap
157      * @attr ref android.R.styleable#Keyboard_verticalGap
158      * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags
159      * @attr ref android.R.styleable#Keyboard_Row_keyboardMode
160      */
161     public static class Row {
162         /** Default width of a key in this row. */
163         public int defaultWidth;
164         /** Default height of a key in this row. */
165         public int defaultHeight;
166         /** Default horizontal gap between keys in this row. */
167         public int defaultHorizontalGap;
168         /** Vertical gap following this row. */
169         public int verticalGap;
170 
171         ArrayList<Key> mKeys = new ArrayList<Key>();
172 
173         /**
174          * Edge flags for this row of keys. Possible values that can be assigned are
175          * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
176          */
177         public int rowEdgeFlags;
178 
179         /** The keyboard mode for this row */
180         public int mode;
181 
182         private Keyboard parent;
183 
Row(Keyboard parent)184         public Row(Keyboard parent) {
185             this.parent = parent;
186         }
187 
Row(Resources res, Keyboard parent, XmlResourceParser parser)188         public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
189             this.parent = parent;
190             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
191                     com.android.internal.R.styleable.Keyboard);
192             defaultWidth = getDimensionOrFraction(a,
193                     com.android.internal.R.styleable.Keyboard_keyWidth,
194                     parent.mDisplayWidth, parent.mDefaultWidth);
195             defaultHeight = getDimensionOrFraction(a,
196                     com.android.internal.R.styleable.Keyboard_keyHeight,
197                     parent.mDisplayHeight, parent.mDefaultHeight);
198             defaultHorizontalGap = getDimensionOrFraction(a,
199                     com.android.internal.R.styleable.Keyboard_horizontalGap,
200                     parent.mDisplayWidth, parent.mDefaultHorizontalGap);
201             verticalGap = getDimensionOrFraction(a,
202                     com.android.internal.R.styleable.Keyboard_verticalGap,
203                     parent.mDisplayHeight, parent.mDefaultVerticalGap);
204             a.recycle();
205             a = res.obtainAttributes(Xml.asAttributeSet(parser),
206                     com.android.internal.R.styleable.Keyboard_Row);
207             rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0);
208             mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode,
209                     0);
210         }
211     }
212 
213     /**
214      * Class for describing the position and characteristics of a single key in the keyboard.
215      *
216      * @attr ref android.R.styleable#Keyboard_keyWidth
217      * @attr ref android.R.styleable#Keyboard_keyHeight
218      * @attr ref android.R.styleable#Keyboard_horizontalGap
219      * @attr ref android.R.styleable#Keyboard_Key_codes
220      * @attr ref android.R.styleable#Keyboard_Key_keyIcon
221      * @attr ref android.R.styleable#Keyboard_Key_keyLabel
222      * @attr ref android.R.styleable#Keyboard_Key_iconPreview
223      * @attr ref android.R.styleable#Keyboard_Key_isSticky
224      * @attr ref android.R.styleable#Keyboard_Key_isRepeatable
225      * @attr ref android.R.styleable#Keyboard_Key_isModifier
226      * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard
227      * @attr ref android.R.styleable#Keyboard_Key_popupCharacters
228      * @attr ref android.R.styleable#Keyboard_Key_keyOutputText
229      * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags
230      */
231     public static class Key {
232         /**
233          * All the key codes (unicode or custom code) that this key could generate, zero'th
234          * being the most important.
235          */
236         public int[] codes;
237 
238         /** Label to display */
239         public CharSequence label;
240 
241         /** Icon to display instead of a label. Icon takes precedence over a label */
242         public Drawable icon;
243         /** Preview version of the icon, for the preview popup */
244         public Drawable iconPreview;
245         /** Width of the key, not including the gap */
246         public int width;
247         /** Height of the key, not including the gap */
248         public int height;
249         /** The horizontal gap before this key */
250         public int gap;
251         /** Whether this key is sticky, i.e., a toggle key */
252         public boolean sticky;
253         /** X coordinate of the key in the keyboard layout */
254         public int x;
255         /** Y coordinate of the key in the keyboard layout */
256         public int y;
257         /** The current pressed state of this key */
258         public boolean pressed;
259         /** If this is a sticky key, is it on? */
260         public boolean on;
261         /** Text to output when pressed. This can be multiple characters, like ".com" */
262         public CharSequence text;
263         /** Popup characters */
264         public CharSequence popupCharacters;
265 
266         /**
267          * Flags that specify the anchoring to edges of the keyboard for detecting touch events
268          * that are just out of the boundary of the key. This is a bit mask of
269          * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
270          * {@link Keyboard#EDGE_BOTTOM}.
271          */
272         public int edgeFlags;
273         /** Whether this is a modifier key, such as Shift or Alt */
274         public boolean modifier;
275         /** The keyboard that this key belongs to */
276         private Keyboard keyboard;
277         /**
278          * If this key pops up a mini keyboard, this is the resource id for the XML layout for that
279          * keyboard.
280          */
281         public int popupResId;
282         /** Whether this key repeats itself when held down */
283         public boolean repeatable;
284 
285 
286         private final static int[] KEY_STATE_NORMAL_ON = {
287             android.R.attr.state_checkable,
288             android.R.attr.state_checked
289         };
290 
291         private final static int[] KEY_STATE_PRESSED_ON = {
292             android.R.attr.state_pressed,
293             android.R.attr.state_checkable,
294             android.R.attr.state_checked
295         };
296 
297         private final static int[] KEY_STATE_NORMAL_OFF = {
298             android.R.attr.state_checkable
299         };
300 
301         private final static int[] KEY_STATE_PRESSED_OFF = {
302             android.R.attr.state_pressed,
303             android.R.attr.state_checkable
304         };
305 
306         private final static int[] KEY_STATE_NORMAL = {
307         };
308 
309         private final static int[] KEY_STATE_PRESSED = {
310             android.R.attr.state_pressed
311         };
312 
313         /** Create an empty key with no attributes. */
Key(Row parent)314         public Key(Row parent) {
315             keyboard = parent.parent;
316             height = parent.defaultHeight;
317             width = parent.defaultWidth;
318             gap = parent.defaultHorizontalGap;
319             edgeFlags = parent.rowEdgeFlags;
320         }
321 
322         /** Create a key with the given top-left coordinate and extract its attributes from
323          * the XML parser.
324          * @param res resources associated with the caller's context
325          * @param parent the row that this key belongs to. The row must already be attached to
326          * a {@link Keyboard}.
327          * @param x the x coordinate of the top-left
328          * @param y the y coordinate of the top-left
329          * @param parser the XML parser containing the attributes for this key
330          */
Key(Resources res, Row parent, int x, int y, XmlResourceParser parser)331         public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
332             this(parent);
333 
334             this.x = x;
335             this.y = y;
336 
337             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
338                     com.android.internal.R.styleable.Keyboard);
339 
340             width = getDimensionOrFraction(a,
341                     com.android.internal.R.styleable.Keyboard_keyWidth,
342                     keyboard.mDisplayWidth, parent.defaultWidth);
343             height = getDimensionOrFraction(a,
344                     com.android.internal.R.styleable.Keyboard_keyHeight,
345                     keyboard.mDisplayHeight, parent.defaultHeight);
346             gap = getDimensionOrFraction(a,
347                     com.android.internal.R.styleable.Keyboard_horizontalGap,
348                     keyboard.mDisplayWidth, parent.defaultHorizontalGap);
349             a.recycle();
350             a = res.obtainAttributes(Xml.asAttributeSet(parser),
351                     com.android.internal.R.styleable.Keyboard_Key);
352             this.x += gap;
353             TypedValue codesValue = new TypedValue();
354             a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes,
355                     codesValue);
356             if (codesValue.type == TypedValue.TYPE_INT_DEC
357                     || codesValue.type == TypedValue.TYPE_INT_HEX) {
358                 codes = new int[] { codesValue.data };
359             } else if (codesValue.type == TypedValue.TYPE_STRING) {
360                 codes = parseCSV(codesValue.string.toString());
361             }
362 
363             iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview);
364             if (iconPreview != null) {
365                 iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
366                         iconPreview.getIntrinsicHeight());
367             }
368             popupCharacters = a.getText(
369                     com.android.internal.R.styleable.Keyboard_Key_popupCharacters);
370             popupResId = a.getResourceId(
371                     com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0);
372             repeatable = a.getBoolean(
373                     com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false);
374             modifier = a.getBoolean(
375                     com.android.internal.R.styleable.Keyboard_Key_isModifier, false);
376             sticky = a.getBoolean(
377                     com.android.internal.R.styleable.Keyboard_Key_isSticky, false);
378             edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0);
379             edgeFlags |= parent.rowEdgeFlags;
380 
381             icon = a.getDrawable(
382                     com.android.internal.R.styleable.Keyboard_Key_keyIcon);
383             if (icon != null) {
384                 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
385             }
386             label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel);
387             text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText);
388 
389             if (codes == null && !TextUtils.isEmpty(label)) {
390                 codes = new int[] { label.charAt(0) };
391             }
392             a.recycle();
393         }
394 
395         /**
396          * Informs the key that it has been pressed, in case it needs to change its appearance or
397          * state.
398          * @see #onReleased(boolean)
399          */
onPressed()400         public void onPressed() {
401             pressed = !pressed;
402         }
403 
404         /**
405          * Changes the pressed state of the key.
406          *
407          * <p>Toggled state of the key will be flipped when all the following conditions are
408          * fulfilled:</p>
409          *
410          * <ul>
411          *     <li>This is a sticky key, that is, {@link #sticky} is {@code true}.
412          *     <li>The parameter {@code inside} is {@code true}.
413          *     <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
414          *         {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
415          * </ul>
416          *
417          * @param inside whether the finger was released inside the key. Works only on Android M and
418          * later. See the method document for details.
419          * @see #onPressed()
420          */
onReleased(boolean inside)421         public void onReleased(boolean inside) {
422             pressed = !pressed;
423             if (sticky && inside) {
424                 on = !on;
425             }
426         }
427 
parseCSV(String value)428         int[] parseCSV(String value) {
429             int count = 0;
430             int lastIndex = 0;
431             if (value.length() > 0) {
432                 count++;
433                 while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
434                     count++;
435                 }
436             }
437             int[] values = new int[count];
438             count = 0;
439             StringTokenizer st = new StringTokenizer(value, ",");
440             while (st.hasMoreTokens()) {
441                 try {
442                     values[count++] = Integer.parseInt(st.nextToken());
443                 } catch (NumberFormatException nfe) {
444                     Log.e(TAG, "Error parsing keycodes " + value);
445                 }
446             }
447             return values;
448         }
449 
450         /**
451          * Detects if a point falls inside this key.
452          * @param x the x-coordinate of the point
453          * @param y the y-coordinate of the point
454          * @return whether or not the point falls inside the key. If the key is attached to an edge,
455          * it will assume that all points between the key and the edge are considered to be inside
456          * the key.
457          */
isInside(int x, int y)458         public boolean isInside(int x, int y) {
459             boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
460             boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
461             boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
462             boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
463             if ((x >= this.x || (leftEdge && x <= this.x + this.width))
464                     && (x < this.x + this.width || (rightEdge && x >= this.x))
465                     && (y >= this.y || (topEdge && y <= this.y + this.height))
466                     && (y < this.y + this.height || (bottomEdge && y >= this.y))) {
467                 return true;
468             } else {
469                 return false;
470             }
471         }
472 
473         /**
474          * Returns the square of the distance between the center of the key and the given point.
475          * @param x the x-coordinate of the point
476          * @param y the y-coordinate of the point
477          * @return the square of the distance of the point from the center of the key
478          */
squaredDistanceFrom(int x, int y)479         public int squaredDistanceFrom(int x, int y) {
480             int xDist = this.x + width / 2 - x;
481             int yDist = this.y + height / 2 - y;
482             return xDist * xDist + yDist * yDist;
483         }
484 
485         /**
486          * Returns the drawable state for the key, based on the current state and type of the key.
487          * @return the drawable state of the key.
488          * @see android.graphics.drawable.StateListDrawable#setState(int[])
489          */
getCurrentDrawableState()490         public int[] getCurrentDrawableState() {
491             int[] states = KEY_STATE_NORMAL;
492 
493             if (on) {
494                 if (pressed) {
495                     states = KEY_STATE_PRESSED_ON;
496                 } else {
497                     states = KEY_STATE_NORMAL_ON;
498                 }
499             } else {
500                 if (sticky) {
501                     if (pressed) {
502                         states = KEY_STATE_PRESSED_OFF;
503                     } else {
504                         states = KEY_STATE_NORMAL_OFF;
505                     }
506                 } else {
507                     if (pressed) {
508                         states = KEY_STATE_PRESSED;
509                     }
510                 }
511             }
512             return states;
513         }
514     }
515 
516     /**
517      * Creates a keyboard from the given xml key layout file.
518      * @param context the application or service context
519      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
520      */
Keyboard(Context context, int xmlLayoutResId)521     public Keyboard(Context context, int xmlLayoutResId) {
522         this(context, xmlLayoutResId, 0);
523     }
524 
525     /**
526      * Creates a keyboard from the given xml key layout file. Weeds out rows
527      * that have a keyboard mode defined but don't match the specified mode.
528      * @param context the application or service context
529      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
530      * @param modeId keyboard mode identifier
531      * @param width sets width of keyboard
532      * @param height sets height of keyboard
533      */
Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, int height)534     public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width,
535             int height) {
536         mDisplayWidth = width;
537         mDisplayHeight = height;
538 
539         mDefaultHorizontalGap = 0;
540         mDefaultWidth = mDisplayWidth / 10;
541         mDefaultVerticalGap = 0;
542         mDefaultHeight = mDefaultWidth;
543         mKeys = new ArrayList<Key>();
544         mModifierKeys = new ArrayList<Key>();
545         mKeyboardMode = modeId;
546         loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
547     }
548 
549     /**
550      * Creates a keyboard from the given xml key layout file. Weeds out rows
551      * that have a keyboard mode defined but don't match the specified mode.
552      * @param context the application or service context
553      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
554      * @param modeId keyboard mode identifier
555      */
Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId)556     public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) {
557         DisplayMetrics dm = context.getResources().getDisplayMetrics();
558         mDisplayWidth = dm.widthPixels;
559         mDisplayHeight = dm.heightPixels;
560         //Log.v(TAG, "keyboard's display metrics:" + dm);
561 
562         mDefaultHorizontalGap = 0;
563         mDefaultWidth = mDisplayWidth / 10;
564         mDefaultVerticalGap = 0;
565         mDefaultHeight = mDefaultWidth;
566         mKeys = new ArrayList<Key>();
567         mModifierKeys = new ArrayList<Key>();
568         mKeyboardMode = modeId;
569         loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
570     }
571 
572     /**
573      * <p>Creates a blank keyboard from the given resource file and populates it with the specified
574      * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
575      * </p>
576      * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
577      * possible in each row.</p>
578      * @param context the application or service context
579      * @param layoutTemplateResId the layout template file, containing no keys.
580      * @param characters the list of characters to display on the keyboard. One key will be created
581      * for each character.
582      * @param columns the number of columns of keys to display. If this number is greater than the
583      * number of keys that can fit in a row, it will be ignored. If this number is -1, the
584      * keyboard will fit as many keys as possible in each row.
585      */
Keyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding)586     public Keyboard(Context context, int layoutTemplateResId,
587             CharSequence characters, int columns, int horizontalPadding) {
588         this(context, layoutTemplateResId);
589         int x = 0;
590         int y = 0;
591         int column = 0;
592         mTotalWidth = 0;
593 
594         Row row = new Row(this);
595         row.defaultHeight = mDefaultHeight;
596         row.defaultWidth = mDefaultWidth;
597         row.defaultHorizontalGap = mDefaultHorizontalGap;
598         row.verticalGap = mDefaultVerticalGap;
599         row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
600         final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
601         for (int i = 0; i < characters.length(); i++) {
602             char c = characters.charAt(i);
603             if (column >= maxColumns
604                     || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
605                 x = 0;
606                 y += mDefaultVerticalGap + mDefaultHeight;
607                 column = 0;
608             }
609             final Key key = new Key(row);
610             key.x = x;
611             key.y = y;
612             key.label = String.valueOf(c);
613             key.codes = new int[] { c };
614             column++;
615             x += key.width + key.gap;
616             mKeys.add(key);
617             row.mKeys.add(key);
618             if (x > mTotalWidth) {
619                 mTotalWidth = x;
620             }
621         }
622         mTotalHeight = y + mDefaultHeight;
623         rows.add(row);
624     }
625 
resize(int newWidth, int newHeight)626     final void resize(int newWidth, int newHeight) {
627         int numRows = rows.size();
628         for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
629             Row row = rows.get(rowIndex);
630             int numKeys = row.mKeys.size();
631             int totalGap = 0;
632             int totalWidth = 0;
633             for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
634                 Key key = row.mKeys.get(keyIndex);
635                 if (keyIndex > 0) {
636                     totalGap += key.gap;
637                 }
638                 totalWidth += key.width;
639             }
640             if (totalGap + totalWidth > newWidth) {
641                 int x = 0;
642                 float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
643                 for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
644                     Key key = row.mKeys.get(keyIndex);
645                     key.width *= scaleFactor;
646                     key.x = x;
647                     x += key.width + key.gap;
648                 }
649             }
650         }
651         mTotalWidth = newWidth;
652         // TODO: This does not adjust the vertical placement according to the new size.
653         // The main problem in the previous code was horizontal placement/size, but we should
654         // also recalculate the vertical sizes/positions when we get this resize call.
655     }
656 
getKeys()657     public List<Key> getKeys() {
658         return mKeys;
659     }
660 
getModifierKeys()661     public List<Key> getModifierKeys() {
662         return mModifierKeys;
663     }
664 
getHorizontalGap()665     protected int getHorizontalGap() {
666         return mDefaultHorizontalGap;
667     }
668 
setHorizontalGap(int gap)669     protected void setHorizontalGap(int gap) {
670         mDefaultHorizontalGap = gap;
671     }
672 
getVerticalGap()673     protected int getVerticalGap() {
674         return mDefaultVerticalGap;
675     }
676 
setVerticalGap(int gap)677     protected void setVerticalGap(int gap) {
678         mDefaultVerticalGap = gap;
679     }
680 
getKeyHeight()681     protected int getKeyHeight() {
682         return mDefaultHeight;
683     }
684 
setKeyHeight(int height)685     protected void setKeyHeight(int height) {
686         mDefaultHeight = height;
687     }
688 
getKeyWidth()689     protected int getKeyWidth() {
690         return mDefaultWidth;
691     }
692 
setKeyWidth(int width)693     protected void setKeyWidth(int width) {
694         mDefaultWidth = width;
695     }
696 
697     /**
698      * Returns the total height of the keyboard
699      * @return the total height of the keyboard
700      */
getHeight()701     public int getHeight() {
702         return mTotalHeight;
703     }
704 
getMinWidth()705     public int getMinWidth() {
706         return mTotalWidth;
707     }
708 
setShifted(boolean shiftState)709     public boolean setShifted(boolean shiftState) {
710         for (Key shiftKey : mShiftKeys) {
711             if (shiftKey != null) {
712                 shiftKey.on = shiftState;
713             }
714         }
715         if (mShifted != shiftState) {
716             mShifted = shiftState;
717             return true;
718         }
719         return false;
720     }
721 
isShifted()722     public boolean isShifted() {
723         return mShifted;
724     }
725 
726     /**
727      * @hide
728      */
getShiftKeyIndices()729     public int[] getShiftKeyIndices() {
730         return mShiftKeyIndices;
731     }
732 
getShiftKeyIndex()733     public int getShiftKeyIndex() {
734         return mShiftKeyIndices[0];
735     }
736 
computeNearestNeighbors()737     private void computeNearestNeighbors() {
738         // Round-up so we don't have any pixels outside the grid
739         mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
740         mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
741         mGridNeighbors = new int[GRID_SIZE][];
742         int[] indices = new int[mKeys.size()];
743         final int gridWidth = GRID_WIDTH * mCellWidth;
744         final int gridHeight = GRID_HEIGHT * mCellHeight;
745         for (int x = 0; x < gridWidth; x += mCellWidth) {
746             for (int y = 0; y < gridHeight; y += mCellHeight) {
747                 int count = 0;
748                 for (int i = 0; i < mKeys.size(); i++) {
749                     final Key key = mKeys.get(i);
750                     if (key.squaredDistanceFrom(x, y) < mProximityThreshold ||
751                             key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold ||
752                             key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
753                                 < mProximityThreshold ||
754                             key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
755                         indices[count++] = i;
756                     }
757                 }
758                 int [] cell = new int[count];
759                 System.arraycopy(indices, 0, cell, 0, count);
760                 mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
761             }
762         }
763     }
764 
765     /**
766      * Returns the indices of the keys that are closest to the given point.
767      * @param x the x-coordinate of the point
768      * @param y the y-coordinate of the point
769      * @return the array of integer indices for the nearest keys to the given point. If the given
770      * point is out of range, then an array of size zero is returned.
771      */
getNearestKeys(int x, int y)772     public int[] getNearestKeys(int x, int y) {
773         if (mGridNeighbors == null) computeNearestNeighbors();
774         if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
775             int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
776             if (index < GRID_SIZE) {
777                 return mGridNeighbors[index];
778             }
779         }
780         return new int[0];
781     }
782 
createRowFromXml(Resources res, XmlResourceParser parser)783     protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
784         return new Row(res, this, parser);
785     }
786 
createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser)787     protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
788             XmlResourceParser parser) {
789         return new Key(res, parent, x, y, parser);
790     }
791 
loadKeyboard(Context context, XmlResourceParser parser)792     private void loadKeyboard(Context context, XmlResourceParser parser) {
793         boolean inKey = false;
794         boolean inRow = false;
795         boolean leftMostKey = false;
796         int row = 0;
797         int x = 0;
798         int y = 0;
799         Key key = null;
800         Row currentRow = null;
801         Resources res = context.getResources();
802         boolean skipRow = false;
803 
804         try {
805             int event;
806             while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
807                 if (event == XmlResourceParser.START_TAG) {
808                     String tag = parser.getName();
809                     if (TAG_ROW.equals(tag)) {
810                         inRow = true;
811                         x = 0;
812                         currentRow = createRowFromXml(res, parser);
813                         rows.add(currentRow);
814                         skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
815                         if (skipRow) {
816                             skipToEndOfRow(parser);
817                             inRow = false;
818                         }
819                    } else if (TAG_KEY.equals(tag)) {
820                         inKey = true;
821                         key = createKeyFromXml(res, currentRow, x, y, parser);
822                         mKeys.add(key);
823                         if (key.codes[0] == KEYCODE_SHIFT) {
824                             // Find available shift key slot and put this shift key in it
825                             for (int i = 0; i < mShiftKeys.length; i++) {
826                                 if (mShiftKeys[i] == null) {
827                                     mShiftKeys[i] = key;
828                                     mShiftKeyIndices[i] = mKeys.size()-1;
829                                     break;
830                                 }
831                             }
832                             mModifierKeys.add(key);
833                         } else if (key.codes[0] == KEYCODE_ALT) {
834                             mModifierKeys.add(key);
835                         }
836                         currentRow.mKeys.add(key);
837                     } else if (TAG_KEYBOARD.equals(tag)) {
838                         parseKeyboardAttributes(res, parser);
839                     }
840                 } else if (event == XmlResourceParser.END_TAG) {
841                     if (inKey) {
842                         inKey = false;
843                         x += key.gap + key.width;
844                         if (x > mTotalWidth) {
845                             mTotalWidth = x;
846                         }
847                     } else if (inRow) {
848                         inRow = false;
849                         y += currentRow.verticalGap;
850                         y += currentRow.defaultHeight;
851                         row++;
852                     } else {
853                         // TODO: error or extend?
854                     }
855                 }
856             }
857         } catch (Exception e) {
858             Log.e(TAG, "Parse error:" + e);
859             e.printStackTrace();
860         }
861         mTotalHeight = y - mDefaultVerticalGap;
862     }
863 
skipToEndOfRow(XmlResourceParser parser)864     private void skipToEndOfRow(XmlResourceParser parser)
865             throws XmlPullParserException, IOException {
866         int event;
867         while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
868             if (event == XmlResourceParser.END_TAG
869                     && parser.getName().equals(TAG_ROW)) {
870                 break;
871             }
872         }
873     }
874 
parseKeyboardAttributes(Resources res, XmlResourceParser parser)875     private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
876         TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
877                 com.android.internal.R.styleable.Keyboard);
878 
879         mDefaultWidth = getDimensionOrFraction(a,
880                 com.android.internal.R.styleable.Keyboard_keyWidth,
881                 mDisplayWidth, mDisplayWidth / 10);
882         mDefaultHeight = getDimensionOrFraction(a,
883                 com.android.internal.R.styleable.Keyboard_keyHeight,
884                 mDisplayHeight, 50);
885         mDefaultHorizontalGap = getDimensionOrFraction(a,
886                 com.android.internal.R.styleable.Keyboard_horizontalGap,
887                 mDisplayWidth, 0);
888         mDefaultVerticalGap = getDimensionOrFraction(a,
889                 com.android.internal.R.styleable.Keyboard_verticalGap,
890                 mDisplayHeight, 0);
891         mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
892         mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
893         a.recycle();
894     }
895 
getDimensionOrFraction(TypedArray a, int index, int base, int defValue)896     static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
897         TypedValue value = a.peekValue(index);
898         if (value == null) return defValue;
899         if (value.type == TypedValue.TYPE_DIMENSION) {
900             return a.getDimensionPixelOffset(index, defValue);
901         } else if (value.type == TypedValue.TYPE_FRACTION) {
902             // Round it to avoid values like 47.9999 from getting truncated
903             return Math.round(a.getFraction(index, base, base, defValue));
904         }
905         return defValue;
906     }
907 }
908