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