1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import static com.android.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED;
20import static com.android.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
21import static com.android.inputmethod.latin.common.Constants.CODE_SHIFT;
22import static com.android.inputmethod.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL;
23import static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
24
25import android.content.res.TypedArray;
26import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29import android.text.TextUtils;
30
31import com.android.inputmethod.keyboard.internal.KeyDrawParams;
32import com.android.inputmethod.keyboard.internal.KeySpecParser;
33import com.android.inputmethod.keyboard.internal.KeyStyle;
34import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
35import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
36import com.android.inputmethod.keyboard.internal.KeyboardParams;
37import com.android.inputmethod.keyboard.internal.KeyboardRow;
38import com.android.inputmethod.keyboard.internal.MoreKeySpec;
39import com.android.inputmethod.latin.R;
40import com.android.inputmethod.latin.common.Constants;
41import com.android.inputmethod.latin.common.StringUtils;
42
43import java.util.Arrays;
44import java.util.Locale;
45
46import javax.annotation.Nonnull;
47import javax.annotation.Nullable;
48
49/**
50 * Class for describing the position and characteristics of a single key in the keyboard.
51 */
52public class Key implements Comparable<Key> {
53    /**
54     * The key code (unicode or custom code) that this key generates.
55     */
56    private final int mCode;
57
58    /** Label to display */
59    private final String mLabel;
60    /** Hint label to display on the key in conjunction with the label */
61    private final String mHintLabel;
62    /** Flags of the label */
63    private final int mLabelFlags;
64    private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02;
65    private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04;
66    private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08;
67    // Font typeface specification.
68    private static final int LABEL_FLAGS_FONT_MASK = 0x30;
69    private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
70    private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
71    private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30;
72    // Start of key text ratio enum values
73    private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
74    private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
75    private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
76    private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
77    private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
78    // End of key text ratio mask enum values
79    private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
80    private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
81    private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
82    // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on
83    // and autoYScale bit is off, the key label may be shrunk only for X-direction.
84    // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled.
85    private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
86    private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000;
87    private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE
88            | LABEL_FLAGS_AUTO_Y_SCALE;
89    private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000;
90    private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
91    private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
92    private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
93    private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000;
94    private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
95    private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
96
97    /** Icon to display instead of a label. Icon takes precedence over a label */
98    private final int mIconId;
99
100    /** Width of the key, excluding the gap */
101    private final int mWidth;
102    /** Height of the key, excluding the gap */
103    private final int mHeight;
104    /**
105     * The combined width in pixels of the horizontal gaps belonging to this key, both to the left
106     * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key.
107     */
108    private final int mHorizontalGap;
109    /**
110     * The combined height in pixels of the vertical gaps belonging to this key, both above and
111     * below. I.e., mHeight + mVerticalGap = total height belonging to the key.
112     */
113    private final int mVerticalGap;
114    /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
115    private final int mX;
116    /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
117    private final int mY;
118    /** Hit bounding box of the key */
119    @Nonnull
120    private final Rect mHitBox = new Rect();
121
122    /** More keys. It is guaranteed that this is null or an array of one or more elements */
123    @Nullable
124    private final MoreKeySpec[] mMoreKeys;
125    /** More keys column number and flags */
126    private final int mMoreKeysColumnAndFlags;
127    private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff;
128    // If this flag is specified, more keys keyboard should have the specified number of columns.
129    // Otherwise more keys keyboard should have less than or equal to the specified maximum number
130    // of columns.
131    private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100;
132    // If this flag is specified, the order of more keys is determined by the order in the more
133    // keys' specification. Otherwise the order of more keys is automatically determined.
134    private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200;
135    private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0;
136    private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER =
137            MORE_KEYS_FLAGS_FIXED_COLUMN;
138    private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER =
139            (MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER);
140    private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
141    private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000;
142    private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000;
143    // TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively.
144    private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
145    private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
146    private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
147    private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!";
148    private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
149
150    /** Background type that represents different key background visual than normal one. */
151    private final int mBackgroundType;
152    public static final int BACKGROUND_TYPE_EMPTY = 0;
153    public static final int BACKGROUND_TYPE_NORMAL = 1;
154    public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
155    public static final int BACKGROUND_TYPE_STICKY_OFF = 3;
156    public static final int BACKGROUND_TYPE_STICKY_ON = 4;
157    public static final int BACKGROUND_TYPE_ACTION = 5;
158    public static final int BACKGROUND_TYPE_SPACEBAR = 6;
159
160    private final int mActionFlags;
161    private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
162    private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
163    private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
164    private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
165
166    @Nullable
167    private final KeyVisualAttributes mKeyVisualAttributes;
168    @Nullable
169    private final OptionalAttributes mOptionalAttributes;
170
171    private static final class OptionalAttributes {
172        /** Text to output when pressed. This can be multiple characters, like ".com" */
173        public final String mOutputText;
174        public final int mAltCode;
175        /** Icon for disabled state */
176        public final int mDisabledIconId;
177        /** The visual insets */
178        public final int mVisualInsetsLeft;
179        public final int mVisualInsetsRight;
180
181        private OptionalAttributes(final String outputText, final int altCode,
182                final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) {
183            mOutputText = outputText;
184            mAltCode = altCode;
185            mDisabledIconId = disabledIconId;
186            mVisualInsetsLeft = visualInsetsLeft;
187            mVisualInsetsRight = visualInsetsRight;
188        }
189
190        @Nullable
191        public static OptionalAttributes newInstance(final String outputText, final int altCode,
192                final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) {
193            if (outputText == null && altCode == CODE_UNSPECIFIED
194                    && disabledIconId == ICON_UNDEFINED && visualInsetsLeft == 0
195                    && visualInsetsRight == 0) {
196                return null;
197            }
198            return new OptionalAttributes(outputText, altCode, disabledIconId, visualInsetsLeft,
199                    visualInsetsRight);
200        }
201    }
202
203    private final int mHashCode;
204
205    /** The current pressed state of this key */
206    private boolean mPressed;
207    /** Key is enabled and responds on press */
208    private boolean mEnabled = true;
209
210    /**
211     * Constructor for a key on <code>MoreKeyKeyboard</code>, on <code>MoreSuggestions</code>,
212     * and in a <GridRows/>.
213     */
214    public Key(@Nullable final String label, final int iconId, final int code,
215            @Nullable final String outputText, @Nullable final String hintLabel,
216            final int labelFlags, final int backgroundType, final int x, final int y,
217            final int width, final int height, final int horizontalGap, final int verticalGap) {
218        mWidth = width - horizontalGap;
219        mHeight = height - verticalGap;
220        mHorizontalGap = horizontalGap;
221        mVerticalGap = verticalGap;
222        mHintLabel = hintLabel;
223        mLabelFlags = labelFlags;
224        mBackgroundType = backgroundType;
225        // TODO: Pass keyActionFlags as an argument.
226        mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW;
227        mMoreKeys = null;
228        mMoreKeysColumnAndFlags = 0;
229        mLabel = label;
230        mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED,
231                ICON_UNDEFINED, 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */);
232        mCode = code;
233        mEnabled = (code != CODE_UNSPECIFIED);
234        mIconId = iconId;
235        // Horizontal gap is divided equally to both sides of the key.
236        mX = x + mHorizontalGap / 2;
237        mY = y;
238        mHitBox.set(x, y, x + width + 1, y + height);
239        mKeyVisualAttributes = null;
240
241        mHashCode = computeHashCode(this);
242    }
243
244    /**
245     * Create a key with the given top-left coordinate and extract its attributes from a key
246     * specification string, Key attribute array, key style, and etc.
247     *
248     * @param keySpec the key specification.
249     * @param keyAttr the Key XML attributes array.
250     * @param style the {@link KeyStyle} of this key.
251     * @param params the keyboard building parameters.
252     * @param row the row that this key belongs to. row's x-coordinate will be the right edge of
253     *        this key.
254     */
255    public Key(@Nullable final String keySpec, @Nonnull final TypedArray keyAttr,
256            @Nonnull final KeyStyle style, @Nonnull final KeyboardParams params,
257            @Nonnull final KeyboardRow row) {
258        mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
259        mVerticalGap = params.mVerticalGap;
260
261        final float horizontalGapFloat = mHorizontalGap;
262        final int rowHeight = row.getRowHeight();
263        mHeight = rowHeight - mVerticalGap;
264
265        final float keyXPos = row.getKeyX(keyAttr);
266        final float keyWidth = row.getKeyWidth(keyAttr, keyXPos);
267        final int keyYPos = row.getKeyY();
268
269        // Horizontal gap is divided equally to both sides of the key.
270        mX = Math.round(keyXPos + horizontalGapFloat / 2);
271        mY = keyYPos;
272        mWidth = Math.round(keyWidth - horizontalGapFloat);
273        mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
274                keyYPos + rowHeight);
275        // Update row to have current x coordinate.
276        row.setXPos(keyXPos + keyWidth);
277
278        mBackgroundType = style.getInt(keyAttr,
279                R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
280
281        final int baseWidth = params.mBaseWidth;
282        final int visualInsetsLeft = Math.round(keyAttr.getFraction(
283                R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0));
284        final int visualInsetsRight = Math.round(keyAttr.getFraction(
285                R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0));
286
287        mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
288                | row.getDefaultKeyLabelFlags();
289        final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId);
290        final Locale localeForUpcasing = params.mId.getLocale();
291        int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
292        String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
293
294        // Get maximum column order number and set a relevant mode value.
295        int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER
296                | style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn,
297                        params.mMaxMoreKeysKeyboardColumn);
298        int value;
299        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
300            // Override with fixed column order number and set a relevant mode value.
301            moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER
302                    | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
303        }
304        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
305            // Override with fixed column order number and set a relevant mode value.
306            moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER
307                    | (value & MORE_KEYS_COLUMN_NUMBER_MASK);
308        }
309        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
310            moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS;
311        }
312        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
313            moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
314        }
315        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
316            moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
317        }
318        mMoreKeysColumnAndFlags = moreKeysColumnAndFlags;
319
320        final String[] additionalMoreKeys;
321        if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
322            additionalMoreKeys = null;
323        } else {
324            additionalMoreKeys = style.getStringArray(keyAttr,
325                    R.styleable.Keyboard_Key_additionalMoreKeys);
326        }
327        moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
328        if (moreKeys != null) {
329            actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
330            mMoreKeys = new MoreKeySpec[moreKeys.length];
331            for (int i = 0; i < moreKeys.length; i++) {
332                mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing);
333            }
334        } else {
335            mMoreKeys = null;
336        }
337        mActionFlags = actionFlags;
338
339        mIconId = KeySpecParser.getIconId(keySpec);
340        final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
341                R.styleable.Keyboard_Key_keyIconDisabled));
342
343        final int code = KeySpecParser.getCode(keySpec);
344        if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
345            mLabel = params.mId.mCustomActionLabel;
346        } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
347            // This is a workaround to have a key that has a supplementary code point in its label.
348            // Because we can put a string in resource neither as a XML entity of a supplementary
349            // code point nor as a surrogate pair.
350            mLabel = new StringBuilder().appendCodePoint(code).toString();
351        } else {
352            final String label = KeySpecParser.getLabel(keySpec);
353            mLabel = needsToUpcase
354                    ? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing)
355                    : label;
356        }
357        if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
358            mHintLabel = null;
359        } else {
360            final String hintLabel = style.getString(
361                    keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
362            mHintLabel = needsToUpcase
363                    ? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing)
364                    : hintLabel;
365        }
366        String outputText = KeySpecParser.getOutputText(keySpec);
367        if (needsToUpcase) {
368            outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing);
369        }
370        // Choose the first letter of the label as primary code if not specified.
371        if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
372                && !TextUtils.isEmpty(mLabel)) {
373            if (StringUtils.codePointCount(mLabel) == 1) {
374                // Use the first letter of the hint label if shiftedLetterActivated flag is
375                // specified.
376                if (hasShiftedLetterHint() && isShiftedLetterActivated()) {
377                    mCode = mHintLabel.codePointAt(0);
378                } else {
379                    mCode = mLabel.codePointAt(0);
380                }
381            } else {
382                // In some locale and case, the character might be represented by multiple code
383                // points, such as upper case Eszett of German alphabet.
384                outputText = mLabel;
385                mCode = CODE_OUTPUT_TEXT;
386            }
387        } else if (code == CODE_UNSPECIFIED && outputText != null) {
388            if (StringUtils.codePointCount(outputText) == 1) {
389                mCode = outputText.codePointAt(0);
390                outputText = null;
391            } else {
392                mCode = CODE_OUTPUT_TEXT;
393            }
394        } else {
395            mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing)
396                    : code;
397        }
398        final int altCodeInAttr = KeySpecParser.parseCode(
399                style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED);
400        final int altCode = needsToUpcase
401                ? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing)
402                : altCodeInAttr;
403        mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
404                disabledIconId, visualInsetsLeft, visualInsetsRight);
405        mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
406        mHashCode = computeHashCode(this);
407    }
408
409    /**
410     * Copy constructor for DynamicGridKeyboard.GridKey.
411     *
412     * @param key the original key.
413     */
414    protected Key(@Nonnull final Key key) {
415        this(key, key.mMoreKeys);
416    }
417
418    private Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys) {
419        // Final attributes.
420        mCode = key.mCode;
421        mLabel = key.mLabel;
422        mHintLabel = key.mHintLabel;
423        mLabelFlags = key.mLabelFlags;
424        mIconId = key.mIconId;
425        mWidth = key.mWidth;
426        mHeight = key.mHeight;
427        mHorizontalGap = key.mHorizontalGap;
428        mVerticalGap = key.mVerticalGap;
429        mX = key.mX;
430        mY = key.mY;
431        mHitBox.set(key.mHitBox);
432        mMoreKeys = moreKeys;
433        mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
434        mBackgroundType = key.mBackgroundType;
435        mActionFlags = key.mActionFlags;
436        mKeyVisualAttributes = key.mKeyVisualAttributes;
437        mOptionalAttributes = key.mOptionalAttributes;
438        mHashCode = key.mHashCode;
439        // Key state.
440        mPressed = key.mPressed;
441        mEnabled = key.mEnabled;
442    }
443
444    @Nonnull
445    public static Key removeRedundantMoreKeys(@Nonnull final Key key,
446            @Nonnull final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) {
447        final MoreKeySpec[] moreKeys = key.getMoreKeys();
448        final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys(
449                moreKeys, lettersOnBaseLayout);
450        return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys);
451    }
452
453    private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) {
454        if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
455        switch (keyboardElementId) {
456        case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
457        case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
458        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
459        case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
460            return true;
461        default:
462            return false;
463        }
464    }
465
466    private static int computeHashCode(final Key key) {
467        return Arrays.hashCode(new Object[] {
468                key.mX,
469                key.mY,
470                key.mWidth,
471                key.mHeight,
472                key.mCode,
473                key.mLabel,
474                key.mHintLabel,
475                key.mIconId,
476                key.mBackgroundType,
477                Arrays.hashCode(key.mMoreKeys),
478                key.getOutputText(),
479                key.mActionFlags,
480                key.mLabelFlags,
481                // Key can be distinguishable without the following members.
482                // key.mOptionalAttributes.mAltCode,
483                // key.mOptionalAttributes.mDisabledIconId,
484                // key.mOptionalAttributes.mPreviewIconId,
485                // key.mHorizontalGap,
486                // key.mVerticalGap,
487                // key.mOptionalAttributes.mVisualInsetLeft,
488                // key.mOptionalAttributes.mVisualInsetRight,
489                // key.mMaxMoreKeysColumn,
490        });
491    }
492
493    private boolean equalsInternal(final Key o) {
494        if (this == o) return true;
495        return o.mX == mX
496                && o.mY == mY
497                && o.mWidth == mWidth
498                && o.mHeight == mHeight
499                && o.mCode == mCode
500                && TextUtils.equals(o.mLabel, mLabel)
501                && TextUtils.equals(o.mHintLabel, mHintLabel)
502                && o.mIconId == mIconId
503                && o.mBackgroundType == mBackgroundType
504                && Arrays.equals(o.mMoreKeys, mMoreKeys)
505                && TextUtils.equals(o.getOutputText(), getOutputText())
506                && o.mActionFlags == mActionFlags
507                && o.mLabelFlags == mLabelFlags;
508    }
509
510    @Override
511    public int compareTo(Key o) {
512        if (equalsInternal(o)) return 0;
513        if (mHashCode > o.mHashCode) return 1;
514        return -1;
515    }
516
517    @Override
518    public int hashCode() {
519        return mHashCode;
520    }
521
522    @Override
523    public boolean equals(final Object o) {
524        return o instanceof Key && equalsInternal((Key)o);
525    }
526
527    @Override
528    public String toString() {
529        return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight();
530    }
531
532    public String toShortString() {
533        final int code = getCode();
534        if (code == Constants.CODE_OUTPUT_TEXT) {
535            return getOutputText();
536        }
537        return Constants.printableCode(code);
538    }
539
540    public String toLongString() {
541        final int iconId = getIconId();
542        final String topVisual = (iconId == KeyboardIconsSet.ICON_UNDEFINED)
543                ? KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(iconId) : getLabel();
544        final String hintLabel = getHintLabel();
545        final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel;
546        return toString() + " " + visual + "/" + backgroundName(mBackgroundType);
547    }
548
549    private static String backgroundName(final int backgroundType) {
550        switch (backgroundType) {
551        case BACKGROUND_TYPE_EMPTY: return "empty";
552        case BACKGROUND_TYPE_NORMAL: return "normal";
553        case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
554        case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff";
555        case BACKGROUND_TYPE_STICKY_ON: return "stickyOn";
556        case BACKGROUND_TYPE_ACTION: return "action";
557        case BACKGROUND_TYPE_SPACEBAR: return "spacebar";
558        default: return null;
559        }
560    }
561
562    public int getCode() {
563        return mCode;
564    }
565
566    @Nullable
567    public String getLabel() {
568        return mLabel;
569    }
570
571    @Nullable
572    public String getHintLabel() {
573        return mHintLabel;
574    }
575
576    @Nullable
577    public MoreKeySpec[] getMoreKeys() {
578        return mMoreKeys;
579    }
580
581    public void markAsLeftEdge(final KeyboardParams params) {
582        mHitBox.left = params.mLeftPadding;
583    }
584
585    public void markAsRightEdge(final KeyboardParams params) {
586        mHitBox.right = params.mOccupiedWidth - params.mRightPadding;
587    }
588
589    public void markAsTopEdge(final KeyboardParams params) {
590        mHitBox.top = params.mTopPadding;
591    }
592
593    public void markAsBottomEdge(final KeyboardParams params) {
594        mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
595    }
596
597    public final boolean isSpacer() {
598        return this instanceof Spacer;
599    }
600
601    public final boolean isActionKey() {
602        return mBackgroundType == BACKGROUND_TYPE_ACTION;
603    }
604
605    public final boolean isShift() {
606        return mCode == CODE_SHIFT;
607    }
608
609    public final boolean isModifier() {
610        return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
611    }
612
613    public final boolean isRepeatable() {
614        return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
615    }
616
617    public final boolean noKeyPreview() {
618        return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
619    }
620
621    public final boolean altCodeWhileTyping() {
622        return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
623    }
624
625    public final boolean isLongPressEnabled() {
626        // We need not start long press timer on the key which has activated shifted letter.
627        return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
628                && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
629    }
630
631    public KeyVisualAttributes getVisualAttributes() {
632        return mKeyVisualAttributes;
633    }
634
635    @Nonnull
636    public final Typeface selectTypeface(final KeyDrawParams params) {
637        switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) {
638        case LABEL_FLAGS_FONT_NORMAL:
639            return Typeface.DEFAULT;
640        case LABEL_FLAGS_FONT_MONO_SPACE:
641            return Typeface.MONOSPACE;
642        case LABEL_FLAGS_FONT_DEFAULT:
643        default:
644            // The type-face is specified by keyTypeface attribute.
645            return params.mTypeface;
646        }
647    }
648
649    public final int selectTextSize(final KeyDrawParams params) {
650        switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
651        case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
652            return params.mLetterSize;
653        case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
654            return params.mLargeLetterSize;
655        case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
656            return params.mLabelSize;
657        case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
658            return params.mHintLabelSize;
659        default: // No follow key ratio flag specified.
660            return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
661        }
662    }
663
664    public final int selectTextColor(final KeyDrawParams params) {
665        if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) {
666            return params.mFunctionalTextColor;
667        }
668        return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
669    }
670
671    public final int selectHintTextSize(final KeyDrawParams params) {
672        if (hasHintLabel()) {
673            return params.mHintLabelSize;
674        }
675        if (hasShiftedLetterHint()) {
676            return params.mShiftedLetterHintSize;
677        }
678        return params.mHintLetterSize;
679    }
680
681    public final int selectHintTextColor(final KeyDrawParams params) {
682        if (hasHintLabel()) {
683            return params.mHintLabelColor;
684        }
685        if (hasShiftedLetterHint()) {
686            return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
687                    : params.mShiftedLetterHintInactivatedColor;
688        }
689        return params.mHintLetterColor;
690    }
691
692    public final int selectMoreKeyTextSize(final KeyDrawParams params) {
693        return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize;
694    }
695
696    public final String getPreviewLabel() {
697        return isShiftedLetterActivated() ? mHintLabel : mLabel;
698    }
699
700    private boolean previewHasLetterSize() {
701        return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0
702                || StringUtils.codePointCount(getPreviewLabel()) == 1;
703    }
704
705    public final int selectPreviewTextSize(final KeyDrawParams params) {
706        if (previewHasLetterSize()) {
707            return params.mPreviewTextSize;
708        }
709        return params.mLetterSize;
710    }
711
712    @Nonnull
713    public Typeface selectPreviewTypeface(final KeyDrawParams params) {
714        if (previewHasLetterSize()) {
715            return selectTypeface(params);
716        }
717        return Typeface.DEFAULT_BOLD;
718    }
719
720    public final boolean isAlignHintLabelToBottom(final int defaultFlags) {
721        return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0;
722    }
723
724    public final boolean isAlignIconToBottom() {
725        return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0;
726    }
727
728    public final boolean isAlignLabelOffCenter() {
729        return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0;
730    }
731
732    public final boolean hasPopupHint() {
733        return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
734    }
735
736    public final boolean hasShiftedLetterHint() {
737        return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0
738                && !TextUtils.isEmpty(mHintLabel);
739    }
740
741    public final boolean hasHintLabel() {
742        return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
743    }
744
745    public final boolean needsAutoXScale() {
746        return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
747    }
748
749    public final boolean needsAutoScale() {
750        return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
751    }
752
753    public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) {
754        return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0;
755    }
756
757    public final boolean hasCustomActionLabel() {
758        return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0;
759    }
760
761    private final boolean isShiftedLetterActivated() {
762        return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
763                && !TextUtils.isEmpty(mHintLabel);
764    }
765
766    public final int getMoreKeysColumnNumber() {
767        return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK;
768    }
769
770    public final boolean isMoreKeysFixedColumn() {
771        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0;
772    }
773
774    public final boolean isMoreKeysFixedOrder() {
775        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0;
776    }
777
778    public final boolean hasLabelsInMoreKeys() {
779        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
780    }
781
782    public final int getMoreKeyLabelFlags() {
783        final int labelSizeFlag = hasLabelsInMoreKeys()
784                ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
785                : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
786        return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE;
787    }
788
789    public final boolean needsDividersInMoreKeys() {
790        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
791    }
792
793    public final boolean hasNoPanelAutoMoreKey() {
794        return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0;
795    }
796
797    @Nullable
798    public final String getOutputText() {
799        final OptionalAttributes attrs = mOptionalAttributes;
800        return (attrs != null) ? attrs.mOutputText : null;
801    }
802
803    public final int getAltCode() {
804        final OptionalAttributes attrs = mOptionalAttributes;
805        return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
806    }
807
808    public int getIconId() {
809        return mIconId;
810    }
811
812    @Nullable
813    public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
814        final OptionalAttributes attrs = mOptionalAttributes;
815        final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
816        final int iconId = mEnabled ? getIconId() : disabledIconId;
817        final Drawable icon = iconSet.getIconDrawable(iconId);
818        if (icon != null) {
819            icon.setAlpha(alpha);
820        }
821        return icon;
822    }
823
824    @Nullable
825    public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
826        return iconSet.getIconDrawable(getIconId());
827    }
828
829    /**
830     * Gets the width of the key in pixels, excluding the gap.
831     * @return The width of the key in pixels, excluding the gap.
832     */
833    public int getWidth() {
834        return mWidth;
835    }
836
837    /**
838     * Gets the height of the key in pixels, excluding the gap.
839     * @return The height of the key in pixels, excluding the gap.
840     */
841    public int getHeight() {
842        return mHeight;
843    }
844
845    /**
846     * The combined width in pixels of the horizontal gaps belonging to this key, both above and
847     * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key.
848     * @return Horizontal gap belonging to this key.
849     */
850    public int getHorizontalGap() {
851        return mHorizontalGap;
852    }
853
854    /**
855     * The combined height in pixels of the vertical gaps belonging to this key, both above and
856     * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key.
857     * @return Vertical gap belonging to this key.
858     */
859    public int getVerticalGap() {
860        return mVerticalGap;
861    }
862
863    /**
864     * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap.
865     * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap.
866     */
867    public int getX() {
868        return mX;
869    }
870
871    /**
872     * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap.
873     * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap.
874     */
875    public int getY() {
876        return mY;
877    }
878
879    public final int getDrawX() {
880        final int x = getX();
881        final OptionalAttributes attrs = mOptionalAttributes;
882        return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
883    }
884
885    public final int getDrawWidth() {
886        final OptionalAttributes attrs = mOptionalAttributes;
887        return (attrs == null) ? mWidth
888                : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
889    }
890
891    /**
892     * Informs the key that it has been pressed, in case it needs to change its appearance or
893     * state.
894     * @see #onReleased()
895     */
896    public void onPressed() {
897        mPressed = true;
898    }
899
900    /**
901     * Informs the key that it has been released, in case it needs to change its appearance or
902     * state.
903     * @see #onPressed()
904     */
905    public void onReleased() {
906        mPressed = false;
907    }
908
909    public final boolean isEnabled() {
910        return mEnabled;
911    }
912
913    public void setEnabled(final boolean enabled) {
914        mEnabled = enabled;
915    }
916
917    @Nonnull
918    public Rect getHitBox() {
919        return mHitBox;
920    }
921
922    /**
923     * Detects if a point falls on this key.
924     * @param x the x-coordinate of the point
925     * @param y the y-coordinate of the point
926     * @return whether or not the point falls on the key. If the key is attached to an edge, it
927     * will assume that all points between the key and the edge are considered to be on the key.
928     * @see #markAsLeftEdge(KeyboardParams) etc.
929     */
930    public boolean isOnKey(final int x, final int y) {
931        return mHitBox.contains(x, y);
932    }
933
934    /**
935     * Returns the square of the distance to the nearest edge of the key and the given point.
936     * @param x the x-coordinate of the point
937     * @param y the y-coordinate of the point
938     * @return the square of the distance of the point from the nearest edge of the key
939     */
940    public int squaredDistanceToEdge(final int x, final int y) {
941        final int left = getX();
942        final int right = left + mWidth;
943        final int top = getY();
944        final int bottom = top + mHeight;
945        final int edgeX = x < left ? left : (x > right ? right : x);
946        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
947        final int dx = x - edgeX;
948        final int dy = y - edgeY;
949        return dx * dx + dy * dy;
950    }
951
952    static class KeyBackgroundState {
953        private final int[] mReleasedState;
954        private final int[] mPressedState;
955
956        private KeyBackgroundState(final int ... attrs) {
957            mReleasedState = attrs;
958            mPressedState = Arrays.copyOf(attrs, attrs.length + 1);
959            mPressedState[attrs.length] = android.R.attr.state_pressed;
960        }
961
962        public int[] getState(final boolean pressed) {
963            return pressed ? mPressedState : mReleasedState;
964        }
965
966        public static final KeyBackgroundState[] STATES = {
967            // 0: BACKGROUND_TYPE_EMPTY
968            new KeyBackgroundState(android.R.attr.state_empty),
969            // 1: BACKGROUND_TYPE_NORMAL
970            new KeyBackgroundState(),
971            // 2: BACKGROUND_TYPE_FUNCTIONAL
972            new KeyBackgroundState(),
973            // 3: BACKGROUND_TYPE_STICKY_OFF
974            new KeyBackgroundState(android.R.attr.state_checkable),
975            // 4: BACKGROUND_TYPE_STICKY_ON
976            new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked),
977            // 5: BACKGROUND_TYPE_ACTION
978            new KeyBackgroundState(android.R.attr.state_active),
979            // 6: BACKGROUND_TYPE_SPACEBAR
980            new KeyBackgroundState(),
981        };
982    }
983
984    /**
985     * Returns the background drawable for the key, based on the current state and type of the key.
986     * @return the background drawable of the key.
987     * @see android.graphics.drawable.StateListDrawable#setState(int[])
988     */
989    @Nonnull
990    public final Drawable selectBackgroundDrawable(@Nonnull final Drawable keyBackground,
991            @Nonnull final Drawable functionalKeyBackground,
992            @Nonnull final Drawable spacebarBackground) {
993        final Drawable background;
994        if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) {
995            background = functionalKeyBackground;
996        } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) {
997            background = spacebarBackground;
998        } else {
999            background = keyBackground;
1000        }
1001        final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed);
1002        background.setState(state);
1003        return background;
1004    }
1005
1006    public static class Spacer extends Key {
1007        public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle,
1008                final KeyboardParams params, final KeyboardRow row) {
1009            super(null /* keySpec */, keyAttr, keyStyle, params, row);
1010        }
1011
1012        /**
1013         * This constructor is being used only for divider in more keys keyboard.
1014         */
1015        protected Spacer(final KeyboardParams params, final int x, final int y, final int width,
1016                final int height) {
1017            super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */,
1018                    null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width,
1019                    height, params.mHorizontalGap, params.mVerticalGap);
1020        }
1021    }
1022}
1023