diff options
Diffstat (limited to 'java/src')
36 files changed, 5055 insertions, 2234 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 4fbb5b012..a3bf25642 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -16,10 +16,6 @@ package com.android.inputmethod.latin; -import java.util.HashMap; -import java.util.Set; -import java.util.Map.Entry; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -30,6 +26,10 @@ import android.os.AsyncTask; import android.provider.BaseColumns; import android.util.Log; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Set; + /** * Stores new words temporarily until they are promoted to the user dictionary * for longevity. Words in the auto dictionary are used to determine if it's ok diff --git a/java/src/com/android/inputmethod/latin/BaseKeyboard.java b/java/src/com/android/inputmethod/latin/BaseKeyboard.java new file mode 100644 index 000000000..e2331f334 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/BaseKeyboard.java @@ -0,0 +1,717 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.BaseKeyboardParser.ParseException; +import com.android.inputmethod.latin.KeyStyles.KeyStyle; +import com.android.inputmethod.latin.KeyboardSwitcher.KeyboardId; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.util.Xml; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> + * <pre> + * <Keyboard + * latin:keyWidth="%10p" + * latin:keyHeight="50px" + * latin:horizontalGap="2px" + * latin:verticalGap="2px" > + * <Row latin:keyWidth="32px" > + * <Key latin:keyLabel="A" /> + * ... + * </Row> + * ... + * </Keyboard> + * </pre> + */ +public class BaseKeyboard { + + static final String TAG = "BaseKeyboard"; + + public static final int EDGE_LEFT = 0x01; + public static final int EDGE_RIGHT = 0x02; + public static final int EDGE_TOP = 0x04; + public static final int EDGE_BOTTOM = 0x08; + + public static final int KEYCODE_SHIFT = -1; + public static final int KEYCODE_MODE_CHANGE = -2; + public static final int KEYCODE_CANCEL = -3; + public static final int KEYCODE_DONE = -4; + public static final int KEYCODE_DELETE = -5; + public static final int KEYCODE_ALT = -6; + + /** Horizontal gap default for all rows */ + private int mDefaultHorizontalGap; + + /** Default key width */ + private int mDefaultWidth; + + /** Default key height */ + private int mDefaultHeight; + + /** Default gap between rows */ + private int mDefaultVerticalGap; + + /** Is the keyboard in the shifted state */ + private boolean mShifted; + + /** List of shift keys in this keyboard */ + private final List<Key> mShiftKeys = new ArrayList<Key>(); + + /** List of shift keys and its shifted state icon */ + private final HashMap<Key, Drawable> mShiftedIcons = new HashMap<Key, Drawable>(); + + /** Total height of the keyboard, including the padding and keys */ + private int mTotalHeight; + + /** + * Total width of the keyboard, including left side gaps and keys, but not any gaps on the + * right side. + */ + private int mTotalWidth; + + /** List of keys in this keyboard */ + private final List<Key> mKeys = new ArrayList<Key>(); + + /** Width of the screen available to fit the keyboard */ + private final int mDisplayWidth; + + /** Height of the screen */ + private final int mDisplayHeight; + + protected final KeyboardId mId; + + // Variables for pre-computing nearest keys. + + public final int GRID_WIDTH; + public final int GRID_HEIGHT; + private final int GRID_SIZE; + private int mCellWidth; + private int mCellHeight; + private int[][] mGridNeighbors; + private int mProximityThreshold; + private static int[] EMPTY_INT_ARRAY = new int[0]; + /** Number of key widths from current touch point to search for nearest keys. */ + private static float SEARCH_DISTANCE = 1.2f; + + /** + * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. + * Some of the key size defaults can be overridden per row from what the {@link BaseKeyboard} + * defines. + */ + public static class Row { + /** Default width of a key in this row. */ + public int defaultWidth; + /** Default height of a key in this row. */ + public int defaultHeight; + /** Default horizontal gap between keys in this row. */ + public int defaultHorizontalGap; + /** Vertical gap following this row. */ + public int verticalGap; + /** + * Edge flags for this row of keys. Possible values that can be assigned are + * {@link BaseKeyboard#EDGE_TOP EDGE_TOP} and {@link BaseKeyboard#EDGE_BOTTOM EDGE_BOTTOM} + */ + public int rowEdgeFlags; + + private final BaseKeyboard parent; + + private Row(BaseKeyboard parent) { + this.parent = parent; + } + + public Row(Resources res, BaseKeyboard parent, XmlResourceParser parser) { + this.parent = parent; + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard); + defaultWidth = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyWidth, + parent.mDisplayWidth, parent.mDefaultWidth); + defaultHeight = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyHeight, + parent.mDisplayHeight, parent.mDefaultHeight); + defaultHorizontalGap = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_horizontalGap, + parent.mDisplayWidth, parent.mDefaultHorizontalGap); + verticalGap = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_verticalGap, + parent.mDisplayHeight, parent.mDefaultVerticalGap); + a.recycle(); + a = res.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard_Row); + rowEdgeFlags = a.getInt(R.styleable.BaseKeyboard_Row_rowEdgeFlags, 0); + } + } + + /** + * Class for describing the position and characteristics of a single key in the keyboard. + */ + public static class Key { + /** + * All the key codes (unicode or custom code) that this key could generate, zero'th + * being the most important. + */ + public int[] codes; + /** The unicode that this key generates in manual temporary upper case mode. */ + public int manualTemporaryUpperCaseCode; + + /** Label to display */ + public CharSequence label; + /** Option of the label */ + public int labelOption; + + /** Icon to display instead of a label. Icon takes precedence over a label */ + public Drawable icon; + /** Hint icon to display on the key in conjunction with the label */ + public Drawable hintIcon; + /** Preview version of the icon, for the preview popup */ + /** + * The hint icon to display on the key when keyboard is in manual temporary upper case + * mode. + */ + public Drawable manualTemporaryUpperCaseHintIcon; + + public Drawable iconPreview; + /** Width of the key, not including the gap */ + public int width; + /** Height of the key, not including the gap */ + public int height; + /** The horizontal gap before this key */ + public int gap; + /** Whether this key is sticky, i.e., a toggle key */ + public boolean sticky; + /** X coordinate of the key in the keyboard layout */ + public int x; + /** Y coordinate of the key in the keyboard layout */ + public int y; + /** The current pressed state of this key */ + public boolean pressed; + /** If this is a sticky key, is it on? */ + public boolean on; + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public CharSequence text; + /** Popup characters */ + public CharSequence popupCharacters; + + /** + * Flags that specify the anchoring to edges of the keyboard for detecting touch events + * that are just out of the boundary of the key. This is a bit mask of + * {@link BaseKeyboard#EDGE_LEFT}, {@link BaseKeyboard#EDGE_RIGHT}, + * {@link BaseKeyboard#EDGE_TOP} and {@link BaseKeyboard#EDGE_BOTTOM}. + */ + public int edgeFlags; + /** Whether this is a modifier key, such as Shift or Alt */ + public boolean modifier; + /** The BaseKeyboard that this key belongs to */ + protected final BaseKeyboard keyboard; + /** + * If this key pops up a mini keyboard, this is the resource id for the XML layout for that + * keyboard. + */ + public int popupResId; + /** Whether this key repeats itself when held down */ + public boolean repeatable; + + + private final static int[] KEY_STATE_NORMAL_ON = { + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_PRESSED_ON = { + android.R.attr.state_pressed, + android.R.attr.state_checkable, + android.R.attr.state_checked + }; + + private final static int[] KEY_STATE_NORMAL_OFF = { + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_PRESSED_OFF = { + android.R.attr.state_pressed, + android.R.attr.state_checkable + }; + + private final static int[] KEY_STATE_NORMAL = { + }; + + private final static int[] KEY_STATE_PRESSED = { + android.R.attr.state_pressed + }; + + /** Create an empty key with no attributes. */ + public Key(Row parent) { + keyboard = parent.parent; + height = parent.defaultHeight; + gap = parent.defaultHorizontalGap; + width = parent.defaultWidth - gap; + edgeFlags = parent.rowEdgeFlags; + } + + /** Create a key with the given top-left coordinate and extract its attributes from + * the XML parser. + * @param res resources associated with the caller's context + * @param parent the row that this key belongs to. The row must already be attached to + * a {@link BaseKeyboard}. + * @param x the x coordinate of the top-left + * @param y the y coordinate of the top-left + * @param parser the XML parser containing the attributes for this key + */ + public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser, + KeyStyles keyStyles) { + this(parent); + + TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard); + height = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyHeight, + keyboard.mDisplayHeight, parent.defaultHeight); + gap = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_horizontalGap, + keyboard.mDisplayWidth, parent.defaultHorizontalGap); + width = BaseKeyboardParser.getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyWidth, + keyboard.mDisplayWidth, parent.defaultWidth) - gap; + a.recycle(); + + a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.BaseKeyboard_Key); + + final KeyStyle style; + if (a.hasValue(R.styleable.BaseKeyboard_Key_keyStyle)) { + String styleName = a.getString(R.styleable.BaseKeyboard_Key_keyStyle); + style = keyStyles.getKeyStyle(styleName); + if (style == null) + throw new ParseException("Unknown key style: " + styleName, parser); + } else { + style = keyStyles.getEmptyKeyStyle(); + } + + // Horizontal gap is divided equally to both sides of the key. + this.x = x + gap / 2; + this.y = y; + + codes = style.getIntArray(a, R.styleable.BaseKeyboard_Key_codes); + iconPreview = style.getDrawable(a, R.styleable.BaseKeyboard_Key_iconPreview); + setDefaultBounds(iconPreview); + popupCharacters = style.getText(a, R.styleable.BaseKeyboard_Key_popupCharacters); + popupResId = style.getResourceId(a, R.styleable.BaseKeyboard_Key_popupKeyboard, 0); + repeatable = style.getBoolean(a, R.styleable.BaseKeyboard_Key_isRepeatable, false); + modifier = style.getBoolean(a, R.styleable.BaseKeyboard_Key_isModifier, false); + sticky = style.getBoolean(a, R.styleable.BaseKeyboard_Key_isSticky, false); + edgeFlags = style.getFlag(a, R.styleable.BaseKeyboard_Key_keyEdgeFlags, 0); + edgeFlags |= parent.rowEdgeFlags; + + icon = style.getDrawable(a, R.styleable.BaseKeyboard_Key_keyIcon); + setDefaultBounds(icon); + hintIcon = style.getDrawable(a, R.styleable.BaseKeyboard_Key_keyHintIcon); + setDefaultBounds(hintIcon); + manualTemporaryUpperCaseHintIcon = style.getDrawable(a, + R.styleable.BaseKeyboard_Key_manualTemporaryUpperCaseHintIcon); + setDefaultBounds(manualTemporaryUpperCaseHintIcon); + + label = style.getText(a, R.styleable.BaseKeyboard_Key_keyLabel); + labelOption = style.getFlag(a, R.styleable.BaseKeyboard_Key_keyLabelOption, 0); + manualTemporaryUpperCaseCode = style.getInt(a, + R.styleable.BaseKeyboard_Key_manualTemporaryUpperCaseCode, 0); + text = style.getText(a, R.styleable.BaseKeyboard_Key_keyOutputText); + final Drawable shiftedIcon = style.getDrawable(a, + R.styleable.BaseKeyboard_Key_shiftedIcon); + if (shiftedIcon != null) + keyboard.getShiftedIcons().put(this, shiftedIcon); + + if (codes == null && !TextUtils.isEmpty(label)) { + codes = new int[] { label.charAt(0) }; + } + a.recycle(); + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see #onReleased(boolean) + */ + public void onPressed() { + pressed = !pressed; + } + + /** + * Changes the pressed state of the key. If it is a sticky key, it will also change the + * toggled state of the key if the finger was release inside. + * @param inside whether the finger was released inside the key + * @see #onPressed() + */ + public void onReleased(boolean inside) { + pressed = !pressed; + if (sticky) { + on = !on; + } + } + + /** + * Detects if a point falls inside this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls inside the key. If the key is attached to an + * edge, it will assume that all points between the key and the edge are considered to be + * inside the key. + */ + public boolean isInside(int x, int y) { + boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; + boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; + boolean topEdge = (edgeFlags & EDGE_TOP) > 0; + boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; + if ((x >= this.x || (leftEdge && x <= this.x + this.width)) + && (x < this.x + this.width || (rightEdge && x >= this.x)) + && (y >= this.y || (topEdge && y <= this.y + this.height)) + && (y < this.y + this.height || (bottomEdge && y >= this.y))) { + return true; + } else { + return false; + } + } + + /** + * Returns the square of the distance to the nearest edge of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the nearest edge of the key + */ + public int squaredDistanceToEdge(int x, int y) { + final int left = this.x; + final int right = left + this.width; + final int top = this.y; + final int bottom = top + this.height; + final int edgeX = x < left ? left : (x > right ? right : x); + final int edgeY = y < top ? top : (y > bottom ? bottom : y); + final int dx = x - edgeX; + final int dy = y - edgeY; + return dx * dx + dy * dy; + } + + /** + * Returns the drawable state for the key, based on the current state and type of the key. + * @return the drawable state of the key. + * @see android.graphics.drawable.StateListDrawable#setState(int[]) + */ + public int[] getCurrentDrawableState() { + int[] states = KEY_STATE_NORMAL; + + if (on) { + if (pressed) { + states = KEY_STATE_PRESSED_ON; + } else { + states = KEY_STATE_NORMAL_ON; + } + } else { + if (sticky) { + if (pressed) { + states = KEY_STATE_PRESSED_OFF; + } else { + states = KEY_STATE_NORMAL_OFF; + } + } else { + if (pressed) { + states = KEY_STATE_PRESSED; + } + } + } + return states; + } + } + + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + */ + public BaseKeyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, null); + } + + /** + * Creates a keyboard from the given keyboard identifier. + * @param context the application or service context + * @param id keyboard identifier + */ + public BaseKeyboard(Context context, KeyboardId id) { + this(context, id.getXmlId(), id); + } + + /** + * Creates a keyboard from the given xml key layout file. + * @param context the application or service context + * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. + * @param id keyboard identifier + */ + private BaseKeyboard(Context context, int xmlLayoutResId, KeyboardId id) { + this(context, xmlLayoutResId, id, + context.getResources().getDisplayMetrics().widthPixels, + context.getResources().getDisplayMetrics().heightPixels); + } + + private BaseKeyboard(Context context, int xmlLayoutResId, KeyboardId id, int width, + int height) { + Resources res = context.getResources(); + GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); + GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); + GRID_SIZE = GRID_WIDTH * GRID_HEIGHT; + + mDisplayWidth = width; + mDisplayHeight = height; + + mDefaultHorizontalGap = 0; + setKeyWidth(mDisplayWidth / 10); + mDefaultVerticalGap = 0; + mDefaultHeight = mDefaultWidth; + mId = id; + loadKeyboard(context, xmlLayoutResId); + } + + /** + * <p>Creates a blank keyboard from the given resource file and populates it with the specified + * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. + * </p> + * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as + * possible in each row.</p> + * @param context the application or service context + * @param layoutTemplateResId the layout template file, containing no keys. + * @param characters the list of characters to display on the keyboard. One key will be created + * for each character. + * @param columns the number of columns of keys to display. If this number is greater than the + * number of keys that can fit in a row, it will be ignored. If this number is -1, the + * keyboard will fit as many keys as possible in each row. + */ + public BaseKeyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + this(context, layoutTemplateResId); + int x = 0; + int y = 0; + int column = 0; + mTotalWidth = 0; + + Row row = new Row(this); + row.defaultHeight = mDefaultHeight; + row.defaultWidth = mDefaultWidth; + row.defaultHorizontalGap = mDefaultHorizontalGap; + row.verticalGap = mDefaultVerticalGap; + row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM; + final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; + for (int i = 0; i < characters.length(); i++) { + char c = characters.charAt(i); + if (column >= maxColumns + || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { + x = 0; + y += mDefaultVerticalGap + mDefaultHeight; + column = 0; + } + final Key key = new Key(row); + // Horizontal gap is divided equally to both sides of the key. + key.x = x + key.gap / 2; + key.y = y; + key.label = String.valueOf(c); + key.codes = new int[] { c }; + column++; + x += key.width + key.gap; + mKeys.add(key); + if (x > mTotalWidth) { + mTotalWidth = x; + } + } + mTotalHeight = y + mDefaultHeight; + } + + public KeyboardId getKeyboardId() { + return mId; + } + + public List<Key> getKeys() { + return mKeys; + } + + protected int getHorizontalGap() { + return mDefaultHorizontalGap; + } + + protected void setHorizontalGap(int gap) { + mDefaultHorizontalGap = gap; + } + + protected int getVerticalGap() { + return mDefaultVerticalGap; + } + + protected void setVerticalGap(int gap) { + mDefaultVerticalGap = gap; + } + + protected int getKeyHeight() { + return mDefaultHeight; + } + + protected void setKeyHeight(int height) { + mDefaultHeight = height; + } + + protected int getKeyWidth() { + return mDefaultWidth; + } + + protected void setKeyWidth(int width) { + mDefaultWidth = width; + final int threshold = (int) (width * SEARCH_DISTANCE); + mProximityThreshold = threshold * threshold; + } + + /** + * Returns the total height of the keyboard + * @return the total height of the keyboard + */ + public int getHeight() { + return mTotalHeight; + } + + public int getMinWidth() { + return mTotalWidth; + } + + public int getKeyboardHeight() { + return mDisplayHeight; + } + + public int getKeyboardWidth() { + return mDisplayWidth; + } + + public boolean setShifted(boolean shiftState) { + for (final Key key : mShiftKeys) { + key.on = shiftState; + } + if (mShifted != shiftState) { + mShifted = shiftState; + return true; + } + return false; + } + + public boolean isShiftedOrShiftLocked() { + return mShifted; + } + + public List<Key> getShiftKeys() { + return mShiftKeys; + } + + public Map<Key, Drawable> getShiftedIcons() { + return mShiftedIcons; + } + + private void computeNearestNeighbors() { + // Round-up so we don't have any pixels outside the grid + mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH; + mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT; + mGridNeighbors = new int[GRID_SIZE][]; + final int[] indices = new int[mKeys.size()]; + final int gridWidth = GRID_WIDTH * mCellWidth; + final int gridHeight = GRID_HEIGHT * mCellHeight; + final int threshold = mProximityThreshold; + for (int x = 0; x < gridWidth; x += mCellWidth) { + for (int y = 0; y < gridHeight; y += mCellHeight) { + final int centerX = x + mCellWidth / 2; + final int centerY = y + mCellHeight / 2; + int count = 0; + for (int i = 0; i < mKeys.size(); i++) { + final Key key = mKeys.get(i); + if (key.squaredDistanceToEdge(centerX, centerY) < threshold) + indices[count++] = i; + } + final int[] cell = new int[count]; + System.arraycopy(indices, 0, cell, 0, count); + mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell; + } + } + } + + /** + * Returns the indices of the keys that are closest to the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the array of integer indices for the nearest keys to the given point. If the given + * point is out of range, then an array of size zero is returned. + */ + public int[] getNearestKeys(int x, int y) { + if (mGridNeighbors == null) computeNearestNeighbors(); + if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) { + int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth); + if (index < GRID_SIZE) { + return mGridNeighbors[index]; + } + } + return EMPTY_INT_ARRAY; + } + + // TODO should be private + protected BaseKeyboard.Row createRowFromXml(Resources res, XmlResourceParser parser) { + return new BaseKeyboard.Row(res, this, parser); + } + + // TODO should be private + protected BaseKeyboard.Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser, KeyStyles keyStyles) { + return new BaseKeyboard.Key(res, parent, x, y, parser, keyStyles); + } + + private void loadKeyboard(Context context, int xmlLayoutResId) { + try { + final Resources res = context.getResources(); + BaseKeyboardParser parser = new BaseKeyboardParser(this, res); + parser.parseKeyboard(res.getXml(xmlLayoutResId)); + // mTotalWidth is the width of this keyboard which is maximum width of row. + mTotalWidth = parser.getMaxRowWidth(); + mTotalHeight = parser.getTotalHeight(); + } catch (XmlPullParserException e) { + Log.w(TAG, "keyboard XML parse error: " + e); + throw new IllegalArgumentException(e); + } catch (IOException e) { + Log.w(TAG, "keyboard XML parse error: " + e); + throw new RuntimeException(e); + } + } + + protected static void setDefaultBounds(Drawable drawable) { + if (drawable != null) + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight()); + } +} diff --git a/java/src/com/android/inputmethod/latin/BaseKeyboardParser.java b/java/src/com/android/inputmethod/latin/BaseKeyboardParser.java new file mode 100644 index 000000000..38b2a1b57 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/BaseKeyboardParser.java @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.BaseKeyboard.Key; +import com.android.inputmethod.latin.BaseKeyboard.Row; +import com.android.inputmethod.latin.KeyboardSwitcher.KeyboardId; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.util.Log; +import android.util.TypedValue; +import android.util.Xml; +import android.view.InflateException; + +import java.io.IOException; +import java.util.List; + +/** + * Parser for BaseKeyboard. + * + * This class parses Keyboard XML file and fill out keys in BaseKeyboard. + * The Keyboard XML file looks like: + * <pre> + * >!-- xml/keyboard.xml --< + * >Keyboard keyboard_attributes*< + * >!-- Keyboard Content --< + * >Row row_attributes*< + * >!-- Row Content --< + * >Key key_attributes* /< + * >Spacer horizontalGap="0.2in" /< + * >include keyboardLayout="@xml/other_keys"< + * ... + * >/Row< + * >include keyboardLayout="@xml/other_rows"< + * ... + * >/Keyboard< + * </pre> + * The XML file which is included in other file must have >merge< as root element, such as: + * <pre> + * >!-- xml/other_keys.xml --< + * >merge< + * >Key key_attributes* /< + * ... + * >/merge< + * </pre> + * and + * <pre> + * >!-- xml/other_rows.xml --< + * >merge< + * >Row row_attributes*< + * >Key key_attributes* /< + * >/Row< + * ... + * >/merge< + * </pre> + * You can also use switch-case-default tags to select Rows and Keys. + * <pre> + * >switch< + * >case case_attribute*< + * >!-- Any valid tags at switch position --< + * >/case< + * ... + * >default< + * >!-- Any valid tags at switch position --< + * >/default< + * >/switch< + * </pre> + * You can declare Key style and specify styles within Key tags. + * <pre> + * >switch< + * >case colorScheme="white"< + * >key-style styleName="shift-key" parentStyle="modifier-key" + * keyIcon="@drawable/sym_keyboard_shift" + * /< + * >/case< + * >case colorScheme="black"< + * >key-style styleName="shift-key" parentStyle="modifier-key" + * keyIcon="@drawable/sym_bkeyboard_shift" + * /< + * >/case< + * >/switch< + * ... + * >Key keyStyle="shift-key" ... /< + * </pre> + */ + +public class BaseKeyboardParser { + private static final String TAG = "BaseKeyboardParser"; + private static final boolean DEBUG_TAG = false; + + // Keyboard XML Tags + private static final String TAG_KEYBOARD = "Keyboard"; + private static final String TAG_ROW = "Row"; + private static final String TAG_KEY = "Key"; + private static final String TAG_SPACER = "Spacer"; + private static final String TAG_INCLUDE = "include"; + private static final String TAG_MERGE = "merge"; + private static final String TAG_SWITCH = "switch"; + private static final String TAG_CASE = "case"; + private static final String TAG_DEFAULT = "default"; + private static final String TAG_KEY_STYLE = "key-style"; + + private final BaseKeyboard mKeyboard; + private final Resources mResources; + + private int mCurrentX = 0; + private int mCurrentY = 0; + private int mMaxRowWidth = 0; + private int mTotalHeight = 0; + private Row mCurrentRow = null; + private final KeyStyles mKeyStyles = new KeyStyles(); + + public BaseKeyboardParser(BaseKeyboard keyboard, Resources res) { + mKeyboard = keyboard; + mResources = res; + } + + public int getMaxRowWidth() { + return mMaxRowWidth; + } + + public int getTotalHeight() { + return mTotalHeight; + } + + public void parseKeyboard(XmlResourceParser parser) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugStartTag("parseKeyboard", tag, false); + if (TAG_KEYBOARD.equals(tag)) { + parseKeyboardAttributes(parser); + parseKeyboardContent(parser, mKeyboard.getKeys()); + break; + } else { + throw new IllegalStartTag(parser, TAG_KEYBOARD); + } + } + } + } + + private void parseKeyboardAttributes(XmlResourceParser parser) { + final BaseKeyboard keyboard = mKeyboard; + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard); + final int width = keyboard.getKeyboardWidth(); + final int height = keyboard.getKeyboardHeight(); + keyboard.setKeyWidth(getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyWidth, width, width / 10)); + keyboard.setKeyHeight(getDimensionOrFraction(a, + R.styleable.BaseKeyboard_keyHeight, height, 50)); + keyboard.setHorizontalGap(getDimensionOrFraction(a, + R.styleable.BaseKeyboard_horizontalGap, width, 0)); + keyboard.setVerticalGap(getDimensionOrFraction(a, + R.styleable.BaseKeyboard_verticalGap, height, 0)); + a.recycle(); + if (DEBUG_TAG) Log.d(TAG, "id=" + keyboard.mId); + } + + private void parseKeyboardContent(XmlResourceParser parser, List<Key> keys) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugStartTag("parseKeyboardContent", tag, keys == null); + if (TAG_ROW.equals(tag)) { + Row row = mKeyboard.createRowFromXml(mResources, parser); + if (keys != null) + startRow(row); + parseRowContent(parser, row, keys); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeKeyboardContent(parser, keys); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchKeyboardContent(parser, keys); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, keys); + } else { + throw new IllegalStartTag(parser, TAG_ROW); + } + } else if (event == XmlResourceParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugEndTag("parseKeyboardContent", tag, keys == null); + if (TAG_KEYBOARD.equals(tag)) { + endKeyboard(mKeyboard.getVerticalGap()); + break; + } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)) { + break; + } else if (TAG_MERGE.equals(tag)) { + break; + } else if (TAG_KEY_STYLE.equals(tag)) { + continue; + } else { + throw new IllegalEndTag(parser, TAG_ROW); + } + } + } + } + + private void parseRowContent(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugStartTag("parseRowContent", tag, keys == null); + if (TAG_KEY.equals(tag)) { + parseKey(parser, row, keys); + } else if (TAG_SPACER.equals(tag)) { + parseSpacer(parser, keys); + } else if (TAG_INCLUDE.equals(tag)) { + parseIncludeRowContent(parser, row, keys); + } else if (TAG_SWITCH.equals(tag)) { + parseSwitchRowContent(parser, row, keys); + } else if (TAG_KEY_STYLE.equals(tag)) { + parseKeyStyle(parser, keys); + } else { + throw new IllegalStartTag(parser, TAG_KEY); + } + } else if (event == XmlResourceParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugEndTag("parseRowContent", tag, keys == null); + if (TAG_ROW.equals(tag)) { + if (keys != null) + endRow(); + break; + } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)) { + break; + } else if (TAG_MERGE.equals(tag)) { + break; + } else if (TAG_KEY_STYLE.equals(tag)) { + continue; + } else { + throw new IllegalEndTag(parser, TAG_KEY); + } + } + } + } + + private void parseKey(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + if (keys == null) { + checkEndTag(TAG_KEY, parser); + } else { + Key key = mKeyboard.createKeyFromXml(mResources, row, mCurrentX, mCurrentY, parser, + mKeyStyles); + checkEndTag(TAG_KEY, parser); + keys.add(key); + if (key.codes[0] == BaseKeyboard.KEYCODE_SHIFT) + mKeyboard.getShiftKeys().add(key); + endKey(key); + } + } + + private void parseSpacer(XmlResourceParser parser, List<Key> keys) + throws XmlPullParserException, IOException { + if (keys == null) { + checkEndTag(TAG_SPACER, parser); + } else { + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard); + final int gap = getDimensionOrFraction(a, R.styleable.BaseKeyboard_horizontalGap, + mKeyboard.getKeyboardWidth(), 0); + a.recycle(); + checkEndTag(TAG_SPACER, parser); + setSpacer(gap); + } + } + + private void parseIncludeKeyboardContent(XmlResourceParser parser, List<Key> keys) + throws XmlPullParserException, IOException { + parseIncludeInternal(parser, null, keys); + } + + private void parseIncludeRowContent(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + parseIncludeInternal(parser, row, keys); + } + + private void parseIncludeInternal(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + if (keys == null) { + checkEndTag(TAG_INCLUDE, parser); + } else { + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard_Include); + final int keyboardLayout = a.getResourceId( + R.styleable.BaseKeyboard_Include_keyboardLayout, 0); + a.recycle(); + + checkEndTag(TAG_INCLUDE, parser); + if (keyboardLayout == 0) + throw new ParseException("No keyboardLayout attribute in <include/>", parser); + if (DEBUG_TAG) Log.d(TAG, String.format(" keyboardLayout=0x%08x", keyboardLayout)); + parseMerge(mResources.getLayout(keyboardLayout), row, keys); + } + } + + private void parseMerge(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + int event; + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugStartTag("parseMerge", tag, keys == null); + if (TAG_MERGE.equals(tag)) { + if (row == null) { + parseKeyboardContent(parser, keys); + } else { + parseRowContent(parser, row, keys); + } + break; + } else { + throw new ParseException( + "Included keyboard layout must have <merge> root element", parser); + } + } + } + } + + private void parseSwitchKeyboardContent(XmlResourceParser parser, List<Key> keys) + throws XmlPullParserException, IOException { + parseSwitchInternal(parser, null, keys); + } + + private void parseSwitchRowContent(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + parseSwitchInternal(parser, row, keys); + } + + private void parseSwitchInternal(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + boolean selected = false; + int event; + if (DEBUG_TAG) Log.d(TAG, "parseSwitchInternal: id=" + mKeyboard.mId); + while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { + if (event == XmlResourceParser.START_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugStartTag("parseSwitchInternal", tag, keys == null); + if (TAG_CASE.equals(tag)) { + selected |= parseCase(parser, row, selected ? null : keys); + } else if (TAG_DEFAULT.equals(tag)) { + selected |= parseDefault(parser, row, selected ? null : keys); + } else { + throw new IllegalStartTag(parser, TAG_KEY); + } + } else if (event == XmlResourceParser.END_TAG) { + final String tag = parser.getName(); + if (DEBUG_TAG) debugEndTag("parseRowContent", tag, keys == null); + if (TAG_SWITCH.equals(tag)) { + break; + } else { + throw new IllegalEndTag(parser, TAG_KEY); + } + } + } + } + + private boolean parseCase(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + final boolean selected = parseCaseCondition(parser); + if (row == null) { + // Processing Rows. + parseKeyboardContent(parser, selected ? keys : null); + } else { + // Processing Keys. + parseRowContent(parser, row, selected ? keys : null); + } + return selected; + } + + private boolean parseCaseCondition(XmlResourceParser parser) { + final KeyboardId id = mKeyboard.mId; + if (id == null) + return true; + + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard_Case); + final TypedArray viewAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboardView); + try { + final boolean modeMatched = matchInteger(a, + R.styleable.BaseKeyboard_Case_mode, id.mMode); + final boolean settingsKeyMatched = matchBoolean(a, + R.styleable.BaseKeyboard_Case_hasSettingsKey, id.mHasSettingsKey); + final boolean voiceEnabledMatched = matchBoolean(a, + R.styleable.BaseKeyboard_Case_voiceKeyEnabled, id.mVoiceKeyEnabled); + final boolean voiceKeyMatched = matchBoolean(a, + R.styleable.BaseKeyboard_Case_hasVoiceKey, id.mHasVoiceKey); + final boolean colorSchemeMatched = matchInteger(viewAttr, + R.styleable.BaseKeyboardView_colorScheme, id.mColorScheme); + // As noted at KeyboardSwitcher.KeyboardId class, we are interested only in + // enum value masked by IME_MASK_ACTION and IME_FLAG_NO_ENTER_ACTION. So matching + // this attribute with id.mImeOptions as integer value is enough for our purpose. + final boolean imeOptionsMatched = matchInteger(a, + R.styleable.BaseKeyboard_Case_imeOptions, id.mImeOptions); + final boolean selected = modeMatched && settingsKeyMatched && voiceEnabledMatched + && voiceKeyMatched && colorSchemeMatched && imeOptionsMatched; + + if (DEBUG_TAG) { + Log.d(TAG, String.format( + "parseCaseCondition: %s%s%s%s%s%s%s", + Boolean.toString(selected).toUpperCase(), + debugInteger(a, R.styleable.BaseKeyboard_Case_mode, "mode"), + debugBoolean(a, R.styleable.BaseKeyboard_Case_hasSettingsKey, + "hasSettingsKey"), + debugBoolean(a, R.styleable.BaseKeyboard_Case_voiceKeyEnabled, + "voiceKeyEnabled"), + debugBoolean(a, R.styleable.BaseKeyboard_Case_hasVoiceKey, "hasVoiceKey"), + debugInteger(viewAttr, R.styleable.BaseKeyboardView_colorScheme, + "colorScheme"), + debugInteger(a, R.styleable.BaseKeyboard_Case_imeOptions, "imeOptions"))); + } + + return selected; + } finally { + a.recycle(); + viewAttr.recycle(); + } + } + + private static boolean matchInteger(TypedArray a, int index, int value) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for the + // attribute. + return !a.hasValue(index) || a.getInt(index, 0) == value; + } + + private static boolean matchBoolean(TypedArray a, int index, boolean value) { + // If <case> does not have "index" attribute, that means this <case> is wild-card for the + // attribute. + return !a.hasValue(index) || a.getBoolean(index, false) == value; + } + + private boolean parseDefault(XmlResourceParser parser, Row row, List<Key> keys) + throws XmlPullParserException, IOException { + if (row == null) { + parseKeyboardContent(parser, keys); + } else { + parseRowContent(parser, row, keys); + } + return true; + } + + private void parseKeyStyle(XmlResourceParser parser, List<Key> keys) + throws XmlPullParserException, IOException { + TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard_KeyStyle); + TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.BaseKeyboard_Key); + try { + if (!a.hasValue(R.styleable.BaseKeyboard_KeyStyle_styleName)) + throw new ParseException("<" + TAG_KEY_STYLE + + "/> needs styleName attribute", parser); + if (keys != null) + mKeyStyles.parseKeyStyleAttributes(a, keyAttrs, parser); + } finally { + a.recycle(); + keyAttrs.recycle(); + } + } + + private static void checkEndTag(String tag, XmlResourceParser parser) + throws XmlPullParserException, IOException { + if (parser.next() == XmlResourceParser.END_TAG && tag.equals(parser.getName())) + return; + throw new NonEmptyTag(tag, parser); + } + + private void startRow(Row row) { + mCurrentX = 0; + mCurrentRow = row; + } + + private void endRow() { + if (mCurrentRow == null) + throw new InflateException("orphant end row tag"); + mCurrentY += mCurrentRow.verticalGap + mCurrentRow.defaultHeight; + mCurrentRow = null; + } + + private void endKey(Key key) { + mCurrentX += key.gap + key.width; + if (mCurrentX > mMaxRowWidth) + mMaxRowWidth = mCurrentX; + } + + private void endKeyboard(int defaultVerticalGap) { + mTotalHeight = mCurrentY - defaultVerticalGap; + } + + private void setSpacer(int gap) { + mCurrentX += gap; + } + + public static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { + final TypedValue value = a.peekValue(index); + if (value == null) + return defValue; + if (value.type == TypedValue.TYPE_DIMENSION) { + return a.getDimensionPixelOffset(index, defValue); + } else if (value.type == TypedValue.TYPE_FRACTION) { + // Round it to avoid values like 47.9999 from getting truncated + return Math.round(a.getFraction(index, base, base, defValue)); + } + return defValue; + } + + @SuppressWarnings("serial") + public static class ParseException extends InflateException { + public ParseException(String msg, XmlResourceParser parser) { + super(msg + " at line " + parser.getLineNumber()); + } + } + + @SuppressWarnings("serial") + private static class IllegalStartTag extends ParseException { + public IllegalStartTag(XmlResourceParser parser, String parent) { + super("Illegal start tag " + parser.getName() + " in " + parent, parser); + } + } + + @SuppressWarnings("serial") + private static class IllegalEndTag extends ParseException { + public IllegalEndTag(XmlResourceParser parser, String parent) { + super("Illegal end tag " + parser.getName() + " in " + parent, parser); + } + } + + @SuppressWarnings("serial") + private static class NonEmptyTag extends ParseException { + public NonEmptyTag(String tag, XmlResourceParser parser) { + super(tag + " must be empty tag", parser); + } + } + + private static void debugStartTag(String title, String tag, boolean skip) { + Log.d(TAG, title + ": <" + tag + ">" + (skip ? " skip" : "")); + } + + private static void debugEndTag(String title, String tag, boolean skip) { + Log.d(TAG, title + ": </" + tag + ">" + (skip ? " skip" : "")); + } + + private static String debugInteger(TypedArray a, int index, String name) { + return a.hasValue(index) ? " " + name + "=" + a.getInt(index, 0) : ""; + } + + private static String debugBoolean(TypedArray a, int index, String name) { + return a.hasValue(index) ? " " + name + "=" + a.getBoolean(index, false) : ""; + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/BaseKeyboardView.java index b1ef08573..3193cd46e 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java +++ b/java/src/com/android/inputmethod/latin/BaseKeyboardView.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.BaseKeyboard.Key; + import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -29,8 +31,6 @@ import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; import android.os.Handler; import android.os.Message; import android.os.SystemClock; @@ -43,6 +43,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; import android.widget.PopupWindow; import android.widget.TextView; @@ -58,18 +59,23 @@ import java.util.WeakHashMap; * * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class. * - * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground - * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout - * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset - * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize - * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize - * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor - * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection - * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout + * @attr ref R.styleable#BaseKeyboardView_keyBackground + * @attr ref R.styleable#BaseKeyboardView_keyPreviewLayout + * @attr ref R.styleable#BaseKeyboardView_keyPreviewOffset + * @attr ref R.styleable#BaseKeyboardView_labelTextSize + * @attr ref R.styleable#BaseKeyboardView_keyTextSize + * @attr ref R.styleable#BaseKeyboardView_keyTextColor + * @attr ref R.styleable#BaseKeyboardView_verticalCorrection + * @attr ref R.styleable#BaseKeyboardView_popupLayout */ -public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy { - private static final String TAG = "LatinKeyboardBaseView"; +public class BaseKeyboardView extends View implements PointerTracker.UIProxy { + private static final String TAG = "BaseKeyboardView"; private static final boolean DEBUG = false; + private static final boolean DEBUG_SHOW_ALIGN = false; + private static final boolean DEBUG_KEYBOARD_GRID = false; + + public static final int COLOR_SCHEME_WHITE = 0; + public static final int COLOR_SCHEME_BLACK = 1; public static final int NOT_A_TOUCH_COORDINATE = -1; @@ -161,14 +167,15 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx // Miscellaneous constants /* package */ static final int NOT_A_KEY = -1; private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; - private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1; + private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1; // XML attribute - private int mKeyTextSize; + private int mKeyLetterSize; private int mKeyTextColor; - private Typeface mKeyTextStyle = Typeface.DEFAULT; + private int mKeyTextColorDisabled; + private Typeface mKeyLetterStyle = Typeface.DEFAULT; private int mLabelTextSize; - private int mSymbolColorScheme = 0; + private int mColorScheme = COLOR_SCHEME_WHITE; private int mShadowColor; private float mShadowRadius; private Drawable mKeyBackground; @@ -180,12 +187,11 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx private int mPopupLayout; // Main keyboard - private Keyboard mKeyboard; + private BaseKeyboard mKeyboard; private Key[] mKeys; - // TODO this attribute should be gotten from Keyboard. - private int mKeyboardVerticalGap; // Key preview popup + private boolean mInForeground; private TextView mPreviewText; private PopupWindow mPreviewPopup; private int mPreviewTextSizeLarge; @@ -202,7 +208,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx // Popup mini keyboard private PopupWindow mMiniKeyboardPopup; - private LatinKeyboardBaseView mMiniKeyboard; + private BaseKeyboardView mMiniKeyboard; private View mMiniKeyboardParent; private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>(); private int mMiniKeyboardOriginX; @@ -248,9 +254,15 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx private final Rect mClipRegion = new Rect(0, 0, 0, 0); // This map caches key label text height in pixel as value and key label text size as map key. private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>(); - // Distance from horizontal center of the key, proportional to key label text height. - private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f; - private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H"; + // Distance from horizontal center of the key, proportional to key label text height and width. + private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.55f; + private final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f; + private final String KEY_LABEL_REFERENCE_CHAR = "H"; + private final int KEY_LABEL_OPTION_ALIGN_LEFT = 1; + private final int KEY_LABEL_OPTION_ALIGN_RIGHT = 2; + private final int KEY_LABEL_OPTION_ALIGN_BOTTOM = 8; + private final int KEY_LABEL_OPTION_FONT_NORMAL = 16; + private final int mKeyLabelHorizontalPadding; private final UIHandler mHandler = new UIHandler(); @@ -259,6 +271,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx private static final int MSG_DISMISS_PREVIEW = 2; private static final int MSG_REPEAT_KEY = 3; private static final int MSG_LONGPRESS_KEY = 4; + private static final int MSG_LONGPRESS_SHIFT_KEY = 5; private boolean mInKeyRepeat; @@ -282,6 +295,11 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx openPopupIfRequired(msg.arg1, tracker); break; } + case MSG_LONGPRESS_SHIFT_KEY: { + final PointerTracker tracker = (PointerTracker)msg.obj; + onLongPressShiftKey(tracker); + break; + } } } @@ -325,17 +343,24 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { - removeMessages(MSG_LONGPRESS_KEY); + cancelLongPressTimers(); sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); } - public void cancelLongPressTimer() { + public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) { + cancelLongPressTimers(); + sendMessageDelayed( + obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay); + } + + public void cancelLongPressTimers() { removeMessages(MSG_LONGPRESS_KEY); + removeMessages(MSG_LONGPRESS_SHIFT_KEY); } public void cancelKeyTimers() { cancelKeyRepeatTimer(); - cancelLongPressTimer(); + cancelLongPressTimers(); } public void cancelAllMessages() { @@ -393,15 +418,15 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } } - public LatinKeyboardBaseView(Context context, AttributeSet attrs) { + public BaseKeyboardView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } - public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) { + public BaseKeyboardView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView); + attrs, R.styleable.BaseKeyboardView, defStyle, R.style.BaseKeyboardView); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); int previewLayout = 0; @@ -413,63 +438,54 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx int attr = a.getIndex(i); switch (attr) { - case R.styleable.LatinKeyboardBaseView_keyBackground: + case R.styleable.BaseKeyboardView_keyBackground: mKeyBackground = a.getDrawable(attr); break; - case R.styleable.LatinKeyboardBaseView_keyHysteresisDistance: + case R.styleable.BaseKeyboardView_keyHysteresisDistance: mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_verticalCorrection: + case R.styleable.BaseKeyboardView_verticalCorrection: mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_keyPreviewLayout: + case R.styleable.BaseKeyboardView_keyPreviewLayout: previewLayout = a.getResourceId(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_keyPreviewOffset: + case R.styleable.BaseKeyboardView_keyPreviewOffset: mPreviewOffset = a.getDimensionPixelOffset(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_keyPreviewHeight: + case R.styleable.BaseKeyboardView_keyPreviewHeight: mPreviewHeight = a.getDimensionPixelSize(attr, 80); break; - case R.styleable.LatinKeyboardBaseView_keyTextSize: - mKeyTextSize = a.getDimensionPixelSize(attr, 18); + case R.styleable.BaseKeyboardView_keyLetterSize: + mKeyLetterSize = a.getDimensionPixelSize(attr, 18); break; - case R.styleable.LatinKeyboardBaseView_keyTextColor: + case R.styleable.BaseKeyboardView_keyTextColor: mKeyTextColor = a.getColor(attr, 0xFF000000); break; - case R.styleable.LatinKeyboardBaseView_labelTextSize: + case R.styleable.BaseKeyboardView_keyTextColorDisabled: + mKeyTextColorDisabled = a.getColor(attr, 0xFF000000); + break; + case R.styleable.BaseKeyboardView_labelTextSize: mLabelTextSize = a.getDimensionPixelSize(attr, 14); break; - case R.styleable.LatinKeyboardBaseView_popupLayout: + case R.styleable.BaseKeyboardView_popupLayout: mPopupLayout = a.getResourceId(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_shadowColor: + case R.styleable.BaseKeyboardView_shadowColor: mShadowColor = a.getColor(attr, 0); break; - case R.styleable.LatinKeyboardBaseView_shadowRadius: + case R.styleable.BaseKeyboardView_shadowRadius: mShadowRadius = a.getFloat(attr, 0f); break; // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) - case R.styleable.LatinKeyboardBaseView_backgroundDimAmount: + case R.styleable.BaseKeyboardView_backgroundDimAmount: mBackgroundDimAmount = a.getFloat(attr, 0.5f); break; - //case android.R.styleable. - case R.styleable.LatinKeyboardBaseView_keyTextStyle: - int textStyle = a.getInt(attr, 0); - switch (textStyle) { - case 0: - mKeyTextStyle = Typeface.DEFAULT; - break; - case 1: - mKeyTextStyle = Typeface.DEFAULT_BOLD; - break; - default: - mKeyTextStyle = Typeface.defaultFromStyle(textStyle); - break; - } + case R.styleable.BaseKeyboardView_keyLetterStyle: + mKeyLetterStyle = Typeface.defaultFromStyle(a.getInt(attr, Typeface.NORMAL)); break; - case R.styleable.LatinKeyboardBaseView_symbolColorScheme: - mSymbolColorScheme = a.getInt(attr, 0); + case R.styleable.BaseKeyboardView_colorScheme: + mColorScheme = a.getInt(attr, COLOR_SCHEME_WHITE); break; } } @@ -489,6 +505,8 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation); mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); + mKeyLabelHorizontalPadding = (int)res.getDimension( + R.dimen.key_label_horizontal_alignment_padding); mMiniKeyboardParent = this; mMiniKeyboardPopup = new PopupWindow(context); @@ -575,11 +593,11 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx /** * Attaches a keyboard to this view. The keyboard can be switched at any time and the * view will re-layout itself to accommodate the keyboard. - * @see Keyboard + * @see BaseKeyboard * @see #getKeyboard() * @param keyboard the keyboard to display in this view */ - public void setKeyboard(Keyboard keyboard) { + protected void setKeyboard(BaseKeyboard keyboard) { if (mKeyboard != null) { dismissKeyPreview(); } @@ -590,24 +608,23 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx LatinImeLogger.onSetKeyboard(keyboard); mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection); - mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap); for (PointerTracker tracker : mPointerTrackers) { - tracker.setKeyboard(mKeys, mKeyHysteresisDistance); + tracker.setKeyboard(keyboard, mKeys, mKeyHysteresisDistance); } requestLayout(); // Hint to reallocate the buffer if the size changed mKeyboardChanged = true; invalidateAllKeys(); - computeProximityThreshold(keyboard); + computeProximityThreshold(keyboard, mKeys); mMiniKeyboardCache.clear(); } /** * Returns the current keyboard being displayed by this view. * @return the currently attached keyboard - * @see #setKeyboard(Keyboard) + * @see #setKeyboard(BaseKeyboard) */ - public Keyboard getKeyboard() { + protected BaseKeyboard getKeyboard() { return mKeyboard; } @@ -620,34 +637,6 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } /** - * Sets the state of the shift key of the keyboard, if any. - * @param shifted whether or not to enable the state of the shift key - * @return true if the shift key state changed, false if there was no change - */ - public boolean setShifted(boolean shifted) { - if (mKeyboard != null) { - if (mKeyboard.setShifted(shifted)) { - // The whole keyboard probably needs to be redrawn - invalidateAllKeys(); - return true; - } - } - return false; - } - - /** - * Returns the state of the shift key of the keyboard, if any. - * @return true if the shift is in a pressed state, false otherwise. If there is - * no shift key on the keyboard or there is no keyboard attached, it returns false. - */ - public boolean isShifted() { - if (mKeyboard != null) { - return mKeyboard.isShifted(); - } - return false; - } - - /** * Enables or disables the key feedback popup. This is a popup that shows a magnified * version of the depressed key. By default the preview is enabled. * @param previewEnabled whether or not to enable the key feedback popup @@ -666,8 +655,8 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx return mShowPreview; } - public int getSymbolColorScheme() { - return mSymbolColorScheme; + public int getColorScheme() { + return mColorScheme; } public void setPopupParent(View v) { @@ -698,7 +687,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } protected CharSequence adjustCase(CharSequence label) { - if (mKeyboard.isShifted() && label != null && label.length() < 3 + if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3 && Character.isLowerCase(label.charAt(0))) { label = label.toString().toUpperCase(); } @@ -722,23 +711,27 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } /** - * Compute the average distance between adjacent keys (horizontally and vertically) - * and square it to get the proximity threshold. We use a square here and in computing - * the touch distance from a key's center to avoid taking a square root. + * Compute the most common key width and use it as proximity key detection threshold. * @param keyboard + * @param keys */ - private void computeProximityThreshold(Keyboard keyboard) { - if (keyboard == null) return; - final Key[] keys = mKeys; - if (keys == null) return; - int length = keys.length; - int dimensionSum = 0; - for (int i = 0; i < length; i++) { - Key key = keys[i]; - dimensionSum += Math.min(key.width, key.height + mKeyboardVerticalGap) + key.gap; + private void computeProximityThreshold(BaseKeyboard keyboard, Key[] keys) { + if (keyboard == null || keys == null || keys.length == 0) return; + final HashMap<Integer, Integer> histogram = new HashMap<Integer, Integer>(); + int maxCount = 0; + int mostCommonWidth = 0; + for (Key key : keys) { + final Integer width = key.width + key.gap; + Integer count = histogram.get(width); + if (count == null) + count = 0; + histogram.put(width, ++count); + if (count > maxCount) { + maxCount = count; + mostCommonWidth = width; + } } - if (dimensionSum < 0 || length == 0) return; - mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length)); + mKeyDetector.setProximityThreshold(mostCommonWidth); } @Override @@ -783,8 +776,9 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx final int kbdPaddingTop = getPaddingTop(); final Key[] keys = mKeys; final Key invalidKey = mInvalidatedKey; + final boolean isManualTemporaryUpperCase = (mKeyboard instanceof LatinKeyboard + && ((LatinKeyboard)mKeyboard).isManualTemporaryUpperCase()); - paint.setColor(mKeyTextColor); boolean drawSingleKey = false; if (invalidKey != null && canvas.getClipBounds(clipRegion)) { // TODO we should use Rect.inset and Rect.contains here. @@ -816,68 +810,112 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); keyBackground.draw(canvas); - boolean shouldDrawIcon = true; + final int rowHeight = padding.top + key.height; + // Draw key label if (label != null) { // For characters, use large font. For labels like "Done", use small font. - final int labelSize; - if (label.length() > 1 && key.codes.length < 2) { - labelSize = mLabelTextSize; - paint.setTypeface(Typeface.DEFAULT_BOLD); + final int labelSize = getLabelSizeAndSetPaint(label, key, paint); + final int labelCharHeight = getLabelCharHeight(labelSize, paint); + + // Vertical label text alignment. + final float baseline; + if ((key.labelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) { + baseline = key.height - + + labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR; + if (DEBUG_SHOW_ALIGN) + drawHorizontalLine(canvas, (int)baseline, key.width, 0xc0008000, + new Paint()); + } else { // Align center + final float centerY = (key.height + padding.top - padding.bottom) / 2; + baseline = centerY + + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER; + } + // Horizontal label text alignment + final int positionX; + if ((key.labelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { + positionX = mKeyLabelHorizontalPadding + padding.left; + paint.setTextAlign(Align.LEFT); + if (DEBUG_SHOW_ALIGN) + drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint()); + } else if ((key.labelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { + positionX = key.width - mKeyLabelHorizontalPadding - padding.right; + paint.setTextAlign(Align.RIGHT); + if (DEBUG_SHOW_ALIGN) + drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint()); } else { - labelSize = mKeyTextSize; - paint.setTypeface(mKeyTextStyle); + positionX = (key.width + padding.left - padding.right) / 2; + paint.setTextAlign(Align.CENTER); + if (DEBUG_SHOW_ALIGN && label.length() > 1) + drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint()); } - paint.setTextSize(labelSize); - - Integer labelHeightValue = mTextHeightCache.get(labelSize); - final int labelHeight; - if (labelHeightValue != null) { - labelHeight = labelHeightValue; + if (key.manualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) { + paint.setColor(mKeyTextColorDisabled); } else { - Rect textBounds = new Rect(); - paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds); - labelHeight = textBounds.height(); - mTextHeightCache.put(labelSize, labelHeight); + paint.setColor(mKeyTextColor); } - - // Draw a drop shadow for the text + // Set a drop shadow for the text paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); - final int centerX = (key.width + padding.left - padding.right) / 2; - final int centerY = (key.height + padding.top - padding.bottom) / 2; - final float baseline = centerY - + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR; - canvas.drawText(label, centerX, baseline, paint); + canvas.drawText(label, positionX, baseline, paint); // Turn off drop shadow paint.setShadowLayer(0, 0, 0, 0); - - // Usually don't draw icon if label is not null, but we draw icon for the number - // hint and popup hint. - shouldDrawIcon = shouldDrawLabelAndIcon(key); } - if (key.icon != null && shouldDrawIcon) { - // Special handing for the upper-right number hint icons - final int drawableWidth; - final int drawableHeight; + // Draw key icon + if (key.label == null && key.icon != null) { + final int drawableWidth = key.icon.getIntrinsicWidth(); + final int drawableHeight = key.icon.getIntrinsicHeight(); final int drawableX; - final int drawableY; - if (shouldDrawIconFully(key)) { - drawableWidth = key.width; - drawableHeight = key.height; - drawableX = 0; - drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL; - } else { - drawableWidth = key.icon.getIntrinsicWidth(); - drawableHeight = key.icon.getIntrinsicHeight(); + final int drawableY = ( + key.height + padding.top - padding.bottom - drawableHeight) / 2; + if ((key.labelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) { + drawableX = padding.left + mKeyLabelHorizontalPadding; + if (DEBUG_SHOW_ALIGN) + drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint()); + } else if ((key.labelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) { + drawableX = key.width - padding.right - mKeyLabelHorizontalPadding + - drawableWidth; + if (DEBUG_SHOW_ALIGN) + drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight, + 0xc0808000, new Paint()); + } else { // Align center drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2; - drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2; + if (DEBUG_SHOW_ALIGN) + drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight, + 0xc0008080, new Paint()); } - canvas.translate(drawableX, drawableY); - key.icon.setBounds(0, 0, drawableWidth, drawableHeight); - key.icon.draw(canvas); - canvas.translate(-drawableX, -drawableY); + drawIcon(canvas, key.icon, drawableX, drawableY, drawableWidth, drawableHeight); + if (DEBUG_SHOW_ALIGN) + drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, + 0x80c00000, new Paint()); + } + if (key.hintIcon != null) { + final int drawableWidth = key.width; + final int drawableHeight = key.height; + final int drawableX = 0; + final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL; + Drawable icon = (isManualTemporaryUpperCase + && key.manualTemporaryUpperCaseHintIcon != null) + ? key.manualTemporaryUpperCaseHintIcon : key.hintIcon; + drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight); + if (DEBUG_SHOW_ALIGN) + drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight, + 0x80c0c000, new Paint()); } canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); } + + if (DEBUG_KEYBOARD_GRID) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setStrokeWidth(1.0f); + p.setColor(0x800000c0); + int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) / mKeyboard.GRID_WIDTH; + int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) / mKeyboard.GRID_HEIGHT; + for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++) + canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p); + for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++) + canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p); + } + mInvalidatedKey = null; // Overlay a dark rectangle to dim the keyboard if (mMiniKeyboard != null) { @@ -908,27 +946,95 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx mDirtyRect.setEmpty(); } + private int getLabelSizeAndSetPaint(CharSequence label, Key key, Paint paint) { + // For characters, use large font. For labels like "Done", use small font. + final int labelSize; + final Typeface labelStyle; + if (label.length() > 1 && key.codes.length < 2) { + labelSize = mLabelTextSize; + if ((key.labelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) { + labelStyle = Typeface.DEFAULT; + } else { + labelStyle = Typeface.DEFAULT_BOLD; + } + } else { + labelSize = mKeyLetterSize; + labelStyle = mKeyLetterStyle; + } + paint.setTextSize(labelSize); + paint.setTypeface(labelStyle); + return labelSize; + } + + private int getLabelCharHeight(int labelSize, Paint paint) { + Integer labelHeightValue = mTextHeightCache.get(labelSize); + final int labelCharHeight; + if (labelHeightValue != null) { + labelCharHeight = labelHeightValue; + } else { + Rect textBounds = new Rect(); + paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds); + labelCharHeight = textBounds.height(); + mTextHeightCache.put(labelSize, labelCharHeight); + } + return labelCharHeight; + } + + private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, + int height) { + canvas.translate(x, y); + icon.setBounds(0, 0, width, height); + icon.draw(canvas); + canvas.translate(-x, -y); + } + + private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(1.0f); + paint.setColor(color); + canvas.drawLine(0, y, w, y, paint); + } + + private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(1.0f); + paint.setColor(color); + canvas.drawLine(x, 0, x, h, paint); + } + + private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color, + Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(1.0f); + paint.setColor(color); + canvas.translate(x, y); + canvas.drawRect(0, 0, w, h, paint); + canvas.translate(-x, -y); + } + + public void setForeground(boolean foreground) { + mInForeground = foreground; + } + // TODO: clean up this method. private void dismissKeyPreview() { for (PointerTracker tracker : mPointerTrackers) - tracker.updateKey(NOT_A_KEY); + tracker.releaseKey(); showPreview(NOT_A_KEY, null); } public void showPreview(int keyIndex, PointerTracker tracker) { int oldKeyIndex = mOldPreviewKeyIndex; mOldPreviewKeyIndex = keyIndex; - final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard) - && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled(); // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show // the space key preview and 3) pointer moves off the space key to other letter key, we // should hide the preview of the previous key. final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) - || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex); + || (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER + && SubtypeSwitcher.getInstance().needsToDisplayLanguage() + && (tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex))); // If key changed and preview is on or the key is space (language switch is enabled) - if (oldKeyIndex != keyIndex - && (mShowPreview - || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) { + if (oldKeyIndex != keyIndex && (mShowPreview || (hidePreviewOrShowSpaceKeyPreview))) { if (keyIndex == NOT_A_KEY) { mHandler.cancelPopupPreview(); mHandler.dismissPreview(mDelayAfterPreview); @@ -938,25 +1044,30 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx } } + // TODO Must fix popup preview on xlarge layout private void showKey(final int keyIndex, PointerTracker tracker) { Key key = tracker.getKey(keyIndex); - if (key == null) + // If keyIndex is invalid or IME is already closed, we must not show key preview. + // Trying to show preview PopupWindow while root window is closed causes + // WindowManager.BadTokenException. + if (key == null || !mInForeground) return; - // Should not draw hint icon in key preview - if (key.icon != null && !shouldDrawLabelAndIcon(key)) { - mPreviewText.setCompoundDrawables(null, null, null, - key.iconPreview != null ? key.iconPreview : key.icon); - mPreviewText.setText(null); - } else { + // What we show as preview should match what we show on key top in onBufferDraw(). + if (key.label != null) { + // TODO Should take care of temporaryShiftLabel here. mPreviewText.setCompoundDrawables(null, null, null, null); mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); if (key.label.length() > 1 && key.codes.length < 2) { - mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize); mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); } else { mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); - mPreviewText.setTypeface(mKeyTextStyle); + mPreviewText.setTypeface(mKeyLetterStyle); } + } else { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); } mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); @@ -1000,13 +1111,18 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx popupPreviewY += popupHeight; } - if (mPreviewPopup.isShowing()) { - mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); - } else { - mPreviewPopup.setWidth(popupWidth); - mPreviewPopup.setHeight(popupHeight); - mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, - popupPreviewX, popupPreviewY); + try { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); + } else { + mPreviewPopup.setWidth(popupWidth); + mPreviewPopup.setHeight(popupHeight); + mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, + popupPreviewX, popupPreviewY); + } + } catch (WindowManager.BadTokenException e) { + // Swallow the exception which will be happened when IME is already closed. + Log.w(TAG, "LatinIME is already closed when tried showing key preview."); } // Record popup preview position to display mini-keyboard later at the same positon mPopupPreviewDisplayedY = popupPreviewY; @@ -1029,7 +1145,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only * one key is changing it's content. Any changes that affect the position or size of the key * may not be honored. - * @param key key in the attached {@link Keyboard}. + * @param key key in the attached {@link BaseKeyboard}. * @see #invalidateAllKeys */ public void invalidateKey(Key key) { @@ -1064,6 +1180,12 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx return result; } + private void onLongPressShiftKey(PointerTracker tracker) { + tracker.setAlreadyProcessed(); + mPointerQueue.remove(tracker); + mKeyboardActionListener.onKey(LatinKeyboardView.KEYCODE_CAPSLOCK, null, 0, 0); + } + private View inflateMiniKeyboardContainer(Key popupKey) { int popupKeyboardId = popupKey.popupResId; LayoutInflater inflater = (LayoutInflater)getContext().getSystemService( @@ -1072,8 +1194,8 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx if (container == null) throw new NullPointerException(); - LatinKeyboardBaseView miniKeyboard = - (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); + BaseKeyboardView miniKeyboard = + (BaseKeyboardView)container.findViewById(R.id.BaseKeyboardView); miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { public void onKey(int primaryCode, int[] keyCodes, int x, int y) { mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y); @@ -1109,12 +1231,12 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx // Remove gesture detector on mini-keyboard miniKeyboard.mGestureDetector = null; - Keyboard keyboard; + BaseKeyboard keyboard; if (popupKey.popupCharacters != null) { - keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters, + keyboard = new BaseKeyboard(getContext(), popupKeyboardId, popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); } else { - keyboard = new Keyboard(getContext(), popupKeyboardId); + keyboard = new BaseKeyboard(getContext(), popupKeyboardId); } miniKeyboard.setKeyboard(keyboard); miniKeyboard.setPopupParent(this); @@ -1134,7 +1256,8 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx // and bottom edge flags on. // When you want to use one row mini-keyboard from xml file, make sure that the row has // both top and bottom edge flags set. - return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; + return (edgeFlags & BaseKeyboard.EDGE_TOP) != 0 + && (edgeFlags & BaseKeyboard.EDGE_BOTTOM) != 0; } /** @@ -1156,7 +1279,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx container = inflateMiniKeyboardContainer(popupKey); mMiniKeyboardCache.put(popupKey, container); } - mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); + mMiniKeyboard = (BaseKeyboardView)container.findViewById(R.id.BaseKeyboardView); if (mWindowOffset == null) { mWindowOffset = new int[2]; getLocationInWindow(mWindowOffset); @@ -1201,7 +1324,12 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0]; mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; mMiniKeyboard.setPopupOffset(adjustedX, y); - mMiniKeyboard.setShifted(isShifted()); + // TODO: change the below line to use getLatinKeyboard() instead of getKeyboard() + BaseKeyboard baseMiniKeyboard = mMiniKeyboard.getKeyboard(); + if (baseMiniKeyboard != null && baseMiniKeyboard.setShifted(mKeyboard == null + ? false : mKeyboard.isShiftedOrShiftLocked())) { + mMiniKeyboard.invalidateAllKeys(); + } // Mini keyboard needs no pop-up key preview displayed. mMiniKeyboard.setPreviewEnabled(false); mMiniKeyboardPopup.setContentView(container); @@ -1228,29 +1356,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx return false; } - private boolean shouldDrawIconFully(Key key) { - return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key) - || LatinKeyboard.hasPuncOrSmileysPopup(key); - } - - private boolean shouldDrawLabelAndIcon(Key key) { - return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key) - || LatinKeyboard.hasPuncOrSmileysPopup(key); - } - - private boolean isLatinF1Key(Key key) { - return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key); - } - - private boolean isNonMicLatinF1Key(Key key) { - return isLatinF1Key(key) && key.label != null; - } - - private static boolean isNumberAtEdgeOfPopupChars(Key key) { - return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key); - } - - /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) { + private static boolean isNumberAtLeftmostPopupChar(Key key) { if (key.popupCharacters != null && key.popupCharacters.length() > 0 && isAsciiDigit(key.popupCharacters.charAt(0))) { return true; @@ -1258,14 +1364,6 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx return false; } - /* package */ static boolean isNumberAtRightmostPopupChar(Key key) { - if (key.popupCharacters != null && key.popupCharacters.length() > 0 - && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) { - return true; - } - return false; - } - private static boolean isAsciiDigit(char c) { return (c < 0x80) && Character.isDigit(c); } @@ -1285,7 +1383,7 @@ public class LatinKeyboardBaseView extends View implements PointerTracker.UIProx final PointerTracker tracker = new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); if (keys != null) - tracker.setKeyboard(keys, mKeyHysteresisDistance); + tracker.setKeyboard(mKeyboard, keys, mKeyHysteresisDistance); if (listener != null) tracker.setOnKeyboardActionListener(listener); pointers.add(tracker); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index d0e143dd0..95c773855 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -16,16 +16,16 @@ package com.android.inputmethod.latin; -import java.io.InputStream; +import android.content.Context; +import android.util.Log; + import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.Channels; import java.util.Arrays; -import android.content.Context; -import android.util.Log; - /** * Implements a static, compacted, binary dictionary of standard words. */ @@ -45,7 +45,6 @@ public class BinaryDictionary extends Dictionary { private static final int MAX_BIGRAMS = 60; private static final int TYPED_LETTER_MULTIPLIER = 2; - private static final boolean ENABLE_MISSED_CHARACTERS = true; private int mDicTypeId; private int mNativeDict; @@ -199,27 +198,11 @@ public class BinaryDictionary extends Dictionary { Arrays.fill(mOutputChars, (char) 0); Arrays.fill(mFrequencies, 0); - int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, - mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1, + int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, + mFrequencies, MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1, nextLettersFrequencies, nextLettersFrequencies != null ? nextLettersFrequencies.length : 0); - // If there aren't sufficient suggestions, search for words by allowing wild cards at - // the different character positions. This feature is not ready for prime-time as we need - // to figure out the best ranking for such words compared to proximity corrections and - // completions. - if (ENABLE_MISSED_CHARACTERS && count < 5) { - for (int skip = 0; skip < codesSize; skip++) { - int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, - mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip, - null, 0); - count = Math.max(count, tempCount); - if (tempCount > 0) break; - } - } - for (int j = 0; j < count; j++) { if (mFrequencies[j] < 1) break; int start = j * MAX_WORD_LENGTH; diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index 68f288925..68f288925 100755..100644 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index e954c0818..2a7767ddd 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -16,11 +16,11 @@ package com.android.inputmethod.latin; -import java.util.LinkedList; - import android.content.Context; import android.os.AsyncTask; +import java.util.LinkedList; + /** * Base class for an in-memory dictionary that can grow dynamically and can * be searched for suggestions and valid words. diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index e811a2cdd..fc517aeb7 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -16,11 +16,6 @@ package com.android.inputmethod.latin; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.Configuration; @@ -32,12 +27,18 @@ import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.text.TextUtils; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + public class InputLanguageSelection extends PreferenceActivity { + private SharedPreferences mPrefs; private String mSelectedLanguages; private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>(); private static final String[] BLACKLIST_LANGUAGES = { - "ko", "ja", "zh", "el" + "ko", "ja", "zh", "el", "zz" }; private static class Loc implements Comparable<Object> { @@ -66,15 +67,15 @@ public class InputLanguageSelection extends PreferenceActivity { super.onCreate(icicle); addPreferencesFromResource(R.xml.language_prefs); // Get the settings preferences - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mSelectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mSelectedLanguages = mPrefs.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); String[] languageList = mSelectedLanguages.split(","); mAvailableLanguages = getUniqueLocales(); PreferenceGroup parent = getPreferenceScreen(); for (int i = 0; i < mAvailableLanguages.size(); i++) { CheckBoxPreference pref = new CheckBoxPreference(this); Locale locale = mAvailableLanguages.get(i).locale; - pref.setTitle(LanguageSwitcher.toTitleCase(locale.getDisplayName(locale))); + pref.setTitle(SubtypeSwitcher.getFullDisplayName(locale, true)); boolean checked = isLocaleIn(locale, languageList); pref.setChecked(checked); if (hasDictionary(locale)) { @@ -140,8 +141,7 @@ public class InputLanguageSelection extends PreferenceActivity { } } if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - Editor editor = sp.edit(); + Editor editor = mPrefs.edit(); editor.putString(LatinIME.PREF_SELECTED_LANGUAGES, checkedLanguages); SharedPreferencesCompat.apply(editor); } @@ -167,7 +167,7 @@ public class InputLanguageSelection extends PreferenceActivity { if (finalSize == 0) { preprocess[finalSize++] = - new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(l)), l); + new Loc(SubtypeSwitcher.getFullDisplayName(l, true), l); } else { // check previous entry: // same lang and a country -> upgrade to full name and @@ -175,15 +175,15 @@ public class InputLanguageSelection extends PreferenceActivity { // diff lang -> insert ours with lang-only name if (preprocess[finalSize-1].locale.getLanguage().equals( language)) { - preprocess[finalSize-1].label = LanguageSwitcher.toTitleCase( - preprocess[finalSize-1].locale.getDisplayName()); + preprocess[finalSize-1].label = SubtypeSwitcher.getFullDisplayName( + preprocess[finalSize-1].locale, false); preprocess[finalSize++] = - new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName()), l); + new Loc(SubtypeSwitcher.getFullDisplayName(l, false), l); } else { String displayName; if (s.equals("zz_ZZ")) { } else { - displayName = LanguageSwitcher.toTitleCase(l.getDisplayName(l)); + displayName = SubtypeSwitcher.getFullDisplayName(l, true); preprocess[finalSize++] = new Loc(displayName, l); } } diff --git a/java/src/com/android/inputmethod/latin/KeyDetector.java b/java/src/com/android/inputmethod/latin/KeyDetector.java index 76fe1200e..600a12fe5 100644 --- a/java/src/com/android/inputmethod/latin/KeyDetector.java +++ b/java/src/com/android/inputmethod/latin/KeyDetector.java @@ -16,14 +16,13 @@ package com.android.inputmethod.latin; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; +import com.android.inputmethod.latin.BaseKeyboard.Key; import java.util.Arrays; import java.util.List; abstract class KeyDetector { - protected Keyboard mKeyboard; + protected BaseKeyboard mKeyboard; private Key[] mKeys; @@ -35,7 +34,7 @@ abstract class KeyDetector { protected int mProximityThresholdSquare; - public Key[] setKeyboard(Keyboard keyboard, float correctionX, float correctionY) { + public Key[] setKeyboard(BaseKeyboard keyboard, float correctionX, float correctionY) { if (keyboard == null) throw new NullPointerException(); mCorrectionX = (int)correctionX; @@ -85,7 +84,7 @@ abstract class KeyDetector { */ public int[] newCodeArray() { int[] codes = new int[getMaxNearbyKeys()]; - Arrays.fill(codes, LatinKeyboardBaseView.NOT_A_KEY); + Arrays.fill(codes, BaseKeyboardView.NOT_A_KEY); return codes; } diff --git a/java/src/com/android/inputmethod/latin/KeyStyles.java b/java/src/com/android/inputmethod/latin/KeyStyles.java new file mode 100644 index 000000000..fceede7c3 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/KeyStyles.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.BaseKeyboardParser.ParseException; + +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.util.TypedValue; + +import java.util.HashMap; +import java.util.StringTokenizer; + +public class KeyStyles { + private static final String TAG = "KeyStyles"; + + private final HashMap<String, DeclaredKeyStyle> mStyles = + new HashMap<String, DeclaredKeyStyle>(); + private static final KeyStyle EMPTY_KEY_STYLE = new EmptyKeyStyle(); + + public interface KeyStyle { + public int[] getIntArray(TypedArray a, int index); + public Drawable getDrawable(TypedArray a, int index); + public CharSequence getText(TypedArray a, int index); + public int getResourceId(TypedArray a, int index, int defaultValue); + public int getInt(TypedArray a, int index, int defaultValue); + public int getFlag(TypedArray a, int index, int defaultValue); + public boolean getBoolean(TypedArray a, int index, boolean defaultValue); + } + + public static class EmptyKeyStyle implements KeyStyle { + private EmptyKeyStyle() { + } + + public int[] getIntArray(TypedArray a, int index) { + return parseIntArray(a, index); + } + + public Drawable getDrawable(TypedArray a, int index) { + return a.getDrawable(index); + } + + public CharSequence getText(TypedArray a, int index) { + return a.getText(index); + } + + public int getResourceId(TypedArray a, int index, int defaultValue) { + return a.getResourceId(index, defaultValue); + } + + public int getInt(TypedArray a, int index, int defaultValue) { + return a.getInt(index, defaultValue); + } + + public int getFlag(TypedArray a, int index, int defaultValue) { + return a.getInt(index, defaultValue); + } + + public boolean getBoolean(TypedArray a, int index, boolean defaultValue) { + return a.getBoolean(index, defaultValue); + } + + protected static int[] parseIntArray(TypedArray a, int index) { + TypedValue v = new TypedValue(); + a.getValue(index, v); + if (v.type == TypedValue.TYPE_INT_DEC || v.type == TypedValue.TYPE_INT_HEX) { + return new int[] { v.data }; + } else if (v.type == TypedValue.TYPE_STRING) { + return parseCSV(v.string.toString()); + } else { + return null; + } + } + + private static int[] parseCSV(String value) { + int count = 0; + int lastIndex = 0; + if (value.length() > 0) { + count++; + while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { + count++; + } + } + int[] values = new int[count]; + count = 0; + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + try { + values[count++] = Integer.parseInt(st.nextToken()); + } catch (NumberFormatException nfe) { + Log.w(TAG, "Error parsing integer CSV " + value); + } + } + return values; + } + } + + public static class DeclaredKeyStyle extends EmptyKeyStyle { + private final HashMap<Integer, Object> mAttributes = new HashMap<Integer, Object>(); + + @Override + public int[] getIntArray(TypedArray a, int index) { + return a.hasValue(index) + ? super.getIntArray(a, index) : (int[])mAttributes.get(index); + } + + @Override + public Drawable getDrawable(TypedArray a, int index) { + return a.hasValue(index) + ? super.getDrawable(a, index) : (Drawable)mAttributes.get(index); + } + + @Override + public CharSequence getText(TypedArray a, int index) { + return a.hasValue(index) + ? super.getText(a, index) : (CharSequence)mAttributes.get(index); + } + + @Override + public int getResourceId(TypedArray a, int index, int defaultValue) { + final Integer value = (Integer)mAttributes.get(index); + return super.getResourceId(a, index, (value != null) ? value : defaultValue); + } + + @Override + public int getFlag(TypedArray a, int index, int defaultValue) { + final Integer value = (Integer)mAttributes.get(index); + return super.getFlag(a, index, defaultValue) | (value != null ? value : 0); + } + + @Override + public boolean getBoolean(TypedArray a, int index, boolean defaultValue) { + final Boolean value = (Boolean)mAttributes.get(index); + return super.getBoolean(a, index, (value != null) ? value : defaultValue); + } + + private DeclaredKeyStyle() { + super(); + } + + private void parseKeyStyleAttributes(TypedArray a) { + // TODO: Currently not all Key attributes can be declared as style. + readIntArray(a, R.styleable.BaseKeyboard_Key_codes); + readText(a, R.styleable.BaseKeyboard_Key_keyLabel); + readFlag(a, R.styleable.BaseKeyboard_Key_keyLabelOption); + readText(a, R.styleable.BaseKeyboard_Key_keyOutputText); + readDrawable(a, R.styleable.BaseKeyboard_Key_keyIcon); + readDrawable(a, R.styleable.BaseKeyboard_Key_iconPreview); + readDrawable(a, R.styleable.BaseKeyboard_Key_keyHintIcon); + readDrawable(a, R.styleable.BaseKeyboard_Key_shiftedIcon); + readResourceId(a, R.styleable.BaseKeyboard_Key_popupKeyboard); + readBoolean(a, R.styleable.BaseKeyboard_Key_isModifier); + readBoolean(a, R.styleable.BaseKeyboard_Key_isSticky); + readBoolean(a, R.styleable.BaseKeyboard_Key_isRepeatable); + } + + private void readDrawable(TypedArray a, int index) { + if (a.hasValue(index)) + mAttributes.put(index, a.getDrawable(index)); + } + + private void readText(TypedArray a, int index) { + if (a.hasValue(index)) + mAttributes.put(index, a.getText(index)); + } + + private void readResourceId(TypedArray a, int index) { + if (a.hasValue(index)) + mAttributes.put(index, a.getResourceId(index, 0)); + } + + private void readFlag(TypedArray a, int index) { + final Integer value = (Integer)mAttributes.get(index); + if (a.hasValue(index)) + mAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0)); + } + + private void readBoolean(TypedArray a, int index) { + if (a.hasValue(index)) + mAttributes.put(index, a.getBoolean(index, false)); + } + + private void readIntArray(TypedArray a, int index) { + if (a.hasValue(index)) { + final int[] value = parseIntArray(a, index); + if (value != null) + mAttributes.put(index, value); + } + } + + private void addParent(DeclaredKeyStyle parentStyle) { + mAttributes.putAll(parentStyle.mAttributes); + } + } + + public void parseKeyStyleAttributes(TypedArray a, TypedArray keyAttrs, + XmlResourceParser parser) { + String styleName = a.getString(R.styleable.BaseKeyboard_KeyStyle_styleName); + if (mStyles.containsKey(styleName)) + throw new ParseException("duplicate key style declared: " + styleName, parser); + + final DeclaredKeyStyle style = new DeclaredKeyStyle(); + if (a.hasValue(R.styleable.BaseKeyboard_KeyStyle_parentStyle)) { + String parentStyle = a.getString( + R.styleable.BaseKeyboard_KeyStyle_parentStyle); + final DeclaredKeyStyle parent = mStyles.get(parentStyle); + if (parent == null) + throw new ParseException("Unknown parentStyle " + parent, parser); + style.addParent(parent); + } + style.parseKeyStyleAttributes(keyAttrs); + mStyles.put(styleName, style); + } + + public KeyStyle getKeyStyle(String styleName) { + return mStyles.get(styleName); + } + + public KeyStyle getEmptyKeyStyle() { + return EMPTY_KEY_STYLE; + } +} diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java index a7b695eb3..00b6f0a43 100644 --- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -16,11 +16,13 @@ package com.android.inputmethod.latin; +import android.content.Context; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.content.res.Resources; -import android.preference.PreferenceManager; +import android.util.Log; import android.view.InflateException; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import java.lang.ref.SoftReference; import java.util.Arrays; @@ -28,187 +30,169 @@ import java.util.HashMap; import java.util.Locale; public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener { - - public static final int MODE_NONE = 0; - public static final int MODE_TEXT = 1; - public static final int MODE_SYMBOLS = 2; - public static final int MODE_PHONE = 3; - public static final int MODE_URL = 4; - public static final int MODE_EMAIL = 5; - public static final int MODE_IM = 6; - public static final int MODE_WEB = 7; - - // Main keyboard layouts without the settings key - public static final int KEYBOARDMODE_NORMAL = R.id.mode_normal; - public static final int KEYBOARDMODE_URL = R.id.mode_url; - public static final int KEYBOARDMODE_EMAIL = R.id.mode_email; - public static final int KEYBOARDMODE_IM = R.id.mode_im; - public static final int KEYBOARDMODE_WEB = R.id.mode_webentry; - // Main keyboard layouts with the settings key - public static final int KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY = - R.id.mode_normal_with_settings_key; - public static final int KEYBOARDMODE_URL_WITH_SETTINGS_KEY = - R.id.mode_url_with_settings_key; - public static final int KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY = - R.id.mode_email_with_settings_key; - public static final int KEYBOARDMODE_IM_WITH_SETTINGS_KEY = - R.id.mode_im_with_settings_key; - public static final int KEYBOARDMODE_WEB_WITH_SETTINGS_KEY = - R.id.mode_webentry_with_settings_key; - - // Symbols keyboard layout without the settings key - public static final int KEYBOARDMODE_SYMBOLS = R.id.mode_symbols; - // Symbols keyboard layout with the settings key - public static final int KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY = - R.id.mode_symbols_with_settings_key; - - public static final String DEFAULT_LAYOUT_ID = "4"; + private static final String TAG = "KeyboardSwitcher"; + private static final boolean DEBUG = false; + public static final boolean DEBUG_STATE = false; + + public static final int MODE_TEXT = 0; + public static final int MODE_URL = 1; + public static final int MODE_EMAIL = 2; + public static final int MODE_IM = 3; + public static final int MODE_WEB = 4; + public static final int MODE_PHONE = 5; + public static final int MODE_NUMBER = 6; + + // Changing DEFAULT_LAYOUT_ID also requires prefs_for_debug.xml to be matched with. + public static final String DEFAULT_LAYOUT_ID = "5"; public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902"; private static final int[] THEMES = new int [] { - R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal, - R.layout.input_stone_bold, R.layout.input_gingerbread}; - - // Ids for each characters' color in the keyboard - private static final int CHAR_THEME_COLOR_WHITE = 0; - private static final int CHAR_THEME_COLOR_BLACK = 1; - - // Tables which contains resource ids for each character theme color - private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black}; - private static final int[] KBD_PHONE_SYMBOLS = new int[] { - R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black}; - private static final int[] KBD_SYMBOLS = new int[] { - R.xml.kbd_symbols, R.xml.kbd_symbols_black}; - private static final int[] KBD_SYMBOLS_SHIFT = new int[] { - R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black}; - private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black}; + R.layout.input_basic, + R.layout.input_basic_highcontrast, + R.layout.input_stone_normal, + R.layout.input_stone_bold, + R.layout.input_gingerbread, + R.layout.input_honeycomb, // DEFAULT_LAYOUT_ID + }; private static final int SYMBOLS_MODE_STATE_NONE = 0; private static final int SYMBOLS_MODE_STATE_BEGIN = 1; private static final int SYMBOLS_MODE_STATE_SYMBOL = 2; + private SubtypeSwitcher mSubtypeSwitcher; + private SharedPreferences mPrefs; + private LatinKeyboardView mInputView; - private static final int[] ALPHABET_MODES = { - KEYBOARDMODE_NORMAL, - KEYBOARDMODE_URL, - KEYBOARDMODE_EMAIL, - KEYBOARDMODE_IM, - KEYBOARDMODE_WEB, - KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY, - KEYBOARDMODE_URL_WITH_SETTINGS_KEY, - KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY, - KEYBOARDMODE_IM_WITH_SETTINGS_KEY, - KEYBOARDMODE_WEB_WITH_SETTINGS_KEY }; - - private final LatinIME mInputMethodService; + private LatinIME mInputMethodService; + + private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift"); + private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol"); private KeyboardId mSymbolsId; private KeyboardId mSymbolsShiftedId; private KeyboardId mCurrentId; - private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboards; + private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache = + new HashMap<KeyboardId, SoftReference<LatinKeyboard>>(); - private int mMode = MODE_NONE; /** One of the MODE_XXX values */ + private int mMode = MODE_TEXT; /* default value */ private int mImeOptions; private boolean mIsSymbols; /** mIsAutoCompletionActive indicates that auto completed word will be input instead of * what user actually typed. */ private boolean mIsAutoCompletionActive; - private boolean mHasVoice; - private boolean mVoiceOnPrimary; - private boolean mPreferSymbols; + private boolean mVoiceKeyEnabled; + private boolean mVoiceButtonOnPrimary; private int mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; // Indicates whether or not we have the settings key private boolean mHasSettingsKey; private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto; - private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW = R.string.settings_key_mode_always_show; + private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW = + R.string.settings_key_mode_always_show; // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to // in the source code now. // Default is SETTINGS_KEY_MODE_AUTO. private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO; - private int mLastDisplayWidth; - private LanguageSwitcher mLanguageSwitcher; - private Locale mInputLocale; - private int mLayoutId; - public KeyboardSwitcher(LatinIME ims) { - mInputMethodService = ims; - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims); - mLayoutId = Integer.valueOf(prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID)); - updateSettingsKeyState(prefs); - prefs.registerOnSharedPreferenceChangeListener(this); + private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); - mKeyboards = new HashMap<KeyboardId, SoftReference<LatinKeyboard>>(); - mSymbolsId = makeSymbolsId(false); - mSymbolsShiftedId = makeSymbolsShiftedId(false); + public static KeyboardSwitcher getInstance() { + return sInstance; } - /** - * Sets the input locale, when there are multiple locales for input. - * If no locale switching is required, then the locale should be set to null. - * @param locale the current input locale, or null for default locale with no locale - * button. - */ - public void setLanguageSwitcher(LanguageSwitcher languageSwitcher) { - mLanguageSwitcher = languageSwitcher; - mInputLocale = mLanguageSwitcher.getInputLocale(); + private KeyboardSwitcher() { } - private KeyboardId makeSymbolsId(boolean hasVoice) { - return new KeyboardId(KBD_SYMBOLS[getCharColorId()], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - } + public static void init(LatinIME ims, SharedPreferences prefs) { + sInstance.mInputMethodService = ims; + sInstance.mPrefs = prefs; + sInstance.mSubtypeSwitcher = SubtypeSwitcher.getInstance(); - private KeyboardId makeSymbolsShiftedId(boolean hasVoice) { - return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); + sInstance.mLayoutId = Integer.valueOf( + prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID)); + prefs.registerOnSharedPreferenceChangeListener(sInstance); } - public void makeKeyboards(boolean forceCreate) { - mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary); - mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary); - - if (forceCreate) mKeyboards.clear(); - // Configuration change is coming after the keyboard gets recreated. So don't rely on that. - // If keyboards have already been made, check if we have a screen width change and - // create the keyboard layouts again at the correct orientation - int displayWidth = mInputMethodService.getMaxWidth(); - if (displayWidth == mLastDisplayWidth) return; - mLastDisplayWidth = displayWidth; - if (!forceCreate) mKeyboards.clear(); + private void makeSymbolsKeyboardIds() { + final Locale locale = mSubtypeSwitcher.getInputLocale(); + final int orientation = mInputMethodService.getResources().getConfiguration().orientation; + final int mode = mMode; + final int colorScheme = getColorScheme(); + final boolean hasSettingsKey = mHasSettingsKey; + final boolean voiceKeyEnabled = mVoiceKeyEnabled; + final boolean hasVoiceKey = voiceKeyEnabled && !mVoiceButtonOnPrimary; + final int imeOptions = mImeOptions; + // Note: This comment is only applied for phone number keyboard layout. + // On non-xlarge device, "@integer/key_switch_alpha_symbol" key code is used to switch + // between "phone keyboard" and "phone symbols keyboard". But on xlarge device, + // "@integer/key_shift" key code is used for that purpose in order to properly display + // "more" and "locked more" key labels. To achieve these behavior, we should initialize + // mSymbolsId and mSymbolsShiftedId to "phone keyboard" and "phone symbols keyboard" + // respectively here for xlarge device's layout switching. + mSymbolsId = new KeyboardId(locale, orientation, mode, + mode == MODE_PHONE ? R.xml.kbd_phone : R.xml.kbd_symbols, + colorScheme, hasSettingsKey, voiceKeyEnabled, hasVoiceKey, imeOptions, true); + mSymbolsShiftedId = new KeyboardId(locale, orientation, mode, + mode == MODE_PHONE ? R.xml.kbd_phone_symbols : R.xml.kbd_symbols_shift, + colorScheme, hasSettingsKey, voiceKeyEnabled, hasVoiceKey, imeOptions, true); } /** * Represents the parameters necessary to construct a new LatinKeyboard, * which also serve as a unique identifier for each keyboard type. */ - private static class KeyboardId { - // TODO: should have locale and portrait/landscape orientation? - public final int mXml; - public final int mKeyboardMode; /** A KEYBOARDMODE_XXX value */ + public static class KeyboardId { + public final Locale mLocale; + public final int mOrientation; + public final int mMode; + public final int mXmlId; + public final int mColorScheme; + public final boolean mHasSettingsKey; + public final boolean mVoiceKeyEnabled; + public final boolean mHasVoiceKey; + public final int mImeOptions; public final boolean mEnableShiftLock; - public final boolean mHasVoice; private final int mHashCode; - public KeyboardId(int xml, int mode, boolean enableShiftLock, boolean hasVoice) { - this.mXml = xml; - this.mKeyboardMode = mode; + public KeyboardId(Locale locale, int orientation, int mode, + int xmlId, int colorScheme, boolean hasSettingsKey, boolean voiceKeyEnabled, + boolean hasVoiceKey, int imeOptions, boolean enableShiftLock) { + this.mLocale = locale; + this.mOrientation = orientation; + this.mMode = mode; + this.mXmlId = xmlId; + this.mColorScheme = colorScheme; + this.mHasSettingsKey = hasSettingsKey; + this.mVoiceKeyEnabled = voiceKeyEnabled; + this.mHasVoiceKey = hasVoiceKey; + // We are interested only in IME_MASK_ACTION enum value and IME_FLAG_NO_ENTER_ACTION. + this.mImeOptions = imeOptions + & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION); this.mEnableShiftLock = enableShiftLock; - this.mHasVoice = hasVoice; this.mHashCode = Arrays.hashCode(new Object[] { - xml, mode, enableShiftLock, hasVoice + locale, + orientation, + mode, + xmlId, + colorScheme, + hasSettingsKey, + voiceKeyEnabled, + hasVoiceKey, + imeOptions, + enableShiftLock, }); } - public KeyboardId(int xml, boolean hasVoice) { - this(xml, 0, false, hasVoice); + public int getXmlId() { + return mXmlId; + } + + public boolean isAlphabetMode() { + return mXmlId == R.xml.kbd_qwerty; } @Override @@ -217,202 +201,438 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha } private boolean equals(KeyboardId other) { - return other.mXml == this.mXml - && other.mKeyboardMode == this.mKeyboardMode - && other.mEnableShiftLock == this.mEnableShiftLock - && other.mHasVoice == this.mHasVoice; + return other.mLocale.equals(this.mLocale) + && other.mOrientation == this.mOrientation + && other.mMode == this.mMode + && other.mXmlId == this.mXmlId + && other.mColorScheme == this.mColorScheme + && other.mHasSettingsKey == this.mHasSettingsKey + && other.mVoiceKeyEnabled == this.mVoiceKeyEnabled + && other.mHasVoiceKey == this.mHasVoiceKey + && other.mImeOptions == this.mImeOptions + && other.mEnableShiftLock == this.mEnableShiftLock; } @Override public int hashCode() { return mHashCode; } - } - public void setVoiceMode(boolean enableVoice, boolean voiceOnPrimary) { - if (enableVoice != mHasVoice || voiceOnPrimary != mVoiceOnPrimary) { - mKeyboards.clear(); + @Override + public String toString() { + return String.format("[%s %s %5s imeOptions=0x%08x xml=0x%08x %s%s%s%s%s]", + mLocale, + (mOrientation == 1 ? "port" : "land"), + modeName(mMode), + mImeOptions, + mXmlId, + colorSchemeName(mColorScheme), + (mHasSettingsKey ? " hasSettingsKey" : ""), + (mVoiceKeyEnabled ? " voiceKeyEnabled" : ""), + (mHasVoiceKey ? " hasVoiceKey" : ""), + (mEnableShiftLock ? " enableShiftLock" : "")); + } + + private static String modeName(int mode) { + switch (mode) { + case MODE_TEXT: return "text"; + case MODE_URL: return "url"; + case MODE_EMAIL: return "email"; + case MODE_IM: return "im"; + case MODE_WEB: return "web"; + case MODE_PHONE: return "phone"; + case MODE_NUMBER: return "number"; + } + return null; + } + + private static String colorSchemeName(int colorScheme) { + switch (colorScheme) { + case BaseKeyboardView.COLOR_SCHEME_WHITE: return "white"; + case BaseKeyboardView.COLOR_SCHEME_BLACK: return "black"; + } + return null; } - mHasVoice = enableVoice; - mVoiceOnPrimary = voiceOnPrimary; - setKeyboardMode(mMode, mImeOptions, mHasVoice, mIsSymbols); } - private boolean hasVoiceButton(boolean isSymbols) { - return mHasVoice && (isSymbols != mVoiceOnPrimary); + private boolean hasVoiceKey(boolean isSymbols) { + return mVoiceKeyEnabled && (isSymbols != mVoiceButtonOnPrimary); } - public void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) { + public void loadKeyboard(int mode, int imeOptions, boolean voiceKeyEnabled, + boolean voiceButtonOnPrimary) { mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; - mPreferSymbols = mode == MODE_SYMBOLS; - if (mode == MODE_SYMBOLS) { - mode = MODE_TEXT; - } try { - setKeyboardMode(mode, imeOptions, enableVoice, mPreferSymbols); + loadKeyboardInternal(mode, imeOptions, voiceKeyEnabled, voiceButtonOnPrimary, + false); } catch (RuntimeException e) { - LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e); + Log.w(TAG, e); + LatinImeLogger.logOnException(mode + "," + imeOptions, e); } } - private void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) { + private void loadKeyboardInternal(int mode, int imeOptions, boolean voiceButtonEnabled, + boolean voiceButtonOnPrimary, boolean isSymbols) { if (mInputView == null) return; + mInputView.setPreviewEnabled(mInputMethodService.getPopupOn()); + mMode = mode; mImeOptions = imeOptions; - if (enableVoice != mHasVoice) { - // TODO clean up this unnecessary recursive call. - setVoiceMode(enableVoice, mVoiceOnPrimary); - } + mVoiceKeyEnabled = voiceButtonEnabled; + mVoiceButtonOnPrimary = voiceButtonOnPrimary; mIsSymbols = isSymbols; + // Update the settings key state because number of enabled IMEs could have been changed + mHasSettingsKey = getSettingsKeyMode(mPrefs, mInputMethodService); + makeSymbolsKeyboardIds(); - mInputView.setPreviewEnabled(mInputMethodService.getPopupOn()); KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols); - LatinKeyboard keyboard = null; - keyboard = getKeyboard(id); - - if (mode == MODE_PHONE) { - mInputView.setPhoneKeyboard(keyboard); - } + LatinKeyboard keyboard = getKeyboard(id); mCurrentId = id; mInputView.setKeyboard(keyboard); - keyboard.setShifted(false); - keyboard.setShiftLocked(keyboard.isShiftLocked()); - keyboard.setImeOptions(mInputMethodService.getResources(), mMode, imeOptions); - keyboard.setColorOfSymbolIcons(mIsAutoCompletionActive, isBlackSym()); - // Update the settings key state because number of enabled IMEs could have been changed - updateSettingsKeyState(PreferenceManager.getDefaultSharedPreferences(mInputMethodService)); } private LatinKeyboard getKeyboard(KeyboardId id) { - SoftReference<LatinKeyboard> ref = mKeyboards.get(id); + final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id); LatinKeyboard keyboard = (ref == null) ? null : ref.get(); if (keyboard == null) { - Resources orig = mInputMethodService.getResources(); - Configuration conf = orig.getConfiguration(); - Locale saveLocale = conf.locale; - conf.locale = mInputLocale; - orig.updateConfiguration(conf, null); - keyboard = new LatinKeyboard(mInputMethodService, id.mXml, id.mKeyboardMode); - keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols - || id.mXml == R.xml.kbd_symbols_black), mHasVoice); - keyboard.setLanguageSwitcher(mLanguageSwitcher, mIsAutoCompletionActive, isBlackSym()); + final Locale savedLocale = mSubtypeSwitcher.changeSystemLocale( + mSubtypeSwitcher.getInputLocale()); + + keyboard = new LatinKeyboard(mInputMethodService, id); if (id.mEnableShiftLock) { keyboard.enableShiftLock(); } - mKeyboards.put(id, new SoftReference<LatinKeyboard>(keyboard)); - conf.locale = saveLocale; - orig.updateConfiguration(conf, null); + mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard)); + if (DEBUG) + Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": " + + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); + + mSubtypeSwitcher.changeSystemLocale(savedLocale); + } else if (DEBUG) { + Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT id=" + id); } + + keyboard.onAutoCompletionStateChanged(mIsAutoCompletionActive); + keyboard.setShifted(false); return keyboard; } private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { - boolean hasVoice = hasVoiceButton(isSymbols); - int charColorId = getCharColorId(); - // TODO: generalize for any KeyboardId - int keyboardRowsResId = KBD_QWERTY[charColorId]; + final boolean hasVoiceKey = hasVoiceKey(isSymbols); + final int charColorId = getColorScheme(); + final int xmlId; + final boolean enableShiftLock; + if (isSymbols) { if (mode == MODE_PHONE) { - return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId], hasVoice); + xmlId = R.xml.kbd_phone_symbols; + } else if (mode == MODE_NUMBER) { + // Note: MODE_NUMBER keyboard layout has no "switch alpha symbol" key. + xmlId = R.xml.kbd_number; } else { - return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); + xmlId = R.xml.kbd_symbols; + } + enableShiftLock = false; + } else { + if (mode == MODE_PHONE) { + xmlId = R.xml.kbd_phone; + enableShiftLock = false; + } else if (mode == MODE_NUMBER) { + xmlId = R.xml.kbd_number; + enableShiftLock = false; + } else { + xmlId = R.xml.kbd_qwerty; + enableShiftLock = true; } } - switch (mode) { - case MODE_NONE: - LatinImeLogger.logOnWarning( - "getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols); - /* fall through */ - case MODE_TEXT: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY : KEYBOARDMODE_NORMAL, - true, hasVoice); - case MODE_SYMBOLS: - return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - case MODE_PHONE: - return new KeyboardId(KBD_PHONE[charColorId], hasVoice); - case MODE_URL: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_URL_WITH_SETTINGS_KEY : KEYBOARDMODE_URL, true, hasVoice); - case MODE_EMAIL: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY : KEYBOARDMODE_EMAIL, true, hasVoice); - case MODE_IM: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_IM_WITH_SETTINGS_KEY : KEYBOARDMODE_IM, true, hasVoice); - case MODE_WEB: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_WEB_WITH_SETTINGS_KEY : KEYBOARDMODE_WEB, true, hasVoice); - } - return null; + final int orientation = mInputMethodService.getResources().getConfiguration().orientation; + final Locale locale = mSubtypeSwitcher.getInputLocale(); + return new KeyboardId(locale, orientation, mode, xmlId, charColorId, + mHasSettingsKey, mVoiceKeyEnabled, hasVoiceKey, imeOptions, enableShiftLock); } public int getKeyboardMode() { return mMode; } - + public boolean isAlphabetMode() { - if (mCurrentId == null) { - return false; + return mCurrentId != null && mCurrentId.isAlphabetMode(); + } + + public boolean isInputViewShown() { + return mInputView != null && mInputView.isShown(); + } + + public boolean isKeyboardAvailable() { + if (mInputView != null) + return mInputView.getLatinKeyboard() != null; + return false; + } + + private LatinKeyboard getLatinKeyboard() { + if (mInputView != null) + return mInputView.getLatinKeyboard(); + return null; + } + + public void setPreferredLetters(int[] frequencies) { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + latinKeyboard.setPreferredLetters(frequencies); + } + + public void keyReleased() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + latinKeyboard.keyReleased(); + } + + public boolean isShiftedOrShiftLocked() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + return latinKeyboard.isShiftedOrShiftLocked(); + return false; + } + + public boolean isShiftLocked() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + return latinKeyboard.isShiftLocked(); + return false; + } + + public boolean isAutomaticTemporaryUpperCase() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + return latinKeyboard.isAutomaticTemporaryUpperCase(); + return false; + } + + public boolean isManualTemporaryUpperCase() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) + return latinKeyboard.isManualTemporaryUpperCase(); + return false; + } + + private void setManualTemporaryUpperCase(boolean shifted) { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) { + // On non-distinct multi touch panel device, we should also turn off the shift locked + // state when shift key is pressed to go to normal mode. + // On the other hand, on distinct multi touch panel device, turning off the shift locked + // state with shift key pressing is handled by onReleaseShift(). + if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) { + latinKeyboard.setShiftLocked(false); + } + if (latinKeyboard.setShifted(shifted)) { + mInputView.invalidateAllKeys(); + } } - int currentMode = mCurrentId.mKeyboardMode; - for (Integer mode : ALPHABET_MODES) { - if (currentMode == mode) { - return true; + } + + private void setShiftLocked(boolean shiftLocked) { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) { + mInputView.invalidateAllKeys(); + } + } + + /** + * Toggle keyboard shift state triggered by user touch event. + */ + public void toggleShift() { + mInputMethodService.mHandler.cancelUpdateShiftState(); + if (DEBUG_STATE) + Log.d(TAG, "toggleShift:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + mShiftKeyState); + if (isAlphabetMode()) { + setManualTemporaryUpperCase(!isShiftedOrShiftLocked()); + } else { + toggleShiftInSymbol(); + } + } + + public void toggleCapsLock() { + mInputMethodService.mHandler.cancelUpdateShiftState(); + if (DEBUG_STATE) + Log.d(TAG, "toggleCapsLock:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + mShiftKeyState); + if (isAlphabetMode()) { + if (isShiftLocked()) { + // Shift key is long pressed while caps lock state, we will toggle back to normal + // state. And mark as if shift key is released. + setShiftLocked(false); + mShiftKeyState.onRelease(); + } else { + setShiftLocked(true); } } - return false; } - public void setShifted(boolean shifted) { - if (mInputView != null) { - mInputView.setShifted(shifted); + private void setAutomaticTemporaryUpperCase() { + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null) { + latinKeyboard.setAutomaticTemporaryUpperCase(); + mInputView.invalidateAllKeys(); } } - public void setShiftLocked(boolean shiftLocked) { - if (mInputView != null) { - mInputView.setShiftLocked(shiftLocked); + /** + * Update keyboard shift state triggered by connected EditText status change. + */ + public void updateShiftState() { + final ShiftKeyState shiftKeyState = mShiftKeyState; + if (DEBUG_STATE) + Log.d(TAG, "updateShiftState:" + + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState() + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + shiftKeyState); + if (isAlphabetMode()) { + if (!isShiftLocked() && !shiftKeyState.isIgnoring()) { + if (shiftKeyState.isReleasing() && mInputMethodService.getCurrentAutoCapsState()) { + // Only when shift key is releasing, automatic temporary upper case will be set. + setAutomaticTemporaryUpperCase(); + } else { + setManualTemporaryUpperCase(shiftKeyState.isMomentary()); + } + } + } else { + // In symbol keyboard mode, we should clear shift key state because only alphabet + // keyboard has shift key. + shiftKeyState.onRelease(); } } - public void toggleShift() { + public void changeKeyboardMode() { + if (DEBUG_STATE) + Log.d(TAG, "changeKeyboardMode:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + mShiftKeyState); + toggleKeyboardMode(); + if (isShiftLocked() && isAlphabetMode()) + setShiftLocked(true); + updateShiftState(); + } + + public void onPressShift() { + if (!isKeyboardAvailable()) + return; + ShiftKeyState shiftKeyState = mShiftKeyState; + if (DEBUG_STATE) + Log.d(TAG, "onPressShift:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + shiftKeyState); + if (isAlphabetMode()) { + if (isShiftLocked()) { + // Shift key is pressed while caps lock state, we will treat this state as shifted + // caps lock state and mark as if shift key pressed while normal state. + shiftKeyState.onPress(); + setManualTemporaryUpperCase(true); + } else if (isAutomaticTemporaryUpperCase()) { + // Shift key is pressed while automatic temporary upper case, we have to move to + // manual temporary upper case. + shiftKeyState.onPress(); + setManualTemporaryUpperCase(true); + } else if (isShiftedOrShiftLocked()) { + // In manual upper case state, we just record shift key has been pressing while + // shifted state. + shiftKeyState.onPressOnShifted(); + } else { + // In base layout, chording or manual temporary upper case mode is started. + shiftKeyState.onPress(); + toggleShift(); + } + } else { + // In symbol mode, just toggle symbol and symbol more keyboard. + shiftKeyState.onPress(); + toggleShift(); + } + } + + public void onReleaseShift() { + if (!isKeyboardAvailable()) + return; + ShiftKeyState shiftKeyState = mShiftKeyState; + if (DEBUG_STATE) + Log.d(TAG, "onReleaseShift:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + shiftKeyState); + if (isAlphabetMode()) { + if (shiftKeyState.isMomentary()) { + // After chording input while normal state. + toggleShift(); + } else if (isShiftLocked() && !shiftKeyState.isIgnoring()) { + // Shift has been pressed without chording while caps lock state. + toggleCapsLock(); + } else if (isShiftedOrShiftLocked() && shiftKeyState.isPressingOnShifted()) { + // Shift has been pressed without chording while shifted state. + toggleShift(); + } + } + shiftKeyState.onRelease(); + } + + public void onPressSymbol() { + if (DEBUG_STATE) + Log.d(TAG, "onReleaseShift:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " symbolKeyState=" + mSymbolKeyState); + changeKeyboardMode(); + mSymbolKeyState.onPress(); + } + + public void onReleaseSymbol() { + if (DEBUG_STATE) + Log.d(TAG, "onReleaseShift:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " symbolKeyState=" + mSymbolKeyState); + if (mSymbolKeyState.isMomentary()) + changeKeyboardMode(); + mSymbolKeyState.onRelease(); + } + + public void onOtherKeyPressed() { + if (DEBUG_STATE) + Log.d(TAG, "onOtherKeyPressed:" + + " keyboard=" + getLatinKeyboard().getKeyboardShiftState() + + " shiftKeyState=" + mShiftKeyState + + " symbolKeyState=" + mSymbolKeyState); + mShiftKeyState.onOtherKeyPressed(); + mSymbolKeyState.onOtherKeyPressed(); + } + + private void toggleShiftInSymbol() { if (isAlphabetMode()) return; + final LatinKeyboard keyboard; if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) { - LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId); mCurrentId = mSymbolsShiftedId; - mInputView.setKeyboard(symbolsShiftedKeyboard); + keyboard = getKeyboard(mCurrentId); // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To // enable the indicator, we need to call enableShiftLock() and setShiftLocked(true). // Thus we can keep the ALT key's Key.on value true while LatinKey.onRelease() is // called. - symbolsShiftedKeyboard.enableShiftLock(); - symbolsShiftedKeyboard.setShiftLocked(true); - symbolsShiftedKeyboard.setImeOptions(mInputMethodService.getResources(), - mMode, mImeOptions); + keyboard.setShiftLocked(true); } else { - LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId); mCurrentId = mSymbolsId; - mInputView.setKeyboard(symbolsKeyboard); + keyboard = getKeyboard(mCurrentId); // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the // indicator, we need to call enableShiftLock() and setShiftLocked(false). - symbolsKeyboard.enableShiftLock(); - symbolsKeyboard.setShifted(false); - symbolsKeyboard.setImeOptions(mInputMethodService.getResources(), mMode, mImeOptions); + keyboard.setShifted(false); } + mInputView.setKeyboard(keyboard); } - public void toggleSymbols() { - setKeyboardMode(mMode, mImeOptions, mHasVoice, !mIsSymbols); - if (mIsSymbols && !mPreferSymbols) { + private void toggleKeyboardMode() { + loadKeyboardInternal(mMode, mImeOptions, mVoiceKeyEnabled, mVoiceButtonOnPrimary, + !mIsSymbols); + if (mIsSymbols) { mSymbolsModeState = SYMBOLS_MODE_STATE_BEGIN; } else { mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; @@ -425,33 +645,34 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha /** * Updates state machine to figure out when to automatically switch back to alpha mode. - * Returns true if the keyboard needs to switch back */ - public boolean onKey(int key) { - // Switch back to alpha mode if user types one or more non-space/enter characters - // followed by a space/enter + public void onKey(int key) { + // Switch back to alpha mode if user types one or more non-space/enter + // characters followed by a space/enter switch (mSymbolsModeState) { - case SYMBOLS_MODE_STATE_BEGIN: - if (key != LatinIME.KEYCODE_SPACE && key != LatinIME.KEYCODE_ENTER && key > 0) { - mSymbolsModeState = SYMBOLS_MODE_STATE_SYMBOL; - } - break; - case SYMBOLS_MODE_STATE_SYMBOL: - if (key == LatinIME.KEYCODE_ENTER || key == LatinIME.KEYCODE_SPACE) return true; - break; + case SYMBOLS_MODE_STATE_BEGIN: + if (key != LatinIME.KEYCODE_SPACE && key != LatinIME.KEYCODE_ENTER && key > 0) { + mSymbolsModeState = SYMBOLS_MODE_STATE_SYMBOL; + } + break; + case SYMBOLS_MODE_STATE_SYMBOL: + if (key == LatinIME.KEYCODE_ENTER || key == LatinIME.KEYCODE_SPACE) { + changeKeyboardMode(); + } + break; } - return false; } public LatinKeyboardView getInputView() { return mInputView; } - public void recreateInputView() { - changeLatinKeyboardView(mLayoutId, true); + public LatinKeyboardView onCreateInputView() { + createInputViewInternal(mLayoutId, true); + return mInputView; } - private void changeLatinKeyboardView(int newLayout, boolean forceReset) { + private void createInputViewInternal(int newLayout, boolean forceReset) { if (mLayoutId != newLayout || mInputView == null || forceReset) { if (mInputView != null) { mInputView.closing(); @@ -468,9 +689,11 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha ).inflate(THEMES[newLayout], null); tryGC = false; } catch (OutOfMemoryError e) { + Log.w(TAG, "load keyboard failed: " + e); tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( mLayoutId + "," + newLayout, e); } catch (InflateException e) { + Log.w(TAG, "load keyboard failed: " + e); tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( mLayoutId + "," + newLayout, e); } @@ -478,38 +701,37 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha mInputView.setOnKeyboardActionListener(mInputMethodService); mLayoutId = newLayout; } + } + + private void postSetInputView() { mInputMethodService.mHandler.post(new Runnable() { + @Override public void run() { if (mInputView != null) { mInputMethodService.setInputView(mInputView); } mInputMethodService.updateInputViewShown(); - }}); + } + }); } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PREF_KEYBOARD_LAYOUT.equals(key)) { - changeLatinKeyboardView( - Integer.valueOf(sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)), false); + final int layoutId = Integer.valueOf( + sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)); + createInputViewInternal(layoutId, false); + postSetInputView(); } else if (LatinIMESettings.PREF_SETTINGS_KEY.equals(key)) { - updateSettingsKeyState(sharedPreferences); - recreateInputView(); - } - } - - public boolean isBlackSym () { - if (mInputView != null && mInputView.getSymbolColorScheme() == 1) { - return true; + mHasSettingsKey = getSettingsKeyMode(sharedPreferences, mInputMethodService); + createInputViewInternal(mLayoutId, true); + postSetInputView(); } - return false; } - private int getCharColorId () { - if (isBlackSym()) { - return CHAR_THEME_COLOR_BLACK; - } else { - return CHAR_THEME_COLOR_WHITE; - } + private int getColorScheme() { + return (mInputView != null) + ? mInputView.getColorScheme() : BaseKeyboardView.COLOR_SCHEME_WHITE; } public void onAutoCompletionStateChanged(boolean isAutoCompletion) { @@ -521,18 +743,23 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha } } - private void updateSettingsKeyState(SharedPreferences prefs) { - Resources resources = mInputMethodService.getResources(); - final String settingsKeyMode = prefs.getString(LatinIMESettings.PREF_SETTINGS_KEY, - resources.getString(DEFAULT_SETTINGS_KEY_MODE)); - // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or - // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system - if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW)) - || (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO)) - && LatinIMEUtil.hasMultipleEnabledIMEs(mInputMethodService))) { - mHasSettingsKey = true; - } else { - mHasSettingsKey = false; + private static boolean getSettingsKeyMode(SharedPreferences prefs, Context context) { + Resources resources = context.getResources(); + final boolean showSettingsKeyOption = resources.getBoolean( + R.bool.config_enable_show_settings_key_option); + if (showSettingsKeyOption) { + final String settingsKeyMode = prefs.getString(LatinIMESettings.PREF_SETTINGS_KEY, + resources.getString(DEFAULT_SETTINGS_KEY_MODE)); + // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or + // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system + if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW)) + || (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO)) + && LatinIMEUtil.hasMultipleEnabledIMEsOrSubtypes( + ((InputMethodManager) context.getSystemService( + Context.INPUT_METHOD_SERVICE))))) { + return true; + } } + return false; } } diff --git a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java index 7b5c30491..fc725bf1b 100644 --- a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java +++ b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java @@ -16,21 +16,21 @@ package com.android.inputmethod.latin; -import java.util.Locale; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; -import android.preference.PreferenceManager; import android.text.TextUtils; +import java.util.ArrayList; +import java.util.Locale; + /** * Keeps track of list of selected input languages and the current * input language that the user has selected. */ public class LanguageSwitcher { - private Locale[] mLocales; - private LatinIME mIme; + private final ArrayList<Locale> mLocales = new ArrayList<Locale>(); + private final LatinIME mIme; private String[] mSelectedLanguageArray; private String mSelectedLanguages; private int mCurrentIndex = 0; @@ -40,15 +40,10 @@ public class LanguageSwitcher { public LanguageSwitcher(LatinIME ime) { mIme = ime; - mLocales = new Locale[0]; - } - - public Locale[] getLocales() { - return mLocales; } public int getLocaleCount() { - return mLocales.length; + return mLocales.size(); } /** @@ -61,10 +56,10 @@ public class LanguageSwitcher { String currentLanguage = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null); if (selectedLanguages == null || selectedLanguages.length() < 1) { loadDefaults(); - if (mLocales.length == 0) { + if (mLocales.size() == 0) { return false; } - mLocales = new Locale[0]; + mLocales.clear(); return true; } if (selectedLanguages.equals(mSelectedLanguages)) { @@ -77,7 +72,7 @@ public class LanguageSwitcher { if (currentLanguage != null) { // Find the index mCurrentIndex = 0; - for (int i = 0; i < mLocales.length; i++) { + for (int i = 0; i < mLocales.size(); i++) { if (mSelectedLanguageArray[i].equals(currentLanguage)) { mCurrentIndex = i; break; @@ -96,11 +91,11 @@ public class LanguageSwitcher { } private void constructLocales() { - mLocales = new Locale[mSelectedLanguageArray.length]; - for (int i = 0; i < mLocales.length; i++) { - final String lang = mSelectedLanguageArray[i]; - mLocales[i] = new Locale(lang.substring(0, 2), + mLocales.clear(); + for (final String lang : mSelectedLanguageArray) { + final Locale locale = new Locale(lang.substring(0, 2), lang.length() > 4 ? lang.substring(3, 5) : ""); + mLocales.add(locale); } } @@ -129,7 +124,17 @@ public class LanguageSwitcher { public Locale getInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - return mLocales[mCurrentIndex]; + return mLocales.get(mCurrentIndex); + } + + private int nextLocaleIndex() { + final int size = mLocales.size(); + return (mCurrentIndex + 1) % size; + } + + private int prevLocaleIndex() { + final int size = mLocales.size(); + return (mCurrentIndex - 1 + size) % size; } /** @@ -139,8 +144,7 @@ public class LanguageSwitcher { */ public Locale getNextInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - - return mLocales[(mCurrentIndex + 1) % mLocales.length]; + return mLocales.get(nextLocaleIndex()); } /** @@ -166,8 +170,7 @@ public class LanguageSwitcher { */ public Locale getPrevInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - - return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length]; + return mLocales.get(prevLocaleIndex()); } public void reset() { @@ -175,27 +178,16 @@ public class LanguageSwitcher { } public void next() { - mCurrentIndex++; - if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around + mCurrentIndex = nextLocaleIndex(); } public void prev() { - mCurrentIndex--; - if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around + mCurrentIndex = prevLocaleIndex(); } - public void persist() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme); - Editor editor = sp.edit(); + public void persist(SharedPreferences prefs) { + Editor editor = prefs.edit(); editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage()); SharedPreferencesCompat.apply(editor); } - - static String toTitleCase(String s) { - if (s.length() == 0) { - return s; - } - - return Character.toUpperCase(s.charAt(0)) + s.substring(1); - } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index b6fee1170..36c77efaf 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -17,9 +17,7 @@ package com.android.inputmethod.latin; import com.android.inputmethod.latin.LatinIMEUtil.RingCharBuffer; -import com.android.inputmethod.voice.FieldContext; -import com.android.inputmethod.voice.SettingsUtil; -import com.android.inputmethod.voice.VoiceInput; +import com.android.inputmethod.voice.VoiceIMEConnector; import org.xmlpull.v1.XmlPullParserException; @@ -34,16 +32,14 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.inputmethodservice.InputMethodService; -import android.inputmethodservice.Keyboard; import android.media.AudioManager; import android.os.Debug; import android.os.Handler; import android.os.Message; import android.os.SystemClock; +import android.os.Vibrator; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; -import android.speech.SpeechRecognizer; -import android.text.ClipboardManager; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; @@ -51,7 +47,6 @@ import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; @@ -63,128 +58,98 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; import android.widget.LinearLayout; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; +import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.Map; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService - implements LatinKeyboardBaseView.OnKeyboardActionListener, - VoiceInput.UiListener, - SharedPreferences.OnSharedPreferenceChangeListener { + implements BaseKeyboardView.OnKeyboardActionListener, + SharedPreferences.OnSharedPreferenceChangeListener, + Tutorial.TutorialListener { private static final String TAG = "LatinIME"; private static final boolean PERF_DEBUG = false; - static final boolean DEBUG = false; - static final boolean TRACE = false; - static final boolean VOICE_INSTALLED = true; - static final boolean ENABLE_VOICE_BUTTON = true; + private static final boolean DEBUG = false; + private static final boolean TRACE = false; - private static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String PREF_SOUND_ON = "sound_on"; private static final String PREF_POPUP_ON = "popup_on"; private static final String PREF_AUTO_CAP = "auto_cap"; private static final String PREF_QUICK_FIXES = "quick_fixes"; - private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; - private static final String PREF_AUTO_COMPLETE = "auto_complete"; - //private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; - private static final String PREF_VOICE_MODE = "voice_mode"; - - // Whether or not the user has used voice input before (and thus, whether to show the - // first-run warning dialog or not). - private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input"; - - // Whether or not the user has used voice input from an unsupported locale UI before. - // For example, the user has a Chinese UI but activates voice input. - private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE = - "has_used_voice_input_unsupported_locale"; - - // A list of locales which are supported by default for voice input, unless we get a - // different list from Gservices. - public static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = - "en " + - "en_US " + - "en_GB " + - "en_AU " + - "en_CA " + - "en_IE " + - "en_IN " + - "en_NZ " + - "en_SG " + - "en_ZA "; - - // The private IME option used to indicate that no microphone should be shown for a - // given text field. For instance this is specified by the search dialog when the - // dialog is already showing a voice search button. - private static final String IME_OPTION_NO_MICROPHONE = "nm"; + private static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; + private static final String PREF_AUTO_COMPLETION_THRESHOLD = "auto_completion_threshold"; + private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; public static final String PREF_INPUT_LANGUAGE = "input_language"; private static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled"; - private static final int MSG_UPDATE_SUGGESTIONS = 0; - private static final int MSG_START_TUTORIAL = 1; - private static final int MSG_UPDATE_SHIFT_STATE = 2; - private static final int MSG_VOICE_RESULTS = 3; - private static final int MSG_UPDATE_OLD_SUGGESTIONS = 4; + private static final int DELAY_UPDATE_SUGGESTIONS = 180; + private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300; + private static final int DELAY_UPDATE_SHIFT_STATE = 300; + private static final int DELAY_START_TUTORIAL = 500; // How many continuous deletes at which to start deleting at a higher speed. private static final int DELETE_ACCELERATE_AT = 20; // Key events coming any faster than this are long-presses. private static final int QUICK_PRESS = 200; - static final int KEYCODE_ENTER = '\n'; - static final int KEYCODE_SPACE = ' '; - static final int KEYCODE_PERIOD = '.'; + public static final int KEYCODE_ENTER = '\n'; + public static final int KEYCODE_TAB = '\t'; + public static final int KEYCODE_SPACE = ' '; + public static final int KEYCODE_PERIOD = '.'; // Contextual menu positions private static final int POS_METHOD = 0; private static final int POS_SETTINGS = 1; - //private LatinKeyboardView mInputView; + private int mSuggestionVisibility; + private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE + = R.string.prefs_suggestion_visibility_show_value; + private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE + = R.string.prefs_suggestion_visibility_show_only_portrait_value; + private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE + = R.string.prefs_suggestion_visibility_hide_value; + + private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { + SUGGESTION_VISIBILILTY_SHOW_VALUE, + SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, + SUGGESTION_VISIBILILTY_HIDE_VALUE + }; + private LinearLayout mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; private CompletionInfo[] mCompletions; private AlertDialog mOptionsDialog; - private AlertDialog mVoiceWarningDialog; - /* package */ KeyboardSwitcher mKeyboardSwitcher; + private InputMethodManager mImm; + private KeyboardSwitcher mKeyboardSwitcher; + private SubtypeSwitcher mSubtypeSwitcher; + private VoiceIMEConnector mVoiceConnector; private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private ContactsDictionary mContactsDictionary; private AutoDictionary mAutoDictionary; - private Hints mHints; - private Resources mResources; + private SharedPreferences mPrefs; - private String mInputLocale; - private String mSystemLocale; - private LanguageSwitcher mLanguageSwitcher; - - private StringBuilder mComposing = new StringBuilder(); + private final StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); - private int mCommittedLength; - private boolean mPredicting; - private boolean mRecognizing; - private boolean mAfterVoiceInput; - private boolean mImmediatelyAfterVoiceInput; - private boolean mShowingVoiceSuggestions; - private boolean mVoiceInputHighlighted; - private boolean mEnableVoiceButton; private CharSequence mBestWord; + private boolean mPredicting; private boolean mPredictionOn; private boolean mCompletionOn; private boolean mHasDictionary; @@ -192,72 +157,49 @@ public class LatinIME extends InputMethodService private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; private boolean mReCorrectionEnabled; - // Bigram Suggestion is disabled in this version. - private final boolean mBigramSuggestionEnabled = false; + private boolean mBigramSuggestionEnabled; private boolean mAutoCorrectOn; - // TODO move this state variable outside LatinIME - private boolean mCapsLock; - private boolean mPasswordText; private boolean mVibrateOn; private boolean mSoundOn; private boolean mPopupOn; private boolean mAutoCap; private boolean mQuickFixes; - private boolean mHasUsedVoiceInput; - private boolean mHasUsedVoiceInputUnsupportedLocale; - private boolean mLocaleSupportedForVoiceInput; - private boolean mShowSuggestions; - private boolean mIsShowingHint; - private int mCorrectionMode; - private boolean mEnableVoice = true; - private boolean mVoiceOnPrimary; - private int mOrientation; - private List<CharSequence> mSuggestPuncList; + + private int mCorrectionMode; + private int mCommittedLength; + private int mOrientation; // Keep track of the last selection range to decide if we need to show word alternatives - private int mLastSelectionStart; - private int mLastSelectionEnd; + private int mLastSelectionStart; + private int mLastSelectionEnd; + private List<CharSequence> mSuggestPuncList; // Input type is such that we should not auto-correct private boolean mInputTypeNoAutoCorrect; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; - private CharSequence mJustRevertedSeparator; + private boolean mJustReverted; private int mDeleteCount; private long mLastKeyTime; - // Modifier keys state - private ModifierKeyState mShiftKeyState = new ModifierKeyState(); - private ModifierKeyState mSymbolKeyState = new ModifierKeyState(); - private Tutorial mTutorial; private AudioManager mAudioManager; // Align sound effect volume on music volume - private final float FX_VOLUME = -1.0f; + private static final float FX_VOLUME = -1.0f; private boolean mSilentMode; /* package */ String mWordSeparators; private String mSentenceSeparators; private String mSuggestPuncs; - private VoiceInput mVoiceInput; - private VoiceResults mVoiceResults = new VoiceResults(); + // TODO: Move this flag to VoiceIMEConnector private boolean mConfigurationChanging; // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; private boolean mRefreshKeyboardRequired; - // For each word, a list of potential replacements, usually from voice. - private Map<String, List<CharSequence>> mWordToSuggestions = - new HashMap<String, List<CharSequence>>(); - - private ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); - - private class VoiceResults { - List<String> candidates; - Map<String, List<CharSequence>> alternatives; - } + private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); public abstract static class WordAlternatives { protected CharSequence mChosenWord; @@ -307,56 +249,101 @@ public class LatinIME extends InputMethodService } } - /* package */ Handler mHandler = new Handler() { + /* package */ final UIHandler mHandler = new UIHandler(); + + public class UIHandler extends Handler { + private static final int MSG_UPDATE_SUGGESTIONS = 0; + private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1; + private static final int MSG_UPDATE_SHIFT_STATE = 2; + private static final int MSG_VOICE_RESULTS = 3; + private static final int MSG_START_TUTORIAL = 4; + @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_UPDATE_SUGGESTIONS: - updateSuggestions(); - break; - case MSG_UPDATE_OLD_SUGGESTIONS: - setOldSuggestions(); - break; - case MSG_START_TUTORIAL: - if (mTutorial == null) { - if (mKeyboardSwitcher.getInputView().isShown()) { - mTutorial = new Tutorial( - LatinIME.this, mKeyboardSwitcher.getInputView()); - mTutorial.start(); - } else { - // Try again soon if the view is not yet showing - sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100); - } + case MSG_UPDATE_SUGGESTIONS: + updateSuggestions(); + break; + case MSG_UPDATE_OLD_SUGGESTIONS: + setOldSuggestions(); + break; + case MSG_UPDATE_SHIFT_STATE: + mKeyboardSwitcher.updateShiftState(); + break; + case MSG_VOICE_RESULTS: + mVoiceConnector.handleVoiceResults(mKeyboardSwitcher, preferCapitalization() + || (mKeyboardSwitcher.isAlphabetMode() + && mKeyboardSwitcher.isShiftedOrShiftLocked())); + break; + case MSG_START_TUTORIAL: + if (mTutorial == null) { + if (mKeyboardSwitcher.isInputViewShown()) { + mTutorial = new Tutorial(LatinIME.this, mKeyboardSwitcher); + mTutorial.start(); + } else { + // Try again soon if the view is not yet showing + sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100); } - break; - case MSG_UPDATE_SHIFT_STATE: - updateShiftKeyState(getCurrentInputEditorInfo()); - break; - case MSG_VOICE_RESULTS: - handleVoiceResults(); - break; + } + break; } } - }; + + public void postUpdateSuggestions() { + removeMessages(MSG_UPDATE_SUGGESTIONS); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS); + } + + public void cancelUpdateSuggestions() { + removeMessages(MSG_UPDATE_SUGGESTIONS); + } + + public boolean hasPendingUpdateSuggestions() { + return hasMessages(MSG_UPDATE_SUGGESTIONS); + } + + public void postUpdateOldSuggestions() { + removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), + DELAY_UPDATE_OLD_SUGGESTIONS); + } + + public void cancelUpdateOldSuggestions() { + removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + } + + public void postUpdateShiftKeyState() { + removeMessages(MSG_UPDATE_SHIFT_STATE); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE); + } + + public void cancelUpdateShiftState() { + removeMessages(MSG_UPDATE_SHIFT_STATE); + } + + public void updateVoiceResults() { + sendMessage(obtainMessage(MSG_VOICE_RESULTS)); + } + + public void startTutorial() { + sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), DELAY_START_TUTORIAL); + } + } @Override public void onCreate() { - LatinImeLogger.init(this); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mPrefs = prefs; + LatinImeLogger.init(this, prefs); + SubtypeSwitcher.init(this, prefs); + KeyboardSwitcher.init(this, prefs); super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); mResources = getResources(); + mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); final Configuration conf = mResources.getConfiguration(); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - mLanguageSwitcher = new LanguageSwitcher(this); - mLanguageSwitcher.loadLocales(prefs); - mKeyboardSwitcher = new KeyboardSwitcher(this); - mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - mSystemLocale = conf.locale.toString(); - mLanguageSwitcher.setSystemLocale(conf.locale); - String inputLanguage = mLanguageSwitcher.getInputLanguage(); - if (inputLanguage == null) { - inputLanguage = conf.locale.toString(); - } + mSubtypeSwitcher = SubtypeSwitcher.getInstance(); + mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mReCorrectionEnabled = prefs.getBoolean(PREF_RECORRECTION_ENABLED, getResources().getBoolean(R.bool.default_recorrection_enabled)); @@ -364,10 +351,10 @@ public class LatinIME extends InputMethodService boolean tryGC = true; for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { try { - initSuggest(inputLanguage); + initSuggest(); tryGC = false; } catch (OutOfMemoryError e) { - tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e); + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); } } @@ -377,19 +364,7 @@ public class LatinIME extends InputMethodService // register to receive ringer mode changes for silent mode IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); registerReceiver(mReceiver, filter); - if (VOICE_INSTALLED) { - mVoiceInput = new VoiceInput(this, this); - mHints = new Hints(this, new Hints.Display() { - public void showHint(int viewResource) { - LayoutInflater inflater = (LayoutInflater) getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(viewResource, null); - setCandidatesView(view); - setCandidatesViewShown(true); - mIsShowingHint = true; - } - }); - } + mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); prefs.registerOnSharedPreferenceChangeListener(this); } @@ -397,7 +372,7 @@ public class LatinIME extends InputMethodService * Loads a dictionary or multiple separated dictionary * @return returns array of dictionary resource ids */ - /* package */ static int[] getDictionary(Resources res) { + public static int[] getDictionary(Resources res) { String packageName = LatinIME.class.getPackage().getName(); XmlResourceParser xrp = res.getXml(R.xml.dictionary); ArrayList<Integer> dictionaries = new ArrayList<Integer>(); @@ -432,37 +407,34 @@ public class LatinIME extends InputMethodService return dict; } - private void initSuggest(String locale) { - mInputLocale = locale; + private void initSuggest() { + updateAutoTextEnabled(); + String locale = mSubtypeSwitcher.getInputLocaleStr(); Resources orig = getResources(); - Configuration conf = orig.getConfiguration(); - Locale saveLocale = conf.locale; - conf.locale = new Locale(locale); - orig.updateConfiguration(conf, orig.getDisplayMetrics()); + Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); if (mSuggest != null) { mSuggest.close(); } - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); + final SharedPreferences prefs = mPrefs; + mQuickFixes = prefs.getBoolean(PREF_QUICK_FIXES, true); int[] dictionaries = getDictionary(orig); mSuggest = new Suggest(this, dictionaries); - updateAutoTextEnabled(saveLocale); + loadAndSetAutoCompletionThreshold(prefs); if (mUserDictionary != null) mUserDictionary.close(); - mUserDictionary = new UserDictionary(this, mInputLocale); + mUserDictionary = new UserDictionary(this, locale); if (mContactsDictionary == null) { mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); } if (mAutoDictionary != null) { mAutoDictionary.close(); } - mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO); + mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO); if (mUserBigramDictionary != null) { mUserBigramDictionary.close(); } - mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale, - Suggest.DIC_USER); + mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER); mSuggest.setUserBigramDictionary(mUserBigramDictionary); mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); @@ -471,8 +443,7 @@ public class LatinIME extends InputMethodService mWordSeparators = mResources.getString(R.string.word_separators); mSentenceSeparators = mResources.getString(R.string.sentence_separators); - conf.locale = saveLocale; - orig.updateConfiguration(conf, orig.getDisplayMetrics()); + mSubtypeSwitcher.changeSystemLocale(savedLocale); } @Override @@ -484,9 +455,7 @@ public class LatinIME extends InputMethodService mContactsDictionary.close(); } unregisterReceiver(mReceiver); - if (VOICE_INSTALLED && mVoiceInput != null) { - mVoiceInput.destroy(); - } + mVoiceConnector.destroy(); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); super.onDestroy(); @@ -494,51 +463,38 @@ public class LatinIME extends InputMethodService @Override public void onConfigurationChanged(Configuration conf) { - // If the system locale changes and is different from the saved - // locale (mSystemLocale), then reload the input locale list from the - // latin ime settings (shared prefs) and reset the input locale - // to the first one. - final String systemLocale = conf.locale.toString(); - if (!TextUtils.equals(systemLocale, mSystemLocale)) { - mSystemLocale = systemLocale; - if (mLanguageSwitcher != null) { - mLanguageSwitcher.loadLocales( - PreferenceManager.getDefaultSharedPreferences(this)); - mLanguageSwitcher.setSystemLocale(conf.locale); - toggleLanguage(true, true); - } else { - reloadKeyboards(); - } - } + mSubtypeSwitcher.onConfigurationChanged(conf); + if (mSubtypeSwitcher.isKeyboardMode()) + onKeyboardLanguageChanged(); + updateAutoTextEnabled(); + // If orientation changed while predicting, commit the change if (conf.orientation != mOrientation) { InputConnection ic = getCurrentInputConnection(); commitTyped(ic); if (ic != null) ic.finishComposingText(); // For voice input mOrientation = conf.orientation; - reloadKeyboards(); + final int mode = mKeyboardSwitcher.getKeyboardMode(); + final EditorInfo attribute = getCurrentInputEditorInfo(); + final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; + mKeyboardSwitcher.loadKeyboard(mode, imeOptions, + mVoiceConnector.isVoiceButtonEnabled(), + mVoiceConnector.isVoiceButtonOnPrimary()); } + mConfigurationChanging = true; super.onConfigurationChanged(conf); - if (mRecognizing) { - switchToRecognitionStatusView(); - } + mVoiceConnector.onConfigurationChanged(mConfigurationChanging); mConfigurationChanging = false; } @Override public View onCreateInputView() { - mKeyboardSwitcher.recreateInputView(); - mKeyboardSwitcher.makeKeyboards(true); - mKeyboardSwitcher.setKeyboardMode( - KeyboardSwitcher.MODE_TEXT, 0, - shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); - return mKeyboardSwitcher.getInputView(); + return mKeyboardSwitcher.onCreateInputView(); } @Override public View onCreateCandidatesView() { - mKeyboardSwitcher.makeKeyboards(true); mCandidateViewContainer = (LinearLayout) getLayoutInflater().inflate( R.layout.candidates, null); mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates); @@ -547,95 +503,89 @@ public class LatinIME extends InputMethodService return mCandidateViewContainer; } + private static boolean isPasswordVariation(int variation) { + return variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD + || variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; + } + + private static boolean isEmailVariation(int variation) { + return variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; + } + @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + final KeyboardSwitcher switcher = mKeyboardSwitcher; + LatinKeyboardView inputView = switcher.getInputView(); + // In landscape mode, this method gets called without the input view being created. if (inputView == null) { return; } + mSubtypeSwitcher.updateParametersOnStartInputView(); + if (mRefreshKeyboardRequired) { mRefreshKeyboardRequired = false; - toggleLanguage(true, true); + onKeyboardLanguageChanged(); } - mKeyboardSwitcher.makeKeyboards(false); - TextEntryState.newSession(this); // Most such things we decide below in the switch statement, but we need to know // now whether this is a password text field, because we need to know now (before // the switch statement) whether we want to enable the voice button. - mPasswordText = false; int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; - if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || - variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { - mPasswordText = true; - } - - mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute); - final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice; - - mAfterVoiceInput = false; - mImmediatelyAfterVoiceInput = false; - mShowingVoiceSuggestions = false; - mVoiceInputHighlighted = false; + mVoiceConnector.resetVoiceStates(isPasswordVariation(variation)); mInputTypeNoAutoCorrect = false; mPredictionOn = false; mCompletionOn = false; mCompletions = null; - mCapsLock = false; mEnteredText = null; + final int mode; switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { case EditorInfo.TYPE_CLASS_NUMBER: case EditorInfo.TYPE_CLASS_DATETIME: - // fall through - // NOTE: For now, we use the phone keyboard for NUMBER and DATETIME until we get - // a dedicated number entry keypad. - // TODO: Use a dedicated number entry keypad here when we get one. + mode = KeyboardSwitcher.MODE_NUMBER; + break; case EditorInfo.TYPE_CLASS_PHONE: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_PHONE; break; case EditorInfo.TYPE_CLASS_TEXT: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions, enableVoiceButton); //startPrediction(); mPredictionOn = true; // Make sure that passwords are not displayed in candidate view - if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || - variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { + if (isPasswordVariation(variation)) { mPredictionOn = false; } - if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + if (isEmailVariation(variation) || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { mAutoSpace = false; } else { mAutoSpace = true; } - if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { + if (isEmailVariation(variation)) { mPredictionOn = false; - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_EMAIL; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { mPredictionOn = false; - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_URL; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_IM; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { mPredictionOn = false; + mode = KeyboardSwitcher.MODE_TEXT; } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_WEB; // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { mInputTypeNoAutoCorrect = true; } + } else { + mode = KeyboardSwitcher.MODE_TEXT; } // If NO_SUGGESTIONS is set, don't do prediction. @@ -654,18 +604,24 @@ public class LatinIME extends InputMethodService } break; default: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardSwitcher.MODE_TEXT; + break; } inputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; mJustAddedAutoSpace = false; - loadSettings(); - updateShiftKeyState(attribute); - setCandidatesViewShownInternal(isCandidateStripVisible() || mCompletionOn, + loadSettings(attribute); + if (mSubtypeSwitcher.isKeyboardMode()) { + switcher.loadKeyboard(mode, attribute.imeOptions, + mVoiceConnector.isVoiceButtonEnabled(), + mVoiceConnector.isVoiceButtonOnPrimary()); + switcher.updateShiftState(); + } + + setCandidatesViewShownInternal(isCandidateStripVisible(), false /* needsInputViewShown */ ); updateSuggestions(); @@ -676,15 +632,19 @@ public class LatinIME extends InputMethodService inputView.setPreviewEnabled(mPopupOn); inputView.setProximityCorrectionEnabled(true); - mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions); + mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || isSuggestionShown()); // If we just entered a text field, maybe it has some old text that requires correction checkReCorrectionOnStart(); checkTutorial(attribute.privateImeOptions); + inputView.setForeground(true); + + mVoiceConnector.onStartInputView(mKeyboardSwitcher.getInputView().getWindowToken()); + if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } private void checkReCorrectionOnStart() { - if (mReCorrectionEnabled && isPredictionOn()) { + if (mReCorrectionEnabled && isSuggestionShown() && isPredictionOn()) { // First get the cursor position. This is required by setOldSuggestions(), so that // it can pass the correct range to setComposingRegion(). At this point, we don't // have valid values for mLastSelectionStart/Stop because onUpdateSelection() has @@ -701,7 +661,7 @@ public class LatinIME extends InputMethodService // Then look for possible corrections in a delayed fashion if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) { - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } } } @@ -713,17 +673,11 @@ public class LatinIME extends InputMethodService LatinImeLogger.commit(); onAutoCompletionStateChanged(false); - if (VOICE_INSTALLED && !mConfigurationChanging) { - if (mAfterVoiceInput) { - mVoiceInput.flushAllTextModificationCounters(); - mVoiceInput.logInputEnded(); - } - mVoiceInput.flushLogs(); - mVoiceInput.cancel(); - } - if (mKeyboardSwitcher.getInputView() != null) { - mKeyboardSwitcher.getInputView().closing(); - } + mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging); + + BaseKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) + inputView.closing(); if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); } @@ -731,21 +685,18 @@ public class LatinIME extends InputMethodService @Override public void onFinishInputView(boolean finishingInput) { super.onFinishInputView(finishingInput); - // Remove penging messages related to update suggestions - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); - mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + BaseKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) + inputView.setForeground(false); + // Remove pending messages related to update suggestions + mHandler.cancelUpdateSuggestions(); + mHandler.cancelUpdateOldSuggestions(); } @Override public void onUpdateExtractedText(int token, ExtractedText text) { super.onUpdateExtractedText(token, text); - InputConnection ic = getCurrentInputConnection(); - if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) { - if (mHints.showPunctuationHintIfNecessary(ic)) { - mVoiceInput.logPunctuationHintDisplayed(); - } - } - mImmediatelyAfterVoiceInput = false; + mVoiceConnector.showPunctuationHintIfNecessary(); } @Override @@ -764,26 +715,22 @@ public class LatinIME extends InputMethodService + ", ce=" + candidatesEnd); } - if (mAfterVoiceInput) { - mVoiceInput.setCursorPos(newSelEnd); - mVoiceInput.setSelectionSpan(newSelEnd - newSelStart); - } + mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart); // If the current selection in the text view changes, we should // clear whatever candidate text we have. - if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted) - && (newSelStart != candidatesEnd - || newSelEnd != candidatesEnd) - && mLastSelectionStart != newSelStart)) { + if ((((mComposing.length() > 0 && mPredicting) + || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) { mComposing.setLength(0); mPredicting = false; - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.finishComposingText(); } - mVoiceInputHighlighted = false; + mVoiceConnector.setVoiceInputHighlighted(false); } else if (!mPredicting && !mJustAccepted) { switch (TextEntryState.getState()) { case ACCEPTED_DEFAULT: @@ -795,33 +742,29 @@ public class LatinIME extends InputMethodService } } mJustAccepted = false; - postUpdateShiftKeyState(); + mHandler.postUpdateShiftKeyState(); // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; - if (mReCorrectionEnabled) { + if (mReCorrectionEnabled && isSuggestionShown()) { // Don't look for corrections if the keyboard is not visible - if (mKeyboardSwitcher != null && mKeyboardSwitcher.getInputView() != null - && mKeyboardSwitcher.getInputView().isShown()) { + if (mKeyboardSwitcher.isInputViewShown()) { // Check if we should go in or out of correction mode. - if (isPredictionOn() - && mJustRevertedSeparator == null + if (isPredictionOn() && !mJustReverted && (candidatesStart == candidatesEnd || newSelStart != oldSelStart || TextEntryState.isCorrecting()) - && (newSelStart < newSelEnd - 1 || (!mPredicting)) - && !mVoiceInputHighlighted) { + && (newSelStart < newSelEnd - 1 || (!mPredicting))) { if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } else { abortCorrection(false); // Show the punctuation suggestions list if the current one is not // and if not showing "Touch again to save". - if (mCandidateView != null - && !mSuggestPuncList.equals(mCandidateView.getSuggestions()) - && !mCandidateView.isShowingAddToDictionaryHint()) { - setNextSuggestions(); + if (mCandidateView != null && !isShowingPunctuationList() + && !mCandidateView.isShowingAddToDictionaryHint()) { + setPunctuationSuggestions(); } } } @@ -870,18 +813,7 @@ public class LatinIME extends InputMethodService mOptionsDialog.dismiss(); mOptionsDialog = null; } - if (!mConfigurationChanging) { - if (mAfterVoiceInput) mVoiceInput.logInputEnded(); - if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { - mVoiceInput.logKeyboardWarningDialogDismissed(); - mVoiceWarningDialog.dismiss(); - mVoiceWarningDialog = null; - } - if (VOICE_INSTALLED & mRecognizing) { - mVoiceInput.cancel(); - } - } - mWordToSuggestions.clear(); + mVoiceConnector.hideVoiceWindow(mConfigurationChanging); mWordHistory.clear(); super.hideWindow(); TextEntryState.endSession(); @@ -917,8 +849,8 @@ public class LatinIME extends InputMethodService private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) { // TODO: Remove this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { - super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null - && (needsInputViewShown ? mKeyboardSwitcher.getInputView().isShown() : true)); + super.setCandidatesViewShown(shown + && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true)); } } @@ -985,10 +917,9 @@ public class LatinIME extends InputMethodService if (mTutorial != null) { return true; } - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); // Enable shift key and DPAD to do selections - if (inputView != null && inputView.isShown() - && inputView.isShifted()) { + if (mKeyboardSwitcher.isInputViewShown() + && mKeyboardSwitcher.isShiftedOrShiftLocked()) { event = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), @@ -1002,30 +933,7 @@ public class LatinIME extends InputMethodService return super.onKeyUp(keyCode, event); } - private void revertVoiceInput() { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.commitText("", 1); - updateSuggestions(); - mVoiceInputHighlighted = false; - } - - private void commitVoiceInput() { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.finishComposingText(); - updateSuggestions(); - mVoiceInputHighlighted = false; - } - - private void reloadKeyboards() { - mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - if (mKeyboardSwitcher.getInputView() != null - && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) { - mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary); - } - mKeyboardSwitcher.makeKeyboards(true); - } - - private void commitTyped(InputConnection inputConnection) { + public void commitTyped(InputConnection inputConnection) { if (mPredicting) { mPredicting = false; if (mComposing.length() > 0) { @@ -1040,27 +948,13 @@ public class LatinIME extends InputMethodService } } - private void postUpdateShiftKeyState() { - mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - // TODO: Should remove this 300ms delay? - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300); - } - - public void updateShiftKeyState(EditorInfo attr) { + public boolean getCurrentAutoCapsState() { InputConnection ic = getCurrentInputConnection(); - if (ic != null && attr != null && mKeyboardSwitcher.isAlphabetMode()) { - mKeyboardSwitcher.setShifted(mShiftKeyState.isMomentary() || mCapsLock - || getCursorCapsMode(ic, attr) != 0); - } - } - - private int getCursorCapsMode(InputConnection ic, EditorInfo attr) { - int caps = 0; EditorInfo ei = getCurrentInputEditorInfo(); - if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { - caps = ic.getCursorCapsMode(attr.inputType); + if (mAutoCap && ic != null && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { + return ic.getCursorCapsMode(ei.inputType) != 0; } - return caps; + return false; } private void swapPunctuationAndSpace() { @@ -1073,7 +967,7 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); mJustAddedAutoSpace = true; } } @@ -1090,7 +984,7 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(3, 0); ic.commitText(" ..", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); } } @@ -1107,7 +1001,7 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); mJustAddedAutoSpace = true; } } @@ -1141,7 +1035,7 @@ public class LatinIME extends InputMethodService mUserDictionary.addWord(word, 128); // Suggestion strip should be updated after the operation of adding word to the // user dictionary - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); return true; } @@ -1153,14 +1047,9 @@ public class LatinIME extends InputMethodService } } - private void showInputMethodPicker() { - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) - .showInputMethodPicker(); - } - private void onOptionKeyPressed() { if (!isShowingOptionDialog()) { - if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) { + if (LatinIMEUtil.hasMultipleEnabledIMEsOrSubtypes(mImm)) { showOptionsMenu(); } else { launchSettings(); @@ -1170,8 +1059,8 @@ public class LatinIME extends InputMethodService private void onOptionKeyLongPressed() { if (!isShowingOptionDialog()) { - if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) { - showInputMethodPicker(); + if (LatinIMEUtil.hasMultipleEnabledIMEsOrSubtypes(mImm)) { + mImm.showInputMethodPicker(); } else { launchSettings(); } @@ -1186,78 +1075,76 @@ public class LatinIME extends InputMethodService public void onKey(int primaryCode, int[] keyCodes, int x, int y) { long when = SystemClock.uptimeMillis(); - if (primaryCode != Keyboard.KEYCODE_DELETE || - when > mLastKeyTime + QUICK_PRESS) { + if (primaryCode != BaseKeyboard.KEYCODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } mLastKeyTime = when; - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); + KeyboardSwitcher switcher = mKeyboardSwitcher; + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); switch (primaryCode) { - case Keyboard.KEYCODE_DELETE: - handleBackspace(); - mDeleteCount++; - LatinImeLogger.logOnDelete(); - break; - case Keyboard.KEYCODE_SHIFT: - // Shift key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) - handleShift(); - break; - case Keyboard.KEYCODE_MODE_CHANGE: - // Symbol key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) - changeKeyboardMode(); - break; - case Keyboard.KEYCODE_CANCEL: - if (!isShowingOptionDialog()) { - handleClose(); - } - break; - case LatinKeyboardView.KEYCODE_OPTIONS: - onOptionKeyPressed(); - break; - case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS: - onOptionKeyLongPressed(); - break; - case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE: - toggleLanguage(false, true); - break; - case LatinKeyboardView.KEYCODE_PREV_LANGUAGE: - toggleLanguage(false, false); - break; - case LatinKeyboardView.KEYCODE_VOICE: - if (VOICE_INSTALLED) { - startListening(false /* was a button press, was not a swipe */); - } - break; - case 9 /*Tab*/: - sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); - break; - default: - if (primaryCode != KEYCODE_ENTER) { - mJustAddedAutoSpace = false; - } - RingCharBuffer.getInstance().push((char)primaryCode, x, y); - LatinImeLogger.logOnInputChar(); - if (isWordSeparator(primaryCode)) { - handleSeparator(primaryCode); - } else { - handleCharacter(primaryCode, keyCodes); - } - // Cancel the just reverted state - mJustRevertedSeparator = null; - } - if (mKeyboardSwitcher.onKey(primaryCode)) { - changeKeyboardMode(); + case BaseKeyboard.KEYCODE_DELETE: + handleBackspace(); + mDeleteCount++; + LatinImeLogger.logOnDelete(); + break; + case BaseKeyboard.KEYCODE_SHIFT: + // Shift key is handled in onPress() when device has distinct multi-touch panel. + if (!distinctMultiTouch) + switcher.toggleShift(); + break; + case BaseKeyboard.KEYCODE_MODE_CHANGE: + // Symbol key is handled in onPress() when device has distinct multi-touch panel. + if (!distinctMultiTouch) + switcher.changeKeyboardMode(); + break; + case BaseKeyboard.KEYCODE_CANCEL: + if (!isShowingOptionDialog()) { + handleClose(); + } + break; + case LatinKeyboardView.KEYCODE_OPTIONS: + onOptionKeyPressed(); + break; + case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS: + onOptionKeyLongPressed(); + break; + case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE: + toggleLanguage(false, true); + break; + case LatinKeyboardView.KEYCODE_PREV_LANGUAGE: + toggleLanguage(false, false); + break; + case LatinKeyboardView.KEYCODE_CAPSLOCK: + switcher.toggleCapsLock(); + break; + case LatinKeyboardView.KEYCODE_VOICE: /* was a button press, was not a swipe */ + mVoiceConnector.startListening(false, + mKeyboardSwitcher.getInputView().getWindowToken(), mConfigurationChanging); + break; + case KEYCODE_TAB: + handleTab(); + break; + default: + if (primaryCode != KEYCODE_ENTER) { + mJustAddedAutoSpace = false; + } + RingCharBuffer.getInstance().push((char)primaryCode, x, y); + LatinImeLogger.logOnInputChar(); + if (isWordSeparator(primaryCode)) { + handleSeparator(primaryCode); + } else { + handleCharacter(primaryCode, keyCodes); + } + // Cancel the just reverted state + mJustReverted = false; } + switcher.onKey(primaryCode); // Reset after any single keystroke mEnteredText = null; } public void onText(CharSequence text) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } + mVoiceConnector.commitVoiceInput(); InputConnection ic = getCurrentInputConnection(); if (ic == null) return; abortCorrection(false); @@ -1268,8 +1155,8 @@ public class LatinIME extends InputMethodService maybeRemovePreviousPeriod(text); ic.commitText(text, 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); - mJustRevertedSeparator = null; + mKeyboardSwitcher.updateShiftState(); + mJustReverted = false; mJustAddedAutoSpace = false; mEnteredText = text; } @@ -1279,29 +1166,14 @@ public class LatinIME extends InputMethodService } private void handleBackspace() { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - mVoiceInput.incrementTextModificationDeleteCount( - mVoiceResults.candidates.get(0).toString().length()); - revertVoiceInput(); - return; - } + if (mVoiceConnector.logAndRevertVoiceInput()) return; boolean deleteChar = false; InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit(); - if (mAfterVoiceInput) { - // Don't log delete if the user is pressing delete at - // the beginning of the text box (hence not deleting anything) - if (mVoiceInput.getCursorPos() > 0) { - // If anything was selected before the delete was pressed, increment the - // delete count by the length of the selection - int deleteLen = mVoiceInput.getSelectionSpan() > 0 ? - mVoiceInput.getSelectionSpan() : 1; - mVoiceInput.incrementTextModificationDeleteCount(deleteLen); - } - } + mVoiceConnector.handleBackspace(); if (mPredicting) { final int length = mComposing.length(); @@ -1312,14 +1184,14 @@ public class LatinIME extends InputMethodService if (mComposing.length() == 0) { mPredicting = false; } - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } else { ic.deleteSurroundingText(1, 0); } } else { deleteChar = true; } - postUpdateShiftKeyState(); + mHandler.postUpdateShiftKeyState(); TextEntryState.backspace(); if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { revertLastWord(deleteChar); @@ -1344,55 +1216,46 @@ public class LatinIME extends InputMethodService } } } - mJustRevertedSeparator = null; + mJustReverted = false; ic.endBatchEdit(); } - private void resetShift() { - handleShiftInternal(true); - } + private void handleTab() { + final int imeOptions = getCurrentInputEditorInfo().imeOptions; + final int navigationFlags = + EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; + if ((imeOptions & navigationFlags) == 0) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); + return; + } - private void handleShift() { - handleShiftInternal(false); - } + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) + return; - private void handleShiftInternal(boolean forceNormal) { - mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - KeyboardSwitcher switcher = mKeyboardSwitcher; - LatinKeyboardView inputView = switcher.getInputView(); - if (switcher.isAlphabetMode()) { - if (mCapsLock || forceNormal) { - mCapsLock = false; - switcher.setShifted(false); - } else if (inputView != null) { - if (inputView.isShifted()) { - mCapsLock = true; - switcher.setShiftLocked(true); - } else { - switcher.setShifted(true); - } - } - } else { - switcher.toggleShift(); + // True if keyboard is in either chording shift or manual temporary upper case mode. + final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); + if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 + && !isManualTemporaryUpperCase) { + ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); + } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 + && isManualTemporaryUpperCase) { + ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); } } private void abortCorrection(boolean force) { if (force || TextEntryState.isCorrecting()) { + TextEntryState.onAbortCorrection(); + setCandidatesViewShown(isCandidateStripVisible()); getCurrentInputConnection().finishComposingText(); clearSuggestions(); } } private void handleCharacter(int primaryCode, int[] keyCodes) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } + mVoiceConnector.handleCharacter(); - if (mAfterVoiceInput) { - // Assume input length is 1. This assumption fails for smiley face insertions. - mVoiceInput.incrementTextModificationInsertCount(1); - } if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) { abortCorrection(false); } @@ -1405,13 +1268,14 @@ public class LatinIME extends InputMethodService mWord.reset(); } } - if (mKeyboardSwitcher.getInputView().isShifted()) { + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (switcher.isShiftedOrShiftLocked()) { if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT || keyCodes[0] > Character.MAX_CODE_POINT) { return; } primaryCode = keyCodes[0]; - if (mKeyboardSwitcher.isAlphabetMode() && Character.isLowerCase(primaryCode)) { + if (switcher.isAlphabetMode() && Character.isLowerCase(primaryCode)) { int upperCaseCode = Character.toUpperCase(primaryCode); if (upperCaseCode != primaryCode) { primaryCode = upperCaseCode; @@ -1424,9 +1288,8 @@ public class LatinIME extends InputMethodService } } if (mPredicting) { - if (mKeyboardSwitcher.getInputView().isShifted() - && mKeyboardSwitcher.isAlphabetMode() - && mComposing.length() == 0) { + if (mComposing.length() == 0 && switcher.isAlphabetMode() + && switcher.isShiftedOrShiftLocked()) { mWord.setFirstCharCapitalized(true); } mComposing.append((char) primaryCode); @@ -1435,33 +1298,25 @@ public class LatinIME extends InputMethodService if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWord.size() == 1) { - mWord.setAutoCapitalized( - getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0); + mWord.setAutoCapitalized(getCurrentAutoCapsState()); } ic.setComposingText(mComposing, 1); } - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } else { sendKeyChar((char)primaryCode); } - updateShiftKeyState(getCurrentInputEditorInfo()); + switcher.updateShiftState(); if (LatinIME.PERF_DEBUG) measureCps(); TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); } private void handleSeparator(int primaryCode) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } - - if (mAfterVoiceInput){ - // Assume input length is 1. This assumption fails for smiley face insertions. - mVoiceInput.incrementTextModificationInsertPunctuationCount(1); - } + mVoiceConnector.handleSeparator(); // Should dismiss the "Touch again to save" message when handling separator if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } boolean pickedDefault = false; @@ -1476,10 +1331,7 @@ public class LatinIME extends InputMethodService // not to auto correct, but accept the typed word. For instance, // in Italian dov' should not be expanded to dove' because the elision // requires the last vowel to be removed. - if (mAutoCorrectOn && primaryCode != '\'' && - (mJustRevertedSeparator == null - || mJustRevertedSeparator.length() == 0 - || mJustRevertedSeparator.charAt(0) != primaryCode)) { + if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) { pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". @@ -1513,7 +1365,7 @@ public class LatinIME extends InputMethodService if (pickedDefault) { TextEntryState.backToAcceptedDefault(mWord.getTypedWord()); } - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); if (ic != null) { ic.endBatchEdit(); } @@ -1521,16 +1373,11 @@ public class LatinIME extends InputMethodService private void handleClose() { commitTyped(getCurrentInputConnection()); - if (VOICE_INSTALLED & mRecognizing) { - mVoiceInput.cancel(); - } + mVoiceConnector.handleClose(); requestHideSelf(0); - if (mKeyboardSwitcher != null) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - if (inputView != null) { - inputView.closing(); - } - } + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) + inputView.closing(); TextEntryState.endSession(); } @@ -1551,216 +1398,60 @@ public class LatinIME extends InputMethodService mWordHistory.add(entry); } - private void postUpdateSuggestions() { - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); + private boolean isPredictionOn() { + return mPredictionOn; } - private void postUpdateOldSuggestions() { - mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300); + private boolean isShowingPunctuationList() { + return mSuggestPuncList.equals(mCandidateView.getSuggestions()); } - private boolean isPredictionOn() { - return mPredictionOn; + private boolean isSuggestionShown() { + return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) + || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE + && mOrientation == Configuration.ORIENTATION_PORTRAIT); } private boolean isCandidateStripVisible() { - return isPredictionOn() && mShowSuggestions; + boolean forceVisible = mCandidateView.isShowingAddToDictionaryHint() + || TextEntryState.isCorrecting(); + return forceVisible || (isSuggestionShown() + && (isPredictionOn() || mCompletionOn || isShowingPunctuationList())); } - public void onCancelVoice() { - if (mRecognizing) { - switchToKeyboardView(); - } - } - - private void switchToKeyboardView() { + public void switchToKeyboardView() { mHandler.post(new Runnable() { public void run() { - mRecognizing = false; - if (mKeyboardSwitcher.getInputView() != null) { - setInputView(mKeyboardSwitcher.getInputView()); + if (DEBUG) { + Log.d(TAG, "Switch to keyboard view."); + } + View v = mKeyboardSwitcher.getInputView(); + if (v != null) { + // Confirms that the keyboard view doesn't have parent view. + ViewParent p = v.getParent(); + if (p != null && p instanceof ViewGroup) { + ((ViewGroup)p).removeView(v); + } + setInputView(v); } - setCandidatesViewShown(true); + setCandidatesViewShown(isCandidateStripVisible()); updateInputViewShown(); - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); }}); } - private void switchToRecognitionStatusView() { - final boolean configChanged = mConfigurationChanging; - mHandler.post(new Runnable() { - public void run() { - setCandidatesViewShown(false); - mRecognizing = true; - View v = mVoiceInput.getView(); - ViewParent p = v.getParent(); - if (p != null && p instanceof ViewGroup) { - ((ViewGroup)v.getParent()).removeView(v); - } - setInputView(v); - updateInputViewShown(); - if (configChanged) { - mVoiceInput.onConfigurationChanged(); - } - }}); - } - - private void startListening(boolean swipe) { - if (!mHasUsedVoiceInput || - (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) { - // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel. - showVoiceWarningDialog(swipe); - } else { - reallyStartListening(swipe); - } - } - - private void reallyStartListening(boolean swipe) { - if (!mHasUsedVoiceInput) { - // The user has started a voice input, so remember that in the - // future (so we don't show the warning dialog after the first run). - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true); - SharedPreferencesCompat.apply(editor); - mHasUsedVoiceInput = true; - } - - if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) { - // The user has started a voice input from an unsupported locale, so remember that - // in the future (so we don't show the warning dialog the next time they do this). - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true); - SharedPreferencesCompat.apply(editor); - mHasUsedVoiceInputUnsupportedLocale = true; - } - - // Clear N-best suggestions - clearSuggestions(); - - FieldContext context = new FieldContext( - getCurrentInputConnection(), - getCurrentInputEditorInfo(), - mLanguageSwitcher.getInputLanguage(), - mLanguageSwitcher.getEnabledLanguages()); - mVoiceInput.startListening(context, swipe); - switchToRecognitionStatusView(); - } - - private void showVoiceWarningDialog(final boolean swipe) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setCancelable(true); - builder.setIcon(R.drawable.ic_mic_dialog); - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - mVoiceInput.logKeyboardWarningDialogOk(); - reallyStartListening(swipe); - } - }); - builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - mVoiceInput.logKeyboardWarningDialogCancel(); - } - }); - - if (mLocaleSupportedForVoiceInput) { - String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_warning_how_to_turn_off); - builder.setMessage(message); - } else { - String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" + - getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_warning_how_to_turn_off); - builder.setMessage(message); - } - - builder.setTitle(R.string.voice_warning_title); - mVoiceWarningDialog = builder.create(); - - Window window = mVoiceWarningDialog.getWindow(); - WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); - lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; - window.setAttributes(lp); - window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); - mVoiceInput.logKeyboardWarningDialogShown(); - mVoiceWarningDialog.show(); - } - - public void onVoiceResults(List<String> candidates, - Map<String, List<CharSequence>> alternatives) { - if (!mRecognizing) { - return; - } - mVoiceResults.candidates = candidates; - mVoiceResults.alternatives = alternatives; - mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS)); - } - - private void handleVoiceResults() { - mAfterVoiceInput = true; - mImmediatelyAfterVoiceInput = true; - - InputConnection ic = getCurrentInputConnection(); - if (!isFullscreenMode()) { - // Start listening for updates to the text from typing, etc. - if (ic != null) { - ExtractedTextRequest req = new ExtractedTextRequest(); - ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); - } - } - - vibrate(); - switchToKeyboardView(); - - final List<CharSequence> nBest = new ArrayList<CharSequence>(); - boolean capitalizeFirstWord = preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() - && mKeyboardSwitcher.getInputView().isShifted()); - for (String c : mVoiceResults.candidates) { - if (capitalizeFirstWord) { - c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); - } - nBest.add(c); - } - - if (nBest.size() == 0) { - return; - } - - String bestResult = nBest.get(0).toString(); - - mVoiceInput.logVoiceInputDelivered(bestResult.length()); - - mHints.registerVoiceResult(bestResult); - - if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text - - commitTyped(ic); - EditingUtil.appendText(ic, bestResult); - - if (ic != null) ic.endBatchEdit(); - - mVoiceInputHighlighted = true; - mWordToSuggestions.putAll(mVoiceResults.alternatives); - } - - private void clearSuggestions() { + public void clearSuggestions() { setSuggestions(null, false, false, false); } - private void setSuggestions( + public void setSuggestions( List<CharSequence> suggestions, boolean completions, boolean typedWordValid, boolean haveMinimalSuggestion) { - if (mIsShowingHint) { + if (mVoiceConnector.getAndResetIsShowingHint()) { setCandidatesView(mCandidateViewContainer); - mIsShowingHint = false; } if (mCandidateView != null) { @@ -1769,17 +1460,17 @@ public class LatinIME extends InputMethodService } } - private void updateSuggestions() { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); + public void updateSuggestions() { + mKeyboardSwitcher.setPreferredLetters(null); // Check if we have a suggestion engine attached. - if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { + if ((mSuggest == null || !isPredictionOn()) + && !mVoiceConnector.isVoiceInputHighlighted()) { return; } if (!mPredicting) { - setNextSuggestions(); + setPunctuationSuggestions(); return; } showSuggestions(mWord); @@ -1792,8 +1483,8 @@ public class LatinIME extends InputMethodService } private void showCorrections(WordAlternatives alternatives) { + mKeyboardSwitcher.setPreferredLetters(null); List<CharSequence> stringList = alternatives.getAlternatives(); - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null); showSuggestions(stringList, alternatives.getOriginalWord(), false, false); } @@ -1808,12 +1499,10 @@ public class LatinIME extends InputMethodService // Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime)); int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); + mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies); - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters( - nextLettersFrequencies); - - boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection(); - //|| mCorrectionMode == mSuggest.CORRECTION_FULL; + boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted + && mSuggest.hasMinimalCorrection(); CharSequence typedWord = word.getTypedWord(); // If we're in basic correct boolean typedWordValid = mSuggest.isValidWord(typedWord) || @@ -1842,13 +1531,13 @@ public class LatinIME extends InputMethodService } else { mBestWord = null; } - setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); + setCandidatesViewShown(isCandidateStripVisible()); } private boolean pickDefaultSuggestion() { // Complete any pending candidate query first - if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); + if (mHandler.hasPendingUpdateSuggestions()) { + mHandler.cancelUpdateSuggestions(); updateSuggestions(); } if (mBestWord != null && mBestWord.length() > 0) { @@ -1864,14 +1553,8 @@ public class LatinIME extends InputMethodService } public void pickSuggestionManually(int index, CharSequence suggestion) { - if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index); List<CharSequence> suggestions = mCandidateView.getSuggestions(); - - if (mAfterVoiceInput && !mShowingVoiceSuggestions) { - mVoiceInput.flushAllTextModificationCounters(); - // send this intent AFTER logging any prior aggregated edits. - mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.length()); - } + mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); final boolean correcting = TextEntryState.isCorrecting(); InputConnection ic = getCurrentInputConnection(); @@ -1888,7 +1571,7 @@ public class LatinIME extends InputMethodService if (mCandidateView != null) { mCandidateView.clear(); } - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); if (ic != null) { ic.endBatchEdit(); } @@ -1903,8 +1586,8 @@ public class LatinIME extends InputMethodService LatinImeLogger.logOnManualSuggestion( "", suggestion.toString(), index, suggestions); final char primaryCode = suggestion.charAt(0); - onKey(primaryCode, new int[]{primaryCode}, LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE); + onKey(primaryCode, new int[]{primaryCode}, BaseKeyboardView.NOT_A_TOUCH_COORDINATE, + BaseKeyboardView.NOT_A_TOUCH_COORDINATE); if (ic != null) { ic.endBatchEdit(); } @@ -1936,12 +1619,12 @@ public class LatinIME extends InputMethodService // we just did a correction, in which case we need to stay in // TextEntryState.State.PICKED_SUGGESTION state. TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); - setNextSuggestions(); + setPunctuationSuggestions(); } else if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. clearSuggestions(); - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } if (showingAddToDictionaryHint) { mCandidateView.showAddToDictionaryHint(suggestion); @@ -1951,27 +1634,6 @@ public class LatinIME extends InputMethodService } } - private void rememberReplacedWord(CharSequence suggestion) { - if (mShowingVoiceSuggestions) { - // Retain the replaced word in the alternatives array. - EditingUtil.Range range = new EditingUtil.Range(); - String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(), - mWordSeparators, range); - if (!mWordToSuggestions.containsKey(wordToBeReplaced)) { - wordToBeReplaced = wordToBeReplaced.toLowerCase(); - } - if (mWordToSuggestions.containsKey(wordToBeReplaced)) { - List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced); - if (suggestions.contains(suggestion)) { - suggestions.remove(suggestion); - } - suggestions.add(wordToBeReplaced); - mWordToSuggestions.remove(wordToBeReplaced); - mWordToSuggestions.put(suggestion.toString(), suggestions); - } - } - } - /** * Commits the chosen word to the text field and saves it for later * retrieval. @@ -1981,61 +1643,23 @@ public class LatinIME extends InputMethodService * word. */ private void pickSuggestion(CharSequence suggestion, boolean correcting) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - if (mCapsLock) { - suggestion = suggestion.toString().toUpperCase(); - } else if (preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() - && inputView.isShifted())) { - suggestion = suggestion.toString().toUpperCase().charAt(0) - + suggestion.subSequence(1, suggestion.length()).toString(); - } + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (!switcher.isKeyboardAvailable()) + return; InputConnection ic = getCurrentInputConnection(); if (ic != null) { - rememberReplacedWord(suggestion); + mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators); ic.commitText(suggestion, 1); } saveWordInHistory(suggestion); mPredicting = false; mCommittedLength = suggestion.length(); - ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); + switcher.setPreferredLetters(null); // If we just corrected a word, then don't show punctuations if (!correcting) { - setNextSuggestions(); + setPunctuationSuggestions(); } - updateShiftKeyState(getCurrentInputEditorInfo()); - } - - /** - * Tries to apply any voice alternatives for the word if this was a spoken word and - * there are voice alternatives. - * @param touching The word that the cursor is touching, with position information - * @return true if an alternative was found, false otherwise. - */ - private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) { - // Search for result in spoken word alternatives - String selectedWord = touching.word.toString().trim(); - if (!mWordToSuggestions.containsKey(selectedWord)) { - selectedWord = selectedWord.toLowerCase(); - } - if (mWordToSuggestions.containsKey(selectedWord)) { - mShowingVoiceSuggestions = true; - List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord); - // If the first letter of touching is capitalized, make all the suggestions - // start with a capital letter. - if (Character.isUpperCase(touching.word.charAt(0))) { - for (int i = 0; i < suggestions.size(); i++) { - String origSugg = (String) suggestions.get(i); - String capsSugg = origSugg.toUpperCase().charAt(0) - + origSugg.subSequence(1, origSugg.length()).toString(); - suggestions.set(i, capsSugg); - } - } - setSuggestions(suggestions, false, true, true); - setCandidatesViewShown(true); - return true; - } - return false; + switcher.updateShiftState(); } /** @@ -2086,7 +1710,7 @@ public class LatinIME extends InputMethodService } private void setOldSuggestions() { - mShowingVoiceSuggestions = false; + mVoiceConnector.setShowingVoiceSuggestions(false); if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) { return; } @@ -2100,7 +1724,8 @@ public class LatinIME extends InputMethodService if (touching != null && touching.word.length() > 1) { ic.beginBatchEdit(); - if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) { + if (!mVoiceConnector.applyVoiceAlternatives(touching) + && !applyTypedAlternatives(touching)) { abortCorrection(true); } else { TextEntryState.selectedForCorrection(); @@ -2110,14 +1735,15 @@ public class LatinIME extends InputMethodService ic.endBatchEdit(); } else { abortCorrection(true); - setNextSuggestions(); // Show the punctuation suggestions list + setPunctuationSuggestions(); // Show the punctuation suggestions list } } else { abortCorrection(true); } } - private void setNextSuggestions() { + private void setPunctuationSuggestions() { + setCandidatesViewShown(isCandidateStripVisible()); setSuggestions(mSuggestPuncList, false, false, false); } @@ -2188,7 +1814,7 @@ public class LatinIME extends InputMethodService if (!mPredicting && length > 0) { final InputConnection ic = getCurrentInputConnection(); mPredicting = true; - mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0); + mJustReverted = true; if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); @@ -2199,10 +1825,10 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(toDelete, 0); ic.setComposingText(mComposing, 1); TextEntryState.backspace(); - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); - mJustRevertedSeparator = null; + mJustReverted = false; } } @@ -2221,7 +1847,7 @@ public class LatinIME extends InputMethodService private void sendSpace() { sendKeyChar((char)KEYCODE_SPACE); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); //onKey(KEY_SPACE[0], KEY_SPACE); } @@ -2229,30 +1855,32 @@ public class LatinIME extends InputMethodService return mWord.isFirstCharCapitalized(); } + // Notify that Language has been changed and toggleLanguage will update KeyboaredID according + // to new Language. + public void onKeyboardLanguageChanged() { + toggleLanguage(true, true); + } + + // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. private void toggleLanguage(boolean reset, boolean next) { - if (reset) { - mLanguageSwitcher.reset(); - } else { - if (next) { - mLanguageSwitcher.next(); - } else { - mLanguageSwitcher.prev(); - } + if (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER) { + mSubtypeSwitcher.toggleLanguage(reset, next); } - int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode(); - reloadKeyboards(); - mKeyboardSwitcher.makeKeyboards(true); - mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0, - mEnableVoiceButton && mEnableVoice); - initSuggest(mLanguageSwitcher.getInputLanguage()); - mLanguageSwitcher.persist(); - updateShiftKeyState(getCurrentInputEditorInfo()); + // Reload keyboard because the current language has been changed. + KeyboardSwitcher switcher = mKeyboardSwitcher; + final int mode = switcher.getKeyboardMode(); + final EditorInfo attribute = getCurrentInputEditorInfo(); + final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; + switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(), + mVoiceConnector.isVoiceButtonOnPrimary()); + initSuggest(); + switcher.updateShiftState(); } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key); if (PREF_SELECTED_LANGUAGES.equals(key)) { - mLanguageSwitcher.loadLocales(sharedPreferences); mRefreshKeyboardRequired = true; } else if (PREF_RECORRECTION_ENABLED.equals(key)) { mReCorrectionEnabled = sharedPreferences.getBoolean(PREF_RECORRECTION_ENABLED, @@ -2262,8 +1890,8 @@ public class LatinIME extends InputMethodService public void swipeRight() { if (LatinKeyboardView.DEBUG_AUTO_PLAY) { - ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); - CharSequence text = cm.getText(); + CharSequence text = ((android.text.ClipboardManager)getSystemService( + CLIPBOARD_SERVICE)).getText(); if (!TextUtils.isEmpty(text)) { mKeyboardSwitcher.getInputView().startPlaying(text.toString()); } @@ -2278,61 +1906,34 @@ public class LatinIME extends InputMethodService } public void swipeUp() { - //launchSettings(); } public void onPress(int primaryCode) { vibrate(); playKeyClick(primaryCode); - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); - if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) { - mShiftKeyState.onPress(); - handleShift(); - } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) { - mSymbolKeyState.onPress(); - changeKeyboardMode(); + KeyboardSwitcher switcher = mKeyboardSwitcher; + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); + if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_SHIFT) { + switcher.onPressShift(); + } else if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_MODE_CHANGE) { + switcher.onPressSymbol(); } else { - mShiftKeyState.onOtherKeyPressed(); - mSymbolKeyState.onOtherKeyPressed(); + switcher.onOtherKeyPressed(); } } public void onRelease(int primaryCode) { + KeyboardSwitcher switcher = mKeyboardSwitcher; // Reset any drag flags in the keyboard - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased(); - //vibrate(); - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); - if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) { - if (mShiftKeyState.isMomentary()) - resetShift(); - mShiftKeyState.onRelease(); - } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) { - if (mSymbolKeyState.isMomentary()) - changeKeyboardMode(); - mSymbolKeyState.onRelease(); + switcher.keyReleased(); + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); + if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_SHIFT) { + switcher.onReleaseShift(); + } else if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_MODE_CHANGE) { + switcher.onReleaseSymbol(); } } - private FieldContext makeFieldContext() { - return new FieldContext( - getCurrentInputConnection(), - getCurrentInputEditorInfo(), - mLanguageSwitcher.getInputLanguage(), - mLanguageSwitcher.getEnabledLanguages()); - } - - private boolean fieldCanDoVoice(FieldContext fieldContext) { - return !mPasswordText - && mVoiceInput != null - && !mVoiceInput.isBlacklistedField(fieldContext); - } - - private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) { - return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) - && !(attribute != null - && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions)) - && SpeechRecognizer.isRecognitionAvailable(this); - } // receive ringer mode changes to detect silent mode private BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -2365,7 +1966,7 @@ public class LatinIME extends InputMethodService // FIXME: These should be triggered after auto-repeat logic int sound = AudioManager.FX_KEYPRESS_STANDARD; switch (primaryCode) { - case Keyboard.KEYCODE_DELETE: + case BaseKeyboard.KEYCODE_DELETE: sound = AudioManager.FX_KEYPRESS_DELETE; break; case KEYCODE_ENTER: @@ -2379,12 +1980,13 @@ public class LatinIME extends InputMethodService } } - private void vibrate() { + public void vibrate() { if (!mVibrateOn) { return; } - if (mKeyboardSwitcher.getInputView() != null) { - mKeyboardSwitcher.getInputView().performHapticFeedback( + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) { + inputView.performHapticFeedback( HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } @@ -2393,7 +1995,7 @@ public class LatinIME extends InputMethodService private void checkTutorial(String privateImeOptions) { if (privateImeOptions == null) return; if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) { - if (mTutorial == null) startTutorial(); + if (mTutorial == null) mHandler.startTutorial(); } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) { if (mTutorial != null) { if (mTutorial.close()) { @@ -2403,24 +2005,22 @@ public class LatinIME extends InputMethodService } } - private void startTutorial() { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500); - } - - /* package */ void tutorialDone() { + // Tutorial.TutorialListener + public void onTutorialDone() { + sendDownUpKeyEvents(-1); // Inform the setupwizard that tutorial is in last bubble mTutorial = null; } - /* package */ void promoteToUserDictionary(String word, int frequency) { + public void promoteToUserDictionary(String word, int frequency) { if (mUserDictionary.isValidWord(word)) return; mUserDictionary.addWord(word, frequency); } - /* package */ WordComposer getCurrentWord() { + public WordComposer getCurrentWord() { return mWord; } - /* package */ boolean getPopupOn() { + public boolean getPopupOn() { return mPopupOn; } @@ -2438,11 +2038,22 @@ public class LatinIME extends InputMethodService } } - private void updateAutoTextEnabled(Locale systemLocale) { + private void updateAutoTextEnabled() { if (mSuggest == null) return; - boolean different = - !systemLocale.getLanguage().equalsIgnoreCase(mInputLocale.substring(0, 2)); - mSuggest.setAutoTextEnabled(!different && mQuickFixes); + mSuggest.setAutoTextEnabled(mQuickFixes + && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); + } + + private void updateSuggestionVisibility(SharedPreferences prefs) { + final String suggestionVisiblityStr = prefs.getString( + PREF_SHOW_SUGGESTIONS_SETTING, mResources.getString( + R.string.prefs_suggestion_visibility_default_value)); + for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { + if (suggestionVisiblityStr.equals(mResources.getString(visibility))) { + mSuggestionVisibility = visibility; + break; + } + } } protected void launchSettings() { @@ -2453,7 +2064,7 @@ public class LatinIME extends InputMethodService launchSettings(LatinIMEDebugSettings.class); } - protected void launchSettings (Class<? extends PreferenceActivity> settingsClass) { + protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); @@ -2461,55 +2072,78 @@ public class LatinIME extends InputMethodService startActivity(intent); } - private void loadSettings() { + private void loadSettings(EditorInfo attribute) { // Get the settings preferences - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false); - mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); - mPopupOn = sp.getBoolean(PREF_POPUP_ON, + final SharedPreferences prefs = mPrefs; + Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + mVibrateOn = vibrator != null && vibrator.hasVibrator() + && prefs.getBoolean(LatinIMESettings.PREF_VIBRATE_ON, false); + mSoundOn = prefs.getBoolean(PREF_SOUND_ON, false); + mPopupOn = prefs.getBoolean(PREF_POPUP_ON, mResources.getBoolean(R.bool.default_popup_preview)); - mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); - mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); - mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); - mHasUsedVoiceInputUnsupportedLocale = - sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false); - - // Get the current list of supported locales and check the current locale against that - // list. We cache this value so as not to check it every time the user starts a voice - // input. Because this method is called by onStartInputView, this should mean that as - // long as the locale doesn't change while the user is keeping the IME open, the - // value should never be stale. - String supportedLocalesString = SettingsUtil.getSettingsString( - getContentResolver(), - SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, - DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); - ArrayList<String> voiceInputSupportedLocales = - newArrayList(supportedLocalesString.split("\\s+")); - - mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mInputLocale); - - mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true); - - if (VOICE_INSTALLED) { - final String voiceMode = sp.getString(PREF_VOICE_MODE, - getString(R.string.voice_mode_main)); - boolean enableVoice = !voiceMode.equals(getString(R.string.voice_mode_off)) - && mEnableVoiceButton; - boolean voiceOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main)); - if (mKeyboardSwitcher != null && - (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) { - mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary); + mAutoCap = prefs.getBoolean(PREF_AUTO_CAP, true); + mQuickFixes = prefs.getBoolean(PREF_QUICK_FIXES, true); + + mAutoCorrectEnabled = isAutoCorrectEnabled(prefs); + mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs); + loadAndSetAutoCompletionThreshold(prefs); + + mVoiceConnector.loadSettings(attribute, prefs); + + updateCorrectionMode(); + updateAutoTextEnabled(); + updateSuggestionVisibility(prefs); + SubtypeSwitcher.getInstance().loadSettings(); + } + + /** + * load Auto completion threshold from SharedPreferences, + * and modify mSuggest's threshold. + */ + private void loadAndSetAutoCompletionThreshold(SharedPreferences sp) { + // When mSuggest is not initialized, cannnot modify mSuggest's threshold. + if (mSuggest == null) return; + // When auto completion setting is turned off, the threshold is ignored. + if (!isAutoCorrectEnabled(sp)) return; + + final String currentAutoCompletionSetting = sp.getString(PREF_AUTO_COMPLETION_THRESHOLD, + mResources.getString(R.string.auto_completion_threshold_mode_value_modest)); + final String[] autoCompletionThresholdValues = mResources.getStringArray( + R.array.auto_complete_threshold_values); + // When autoCompletionThreshold is greater than 1.0, + // auto completion is virtually turned off. + double autoCompletionThreshold = Double.MAX_VALUE; + try { + final int arrayIndex = Integer.valueOf(currentAutoCompletionSetting); + if (arrayIndex >= 0 && arrayIndex < autoCompletionThresholdValues.length) { + autoCompletionThreshold = Double.parseDouble( + autoCompletionThresholdValues[arrayIndex]); } - mEnableVoice = enableVoice; - mVoiceOnPrimary = voiceOnPrimary; + } catch (NumberFormatException e) { + // Whenever the threshold settings are correct, + // never come here. + autoCompletionThreshold = Double.MAX_VALUE; + Log.w(TAG, "Cannot load auto completion threshold setting." + + " currentAutoCompletionSetting: " + currentAutoCompletionSetting + + ", autoCompletionThresholdValues: " + + Arrays.toString(autoCompletionThresholdValues)); } - mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, - mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; - //mBigramSuggestionEnabled = sp.getBoolean( - // PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions; - updateCorrectionMode(); - updateAutoTextEnabled(mResources.getConfiguration().locale); - mLanguageSwitcher.loadLocales(sp); + // TODO: This should be refactored : + // setAutoCompleteThreshold should be called outside of this method. + mSuggest.setAutoCompleteThreshold(autoCompletionThreshold); + } + + private boolean isAutoCorrectEnabled(SharedPreferences sp) { + final String currentAutoCompletionSetting = sp.getString(PREF_AUTO_COMPLETION_THRESHOLD, + mResources.getString(R.string.auto_completion_threshold_mode_value_modest)); + final String autoCompletionOff = mResources.getString( + R.string.auto_completion_threshold_mode_value_off); + return !currentAutoCompletionSetting.equals(autoCompletionOff); + } + + private boolean isBigramSuggestionEnabled(SharedPreferences sp) { + // TODO: Define default value instead of 'true'. + return sp.getBoolean(PREF_BIGRAM_SUGGESTIONS, true); } private void initSuggestPuncList() { @@ -2544,8 +2178,7 @@ public class LatinIME extends InputMethodService launchSettings(); break; case POS_METHOD: - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) - .showInputMethodPicker(); + mImm.showInputMethodPicker(); break; } } @@ -2561,22 +2194,6 @@ public class LatinIME extends InputMethodService mOptionsDialog.show(); } - private void changeKeyboardMode() { - mKeyboardSwitcher.toggleSymbols(); - if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { - mKeyboardSwitcher.setShiftLocked(mCapsLock); - } - - updateShiftKeyState(getCurrentInputEditorInfo()); - } - - public static <E> ArrayList<E> newArrayList(E... elements) { - int capacity = (elements.length * 110) / 100 + 5; - ArrayList<E> list = new ArrayList<E>(capacity); - Collections.addAll(list, elements); - return list; - } - @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { super.dump(fd, fout, args); @@ -2584,7 +2201,6 @@ public class LatinIME extends InputMethodService final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); - p.println(" mCapsLock=" + mCapsLock); p.println(" mComposing=" + mComposing.toString()); p.println(" mPredictionOn=" + mPredictionOn); p.println(" mCorrectionMode=" + mCorrectionMode); @@ -2619,4 +2235,9 @@ public class LatinIME extends InputMethodService public void onAutoCompletionStateChanged(boolean isAutoCompletion) { mKeyboardSwitcher.onAutoCompletionStateChanged(isAutoCompletion); } + + @Override + public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { + SubtypeSwitcher.getInstance().updateSubtype(subtype); + } } diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java index ffff33da2..ae6646113 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java +++ b/java/src/com/android/inputmethod/latin/LatinIMESettings.java @@ -16,8 +16,8 @@ package com.android.inputmethod.latin; -import java.util.ArrayList; -import java.util.Locale; +import com.android.inputmethod.voice.VoiceIMEConnector; +import com.android.inputmethod.voice.VoiceInputLogger; import android.app.AlertDialog; import android.app.Dialog; @@ -25,16 +25,19 @@ import android.app.backup.BackupManager; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Vibrator; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; import android.speech.SpeechRecognizer; import android.text.AutoText; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; import android.util.Log; +import android.widget.TextView; -import com.android.inputmethod.voice.SettingsUtil; -import com.android.inputmethod.voice.VoiceInputLogger; +import java.util.Locale; public class LatinIMESettings extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener, @@ -43,7 +46,10 @@ public class LatinIMESettings extends PreferenceActivity private static final String QUICK_FIXES_KEY = "quick_fixes"; private static final String PREDICTION_SETTINGS_KEY = "prediction_settings"; private static final String VOICE_SETTINGS_KEY = "voice_mode"; + private static final String PREF_AUTO_COMPLETION_THRESHOLD = "auto_completion_threshold"; + private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; /* package */ static final String PREF_SETTINGS_KEY = "settings_key"; + /* package */ static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String TAG = "LatinIMESettings"; @@ -53,13 +59,23 @@ public class LatinIMESettings extends PreferenceActivity private CheckBoxPreference mQuickFixes; private ListPreference mVoicePreference; private ListPreference mSettingsKeyPreference; + private ListPreference mAutoCompletionThreshold; + private CheckBoxPreference mBigramSuggestion; private boolean mVoiceOn; + private AlertDialog mDialog; + private VoiceInputLogger mLogger; private boolean mOkClicked = false; private String mVoiceModeOff; + private void ensureConsistencyOfAutoCompletionSettings() { + final String autoCompletionOff = getResources().getString( + R.string.auto_completion_threshold_mode_value_off); + final String currentSetting = mAutoCompletionThreshold.getValue(); + mBigramSuggestion.setEnabled(!currentSetting.equals(autoCompletionOff)); + } @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -73,6 +89,28 @@ public class LatinIMESettings extends PreferenceActivity mVoiceModeOff = getString(R.string.voice_mode_off); mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); mLogger = VoiceInputLogger.getLogger(this); + + mAutoCompletionThreshold = (ListPreference) findPreference(PREF_AUTO_COMPLETION_THRESHOLD); + mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS); + ensureConsistencyOfAutoCompletionSettings(); + + final boolean showSettingsKeyOption = getResources().getBoolean( + R.bool.config_enable_show_settings_key_option); + if (!showSettingsKeyOption) { + getPreferenceScreen().removePreference(mSettingsKeyPreference); + } + + final boolean showVoiceKeyOption = getResources().getBoolean( + R.bool.config_enable_show_voice_key_option); + if (!showVoiceKeyOption) { + getPreferenceScreen().removePreference(mVoicePreference); + } + + Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); + if (vibrator == null || !vibrator.hasVibrator()) { + getPreferenceScreen().removePreference( + getPreferenceScreen().findPreference(PREF_VIBRATE_ON)); + } } @Override @@ -83,7 +121,7 @@ public class LatinIMESettings extends PreferenceActivity ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY)) .removePreference(mQuickFixes); } - if (!LatinIME.VOICE_INSTALLED + if (!VoiceIMEConnector.VOICE_INSTALLED || !SpeechRecognizer.isRecognitionAvailable(this)) { getPreferenceScreen().removePreference(mVoicePreference); } else { @@ -108,6 +146,7 @@ public class LatinIMESettings extends PreferenceActivity showVoiceConfirmation(); } } + ensureConsistencyOfAutoCompletionSettings(); mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); updateVoiceModeSummary(); updateSettingsKeySummary(); @@ -122,6 +161,13 @@ public class LatinIMESettings extends PreferenceActivity private void showVoiceConfirmation() { mOkClicked = false; showDialog(VOICE_INPUT_CONFIRM_DIALOG); + // Make URL in the dialog message clickable + if (mDialog != null) { + TextView textView = (TextView) mDialog.findViewById(android.R.id.message); + if (textView != null) { + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } + } } private void updateVoiceModeSummary() { @@ -154,27 +200,23 @@ public class LatinIMESettings extends PreferenceActivity // Get the current list of supported locales and check the current locale against // that list, to decide whether to put a warning that voice input will not work in // the current language as part of the pop-up confirmation dialog. - String supportedLocalesString = SettingsUtil.getSettingsString( - getContentResolver(), - SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, - LatinIME.DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); - ArrayList<String> voiceInputSupportedLocales = - LatinIME.newArrayList(supportedLocalesString.split("\\s+")); - boolean localeSupported = voiceInputSupportedLocales.contains( + boolean localeSupported = SubtypeSwitcher.getInstance().isVoiceSupported( Locale.getDefault().toString()); + final CharSequence message; if (localeSupported) { - String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_hint_dialog_message); - builder.setMessage(message); + message = TextUtils.concat( + getText(R.string.voice_warning_may_not_understand), "\n\n", + getText(R.string.voice_hint_dialog_message)); } else { - String message = getString(R.string.voice_warning_locale_not_supported) + - "\n\n" + getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_hint_dialog_message); - builder.setMessage(message); + message = TextUtils.concat( + getText(R.string.voice_warning_locale_not_supported), "\n\n", + getText(R.string.voice_warning_may_not_understand), "\n\n", + getText(R.string.voice_hint_dialog_message)); } - + builder.setMessage(message); AlertDialog dialog = builder.create(); + mDialog = dialog; dialog.setOnDismissListener(this); mLogger.settingsWarningDialogShown(); return dialog; diff --git a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java index 85ecaee50..a58f630cd 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java +++ b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java @@ -16,12 +16,24 @@ package com.android.inputmethod.latin; -import android.view.inputmethod.InputMethodManager; - -import android.content.Context; +import android.inputmethodservice.InputMethodService; import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; import android.text.format.DateUtils; import android.util.Log; +import android.view.inputmethod.InputMethodManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; public class LatinIMEUtil { @@ -76,9 +88,11 @@ public class LatinIMEUtil { } } - public static boolean hasMultipleEnabledIMEs(Context context) { - return ((InputMethodManager) context.getSystemService( - Context.INPUT_METHOD_SERVICE)).getEnabledInputMethodList().size() > 1; + public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm) { + return imm.getEnabledInputMethodList().size() > 1 + // imm.getEnabledInputMethodSubtypeList(null) will return the current IME's enabled input + // method subtype (The current IME should be LatinIME.) + || imm.getEnabledInputMethodSubtypeList(null).size() > 1; } /* package */ static class RingCharBuffer { @@ -86,8 +100,9 @@ public class LatinIMEUtil { private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; private static final int INVALID_COORDINATE = -2; /* package */ static final int BUFSIZE = 20; - private Context mContext; + private InputMethodService mContext; private boolean mEnabled = false; + private boolean mUsabilityStudy = false; private int mEnd = 0; /* package */ int mLength = 0; private char[] mCharBuf = new char[BUFSIZE]; @@ -99,9 +114,12 @@ public class LatinIMEUtil { public static RingCharBuffer getInstance() { return sRingCharBuffer; } - public static RingCharBuffer init(Context context, boolean enabled) { + public static RingCharBuffer init(InputMethodService context, boolean enabled, + boolean usabilityStudy) { sRingCharBuffer.mContext = context; - sRingCharBuffer.mEnabled = enabled; + sRingCharBuffer.mEnabled = enabled || usabilityStudy; + sRingCharBuffer.mUsabilityStudy = usabilityStudy; + UsabilityStudyLogUtils.getInstance().init(context); return sRingCharBuffer; } private int normalize(int in) { @@ -110,6 +128,9 @@ public class LatinIMEUtil { } public void push(char c, int x, int y) { if (!mEnabled) return; + if (mUsabilityStudy) { + UsabilityStudyLogUtils.getInstance().writeChar(c, x, y); + } mCharBuf[mEnd] = c; mXBuf[mEnd] = x; mYBuf[mEnd] = y; @@ -153,7 +174,7 @@ public class LatinIMEUtil { } } public String getLastString() { - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < mLength; ++i) { char c = mCharBuf[normalize(mEnd - 1 - i)]; if (!((LatinIME)mContext).isWordSeparator(c)) { @@ -168,4 +189,202 @@ public class LatinIMEUtil { mLength = 0; } } + + public static int editDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("editDistance: Arguments should not be null."); + } + final int sl = s.length(); + final int tl = t.length(); + int[][] dp = new int [sl + 1][tl + 1]; + for (int i = 0; i <= sl; i++) { + dp[i][0] = i; + } + for (int j = 0; j <= tl; j++) { + dp[0][j] = j; + } + for (int i = 0; i < sl; ++i) { + for (int j = 0; j < tl; ++j) { + if (s.charAt(i) == t.charAt(j)) { + dp[i + 1][j + 1] = dp[i][j]; + } else { + dp[i + 1][j + 1] = 1 + Math.min(dp[i][j], + Math.min(dp[i + 1][j], dp[i][j + 1])); + } + } + } + return dp[sl][tl]; + } + + // In dictionary.cpp, getSuggestion() method, + // suggestion scores are computed using the below formula. + // original score (called 'frequency') + // := pow(mTypedLetterMultiplier (this is defined 2), + // (the number of matched characters between typed word and suggested word)) + // * (individual word's score which defined in the unigram dictionary, + // and this score is defined in range [0, 255].) + // * (when before.length() == after.length(), + // mFullWordMultiplier (this is defined 2)) + // So, maximum original score is pow(2, before.length()) * 255 * 2 + // So, we can normalize original score by dividing this value. + private static final int MAX_INITIAL_SCORE = 255; + private static final int TYPED_LETTER_MULTIPLIER = 2; + private static final int FULL_WORD_MULTIPLYER = 2; + public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) { + final int beforeLength = before.length(); + final int afterLength = after.length(); + final int distance = editDistance(before, after); + final double maximumScore = MAX_INITIAL_SCORE + * Math.pow(TYPED_LETTER_MULTIPLIER, beforeLength) + * FULL_WORD_MULTIPLYER; + // add a weight based on edit distance. + // distance <= max(afterLength, beforeLength) == afterLength, + // so, 0 <= distance / afterLength <= 1 + final double weight = 1.0 - (double) distance / afterLength; + return (score / maximumScore) * weight; + } + + public static class UsabilityStudyLogUtils { + private static final String TAG = "UsabilityStudyLogUtils"; + private static final String FILENAME = "log.txt"; + private static final UsabilityStudyLogUtils sInstance = + new UsabilityStudyLogUtils(); + private final Handler mLoggingHandler; + private File mFile; + private File mDirectory; + private InputMethodService mIms; + private PrintWriter mWriter; + private final Date mDate; + private final SimpleDateFormat mDateFormat; + + private UsabilityStudyLogUtils() { + mDate = new Date(); + mDateFormat = new SimpleDateFormat("dd MMM HH:mm:ss.SSS"); + + HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task", + Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + mLoggingHandler = new Handler(handlerThread.getLooper()); + } + + public static UsabilityStudyLogUtils getInstance() { + return sInstance; + } + + public void init(InputMethodService ims) { + mIms = ims; + mDirectory = ims.getFilesDir(); + } + + private void createLogFileIfNotExist() { + if ((mFile == null || !mFile.exists()) + && (mDirectory != null && mDirectory.exists())) { + try { + mWriter = getPrintWriter(mDirectory, FILENAME, false); + } catch (IOException e) { + Log.e(TAG, "Can't create log file."); + } + } + } + + public void writeBackSpace() { + UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0"); + } + + public void writeChar(char c, int x, int y) { + String inputChar = String.valueOf(c); + switch (c) { + case '\n': + inputChar = "<enter>"; + break; + case '\t': + inputChar = "<tab>"; + break; + case ' ': + inputChar = "<space>"; + break; + } + UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y); + LatinImeLogger.onPrintAllUsabilityStudtyLogs(); + } + + public void write(final String log) { + mLoggingHandler.post(new Runnable() { + public void run() { + createLogFileIfNotExist(); + final long currentTime = System.currentTimeMillis(); + mDate.setTime(currentTime); + + final String printString = String.format("%s\t%d\t%s\n", + mDateFormat.format(mDate), currentTime, log); + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Write: " + log); + } + mWriter.print(printString); + } + }); + } + + public void printAll() { + mLoggingHandler.post(new Runnable() { + public void run() { + mWriter.flush(); + StringBuilder sb = new StringBuilder(); + BufferedReader br = getBufferedReader(); + String line; + try { + while ((line = br.readLine()) != null) { + sb.append('\n'); + sb.append(line); + } + } catch (IOException e) { + Log.e(TAG, "Can't read log file."); + } finally { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "output all logs\n" + sb.toString()); + } + mIms.getCurrentInputConnection().commitText(sb.toString(), 0); + try { + br.close(); + } catch (IOException e) { + } + } + } + }); + } + + public void clearAll() { + mLoggingHandler.post(new Runnable() { + public void run() { + if (mFile != null && mFile.exists()) { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Delete log file."); + } + mFile.delete(); + mWriter.close(); + } + } + }); + } + + private BufferedReader getBufferedReader() { + createLogFileIfNotExist(); + try { + return new BufferedReader(new FileReader(mFile)); + } catch (FileNotFoundException e) { + return null; + } + } + + private PrintWriter getPrintWriter( + File dir, String filename, boolean renew) throws IOException { + mFile = new File(dir, filename); + if (mFile.exists()) { + if (renew) { + mFile.delete(); + } + } + return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */); + } + } } diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index a8ab9cc98..b64e3ddb2 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -20,15 +20,17 @@ import com.android.inputmethod.latin.Dictionary.DataType; import android.content.Context; import android.content.SharedPreferences; -import android.inputmethodservice.Keyboard; + import java.util.List; public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener { + public static boolean sDBG = false; + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { } - public static void init(Context context) { + public static void init(Context context, SharedPreferences prefs) { } public static void commit() { @@ -65,7 +67,9 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void onAddSuggestedWord(String word, int typeId, DataType dataType) { } - public static void onSetKeyboard(Keyboard kb) { + public static void onSetKeyboard(BaseKeyboard kb) { } + public static void onPrintAllUsabilityStudtyLogs() { + } } diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java index 45a4a9508..cae0b10b3 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -30,57 +30,36 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.inputmethodservice.Keyboard; import android.text.TextPaint; import android.util.Log; import android.view.ViewConfiguration; -import android.view.inputmethod.EditorInfo; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; -public class LatinKeyboard extends Keyboard { +public class LatinKeyboard extends BaseKeyboard { private static final boolean DEBUG_PREFERRED_LETTER = false; private static final String TAG = "LatinKeyboard"; private static final int OPACITY_FULLY_OPAQUE = 255; private static final int SPACE_LED_LENGTH_PERCENT = 80; - private Drawable mShiftLockIcon; private Drawable mShiftLockPreviewIcon; - private Drawable mOldShiftIcon; + private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>(); private Drawable mSpaceIcon; private Drawable mSpaceAutoCompletionIndicator; private Drawable mSpacePreviewIcon; - private Drawable mMicIcon; - private Drawable mMicPreviewIcon; - private Drawable m123MicIcon; - private Drawable m123MicPreviewIcon; private final Drawable mButtonArrowLeftIcon; private final Drawable mButtonArrowRightIcon; - private Key mShiftKey; - private Key mEnterKey; - private Key mF1Key; - private final Drawable mHintIcon; + private final int mSpaceBarTextShadowColor; private Key mSpaceKey; - private Key m123Key; - private final int NUMBER_HINT_COUNT = 10; - private Key[] mNumberHintKeys; - private Drawable[] mNumberHintIcons = new Drawable[NUMBER_HINT_COUNT]; private int mSpaceKeyIndex = -1; private int mSpaceDragStartX; private int mSpaceDragLastDiff; - private Locale mLocale; - private LanguageSwitcher mLanguageSwitcher; private final Resources mRes; private final Context mContext; - private int mMode; - // Whether this keyboard has voice icon on it - private boolean mHasVoiceButton; - // Whether voice icon is enabled at all - private boolean mVoiceEnabled; - private final boolean mIsAlphaKeyboard; - private CharSequence m123Label; private boolean mCurrentlyInSpace; private SlidingLocaleDrawable mSlidingLocaleIcon; private int[] mPrefLetterFrequencies; @@ -89,18 +68,7 @@ public class LatinKeyboard extends Keyboard { private int mPrefLetterY; private int mPrefDistance; - // TODO: generalize for any keyboardId - private boolean mIsBlackSym; - - // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes - // non-private. - private final int mVerticalGap; - - private static final int SHIFT_OFF = 0; - private static final int SHIFT_ON = 1; - private static final int SHIFT_LOCKED = 2; - - private int mShiftState = SHIFT_OFF; + private LatinKeyboardShiftState mShiftState = new LatinKeyboardShiftState(); private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; @@ -115,354 +83,141 @@ public class LatinKeyboard extends Keyboard { private static int sSpacebarVerticalCorrection; - public LatinKeyboard(Context context, int xmlLayoutResId) { - this(context, xmlLayoutResId, 0); - } - - public LatinKeyboard(Context context, int xmlLayoutResId, int mode) { - super(context, xmlLayoutResId, mode); + public LatinKeyboard(Context context, KeyboardSwitcher.KeyboardId id) { + super(context, id); final Resources res = context.getResources(); mContext = context; - mMode = mode; mRes = res; - mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); + if (id.mColorScheme == BaseKeyboardView.COLOR_SCHEME_BLACK) { + mSpaceBarTextShadowColor = res.getColor( + R.color.latinkeyboard_bar_language_shadow_black); + } else { // default color scheme is BaseKeyboardView.COLOR_SCHEME_WHITE + mSpaceBarTextShadowColor = res.getColor( + R.color.latinkeyboard_bar_language_shadow_white); + } mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); setDefaultBounds(mShiftLockPreviewIcon); - mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led); - mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space); - mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic); - mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic); - setDefaultBounds(mMicPreviewIcon); mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); - m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic); - m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic); - mHintIcon = res.getDrawable(R.drawable.hint_popup); - setDefaultBounds(m123MicPreviewIcon); sSpacebarVerticalCorrection = res.getDimensionPixelOffset( R.dimen.spacebar_vertical_correction); - mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty - || xmlLayoutResId == R.xml.kbd_qwerty_black; mSpaceKeyIndex = indexOf(LatinIME.KEYCODE_SPACE); - initializeNumberHintResources(context); - // TODO remove this initialization after cleanup - mVerticalGap = super.getVerticalGap(); - } - - private void initializeNumberHintResources(Context context) { - final Resources res = context.getResources(); - mNumberHintIcons[0] = res.getDrawable(R.drawable.keyboard_hint_0); - mNumberHintIcons[1] = res.getDrawable(R.drawable.keyboard_hint_1); - mNumberHintIcons[2] = res.getDrawable(R.drawable.keyboard_hint_2); - mNumberHintIcons[3] = res.getDrawable(R.drawable.keyboard_hint_3); - mNumberHintIcons[4] = res.getDrawable(R.drawable.keyboard_hint_4); - mNumberHintIcons[5] = res.getDrawable(R.drawable.keyboard_hint_5); - mNumberHintIcons[6] = res.getDrawable(R.drawable.keyboard_hint_6); - mNumberHintIcons[7] = res.getDrawable(R.drawable.keyboard_hint_7); - mNumberHintIcons[8] = res.getDrawable(R.drawable.keyboard_hint_8); - mNumberHintIcons[9] = res.getDrawable(R.drawable.keyboard_hint_9); } @Override protected Key createKeyFromXml(Resources res, Row parent, int x, int y, - XmlResourceParser parser) { - Key key = new LatinKey(res, parent, x, y, parser); + XmlResourceParser parser, KeyStyles keyStyles) { + Key key = new LatinKey(res, parent, x, y, parser, keyStyles); switch (key.codes[0]) { - case LatinIME.KEYCODE_ENTER: - mEnterKey = key; - break; - case LatinKeyboardView.KEYCODE_F1: - mF1Key = key; - break; case LatinIME.KEYCODE_SPACE: mSpaceKey = key; + mSpaceIcon = key.icon; + mSpacePreviewIcon = key.iconPreview; break; - case KEYCODE_MODE_CHANGE: - m123Key = key; - m123Label = key.label; - break; - } - - // For number hints on the upper-right corner of key - if (mNumberHintKeys == null) { - // NOTE: This protected method is being called from the base class constructor before - // mNumberHintKeys gets initialized. - mNumberHintKeys = new Key[NUMBER_HINT_COUNT]; - } - int hintNumber = -1; - if (LatinKeyboardBaseView.isNumberAtLeftmostPopupChar(key)) { - hintNumber = key.popupCharacters.charAt(0) - '0'; - } else if (LatinKeyboardBaseView.isNumberAtRightmostPopupChar(key)) { - hintNumber = key.popupCharacters.charAt(key.popupCharacters.length() - 1) - '0'; - } - if (hintNumber >= 0 && hintNumber <= 9) { - mNumberHintKeys[hintNumber] = key; } return key; } - void setImeOptions(Resources res, int mode, int options) { - mMode = mode; - // TODO should clean up this method - if (mEnterKey != null) { - // Reset some of the rarely used attributes. - mEnterKey.popupCharacters = null; - mEnterKey.popupResId = 0; - mEnterKey.text = null; - switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { - case EditorInfo.IME_ACTION_GO: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_go_key); - break; - case EditorInfo.IME_ACTION_NEXT: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_next_key); - break; - case EditorInfo.IME_ACTION_DONE: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_done_key); - break; - case EditorInfo.IME_ACTION_SEARCH: - mEnterKey.iconPreview = res.getDrawable( - R.drawable.sym_keyboard_feedback_search); - mEnterKey.icon = res.getDrawable(mIsBlackSym ? - R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search); - mEnterKey.label = null; - break; - case EditorInfo.IME_ACTION_SEND: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_send_key); - break; - default: - if (mode == KeyboardSwitcher.MODE_IM) { - mEnterKey.icon = mHintIcon; - mEnterKey.iconPreview = null; - mEnterKey.label = ":-)"; - mEnterKey.text = ":-) "; - mEnterKey.popupResId = R.xml.popup_smileys; - } else { - mEnterKey.iconPreview = res.getDrawable( - R.drawable.sym_keyboard_feedback_return); - mEnterKey.icon = res.getDrawable(mIsBlackSym ? - R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return); - mEnterKey.label = null; - } - break; - } - // Set the initial size of the preview icon - if (mEnterKey.iconPreview != null) { - setDefaultBounds(mEnterKey.iconPreview); + public void enableShiftLock() { + for (final Key key : getShiftKeys()) { + if (key instanceof LatinKey) { + ((LatinKey)key).enableShiftLock(); } - } - } - - void enableShiftLock() { - int index = getShiftKeyIndex(); - if (index >= 0) { - mShiftKey = getKeys().get(index); - if (mShiftKey instanceof LatinKey) { - ((LatinKey)mShiftKey).enableShiftLock(); - } - mOldShiftIcon = mShiftKey.icon; + mNormalShiftIcons.put(key, key.icon); } } - void setShiftLocked(boolean shiftLocked) { - if (mShiftKey != null) { - if (shiftLocked) { - mShiftKey.on = true; - mShiftKey.icon = mShiftLockIcon; - mShiftState = SHIFT_LOCKED; - } else { - mShiftKey.on = false; - mShiftKey.icon = mShiftLockIcon; - mShiftState = SHIFT_ON; - } + public boolean setShiftLocked(boolean newShiftLockState) { + final Map<Key, Drawable> shiftedIcons = getShiftedIcons(); + for (final Key key : getShiftKeys()) { + key.on = newShiftLockState; + key.icon = newShiftLockState ? shiftedIcons.get(key) : mNormalShiftIcons.get(key); } + mShiftState.setShiftLocked(newShiftLockState); + return true; } - boolean isShiftLocked() { - return mShiftState == SHIFT_LOCKED; + public boolean isShiftLocked() { + return mShiftState.isShiftLocked(); } - + @Override - public boolean setShifted(boolean shiftState) { - boolean shiftChanged = false; - if (mShiftKey != null) { - if (shiftState == false) { - shiftChanged = mShiftState != SHIFT_OFF; - mShiftState = SHIFT_OFF; - mShiftKey.on = false; - mShiftKey.icon = mOldShiftIcon; - } else { - if (mShiftState == SHIFT_OFF) { - shiftChanged = mShiftState == SHIFT_OFF; - mShiftState = SHIFT_ON; - mShiftKey.icon = mShiftLockIcon; - } + public boolean setShifted(boolean newShiftState) { + if (getShiftKeys().size() == 0) + return super.setShifted(newShiftState); + + final Map<Key, Drawable> shiftedIcons = getShiftedIcons(); + for (final Key key : getShiftKeys()) { + if (!newShiftState && !mShiftState.isShiftLocked()) { + key.icon = mNormalShiftIcons.get(key); + } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) { + key.icon = shiftedIcons.get(key); } - } else { - return super.setShifted(shiftState); } - return shiftChanged; + return mShiftState.setShifted(newShiftState); } @Override - public boolean isShifted() { - if (mShiftKey != null) { - return mShiftState != SHIFT_OFF; + public boolean isShiftedOrShiftLocked() { + if (getShiftKeys().size() > 0) { + return mShiftState.isShiftedOrShiftLocked(); } else { - return super.isShifted(); - } - } - - /* package */ boolean isAlphaKeyboard() { - return mIsAlphaKeyboard; - } - - public void setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack) { - mIsBlackSym = isBlack; - if (isBlack) { - mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked); - mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space); - mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic); - m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic); - } else { - mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked); - mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space); - mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic); - m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic); - } - updateDynamicKeys(); - if (mSpaceKey != null) { - updateSpaceBarForLocale(isAutoCompletion, isBlack); + return super.isShiftedOrShiftLocked(); } - updateNumberHintKeys(); } - private void setDefaultBounds(Drawable drawable) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + public void setAutomaticTemporaryUpperCase() { + setShifted(true); + mShiftState.setAutomaticTemporaryUpperCase(); } - public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) { - mHasVoiceButton = hasVoiceButton; - mVoiceEnabled = hasVoice; - updateDynamicKeys(); + public boolean isAutomaticTemporaryUpperCase() { + return isAlphaKeyboard() && mShiftState.isAutomaticTemporaryUpperCase(); } - private void updateDynamicKeys() { - update123Key(); - updateF1Key(); + public boolean isManualTemporaryUpperCase() { + return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCase(); } - private void update123Key() { - // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode. - if (m123Key != null && mIsAlphaKeyboard) { - if (mVoiceEnabled && !mHasVoiceButton) { - m123Key.icon = m123MicIcon; - m123Key.iconPreview = m123MicPreviewIcon; - m123Key.label = null; - } else { - m123Key.icon = null; - m123Key.iconPreview = null; - m123Key.label = m123Label; - } - } + /* package */ LatinKeyboardShiftState getKeyboardShiftState() { + return mShiftState; } - private void updateF1Key() { - // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key. - if (mF1Key == null) - return; - - if (mIsAlphaKeyboard) { - if (mMode == KeyboardSwitcher.MODE_URL) { - setNonMicF1Key(mF1Key, "/", R.xml.popup_slash); - } else if (mMode == KeyboardSwitcher.MODE_EMAIL) { - setNonMicF1Key(mF1Key, "@", R.xml.popup_at); - } else { - if (mVoiceEnabled && mHasVoiceButton) { - setMicF1Key(mF1Key); - } else { - setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); - } - } - } else { // Symbols keyboard - if (mVoiceEnabled && mHasVoiceButton) { - setMicF1Key(mF1Key); - } else { - setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); - } - } - } - - private void setMicF1Key(Key key) { - // HACK: draw mMicIcon and mHintIcon at the same time - final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes, - drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon)); - - key.label = null; - key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE }; - key.popupResId = R.xml.popup_mic; - key.icon = micWithSettingsHintDrawable; - key.iconPreview = mMicPreviewIcon; - } - - private void setNonMicF1Key(Key key, String label, int popupResId) { - key.label = label; - key.codes = new int[] { label.charAt(0) }; - key.popupResId = popupResId; - key.icon = mHintIcon; - key.iconPreview = null; + public boolean isAlphaKeyboard() { + return mId.getXmlId() == R.xml.kbd_qwerty; } - public boolean isF1Key(Key key) { - return key == mF1Key; + public boolean isPhoneKeyboard() { + return mId.mMode == KeyboardSwitcher.MODE_PHONE; } - public static boolean hasPuncOrSmileysPopup(Key key) { - return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys; + public boolean isNumberKeyboard() { + return mId.mMode == KeyboardSwitcher.MODE_NUMBER; } /** * @return a key which should be invalidated. */ public Key onAutoCompletionStateChanged(boolean isAutoCompletion) { - updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym); + updateSpaceBarForLocale(isAutoCompletion); return mSpaceKey; } - private void updateNumberHintKeys() { - for (int i = 0; i < mNumberHintKeys.length; ++i) { - if (mNumberHintKeys[i] != null) { - mNumberHintKeys[i].icon = mNumberHintIcons[i]; - } - } - } - - public boolean isLanguageSwitchEnabled() { - return mLocale != null; - } - - private void updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack) { + private void updateSpaceBarForLocale(boolean isAutoCompletion) { + final Resources res = mRes; // If application locales are explicitly selected. - if (mLocale != null) { - mSpaceKey.icon = new BitmapDrawable(mRes, - drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); + if (SubtypeSwitcher.getInstance().needsToDisplayLanguage()) { + mSpaceKey.icon = new BitmapDrawable(res, + drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion)); } else { // sym_keyboard_space_led can be shared with Black and White symbol themes. if (isAutoCompletion) { - mSpaceKey.icon = new BitmapDrawable(mRes, - drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); + mSpaceKey.icon = new BitmapDrawable(res, + drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion)); } else { - mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space) - : mRes.getDrawable(R.drawable.sym_keyboard_space); + mSpaceKey.icon = mSpaceIcon; } } } @@ -474,34 +229,6 @@ public class LatinKeyboard extends Keyboard { return bounds.width(); } - // Overlay two images: mainIcon and hintIcon. - private Bitmap drawSynthesizedSettingsHintImage( - int width, int height, Drawable mainIcon, Drawable hintIcon) { - if (mainIcon == null || hintIcon == null) - return null; - Rect hintIconPadding = new Rect(0, 0, 0, 0); - hintIcon.getPadding(hintIconPadding); - final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(buffer); - canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); - - // Draw main icon at the center of the key visual - // Assuming the hintIcon shares the same padding with the key's background drawable - final int drawableX = (width + hintIconPadding.left - hintIconPadding.right - - mainIcon.getIntrinsicWidth()) / 2; - final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom - - mainIcon.getIntrinsicHeight()) / 2; - setDefaultBounds(mainIcon); - canvas.translate(drawableX, drawableY); - mainIcon.draw(canvas); - canvas.translate(-drawableX, -drawableY); - - // Draw hint icon fully in the key - hintIcon.setBounds(0, 0, width, height); - hintIcon.draw(canvas); - return buffer; - } - // Layout local language name and left and right arrow on space bar. private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow, Drawable rArrow, int width, int height, float origTextSize, @@ -512,7 +239,7 @@ public class LatinKeyboard extends Keyboard { final Rect bounds = new Rect(); // Estimate appropriate language name text size to fit in maxTextWidth. - String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); + String language = SubtypeSwitcher.getDisplayLanguage(locale); int textWidth = getTextWidth(paint, language, origTextSize, bounds); // Assuming text width and text size are proportional to each other. float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); @@ -528,7 +255,7 @@ public class LatinKeyboard extends Keyboard { textSize = origTextSize; } if (useShortName) { - language = LanguageSwitcher.toTitleCase(locale.getLanguage()); + language = SubtypeSwitcher.getShortDisplayLanguage(locale); textWidth = getTextWidth(paint, language, origTextSize, bounds); textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); } @@ -545,39 +272,39 @@ public class LatinKeyboard extends Keyboard { return language; } - private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack) { + private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion) { final int width = mSpaceKey.width; final int height = mSpaceIcon.getIntrinsicHeight(); final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(buffer); - canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); + final Resources res = mRes; + canvas.drawColor(res.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); + SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance(); // If application locales are explicitly selected. - if (mLocale != null) { + if (subtypeSwitcher.needsToDisplayLanguage()) { final Paint paint = new Paint(); paint.setAlpha(opacity); paint.setAntiAlias(true); paint.setTextAlign(Align.CENTER); final boolean allowVariableTextSize = true; - final String language = layoutSpaceBar(paint, mLanguageSwitcher.getInputLocale(), + final String language = layoutSpaceBar(paint, subtypeSwitcher.getInputLocale(), mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height, getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14), allowVariableTextSize); // Draw language text with shadow - final int shadowColor = mRes.getColor(isBlack - ? R.color.latinkeyboard_bar_language_shadow_black - : R.color.latinkeyboard_bar_language_shadow_white); final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; final float descent = paint.descent(); - paint.setColor(shadowColor); + paint.setColor(mSpaceBarTextShadowColor); canvas.drawText(language, width / 2, baseline - descent - 1, paint); - paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text)); + paint.setColor(res.getColor(R.color.latinkeyboard_bar_language_text)); canvas.drawText(language, width / 2, baseline - descent, paint); // Put arrows that are already layed out on either side of the text - if (mLanguageSwitcher.getLocaleCount() > 1) { + if (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER + && subtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) { mButtonArrowLeftIcon.draw(canvas); mButtonArrowRightIcon.draw(canvas); } @@ -621,40 +348,23 @@ public class LatinKeyboard extends Keyboard { } public int getLanguageChangeDirection() { - if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2 - || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) { + if (mSpaceKey == null || SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() <= 1 + || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD) { return 0; // No change } return mSpaceDragLastDiff > 0 ? 1 : -1; } - public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion, - boolean isBlackSym) { - mLanguageSwitcher = switcher; - Locale locale = mLanguageSwitcher.getLocaleCount() > 0 - ? mLanguageSwitcher.getInputLocale() - : null; - // If the language count is 1 and is the same as the system language, don't show it. - if (locale != null - && mLanguageSwitcher.getLocaleCount() == 1 - && mLanguageSwitcher.getSystemLocale().getLanguage() - .equalsIgnoreCase(locale.getLanguage())) { - locale = null; - } - mLocale = locale; - setColorOfSymbolIcons(isAutoCompletion, isBlackSym); - } - boolean isCurrentlyInSpace() { return mCurrentlyInSpace; } - void setPreferredLetters(int[] frequencies) { + public void setPreferredLetters(int[] frequencies) { mPrefLetterFrequencies = frequencies; mPrefLetter = 0; } - void keyReleased() { + public void keyReleased() { mCurrentlyInSpace = false; mSpaceDragLastDiff = 0; mPrefLetter = 0; @@ -670,16 +380,16 @@ public class LatinKeyboard extends Keyboard { * Does the magic of locking the touch gesture into the spacebar when * switching input languages. */ - boolean isInside(LatinKey key, int x, int y) { + public boolean isInside(LatinKey key, int x, int y) { final int code = key.codes[0]; - if (code == KEYCODE_SHIFT || - code == KEYCODE_DELETE) { + if (code == KEYCODE_SHIFT || code == KEYCODE_DELETE) { y -= key.height / 10; if (code == KEYCODE_SHIFT) x += key.width / 6; if (code == KEYCODE_DELETE) x -= key.width / 6; } else if (code == LatinIME.KEYCODE_SPACE) { y += LatinKeyboard.sSpacebarVerticalCorrection; - if (mLanguageSwitcher.getLocaleCount() > 1) { + if (SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCHER + && SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() > 1) { if (mCurrentlyInSpace) { int diff = x - mSpaceDragStartX; if (Math.abs(diff - mSpaceDragLastDiff) > 0) { @@ -819,8 +529,7 @@ public class LatinKeyboard extends Keyboard { return textSize; } - // TODO LatinKey could be static class - class LatinKey extends Keyboard.Key { + public static class LatinKey extends BaseKeyboard.Key { // functional normal state (with properties) private final int[] KEY_STATE_FUNCTIONAL_NORMAL = { @@ -835,9 +544,9 @@ public class LatinKeyboard extends Keyboard { private boolean mShiftLockEnabled; - public LatinKey(Resources res, Keyboard.Row parent, int x, int y, - XmlResourceParser parser) { - super(res, parent, x, y, parser); + public LatinKey(Resources res, BaseKeyboard.Row parent, int x, int y, + XmlResourceParser parser, KeyStyles keyStyles) { + super(res, parent, x, y, parser, keyStyles); if (popupCharacters != null && popupCharacters.length() == 0) { // If there is a keyboard with no keys specified in popupCharacters popupResId = 0; @@ -868,13 +577,12 @@ public class LatinKeyboard extends Keyboard { */ @Override public boolean isInside(int x, int y) { - // TODO This should be done by parent.isInside(this, x, y) - // if Key.parent were protected. - boolean result = LatinKeyboard.this.isInside(this, x, y); + boolean result = (keyboard instanceof LatinKeyboard) + && ((LatinKeyboard)keyboard).isInside(this, x, y); return result; } - boolean isInsideSuper(int x, int y) { + private boolean isInsideSuper(int x, int y) { return super.isInside(x, y); } @@ -889,15 +597,6 @@ public class LatinKeyboard extends Keyboard { } return super.getCurrentDrawableState(); } - - @Override - public int squaredDistanceFrom(int x, int y) { - // We should count vertical gap between rows to calculate the center of this Key. - final int verticalGap = LatinKeyboard.this.mVerticalGap; - final int xDist = this.x + width / 2 - x; - final int yDist = this.y + (height + verticalGap) / 2 - y; - return xDist * xDist + yDist * yDist; - } } /** @@ -905,7 +604,7 @@ public class LatinKeyboard extends Keyboard { * languages by swiping the spacebar. It draws the current, previous and * next languages and moves them by the delta of touch movement on the spacebar. */ - class SlidingLocaleDrawable extends Drawable { + private class SlidingLocaleDrawable extends Drawable { private final int mWidth; private final int mHeight; @@ -926,17 +625,19 @@ public class LatinKeyboard extends Keyboard { setDefaultBounds(mBackground); mWidth = width; mHeight = height; - mTextPaint = new TextPaint(); - mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18)); - mTextPaint.setColor(R.color.latinkeyboard_transparent); - mTextPaint.setTextAlign(Align.CENTER); - mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE); - mTextPaint.setAntiAlias(true); + final TextPaint textPaint = new TextPaint(); + textPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18)); + textPaint.setColor(R.color.latinkeyboard_transparent); + textPaint.setTextAlign(Align.CENTER); + textPaint.setAlpha(OPACITY_FULLY_OPAQUE); + textPaint.setAntiAlias(true); + mTextPaint = textPaint; mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2; - mLeftDrawable = - mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left); - mRightDrawable = - mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right); + final Resources res = mRes; + mLeftDrawable = res.getDrawable( + R.drawable.sym_keyboard_feedback_language_arrows_left); + mRightDrawable = res.getDrawable( + R.drawable.sym_keyboard_feedback_language_arrows_right); mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop(); } @@ -953,9 +654,6 @@ public class LatinKeyboard extends Keyboard { invalidateSelf(); } - private String getLanguageName(Locale locale) { - return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); - } @Override public void draw(Canvas canvas) { @@ -969,10 +667,10 @@ public class LatinKeyboard extends Keyboard { final Drawable rArrow = mRightDrawable; canvas.clipRect(0, 0, width, height); if (mCurrentLanguage == null) { - final LanguageSwitcher languageSwitcher = mLanguageSwitcher; - mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale()); - mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale()); - mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale()); + SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance(); + mCurrentLanguage = subtypeSwitcher.getInputLanguageName(); + mNextLanguage = subtypeSwitcher.getNextInputLanguageName(); + mPrevLanguage = subtypeSwitcher.getPreviousInputLanguageName(); } // Draw language text with shadow final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent(); diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java b/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java new file mode 100644 index 000000000..e916306c8 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java @@ -0,0 +1,89 @@ +package com.android.inputmethod.latin; + +import android.util.Log; + +public class LatinKeyboardShiftState { + private static final String TAG = "LatinKeyboardShiftState"; + private static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE; + + private static final int NORMAL = 0; + private static final int MANUAL_SHIFTED = 1; + private static final int SHIFT_LOCKED = 2; + private static final int AUTO_SHIFTED = 3; + private static final int SHIFT_LOCK_SHIFTED = 4; + + private int mState = NORMAL; + + public boolean setShifted(boolean newShiftState) { + final int oldState = mState; + if (newShiftState) { + if (oldState == NORMAL || oldState == AUTO_SHIFTED) { + mState = MANUAL_SHIFTED; + } else if (oldState == SHIFT_LOCKED) { + mState = SHIFT_LOCK_SHIFTED; + } + } else { + if (oldState == MANUAL_SHIFTED || oldState == AUTO_SHIFTED) { + mState = NORMAL; + } else if (oldState == SHIFT_LOCK_SHIFTED) { + mState = SHIFT_LOCKED; + } + } + if (DEBUG) + Log.d(TAG, "setShifted(" + newShiftState + "): " + toString(oldState) + " > " + this); + return mState != oldState; + } + + public void setShiftLocked(boolean newShiftLockState) { + final int oldState = mState; + if (newShiftLockState) { + if (oldState == NORMAL || oldState == MANUAL_SHIFTED || oldState == AUTO_SHIFTED) + mState = SHIFT_LOCKED; + } else { + if (oldState == SHIFT_LOCKED || oldState == SHIFT_LOCK_SHIFTED) + mState = NORMAL; + } + if (DEBUG) + Log.d(TAG, "setShiftLocked(" + newShiftLockState + "): " + toString(oldState) + + " > " + this); + } + + public void setAutomaticTemporaryUpperCase() { + final int oldState = mState; + mState = AUTO_SHIFTED; + if (DEBUG) + Log.d(TAG, "setAutomaticTemporaryUpperCase: " + toString(oldState) + " > " + this); + } + + public boolean isShiftedOrShiftLocked() { + return mState != NORMAL; + } + + public boolean isShiftLocked() { + return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED; + } + + public boolean isAutomaticTemporaryUpperCase() { + return mState == AUTO_SHIFTED; + } + + public boolean isManualTemporaryUpperCase() { + return mState == MANUAL_SHIFTED || mState == SHIFT_LOCK_SHIFTED; + } + + @Override + public String toString() { + return toString(mState); + } + + private static String toString(int state) { + switch (state) { + case NORMAL: return "NORMAL"; + case MANUAL_SHIFTED: return "MANUAL_SHIFTED"; + case SHIFT_LOCKED: return "SHIFT_LOCKED"; + case AUTO_SHIFTED: return "AUTO_SHIFTED"; + case SHIFT_LOCK_SHIFTED: return "SHIFT_LOCK_SHIFTED"; + default: return "UKNOWN"; + } + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java index 22d39f7aa..ac68e3c39 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -16,11 +16,12 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.BaseKeyboard.Key; +import com.android.inputmethod.voice.VoiceIMEConnector; + import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; import android.os.Handler; import android.os.Message; import android.os.SystemClock; @@ -30,16 +31,15 @@ import android.view.MotionEvent; import java.util.List; -public class LatinKeyboardView extends LatinKeyboardBaseView { - - static final int KEYCODE_OPTIONS = -100; - static final int KEYCODE_OPTIONS_LONGPRESS = -101; - static final int KEYCODE_VOICE = -102; - static final int KEYCODE_F1 = -103; - static final int KEYCODE_NEXT_LANGUAGE = -104; - static final int KEYCODE_PREV_LANGUAGE = -105; +public class LatinKeyboardView extends BaseKeyboardView { - private Keyboard mPhoneKeyboard; + public static final int KEYCODE_OPTIONS = -100; + public static final int KEYCODE_OPTIONS_LONGPRESS = -101; + // TODO: remove this once LatinIME stops referring to this. + public static final int KEYCODE_VOICE = -102; + public static final int KEYCODE_NEXT_LANGUAGE = -104; + public static final int KEYCODE_PREV_LANGUAGE = -105; + public static final int KEYCODE_CAPSLOCK = -106; /** Whether we've started dropping move events because we found a big jump */ private boolean mDroppingEvents; @@ -61,22 +61,19 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { super(context, attrs, defStyle); } - public void setPhoneKeyboard(Keyboard phoneKeyboard) { - mPhoneKeyboard = phoneKeyboard; - } - @Override public void setPreviewEnabled(boolean previewEnabled) { - if (getKeyboard() == mPhoneKeyboard) { - // Phone keyboard never shows popup preview (except language switch). + LatinKeyboard latinKeyboard = getLatinKeyboard(); + if (latinKeyboard != null + && (latinKeyboard.isPhoneKeyboard() || latinKeyboard.isNumberKeyboard())) { + // Phone and number keyboard never shows popup preview (except language switch). super.setPreviewEnabled(false); } else { super.setPreviewEnabled(previewEnabled); } } - @Override - public void setKeyboard(Keyboard k) { + public void setLatinKeyboard(LatinKeyboard k) { super.setKeyboard(k); // One-seventh of the keyboard width seems like a reasonable threshold mJumpThresholdSquare = k.getMinWidth() / 7; @@ -86,12 +83,21 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { setKeyboardLocal(k); } + public LatinKeyboard getLatinKeyboard() { + BaseKeyboard keyboard = getKeyboard(); + if (keyboard instanceof LatinKeyboard) { + return (LatinKeyboard)keyboard; + } else { + return null; + } + } + @Override protected boolean onLongPress(Key key) { int primaryCode = key.codes[0]; if (primaryCode == KEYCODE_OPTIONS) { return invokeOnKey(KEYCODE_OPTIONS_LONGPRESS); - } else if (primaryCode == '0' && getKeyboard() == mPhoneKeyboard) { + } else if (primaryCode == '0' && getLatinKeyboard().isPhoneKeyboard()) { // Long pressing on 0 in phone number keypad gives you a '+'. return invokeOnKey('+'); } else { @@ -101,17 +107,16 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { private boolean invokeOnKey(int primaryCode) { getOnKeyboardActionListener().onKey(primaryCode, null, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE); + BaseKeyboardView.NOT_A_TOUCH_COORDINATE, + BaseKeyboardView.NOT_A_TOUCH_COORDINATE); return true; } @Override protected CharSequence adjustCase(CharSequence label) { - Keyboard keyboard = getKeyboard(); - if (keyboard.isShifted() - && keyboard instanceof LatinKeyboard - && ((LatinKeyboard) keyboard).isAlphaKeyboard() + LatinKeyboard keyboard = getLatinKeyboard(); + if (keyboard.isAlphaKeyboard() + && keyboard.isShiftedOrShiftLocked() && !TextUtils.isEmpty(label) && label.length() < 3 && Character.isLowerCase(label.charAt(0))) { label = label.toString().toUpperCase(); @@ -119,16 +124,6 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { return label; } - public boolean setShiftLocked(boolean shiftLocked) { - Keyboard keyboard = getKeyboard(); - if (keyboard instanceof LatinKeyboard) { - ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked); - invalidateAllKeys(); - return true; - } - return false; - } - /** * This function checks to see if we need to handle any sudden jumps in the pointer location * that could be due to a multi-touch being treated as a move by the firmware or hardware. @@ -208,7 +203,7 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { @Override public boolean onTouchEvent(MotionEvent me) { - LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); + LatinKeyboard keyboard = getLatinKeyboard(); if (DEBUG_LINE) { mLastX = (int) me.getX(); mLastY = (int) me.getY(); @@ -257,7 +252,7 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { private int mLastY; private Paint mPaint; - private void setKeyboardLocal(Keyboard k) { + private void setKeyboardLocal(LatinKeyboard k) { if (DEBUG_AUTO_PLAY) { findKeys(); if (mHandler2 == null) { @@ -318,7 +313,7 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { } private void findKeys() { - List<Key> keys = getKeyboard().getKeys(); + List<Key> keys = getLatinKeyboard().getKeys(); // Get the keys on this keyboard for (int i = 0; i < keys.size(); i++) { int code = keys.get(i).codes[0]; @@ -372,4 +367,10 @@ public class LatinKeyboardView extends LatinKeyboardBaseView { c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); } } + + @Override + protected void onAttachedToWindow() { + // Token is available from here. + VoiceIMEConnector.getInstance().onAttachedToWindow(); + } } diff --git a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java b/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java index 356e62d48..3cc43b99c 100644 --- a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java +++ b/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java @@ -16,7 +16,7 @@ package com.android.inputmethod.latin; -import android.inputmethodservice.Keyboard.Key; +import com.android.inputmethod.latin.BaseKeyboard.Key; class MiniKeyboardKeyDetector extends KeyDetector { private static final int MAX_NEARBY_KEYS = 1; @@ -41,18 +41,19 @@ class MiniKeyboardKeyDetector extends KeyDetector { final Key[] keys = getKeys(); final int touchX = getTouchX(x); final int touchY = getTouchY(y); - int closestKeyIndex = LatinKeyboardBaseView.NOT_A_KEY; + + int closestKeyIndex = BaseKeyboardView.NOT_A_KEY; int closestKeyDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare; final int keyCount = keys.length; - for (int i = 0; i < keyCount; i++) { - final Key key = keys[i]; - int dist = key.squaredDistanceFrom(touchX, touchY); + for (int index = 0; index < keyCount; index++) { + final int dist = keys[index].squaredDistanceToEdge(touchX, touchY); if (dist < closestKeyDist) { - closestKeyIndex = i; + closestKeyIndex = index; closestKeyDist = dist; } } - if (allKeys != null && closestKeyIndex != LatinKeyboardBaseView.NOT_A_KEY) + + if (allKeys != null && closestKeyIndex != BaseKeyboardView.NOT_A_KEY) allKeys[0] = keys[closestKeyIndex].codes[0]; return closestKeyIndex; } diff --git a/java/src/com/android/inputmethod/latin/ModifierKeyState.java b/java/src/com/android/inputmethod/latin/ModifierKeyState.java index 097e87abe..eb1204f70 100644 --- a/java/src/com/android/inputmethod/latin/ModifierKeyState.java +++ b/java/src/com/android/inputmethod/latin/ModifierKeyState.java @@ -16,27 +16,64 @@ package com.android.inputmethod.latin; -class ModifierKeyState { - private static final int RELEASING = 0; - private static final int PRESSING = 1; - private static final int MOMENTARY = 2; +import android.util.Log; - private int mState = RELEASING; +public class ModifierKeyState { + protected static final String TAG = "ModifierKeyState"; + protected static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE; + + protected static final int RELEASING = 0; + protected static final int PRESSING = 1; + protected static final int MOMENTARY = 2; + + protected final String mName; + protected int mState = RELEASING; + + public ModifierKeyState(String name) { + mName = name; + } public void onPress() { + final int oldState = mState; mState = PRESSING; + if (DEBUG) + Log.d(TAG, mName + ".onPress: " + toString(oldState) + " > " + this); } public void onRelease() { + final int oldState = mState; mState = RELEASING; + if (DEBUG) + Log.d(TAG, mName + ".onRelease: " + toString(oldState) + " > " + this); } public void onOtherKeyPressed() { - if (mState == PRESSING) + final int oldState = mState; + if (oldState == PRESSING) mState = MOMENTARY; + if (DEBUG) + Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this); + } + + public boolean isReleasing() { + return mState == RELEASING; } public boolean isMomentary() { return mState == MOMENTARY; } + + @Override + public String toString() { + return toString(mState); + } + + protected String toString(int state) { + switch (state) { + case RELEASING: return "RELEASING"; + case PRESSING: return "PRESSING"; + case MOMENTARY: return "MOMENTARY"; + default: return "UNKNOWN"; + } + } } diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java index f6fd5bd7a..327fef107 100644 --- a/java/src/com/android/inputmethod/latin/PointerTracker.java +++ b/java/src/com/android/inputmethod/latin/PointerTracker.java @@ -16,12 +16,11 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener; -import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler; +import com.android.inputmethod.latin.BaseKeyboard.Key; +import com.android.inputmethod.latin.BaseKeyboardView.OnKeyboardActionListener; +import com.android.inputmethod.latin.BaseKeyboardView.UIHandler; import android.content.res.Resources; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; import android.util.Log; import android.view.MotionEvent; @@ -41,11 +40,12 @@ public class PointerTracker { // Timing constants private final int mDelayBeforeKeyRepeatStart; private final int mLongPressKeyTimeout; + private final int mLongPressShiftKeyTimeout; private final int mMultiTapKeyTimeout; // Miscellaneous constants - private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY; - private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int NOT_A_KEY = BaseKeyboardView.NOT_A_KEY; + private static final int[] KEY_DELETE = { BaseKeyboard.KEYCODE_DELETE }; private final UIProxy mProxy; private final UIHandler mHandler; @@ -53,6 +53,7 @@ public class PointerTracker { private OnKeyboardActionListener mListener; private final boolean mHasDistinctMultitouch; + private BaseKeyboard mKeyboard; private Key[] mKeys; private int mKeyHysteresisDistanceSquared = -1; @@ -175,6 +176,7 @@ public class PointerTracker { mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout); + mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout); mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout); resetMultiTap(); } @@ -183,9 +185,10 @@ public class PointerTracker { mListener = listener; } - public void setKeyboard(Key[] keys, float keyHysteresisDistance) { - if (keys == null || keyHysteresisDistance < 0) + public void setKeyboard(BaseKeyboard keyboard, Key[] keys, float keyHysteresisDistance) { + if (keyboard == null || keys == null || keyHysteresisDistance < 0) throw new IllegalArgumentException(); + mKeyboard = keyboard; mKeys = keys; mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); // Update current key index because keyboard layout has been changed. @@ -205,8 +208,8 @@ public class PointerTracker { if (key == null) return false; int primaryCode = key.codes[0]; - return primaryCode == Keyboard.KEYCODE_SHIFT - || primaryCode == Keyboard.KEYCODE_MODE_CHANGE; + return primaryCode == BaseKeyboard.KEYCODE_SHIFT + || primaryCode == BaseKeyboard.KEYCODE_MODE_CHANGE; } public boolean isModifier() { @@ -222,9 +225,11 @@ public class PointerTracker { return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE; } - public void updateKey(int keyIndex) { - if (mKeyAlreadyProcessed) - return; + public void releaseKey() { + updateKeyGraphics(NOT_A_KEY); + } + + private void updateKeyGraphics(int keyIndex) { int oldKeyIndex = mPreviousKey; mPreviousKey = keyIndex; if (keyIndex != oldKeyIndex) { @@ -285,9 +290,9 @@ public class PointerTracker { mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this); mIsRepeatableKey = true; } - mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); + startLongPressTimer(keyIndex); } - showKeyPreviewAndUpdateKey(keyIndex); + showKeyPreviewAndUpdateKeyGraphics(keyIndex); } public void onMoveEvent(int x, int y, long eventTime) { @@ -296,32 +301,38 @@ public class PointerTracker { if (mKeyAlreadyProcessed) return; KeyState keyState = mKeyState; - int keyIndex = keyState.onMoveKey(x, y); + final int keyIndex = keyState.onMoveKey(x, y); + final Key oldKey = getKey(keyState.getKeyIndex()); if (isValidKeyIndex(keyIndex)) { - if (keyState.getKeyIndex() == NOT_A_KEY) { + if (oldKey == null) { keyState.onMoveToNewKey(keyIndex, x, y); - mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); + startLongPressTimer(keyIndex); } else if (!isMinorMoveBounce(x, y, keyIndex)) { + if (mListener != null) + mListener.onRelease(oldKey.codes[0]); resetMultiTap(); keyState.onMoveToNewKey(keyIndex, x, y); - mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); + startLongPressTimer(keyIndex); } } else { - if (keyState.getKeyIndex() != NOT_A_KEY) { + if (oldKey != null) { + if (mListener != null) + mListener.onRelease(oldKey.codes[0]); keyState.onMoveToNewKey(keyIndex, x ,y); - mHandler.cancelLongPressTimer(); + mHandler.cancelLongPressTimers(); } else if (!isMinorMoveBounce(x, y, keyIndex)) { resetMultiTap(); keyState.onMoveToNewKey(keyIndex, x ,y); - mHandler.cancelLongPressTimer(); + mHandler.cancelLongPressTimers(); } } - showKeyPreviewAndUpdateKey(mKeyState.getKeyIndex()); + showKeyPreviewAndUpdateKeyGraphics(mKeyState.getKeyIndex()); } public void onUpEvent(int x, int y, long eventTime) { if (DEBUG) debugLog("onUpEvent :", x, y); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); if (mKeyAlreadyProcessed) return; mHandler.cancelKeyTimers(); @@ -333,7 +344,6 @@ public class PointerTracker { x = mKeyState.getKeyX(); y = mKeyState.getKeyY(); } - showKeyPreviewAndUpdateKey(NOT_A_KEY); if (!mIsRepeatableKey) { detectAndSendKey(keyIndex, x, y, eventTime); } @@ -347,7 +357,7 @@ public class PointerTracker { debugLog("onCancelEvt:", x, y); mHandler.cancelKeyTimers(); mHandler.cancelPopupPreview(); - showKeyPreviewAndUpdateKey(NOT_A_KEY); + showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY); int keyIndex = mKeyState.getKeyIndex(); if (isValidKeyIndex(keyIndex)) mProxy.invalidateKey(mKeys[keyIndex]); @@ -390,28 +400,16 @@ public class PointerTracker { if (newKey == curKey) { return true; } else if (isValidKeyIndex(curKey)) { - return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared; + return mKeys[curKey].squaredDistanceToEdge(x, y) < mKeyHysteresisDistanceSquared; } else { return false; } } - private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { - final int left = key.x; - final int right = key.x + key.width; - final int top = key.y; - final int bottom = key.y + key.height; - final int edgeX = x < left ? left : (x > right ? right : x); - final int edgeY = y < top ? top : (y > bottom ? bottom : y); - final int dx = x - edgeX; - final int dy = y - edgeY; - return dx * dx + dy * dy; - } - - private void showKeyPreviewAndUpdateKey(int keyIndex) { - updateKey(keyIndex); + private void showKeyPreviewAndUpdateKeyGraphics(int keyIndex) { + updateKeyGraphics(keyIndex); // The modifier key, such as shift key, should not be shown as preview when multi-touch is - // supported. On thge other hand, if multi-touch is not supported, the modifier key should + // supported. On the other hand, if multi-touch is not supported, the modifier key should // be shown as preview. if (mHasDistinctMultitouch && isModifier()) { mProxy.showPreview(NOT_A_KEY, this); @@ -420,6 +418,20 @@ public class PointerTracker { } } + private void startLongPressTimer(int keyIndex) { + Key key = getKey(keyIndex); + if (key.codes[0] == BaseKeyboard.KEYCODE_SHIFT) { + mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this); + } else { + mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); + } + } + + private boolean isManualTemporaryUpperCase() { + return mKeyboard instanceof LatinKeyboard + && ((LatinKeyboard)mKeyboard).isManualTemporaryUpperCase(); + } + private void detectAndSendKey(int index, int x, int y, long eventTime) { final OnKeyboardActionListener listener = mListener; final Key key = getKey(index); @@ -441,12 +453,20 @@ public class PointerTracker { // Multi-tap if (mInMultiTap) { if (mTapCount != -1) { - mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y); + mListener.onKey(BaseKeyboard.KEYCODE_DELETE, KEY_DELETE, x, y); } else { mTapCount = 0; } code = key.codes[mTapCount]; } + + // If keyboard is in manual temporary upper case state and key has manual temporary + // shift code, alternate character code should be sent. + if (isManualTemporaryUpperCase() && key.manualTemporaryUpperCaseCode != 0) { + code = key.manualTemporaryUpperCaseCode; + codes[0] = code; + } + /* * Swap the first and second values in the codes array if the primary code is not * the first value but the second value in the array. This happens when key diff --git a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java index 325ce674c..35bdc6728 100644 --- a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java +++ b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java @@ -16,7 +16,7 @@ package com.android.inputmethod.latin; -import android.inputmethodservice.Keyboard.Key; +import com.android.inputmethod.latin.BaseKeyboard.Key; import java.util.Arrays; @@ -36,41 +36,34 @@ class ProximityKeyDetector extends KeyDetector { final Key[] keys = getKeys(); final int touchX = getTouchX(x); final int touchY = getTouchY(y); - int primaryIndex = LatinKeyboardBaseView.NOT_A_KEY; - int closestKey = LatinKeyboardBaseView.NOT_A_KEY; + + int primaryIndex = BaseKeyboardView.NOT_A_KEY; + int closestKeyIndex = BaseKeyboardView.NOT_A_KEY; int closestKeyDist = mProximityThresholdSquare + 1; - int[] distances = mDistances; + final int[] distances = mDistances; Arrays.fill(distances, Integer.MAX_VALUE); - int [] nearestKeyIndices = mKeyboard.getNearestKeys(touchX, touchY); - final int keyCount = nearestKeyIndices.length; - for (int i = 0; i < keyCount; i++) { - final Key key = keys[nearestKeyIndices[i]]; - int dist = 0; - boolean isInside = key.isInside(touchX, touchY); - if (isInside) { - primaryIndex = nearestKeyIndices[i]; - } - - if (((mProximityCorrectOn - && (dist = key.squaredDistanceFrom(touchX, touchY)) < mProximityThresholdSquare) - || isInside) - && key.codes[0] > 32) { - // Find insertion point - final int nCodes = key.codes.length; + for (final int index : mKeyboard.getNearestKeys(touchX, touchY)) { + final Key key = keys[index]; + final boolean isInside = key.isInside(touchX, touchY); + if (isInside) + primaryIndex = index; + final int dist = key.squaredDistanceToEdge(touchX, touchY); + if (isInside || (mProximityCorrectOn && dist < mProximityThresholdSquare)) { if (dist < closestKeyDist) { closestKeyDist = dist; - closestKey = nearestKeyIndices[i]; + closestKeyIndex = index; } if (allKeys == null) continue; - + final int nCodes = key.codes.length; + // Find insertion point for (int j = 0; j < distances.length; j++) { if (distances[j] > dist) { // Make space for nCodes codes System.arraycopy(distances, j, distances, j + nCodes, - distances.length - j - nCodes); + distances.length - (j + nCodes)); System.arraycopy(allKeys, j, allKeys, j + nCodes, - allKeys.length - j - nCodes); + allKeys.length - (j + nCodes)); System.arraycopy(key.codes, 0, allKeys, j, nCodes); Arrays.fill(distances, j, j + nCodes, dist); break; @@ -78,9 +71,7 @@ class ProximityKeyDetector extends KeyDetector { } } } - if (primaryIndex == LatinKeyboardBaseView.NOT_A_KEY) { - primaryIndex = closestKey; - } - return primaryIndex; + + return primaryIndex == BaseKeyboardView.NOT_A_KEY ? closestKeyIndex : primaryIndex; } } diff --git a/java/src/com/android/inputmethod/latin/ShiftKeyState.java b/java/src/com/android/inputmethod/latin/ShiftKeyState.java new file mode 100644 index 000000000..7412a566d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ShiftKeyState.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import android.util.Log; + +public class ShiftKeyState extends ModifierKeyState { + private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked + private static final int IGNORING = 4; + + public ShiftKeyState(String name) { + super(name); + } + + @Override + public void onOtherKeyPressed() { + int oldState = mState; + if (oldState == PRESSING) { + mState = MOMENTARY; + } else if (oldState == PRESSING_ON_SHIFTED) { + mState = IGNORING; + } + if (DEBUG) + Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this); + } + + public void onPressOnShifted() { + int oldState = mState; + mState = PRESSING_ON_SHIFTED; + if (DEBUG) + Log.d(TAG, mName + ".onPressOnShifted: " + toString(oldState) + " > " + this); + } + + public boolean isPressingOnShifted() { + return mState == PRESSING_ON_SHIFTED; + } + + public boolean isIgnoring() { + return mState == IGNORING; + } + + @Override + public String toString() { + return toString(mState); + } + + @Override + protected String toString(int state) { + switch (state) { + case PRESSING_ON_SHIFTED: return "PRESSING_ON_SHIFTED"; + case IGNORING: return "IGNORING"; + default: return super.toString(state); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java new file mode 100644 index 000000000..103443e6d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -0,0 +1,477 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.voice.SettingsUtil; +import com.android.inputmethod.voice.VoiceIMEConnector; +import com.android.inputmethod.voice.VoiceInput; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public class SubtypeSwitcher { + // This flag indicates if we support language switching by swipe on space bar. + // We may or may not draw the current language on space bar regardless of this flag. + public static final boolean USE_SPACEBAR_LANGUAGE_SWITCHER = false; + private static final boolean DBG = false; + private static final String TAG = "SubtypeSwitcher"; + + private static final char LOCALE_SEPARATER = '_'; + private static final String KEYBOARD_MODE = "keyboard"; + private static final String VOICE_MODE = "voice"; + private final TextUtils.SimpleStringSplitter mLocaleSplitter = + new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); + + private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); + private /* final */ LatinIME mService; + private /* final */ SharedPreferences mPrefs; + private /* final */ InputMethodManager mImm; + private /* final */ Resources mResources; + private final ArrayList<InputMethodSubtype> mEnabledKeyboardSubtypesOfCurrentInputMethod = + new ArrayList<InputMethodSubtype>(); + private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); + + /*-----------------------------------------------------------*/ + // Variants which should be changed only by reload functions. + private Locale mSystemLocale; + private Locale mInputLocale; + private String mInputLocaleStr; + private String mMode; + private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod; + private VoiceInput mVoiceInput; + private boolean mNeedsToDisplayLanguage; + private boolean mIsSystemLanguageSameAsInputLanguage; + /*-----------------------------------------------------------*/ + + public static SubtypeSwitcher getInstance() { + return sInstance; + } + + public static void init(LatinIME service, SharedPreferences prefs) { + sInstance.mPrefs = prefs; + sInstance.resetParams(service); + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + sInstance.initLanguageSwitcher(service); + } + + sInstance.updateAllParameters(); + } + + private SubtypeSwitcher() { + } + + private void resetParams(LatinIME service) { + mService = service; + mResources = service.getResources(); + mImm = (InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE); + mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); + mEnabledLanguagesOfCurrentInputMethod.clear(); + mSystemLocale = null; + mInputLocale = null; + mInputLocaleStr = null; + mMode = null; + mAllEnabledSubtypesOfCurrentInputMethod = null; + // TODO: Voice input should be created here + mVoiceInput = null; + } + + // Update all parameters stored in SubtypeSwitcher. + // Only configuration changed event is allowed to call this because this is heavy. + private void updateAllParameters() { + mSystemLocale = mResources.getConfiguration().locale; + updateSubtype(mImm.getCurrentInputMethodSubtype()); + updateParametersOnStartInputView(); + } + + // Update parameters which are changed outside LatinIME. This parameters affect UI so they + // should be updated every time onStartInputview. + public void updateParametersOnStartInputView() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + updateForSpaceBarLanguageSwitch(); + } else { + updateEnabledSubtypes(); + } + } + + // Reload enabledSubtypes from the framework. + private void updateEnabledSubtypes() { + boolean foundCurrentSubtypeBecameDisabled = true; + mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList(null); + mEnabledLanguagesOfCurrentInputMethod.clear(); + mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); + for (InputMethodSubtype ims: mAllEnabledSubtypesOfCurrentInputMethod) { + final String locale = ims.getLocale(); + final String mode = ims.getMode(); + mLocaleSplitter.setString(locale); + if (mLocaleSplitter.hasNext()) { + mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); + } + if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) { + foundCurrentSubtypeBecameDisabled = false; + } + if (KEYBOARD_MODE.equals(ims.getMode())) { + mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims); + } + } + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && mIsSystemLanguageSameAsInputLanguage); + if (foundCurrentSubtypeBecameDisabled) { + if (DBG) { + Log.w(TAG, "Last subtype was disabled. Update to the current one."); + } + updateSubtype(mImm.getCurrentInputMethodSubtype()); + } + } + + // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. + public void updateSubtype(InputMethodSubtype newSubtype) { + final String newLocale; + final String newMode; + if (newSubtype == null) { + // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, + // fallback to the default locale and mode. + Log.w(TAG, "Couldn't get the current subtype."); + newLocale = "en_US"; + newMode =KEYBOARD_MODE; + } else { + newLocale = newSubtype.getLocale(); + newMode = newSubtype.getMode(); + } + if (DBG) { + Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode + + ", from: " + mInputLocaleStr + ", " + mMode); + } + boolean languageChanged = false; + if (!newLocale.equals(mInputLocaleStr)) { + if (mInputLocaleStr != null) { + languageChanged = true; + } + updateInputLocale(newLocale); + } + boolean modeChanged = false; + String oldMode = mMode; + if (!newMode.equals(mMode)) { + if (mMode != null) { + modeChanged = true; + } + mMode = newMode; + } + if (isKeyboardMode()) { + if (modeChanged) { + if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { + mVoiceInput.cancel(); + } + } + if (languageChanged) { + mService.onKeyboardLanguageChanged(); + } + } else if (isVoiceMode()) { + // If needsToShowWarningDialog is true, voice input need to show warning before + // show recognition view. + if (languageChanged || modeChanged + || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) { + if (mVoiceInput != null) { + // TODO: Call proper function to trigger VoiceIME + mService.onKey(LatinKeyboardView.KEYCODE_VOICE, null, 0, 0); + } + } + } else { + Log.w(TAG, "Unknown subtype mode: " + mMode); + } + } + + // Update the current input locale from Locale string. + private void updateInputLocale(String inputLocaleStr) { + // example: inputLocaleStr = "en_US" "en" "" + // "en_US" --> language: en & country: US + // "en" --> language: en + // "" --> the system locale + mLocaleSplitter.setString(inputLocaleStr); + if (mLocaleSplitter.hasNext()) { + String language = mLocaleSplitter.next(); + if (mLocaleSplitter.hasNext()) { + mInputLocale = new Locale(language, mLocaleSplitter.next()); + } else { + mInputLocale = new Locale(language); + } + mInputLocaleStr = inputLocaleStr; + } else { + mInputLocale = mSystemLocale; + String country = mSystemLocale.getCountry(); + mInputLocaleStr = mSystemLocale.getLanguage() + + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage()); + } + mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocale().getLanguage()); + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && mIsSystemLanguageSameAsInputLanguage); + } + + ////////////////////////////////// + // Language Switching functions // + ////////////////////////////////// + + public int getEnabledKeyboardLocaleCount() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return mLanguageSwitcher.getLocaleCount(); + } else { + return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); + } + } + + public boolean needsToDisplayLanguage() { + return mNeedsToDisplayLanguage; + } + + public Locale getInputLocale() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return mLanguageSwitcher.getInputLocale(); + } else { + return mInputLocale; + } + } + + public String getInputLocaleStr() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + String inputLanguage = null; + inputLanguage = mLanguageSwitcher.getInputLanguage(); + // Should return system locale if there is no Language available. + if (inputLanguage == null) { + inputLanguage = getSystemLocale().getLanguage(); + } + return inputLanguage; + } else { + return mInputLocaleStr; + } + } + + public String[] getEnabledLanguages() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return mLanguageSwitcher.getEnabledLanguages(); + } else { + return mEnabledLanguagesOfCurrentInputMethod.toArray( + new String[mEnabledLanguagesOfCurrentInputMethod.size()]); + } + } + + public Locale getSystemLocale() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return mLanguageSwitcher.getSystemLocale(); + } else { + return mSystemLocale; + } + } + + public boolean isSystemLanguageSameAsInputLanguage() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocaleStr().substring(0, 2)); + } else { + return mIsSystemLanguageSameAsInputLanguage; + } + } + + public void onConfigurationChanged(Configuration conf) { + final Locale systemLocale = conf.locale; + // If system configuration was changed, update all parameters. + if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + // If the system locale changes and is different from the saved + // locale (mSystemLocale), then reload the input locale list from the + // latin ime settings (shared prefs) and reset the input locale + // to the first one. + mLanguageSwitcher.loadLocales(mPrefs); + mLanguageSwitcher.setSystemLocale(systemLocale); + } else { + updateAllParameters(); + } + } + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + if (LatinIME.PREF_SELECTED_LANGUAGES.equals(key)) { + mLanguageSwitcher.loadLocales(sharedPreferences); + } + } + } + + /** + * Change system locale for this application + * @param newLocale + * @return oldLocale + */ + public Locale changeSystemLocale(Locale newLocale) { + Configuration conf = mResources.getConfiguration(); + Locale oldLocale = conf.locale; + conf.locale = newLocale; + mResources.updateConfiguration(conf, mResources.getDisplayMetrics()); + return oldLocale; + } + + public boolean isKeyboardMode() { + return KEYBOARD_MODE.equals(mMode); + } + + + /////////////////////////// + // Voice Input functions // + /////////////////////////// + + public boolean setVoiceInput(VoiceInput vi) { + if (mVoiceInput == null && vi != null) { + mVoiceInput = vi; + if (isVoiceMode()) { + if (DBG) { + Log.d(TAG, "Set and call voice input."); + } + mService.onKey(LatinKeyboardView.KEYCODE_VOICE, null, 0, 0); + return true; + } + } + return false; + } + + public boolean isVoiceMode() { + return VOICE_MODE.equals(mMode); + } + + ////////////////////////////////////// + // SpaceBar Language Switch support // + ////////////////////////////////////// + + private LanguageSwitcher mLanguageSwitcher; + + public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) { + if (returnsNameInThisLocale) { + return toTitleCase(locale.getDisplayName(locale)); + } else { + return toTitleCase(locale.getDisplayName()); + } + } + + public static String getDisplayLanguage(Locale locale) { + return toTitleCase(locale.getDisplayLanguage(locale)); + } + + public static String getShortDisplayLanguage(Locale locale) { + return toTitleCase(locale.getLanguage()); + } + + private static String toTitleCase(String s) { + if (s.length() == 0) { + return s; + } + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + + private void updateForSpaceBarLanguageSwitch() { + // We need to update mNeedsToDisplayLanguage in onStartInputView because + // getEnabledKeyboardLocaleCount could have been changed. + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocale().getLanguage())); + } + + public String getInputLanguageName() { + return getDisplayLanguage(getInputLocale()); + } + + public String getNextInputLanguageName() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale()); + } else { + return ""; + } + } + + public String getPreviousInputLanguageName() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale()); + } else { + return ""; + } + } + + // A list of locales which are supported by default for voice input, unless we get a + // different list from Gservices. + private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = + "en " + + "en_US " + + "en_GB " + + "en_AU " + + "en_CA " + + "en_IE " + + "en_IN " + + "en_NZ " + + "en_SG " + + "en_ZA "; + + public boolean isVoiceSupported(String locale) { + // Get the current list of supported locales and check the current locale against that + // list. We cache this value so as not to check it every time the user starts a voice + // input. Because this method is called by onStartInputView, this should mean that as + // long as the locale doesn't change while the user is keeping the IME open, the + // value should never be stale. + String supportedLocalesString = SettingsUtil.getSettingsString( + mService.getContentResolver(), + SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, + DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); + List<String> voiceInputSupportedLocales = Arrays.asList( + supportedLocalesString.split("\\s+")); + return voiceInputSupportedLocales.contains(locale); + } + + public void loadSettings() { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + mLanguageSwitcher.loadLocales(mPrefs); + } + } + + public void toggleLanguage(boolean reset, boolean next) { + if (USE_SPACEBAR_LANGUAGE_SWITCHER) { + if (reset) { + mLanguageSwitcher.reset(); + } else { + if (next) { + mLanguageSwitcher.next(); + } else { + mLanguageSwitcher.prev(); + } + } + mLanguageSwitcher.persist(mPrefs); + } + } + + private void initLanguageSwitcher(LatinIME service) { + final Configuration conf = service.getResources().getConfiguration(); + mLanguageSwitcher = new LanguageSwitcher(service); + mLanguageSwitcher.loadLocales(mPrefs); + mLanguageSwitcher.setSystemLocale(conf.locale); + } +} diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 3b898941f..ca2ffe13a 100755..100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -16,17 +16,17 @@ package com.android.inputmethod.latin; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import android.content.Context; import android.text.AutoText; import android.text.TextUtils; import android.util.Log; import android.view.View; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * This class loads a dictionary and provides a list of suggestions for a given sequence of * characters. This includes corrections and completions. @@ -81,6 +81,7 @@ public class Suggest implements Dictionary.WordCallback { private boolean mAutoTextEnabled; + private double mAutoCompleteThreshold; private int[] mPriorities = new int[mPrefMaxSuggestions]; private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; @@ -163,6 +164,10 @@ public class Suggest implements Dictionary.WordCallback { mUserBigramDictionary = userBigramDictionary; } + public void setAutoCompleteThreshold(double threshold) { + mAutoCompleteThreshold = threshold; + } + /** * Number of suggestions to generate from the input key sequence. This has * to be a number between 1 and 100 (inclusive). @@ -301,8 +306,14 @@ public class Suggest implements Dictionary.WordCallback { } mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM) - && mSuggestions.size() > 0) { - mHaveCorrection = true; + && mSuggestions.size() > 0 && mPriorities.length > 0) { + // TODO: when the normalized score of the first suggestion is nearly equals to + // the normalized score of the second suggestion, behave less aggressive. + final double normalizedScore = LatinIMEUtil.calcNormalizedScore( + mOriginalWord, mSuggestions.get(0), mPriorities[0]); + if (normalizedScore >= mAutoCompleteThreshold) { + mHaveCorrection = true; + } } } if (mOriginalWord != null) { @@ -326,8 +337,25 @@ public class Suggest implements Dictionary.WordCallback { String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); CharSequence autoText = AutoText.get(suggestedWord, 0, suggestedWord.length(), view); - // Is there an AutoText correction? + // Is there an AutoText (also known as Quick Fixes) correction? boolean canAdd = autoText != null; + // Capitalize as needed + final int autoTextLength = autoText != null ? autoText.length() : 0; + if (autoTextLength > 0 && (mIsAllUpperCase || mIsFirstCharCapitalized)) { + int poolSize = mStringPool.size(); + StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove( + poolSize - 1) : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + if (mIsAllUpperCase) { + sb.append(autoText.toString().toUpperCase()); + } else if (mIsFirstCharCapitalized) { + sb.append(Character.toUpperCase(autoText.charAt(0))); + if (autoTextLength > 1) { + sb.append(autoText.subSequence(1, autoTextLength)); + } + } + autoText = sb.toString(); + } // Is that correction already the current prediction (or original word)? canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i)); // Is that correction already the next predicted word? @@ -450,8 +478,7 @@ public class Suggest implements Dictionary.WordCallback { return true; } - System.arraycopy(priorities, pos, priorities, pos + 1, - prefMaxSuggestions - pos - 1); + System.arraycopy(priorities, pos, priorities, pos + 1, prefMaxSuggestions - pos - 1); priorities[pos] = freq; int poolSize = mStringPool.size(); StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index 9011191f1..56347af5d 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -16,8 +16,9 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.BaseKeyboard.Key; + import android.content.Context; -import android.inputmethodservice.Keyboard.Key; import android.text.format.DateFormat; import android.util.Log; @@ -61,7 +62,7 @@ public class TextEntryState { SPACE_AFTER_PICKED, UNDO_COMMIT, CORRECTING, - PICKED_CORRECTION; + PICKED_CORRECTION, } private static State sState = State.UNKNOWN; @@ -96,7 +97,7 @@ public class TextEntryState { } try { sKeyLocationFile.close(); - // Write to log file + // Write to log file // Write timestamp, settings, String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime()) .toString() @@ -168,6 +169,13 @@ public class TextEntryState { displayState(); } + public static void onAbortCorrection() { + if (isCorrecting()) { + sState = State.START; + } + displayState(); + } + public static void typedCharacter(char c, boolean isSeparator) { boolean isSpace = c == ' '; switch (sState) { diff --git a/java/src/com/android/inputmethod/latin/Tutorial.java b/java/src/com/android/inputmethod/latin/Tutorial.java index d3eaf30c6..cd7636f3c 100644 --- a/java/src/com/android/inputmethod/latin/Tutorial.java +++ b/java/src/com/android/inputmethod/latin/Tutorial.java @@ -32,20 +32,24 @@ import android.widget.PopupWindow; import android.widget.TextView; import java.util.ArrayList; -import java.util.List; public class Tutorial implements OnTouchListener { - - private List<Bubble> mBubbles = new ArrayList<Bubble>(); - private View mInputView; - private LatinIME mIme; - private int[] mLocation = new int[2]; - + + public interface TutorialListener { + public void onTutorialDone(); + } + + private final ArrayList<Bubble> mBubbles = new ArrayList<Bubble>(); + private final KeyboardSwitcher mKeyboardSwitcher; + private final View mInputView; + private final TutorialListener mListener; + private final int[] mLocation = new int[2]; + private static final int MSG_SHOW_BUBBLE = 0; - + private int mBubbleIndex; - - Handler mHandler = new Handler() { + + private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -57,20 +61,18 @@ public class Tutorial implements OnTouchListener { } }; - class Bubble { - Drawable bubbleBackground; - int x; - int y; - int width; - int gravity; - CharSequence text; - boolean dismissOnTouch; - boolean dismissOnClose; - PopupWindow window; - TextView textView; - View inputView; - - Bubble(Context context, View inputView, + private class Bubble { + private final Drawable bubbleBackground; + private final int x; + private final int y; + private final int width; + private final int gravity; + private final CharSequence text; + private final PopupWindow window; + private final TextView textView; + private final View inputView; + + private Bubble(Context context, View inputView, int backgroundResource, int bx, int by, int textResource1, int textResource2) { bubbleBackground = context.getResources().getDrawable(backgroundResource); x = bx; @@ -81,8 +83,6 @@ public class Tutorial implements OnTouchListener { .append(context.getResources().getText(textResource1)) .append("\n") .append(context.getResources().getText(textResource2)); - this.dismissOnTouch = true; - this.dismissOnClose = false; this.inputView = inputView; window = new PopupWindow(context); window.setBackgroundDrawable(null); @@ -124,7 +124,7 @@ public class Tutorial implements OnTouchListener { return l.getHeight(); } - void show(int offx, int offy) { + private void show(int offx, int offy) { int textHeight = chooseSize(window, inputView, text, textView); offy -= textView.getPaddingTop() + textHeight; if (inputView.getVisibility() == View.VISIBLE @@ -144,63 +144,66 @@ public class Tutorial implements OnTouchListener { } } } - - void hide() { + + private void hide() { if (window.isShowing()) { textView.setOnTouchListener(null); window.dismiss(); } } - - boolean isShowing() { + + private boolean isShowing() { return window.isShowing(); } } - - public Tutorial(LatinIME ime, LatinKeyboardView inputView) { + + public Tutorial(TutorialListener listener, KeyboardSwitcher keyboardSwitcher) { + mListener = listener; + mKeyboardSwitcher = keyboardSwitcher; + LatinKeyboardView inputView = keyboardSwitcher.getInputView(); + mInputView = inputView; Context context = inputView.getContext(); - mIme = ime; int inputWidth = inputView.getWidth(); final int x = inputWidth / 20; // Half of 1/10th + ArrayList<Bubble> bubbles = mBubbles; Bubble bWelcome = new Bubble(context, inputView, R.drawable.dialog_bubble_step02, x, 0, R.string.tip_to_open_keyboard, R.string.touch_to_continue); - mBubbles.add(bWelcome); + bubbles.add(bWelcome); Bubble bAccents = new Bubble(context, inputView, R.drawable.dialog_bubble_step02, x, 0, R.string.tip_to_view_accents, R.string.touch_to_continue); - mBubbles.add(bAccents); + bubbles.add(bAccents); Bubble b123 = new Bubble(context, inputView, R.drawable.dialog_bubble_step07, x, 0, R.string.tip_to_open_symbols, R.string.touch_to_continue); - mBubbles.add(b123); + bubbles.add(b123); Bubble bABC = new Bubble(context, inputView, R.drawable.dialog_bubble_step07, x, 0, R.string.tip_to_close_symbols, R.string.touch_to_continue); - mBubbles.add(bABC); + bubbles.add(bABC); Bubble bSettings = new Bubble(context, inputView, R.drawable.dialog_bubble_step07, x, 0, R.string.tip_to_launch_settings, R.string.touch_to_continue); - mBubbles.add(bSettings); + bubbles.add(bSettings); Bubble bDone = new Bubble(context, inputView, R.drawable.dialog_bubble_step02, x, 0, R.string.tip_to_start_typing, R.string.touch_to_finish); - mBubbles.add(bDone); - mInputView = inputView; + bubbles.add(bDone); } - - void start() { + + public void start() { mInputView.getLocationInWindow(mLocation); mBubbleIndex = -1; mInputView.setOnTouchListener(this); next(); } - boolean next() { + private void next() { if (mBubbleIndex >= 0) { // If the bubble is not yet showing, don't move to the next. if (!mBubbles.get(mBubbleIndex).isShowing()) { - return true; + return; } // Hide all previous bubbles as well, as they may have had a delayed show for (int i = 0; i <= mBubbleIndex; i++) { @@ -210,26 +213,25 @@ public class Tutorial implements OnTouchListener { mBubbleIndex++; if (mBubbleIndex >= mBubbles.size()) { mInputView.setOnTouchListener(null); - mIme.sendDownUpKeyEvents(-1); // Inform the setupwizard that tutorial is in last bubble - mIme.tutorialDone(); - return false; + mListener.onTutorialDone(); + return; } if (mBubbleIndex == 3 || mBubbleIndex == 4) { - mIme.mKeyboardSwitcher.toggleSymbols(); + mKeyboardSwitcher.changeKeyboardMode(); } mHandler.sendMessageDelayed( mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(mBubbleIndex)), 500); - return true; + return; } - - void hide() { - for (int i = 0; i < mBubbles.size(); i++) { - mBubbles.get(i).hide(); + + private void hide() { + for (Bubble bubble : mBubbles) { + bubble.hide(); } mInputView.setOnTouchListener(null); } - boolean close() { + public boolean close() { mHandler.removeMessages(MSG_SHOW_BUBBLE); hide(); return true; diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java index 67d9c0bcf..6d2f6b611 100644 --- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -16,10 +16,6 @@ package com.android.inputmethod.latin; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -30,6 +26,10 @@ import android.os.AsyncTask; import android.provider.BaseColumns; import android.util.Log; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + /** * Stores all the pairs user types in databases. Prune the database if the size * gets too big. Unlike AutoDictionary, it even stores the pairs that are already diff --git a/java/src/com/android/inputmethod/latin/Hints.java b/java/src/com/android/inputmethod/voice/Hints.java index c467365e7..d11d3b042 100644 --- a/java/src/com/android/inputmethod/latin/Hints.java +++ b/java/src/com/android/inputmethod/voice/Hints.java @@ -14,14 +14,14 @@ * the License. */ -package com.android.inputmethod.latin; +package com.android.inputmethod.voice; -import com.android.inputmethod.voice.SettingsUtil; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SharedPreferencesCompat; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.view.inputmethod.InputConnection; import java.util.Calendar; @@ -47,8 +47,9 @@ public class Hints { private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7; private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7; - private Context mContext; - private Display mDisplay; + private final Context mContext; + private final SharedPreferences mPrefs; + private final Display mDisplay; private boolean mVoiceResultContainedPunctuation; private int mSwipeHintMaxDaysToShow; private int mPunctuationHintMaxDisplays; @@ -62,8 +63,9 @@ public class Hints { SPEAKABLE_PUNCTUATION.put("?", "question mark"); } - public Hints(Context context, Display display) { + public Hints(Context context, SharedPreferences prefs, Display display) { mContext = context; + mPrefs = prefs; mDisplay = display; ContentResolver cr = mContext.getContentResolver(); @@ -103,8 +105,7 @@ public class Hints { public void registerVoiceResult(String text) { // Update the current time as the last time voice input was used. - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(mContext).edit(); + SharedPreferences.Editor editor = mPrefs.edit(); editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis()); SharedPreferencesCompat.apply(editor); @@ -118,14 +119,14 @@ public class Hints { } private boolean shouldShowSwipeHint() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + final SharedPreferences prefs = mPrefs; - int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); + int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); // If we've already shown the hint for enough days, we'll return false. if (numUniqueDaysShown < mSwipeHintMaxDaysToShow) { - long lastTimeVoiceWasUsed = sp.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0); + long lastTimeVoiceWasUsed = prefs.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0); // If the user has used voice today, we'll return false. (We don't show the hint on // any day that the user has already used voice.) @@ -156,16 +157,16 @@ public class Hints { } private void showHint(int hintViewResource) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + final SharedPreferences prefs = mPrefs; - int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); - long lastTimeHintWasShown = sp.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0); + int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); + long lastTimeHintWasShown = prefs.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0); // If this is the first time the hint is being shown today, increase the saved values // to represent that. We don't need to increase the last time the hint was shown unless // it is a different day from the current value. if (!isFromToday(lastTimeHintWasShown)) { - SharedPreferences.Editor editor = sp.edit(); + SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1); editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis()); SharedPreferencesCompat.apply(editor); @@ -177,9 +178,9 @@ public class Hints { } private int getAndIncrementPref(String pref) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); - int value = sp.getInt(pref, 0); - SharedPreferences.Editor editor = sp.edit(); + final SharedPreferences prefs = mPrefs; + int value = prefs.getInt(pref, 0); + SharedPreferences.Editor editor = prefs.edit(); editor.putInt(pref, value + 1); SharedPreferencesCompat.apply(editor); return value; diff --git a/java/src/com/android/inputmethod/voice/RecognitionView.java b/java/src/com/android/inputmethod/voice/RecognitionView.java index 7cec0b04a..1d1297713 100644 --- a/java/src/com/android/inputmethod/voice/RecognitionView.java +++ b/java/src/com/android/inputmethod/voice/RecognitionView.java @@ -16,12 +16,7 @@ package com.android.inputmethod.voice; -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.ShortBuffer; -import java.util.ArrayList; -import java.util.List; +import com.android.inputmethod.latin.R; import android.content.ContentResolver; import android.content.Context; @@ -43,7 +38,12 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; -import com.android.inputmethod.latin.R; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.List; /** * The user interface for the "Speak now" and "working" states. diff --git a/java/src/com/android/inputmethod/voice/SettingsUtil.java b/java/src/com/android/inputmethod/voice/SettingsUtil.java index abf52047f..4d746e120 100644 --- a/java/src/com/android/inputmethod/voice/SettingsUtil.java +++ b/java/src/com/android/inputmethod/voice/SettingsUtil.java @@ -17,10 +17,7 @@ package com.android.inputmethod.voice; import android.content.ContentResolver; -import android.database.Cursor; -import android.net.Uri; import android.provider.Settings; -import android.util.Log; /** * Utility for retrieving settings from Settings.Secure. diff --git a/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java b/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java new file mode 100644 index 000000000..5574a21de --- /dev/null +++ b/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java @@ -0,0 +1,687 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.voice; + +import com.android.inputmethod.latin.EditingUtil; +import com.android.inputmethod.latin.KeyboardSwitcher; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.LatinIME.UIHandler; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SharedPreferencesCompat; +import com.android.inputmethod.latin.SubtypeSwitcher; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.provider.Browser; +import android.speech.SpeechRecognizer; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.text.method.MovementMethod; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VoiceIMEConnector implements VoiceInput.UiListener { + private static final VoiceIMEConnector sInstance = new VoiceIMEConnector(); + + public static final boolean VOICE_INSTALLED = true; + private static final boolean ENABLE_VOICE_BUTTON = true; + private static final String PREF_VOICE_MODE = "voice_mode"; + // Whether or not the user has used voice input before (and thus, whether to show the + // first-run warning dialog or not). + private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input"; + // Whether or not the user has used voice input from an unsupported locale UI before. + // For example, the user has a Chinese UI but activates voice input. + private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE = + "has_used_voice_input_unsupported_locale"; + // The private IME option used to indicate that no microphone should be shown for a + // given text field. For instance this is specified by the search dialog when the + // dialog is already showing a voice search button. + private static final String IME_OPTION_NO_MICROPHONE = "nm"; + + private boolean mAfterVoiceInput; + private boolean mHasUsedVoiceInput; + private boolean mHasUsedVoiceInputUnsupportedLocale; + private boolean mImmediatelyAfterVoiceInput; + private boolean mIsShowingHint; + private boolean mLocaleSupportedForVoiceInput; + private boolean mPasswordText; + private boolean mRecognizing; + private boolean mShowingVoiceSuggestions; + private boolean mVoiceButtonEnabled; + private boolean mVoiceButtonOnPrimary; + private boolean mVoiceInputHighlighted; + + private InputMethodManager mImm; + private LatinIME mContext; + private AlertDialog mVoiceWarningDialog; + private VoiceInput mVoiceInput; + private final VoiceResults mVoiceResults = new VoiceResults(); + private Hints mHints; + private UIHandler mHandler; + private SubtypeSwitcher mSubtypeSwitcher; + // For each word, a list of potential replacements, usually from voice. + private final Map<String, List<CharSequence>> mWordToSuggestions = + new HashMap<String, List<CharSequence>>(); + + public static VoiceIMEConnector init(LatinIME context, SharedPreferences prefs, UIHandler h) { + sInstance.initInternal(context, prefs, h); + return sInstance; + } + + public static VoiceIMEConnector getInstance() { + return sInstance; + } + + private void initInternal(LatinIME context, SharedPreferences prefs, UIHandler h) { + mContext = context; + mHandler = h; + mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + mSubtypeSwitcher = SubtypeSwitcher.getInstance(); + if (VOICE_INSTALLED) { + mVoiceInput = new VoiceInput(context, this); + mHints = new Hints(context, prefs, new Hints.Display() { + public void showHint(int viewResource) { + LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(viewResource, null); + mContext.setCandidatesView(view); + mContext.setCandidatesViewShown(true); + mIsShowingHint = true; + } + }); + } + } + + private VoiceIMEConnector() { + } + + public void resetVoiceStates(boolean isPasswordText) { + mAfterVoiceInput = false; + mImmediatelyAfterVoiceInput = false; + mShowingVoiceSuggestions = false; + mVoiceInputHighlighted = false; + mPasswordText = isPasswordText; + } + + public void flushVoiceInputLogs(boolean configurationChanged) { + if (VOICE_INSTALLED && !configurationChanged) { + if (mAfterVoiceInput) { + mVoiceInput.flushAllTextModificationCounters(); + mVoiceInput.logInputEnded(); + } + mVoiceInput.flushLogs(); + mVoiceInput.cancel(); + } + } + + public void flushAndLogAllTextModificationCounters(int index, CharSequence suggestion, + String wordSeparators) { + if (mAfterVoiceInput && mShowingVoiceSuggestions) { + mVoiceInput.flushAllTextModificationCounters(); + // send this intent AFTER logging any prior aggregated edits. + mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index, + wordSeparators, mContext.getCurrentInputConnection()); + } + } + + private void showVoiceWarningDialog(final boolean swipe, IBinder token, + final boolean configurationChanging) { + if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setCancelable(true); + builder.setIcon(R.drawable.ic_mic_dialog); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + mVoiceInput.logKeyboardWarningDialogOk(); + reallyStartListening(swipe, configurationChanging); + } + }); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + mVoiceInput.logKeyboardWarningDialogCancel(); + switchToLastInputMethod(); + } + }); + // When the dialog is dismissed by user's cancellation, switch back to the last input method + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface arg0) { + mVoiceInput.logKeyboardWarningDialogCancel(); + switchToLastInputMethod(); + } + }); + + final CharSequence message; + if (mLocaleSupportedForVoiceInput) { + message = TextUtils.concat( + mContext.getText(R.string.voice_warning_may_not_understand), "\n\n", + mContext.getText(R.string.voice_warning_how_to_turn_off)); + } else { + message = TextUtils.concat( + mContext.getText(R.string.voice_warning_locale_not_supported), "\n\n", + mContext.getText(R.string.voice_warning_may_not_understand), "\n\n", + mContext.getText(R.string.voice_warning_how_to_turn_off)); + } + builder.setMessage(message); + + builder.setTitle(R.string.voice_warning_title); + mVoiceWarningDialog = builder.create(); + Window window = mVoiceWarningDialog.getWindow(); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.token = token; + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; + window.setAttributes(lp); + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + mVoiceInput.logKeyboardWarningDialogShown(); + mVoiceWarningDialog.show(); + // Make URL in the dialog message clickable + TextView textView = (TextView) mVoiceWarningDialog.findViewById(android.R.id.message); + if (textView != null) { + final CustomLinkMovementMethod method = CustomLinkMovementMethod.getInstance(); + method.setVoiceWarningDialog(mVoiceWarningDialog); + textView.setMovementMethod(method); + } + } + + private static class CustomLinkMovementMethod extends LinkMovementMethod { + private static CustomLinkMovementMethod sInstance = new CustomLinkMovementMethod(); + private AlertDialog mAlertDialog; + + public void setVoiceWarningDialog(AlertDialog alertDialog) { + mAlertDialog = alertDialog; + } + + public static CustomLinkMovementMethod getInstance() { + return sInstance; + } + + // Almost the same as LinkMovementMethod.onTouchEvent(), but overrides it for + // FLAG_ACTIVITY_NEW_TASK and mAlertDialog.cancel(). + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); + + if (link.length != 0) { + if (action == MotionEvent.ACTION_UP) { + if (link[0] instanceof URLSpan) { + URLSpan urlSpan = (URLSpan) link[0]; + Uri uri = Uri.parse(urlSpan.getURL()); + Context context = widget.getContext(); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); + if (mAlertDialog != null) { + // Go back to the previous IME for now. + // TODO: If we can find a way to bring the new activity to front + // while keeping the warning dialog, we don't need to cancel here. + mAlertDialog.cancel(); + } + context.startActivity(intent); + } else { + link[0].onClick(widget); + } + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0])); + } + return true; + } else { + Selection.removeSelection(buffer); + } + } + return super.onTouchEvent(widget, buffer, event); + } + } + + public void showPunctuationHintIfNecessary() { + InputConnection ic = mContext.getCurrentInputConnection(); + if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) { + if (mHints.showPunctuationHintIfNecessary(ic)) { + mVoiceInput.logPunctuationHintDisplayed(); + } + } + mImmediatelyAfterVoiceInput = false; + } + + public void hideVoiceWindow(boolean configurationChanging) { + if (!configurationChanging) { + if (mAfterVoiceInput) + mVoiceInput.logInputEnded(); + if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { + mVoiceInput.logKeyboardWarningDialogDismissed(); + mVoiceWarningDialog.dismiss(); + mVoiceWarningDialog = null; + } + if (VOICE_INSTALLED & mRecognizing) { + mVoiceInput.cancel(); + } + } + mWordToSuggestions.clear(); + } + + public void setCursorAndSelection(int newSelEnd, int newSelStart) { + if (mAfterVoiceInput) { + mVoiceInput.setCursorPos(newSelEnd); + mVoiceInput.setSelectionSpan(newSelEnd - newSelStart); + } + } + + public void setVoiceInputHighlighted(boolean b) { + mVoiceInputHighlighted = b; + } + + public void setShowingVoiceSuggestions(boolean b) { + mShowingVoiceSuggestions = b; + } + + public boolean isVoiceButtonEnabled() { + return mVoiceButtonEnabled; + } + + public boolean isVoiceButtonOnPrimary() { + return mVoiceButtonOnPrimary; + } + + public boolean isVoiceInputHighlighted() { + return mVoiceInputHighlighted; + } + + public boolean isRecognizing() { + return mRecognizing; + } + + public boolean needsToShowWarningDialog() { + return !mHasUsedVoiceInput + || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale); + } + + public boolean getAndResetIsShowingHint() { + boolean ret = mIsShowingHint; + mIsShowingHint = false; + return ret; + } + + private void revertVoiceInput() { + InputConnection ic = mContext.getCurrentInputConnection(); + if (ic != null) ic.commitText("", 1); + mContext.updateSuggestions(); + mVoiceInputHighlighted = false; + } + + public void commitVoiceInput() { + if (VOICE_INSTALLED && mVoiceInputHighlighted) { + InputConnection ic = mContext.getCurrentInputConnection(); + if (ic != null) ic.finishComposingText(); + mContext.updateSuggestions(); + mVoiceInputHighlighted = false; + } + } + + public boolean logAndRevertVoiceInput() { + if (VOICE_INSTALLED && mVoiceInputHighlighted) { + mVoiceInput.incrementTextModificationDeleteCount( + mVoiceResults.candidates.get(0).toString().length()); + revertVoiceInput(); + return true; + } else { + return false; + } + } + + public void rememberReplacedWord(CharSequence suggestion,String wordSeparators) { + if (mShowingVoiceSuggestions) { + // Retain the replaced word in the alternatives array. + EditingUtil.Range range = new EditingUtil.Range(); + String wordToBeReplaced = EditingUtil.getWordAtCursor( + mContext.getCurrentInputConnection(), wordSeparators, range); + if (!mWordToSuggestions.containsKey(wordToBeReplaced)) { + wordToBeReplaced = wordToBeReplaced.toLowerCase(); + } + if (mWordToSuggestions.containsKey(wordToBeReplaced)) { + List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced); + if (suggestions.contains(suggestion)) { + suggestions.remove(suggestion); + } + suggestions.add(wordToBeReplaced); + mWordToSuggestions.remove(wordToBeReplaced); + mWordToSuggestions.put(suggestion.toString(), suggestions); + } + } + } + + /** + * Tries to apply any voice alternatives for the word if this was a spoken word and + * there are voice alternatives. + * @param touching The word that the cursor is touching, with position information + * @return true if an alternative was found, false otherwise. + */ + public boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) { + // Search for result in spoken word alternatives + String selectedWord = touching.word.toString().trim(); + if (!mWordToSuggestions.containsKey(selectedWord)) { + selectedWord = selectedWord.toLowerCase(); + } + if (mWordToSuggestions.containsKey(selectedWord)) { + mShowingVoiceSuggestions = true; + List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord); + // If the first letter of touching is capitalized, make all the suggestions + // start with a capital letter. + if (Character.isUpperCase(touching.word.charAt(0))) { + for (int i = 0; i < suggestions.size(); i++) { + String origSugg = (String) suggestions.get(i); + String capsSugg = origSugg.toUpperCase().charAt(0) + + origSugg.subSequence(1, origSugg.length()).toString(); + suggestions.set(i, capsSugg); + } + } + mContext.setSuggestions(suggestions, false, true, true); + mContext.setCandidatesViewShown(true); + return true; + } + return false; + } + + public void handleBackspace() { + if (mAfterVoiceInput) { + // Don't log delete if the user is pressing delete at + // the beginning of the text box (hence not deleting anything) + if (mVoiceInput.getCursorPos() > 0) { + // If anything was selected before the delete was pressed, increment the + // delete count by the length of the selection + int deleteLen = mVoiceInput.getSelectionSpan() > 0 ? + mVoiceInput.getSelectionSpan() : 1; + mVoiceInput.incrementTextModificationDeleteCount(deleteLen); + } + } + } + + public void handleCharacter() { + commitVoiceInput(); + if (mAfterVoiceInput) { + // Assume input length is 1. This assumption fails for smiley face insertions. + mVoiceInput.incrementTextModificationInsertCount(1); + } + } + + public void handleSeparator() { + commitVoiceInput(); + if (mAfterVoiceInput){ + // Assume input length is 1. This assumption fails for smiley face insertions. + mVoiceInput.incrementTextModificationInsertPunctuationCount(1); + } + } + + public void handleClose() { + if (VOICE_INSTALLED & mRecognizing) { + mVoiceInput.cancel(); + } + } + + + public void handleVoiceResults(KeyboardSwitcher switcher, boolean capitalizeFirstWord) { + mAfterVoiceInput = true; + mImmediatelyAfterVoiceInput = true; + + InputConnection ic = mContext.getCurrentInputConnection(); + if (!mContext.isFullscreenMode()) { + // Start listening for updates to the text from typing, etc. + if (ic != null) { + ExtractedTextRequest req = new ExtractedTextRequest(); + ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); + } + } + mContext.vibrate(); + + final List<CharSequence> nBest = new ArrayList<CharSequence>(); + for (String c : mVoiceResults.candidates) { + if (capitalizeFirstWord) { + c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); + } + nBest.add(c); + } + if (nBest.size() == 0) { + return; + } + String bestResult = nBest.get(0).toString(); + mVoiceInput.logVoiceInputDelivered(bestResult.length()); + mHints.registerVoiceResult(bestResult); + + if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text + mContext.commitTyped(ic); + EditingUtil.appendText(ic, bestResult); + if (ic != null) ic.endBatchEdit(); + + mVoiceInputHighlighted = true; + mWordToSuggestions.putAll(mVoiceResults.alternatives); + onCancelVoice(); + } + + public void switchToRecognitionStatusView(final boolean configurationChanging) { + final boolean configChanged = configurationChanging; + mHandler.post(new Runnable() { + public void run() { + mContext.setCandidatesViewShown(false); + mRecognizing = true; + View v = mVoiceInput.getView(); + ViewParent p = v.getParent(); + if (p != null && p instanceof ViewGroup) { + ((ViewGroup)p).removeView(v); + } + mContext.setInputView(v); + mContext.updateInputViewShown(); + if (configChanged) { + mVoiceInput.onConfigurationChanged(); + } + }}); + } + + private void switchToLastInputMethod() { + IBinder token = mContext.getWindow().getWindow().getAttributes().token; + mImm.switchToLastInputMethod(token); + } + + private void reallyStartListening(boolean swipe, final boolean configurationChanging) { + if (!VOICE_INSTALLED) { + return; + } + if (!mHasUsedVoiceInput) { + // The user has started a voice input, so remember that in the + // future (so we don't show the warning dialog after the first run). + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(mContext).edit(); + editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true); + SharedPreferencesCompat.apply(editor); + mHasUsedVoiceInput = true; + } + + if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) { + // The user has started a voice input from an unsupported locale, so remember that + // in the future (so we don't show the warning dialog the next time they do this). + SharedPreferences.Editor editor = + PreferenceManager.getDefaultSharedPreferences(mContext).edit(); + editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true); + SharedPreferencesCompat.apply(editor); + mHasUsedVoiceInputUnsupportedLocale = true; + } + + // Clear N-best suggestions + mContext.clearSuggestions(); + + FieldContext context = makeFieldContext(); + mVoiceInput.startListening(context, swipe); + switchToRecognitionStatusView(configurationChanging); + } + + public void startListening(final boolean swipe, IBinder token, + final boolean configurationChanging) { + // TODO: remove swipe which is no longer used. + if (VOICE_INSTALLED) { + if (needsToShowWarningDialog()) { + // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel. + showVoiceWarningDialog(swipe, token, configurationChanging); + } else { + reallyStartListening(swipe, configurationChanging); + } + } + } + + + private boolean fieldCanDoVoice(FieldContext fieldContext) { + return !mPasswordText + && mVoiceInput != null + && !mVoiceInput.isBlacklistedField(fieldContext); + } + + private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) { + return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) + && !(attribute != null + && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions)) + && SpeechRecognizer.isRecognitionAvailable(mContext); + } + + public void loadSettings(EditorInfo attribute, SharedPreferences sp) { + mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); + mHasUsedVoiceInputUnsupportedLocale = + sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false); + + mLocaleSupportedForVoiceInput = SubtypeSwitcher.getInstance().isVoiceSupported( + SubtypeSwitcher.getInstance().getInputLocaleStr()); + + if (VOICE_INSTALLED) { + final String voiceMode = sp.getString(PREF_VOICE_MODE, + mContext.getString(R.string.voice_mode_main)); + mVoiceButtonEnabled = !voiceMode.equals(mContext.getString(R.string.voice_mode_off)) + && shouldShowVoiceButton(makeFieldContext(), attribute); + mVoiceButtonOnPrimary = voiceMode.equals(mContext.getString(R.string.voice_mode_main)); + } + } + + public void destroy() { + if (VOICE_INSTALLED && mVoiceInput != null) { + mVoiceInput.destroy(); + } + } + + public void onStartInputView(IBinder token) { + // If IME is in voice mode, but still needs to show the voice warning dialog, + // keep showing the warning. + if (mSubtypeSwitcher.isVoiceMode() && needsToShowWarningDialog() && token != null) { + showVoiceWarningDialog(false, token, false); + } + } + + public void onAttachedToWindow() { + // After onAttachedToWindow, we can show the voice warning dialog. See startListening() + // above. + mSubtypeSwitcher.setVoiceInput(mVoiceInput); + } + + public void onConfigurationChanged(boolean configurationChanging) { + if (mRecognizing) { + switchToRecognitionStatusView(configurationChanging); + } + } + + @Override + public void onCancelVoice() { + if (mRecognizing) { + if (mSubtypeSwitcher.isVoiceMode()) { + // If voice mode is being canceled within LatinIME (i.e. time-out or user + // cancellation etc.), onCancelVoice() will be called first. LatinIME thinks it's + // still in voice mode. LatinIME needs to call switchToLastInputMethod(). + // Note that onCancelVoice() will be called again from SubtypeSwitcher. + switchToLastInputMethod(); + } else if (mSubtypeSwitcher.isKeyboardMode()) { + // If voice mode is being canceled out of LatinIME (i.e. by user's IME switching or + // as a result of switchToLastInputMethod() etc.), + // onCurrentInputMethodSubtypeChanged() will be called first. LatinIME will know + // that it's in keyboard mode and SubtypeSwitcher will call onCancelVoice(). + mRecognizing = false; + mContext.switchToKeyboardView(); + } + } + } + + @Override + public void onVoiceResults(List<String> candidates, + Map<String, List<CharSequence>> alternatives) { + if (!mRecognizing) { + return; + } + mVoiceResults.candidates = candidates; + mVoiceResults.alternatives = alternatives; + mHandler.updateVoiceResults(); + } + + public FieldContext makeFieldContext() { + SubtypeSwitcher switcher = SubtypeSwitcher.getInstance(); + return new FieldContext(mContext.getCurrentInputConnection(), + mContext.getCurrentInputEditorInfo(), switcher.getInputLocaleStr(), + switcher.getEnabledLanguages()); + } + + private class VoiceResults { + List<String> candidates; + Map<String, List<CharSequence>> alternatives; + } +} diff --git a/java/src/com/android/inputmethod/voice/VoiceInput.java b/java/src/com/android/inputmethod/voice/VoiceInput.java index f24c180d0..6d45ef97c 100644 --- a/java/src/com/android/inputmethod/voice/VoiceInput.java +++ b/java/src/com/android/inputmethod/voice/VoiceInput.java @@ -16,6 +16,7 @@ package com.android.inputmethod.voice; +import com.android.inputmethod.latin.EditingUtil; import com.android.inputmethod.latin.R; import android.content.ContentResolver; @@ -27,11 +28,12 @@ import android.os.Handler; import android.os.Message; import android.os.Parcelable; import android.speech.RecognitionListener; -import android.speech.SpeechRecognizer; import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; +import android.view.inputmethod.InputConnection; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -423,8 +425,14 @@ public class VoiceInput implements OnClickListener { mLogger.textModifiedByTypingDeletion(length); } - public void logTextModifiedByChooseSuggestion(int length) { - mLogger.textModifiedByChooseSuggestion(length); + public void logTextModifiedByChooseSuggestion(String suggestion, int index, + String wordSeparators, InputConnection ic) { + EditingUtil.Range range = new EditingUtil.Range(); + String wordToBeReplaced = EditingUtil.getWordAtCursor(ic, wordSeparators, range); + // If we enable phrase-based alternatives, only send up the first word + // in suggestion and wordToBeReplaced. + mLogger.textModifiedByChooseSuggestion(suggestion.length(), wordToBeReplaced.length(), + index, wordToBeReplaced, suggestion); } public void logKeyboardWarningDialogShown() { @@ -455,10 +463,6 @@ public class VoiceInput implements OnClickListener { mLogger.voiceInputDelivered(length); } - public void logNBestChoose(int index) { - mLogger.nBestChoose(index); - } - public void logInputEnded() { mLogger.inputEnded(); } diff --git a/java/src/com/android/inputmethod/voice/VoiceInputLogger.java b/java/src/com/android/inputmethod/voice/VoiceInputLogger.java index 188d1376e..ec0ae649a 100644 --- a/java/src/com/android/inputmethod/voice/VoiceInputLogger.java +++ b/java/src/com/android/inputmethod/voice/VoiceInputLogger.java @@ -205,22 +205,22 @@ public class VoiceInputLogger { mContext.sendBroadcast(i); } - public void textModifiedByChooseSuggestion(int length) { + + public void textModifiedByChooseSuggestion(int suggestionLength, int replacedPhraseLength, + int index, String before, String after) { setHasLoggingInfo(true); Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED); - i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length); + i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, suggestionLength); + i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_REPLACED_LENGTH, replacedPhraseLength); i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE, LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_CHOOSE_SUGGESTION); - mContext.sendBroadcast(i); - } - public void nBestChoose(int index) { - setHasLoggingInfo(true); - Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.N_BEST_CHOOSE); i.putExtra(LoggingEvents.VoiceIme.EXTRA_N_BEST_CHOOSE_INDEX, index); + i.putExtra(LoggingEvents.VoiceIme.EXTRA_BEFORE_N_BEST_CHOOSE, before); + i.putExtra(LoggingEvents.VoiceIme.EXTRA_AFTER_N_BEST_CHOOSE, after); mContext.sendBroadcast(i); } - + public void inputEnded() { setHasLoggingInfo(true); mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.INPUT_ENDED)); diff --git a/java/src/com/android/inputmethod/voice/Whitelist.java b/java/src/com/android/inputmethod/voice/Whitelist.java index 167b688ca..f4c24de0c 100644 --- a/java/src/com/android/inputmethod/voice/Whitelist.java +++ b/java/src/com/android/inputmethod/voice/Whitelist.java @@ -17,6 +17,7 @@ package com.android.inputmethod.voice; import android.os.Bundle; + import java.util.ArrayList; import java.util.List; |