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