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