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