Keyboard.java revision f013e1afd1e68af5e3b868c26a653bbfb39538f8
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.mDisplayWidth, 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.mDisplayWidth, 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        /**
443         * Returns the square of the distance between the center of the key and the given point.
444         * @param x the x-coordinate of the point
445         * @param y the y-coordinate of the point
446         * @return the square of the distance of the point from the center of the key
447         */
448        public int squaredDistanceFrom(int x, int y) {
449            float xDist = Math.abs((this.x + this.x + width) / 2f - x);
450            float yDist = Math.abs((this.y + this.y + height) / 2f - y);
451            return (int) (xDist * xDist + yDist * yDist);
452        }
453
454        /**
455         * Returns the drawable state for the key, based on the current state and type of the key.
456         * @return the drawable state of the key.
457         * @see android.graphics.drawable.StateListDrawable#setState(int[])
458         */
459        public int[] getCurrentDrawableState() {
460            int[] states = KEY_STATE_NORMAL;
461
462            if (on) {
463                if (pressed) {
464                    states = KEY_STATE_PRESSED_ON;
465                } else {
466                    states = KEY_STATE_NORMAL_ON;
467                }
468            } else {
469                if (sticky) {
470                    if (pressed) {
471                        states = KEY_STATE_PRESSED_OFF;
472                    } else {
473                        states = KEY_STATE_NORMAL_OFF;
474                    }
475                } else {
476                    if (pressed) {
477                        states = KEY_STATE_PRESSED;
478                    }
479                }
480            }
481            return states;
482        }
483    }
484
485    /**
486     * Creates a keyboard from the given xml key layout file.
487     * @param context the application or service context
488     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
489     */
490    public Keyboard(Context context, int xmlLayoutResId) {
491        this(context, xmlLayoutResId, 0);
492    }
493
494    /**
495     * Creates a keyboard from the given xml key layout file. Weeds out rows
496     * that have a keyboard mode defined but don't match the specified mode.
497     * @param context the application or service context
498     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
499     * @param modeId keyboard mode identifier
500     */
501    public Keyboard(Context context, int xmlLayoutResId, int modeId) {
502        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
503        final Display display = wm.getDefaultDisplay();
504        mDisplayWidth = display.getWidth();
505        mDisplayHeight = display.getHeight();
506        mDefaultHorizontalGap = 0;
507        mDefaultWidth = mDisplayWidth / 10;
508        mDefaultVerticalGap = 0;
509        mDefaultHeight = mDefaultWidth;
510        mKeys = new ArrayList<Key>();
511        mModifierKeys = new ArrayList<Key>();
512        mKeyboardMode = modeId;
513        loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
514    }
515
516    /**
517     * <p>Creates a blank keyboard from the given resource file and populates it with the specified
518     * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
519     * </p>
520     * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
521     * possible in each row.</p>
522     * @param context the application or service context
523     * @param layoutTemplateResId the layout template file, containing no keys.
524     * @param characters the list of characters to display on the keyboard. One key will be created
525     * for each character.
526     * @param columns the number of columns of keys to display. If this number is greater than the
527     * number of keys that can fit in a row, it will be ignored. If this number is -1, the
528     * keyboard will fit as many keys as possible in each row.
529     */
530    public Keyboard(Context context, int layoutTemplateResId,
531            CharSequence characters, int columns, int horizontalPadding) {
532        this(context, layoutTemplateResId);
533        int x = 0;
534        int y = 0;
535        int column = 0;
536        mTotalWidth = 0;
537
538        Row row = new Row(this);
539        row.defaultHeight = mDefaultHeight;
540        row.defaultWidth = mDefaultWidth;
541        row.defaultHorizontalGap = mDefaultHorizontalGap;
542        row.verticalGap = mDefaultVerticalGap;
543        row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
544
545        final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
546        for (int i = 0; i < characters.length(); i++) {
547            char c = characters.charAt(i);
548            if (column >= maxColumns
549                    || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
550                x = 0;
551                y += mDefaultVerticalGap + mDefaultHeight;
552                column = 0;
553            }
554            final Key key = new Key(row);
555            key.x = x;
556            key.y = y;
557            key.width = mDefaultWidth;
558            key.height = mDefaultHeight;
559            key.gap = mDefaultHorizontalGap;
560            key.label = String.valueOf(c);
561            key.codes = new int[] { c };
562            column++;
563            x += key.width + key.gap;
564            mKeys.add(key);
565            if (x > mTotalWidth) {
566                mTotalWidth = x;
567            }
568        }
569        mTotalHeight = y + mDefaultHeight;
570    }
571
572    public List<Key> getKeys() {
573        return mKeys;
574    }
575
576    public List<Key> getModifierKeys() {
577        return mModifierKeys;
578    }
579
580    protected int getHorizontalGap() {
581        return mDefaultHorizontalGap;
582    }
583
584    protected void setHorizontalGap(int gap) {
585        mDefaultHorizontalGap = gap;
586    }
587
588    protected int getVerticalGap() {
589        return mDefaultVerticalGap;
590    }
591
592    protected void setVerticalGap(int gap) {
593        mDefaultVerticalGap = gap;
594    }
595
596    protected int getKeyHeight() {
597        return mDefaultHeight;
598    }
599
600    protected void setKeyHeight(int height) {
601        mDefaultHeight = height;
602    }
603
604    protected int getKeyWidth() {
605        return mDefaultWidth;
606    }
607
608    protected void setKeyWidth(int width) {
609        mDefaultWidth = width;
610    }
611
612    /**
613     * Returns the total height of the keyboard
614     * @return the total height of the keyboard
615     */
616    public int getHeight() {
617        return mTotalHeight;
618    }
619
620    public int getMinWidth() {
621        return mTotalWidth;
622    }
623
624    public boolean setShifted(boolean shiftState) {
625        if (mShiftKey != null) {
626            mShiftKey.on = shiftState;
627        }
628        if (mShifted != shiftState) {
629            mShifted = shiftState;
630            return true;
631        }
632        return false;
633    }
634
635    public boolean isShifted() {
636        return mShifted;
637    }
638
639    public int getShiftKeyIndex() {
640        return mShiftKeyIndex;
641    }
642
643    protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
644        return new Row(res, this, parser);
645    }
646
647    protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
648            XmlResourceParser parser) {
649        return new Key(res, parent, x, y, parser);
650    }
651
652    private void loadKeyboard(Context context, XmlResourceParser parser) {
653        boolean inKey = false;
654        boolean inRow = false;
655        boolean leftMostKey = false;
656        int row = 0;
657        int x = 0;
658        int y = 0;
659        Key key = null;
660        Row currentRow = null;
661        Resources res = context.getResources();
662        boolean skipRow = false;
663
664        try {
665            int event;
666            while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
667                if (event == XmlResourceParser.START_TAG) {
668                    String tag = parser.getName();
669                    if (TAG_ROW.equals(tag)) {
670                        inRow = true;
671                        x = 0;
672                        currentRow = createRowFromXml(res, parser);
673                        skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
674                        if (skipRow) {
675                            skipToEndOfRow(parser);
676                            inRow = false;
677                        }
678                   } else if (TAG_KEY.equals(tag)) {
679                        inKey = true;
680                        key = createKeyFromXml(res, currentRow, x, y, parser);
681                        mKeys.add(key);
682                        if (key.codes[0] == KEYCODE_SHIFT) {
683                            mShiftKey = key;
684                            mShiftKeyIndex = mKeys.size()-1;
685                            mModifierKeys.add(key);
686                        } else if (key.codes[0] == KEYCODE_ALT) {
687                            mModifierKeys.add(key);
688                        }
689                    } else if (TAG_KEYBOARD.equals(tag)) {
690                        parseKeyboardAttributes(res, parser);
691                    }
692                } else if (event == XmlResourceParser.END_TAG) {
693                    if (inKey) {
694                        inKey = false;
695                        x += key.gap + key.width;
696                        if (x > mTotalWidth) {
697                            mTotalWidth = x;
698                        }
699                    } else if (inRow) {
700                        inRow = false;
701                        y += currentRow.verticalGap;
702                        y += currentRow.defaultHeight;
703                        row++;
704                    } else {
705                        // TODO: error or extend?
706                    }
707                }
708            }
709        } catch (Exception e) {
710            Log.e(TAG, "Parse error:" + e);
711            e.printStackTrace();
712        }
713        mTotalHeight = y - mDefaultVerticalGap;
714    }
715
716    private void skipToEndOfRow(XmlResourceParser parser)
717            throws XmlPullParserException, IOException {
718        int event;
719        while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
720            if (event == XmlResourceParser.END_TAG
721                    && parser.getName().equals(TAG_ROW)) {
722                break;
723            }
724        }
725    }
726
727    private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
728        TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
729                com.android.internal.R.styleable.Keyboard);
730
731        mDefaultWidth = getDimensionOrFraction(a,
732                com.android.internal.R.styleable.Keyboard_keyWidth,
733                mDisplayWidth, mDisplayWidth / 10);
734        mDefaultHeight = getDimensionOrFraction(a,
735                com.android.internal.R.styleable.Keyboard_keyHeight,
736                mDisplayHeight, 50);
737        mDefaultHorizontalGap = getDimensionOrFraction(a,
738                com.android.internal.R.styleable.Keyboard_horizontalGap,
739                mDisplayWidth, 0);
740        mDefaultVerticalGap = getDimensionOrFraction(a,
741                com.android.internal.R.styleable.Keyboard_verticalGap,
742                mDisplayHeight, 0);
743        a.recycle();
744    }
745
746    static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
747        TypedValue value = a.peekValue(index);
748        if (value == null) return defValue;
749        if (value.type == TypedValue.TYPE_DIMENSION) {
750            return a.getDimensionPixelOffset(index, defValue);
751        } else if (value.type == TypedValue.TYPE_FRACTION) {
752            return (int) a.getFraction(index, base, base, defValue);
753        }
754        return defValue;
755    }
756}
757