Keyboard.java revision d24b8183b93e781080b2c16c487e60d51c12da31
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
17package android.inputmethodservice;
18
19import org.xmlpull.v1.XmlPullParserException;
20
21import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.content.res.XmlResourceParser;
25import android.graphics.drawable.Drawable;
26import android.text.TextUtils;
27import android.util.Log;
28import android.util.TypedValue;
29import android.util.Xml;
30import android.view.Display;
31import android.view.WindowManager;
32
33import java.io.IOException;
34import java.util.ArrayList;
35import java.util.List;
36import 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 */
61public 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 mShiftKey;
102
103    /** Key index for the shift key, if present */
104    private int mShiftKeyIndex = -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    /**
137     * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
138     * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
139     * defines.
140     * @attr ref android.R.styleable#Keyboard_keyWidth
141     * @attr ref android.R.styleable#Keyboard_keyHeight
142     * @attr ref android.R.styleable#Keyboard_horizontalGap
143     * @attr ref android.R.styleable#Keyboard_verticalGap
144     * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags
145     * @attr ref android.R.styleable#Keyboard_Row_keyboardMode
146     */
147    public static class Row {
148        /** Default width of a key in this row. */
149        public int defaultWidth;
150        /** Default height of a key in this row. */
151        public int defaultHeight;
152        /** Default horizontal gap between keys in this row. */
153        public int defaultHorizontalGap;
154        /** Vertical gap following this row. */
155        public int verticalGap;
156        /**
157         * Edge flags for this row of keys. Possible values that can be assigned are
158         * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
159         */
160        public int rowEdgeFlags;
161
162        /** The keyboard mode for this row */
163        public int mode;
164
165        private Keyboard parent;
166
167        public Row(Keyboard parent) {
168            this.parent = parent;
169        }
170
171        public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
172            this.parent = parent;
173            TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
174                    com.android.internal.R.styleable.Keyboard);
175            defaultWidth = getDimensionOrFraction(a,
176                    com.android.internal.R.styleable.Keyboard_keyWidth,
177                    parent.mDisplayWidth, parent.mDefaultWidth);
178            defaultHeight = getDimensionOrFraction(a,
179                    com.android.internal.R.styleable.Keyboard_keyHeight,
180                    parent.mDisplayHeight, parent.mDefaultHeight);
181            defaultHorizontalGap = getDimensionOrFraction(a,
182                    com.android.internal.R.styleable.Keyboard_horizontalGap,
183                    parent.mDisplayWidth, parent.mDefaultHorizontalGap);
184            verticalGap = getDimensionOrFraction(a,
185                    com.android.internal.R.styleable.Keyboard_verticalGap,
186                    parent.mDisplayHeight, parent.mDefaultVerticalGap);
187            a.recycle();
188            a = res.obtainAttributes(Xml.asAttributeSet(parser),
189                    com.android.internal.R.styleable.Keyboard_Row);
190            rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0);
191            mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode,
192                    0);
193        }
194    }
195
196    /**
197     * Class for describing the position and characteristics of a single key in the keyboard.
198     *
199     * @attr ref android.R.styleable#Keyboard_keyWidth
200     * @attr ref android.R.styleable#Keyboard_keyHeight
201     * @attr ref android.R.styleable#Keyboard_horizontalGap
202     * @attr ref android.R.styleable#Keyboard_Key_codes
203     * @attr ref android.R.styleable#Keyboard_Key_keyIcon
204     * @attr ref android.R.styleable#Keyboard_Key_keyLabel
205     * @attr ref android.R.styleable#Keyboard_Key_iconPreview
206     * @attr ref android.R.styleable#Keyboard_Key_isSticky
207     * @attr ref android.R.styleable#Keyboard_Key_isRepeatable
208     * @attr ref android.R.styleable#Keyboard_Key_isModifier
209     * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard
210     * @attr ref android.R.styleable#Keyboard_Key_popupCharacters
211     * @attr ref android.R.styleable#Keyboard_Key_keyOutputText
212     * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags
213     */
214    public static class Key {
215        /**
216         * All the key codes (unicode or custom code) that this key could generate, zero'th
217         * being the most important.
218         */
219        public int[] codes;
220
221        /** Label to display */
222        public CharSequence label;
223
224        /** Icon to display instead of a label. Icon takes precedence over a label */
225        public Drawable icon;
226        /** Preview version of the icon, for the preview popup */
227        public Drawable iconPreview;
228        /** Width of the key, not including the gap */
229        public int width;
230        /** Height of the key, not including the gap */
231        public int height;
232        /** The horizontal gap before this key */
233        public int gap;
234        /** Whether this key is sticky, i.e., a toggle key */
235        public boolean sticky;
236        /** X coordinate of the key in the keyboard layout */
237        public int x;
238        /** Y coordinate of the key in the keyboard layout */
239        public int y;
240        /** The current pressed state of this key */
241        public boolean pressed;
242        /** If this is a sticky key, is it on? */
243        public boolean on;
244        /** Text to output when pressed. This can be multiple characters, like ".com" */
245        public CharSequence text;
246        /** Popup characters */
247        public CharSequence popupCharacters;
248
249        /**
250         * Flags that specify the anchoring to edges of the keyboard for detecting touch events
251         * that are just out of the boundary of the key. This is a bit mask of
252         * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
253         * {@link Keyboard#EDGE_BOTTOM}.
254         */
255        public int edgeFlags;
256        /** Whether this is a modifier key, such as Shift or Alt */
257        public boolean modifier;
258        /** The keyboard that this key belongs to */
259        private Keyboard keyboard;
260        /**
261         * If this key pops up a mini keyboard, this is the resource id for the XML layout for that
262         * keyboard.
263         */
264        public int popupResId;
265        /** Whether this key repeats itself when held down */
266        public boolean repeatable;
267
268
269        private final static int[] KEY_STATE_NORMAL_ON = {
270            android.R.attr.state_checkable,
271            android.R.attr.state_checked
272        };
273
274        private final static int[] KEY_STATE_PRESSED_ON = {
275            android.R.attr.state_pressed,
276            android.R.attr.state_checkable,
277            android.R.attr.state_checked
278        };
279
280        private final static int[] KEY_STATE_NORMAL_OFF = {
281            android.R.attr.state_checkable
282        };
283
284        private final static int[] KEY_STATE_PRESSED_OFF = {
285            android.R.attr.state_pressed,
286            android.R.attr.state_checkable
287        };
288
289        private final static int[] KEY_STATE_NORMAL = {
290        };
291
292        private final static int[] KEY_STATE_PRESSED = {
293            android.R.attr.state_pressed
294        };
295
296        /** Create an empty key with no attributes. */
297        public Key(Row parent) {
298            keyboard = parent.parent;
299        }
300
301        /** Create a key with the given top-left coordinate and extract its attributes from
302         * the XML parser.
303         * @param res resources associated with the caller's context
304         * @param parent the row that this key belongs to. The row must already be attached to
305         * a {@link Keyboard}.
306         * @param x the x coordinate of the top-left
307         * @param y the y coordinate of the top-left
308         * @param parser the XML parser containing the attributes for this key
309         */
310        public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
311            this(parent);
312
313            this.x = x;
314            this.y = y;
315
316            TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
317                    com.android.internal.R.styleable.Keyboard);
318
319            width = getDimensionOrFraction(a,
320                    com.android.internal.R.styleable.Keyboard_keyWidth,
321                    keyboard.mDisplayWidth, parent.defaultWidth);
322            height = getDimensionOrFraction(a,
323                    com.android.internal.R.styleable.Keyboard_keyHeight,
324                    keyboard.mDisplayHeight, parent.defaultHeight);
325            gap = getDimensionOrFraction(a,
326                    com.android.internal.R.styleable.Keyboard_horizontalGap,
327                    keyboard.mDisplayWidth, parent.defaultHorizontalGap);
328            a.recycle();
329            a = res.obtainAttributes(Xml.asAttributeSet(parser),
330                    com.android.internal.R.styleable.Keyboard_Key);
331            this.x += gap;
332            TypedValue codesValue = new TypedValue();
333            a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes,
334                    codesValue);
335            if (codesValue.type == TypedValue.TYPE_INT_DEC
336                    || codesValue.type == TypedValue.TYPE_INT_HEX) {
337                codes = new int[] { codesValue.data };
338            } else if (codesValue.type == TypedValue.TYPE_STRING) {
339                codes = parseCSV(codesValue.string.toString());
340            }
341
342            iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview);
343            if (iconPreview != null) {
344                iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
345                        iconPreview.getIntrinsicHeight());
346            }
347            popupCharacters = a.getText(
348                    com.android.internal.R.styleable.Keyboard_Key_popupCharacters);
349            popupResId = a.getResourceId(
350                    com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0);
351            repeatable = a.getBoolean(
352                    com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false);
353            modifier = a.getBoolean(
354                    com.android.internal.R.styleable.Keyboard_Key_isModifier, false);
355            sticky = a.getBoolean(
356                    com.android.internal.R.styleable.Keyboard_Key_isSticky, false);
357            edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0);
358            edgeFlags |= parent.rowEdgeFlags;
359
360            icon = a.getDrawable(
361                    com.android.internal.R.styleable.Keyboard_Key_keyIcon);
362            if (icon != null) {
363                icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
364            }
365            label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel);
366            text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText);
367
368            if (codes == null && !TextUtils.isEmpty(label)) {
369                codes = new int[] { label.charAt(0) };
370            }
371            a.recycle();
372        }
373
374        /**
375         * Informs the key that it has been pressed, in case it needs to change its appearance or
376         * state.
377         * @see #onReleased(boolean)
378         */
379        public void onPressed() {
380            pressed = !pressed;
381        }
382
383        /**
384         * Changes the pressed state of the key. If it is a sticky key, it will also change the
385         * toggled state of the key if the finger was release inside.
386         * @param inside whether the finger was released inside the key
387         * @see #onPressed()
388         */
389        public void onReleased(boolean inside) {
390            pressed = !pressed;
391            if (sticky) {
392                on = !on;
393            }
394        }
395
396        int[] parseCSV(String value) {
397            int count = 0;
398            int lastIndex = 0;
399            if (value.length() > 0) {
400                count++;
401                while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
402                    count++;
403                }
404            }
405            int[] values = new int[count];
406            count = 0;
407            StringTokenizer st = new StringTokenizer(value, ",");
408            while (st.hasMoreTokens()) {
409                try {
410                    values[count++] = Integer.parseInt(st.nextToken());
411                } catch (NumberFormatException nfe) {
412                    Log.e(TAG, "Error parsing keycodes " + value);
413                }
414            }
415            return values;
416        }
417
418        /**
419         * Detects if a point falls inside this key.
420         * @param x the x-coordinate of the point
421         * @param y the y-coordinate of the point
422         * @return whether or not the point falls inside the key. If the key is attached to an edge,
423         * it will assume that all points between the key and the edge are considered to be inside
424         * the key.
425         */
426        public boolean isInside(int x, int y) {
427            boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
428            boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
429            boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
430            boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
431            if ((x >= this.x || (leftEdge && x <= this.x + this.width))
432                    && (x < this.x + this.width || (rightEdge && x >= this.x))
433                    && (y >= this.y || (topEdge && y <= this.y + this.height))
434                    && (y < this.y + this.height || (bottomEdge && y >= this.y))) {
435                return true;
436            } else {
437                return false;
438            }
439        }
440
441        /**
442         * Returns the square of the distance between the center of the key and the given point.
443         * @param x the x-coordinate of the point
444         * @param y the y-coordinate of the point
445         * @return the square of the distance of the point from the center of the key
446         */
447        public int squaredDistanceFrom(int x, int y) {
448            int xDist = this.x + width / 2 - x;
449            int yDist = this.y + height / 2 - y;
450            return xDist * xDist + yDist * yDist;
451        }
452
453        /**
454         * Returns the drawable state for the key, based on the current state and type of the key.
455         * @return the drawable state of the key.
456         * @see android.graphics.drawable.StateListDrawable#setState(int[])
457         */
458        public int[] getCurrentDrawableState() {
459            int[] states = KEY_STATE_NORMAL;
460
461            if (on) {
462                if (pressed) {
463                    states = KEY_STATE_PRESSED_ON;
464                } else {
465                    states = KEY_STATE_NORMAL_ON;
466                }
467            } else {
468                if (sticky) {
469                    if (pressed) {
470                        states = KEY_STATE_PRESSED_OFF;
471                    } else {
472                        states = KEY_STATE_NORMAL_OFF;
473                    }
474                } else {
475                    if (pressed) {
476                        states = KEY_STATE_PRESSED;
477                    }
478                }
479            }
480            return states;
481        }
482    }
483
484    /**
485     * Creates a keyboard from the given xml key layout file.
486     * @param context the application or service context
487     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
488     */
489    public Keyboard(Context context, int xmlLayoutResId) {
490        this(context, xmlLayoutResId, 0);
491    }
492
493    /**
494     * Creates a keyboard from the given xml key layout file. Weeds out rows
495     * that have a keyboard mode defined but don't match the specified mode.
496     * @param context the application or service context
497     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
498     * @param modeId keyboard mode identifier
499     */
500    public Keyboard(Context context, int xmlLayoutResId, int modeId) {
501        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
502        final Display display = wm.getDefaultDisplay();
503        mDisplayWidth = display.getWidth();
504        mDisplayHeight = display.getHeight();
505        mDefaultHorizontalGap = 0;
506        mDefaultWidth = mDisplayWidth / 10;
507        mDefaultVerticalGap = 0;
508        mDefaultHeight = mDefaultWidth;
509        mKeys = new ArrayList<Key>();
510        mModifierKeys = new ArrayList<Key>();
511        mKeyboardMode = modeId;
512        loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
513    }
514
515    /**
516     * <p>Creates a blank keyboard from the given resource file and populates it with the specified
517     * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
518     * </p>
519     * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
520     * possible in each row.</p>
521     * @param context the application or service context
522     * @param layoutTemplateResId the layout template file, containing no keys.
523     * @param characters the list of characters to display on the keyboard. One key will be created
524     * for each character.
525     * @param columns the number of columns of keys to display. If this number is greater than the
526     * number of keys that can fit in a row, it will be ignored. If this number is -1, the
527     * keyboard will fit as many keys as possible in each row.
528     */
529    public Keyboard(Context context, int layoutTemplateResId,
530            CharSequence characters, int columns, int horizontalPadding) {
531        this(context, layoutTemplateResId);
532        int x = 0;
533        int y = 0;
534        int column = 0;
535        mTotalWidth = 0;
536
537        Row row = new Row(this);
538        row.defaultHeight = mDefaultHeight;
539        row.defaultWidth = mDefaultWidth;
540        row.defaultHorizontalGap = mDefaultHorizontalGap;
541        row.verticalGap = mDefaultVerticalGap;
542        row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
543        final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
544        for (int i = 0; i < characters.length(); i++) {
545            char c = characters.charAt(i);
546            if (column >= maxColumns
547                    || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
548                x = 0;
549                y += mDefaultVerticalGap + mDefaultHeight;
550                column = 0;
551            }
552            final Key key = new Key(row);
553            key.x = x;
554            key.y = y;
555            key.width = mDefaultWidth;
556            key.height = mDefaultHeight;
557            key.gap = mDefaultHorizontalGap;
558            key.label = String.valueOf(c);
559            key.codes = new int[] { c };
560            column++;
561            x += key.width + key.gap;
562            mKeys.add(key);
563            if (x > mTotalWidth) {
564                mTotalWidth = x;
565            }
566        }
567        mTotalHeight = y + mDefaultHeight;
568    }
569
570    public List<Key> getKeys() {
571        return mKeys;
572    }
573
574    public List<Key> getModifierKeys() {
575        return mModifierKeys;
576    }
577
578    protected int getHorizontalGap() {
579        return mDefaultHorizontalGap;
580    }
581
582    protected void setHorizontalGap(int gap) {
583        mDefaultHorizontalGap = gap;
584    }
585
586    protected int getVerticalGap() {
587        return mDefaultVerticalGap;
588    }
589
590    protected void setVerticalGap(int gap) {
591        mDefaultVerticalGap = gap;
592    }
593
594    protected int getKeyHeight() {
595        return mDefaultHeight;
596    }
597
598    protected void setKeyHeight(int height) {
599        mDefaultHeight = height;
600    }
601
602    protected int getKeyWidth() {
603        return mDefaultWidth;
604    }
605
606    protected void setKeyWidth(int width) {
607        mDefaultWidth = width;
608    }
609
610    /**
611     * Returns the total height of the keyboard
612     * @return the total height of the keyboard
613     */
614    public int getHeight() {
615        return mTotalHeight;
616    }
617
618    public int getMinWidth() {
619        return mTotalWidth;
620    }
621
622    public boolean setShifted(boolean shiftState) {
623        if (mShiftKey != null) {
624            mShiftKey.on = shiftState;
625        }
626        if (mShifted != shiftState) {
627            mShifted = shiftState;
628            return true;
629        }
630        return false;
631    }
632
633    public boolean isShifted() {
634        return mShifted;
635    }
636
637    public int getShiftKeyIndex() {
638        return mShiftKeyIndex;
639    }
640
641    protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
642        return new Row(res, this, parser);
643    }
644
645    protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
646            XmlResourceParser parser) {
647        return new Key(res, parent, x, y, parser);
648    }
649
650    private void loadKeyboard(Context context, XmlResourceParser parser) {
651        boolean inKey = false;
652        boolean inRow = false;
653        boolean leftMostKey = false;
654        int row = 0;
655        int x = 0;
656        int y = 0;
657        Key key = null;
658        Row currentRow = null;
659        Resources res = context.getResources();
660        boolean skipRow = false;
661
662        try {
663            int event;
664            while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
665                if (event == XmlResourceParser.START_TAG) {
666                    String tag = parser.getName();
667                    if (TAG_ROW.equals(tag)) {
668                        inRow = true;
669                        x = 0;
670                        currentRow = createRowFromXml(res, parser);
671                        skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
672                        if (skipRow) {
673                            skipToEndOfRow(parser);
674                            inRow = false;
675                        }
676                   } else if (TAG_KEY.equals(tag)) {
677                        inKey = true;
678                        key = createKeyFromXml(res, currentRow, x, y, parser);
679                        mKeys.add(key);
680                        if (key.codes[0] == KEYCODE_SHIFT) {
681                            mShiftKey = key;
682                            mShiftKeyIndex = mKeys.size()-1;
683                            mModifierKeys.add(key);
684                        } else if (key.codes[0] == KEYCODE_ALT) {
685                            mModifierKeys.add(key);
686                        }
687                    } else if (TAG_KEYBOARD.equals(tag)) {
688                        parseKeyboardAttributes(res, parser);
689                    }
690                } else if (event == XmlResourceParser.END_TAG) {
691                    if (inKey) {
692                        inKey = false;
693                        x += key.gap + key.width;
694                        if (x > mTotalWidth) {
695                            mTotalWidth = x;
696                        }
697                    } else if (inRow) {
698                        inRow = false;
699                        y += currentRow.verticalGap;
700                        y += currentRow.defaultHeight;
701                        row++;
702                    } else {
703                        // TODO: error or extend?
704                    }
705                }
706            }
707        } catch (Exception e) {
708            Log.e(TAG, "Parse error:" + e);
709            e.printStackTrace();
710        }
711        mTotalHeight = y - mDefaultVerticalGap;
712    }
713
714    private void skipToEndOfRow(XmlResourceParser parser)
715            throws XmlPullParserException, IOException {
716        int event;
717        while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
718            if (event == XmlResourceParser.END_TAG
719                    && parser.getName().equals(TAG_ROW)) {
720                break;
721            }
722        }
723    }
724
725    private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
726        TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
727                com.android.internal.R.styleable.Keyboard);
728
729        mDefaultWidth = getDimensionOrFraction(a,
730                com.android.internal.R.styleable.Keyboard_keyWidth,
731                mDisplayWidth, mDisplayWidth / 10);
732        mDefaultHeight = getDimensionOrFraction(a,
733                com.android.internal.R.styleable.Keyboard_keyHeight,
734                mDisplayHeight, 50);
735        mDefaultHorizontalGap = getDimensionOrFraction(a,
736                com.android.internal.R.styleable.Keyboard_horizontalGap,
737                mDisplayWidth, 0);
738        mDefaultVerticalGap = getDimensionOrFraction(a,
739                com.android.internal.R.styleable.Keyboard_verticalGap,
740                mDisplayHeight, 0);
741        a.recycle();
742    }
743
744    static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
745        TypedValue value = a.peekValue(index);
746        if (value == null) return defValue;
747        if (value.type == TypedValue.TYPE_DIMENSION) {
748            return a.getDimensionPixelOffset(index, defValue);
749        } else if (value.type == TypedValue.TYPE_FRACTION) {
750            // Round it to avoid values like 47.9999 from getting truncated
751            return Math.round(a.getFraction(index, base, base, defValue));
752        }
753        return defValue;
754    }
755}
756