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