aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
-rw-r--r--java/src/com/android/inputmethod/latin/AutoDictionary.java217
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java151
-rwxr-xr-xjava/src/com/android/inputmethod/latin/CandidateView.java533
-rw-r--r--java/src/com/android/inputmethod/latin/CandidateViewContainer.java83
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsDictionary.java176
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java99
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java465
-rw-r--r--java/src/com/android/inputmethod/latin/Hints.java188
-rw-r--r--java/src/com/android/inputmethod/latin/InputLanguageSelection.java183
-rw-r--r--java/src/com/android/inputmethod/latin/KeyboardSwitcher.java351
-rw-r--r--java/src/com/android/inputmethod/latin/LanguageSwitcher.java182
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java1949
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java31
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIMESettings.java200
-rw-r--r--java/src/com/android/inputmethod/latin/LatinKeyboard.java762
-rw-r--r--java/src/com/android/inputmethod/latin/LatinKeyboardView.java332
-rwxr-xr-xjava/src/com/android/inputmethod/latin/Suggest.java378
-rw-r--r--java/src/com/android/inputmethod/latin/TextEntryState.java233
-rw-r--r--java/src/com/android/inputmethod/latin/Tutorial.java251
-rw-r--r--java/src/com/android/inputmethod/latin/UserDictionary.java138
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java174
21 files changed, 7076 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java
new file mode 100644
index 000000000..3d76dc301
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java
@@ -0,0 +1,217 @@
+/*
+ * 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 java.util.HashMap;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.provider.BaseColumns;
+import android.provider.UserDictionary.Words;
+import android.util.Log;
+
+/**
+ * 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
+ * to accept a word that's not in the main or user dictionary. Using a new word
+ * repeatedly will promote it to the user dictionary.
+ */
+public class AutoDictionary extends ExpandableDictionary {
+ // Weight added to a user picking a new word from the suggestion strip
+ static final int FREQUENCY_FOR_PICKED = 3;
+ // Weight added to a user typing a new word that doesn't get corrected (or is reverted)
+ static final int FREQUENCY_FOR_TYPED = 1;
+ // A word that is frequently typed and gets promoted to the user dictionary, uses this
+ // frequency.
+ static final int FREQUENCY_FOR_AUTO_ADD = 250;
+ // If the user touches a typed word 2 times or more, it will become valid.
+ private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
+ // If the user touches a typed word 4 times or more, it will be added to the user dict.
+ private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
+
+ private LatinIME mIme;
+ // Locale for which this auto dictionary is storing words
+ private String mLocale;
+
+ private static final String DATABASE_NAME = "auto_dict.db";
+ private static final int DATABASE_VERSION = 1;
+
+ // These are the columns in the dictionary
+ // TODO: Consume less space by using a unique id for locale instead of the whole
+ // 2-5 character string.
+ private static final String COLUMN_ID = BaseColumns._ID;
+ private static final String COLUMN_WORD = "word";
+ private static final String COLUMN_FREQUENCY = "freq";
+ private static final String COLUMN_LOCALE = "locale";
+
+ /** Sort by descending order of frequency. */
+ public static final String DEFAULT_SORT_ORDER = COLUMN_FREQUENCY + " DESC";
+
+ /** Name of the words table in the auto_dict.db */
+ private static final String AUTODICT_TABLE_NAME = "words";
+
+ private static HashMap<String, String> sDictProjectionMap;
+
+ static {
+ sDictProjectionMap = new HashMap<String, String>();
+ sDictProjectionMap.put(COLUMN_ID, COLUMN_ID);
+ sDictProjectionMap.put(COLUMN_WORD, COLUMN_WORD);
+ sDictProjectionMap.put(COLUMN_FREQUENCY, COLUMN_FREQUENCY);
+ sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE);
+ }
+
+ private DatabaseHelper mOpenHelper;
+
+ public AutoDictionary(Context context, LatinIME ime, String locale) {
+ super(context);
+ mIme = ime;
+ mLocale = locale;
+ mOpenHelper = new DatabaseHelper(getContext());
+ if (mLocale != null && mLocale.length() > 1) {
+ loadDictionary();
+ }
+ }
+
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ final int frequency = getWordFrequency(word);
+ return frequency >= VALIDITY_THRESHOLD;
+ }
+
+ public void close() {
+ mOpenHelper.close();
+ }
+
+ private void loadDictionary() {
+ // Load the words that correspond to the current input locale
+ Cursor cursor = query(COLUMN_LOCALE + "=?", new String[] { mLocale });
+ if (cursor.moveToFirst()) {
+ int wordIndex = cursor.getColumnIndex(COLUMN_WORD);
+ int frequencyIndex = cursor.getColumnIndex(COLUMN_FREQUENCY);
+ while (!cursor.isAfterLast()) {
+ String word = cursor.getString(wordIndex);
+ int frequency = cursor.getInt(frequencyIndex);
+ // Safeguard against adding really long words. Stack may overflow due
+ // to recursive lookup
+ if (word.length() < getMaxWordLength()) {
+ super.addWord(word, frequency);
+ }
+ cursor.moveToNext();
+ }
+ }
+ cursor.close();
+ }
+
+ @Override
+ public void addWord(String word, int addFrequency) {
+ final int length = word.length();
+ // Don't add very short or very long words.
+ if (length < 2 || length > getMaxWordLength()) return;
+ if (mIme.getCurrentWord().isAutoCapitalized()) {
+ // Remove caps before adding
+ word = Character.toLowerCase(word.charAt(0)) + word.substring(1);
+ }
+ int freq = getWordFrequency(word);
+ freq = freq < 0 ? addFrequency : freq + addFrequency;
+ super.addWord(word, freq);
+ if (freq >= PROMOTION_THRESHOLD) {
+ mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
+ // Delete the word (for input locale) from the auto dictionary db, as it
+ // is now in the user dictionary provider.
+ delete(COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
+ new String[] { word, mLocale });
+ } else {
+ update(word, freq, mLocale);
+ }
+ }
+
+ /**
+ * This class helps open, create, and upgrade the database file.
+ */
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + AUTODICT_TABLE_NAME + " ("
+ + COLUMN_ID + " INTEGER PRIMARY KEY,"
+ + COLUMN_WORD + " TEXT,"
+ + COLUMN_FREQUENCY + " INTEGER,"
+ + COLUMN_LOCALE + " TEXT"
+ + ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w("AutoDictionary", "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS " + AUTODICT_TABLE_NAME);
+ onCreate(db);
+ }
+ }
+
+ private Cursor query(String selection, String[] selectionArgs) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(AUTODICT_TABLE_NAME);
+ qb.setProjectionMap(sDictProjectionMap);
+
+ // Get the database and run the query
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ Cursor c = qb.query(db, null, selection, selectionArgs, null, null,
+ DEFAULT_SORT_ORDER);
+ return c;
+ }
+
+ private boolean insert(ContentValues values) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long rowId = db.insert(AUTODICT_TABLE_NAME, Words.WORD, values);
+ if (rowId > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ private int delete(String where, String[] whereArgs) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ int count = db.delete(AUTODICT_TABLE_NAME, where, whereArgs);
+ return count;
+ }
+
+ private int update(String word, int frequency, String locale) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ long count = db.delete(AUTODICT_TABLE_NAME, COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
+ new String[] { word, locale });
+ count = db.insert(AUTODICT_TABLE_NAME, null,
+ getContentValues(word, frequency, locale));
+ return (int) count;
+ }
+
+ private ContentValues getContentValues(String word, int frequency, String locale) {
+ ContentValues values = new ContentValues(4);
+ values.put(COLUMN_WORD, word);
+ values.put(COLUMN_FREQUENCY, frequency);
+ values.put(COLUMN_LOCALE, locale);
+ return values;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
new file mode 100644
index 000000000..43f4c4cb6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.Arrays;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.util.Log;
+
+/**
+ * Implements a static, compacted, binary dictionary of standard words.
+ */
+public class BinaryDictionary extends Dictionary {
+
+ public static final int MAX_WORD_LENGTH = 48;
+ private static final int MAX_ALTERNATIVES = 16;
+ private static final int MAX_WORDS = 16;
+
+ private static final int TYPED_LETTER_MULTIPLIER = 2;
+ private static final boolean ENABLE_MISSED_CHARACTERS = true;
+
+ private int mNativeDict;
+ private int mDictLength; // This value is set from native code, don't change the name!!!!
+ private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES];
+ private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
+ private int[] mFrequencies = new int[MAX_WORDS];
+
+ static {
+ try {
+ System.loadLibrary("jni_latinime");
+ } catch (UnsatisfiedLinkError ule) {
+ Log.e("BinaryDictionary", "Could not load native library jni_latinime");
+ }
+ }
+
+ /**
+ * Create a dictionary from a raw resource file
+ * @param context application context for reading resources
+ * @param resId the resource containing the raw binary dictionary
+ */
+ public BinaryDictionary(Context context, int resId) {
+ if (resId != 0) {
+ loadDictionary(context, resId);
+ }
+ }
+
+ private native int openNative(AssetManager am, String resourcePath, int typedLetterMultiplier,
+ int fullWordMultiplier);
+ private native void closeNative(int dict);
+ private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
+ private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize,
+ char[] outputChars, int[] frequencies,
+ int maxWordLength, int maxWords, int maxAlternatives, int skipPos,
+ int[] nextLettersFrequencies, int nextLettersSize);
+
+ private final void loadDictionary(Context context, int resId) {
+ AssetManager am = context.getResources().getAssets();
+ String assetName = context.getResources().getString(resId);
+ mNativeDict = openNative(am, assetName, TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER);
+ }
+
+ @Override
+ public void getWords(final WordComposer codes, final WordCallback callback,
+ int[] nextLettersFrequencies) {
+ final int codesSize = codes.size();
+ // Wont deal with really long words.
+ if (codesSize > MAX_WORD_LENGTH - 1) return;
+
+ Arrays.fill(mInputCodes, -1);
+ for (int i = 0; i < codesSize; i++) {
+ int[] alternatives = codes.getCodesAt(i);
+ System.arraycopy(alternatives, 0, mInputCodes, i * MAX_ALTERNATIVES,
+ Math.min(alternatives.length, MAX_ALTERNATIVES));
+ }
+ 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,
+ 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;
+ int len = 0;
+ while (mOutputChars[start + len] != 0) {
+ len++;
+ }
+ if (len > 0) {
+ callback.addWord(mOutputChars, start, len, mFrequencies[j]);
+ }
+ }
+ }
+
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ if (word == null) return false;
+ char[] chars = word.toString().toCharArray();
+ return isValidWordNative(mNativeDict, chars, chars.length);
+ }
+
+ public int getSize() {
+ return mDictLength; // This value is initialized on the call to openNative()
+ }
+
+ @Override
+ public synchronized void close() {
+ if (mNativeDict != 0) {
+ closeNative(mNativeDict);
+ mNativeDict = 0;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
new file mode 100755
index 000000000..0c0373b3b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+public class CandidateView extends View {
+
+ private static final int OUT_OF_BOUNDS = -1;
+ private static final List<CharSequence> EMPTY_LIST = new ArrayList<CharSequence>();
+
+ private LatinIME mService;
+ private List<CharSequence> mSuggestions = EMPTY_LIST;
+ private boolean mShowingCompletions;
+ private CharSequence mSelectedString;
+ private int mSelectedIndex;
+ private int mTouchX = OUT_OF_BOUNDS;
+ private Drawable mSelectionHighlight;
+ private boolean mTypedWordValid;
+
+ private boolean mHaveMinimalSuggestion;
+
+ private Rect mBgPadding;
+
+ private TextView mPreviewText;
+ private PopupWindow mPreviewPopup;
+ private int mCurrentWordIndex;
+ private Drawable mDivider;
+
+ private static final int MAX_SUGGESTIONS = 32;
+ private static final int SCROLL_PIXELS = 20;
+
+ private static final int MSG_REMOVE_PREVIEW = 1;
+ private static final int MSG_REMOVE_THROUGH_PREVIEW = 2;
+
+ private int[] mWordWidth = new int[MAX_SUGGESTIONS];
+ private int[] mWordX = new int[MAX_SUGGESTIONS];
+ private int mPopupPreviewX;
+ private int mPopupPreviewY;
+
+ private static final int X_GAP = 10;
+
+ private int mColorNormal;
+ private int mColorRecommended;
+ private int mColorOther;
+ private Paint mPaint;
+ private int mDescent;
+ private boolean mScrolled;
+ private boolean mShowingAddToDictionary;
+ private CharSequence mWordToAddToDictionary;
+ private CharSequence mAddToDictionaryHint;
+
+ private int mTargetScrollX;
+
+ private int mMinTouchableWidth;
+
+ private int mTotalWidth;
+
+ private GestureDetector mGestureDetector;
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REMOVE_PREVIEW:
+ mPreviewText.setVisibility(GONE);
+ break;
+ case MSG_REMOVE_THROUGH_PREVIEW:
+ mPreviewText.setVisibility(GONE);
+ if (mTouchX != OUT_OF_BOUNDS) {
+ removeHighlight();
+ }
+ break;
+ }
+
+ }
+ };
+
+ /**
+ * Construct a CandidateView for showing suggested words for completion.
+ * @param context
+ * @param attrs
+ */
+ public CandidateView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mSelectionHighlight = context.getResources().getDrawable(
+ R.drawable.list_selector_background_pressed);
+
+ LayoutInflater inflate =
+ (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ Resources res = context.getResources();
+ mPreviewPopup = new PopupWindow(context);
+ mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null);
+ mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ mPreviewPopup.setContentView(mPreviewText);
+ mPreviewPopup.setBackgroundDrawable(null);
+ mColorNormal = res.getColor(R.color.candidate_normal);
+ mColorRecommended = res.getColor(R.color.candidate_recommended);
+ mColorOther = res.getColor(R.color.candidate_other);
+ mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider);
+ mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary);
+
+ mPaint = new Paint();
+ mPaint.setColor(mColorNormal);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextSize(mPreviewText.getTextSize());
+ mPaint.setStrokeWidth(0);
+ mPaint.setTextAlign(Align.CENTER);
+ mDescent = (int) mPaint.descent();
+ // 80 pixels for a 160dpi device would mean half an inch
+ mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50);
+
+ mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public void onLongPress(MotionEvent me) {
+ if (mSuggestions.size() > 0) {
+ if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) {
+ longPressFirstWord();
+ }
+ }
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2,
+ float distanceX, float distanceY) {
+ final int width = getWidth();
+ mScrolled = true;
+ int scrollX = getScrollX();
+ scrollX += (int) distanceX;
+ if (scrollX < 0) {
+ scrollX = 0;
+ }
+ if (distanceX > 0 && scrollX + width > mTotalWidth) {
+ scrollX -= (int) distanceX;
+ }
+ mTargetScrollX = scrollX;
+ scrollTo(scrollX, getScrollY());
+ hidePreview();
+ invalidate();
+ return true;
+ }
+ });
+ setHorizontalFadingEdgeEnabled(true);
+ setWillNotDraw(false);
+ setHorizontalScrollBarEnabled(false);
+ setVerticalScrollBarEnabled(false);
+ scrollTo(0, getScrollY());
+ }
+
+ /**
+ * A connection back to the service to communicate with the text field
+ * @param listener
+ */
+ public void setService(LatinIME listener) {
+ mService = listener;
+ }
+
+ @Override
+ public int computeHorizontalScrollRange() {
+ return mTotalWidth;
+ }
+
+ /**
+ * If the canvas is null, then only touch calculations are performed to pick the target
+ * candidate.
+ */
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (canvas != null) {
+ super.onDraw(canvas);
+ }
+ mTotalWidth = 0;
+ if (mSuggestions == null) return;
+
+ final int height = getHeight();
+ if (mBgPadding == null) {
+ mBgPadding = new Rect(0, 0, 0, 0);
+ if (getBackground() != null) {
+ getBackground().getPadding(mBgPadding);
+ }
+ mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(),
+ mDivider.getIntrinsicHeight());
+ }
+ int x = 0;
+ final int count = mSuggestions.size();
+ final int width = getWidth();
+ final Rect bgPadding = mBgPadding;
+ final Paint paint = mPaint;
+ final int touchX = mTouchX;
+ final int scrollX = getScrollX();
+ final boolean scrolled = mScrolled;
+ final boolean typedWordValid = mTypedWordValid;
+ final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2;
+
+ for (int i = 0; i < count; i++) {
+ CharSequence suggestion = mSuggestions.get(i);
+ if (suggestion == null) continue;
+ paint.setColor(mColorNormal);
+ if (mHaveMinimalSuggestion
+ && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) {
+ paint.setTypeface(Typeface.DEFAULT_BOLD);
+ paint.setColor(mColorRecommended);
+ } else if (i != 0) {
+ paint.setColor(mColorOther);
+ }
+ final int wordWidth;
+ if (mWordWidth[i] != 0) {
+ wordWidth = mWordWidth[i];
+ } else {
+ float textWidth = paint.measureText(suggestion, 0, suggestion.length());
+ wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2);
+ mWordWidth[i] = wordWidth;
+ }
+
+ mWordX[i] = x;
+
+ if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled &&
+ touchX != OUT_OF_BOUNDS) {
+ if (canvas != null && !mShowingAddToDictionary) {
+ canvas.translate(x, 0);
+ mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height);
+ mSelectionHighlight.draw(canvas);
+ canvas.translate(-x, 0);
+ showPreview(i, null);
+ }
+ mSelectedString = suggestion;
+ mSelectedIndex = i;
+ }
+
+ if (canvas != null) {
+ canvas.drawText(suggestion, 0, suggestion.length(), x + wordWidth / 2, y, paint);
+ paint.setColor(mColorOther);
+ canvas.translate(x + wordWidth, 0);
+ // Draw a divider unless it's after the hint
+ if (!(mShowingAddToDictionary && i == 1)) {
+ mDivider.draw(canvas);
+ }
+ canvas.translate(-x - wordWidth, 0);
+ }
+ paint.setTypeface(Typeface.DEFAULT);
+ x += wordWidth;
+ }
+ mTotalWidth = x;
+ if (mTargetScrollX != scrollX) {
+ scrollToTarget();
+ }
+ }
+
+ private void scrollToTarget() {
+ int scrollX = getScrollX();
+ if (mTargetScrollX > scrollX) {
+ scrollX += SCROLL_PIXELS;
+ if (scrollX >= mTargetScrollX) {
+ scrollX = mTargetScrollX;
+ scrollTo(scrollX, getScrollY());
+ requestLayout();
+ } else {
+ scrollTo(scrollX, getScrollY());
+ }
+ } else {
+ scrollX -= SCROLL_PIXELS;
+ if (scrollX <= mTargetScrollX) {
+ scrollX = mTargetScrollX;
+ scrollTo(scrollX, getScrollY());
+ requestLayout();
+ } else {
+ scrollTo(scrollX, getScrollY());
+ }
+ }
+ invalidate();
+ }
+
+ public void setSuggestions(List<CharSequence> suggestions, boolean completions,
+ boolean typedWordValid, boolean haveMinimalSuggestion) {
+ clear();
+ if (suggestions != null) {
+ mSuggestions = new ArrayList<CharSequence>(suggestions);
+ }
+ mShowingCompletions = completions;
+ mTypedWordValid = typedWordValid;
+ scrollTo(0, getScrollY());
+ mTargetScrollX = 0;
+ mHaveMinimalSuggestion = haveMinimalSuggestion;
+ // Compute the total width
+ onDraw(null);
+ invalidate();
+ requestLayout();
+ }
+
+ public void showAddToDictionaryHint(CharSequence word) {
+ mWordToAddToDictionary = word;
+ ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>();
+ suggestions.add(word);
+ suggestions.add(mAddToDictionaryHint);
+ setSuggestions(suggestions, false, false, false);
+ mShowingAddToDictionary = true;
+ }
+
+ public void scrollPrev() {
+ int i = 0;
+ final int count = mSuggestions.size();
+ int firstItem = 0; // Actually just before the first item, if at the boundary
+ while (i < count) {
+ if (mWordX[i] < getScrollX()
+ && mWordX[i] + mWordWidth[i] >= getScrollX() - 1) {
+ firstItem = i;
+ break;
+ }
+ i++;
+ }
+ int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth();
+ if (leftEdge < 0) leftEdge = 0;
+ updateScrollPosition(leftEdge);
+ }
+
+ public void scrollNext() {
+ int i = 0;
+ int scrollX = getScrollX();
+ int targetX = scrollX;
+ final int count = mSuggestions.size();
+ int rightEdge = scrollX + getWidth();
+ while (i < count) {
+ if (mWordX[i] <= rightEdge &&
+ mWordX[i] + mWordWidth[i] >= rightEdge) {
+ targetX = Math.min(mWordX[i], mTotalWidth - getWidth());
+ break;
+ }
+ i++;
+ }
+ updateScrollPosition(targetX);
+ }
+
+ private void updateScrollPosition(int targetX) {
+ if (targetX != getScrollX()) {
+ // TODO: Animate
+ mTargetScrollX = targetX;
+ requestLayout();
+ invalidate();
+ mScrolled = true;
+ }
+ }
+
+ public void clear() {
+ mSuggestions = EMPTY_LIST;
+ mTouchX = OUT_OF_BOUNDS;
+ mSelectedString = null;
+ mSelectedIndex = -1;
+ mShowingAddToDictionary = false;
+ invalidate();
+ Arrays.fill(mWordWidth, 0);
+ Arrays.fill(mWordX, 0);
+ if (mPreviewPopup.isShowing()) {
+ mPreviewPopup.dismiss();
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+
+ if (mGestureDetector.onTouchEvent(me)) {
+ return true;
+ }
+
+ int action = me.getAction();
+ int x = (int) me.getX();
+ int y = (int) me.getY();
+ mTouchX = x;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mScrolled = false;
+ invalidate();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (y <= 0) {
+ // Fling up!?
+ if (mSelectedString != null) {
+ if (!mShowingCompletions) {
+ TextEntryState.acceptedSuggestion(mSuggestions.get(0),
+ mSelectedString);
+ }
+ mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
+ mSelectedString = null;
+ mSelectedIndex = -1;
+ }
+ }
+ invalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (!mScrolled) {
+ if (mSelectedString != null) {
+ if (mShowingAddToDictionary) {
+ longPressFirstWord();
+ clear();
+ } else {
+ if (!mShowingCompletions) {
+ TextEntryState.acceptedSuggestion(mSuggestions.get(0),
+ mSelectedString);
+ }
+ mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
+ }
+ }
+ }
+ mSelectedString = null;
+ mSelectedIndex = -1;
+ removeHighlight();
+ hidePreview();
+ requestLayout();
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * For flick through from keyboard, call this method with the x coordinate of the flick
+ * gesture.
+ * @param x
+ */
+ public void takeSuggestionAt(float x) {
+ mTouchX = (int) x;
+ // To detect candidate
+ onDraw(null);
+ if (mSelectedString != null) {
+ if (!mShowingCompletions) {
+ TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString);
+ }
+ mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
+ }
+ invalidate();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_REMOVE_THROUGH_PREVIEW), 200);
+ }
+
+ private void hidePreview() {
+ mCurrentWordIndex = OUT_OF_BOUNDS;
+ if (mPreviewPopup.isShowing()) {
+ mHandler.sendMessageDelayed(mHandler
+ .obtainMessage(MSG_REMOVE_PREVIEW), 60);
+ }
+ }
+
+ private void showPreview(int wordIndex, String altText) {
+ int oldWordIndex = mCurrentWordIndex;
+ mCurrentWordIndex = wordIndex;
+ // If index changed or changing text
+ if (oldWordIndex != mCurrentWordIndex || altText != null) {
+ if (wordIndex == OUT_OF_BOUNDS) {
+ hidePreview();
+ } else {
+ CharSequence word = altText != null? altText : mSuggestions.get(wordIndex);
+ mPreviewText.setText(word);
+ mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2);
+ final int popupWidth = wordWidth
+ + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight();
+ final int popupHeight = mPreviewText.getMeasuredHeight();
+ //mPreviewText.setVisibility(INVISIBLE);
+ mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX()
+ + (mWordWidth[wordIndex] - wordWidth) / 2;
+ mPopupPreviewY = - popupHeight;
+ mHandler.removeMessages(MSG_REMOVE_PREVIEW);
+ int [] offsetInWindow = new int[2];
+ getLocationInWindow(offsetInWindow);
+ if (mPreviewPopup.isShowing()) {
+ mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1],
+ popupWidth, popupHeight);
+ } else {
+ mPreviewPopup.setWidth(popupWidth);
+ mPreviewPopup.setHeight(popupHeight);
+ mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX,
+ mPopupPreviewY + offsetInWindow[1]);
+ }
+ mPreviewText.setVisibility(VISIBLE);
+ }
+ }
+ }
+
+ private void removeHighlight() {
+ mTouchX = OUT_OF_BOUNDS;
+ invalidate();
+ }
+
+ private void longPressFirstWord() {
+ CharSequence word = mSuggestions.get(0);
+ if (word.length() < 2) return;
+ if (mService.addWordToDictionary(word.toString())) {
+ showPreview(0, getContext().getResources().getString(R.string.added_word, word));
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ hidePreview();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/CandidateViewContainer.java b/java/src/com/android/inputmethod/latin/CandidateViewContainer.java
new file mode 100644
index 000000000..e13f2738c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CandidateViewContainer.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2008-2009 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.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.widget.LinearLayout;
+
+public class CandidateViewContainer extends LinearLayout implements OnTouchListener {
+
+ private View mButtonLeft;
+ private View mButtonRight;
+ private View mButtonLeftLayout;
+ private View mButtonRightLayout;
+ private CandidateView mCandidates;
+
+ public CandidateViewContainer(Context screen, AttributeSet attrs) {
+ super(screen, attrs);
+ }
+
+ public void initViews() {
+ if (mCandidates == null) {
+ mButtonLeftLayout = findViewById(R.id.candidate_left_parent);
+ mButtonLeft = findViewById(R.id.candidate_left);
+ if (mButtonLeft != null) {
+ mButtonLeft.setOnTouchListener(this);
+ }
+ mButtonRightLayout = findViewById(R.id.candidate_right_parent);
+ mButtonRight = findViewById(R.id.candidate_right);
+ if (mButtonRight != null) {
+ mButtonRight.setOnTouchListener(this);
+ }
+ mCandidates = (CandidateView) findViewById(R.id.candidates);
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (mCandidates != null) {
+ int availableWidth = mCandidates.getWidth();
+ int neededWidth = mCandidates.computeHorizontalScrollRange();
+ int x = mCandidates.getScrollX();
+ boolean leftVisible = x > 0;
+ boolean rightVisible = x + availableWidth < neededWidth;
+ if (mButtonLeftLayout != null) {
+ mButtonLeftLayout.setVisibility(leftVisible ? VISIBLE : GONE);
+ }
+ if (mButtonRightLayout != null) {
+ mButtonRightLayout.setVisibility(rightVisible ? VISIBLE : GONE);
+ }
+ }
+ super.requestLayout();
+ }
+
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (v == mButtonRight) {
+ mCandidates.scrollNext();
+ } else if (v == mButtonLeft) {
+ mCandidates.scrollPrev();
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
new file mode 100644
index 000000000..f53ebf3f5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2009 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 android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.SystemClock;
+import android.provider.ContactsContract.Contacts;
+
+public class ContactsDictionary extends ExpandableDictionary {
+
+ private static final String[] PROJECTION = {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME,
+ };
+
+ private static final int INDEX_NAME = 1;
+
+ private ContentObserver mObserver;
+
+ private boolean mRequiresReload;
+
+ private long mLastLoadedContacts;
+
+ private boolean mUpdatingContacts;
+
+ // Use this lock before touching mUpdatingContacts & mRequiresDownload
+ private Object mUpdatingLock = new Object();
+
+ public ContactsDictionary(Context context) {
+ super(context);
+ // Perform a managed query. The Activity will handle closing and requerying the cursor
+ // when needed.
+ ContentResolver cres = context.getContentResolver();
+
+ cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean self) {
+ synchronized (mUpdatingLock) {
+ mRequiresReload = true;
+ }
+ }
+ });
+
+ synchronized (mUpdatingLock) {
+ loadDictionaryAsyncLocked();
+ }
+ }
+
+ public synchronized void close() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ private synchronized void loadDictionaryAsyncLocked() {
+ long now = SystemClock.uptimeMillis();
+ if (mLastLoadedContacts == 0
+ || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
+ if (!mUpdatingContacts) {
+ mUpdatingContacts = true;
+ mRequiresReload = false;
+ new LoadContactsTask().execute();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void getWords(final WordComposer codes, final WordCallback callback,
+ int[] nextLettersFrequencies) {
+ synchronized (mUpdatingLock) {
+ // If we need to update, start off a background task
+ if (mRequiresReload) loadDictionaryAsyncLocked();
+ // Currently updating contacts, don't return any results.
+ if (mUpdatingContacts) return;
+ }
+ super.getWords(codes, callback, nextLettersFrequencies);
+ }
+
+ @Override
+ public synchronized boolean isValidWord(CharSequence word) {
+ synchronized (mUpdatingLock) {
+ // If we need to update, start off a background task
+ if (mRequiresReload) loadDictionaryAsyncLocked();
+ if (mUpdatingContacts) return false;
+ }
+
+ return super.isValidWord(word);
+ }
+
+ private void addWords(Cursor cursor) {
+ clearDictionary();
+
+ final int maxWordLength = getMaxWordLength();
+ if (cursor.moveToFirst()) {
+ while (!cursor.isAfterLast()) {
+ String name = cursor.getString(INDEX_NAME);
+
+ if (name != null) {
+ int len = name.length();
+
+ // TODO: Better tokenization for non-Latin writing systems
+ for (int i = 0; i < len; i++) {
+ if (Character.isLetter(name.charAt(i))) {
+ int j;
+ for (j = i + 1; j < len; j++) {
+ char c = name.charAt(j);
+
+ if (!(c == '-' || c == '\'' ||
+ Character.isLetter(c))) {
+ break;
+ }
+ }
+
+ String word = name.substring(i, j);
+ i = j - 1;
+
+ // Safeguard against adding really long words. Stack
+ // may overflow due to recursion
+ // Also don't add single letter words, possibly confuses
+ // capitalization of i.
+ final int wordLen = word.length();
+ if (wordLen < maxWordLength && wordLen > 1) {
+ super.addWord(word, 128);
+ }
+ }
+ }
+ }
+
+ cursor.moveToNext();
+ }
+ }
+ cursor.close();
+ }
+
+ private class LoadContactsTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected Void doInBackground(Void... v) {
+ Cursor cursor = getContext().getContentResolver()
+ .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
+ if (cursor != null) {
+ addWords(cursor);
+ }
+ mLastLoadedContacts = SystemClock.uptimeMillis();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ // TODO Auto-generated method stub
+ synchronized (mUpdatingLock) {
+ mUpdatingContacts = false;
+ }
+ super.onPostExecute(result);
+ }
+
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
new file mode 100644
index 000000000..b656d04dc
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2008-2009 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;
+
+/**
+ * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
+ * strokes.
+ */
+abstract public class Dictionary {
+
+ /**
+ * Whether or not to replicate the typed word in the suggested list, even if it's valid.
+ */
+ protected static final boolean INCLUDE_TYPED_WORD_IF_VALID = false;
+
+ /**
+ * The weight to give to a word if it's length is the same as the number of typed characters.
+ */
+ protected static final int FULL_WORD_FREQ_MULTIPLIER = 2;
+
+ /**
+ * Interface to be implemented by classes requesting words to be fetched from the dictionary.
+ * @see #getWords(WordComposer, WordCallback)
+ */
+ public interface WordCallback {
+ /**
+ * Adds a word to a list of suggestions. The word is expected to be ordered based on
+ * the provided frequency.
+ * @param word the character array containing the word
+ * @param wordOffset starting offset of the word in the character array
+ * @param wordLength length of valid characters in the character array
+ * @param frequency the frequency of occurence. This is normalized between 1 and 255, but
+ * can exceed those limits
+ * @return true if the word was added, false if no more words are required
+ */
+ boolean addWord(char[] word, int wordOffset, int wordLength, int frequency);
+ }
+
+ /**
+ * Searches for words in the dictionary that match the characters in the composer. Matched
+ * words are added through the callback object.
+ * @param composer the key sequence to match
+ * @param callback the callback object to send matched words to as possible candidates
+ * @param nextLettersFrequencies array of frequencies of next letters that could follow the
+ * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
+ * a non-zero value on returning from this method.
+ * Pass in null if you don't want the dictionary to look up next letters.
+ * @see WordCallback#addWord(char[], int, int)
+ */
+ abstract public void getWords(final WordComposer composer, final WordCallback callback,
+ int[] nextLettersFrequencies);
+
+ /**
+ * Checks if the given word occurs in the dictionary
+ * @param word the word to search for. The search should be case-insensitive.
+ * @return true if the word exists, false otherwise
+ */
+ abstract public boolean isValidWord(CharSequence word);
+
+ /**
+ * Compares the contents of the character array with the typed word and returns true if they
+ * are the same.
+ * @param word the array of characters that make up the word
+ * @param length the number of valid characters in the character array
+ * @param typedWord the word to compare with
+ * @return true if they are the same, false otherwise.
+ */
+ protected boolean same(final char[] word, final int length, final CharSequence typedWord) {
+ if (typedWord.length() != length) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (word[i] != typedWord.charAt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Override to clean up any resources.
+ */
+ public void close() {
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
new file mode 100644
index 000000000..006593700
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2009 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 android.content.Context;
+
+/**
+ * Base class for an in-memory dictionary that can grow dynamically and can
+ * be searched for suggestions and valid words.
+ */
+public class ExpandableDictionary extends Dictionary {
+ private Context mContext;
+ private char[] mWordBuilder = new char[MAX_WORD_LENGTH];
+ private int mMaxDepth;
+ private int mInputLength;
+ private int[] mNextLettersFrequencies;
+
+ public static final int MAX_WORD_LENGTH = 32;
+ private static final char QUOTE = '\'';
+
+ static class Node {
+ char code;
+ int frequency;
+ boolean terminal;
+ NodeArray children;
+ }
+
+ static class NodeArray {
+ Node[] data;
+ int length = 0;
+ private static final int INCREMENT = 2;
+
+ NodeArray() {
+ data = new Node[INCREMENT];
+ }
+
+ void add(Node n) {
+ if (length + 1 > data.length) {
+ Node[] tempData = new Node[length + INCREMENT];
+ if (length > 0) {
+ System.arraycopy(data, 0, tempData, 0, length);
+ }
+ data = tempData;
+ }
+ data[length++] = n;
+ }
+ }
+
+ private NodeArray mRoots;
+
+ private int[][] mCodes;
+
+ ExpandableDictionary(Context context) {
+ mContext = context;
+ clearDictionary();
+ mCodes = new int[MAX_WORD_LENGTH][];
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ int getMaxWordLength() {
+ return MAX_WORD_LENGTH;
+ }
+
+ public void addWord(String word, int frequency) {
+ addWordRec(mRoots, word, 0, frequency);
+ }
+
+ private void addWordRec(NodeArray children, final String word,
+ final int depth, final int frequency) {
+
+ final int wordLength = word.length();
+ final char c = word.charAt(depth);
+ // Does children have the current character?
+ final int childrenLength = children.length;
+ Node childNode = null;
+ boolean found = false;
+ for (int i = 0; i < childrenLength; i++) {
+ childNode = children.data[i];
+ if (childNode.code == c) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ childNode = new Node();
+ childNode.code = c;
+ children.add(childNode);
+ }
+ if (wordLength == depth + 1) {
+ // Terminate this word
+ childNode.terminal = true;
+ childNode.frequency = Math.max(frequency, childNode.frequency);
+ if (childNode.frequency > 255) childNode.frequency = 255;
+ return;
+ }
+ if (childNode.children == null) {
+ childNode.children = new NodeArray();
+ }
+ addWordRec(childNode.children, word, depth + 1, frequency);
+ }
+
+ @Override
+ public void getWords(final WordComposer codes, final WordCallback callback,
+ int[] nextLettersFrequencies) {
+ mInputLength = codes.size();
+ mNextLettersFrequencies = nextLettersFrequencies;
+ if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
+ // Cache the codes so that we don't have to lookup an array list
+ for (int i = 0; i < mInputLength; i++) {
+ mCodes[i] = codes.getCodesAt(i);
+ }
+ mMaxDepth = mInputLength * 3;
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
+ for (int i = 0; i < mInputLength; i++) {
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
+ }
+ }
+
+ @Override
+ public synchronized boolean isValidWord(CharSequence word) {
+ final int freq = getWordFrequencyRec(mRoots, word, 0, word.length());
+ return freq > -1;
+ }
+
+ /**
+ * Returns the word's frequency or -1 if not found
+ */
+ public int getWordFrequency(CharSequence word) {
+ return getWordFrequencyRec(mRoots, word, 0, word.length());
+ }
+
+ /**
+ * Returns the word's frequency or -1 if not found
+ */
+ private int getWordFrequencyRec(final NodeArray children, final CharSequence word,
+ final int offset, final int length) {
+ final int count = children.length;
+ char currentChar = word.charAt(offset);
+ for (int j = 0; j < count; j++) {
+ final Node node = children.data[j];
+ if (node.code == currentChar) {
+ if (offset == length - 1) {
+ if (node.terminal) {
+ return node.frequency;
+ }
+ } else {
+ if (node.children != null) {
+ int freq = getWordFrequencyRec(node.children, word, offset + 1, length);
+ if (freq > -1) return freq;
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Recursively traverse the tree for words that match the input. Input consists of
+ * a list of arrays. Each item in the list is one input character position. An input
+ * character is actually an array of multiple possible candidates. This function is not
+ * optimized for speed, assuming that the user dictionary will only be a few hundred words in
+ * size.
+ * @param roots node whose children have to be search for matches
+ * @param codes the input character codes
+ * @param word the word being composed as a possible match
+ * @param depth the depth of traversal - the length of the word being composed thus far
+ * @param completion whether the traversal is now in completion mode - meaning that we've
+ * exhausted the input and we're looking for all possible suffixes.
+ * @param snr current weight of the word being formed
+ * @param inputIndex position in the input characters. This can be off from the depth in
+ * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
+ * "wouldve", it could be matching "would've", so the depth will be one more than the
+ * inputIndex
+ * @param callback the callback class for adding a word
+ */
+ protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
+ final int depth, boolean completion, int snr, int inputIndex, int skipPos,
+ WordCallback callback) {
+ final int count = roots.length;
+ final int codeSize = mInputLength;
+ // Optimization: Prune out words that are too long compared to how much was typed.
+ if (depth > mMaxDepth) {
+ return;
+ }
+ int[] currentChars = null;
+ if (codeSize <= inputIndex) {
+ completion = true;
+ } else {
+ currentChars = mCodes[inputIndex];
+ }
+
+ for (int i = 0; i < count; i++) {
+ final Node node = roots.data[i];
+ final char c = node.code;
+ final char lowerC = toLowerCase(c);
+ final boolean terminal = node.terminal;
+ final NodeArray children = node.children;
+ final int freq = node.frequency;
+ if (completion) {
+ word[depth] = c;
+ if (terminal) {
+ if (!callback.addWord(word, 0, depth + 1, freq * snr)) {
+ return;
+ }
+ // Add to frequency of next letters for predictive correction
+ if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0
+ && mNextLettersFrequencies.length > word[inputIndex]) {
+ mNextLettersFrequencies[word[inputIndex]]++;
+ }
+ }
+ if (children != null) {
+ getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
+ skipPos, callback);
+ }
+ } else if ((c == QUOTE && currentChars[0] != QUOTE) || depth == skipPos) {
+ // Skip the ' and continue deeper
+ word[depth] = c;
+ if (children != null) {
+ getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
+ skipPos, callback);
+ }
+ } else {
+ // Don't use alternatives if we're looking for missing characters
+ final int alternativesSize = skipPos >= 0? 1 : currentChars.length;
+ for (int j = 0; j < alternativesSize; j++) {
+ final int addedAttenuation = (j > 0 ? 1 : 2);
+ final int currentChar = currentChars[j];
+ if (currentChar == -1) {
+ break;
+ }
+ if (currentChar == lowerC || currentChar == c) {
+ word[depth] = c;
+
+ if (codeSize == inputIndex + 1) {
+ if (terminal) {
+ if (INCLUDE_TYPED_WORD_IF_VALID
+ || !same(word, depth + 1, codes.getTypedWord())) {
+ int finalFreq = freq * snr * addedAttenuation;
+ if (skipPos < 0) finalFreq *= FULL_WORD_FREQ_MULTIPLIER;
+ callback.addWord(word, 0, depth + 1, finalFreq);
+ }
+ }
+ if (children != null) {
+ getWordsRec(children, codes, word, depth + 1,
+ true, snr * addedAttenuation, inputIndex + 1,
+ skipPos, callback);
+ }
+ } else if (children != null) {
+ getWordsRec(children, codes, word, depth + 1,
+ false, snr * addedAttenuation, inputIndex + 1,
+ skipPos, callback);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ protected void clearDictionary() {
+ mRoots = new NodeArray();
+ }
+
+ static char toLowerCase(char c) {
+ if (c < BASE_CHARS.length) {
+ c = BASE_CHARS[c];
+ }
+ if (c >= 'A' && c <= 'Z') {
+ c = (char) (c | 32);
+ } else if (c > 127) {
+ c = Character.toLowerCase(c);
+ }
+ return c;
+ }
+
+ /**
+ * Table mapping most combined Latin, Greek, and Cyrillic characters
+ * to their base characters. If c is in range, BASE_CHARS[c] == c
+ * if c is not a combined character, or the base character if it
+ * is combined.
+ */
+ static final char BASE_CHARS[] = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
+ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
+ 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
+ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
+ 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
+ 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
+ 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020,
+ 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
+ 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043,
+ 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7,
+ 0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f
+ // Manually changed df to 73
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
+ 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f
+ 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
+ 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
+ 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
+ 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
+ 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
+ 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
+ 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b,
+ 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c,
+ 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e,
+ 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f,
+ 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
+ 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
+ 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
+ 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
+ 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
+ 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073,
+ 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
+ 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f,
+ 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
+ 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f,
+ 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7,
+ 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055,
+ 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7,
+ 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf,
+ 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c,
+ 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049,
+ 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc,
+ 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4,
+ 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067,
+ 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292,
+ 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7,
+ 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8,
+ 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
+ 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f,
+ 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
+ 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068,
+ 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
+ 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f,
+ 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
+ 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f,
+ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
+ 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f,
+ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
+ 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
+ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
+ 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
+ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
+ 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
+ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
+ 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
+ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
+ 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
+ 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
+ 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
+ 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
+ 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
+ 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
+ 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
+ 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
+ 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
+ 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
+ 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
+ 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
+ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
+ 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f,
+ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
+ 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f,
+ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
+ 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f,
+ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
+ 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f,
+ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
+ 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f,
+ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
+ 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
+ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
+ 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
+ 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377,
+ 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
+ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7,
+ 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9,
+ 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
+ 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f,
+ 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7,
+ 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
+ 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
+ 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
+ 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
+ 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf,
+ 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7,
+ 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df,
+ 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7,
+ 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef,
+ 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7,
+ 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff,
+ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
+ 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f,
+ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
+ 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f,
+ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
+ 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
+ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
+ 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
+ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
+ 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
+ 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
+ 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f,
+ 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
+ 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f,
+ 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
+ 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f,
+ 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
+ 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f,
+ 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7,
+ 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af,
+ 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7,
+ 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf,
+ 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7,
+ 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf,
+ 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435,
+ 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437,
+ 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e,
+ 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443,
+ 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
+ 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
+ };
+
+ // generated with:
+ // cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }'
+
+}
diff --git a/java/src/com/android/inputmethod/latin/Hints.java b/java/src/com/android/inputmethod/latin/Hints.java
new file mode 100644
index 000000000..689c8d852
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Hints.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2009 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.voice.SettingsUtil;
+
+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;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Logic to determine when to display hints on usage to the user.
+ */
+public class Hints {
+ public interface Display {
+ public void showHint(int viewResource);
+ }
+
+ private static final String TAG = "Hints";
+ private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN =
+ "voice_hint_num_unique_days_shown";
+ private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN =
+ "voice_hint_last_time_shown";
+ private static final String PREF_VOICE_INPUT_LAST_TIME_USED =
+ "voice_input_last_time_used";
+ private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT =
+ "voice_punctuation_hint_view_count";
+ 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 boolean mVoiceResultContainedPunctuation;
+ private int mSwipeHintMaxDaysToShow;
+ private int mPunctuationHintMaxDisplays;
+
+ // Only show punctuation hint if voice result did not contain punctuation.
+ static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION
+ = new HashMap<CharSequence, String>();
+ static {
+ SPEAKABLE_PUNCTUATION.put(",", "comma");
+ SPEAKABLE_PUNCTUATION.put(".", "period");
+ SPEAKABLE_PUNCTUATION.put("?", "question mark");
+ }
+
+ public Hints(Context context, Display display) {
+ mContext = context;
+ mDisplay = display;
+
+ ContentResolver cr = mContext.getContentResolver();
+ mSwipeHintMaxDaysToShow = SettingsUtil.getSettingsInt(
+ cr,
+ SettingsUtil.LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS,
+ DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW);
+ mPunctuationHintMaxDisplays = SettingsUtil.getSettingsInt(
+ cr,
+ SettingsUtil.LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS,
+ DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS);
+ }
+
+ public boolean showSwipeHintIfNecessary(boolean fieldRecommended) {
+ if (fieldRecommended && shouldShowSwipeHint()) {
+ showHint(R.layout.voice_swipe_hint);
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean showPunctuationHintIfNecessary(InputConnection ic) {
+ if (!mVoiceResultContainedPunctuation
+ && ic != null
+ && getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT)
+ < mPunctuationHintMaxDisplays) {
+ CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0);
+ if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) {
+ showHint(R.layout.voice_punctuation_hint);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void registerVoiceResult(String text) {
+ // Update the current time as the last time voice input was used.
+ SharedPreferences.Editor editor =
+ PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+ editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis());
+ editor.commit();
+
+ mVoiceResultContainedPunctuation = false;
+ for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) {
+ if (text.indexOf(s.toString()) >= 0) {
+ mVoiceResultContainedPunctuation = true;
+ break;
+ }
+ }
+ }
+
+ private boolean shouldShowSwipeHint() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+
+ int numUniqueDaysShown = sp.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);
+
+ // 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.)
+ if (!isFromToday(lastTimeVoiceWasUsed)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines whether the provided time is from some time today (i.e., this day, month,
+ * and year).
+ */
+ private boolean isFromToday(long timeInMillis) {
+ if (timeInMillis == 0) return false;
+
+ Calendar today = Calendar.getInstance();
+ today.setTimeInMillis(System.currentTimeMillis());
+
+ Calendar timestamp = Calendar.getInstance();
+ timestamp.setTimeInMillis(timeInMillis);
+
+ return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) &&
+ today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) &&
+ today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH));
+ }
+
+ private void showHint(int hintViewResource) {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+
+ int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
+ long lastTimeHintWasShown = sp.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();
+ editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1);
+ editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis());
+ editor.commit();
+ }
+
+ if (mDisplay != null) {
+ mDisplay.showHint(hintViewResource);
+ }
+ }
+
+ private int getAndIncrementPref(String pref) {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int value = sp.getInt(pref, 0);
+ SharedPreferences.Editor editor = sp.edit();
+ editor.putInt(pref, value + 1);
+ editor.commit();
+ return value;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java
new file mode 100644
index 000000000..b1ddb2175
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2008-2009 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 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.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+public class InputLanguageSelection extends PreferenceActivity {
+
+ private String mSelectedLanguages;
+ private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>();
+ private static final String[] BLACKLIST_LANGUAGES = {
+ "ko", "ja", "zh"
+ };
+
+ private static class Loc implements Comparable {
+ static Collator sCollator = Collator.getInstance();
+
+ String label;
+ Locale locale;
+
+ public Loc(String label, Locale locale) {
+ this.label = label;
+ this.locale = locale;
+ }
+
+ @Override
+ public String toString() {
+ return this.label;
+ }
+
+ public int compareTo(Object o) {
+ return sCollator.compare(this.label, ((Loc) o).label);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.language_prefs);
+ // Get the settings preferences
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ mSelectedLanguages = sp.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)));
+ boolean checked = isLocaleIn(locale, languageList);
+ pref.setChecked(checked);
+ parent.addPreference(pref);
+ }
+ }
+
+ private boolean isLocaleIn(Locale locale, String[] list) {
+ String lang = get5Code(locale);
+ for (int i = 0; i < list.length; i++) {
+ if (lang.equalsIgnoreCase(list[i])) return true;
+ }
+ // If it matches the current locale
+ Locale displayLocale = getResources().getConfiguration().locale;
+ if (lang.equalsIgnoreCase(get5Code(displayLocale))) {
+ return true;
+ }
+ return false;
+ }
+
+ private String get5Code(Locale locale) {
+ String country = locale.getCountry();
+ return locale.getLanguage()
+ + (TextUtils.isEmpty(country) ? "" : "_" + country);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ // Save the selected languages
+ String checkedLanguages = "";
+ PreferenceGroup parent = getPreferenceScreen();
+ int count = parent.getPreferenceCount();
+ for (int i = 0; i < count; i++) {
+ CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i);
+ if (pref.isChecked()) {
+ Locale locale = mAvailableLanguages.get(i).locale;
+ checkedLanguages += get5Code(locale) + ",";
+ }
+ }
+ if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ Editor editor = sp.edit();
+ editor.putString(LatinIME.PREF_SELECTED_LANGUAGES, checkedLanguages);
+ editor.commit();
+ }
+
+ ArrayList<Loc> getUniqueLocales() {
+ String[] locales = getAssets().getLocales();
+ Arrays.sort(locales);
+ ArrayList<Loc> uniqueLocales = new ArrayList<Loc>();
+
+ final int origSize = locales.length;
+ Loc[] preprocess = new Loc[origSize];
+ int finalSize = 0;
+ for (int i = 0 ; i < origSize; i++ ) {
+ String s = locales[i];
+ int len = s.length();
+ if (len == 5) {
+ String language = s.substring(0, 2);
+ String country = s.substring(3, 5);
+ Locale l = new Locale(language, country);
+
+ // Exclude languages that are not relevant to LatinIME
+ if (arrayContains(BLACKLIST_LANGUAGES, language)) continue;
+
+ if (finalSize == 0) {
+ preprocess[finalSize++] =
+ new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(l)), l);
+ } else {
+ // check previous entry:
+ // same lang and a country -> upgrade to full name and
+ // insert ours with full name
+ // 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++] =
+ new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName()), l);
+ } else {
+ String displayName;
+ if (s.equals("zz_ZZ")) {
+ } else {
+ displayName = LanguageSwitcher.toTitleCase(l.getDisplayName(l));
+ preprocess[finalSize++] = new Loc(displayName, l);
+ }
+ }
+ }
+ }
+ }
+ for (int i = 0; i < finalSize ; i++) {
+ uniqueLocales.add(preprocess[i]);
+ }
+ return uniqueLocales;
+ }
+
+ private boolean arrayContains(String[] array, String value) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i].equalsIgnoreCase(value)) return true;
+ }
+ return false;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
new file mode 100644
index 000000000..438680cf8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2008 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 java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
+
+public class KeyboardSwitcher {
+
+ 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;
+
+ public static final int MODE_TEXT_QWERTY = 0;
+ public static final int MODE_TEXT_ALPHA = 1;
+ public static final int MODE_TEXT_COUNT = 2;
+
+ 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;
+
+ 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;
+
+ LatinKeyboardView mInputView;
+ private static final int[] ALPHABET_MODES = {
+ KEYBOARDMODE_NORMAL,
+ KEYBOARDMODE_URL,
+ KEYBOARDMODE_EMAIL,
+ KEYBOARDMODE_IM,
+ KEYBOARDMODE_WEB};
+
+ //LatinIME mContext;
+ Context mContext;
+ InputMethodService mInputMethodService;
+
+ private KeyboardId mSymbolsId;
+ private KeyboardId mSymbolsShiftedId;
+
+ private KeyboardId mCurrentId;
+ private Map<KeyboardId, LatinKeyboard> mKeyboards;
+
+ private int mMode; /** One of the MODE_XXX values */
+ private int mImeOptions;
+ private int mTextMode = MODE_TEXT_QWERTY;
+ private boolean mIsSymbols;
+ private boolean mHasVoice;
+ private boolean mVoiceOnPrimary;
+ private boolean mPreferSymbols;
+ private int mSymbolsModeState = SYMBOLS_MODE_STATE_NONE;
+
+ private int mLastDisplayWidth;
+ private LanguageSwitcher mLanguageSwitcher;
+ private Locale mInputLocale;
+ private boolean mEnableMultipleLanguages;
+
+ KeyboardSwitcher(Context context, InputMethodService ims) {
+ mContext = context;
+ mKeyboards = new HashMap<KeyboardId, LatinKeyboard>();
+ mSymbolsId = new KeyboardId(R.xml.kbd_symbols, false);
+ mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, false);
+ mInputMethodService = ims;
+ }
+
+ /**
+ * 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.
+ */
+ void setLanguageSwitcher(LanguageSwitcher languageSwitcher) {
+ mLanguageSwitcher = languageSwitcher;
+ mInputLocale = mLanguageSwitcher.getInputLocale();
+ mEnableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 1;
+ }
+
+ void setInputView(LatinKeyboardView inputView) {
+ mInputView = inputView;
+ }
+
+ void makeKeyboards(boolean forceCreate) {
+ 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();
+ mSymbolsId = new KeyboardId(R.xml.kbd_symbols, mHasVoice && !mVoiceOnPrimary);
+ mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift,
+ mHasVoice && !mVoiceOnPrimary);
+ }
+
+ /**
+ * Represents the parameters necessary to construct a new LatinKeyboard,
+ * which also serve as a unique identifier for each keyboard type.
+ */
+ private static class KeyboardId {
+ public int mXml;
+ public int mKeyboardMode; /** A KEYBOARDMODE_XXX value */
+ public boolean mEnableShiftLock;
+ public boolean mHasVoice;
+
+ public KeyboardId(int xml, int mode, boolean enableShiftLock, boolean hasVoice) {
+ this.mXml = xml;
+ this.mKeyboardMode = mode;
+ this.mEnableShiftLock = enableShiftLock;
+ this.mHasVoice = hasVoice;
+ }
+
+ public KeyboardId(int xml, boolean hasVoice) {
+ this(xml, 0, false, hasVoice);
+ }
+
+ public boolean equals(Object other) {
+ return other instanceof KeyboardId && equals((KeyboardId) other);
+ }
+
+ public boolean equals(KeyboardId other) {
+ return other.mXml == this.mXml
+ && other.mKeyboardMode == this.mKeyboardMode
+ && other.mEnableShiftLock == this.mEnableShiftLock;
+ }
+
+ public int hashCode() {
+ return (mXml + 1) * (mKeyboardMode + 1) * (mEnableShiftLock ? 2 : 1)
+ * (mHasVoice ? 4 : 8);
+ }
+ }
+
+ void setVoiceMode(boolean enableVoice, boolean voiceOnPrimary) {
+ if (enableVoice != mHasVoice || voiceOnPrimary != mVoiceOnPrimary) {
+ mKeyboards.clear();
+ }
+ mHasVoice = enableVoice;
+ mVoiceOnPrimary = voiceOnPrimary;
+ setKeyboardMode(mMode, mImeOptions, mHasVoice,
+ mIsSymbols);
+ }
+
+ boolean hasVoiceButton(boolean isSymbols) {
+ return mHasVoice && (isSymbols != mVoiceOnPrimary);
+ }
+
+ void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) {
+ mSymbolsModeState = SYMBOLS_MODE_STATE_NONE;
+ mPreferSymbols = mode == MODE_SYMBOLS;
+ setKeyboardMode(mode == MODE_SYMBOLS ? MODE_TEXT : mode, imeOptions, enableVoice,
+ mPreferSymbols);
+ }
+
+ void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) {
+ if (mInputView == null) return;
+ mMode = mode;
+ mImeOptions = imeOptions;
+ if (enableVoice != mHasVoice) {
+ setVoiceMode(mHasVoice, mVoiceOnPrimary);
+ }
+ mIsSymbols = isSymbols;
+
+ mInputView.setPreviewEnabled(true);
+ KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols);
+
+ LatinKeyboard keyboard = getKeyboard(id);
+
+ if (mode == MODE_PHONE) {
+ mInputView.setPhoneKeyboard(keyboard);
+ mInputView.setPreviewEnabled(false);
+ }
+
+ mCurrentId = id;
+ mInputView.setKeyboard(keyboard);
+ keyboard.setShifted(false);
+ keyboard.setShiftLocked(keyboard.isShiftLocked());
+ keyboard.setImeOptions(mContext.getResources(), mMode, imeOptions);
+ }
+
+ private LatinKeyboard getKeyboard(KeyboardId id) {
+ if (!mKeyboards.containsKey(id)) {
+ Resources orig = mContext.getResources();
+ Configuration conf = orig.getConfiguration();
+ Locale saveLocale = conf.locale;
+ conf.locale = mInputLocale;
+ orig.updateConfiguration(conf, null);
+ LatinKeyboard keyboard = new LatinKeyboard(
+ mContext, id.mXml, id.mKeyboardMode);
+ keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols), mHasVoice);
+ keyboard.setLanguageSwitcher(mLanguageSwitcher);
+ if (id.mKeyboardMode == KEYBOARDMODE_NORMAL
+ || id.mKeyboardMode == KEYBOARDMODE_URL
+ || id.mKeyboardMode == KEYBOARDMODE_IM
+ || id.mKeyboardMode == KEYBOARDMODE_EMAIL
+ || id.mKeyboardMode == KEYBOARDMODE_WEB
+ ) {
+ keyboard.setExtension(R.xml.kbd_extension);
+ }
+
+ if (id.mEnableShiftLock) {
+ keyboard.enableShiftLock();
+ }
+ mKeyboards.put(id, keyboard);
+
+ conf.locale = saveLocale;
+ orig.updateConfiguration(conf, null);
+ }
+ return mKeyboards.get(id);
+ }
+
+ private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) {
+ boolean hasVoice = hasVoiceButton(isSymbols);
+ if (isSymbols) {
+ return (mode == MODE_PHONE)
+ ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice)
+ : new KeyboardId(R.xml.kbd_symbols, hasVoice);
+ }
+ switch (mode) {
+ case MODE_TEXT:
+ if (mTextMode == MODE_TEXT_QWERTY) {
+ return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_NORMAL, true, hasVoice);
+ } else if (mTextMode == MODE_TEXT_ALPHA) {
+ return new KeyboardId(R.xml.kbd_alpha, KEYBOARDMODE_NORMAL, true, hasVoice);
+ }
+ break;
+ case MODE_SYMBOLS:
+ return new KeyboardId(R.xml.kbd_symbols, hasVoice);
+ case MODE_PHONE:
+ return new KeyboardId(R.xml.kbd_phone, hasVoice);
+ case MODE_URL:
+ return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true, hasVoice);
+ case MODE_EMAIL:
+ return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL, true, hasVoice);
+ case MODE_IM:
+ return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_IM, true, hasVoice);
+ case MODE_WEB:
+ return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_WEB, true, hasVoice);
+ }
+ return null;
+ }
+
+ int getKeyboardMode() {
+ return mMode;
+ }
+
+ boolean isTextMode() {
+ return mMode == MODE_TEXT;
+ }
+
+ int getTextMode() {
+ return mTextMode;
+ }
+
+ void setTextMode(int position) {
+ if (position < MODE_TEXT_COUNT && position >= 0) {
+ mTextMode = position;
+ }
+ if (isTextMode()) {
+ setKeyboardMode(MODE_TEXT, mImeOptions, mHasVoice);
+ }
+ }
+
+ int getTextModeCount() {
+ return MODE_TEXT_COUNT;
+ }
+
+ boolean isAlphabetMode() {
+ int currentMode = mCurrentId.mKeyboardMode;
+ for (Integer mode : ALPHABET_MODES) {
+ if (currentMode == mode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void toggleShift() {
+ if (mCurrentId.equals(mSymbolsId)) {
+ LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId);
+ LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId);
+ symbolsKeyboard.setShifted(true);
+ mCurrentId = mSymbolsShiftedId;
+ mInputView.setKeyboard(symbolsShiftedKeyboard);
+ symbolsShiftedKeyboard.setShifted(true);
+ symbolsShiftedKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions);
+ } else if (mCurrentId.equals(mSymbolsShiftedId)) {
+ LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId);
+ LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId);
+ symbolsShiftedKeyboard.setShifted(false);
+ mCurrentId = mSymbolsId;
+ mInputView.setKeyboard(getKeyboard(mSymbolsId));
+ symbolsKeyboard.setShifted(false);
+ symbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions);
+ }
+ }
+
+ void toggleSymbols() {
+ setKeyboardMode(mMode, mImeOptions, mHasVoice, !mIsSymbols);
+ if (mIsSymbols && !mPreferSymbols) {
+ mSymbolsModeState = SYMBOLS_MODE_STATE_BEGIN;
+ } else {
+ mSymbolsModeState = SYMBOLS_MODE_STATE_NONE;
+ }
+ }
+
+ /**
+ * Updates state machine to figure out when to automatically switch back to alpha mode.
+ * Returns true if the keyboard needs to switch back
+ */
+ boolean 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;
+ }
+ return false;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java
new file mode 100644
index 000000000..12045125f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java
@@ -0,0 +1,182 @@
+/*
+ * 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 java.util.Locale;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+/**
+ * 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 String[] mSelectedLanguageArray;
+ private String mSelectedLanguages;
+ private int mCurrentIndex = 0;
+ private String mDefaultInputLanguage;
+ private Locale mDefaultInputLocale;
+
+ public LanguageSwitcher(LatinIME ime) {
+ mIme = ime;
+ mLocales = new Locale[0];
+ }
+
+ public Locale[] getLocales() {
+ return mLocales;
+ }
+
+ public int getLocaleCount() {
+ return mLocales.length;
+ }
+
+ /**
+ * Loads the currently selected input languages from shared preferences.
+ * @param sp
+ * @return whether there was any change
+ */
+ public boolean loadLocales(SharedPreferences sp) {
+ String selectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, null);
+ String currentLanguage = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null);
+ if (selectedLanguages == null || selectedLanguages.length() < 1) {
+ loadDefaults();
+ if (mLocales.length == 0) {
+ return false;
+ }
+ mLocales = new Locale[0];
+ return true;
+ }
+ if (selectedLanguages.equals(mSelectedLanguages)) {
+ return false;
+ }
+ mSelectedLanguageArray = selectedLanguages.split(",");
+ mSelectedLanguages = selectedLanguages; // Cache it for comparison later
+ constructLocales();
+ mCurrentIndex = 0;
+ if (currentLanguage != null) {
+ // Find the index
+ mCurrentIndex = 0;
+ for (int i = 0; i < mLocales.length; i++) {
+ if (mSelectedLanguageArray[i].equals(currentLanguage)) {
+ mCurrentIndex = i;
+ break;
+ }
+ }
+ // If we didn't find the index, use the first one
+ }
+ return true;
+ }
+
+ private void loadDefaults() {
+ mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
+ String country = mDefaultInputLocale.getCountry();
+ mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
+ (TextUtils.isEmpty(country) ? "" : "_" + country);
+ }
+
+ private void constructLocales() {
+ mLocales = new Locale[mSelectedLanguageArray.length];
+ for (int i = 0; i < mLocales.length; i++) {
+ mLocales[i] = new Locale(mSelectedLanguageArray[i]);
+ }
+ }
+
+ /**
+ * Returns the currently selected input language code, or the display language code if
+ * no specific locale was selected for input.
+ */
+ public String getInputLanguage() {
+ if (getLocaleCount() == 0) return mDefaultInputLanguage;
+
+ return mSelectedLanguageArray[mCurrentIndex];
+ }
+
+ /**
+ * Returns the list of enabled language codes.
+ */
+ public String[] getEnabledLanguages() {
+ return mSelectedLanguageArray;
+ }
+
+ /**
+ * Returns the currently selected input locale, or the display locale if no specific
+ * locale was selected for input.
+ * @return
+ */
+ public Locale getInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+ return mLocales[mCurrentIndex];
+ }
+
+ /**
+ * Returns the next input locale in the list. Wraps around to the beginning of the
+ * list if we're at the end of the list.
+ * @return
+ */
+ public Locale getNextInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+ return mLocales[(mCurrentIndex + 1) % mLocales.length];
+ }
+
+ /**
+ * Returns the previous input locale in the list. Wraps around to the end of the
+ * list if we're at the beginning of the list.
+ * @return
+ */
+ public Locale getPrevInputLocale() {
+ if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+ return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length];
+ }
+
+ public void reset() {
+ mCurrentIndex = 0;
+ }
+
+ public void next() {
+ mCurrentIndex++;
+ if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around
+ }
+
+ public void prev() {
+ mCurrentIndex--;
+ if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around
+ }
+
+ public void persist() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme);
+ Editor editor = sp.edit();
+ editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage());
+ editor.commit();
+ }
+
+ 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
new file mode 100644
index 000000000..43972d98b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -0,0 +1,1949 @@
+/*
+ * Copyright (C) 2008-2009 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.voice.EditingUtil;
+import com.android.inputmethod.voice.FieldContext;
+import com.android.inputmethod.voice.SettingsUtil;
+import com.android.inputmethod.voice.VoiceInput;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.KeyboardView;
+import android.media.AudioManager;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.speech.RecognitionManager;
+import android.text.AutoText;
+import android.text.ClipboardManager;
+import android.text.TextUtils;
+import android.util.Log;
+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.ViewParent;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+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 KeyboardView.OnKeyboardActionListener,
+ VoiceInput.UiListener,
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = "LatinIME";
+ 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 String PREF_VIBRATE_ON = "vibrate_on";
+ private static final String PREF_SOUND_ON = "sound_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_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";
+
+ public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
+ public static final String PREF_INPUT_LANGUAGE = "input_language";
+
+ 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_START_LISTENING_AFTER_SWIPE = 4;
+
+ // If we detect a swipe gesture within N ms of typing, then swipe is
+ // ignored, since it may in fact be two key presses in quick succession.
+ private static final long MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE = 1000;
+
+ // 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 = '.';
+
+ // Contextual menu positions
+ private static final int POS_SETTINGS = 0;
+ private static final int POS_METHOD = 1;
+
+ private LatinKeyboardView mInputView;
+ private CandidateViewContainer mCandidateViewContainer;
+ private CandidateView mCandidateView;
+ private Suggest mSuggest;
+ private CompletionInfo[] mCompletions;
+
+ private AlertDialog mOptionsDialog;
+ private AlertDialog mVoiceWarningDialog;
+
+ KeyboardSwitcher mKeyboardSwitcher;
+
+ private UserDictionary mUserDictionary;
+ private ContactsDictionary mContactsDictionary;
+ private ExpandableDictionary mAutoDictionary;
+
+ private Hints mHints;
+
+ Resources mResources;
+
+ private String mLocale;
+ private LanguageSwitcher mLanguageSwitcher;
+
+ private 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 mImmediatelyAfterVoiceSuggestions;
+ private boolean mVoiceInputHighlighted;
+ private boolean mEnableVoiceButton;
+ private CharSequence mBestWord;
+ private boolean mPredictionOn;
+ private boolean mCompletionOn;
+ private boolean mHasDictionary;
+ private boolean mAutoSpace;
+ private boolean mJustAddedAutoSpace;
+ private boolean mAutoCorrectEnabled;
+ private boolean mAutoCorrectOn;
+ private boolean mCapsLock;
+ private boolean mPasswordText;
+ private boolean mEmailText;
+ private boolean mVibrateOn;
+ private boolean mSoundOn;
+ private boolean mAutoCap;
+ private boolean mQuickFixes;
+ private boolean mHasUsedVoiceInput;
+ private boolean mHasUsedVoiceInputUnsupportedLocale;
+ private boolean mLocaleSupportedForVoiceInput;
+ private boolean mShowSuggestions;
+ private boolean mSuggestionShouldReplaceCurrentWord;
+ private boolean mIsShowingHint;
+ private int mCorrectionMode;
+ private boolean mEnableVoice = true;
+ private boolean mVoiceOnPrimary;
+ private int mOrientation;
+ private List<CharSequence> mSuggestPuncList;
+
+ // Indicates whether the suggestion strip is to be on in landscape
+ private boolean mJustAccepted;
+ private CharSequence mJustRevertedSeparator;
+ private int mDeleteCount;
+ private long mLastKeyTime;
+
+ private Tutorial mTutorial;
+
+ private AudioManager mAudioManager;
+ // Align sound effect volume on music volume
+ private final float FX_VOLUME = -1.0f;
+ private boolean mSilentMode;
+
+ private String mWordSeparators;
+ private String mSentenceSeparators;
+ private VoiceInput mVoiceInput;
+ private VoiceResults mVoiceResults = new VoiceResults();
+ private long mSwipeTriggerTimeMillis;
+ private boolean mConfigurationChanging;
+
+ // For each word, a list of potential replacements, usually from voice.
+ private Map<String, List<CharSequence>> mWordToSuggestions =
+ new HashMap<String, List<CharSequence>>();
+
+ private class VoiceResults {
+ List<String> candidates;
+ Map<String, List<CharSequence>> alternatives;
+ }
+ private boolean mRefreshKeyboardRequired;
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_SUGGESTIONS:
+ updateSuggestions();
+ break;
+ case MSG_START_TUTORIAL:
+ if (mTutorial == null) {
+ if (mInputView.isShown()) {
+ mTutorial = new Tutorial(LatinIME.this, mInputView);
+ 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;
+ case MSG_START_LISTENING_AFTER_SWIPE:
+ if (mLastKeyTime < mSwipeTriggerTimeMillis) {
+ startListening(true);
+ }
+ }
+ }
+ };
+
+ @Override public void onCreate() {
+ super.onCreate();
+ //setStatusIcon(R.drawable.ime_qwerty);
+ mResources = getResources();
+ final Configuration conf = mResources.getConfiguration();
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mLanguageSwitcher = new LanguageSwitcher(this);
+ mLanguageSwitcher.loadLocales(prefs);
+ mKeyboardSwitcher = new KeyboardSwitcher(this, this);
+ mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
+ boolean enableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 0;
+ String inputLanguage = mLanguageSwitcher.getInputLanguage();
+ if (inputLanguage == null) {
+ inputLanguage = conf.locale.toString();
+ }
+ initSuggest(inputLanguage);
+ mOrientation = conf.orientation;
+ initSuggestPuncList();
+
+ // 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;
+ }
+ });
+ }
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ private void initSuggest(String locale) {
+ mLocale = locale;
+
+ Resources orig = getResources();
+ Configuration conf = orig.getConfiguration();
+ Locale saveLocale = conf.locale;
+ conf.locale = new Locale(locale);
+ orig.updateConfiguration(conf, orig.getDisplayMetrics());
+ if (mSuggest != null) {
+ mSuggest.close();
+ }
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
+ mSuggest = new Suggest(this, R.raw.main);
+ updateAutoTextEnabled(saveLocale);
+ if (mUserDictionary != null) mUserDictionary.close();
+ mUserDictionary = new UserDictionary(this, mLocale);
+ if (mContactsDictionary == null) {
+ mContactsDictionary = new ContactsDictionary(this);
+ }
+ if (mAutoDictionary != null) {
+ mAutoDictionary.close();
+ }
+ mAutoDictionary = new AutoDictionary(this, this, mLocale);
+ mSuggest.setUserDictionary(mUserDictionary);
+ mSuggest.setContactsDictionary(mContactsDictionary);
+ mSuggest.setAutoDictionary(mAutoDictionary);
+ updateCorrectionMode();
+ mWordSeparators = mResources.getString(R.string.word_separators);
+ mSentenceSeparators = mResources.getString(R.string.sentence_separators);
+
+ conf.locale = saveLocale;
+ orig.updateConfiguration(conf, orig.getDisplayMetrics());
+ }
+
+ @Override
+ public void onDestroy() {
+ mUserDictionary.close();
+ mContactsDictionary.close();
+ unregisterReceiver(mReceiver);
+ if (VOICE_INSTALLED) {
+ mVoiceInput.destroy();
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration conf) {
+ // If the system locale changes and is different from the saved
+ // locale (mLocale), then reload the input locale list from the
+ // latin ime settings (shared prefs) and reset the input locale
+ // to the first one.
+ if (!TextUtils.equals(conf.locale.toString(), mLocale)) {
+ if (mLanguageSwitcher != null) {
+ mLanguageSwitcher.loadLocales(
+ PreferenceManager.getDefaultSharedPreferences(this));
+ toggleLanguage(true, true);
+ } else {
+ reloadKeyboards();
+ }
+ }
+ // 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();
+ }
+ mConfigurationChanging = true;
+ super.onConfigurationChanged(conf);
+ if (mRecognizing) {
+ switchToRecognitionStatusView();
+ }
+ mConfigurationChanging = false;
+ }
+
+ @Override
+ public View onCreateInputView() {
+ mInputView = (LatinKeyboardView) getLayoutInflater().inflate(
+ R.layout.input, null);
+ mKeyboardSwitcher.setInputView(mInputView);
+ mKeyboardSwitcher.makeKeyboards(true);
+ mInputView.setOnKeyboardActionListener(this);
+ mKeyboardSwitcher.setKeyboardMode(
+ KeyboardSwitcher.MODE_TEXT, 0,
+ shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo()));
+ return mInputView;
+ }
+
+ @Override
+ public View onCreateCandidatesView() {
+ mKeyboardSwitcher.makeKeyboards(true);
+ mCandidateViewContainer = (CandidateViewContainer) getLayoutInflater().inflate(
+ R.layout.candidates, null);
+ mCandidateViewContainer.initViews();
+ mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates);
+ mCandidateView.setService(this);
+ setCandidatesViewShown(true);
+ return mCandidateViewContainer;
+ }
+
+ @Override
+ public void onStartInputView(EditorInfo attribute, boolean restarting) {
+ // In landscape mode, this method gets called without the input view being created.
+ if (mInputView == null) {
+ return;
+ }
+
+ if (mRefreshKeyboardRequired) {
+ mRefreshKeyboardRequired = false;
+ toggleLanguage(true, true);
+ }
+
+ 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;
+ mImmediatelyAfterVoiceSuggestions = false;
+ mVoiceInputHighlighted = false;
+ mWordToSuggestions.clear();
+ mInputTypeNoAutoCorrect = false;
+ mPredictionOn = false;
+ mCompletionOn = false;
+ mCompletions = null;
+ mCapsLock = false;
+ mEmailText = false;
+ switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) {
+ case EditorInfo.TYPE_CLASS_NUMBER:
+ case EditorInfo.TYPE_CLASS_DATETIME:
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_SYMBOLS,
+ attribute.imeOptions, enableVoiceButton);
+ break;
+ case EditorInfo.TYPE_CLASS_PHONE:
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE,
+ attribute.imeOptions, enableVoiceButton);
+ 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 ) {
+ mPredictionOn = false;
+ }
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
+ mEmailText = true;
+ }
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) {
+ mAutoSpace = false;
+ } else {
+ mAutoSpace = true;
+ }
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) {
+ mPredictionOn = false;
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL,
+ attribute.imeOptions, enableVoiceButton);
+ } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) {
+ mPredictionOn = false;
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL,
+ attribute.imeOptions, enableVoiceButton);
+ } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM,
+ attribute.imeOptions, enableVoiceButton);
+ } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) {
+ mPredictionOn = false;
+ } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB,
+ attribute.imeOptions, enableVoiceButton);
+ // 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;
+ }
+ }
+
+ // If NO_SUGGESTIONS is set, don't do prediction.
+ if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
+ mPredictionOn = false;
+ mInputTypeNoAutoCorrect = true;
+ }
+ // If it's not multiline and the autoCorrect flag is not set, then don't correct
+ if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 &&
+ (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
+ mInputTypeNoAutoCorrect = true;
+ }
+ if ((attribute.inputType&EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
+ mPredictionOn = false;
+ mCompletionOn = true && isFullscreenMode();
+ }
+ updateShiftKeyState(attribute);
+ break;
+ default:
+ mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT,
+ attribute.imeOptions, enableVoiceButton);
+ updateShiftKeyState(attribute);
+ }
+ mInputView.closing();
+ mComposing.setLength(0);
+ mPredicting = false;
+ mDeleteCount = 0;
+ mJustAddedAutoSpace = false;
+ loadSettings();
+ updateShiftKeyState(attribute);
+
+ setCandidatesViewShown(false);
+ setSuggestions(null, false, false, false);
+
+ // If the dictionary is not big enough, don't auto correct
+ mHasDictionary = mSuggest.hasMainDictionary();
+
+ updateCorrectionMode();
+
+ mInputView.setProximityCorrectionEnabled(true);
+ mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions);
+ checkTutorial(attribute.privateImeOptions);
+ if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
+ }
+
+ @Override
+ public void onFinishInput() {
+ super.onFinishInput();
+
+ if (VOICE_INSTALLED && !mConfigurationChanging) {
+ if (mAfterVoiceInput) {
+ mVoiceInput.logInputEnded();
+ }
+ mVoiceInput.flushLogs();
+ mVoiceInput.cancel();
+ }
+ if (mInputView != null) {
+ mInputView.closing();
+ }
+ }
+
+ @Override
+ public void onUpdateExtractedText(int token, ExtractedText text) {
+ super.onUpdateExtractedText(token, text);
+ InputConnection ic = getCurrentInputConnection();
+ if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
+ mVoiceInput.logTextModified();
+
+ if (mHints.showPunctuationHintIfNecessary(ic)) {
+ mVoiceInput.logPunctuationHintDisplayed();
+ }
+ }
+ mImmediatelyAfterVoiceInput = false;
+ }
+
+ @Override
+ public void onUpdateSelection(int oldSelStart, int oldSelEnd,
+ int newSelStart, int newSelEnd,
+ int candidatesStart, int candidatesEnd) {
+ super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
+ candidatesStart, candidatesEnd);
+
+ if (DEBUG) {
+ Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ + ", ose=" + oldSelEnd
+ + ", nss=" + newSelStart
+ + ", nse=" + newSelEnd
+ + ", cs=" + candidatesStart
+ + ", ce=" + candidatesEnd);
+ }
+
+ mSuggestionShouldReplaceCurrentWord = false;
+ // 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))) {
+ mComposing.setLength(0);
+ mPredicting = false;
+ updateSuggestions();
+ TextEntryState.reset();
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.finishComposingText();
+ }
+ mVoiceInputHighlighted = false;
+ } else if (!mPredicting && !mJustAccepted) {
+ switch (TextEntryState.getState()) {
+ case TextEntryState.STATE_ACCEPTED_DEFAULT:
+ TextEntryState.reset();
+ // fall through
+ case TextEntryState.STATE_SPACE_AFTER_PICKED:
+ mJustAddedAutoSpace = false; // The user moved the cursor.
+ break;
+ }
+ }
+ mJustAccepted = false;
+ postUpdateShiftKeyState();
+
+ if (VOICE_INSTALLED) {
+ if (mShowingVoiceSuggestions) {
+ if (mImmediatelyAfterVoiceSuggestions) {
+ mImmediatelyAfterVoiceSuggestions = false;
+ } else {
+ updateSuggestions();
+ mShowingVoiceSuggestions = false;
+ }
+ }
+ if (VoiceInput.ENABLE_WORD_CORRECTIONS) {
+ // If we have alternatives for the current word, then show them.
+ String word = EditingUtil.getWordAtCursor(
+ getCurrentInputConnection(), getWordSeparators());
+ if (word != null && mWordToSuggestions.containsKey(word.trim())) {
+ mSuggestionShouldReplaceCurrentWord = true;
+ final List<CharSequence> suggestions = mWordToSuggestions.get(word.trim());
+
+ setSuggestions(suggestions, false, true, true);
+ setCandidatesViewShown(true);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void hideWindow() {
+ if (TRACE) Debug.stopMethodTracing();
+ if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
+ 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();
+ }
+ }
+ super.hideWindow();
+ TextEntryState.endSession();
+ }
+
+ @Override
+ public void onDisplayCompletions(CompletionInfo[] completions) {
+ if (false) {
+ Log.i("foo", "Received completions:");
+ for (int i=0; i<(completions != null ? completions.length : 0); i++) {
+ Log.i("foo", " #" + i + ": " + completions[i]);
+ }
+ }
+ if (mCompletionOn) {
+ mCompletions = completions;
+ if (completions == null) {
+ setSuggestions(null, false, false, false);
+ return;
+ }
+
+ List<CharSequence> stringList = new ArrayList<CharSequence>();
+ for (int i=0; i<(completions != null ? completions.length : 0); i++) {
+ CompletionInfo ci = completions[i];
+ if (ci != null) stringList.add(ci.getText());
+ }
+ //CharSequence typedWord = mWord.getTypedWord();
+ setSuggestions(stringList, true, true, true);
+ mBestWord = null;
+ setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn);
+ }
+ }
+
+ @Override
+ public void setCandidatesViewShown(boolean shown) {
+ // TODO: Remove this if we support candidates with hard keyboard
+ if (onEvaluateInputViewShown()) {
+ super.setCandidatesViewShown(shown);
+ }
+ }
+
+ @Override
+ public void onComputeInsets(InputMethodService.Insets outInsets) {
+ super.onComputeInsets(outInsets);
+ if (!isFullscreenMode()) {
+ outInsets.contentTopInsets = outInsets.visibleTopInsets;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (event.getRepeatCount() == 0 && mInputView != null) {
+ if (mInputView.handleBack()) {
+ return true;
+ } else if (mTutorial != null) {
+ mTutorial.close();
+ mTutorial = null;
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // If tutorial is visible, don't allow dpad to work
+ if (mTutorial != null) {
+ return true;
+ }
+ break;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ // If tutorial is visible, don't allow dpad to work
+ if (mTutorial != null) {
+ return true;
+ }
+ // Enable shift key and DPAD to do selections
+ if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) {
+ event = new KeyEvent(event.getDownTime(), event.getEventTime(),
+ event.getAction(), event.getKeyCode(), event.getRepeatCount(),
+ event.getDeviceId(), event.getScanCode(),
+ KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) ic.sendKeyEvent(event);
+ return true;
+ }
+ break;
+ }
+ 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() {
+ if (mKeyboardSwitcher == null) {
+ mKeyboardSwitcher = new KeyboardSwitcher(this, this);
+ }
+ mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
+ if (mInputView != null) {
+ mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary);
+ }
+ mKeyboardSwitcher.makeKeyboards(true);
+ }
+
+ private void commitTyped(InputConnection inputConnection) {
+ if (mPredicting) {
+ mPredicting = false;
+ if (mComposing.length() > 0) {
+ if (inputConnection != null) {
+ inputConnection.commitText(mComposing, 1);
+ }
+ mCommittedLength = mComposing.length();
+ TextEntryState.acceptedTyped(mComposing);
+ checkAddToDictionary(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
+ }
+ updateSuggestions();
+ }
+ }
+
+ private void postUpdateShiftKeyState() {
+ mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300);
+ }
+
+ public void updateShiftKeyState(EditorInfo attr) {
+ InputConnection ic = getCurrentInputConnection();
+ if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode()
+ && ic != null) {
+ mInputView.setShifted(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);
+ }
+ return caps;
+ }
+
+ private void swapPunctuationAndSpace() {
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+ CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+ if (lastTwo != null && lastTwo.length() == 2
+ && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) {
+ ic.beginBatchEdit();
+ ic.deleteSurroundingText(2, 0);
+ ic.commitText(lastTwo.charAt(1) + " ", 1);
+ ic.endBatchEdit();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ mJustAddedAutoSpace = true;
+ }
+ }
+
+ private void reswapPeriodAndSpace() {
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+ CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+ if (lastThree != null && lastThree.length() == 3
+ && lastThree.charAt(0) == KEYCODE_PERIOD
+ && lastThree.charAt(1) == KEYCODE_SPACE
+ && lastThree.charAt(2) == KEYCODE_PERIOD) {
+ ic.beginBatchEdit();
+ ic.deleteSurroundingText(3, 0);
+ ic.commitText(" ..", 1);
+ ic.endBatchEdit();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ }
+ }
+
+ private void doubleSpace() {
+ //if (!mAutoPunctuate) return;
+ if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+ CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+ if (lastThree != null && lastThree.length() == 3
+ && Character.isLetterOrDigit(lastThree.charAt(0))
+ && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) {
+ ic.beginBatchEdit();
+ ic.deleteSurroundingText(2, 0);
+ ic.commitText(". ", 1);
+ ic.endBatchEdit();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ mJustAddedAutoSpace = true;
+ }
+ }
+
+ private void maybeRemovePreviousPeriod(CharSequence text) {
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+
+ // When the text's first character is '.', remove the previous period
+ // if there is one.
+ CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == KEYCODE_PERIOD
+ && text.charAt(0) == KEYCODE_PERIOD) {
+ ic.deleteSurroundingText(1, 0);
+ }
+ }
+
+ private void removeTrailingSpace() {
+ final InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+
+ CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == KEYCODE_SPACE) {
+ ic.deleteSurroundingText(1, 0);
+ }
+ }
+
+ public boolean addWordToDictionary(String word) {
+ mUserDictionary.addWord(word, 128);
+ return true;
+ }
+
+ private boolean isAlphabet(int code) {
+ if (Character.isLetter(code)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Implementation of KeyboardViewListener
+
+ public void onKey(int primaryCode, int[] keyCodes) {
+ long when = SystemClock.uptimeMillis();
+ if (primaryCode != Keyboard.KEYCODE_DELETE ||
+ when > mLastKeyTime + QUICK_PRESS) {
+ mDeleteCount = 0;
+ }
+ mLastKeyTime = when;
+ switch (primaryCode) {
+ case Keyboard.KEYCODE_DELETE:
+ handleBackspace();
+ mDeleteCount++;
+ break;
+ case Keyboard.KEYCODE_SHIFT:
+ handleShift();
+ break;
+ case Keyboard.KEYCODE_CANCEL:
+ if (mOptionsDialog == null || !mOptionsDialog.isShowing()) {
+ handleClose();
+ }
+ break;
+ case LatinKeyboardView.KEYCODE_OPTIONS:
+ showOptionsMenu();
+ break;
+ case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
+ toggleLanguage(false, true);
+ break;
+ case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
+ toggleLanguage(false, false);
+ break;
+ case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS:
+ if (mCapsLock) {
+ handleShift();
+ } else {
+ toggleCapsLock();
+ }
+ break;
+ case Keyboard.KEYCODE_MODE_CHANGE:
+ changeKeyboardMode();
+ 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;
+ }
+ if (isWordSeparator(primaryCode)) {
+ handleSeparator(primaryCode);
+ } else {
+ handleCharacter(primaryCode, keyCodes);
+ }
+ // Cancel the just reverted state
+ mJustRevertedSeparator = null;
+ }
+ if (mKeyboardSwitcher.onKey(primaryCode)) {
+ changeKeyboardMode();
+ }
+ }
+
+ public void onText(CharSequence text) {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ commitVoiceInput();
+ }
+ InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+ ic.beginBatchEdit();
+ if (mPredicting) {
+ commitTyped(ic);
+ }
+ maybeRemovePreviousPeriod(text);
+ ic.commitText(text, 1);
+ ic.endBatchEdit();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ mJustRevertedSeparator = null;
+ mJustAddedAutoSpace = false;
+ }
+
+ private void handleBackspace() {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ revertVoiceInput();
+ return;
+ }
+ boolean deleteChar = false;
+ InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+ if (mPredicting) {
+ final int length = mComposing.length();
+ if (length > 0) {
+ mComposing.delete(length - 1, length);
+ mWord.deleteLast();
+ ic.setComposingText(mComposing, 1);
+ if (mComposing.length() == 0) {
+ mPredicting = false;
+ }
+ postUpdateSuggestions();
+ } else {
+ ic.deleteSurroundingText(1, 0);
+ }
+ } else {
+ deleteChar = true;
+ }
+ postUpdateShiftKeyState();
+ TextEntryState.backspace();
+ if (TextEntryState.getState() == TextEntryState.STATE_UNDO_COMMIT) {
+ revertLastWord(deleteChar);
+ return;
+ } else if (deleteChar) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ if (mDeleteCount > DELETE_ACCELERATE_AT) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ }
+ }
+ mJustRevertedSeparator = null;
+ }
+
+ private void handleShift() {
+ mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
+ if (mKeyboardSwitcher.isAlphabetMode()) {
+ // Alphabet keyboard
+ checkToggleCapsLock();
+ mInputView.setShifted(mCapsLock || !mInputView.isShifted());
+ } else {
+ mKeyboardSwitcher.toggleShift();
+ }
+ }
+
+ private void handleCharacter(int primaryCode, int[] keyCodes) {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ commitVoiceInput();
+ }
+ if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) {
+ if (!mPredicting) {
+ mPredicting = true;
+ mComposing.setLength(0);
+ mWord.reset();
+ }
+ }
+ if (mInputView.isShifted()) {
+ // TODO: This doesn't work with ß, need to fix it in the next release.
+ if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
+ || keyCodes[0] > Character.MAX_CODE_POINT) {
+ return;
+ }
+ primaryCode = new String(keyCodes, 0, 1).toUpperCase().charAt(0);
+ }
+ if (mPredicting) {
+ if (mInputView.isShifted() && mComposing.length() == 0) {
+ mWord.setCapitalized(true);
+ }
+ mComposing.append((char) primaryCode);
+ mWord.add(primaryCode, keyCodes);
+ InputConnection ic = getCurrentInputConnection();
+ 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);
+ }
+ ic.setComposingText(mComposing, 1);
+ }
+ postUpdateSuggestions();
+ } else {
+ sendKeyChar((char)primaryCode);
+ }
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ measureCps();
+ TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode));
+ }
+
+ private void handleSeparator(int primaryCode) {
+ if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+ commitVoiceInput();
+ }
+ boolean pickedDefault = false;
+ // Handle separator
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.beginBatchEdit();
+ }
+ if (mPredicting) {
+ // In certain languages where single quote is a separator, it's better
+ // 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)) {
+ pickDefaultSuggestion();
+ pickedDefault = true;
+ // Picked the suggestion by the space key. We consider this
+ // as "added an auto space".
+ if (primaryCode == KEYCODE_SPACE) {
+ mJustAddedAutoSpace = true;
+ }
+ } else {
+ commitTyped(ic);
+ }
+ }
+ if (mJustAddedAutoSpace && primaryCode == KEYCODE_ENTER) {
+ removeTrailingSpace();
+ mJustAddedAutoSpace = false;
+ }
+ sendKeyChar((char)primaryCode);
+
+ // Handle the case of ". ." -> " .." with auto-space if necessary
+ // before changing the TextEntryState.
+ if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED
+ && primaryCode == KEYCODE_PERIOD) {
+ reswapPeriodAndSpace();
+ }
+
+ TextEntryState.typedCharacter((char) primaryCode, true);
+ if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED
+ && primaryCode != KEYCODE_ENTER) {
+ swapPunctuationAndSpace();
+ } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) {
+ //else if (TextEntryState.STATE_SPACE_AFTER_ACCEPTED) {
+ doubleSpace();
+ }
+ if (pickedDefault && mBestWord != null) {
+ TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord);
+ }
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ if (ic != null) {
+ ic.endBatchEdit();
+ }
+ }
+
+ private void handleClose() {
+ commitTyped(getCurrentInputConnection());
+ if (VOICE_INSTALLED & mRecognizing) {
+ mVoiceInput.cancel();
+ }
+ requestHideSelf(0);
+ mInputView.closing();
+ TextEntryState.endSession();
+ }
+
+ private void checkToggleCapsLock() {
+ if (mInputView.getKeyboard().isShifted()) {
+ toggleCapsLock();
+ }
+ }
+
+ private void toggleCapsLock() {
+ mCapsLock = !mCapsLock;
+ if (mKeyboardSwitcher.isAlphabetMode()) {
+ ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock);
+ }
+ }
+
+ private void postUpdateSuggestions() {
+ mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100);
+ }
+
+ private boolean isPredictionOn() {
+ boolean predictionOn = mPredictionOn;
+ return predictionOn;
+ }
+
+ private boolean isCandidateStripVisible() {
+ return isPredictionOn() && mShowSuggestions;
+ }
+
+ public void onCancelVoice() {
+ if (mRecognizing) {
+ switchToKeyboardView();
+ }
+ }
+
+ private void switchToKeyboardView() {
+ mHandler.post(new Runnable() {
+ public void run() {
+ mRecognizing = false;
+ if (mInputView != null) {
+ setInputView(mInputView);
+ }
+ updateInputViewShown();
+ }});
+ }
+
+ private void switchToRecognitionStatusView() {
+ final boolean configChanged = mConfigurationChanging;
+ mHandler.post(new Runnable() {
+ public void run() {
+ 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);
+ editor.commit();
+ 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);
+ editor.commit();
+ mHasUsedVoiceInputUnsupportedLocale = true;
+ }
+
+ // Clear N-best suggestions
+ setSuggestions(null, false, false, true);
+
+ 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 = mInputView.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() && mInputView.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();
+
+ 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();
+
+ // Show N-Best alternates, if there is more than one choice.
+ if (nBest.size() > 1) {
+ mImmediatelyAfterVoiceSuggestions = true;
+ mShowingVoiceSuggestions = true;
+ setSuggestions(nBest.subList(1, nBest.size()), false, true, true);
+ setCandidatesViewShown(true);
+ }
+ mVoiceInputHighlighted = true;
+ mWordToSuggestions.putAll(mVoiceResults.alternatives);
+
+ }
+
+ private void setSuggestions(
+ List<CharSequence> suggestions,
+ boolean completions,
+
+ boolean typedWordValid,
+ boolean haveMinimalSuggestion) {
+
+ if (mIsShowingHint) {
+ setCandidatesView(mCandidateViewContainer);
+ mIsShowingHint = false;
+ }
+
+ if (mCandidateView != null) {
+ mCandidateView.setSuggestions(
+ suggestions, completions, typedWordValid, haveMinimalSuggestion);
+ }
+ }
+
+ private void updateSuggestions() {
+ mSuggestionShouldReplaceCurrentWord = false;
+
+ ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null);
+
+ // Check if we have a suggestion engine attached.
+ if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) {
+ return;
+ }
+
+ if (!mPredicting) {
+ setNextSuggestions();
+ return;
+ }
+
+ List<CharSequence> stringList = mSuggest.getSuggestions(mInputView, mWord, false);
+ int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
+
+ ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies);
+
+ boolean correctionAvailable = mSuggest.hasMinimalCorrection();
+ //|| mCorrectionMode == mSuggest.CORRECTION_FULL;
+ CharSequence typedWord = mWord.getTypedWord();
+ // If we're in basic correct
+ boolean typedWordValid = mSuggest.isValidWord(typedWord) ||
+ (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase()));
+ if (mCorrectionMode == Suggest.CORRECTION_FULL) {
+ correctionAvailable |= typedWordValid;
+ }
+ // Don't auto-correct words with multiple capital letter
+ correctionAvailable &= !mWord.isMostlyCaps();
+
+ setSuggestions(stringList, false, typedWordValid, correctionAvailable);
+ if (stringList.size() > 0) {
+ if (correctionAvailable && !typedWordValid && stringList.size() > 1) {
+ mBestWord = stringList.get(1);
+ } else {
+ mBestWord = typedWord;
+ }
+ } else {
+ mBestWord = null;
+ }
+ setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn);
+ }
+
+ private void pickDefaultSuggestion() {
+ // Complete any pending candidate query first
+ if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) {
+ mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS);
+ updateSuggestions();
+ }
+ if (mBestWord != null) {
+ TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord);
+ mJustAccepted = true;
+ pickSuggestion(mBestWord);
+ // Add the word to the auto dictionary if it's not a known word
+ checkAddToDictionary(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
+ }
+ }
+
+ public void pickSuggestionManually(int index, CharSequence suggestion) {
+ if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index);
+
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ ic.beginBatchEdit();
+ }
+ if (mCompletionOn && mCompletions != null && index >= 0
+ && index < mCompletions.length) {
+ CompletionInfo ci = mCompletions[index];
+ if (ic != null) {
+ ic.commitCompletion(ci);
+ }
+ mCommittedLength = suggestion.length();
+ if (mCandidateView != null) {
+ mCandidateView.clear();
+ }
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ if (ic != null) {
+ ic.endBatchEdit();
+ }
+ return;
+ }
+
+ // If this is a punctuation, apply it through the normal key press
+ if (suggestion.length() == 1 && isWordSeparator(suggestion.charAt(0))) {
+ onKey(suggestion.charAt(0), null);
+ if (ic != null) {
+ ic.endBatchEdit();
+ }
+ return;
+ }
+ mJustAccepted = true;
+ pickSuggestion(suggestion);
+ // Add the word to the auto dictionary if it's not a known word
+ checkAddToDictionary(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
+ TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion);
+ // Follow it with a space
+ if (mAutoSpace) {
+ sendSpace();
+ mJustAddedAutoSpace = true;
+ }
+ // Fool the state watcher so that a subsequent backspace will not do a revert
+ TextEntryState.typedCharacter((char) KEYCODE_SPACE, true);
+ if (index == 0 && mCorrectionMode > 0 && !mSuggest.isValidWord(suggestion)) {
+ mCandidateView.showAddToDictionaryHint(suggestion);
+ }
+ if (ic != null) {
+ ic.endBatchEdit();
+ }
+ }
+
+ private void pickSuggestion(CharSequence suggestion) {
+ if (mCapsLock) {
+ suggestion = suggestion.toString().toUpperCase();
+ } else if (preferCapitalization()
+ || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) {
+ suggestion = suggestion.toString().toUpperCase().charAt(0)
+ + suggestion.subSequence(1, suggestion.length()).toString();
+ }
+ InputConnection ic = getCurrentInputConnection();
+ if (ic != null) {
+ if (mSuggestionShouldReplaceCurrentWord) {
+ EditingUtil.deleteWordAtCursor(ic, getWordSeparators());
+ }
+ if (!VoiceInput.DELETE_SYMBOL.equals(suggestion)) {
+ ic.commitText(suggestion, 1);
+ }
+ }
+ mPredicting = false;
+ mCommittedLength = suggestion.length();
+ ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null);
+ setNextSuggestions();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ }
+
+ private void setNextSuggestions() {
+ setSuggestions(mSuggestPuncList, false, false, false);
+ }
+
+ private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta) {
+ if (mAutoDictionary.isValidWord(suggestion)
+ || !mSuggest.isValidWord(suggestion.toString().toLowerCase())) {
+ mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
+ }
+ }
+
+ private boolean isCursorTouchingWord() {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return false;
+ CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
+ CharSequence toRight = ic.getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(toLeft)
+ && !isWordSeparator(toLeft.charAt(0))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(toRight)
+ && !isWordSeparator(toRight.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+
+ public void revertLastWord(boolean deleteChar) {
+ final int length = mComposing.length();
+ if (!mPredicting && length > 0) {
+ final InputConnection ic = getCurrentInputConnection();
+ mPredicting = true;
+ ic.beginBatchEdit();
+ mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0);
+ if (deleteChar) ic.deleteSurroundingText(1, 0);
+ int toDelete = mCommittedLength;
+ CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
+ if (toTheLeft != null && toTheLeft.length() > 0
+ && isWordSeparator(toTheLeft.charAt(0))) {
+ toDelete--;
+ }
+ ic.deleteSurroundingText(toDelete, 0);
+ ic.setComposingText(mComposing, 1);
+ TextEntryState.backspace();
+ ic.endBatchEdit();
+ postUpdateSuggestions();
+ } else {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ mJustRevertedSeparator = null;
+ }
+ }
+
+ protected String getWordSeparators() {
+ return mWordSeparators;
+ }
+
+ public boolean isWordSeparator(int code) {
+ String separators = getWordSeparators();
+ return separators.contains(String.valueOf((char)code));
+ }
+
+ public boolean isSentenceSeparator(int code) {
+ return mSentenceSeparators.contains(String.valueOf((char)code));
+ }
+
+ private void sendSpace() {
+ sendKeyChar((char)KEYCODE_SPACE);
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ //onKey(KEY_SPACE[0], KEY_SPACE);
+ }
+
+ public boolean preferCapitalization() {
+ return mWord.isCapitalized();
+ }
+
+ public void swipeRight() {
+ if (userHasNotTypedRecently() && VOICE_INSTALLED && mEnableVoice &&
+ fieldCanDoVoice(makeFieldContext())) {
+ startListening(true /* was a swipe */);
+ }
+
+ if (LatinKeyboardView.DEBUG_AUTO_PLAY) {
+ ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE));
+ CharSequence text = cm.getText();
+ if (!TextUtils.isEmpty(text)) {
+ mInputView.startPlaying(text.toString());
+ }
+ }
+ }
+
+ private void toggleLanguage(boolean reset, boolean next) {
+ if (reset) {
+ mLanguageSwitcher.reset();
+ } else {
+ if (next) {
+ mLanguageSwitcher.next();
+ } else {
+ mLanguageSwitcher.prev();
+ }
+ }
+ int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode();
+ reloadKeyboards();
+ mKeyboardSwitcher.makeKeyboards(true);
+ mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0,
+ mEnableVoiceButton && mEnableVoice);
+ initSuggest(mLanguageSwitcher.getInputLanguage());
+ mLanguageSwitcher.persist();
+ updateShiftKeyState(getCurrentInputEditorInfo());
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (PREF_SELECTED_LANGUAGES.equals(key)) {
+ mLanguageSwitcher.loadLocales(sharedPreferences);
+ mRefreshKeyboardRequired = true;
+ }
+ }
+
+ public void swipeLeft() {
+ }
+
+ public void swipeDown() {
+ handleClose();
+ }
+
+ public void swipeUp() {
+ //launchSettings();
+ }
+
+ public void onPress(int primaryCode) {
+ vibrate();
+ playKeyClick(primaryCode);
+ }
+
+ public void onRelease(int primaryCode) {
+ // Reset any drag flags in the keyboard
+ ((LatinKeyboard) mInputView.getKeyboard()).keyReleased();
+ //vibrate();
+ }
+
+ 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 && attribute.privateImeOptions != null
+ && attribute.privateImeOptions.equals(IME_OPTION_NO_MICROPHONE))
+ && RecognitionManager.isRecognitionAvailable(this);
+ }
+
+ // receive ringer mode changes to detect silent mode
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateRingerMode();
+ }
+ };
+
+ // update flags for silent mode
+ private void updateRingerMode() {
+ if (mAudioManager == null) {
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ }
+ if (mAudioManager != null) {
+ mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
+ }
+ }
+
+ private boolean userHasNotTypedRecently() {
+ return (SystemClock.uptimeMillis() - mLastKeyTime)
+ > MIN_MILLIS_AFTER_TYPING_BEFORE_SWIPE;
+ }
+
+ private void playKeyClick(int primaryCode) {
+ // if mAudioManager is null, we don't have the ringer state yet
+ // mAudioManager will be set by updateRingerMode
+ if (mAudioManager == null) {
+ if (mInputView != null) {
+ updateRingerMode();
+ }
+ }
+ if (mSoundOn && !mSilentMode) {
+ // FIXME: Volume and enable should come from UI settings
+ // FIXME: These should be triggered after auto-repeat logic
+ int sound = AudioManager.FX_KEYPRESS_STANDARD;
+ switch (primaryCode) {
+ case Keyboard.KEYCODE_DELETE:
+ sound = AudioManager.FX_KEYPRESS_DELETE;
+ break;
+ case KEYCODE_ENTER:
+ sound = AudioManager.FX_KEYPRESS_RETURN;
+ break;
+ case KEYCODE_SPACE:
+ sound = AudioManager.FX_KEYPRESS_SPACEBAR;
+ break;
+ }
+ mAudioManager.playSoundEffect(sound, FX_VOLUME);
+ }
+ }
+
+ private void vibrate() {
+ if (!mVibrateOn) {
+ return;
+ }
+ if (mInputView != null) {
+ mInputView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
+ }
+ }
+
+ private void checkTutorial(String privateImeOptions) {
+ if (privateImeOptions == null) return;
+ if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) {
+ if (mTutorial == null) startTutorial();
+ } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) {
+ if (mTutorial != null) {
+ if (mTutorial.close()) {
+ mTutorial = null;
+ }
+ }
+ }
+ }
+
+ private void startTutorial() {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500);
+ }
+
+ void tutorialDone() {
+ mTutorial = null;
+ }
+
+ void promoteToUserDictionary(String word, int frequency) {
+ if (mUserDictionary.isValidWord(word)) return;
+ mUserDictionary.addWord(word, frequency);
+ }
+
+ WordComposer getCurrentWord() {
+ return mWord;
+ }
+
+ private void updateCorrectionMode() {
+ mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
+ mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes)
+ && !mInputTypeNoAutoCorrect && mHasDictionary;
+ mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled)
+ ? Suggest.CORRECTION_FULL
+ : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
+ if (mSuggest != null) {
+ mSuggest.setCorrectionMode(mCorrectionMode);
+ }
+ }
+
+ private void updateAutoTextEnabled(Locale systemLocale) {
+ if (mSuggest == null) return;
+ boolean different = !systemLocale.getLanguage().equalsIgnoreCase(mLocale.substring(0, 2));
+ mSuggest.setAutoTextEnabled(!different && mQuickFixes);
+ }
+
+ protected void launchSettings() {
+ launchSettings(LatinIMESettings.class);
+ }
+
+ protected void launchSettings(Class settingsClass) {
+ handleClose();
+ Intent intent = new Intent();
+ intent.setClass(LatinIME.this, settingsClass);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+
+ private void loadSettings() {
+ // Get the settings preferences
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false);
+ mSoundOn = sp.getBoolean(PREF_SOUND_ON, false);
+ 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(mLocale);
+
+ 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);
+ }
+ mEnableVoice = enableVoice;
+ mVoiceOnPrimary = voiceOnPrimary;
+ }
+ mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE,
+ mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions;
+ updateCorrectionMode();
+ updateAutoTextEnabled(mResources.getConfiguration().locale);
+ mLanguageSwitcher.loadLocales(sp);
+ }
+
+ private void initSuggestPuncList() {
+ mSuggestPuncList = new ArrayList<CharSequence>();
+ String suggestPuncs = mResources.getString(R.string.suggested_punctuations);
+ if (suggestPuncs != null) {
+ for (int i = 0; i < suggestPuncs.length(); i++) {
+ mSuggestPuncList.add(suggestPuncs.subSequence(i, i + 1));
+ }
+ }
+ }
+
+ private void showOptionsMenu() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setCancelable(true);
+ builder.setIcon(R.drawable.ic_dialog_keyboard);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ CharSequence itemSettings = getString(R.string.english_ime_settings);
+ CharSequence itemInputMethod = getString(R.string.inputMethod);
+ builder.setItems(new CharSequence[] {
+ itemSettings, itemInputMethod},
+ new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface di, int position) {
+ di.dismiss();
+ switch (position) {
+ case POS_SETTINGS:
+ launchSettings();
+ break;
+ case POS_METHOD:
+ ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
+ .showInputMethodPicker();
+ break;
+ }
+ }
+ });
+ builder.setTitle(mResources.getString(R.string.english_ime_name));
+ mOptionsDialog = builder.create();
+ Window window = mOptionsDialog.getWindow();
+ WindowManager.LayoutParams lp = window.getAttributes();
+ lp.token = mInputView.getWindowToken();
+ lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+ window.setAttributes(lp);
+ window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ mOptionsDialog.show();
+ }
+
+ private void changeKeyboardMode() {
+ mKeyboardSwitcher.toggleSymbols();
+ if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) {
+ ((LatinKeyboard) mInputView.getKeyboard()).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);
+
+ 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);
+ p.println(" mPredicting=" + mPredicting);
+ p.println(" mAutoCorrectOn=" + mAutoCorrectOn);
+ p.println(" mAutoSpace=" + mAutoSpace);
+ p.println(" mCompletionOn=" + mCompletionOn);
+ p.println(" TextEntryState.state=" + TextEntryState.getState());
+ p.println(" mSoundOn=" + mSoundOn);
+ p.println(" mVibrateOn=" + mVibrateOn);
+ }
+
+ // Characters per second measurement
+
+ private static final boolean PERF_DEBUG = false;
+ private long mLastCpsTime;
+ private static final int CPS_BUFFER_SIZE = 16;
+ private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
+ private int mCpsIndex;
+ private boolean mInputTypeNoAutoCorrect;
+
+ private void measureCps() {
+ if (!LatinIME.PERF_DEBUG) return;
+ long now = System.currentTimeMillis();
+ if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
+ mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
+ mLastCpsTime = now;
+ mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
+ long total = 0;
+ for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
+ System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java b/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java
new file mode 100644
index 000000000..62da86cd4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2009 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.app.backup.BackupHelperAgent;
+import android.app.backup.SharedPreferencesBackupHelper;
+
+/**
+ * Backs up the Latin IME shared preferences.
+ */
+public class LatinIMEBackupAgent extends BackupHelperAgent {
+
+ public void onCreate() {
+ addHelper("shared_pref", new SharedPreferencesBackupHelper(this,
+ getPackageName() + "_preferences"));
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java
new file mode 100644
index 000000000..f76ec99b6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinIMESettings.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.ArrayList;
+import java.util.Locale;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.backup.BackupManager;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.speech.RecognitionManager;
+import android.text.AutoText;
+import android.util.Log;
+
+import com.android.inputmethod.voice.SettingsUtil;
+import com.android.inputmethod.voice.VoiceInputLogger;
+
+public class LatinIMESettings extends PreferenceActivity
+ implements SharedPreferences.OnSharedPreferenceChangeListener,
+ DialogInterface.OnDismissListener {
+
+ private static final String QUICK_FIXES_KEY = "quick_fixes";
+ private static final String SHOW_SUGGESTIONS_KEY = "show_suggestions";
+ private static final String PREDICTION_SETTINGS_KEY = "prediction_settings";
+ private static final String VOICE_SETTINGS_KEY = "voice_mode";
+ private static final String VOICE_ON_PRIMARY_KEY = "voice_on_main";
+ private static final String VOICE_SERVER_KEY = "voice_server_url";
+
+ private static final String TAG = "LatinIMESettings";
+
+ // Dialog ids
+ private static final int VOICE_INPUT_CONFIRM_DIALOG = 0;
+
+ private CheckBoxPreference mQuickFixes;
+ private CheckBoxPreference mShowSuggestions;
+ private ListPreference mVoicePreference;
+ private boolean mVoiceOn;
+
+ private VoiceInputLogger mLogger;
+
+ private boolean mOkClicked = false;
+ private String mVoiceModeOff;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ addPreferencesFromResource(R.xml.prefs);
+ mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY);
+ mShowSuggestions = (CheckBoxPreference) findPreference(SHOW_SUGGESTIONS_KEY);
+ mVoicePreference = (ListPreference) findPreference(VOICE_SETTINGS_KEY);
+ SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ mVoiceModeOff = getString(R.string.voice_mode_off);
+ mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff));
+ mLogger = VoiceInputLogger.getLogger(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ int autoTextSize = AutoText.getSize(getListView());
+ if (autoTextSize < 1) {
+ ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY))
+ .removePreference(mQuickFixes);
+ }
+ if (!LatinIME.VOICE_INSTALLED
+ || !RecognitionManager.isRecognitionAvailable(this)) {
+ getPreferenceScreen().removePreference(mVoicePreference);
+ } else {
+ updateVoiceModeSummary();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
+ this);
+ super.onDestroy();
+ }
+
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ (new BackupManager(this)).dataChanged();
+ // If turning on voice input, show dialog
+ if (key.equals(VOICE_SETTINGS_KEY) && !mVoiceOn) {
+ if (! prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff)
+ .equals(mVoiceModeOff)) {
+ showVoiceConfirmation();
+ }
+ }
+ mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff));
+ updateVoiceModeSummary();
+ }
+
+ private void showVoiceConfirmation() {
+ mOkClicked = false;
+ showDialog(VOICE_INPUT_CONFIRM_DIALOG);
+ }
+
+ private void updateVoiceModeSummary() {
+ mVoicePreference.setSummary(
+ getResources().getStringArray(R.array.voice_input_modes_summary)
+ [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case VOICE_INPUT_CONFIRM_DIALOG:
+ DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (whichButton == DialogInterface.BUTTON_NEGATIVE) {
+ mVoicePreference.setValue(mVoiceModeOff);
+ mLogger.settingsWarningDialogCancel();
+ } else if (whichButton == DialogInterface.BUTTON_POSITIVE) {
+ mOkClicked = true;
+ mLogger.settingsWarningDialogOk();
+ }
+ updateVoicePreference();
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.voice_warning_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setNegativeButton(android.R.string.cancel, listener);
+
+ // 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(
+ Locale.getDefault().toString());
+
+ if (localeSupported) {
+ String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" +
+ getString(R.string.voice_hint_dialog_message);
+ 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_hint_dialog_message);
+ builder.setMessage(message);
+ }
+
+ AlertDialog dialog = builder.create();
+ dialog.setOnDismissListener(this);
+ mLogger.settingsWarningDialogShown();
+ return dialog;
+ default:
+ Log.e(TAG, "unknown dialog " + id);
+ return null;
+ }
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ mLogger.settingsWarningDialogDismissed();
+ if (!mOkClicked) {
+ // This assumes that onPreferenceClick gets called first, and this if the user
+ // agreed after the warning, we set the mOkClicked value to true.
+ mVoicePreference.setValue(mVoiceModeOff);
+ }
+ }
+
+ private void updateVoicePreference() {
+ boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff);
+ if (isChecked) {
+ mLogger.voiceInputSettingEnabled();
+ } else {
+ mLogger.voiceInputSettingDisabled();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
new file mode 100644
index 000000000..58e1dc767
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.List;
+import java.util.Locale;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Paint.Align;
+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;
+
+public class LatinKeyboard extends Keyboard {
+
+ private static final boolean DEBUG_PREFERRED_LETTER = false;
+ private static final String TAG = "LatinKeyboard";
+
+ private Drawable mShiftLockIcon;
+ private Drawable mShiftLockPreviewIcon;
+ private Drawable mOldShiftIcon;
+ private Drawable mOldShiftPreviewIcon;
+ private Drawable mSpaceIcon;
+ private Drawable mSpacePreviewIcon;
+ private Drawable mMicIcon;
+ private Drawable mMicPreviewIcon;
+ private Drawable m123MicIcon;
+ private Drawable m123MicPreviewIcon;
+ private Drawable mButtonArrowLeftIcon;
+ private Drawable mButtonArrowRightIcon;
+ private Key mShiftKey;
+ private Key mEnterKey;
+ private Key mF1Key;
+ private Key mSpaceKey;
+ private Key m123Key;
+ private int mSpaceKeyIndex = -1;
+ private int mSpaceDragStartX;
+ private int mSpaceDragLastDiff;
+ /* package */ Locale mLocale;
+ private LanguageSwitcher mLanguageSwitcher;
+ private Resources mRes;
+ private 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 boolean mIsAlphaKeyboard;
+ private CharSequence m123Label;
+ private boolean mCurrentlyInSpace;
+ private SlidingLocaleDrawable mSlidingLocaleIcon;
+ private Rect mBounds = new Rect();
+ private int[] mPrefLetterFrequencies;
+ private boolean mPreemptiveCorrection;
+ private int mPrefLetter;
+ private int mPrefLetterX;
+ private int mPrefLetterY;
+ private int mPrefDistance;
+
+ private int mExtensionResId;
+
+ 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 static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
+ private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
+ private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f;
+
+ 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);
+ final Resources res = context.getResources();
+ mContext = context;
+ mMode = mode;
+ mRes = res;
+ mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
+ mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked);
+ mShiftLockPreviewIcon.setBounds(0, 0,
+ mShiftLockPreviewIcon.getIntrinsicWidth(),
+ mShiftLockPreviewIcon.getIntrinsicHeight());
+ mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
+ 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);
+ setDefaultBounds(m123MicPreviewIcon);
+ sSpacebarVerticalCorrection = res.getDimensionPixelOffset(
+ R.dimen.spacebar_vertical_correction);
+ mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty;
+ mSpaceKeyIndex = indexOf((int) ' ');
+ }
+
+ public LatinKeyboard(Context context, int layoutTemplateResId,
+ CharSequence characters, int columns, int horizontalPadding) {
+ super(context, layoutTemplateResId, characters, columns, horizontalPadding);
+ }
+
+ @Override
+ protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
+ XmlResourceParser parser) {
+ Key key = new LatinKey(res, parent, x, y, parser);
+ switch (key.codes[0]) {
+ case 10:
+ mEnterKey = key;
+ break;
+ case LatinKeyboardView.KEYCODE_F1:
+ mF1Key = key;
+ break;
+ case 32:
+ mSpaceKey = key;
+ break;
+ case KEYCODE_MODE_CHANGE:
+ m123Key = key;
+ m123Label = key.label;
+ break;
+ }
+ return key;
+ }
+
+ void setImeOptions(Resources res, int mode, int options) {
+ 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(
+ 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 = null;
+ 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(
+ R.drawable.sym_keyboard_return);
+ mEnterKey.label = null;
+ }
+ break;
+ }
+ // Set the initial size of the preview icon
+ if (mEnterKey.iconPreview != null) {
+ mEnterKey.iconPreview.setBounds(0, 0,
+ mEnterKey.iconPreview.getIntrinsicWidth(),
+ mEnterKey.iconPreview.getIntrinsicHeight());
+ }
+ }
+ }
+
+ void enableShiftLock() {
+ int index = getShiftKeyIndex();
+ if (index >= 0) {
+ mShiftKey = getKeys().get(index);
+ if (mShiftKey instanceof LatinKey) {
+ ((LatinKey)mShiftKey).enableShiftLock();
+ }
+ mOldShiftIcon = mShiftKey.icon;
+ mOldShiftPreviewIcon = mShiftKey.iconPreview;
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ boolean isShiftLocked() {
+ return mShiftState == SHIFT_LOCKED;
+ }
+
+ @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;
+ }
+ }
+ } else {
+ return super.setShifted(shiftState);
+ }
+ return shiftChanged;
+ }
+
+ @Override
+ public boolean isShifted() {
+ if (mShiftKey != null) {
+ return mShiftState != SHIFT_OFF;
+ } else {
+ return super.isShifted();
+ }
+ }
+
+ public void setExtension(int resId) {
+ mExtensionResId = resId;
+ }
+
+ public int getExtension() {
+ return mExtensionResId;
+ }
+
+ private void setDefaultBounds(Drawable drawable) {
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ }
+
+ public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) {
+ mHasVoiceButton = hasVoiceButton;
+ mVoiceEnabled = hasVoice;
+ updateF1Key();
+ }
+
+ private void updateF1Key() {
+ if (mF1Key == null) return;
+ 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;
+ }
+ }
+
+ if (mHasVoiceButton && mVoiceEnabled) {
+ mF1Key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE };
+ mF1Key.label = null;
+ mF1Key.icon = mMicIcon;
+ mF1Key.iconPreview = mMicPreviewIcon;
+ } else {
+ mF1Key.label = ",";
+ mF1Key.codes = new int[] { ',' };
+ mF1Key.icon = null;
+ mF1Key.iconPreview = null;
+ }
+ }
+
+ private void updateSpaceBarForLocale() {
+ if (mLocale != null) {
+ // Create the graphic for spacebar
+ Bitmap buffer = Bitmap.createBitmap(mSpaceKey.width, mSpaceIcon.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(buffer);
+ drawSpaceBar(canvas, buffer.getWidth(), buffer.getHeight(), 255);
+ mSpaceKey.icon = new BitmapDrawable(mRes, buffer);
+ mSpaceKey.repeatable = mLanguageSwitcher.getLocaleCount() < 2;
+ } else {
+ mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space);
+ mSpaceKey.repeatable = true;
+ }
+ }
+
+ private void drawSpaceBar(Canvas canvas, int width, int height, int opacity) {
+ canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setAlpha(opacity);
+ // Get the text size from the theme
+ paint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14));
+ paint.setTextAlign(Align.CENTER);
+ //// Draw a drop shadow for the text
+ //paint.setShadowLayer(2f, 0, 0, 0xFF000000);
+ final String language = getInputLanguage(mSpaceKey.width, paint);
+ final int ascent = (int) -paint.ascent();
+ paint.setColor(0x80000000);
+ canvas.drawText(language,
+ width / 2, ascent - 1, paint);
+ paint.setColor(0xFF808080);
+ canvas.drawText(language,
+ width / 2, ascent, paint);
+ // Put arrows on either side of the text
+ if (mLanguageSwitcher.getLocaleCount() > 1) {
+ Rect bounds = new Rect();
+ paint.getTextBounds(language, 0, language.length(), bounds);
+ drawButtonArrow(mButtonArrowLeftIcon, canvas,
+ (mSpaceKey.width - bounds.right) / 2
+ - mButtonArrowLeftIcon.getIntrinsicWidth(),
+ (int) paint.getTextSize());
+ drawButtonArrow(mButtonArrowRightIcon, canvas,
+ (mSpaceKey.width + bounds.right) / 2, (int) paint.getTextSize());
+ }
+ // Draw the spacebar icon at the bottom
+ int x = (width - mSpaceIcon.getIntrinsicWidth()) / 2;
+ int y = height - mSpaceIcon.getIntrinsicHeight();
+ mSpaceIcon.setBounds(x, y,
+ x + mSpaceIcon.getIntrinsicWidth(), y + mSpaceIcon.getIntrinsicHeight());
+ mSpaceIcon.draw(canvas);
+ }
+
+ private void drawButtonArrow(Drawable arrow, Canvas canvas, int x, int bottomY) {
+ arrow.setBounds(x, bottomY - arrow.getIntrinsicHeight(), x + arrow.getIntrinsicWidth(),
+ bottomY);
+ arrow.draw(canvas);
+ }
+
+ private String getInputLanguage(int widthAvail, Paint paint) {
+ return chooseDisplayName(mLanguageSwitcher.getInputLocale(), widthAvail, paint);
+ }
+
+ private String getNextInputLanguage(int widthAvail, Paint paint) {
+ return chooseDisplayName(mLanguageSwitcher.getNextInputLocale(), widthAvail, paint);
+ }
+
+ private String getPrevInputLanguage(int widthAvail, Paint paint) {
+ return chooseDisplayName(mLanguageSwitcher.getPrevInputLocale(), widthAvail, paint);
+ }
+
+ private String chooseDisplayName(Locale locale, int widthAvail, Paint paint) {
+ if (widthAvail < (int) (.35 * getMinWidth())) {
+ return LanguageSwitcher.toTitleCase(locale.getLanguage().substring(0, 2));
+ } else {
+ return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale));
+ }
+ }
+
+ private void updateLocaleDrag(int diff) {
+ if (mSlidingLocaleIcon == null) {
+ mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, mSpaceKey.width,
+ mSpacePreviewIcon.getIntrinsicHeight());
+ mSlidingLocaleIcon.setBounds(0, 0, mSpaceKey.width,
+ mSpacePreviewIcon.getIntrinsicHeight());
+ mSpaceKey.iconPreview = mSlidingLocaleIcon;
+ }
+ mSlidingLocaleIcon.setDiff(diff);
+ if (Math.abs(diff) == Integer.MAX_VALUE) {
+ mSpaceKey.iconPreview = mSpacePreviewIcon;
+ } else {
+ mSpaceKey.iconPreview = mSlidingLocaleIcon;
+ }
+ mSpaceKey.iconPreview.invalidateSelf();
+ }
+
+ public int getLanguageChangeDirection() {
+ if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2
+ || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) {
+ return 0; // No change
+ }
+ return mSpaceDragLastDiff > 0 ? 1 : -1;
+ }
+
+ public void setLanguageSwitcher(LanguageSwitcher switcher) {
+ mLanguageSwitcher = switcher;
+ Locale locale = mLanguageSwitcher.getLocaleCount() > 0
+ ? mLanguageSwitcher.getInputLocale()
+ : null;
+ if (mLocale != null && mLocale.equals(locale)) return;
+ mLocale = locale;
+ updateSpaceBarForLocale();
+ }
+
+ boolean isCurrentlyInSpace() {
+ return mCurrentlyInSpace;
+ }
+
+ void setPreferredLetters(int[] frequencies) {
+ mPrefLetterFrequencies = frequencies;
+ mPrefLetter = 0;
+ }
+
+ void keyReleased() {
+ mCurrentlyInSpace = false;
+ mSpaceDragLastDiff = 0;
+ mPrefLetter = 0;
+ mPrefLetterX = 0;
+ mPrefLetterY = 0;
+ mPrefDistance = Integer.MAX_VALUE;
+ if (mSpaceKey != null) {
+ updateLocaleDrag(Integer.MAX_VALUE);
+ }
+ }
+
+ /**
+ * Does the magic of locking the touch gesture into the spacebar when
+ * switching input languages.
+ */
+ boolean isInside(LatinKey key, int x, int y) {
+ final int code = key.codes[0];
+ 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 (mCurrentlyInSpace) {
+ int diff = x - mSpaceDragStartX;
+ if (Math.abs(diff - mSpaceDragLastDiff) > 0) {
+ updateLocaleDrag(diff);
+ }
+ mSpaceDragLastDiff = diff;
+ return true;
+ } else {
+ boolean insideSpace = key.isInsideSuper(x, y);
+ if (insideSpace) {
+ mCurrentlyInSpace = true;
+ mSpaceDragStartX = x;
+ updateLocaleDrag(0);
+ }
+ return insideSpace;
+ }
+ }
+ } else if (mPrefLetterFrequencies != null) {
+ // New coordinate? Reset
+ if (mPrefLetterX != x || mPrefLetterY != y) {
+ mPrefLetter = 0;
+ mPrefDistance = Integer.MAX_VALUE;
+ }
+ // Handle preferred next letter
+ final int[] pref = mPrefLetterFrequencies;
+ if (mPrefLetter > 0) {
+ if (DEBUG_PREFERRED_LETTER && mPrefLetter == code
+ && !key.isInsideSuper(x, y)) {
+ Log.d(TAG, "CORRECTED !!!!!!");
+ }
+ return mPrefLetter == code;
+ } else {
+ final boolean inside = key.isInsideSuper(x, y);
+ int[] nearby = getNearestKeys(x, y);
+ List<Key> nearbyKeys = getKeys();
+ if (inside) {
+ // If it's a preferred letter
+ if (inPrefList(code, pref)) {
+ // Check if its frequency is much lower than a nearby key
+ mPrefLetter = code;
+ mPrefLetterX = x;
+ mPrefLetterY = y;
+ for (int i = 0; i < nearby.length; i++) {
+ Key k = nearbyKeys.get(nearby[i]);
+ if (k != key && inPrefList(k.codes[0], pref)) {
+ final int dist = distanceFrom(k, x, y);
+ if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) &&
+ (pref[k.codes[0]] > pref[mPrefLetter] * 3)) {
+ mPrefLetter = k.codes[0];
+ mPrefDistance = dist;
+ if (DEBUG_PREFERRED_LETTER) {
+ Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!");
+ }
+ break;
+ }
+ }
+ }
+
+ return mPrefLetter == code;
+ }
+ }
+
+ // Get the surrounding keys and intersect with the preferred list
+ // For all in the intersection
+ // if distance from touch point is within a reasonable distance
+ // make this the pref letter
+ // If no pref letter
+ // return inside;
+ // else return thiskey == prefletter;
+
+ for (int i = 0; i < nearby.length; i++) {
+ Key k = nearbyKeys.get(nearby[i]);
+ if (inPrefList(k.codes[0], pref)) {
+ final int dist = distanceFrom(k, x, y);
+ if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB)
+ && dist < mPrefDistance) {
+ mPrefLetter = k.codes[0];
+ mPrefLetterX = x;
+ mPrefLetterY = y;
+ mPrefDistance = dist;
+ }
+ }
+ }
+ // Didn't find any
+ if (mPrefLetter == 0) {
+ return inside;
+ } else {
+ return mPrefLetter == code;
+ }
+ }
+ }
+
+ // Lock into the spacebar
+ if (mCurrentlyInSpace) return false;
+
+ return key.isInsideSuper(x, y);
+ }
+
+ private boolean inPrefList(int code, int[] pref) {
+ if (code < pref.length && code >= 0) return pref[code] > 0;
+ return false;
+ }
+
+ private int distanceFrom(Key k, int x, int y) {
+ if (y > k.y && y < k.y + k.height) {
+ return Math.abs(k.x + k.width / 2 - x);
+ } else {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public int[] getNearestKeys(int x, int y) {
+ if (mCurrentlyInSpace) {
+ return new int[] { mSpaceKeyIndex };
+ } else {
+ return super.getNearestKeys(x, y);
+ }
+ }
+
+ private int indexOf(int code) {
+ List<Key> keys = getKeys();
+ int count = keys.size();
+ for (int i = 0; i < count; i++) {
+ if (keys.get(i).codes[0] == code) return i;
+ }
+ return -1;
+ }
+
+ private int getTextSizeFromTheme(int style, int defValue) {
+ TypedArray array = mContext.getTheme().obtainStyledAttributes(
+ style, new int[] { android.R.attr.textSize });
+ int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
+ return textSize;
+ }
+
+ class LatinKey extends Keyboard.Key {
+
+ private boolean mShiftLockEnabled;
+
+ public LatinKey(Resources res, Keyboard.Row parent, int x, int y,
+ XmlResourceParser parser) {
+ super(res, parent, x, y, parser);
+ if (popupCharacters != null && popupCharacters.length() == 0) {
+ // If there is a keyboard with no keys specified in popupCharacters
+ popupResId = 0;
+ }
+ }
+
+ void enableShiftLock() {
+ mShiftLockEnabled = true;
+ }
+
+ @Override
+ public void onReleased(boolean inside) {
+ if (!mShiftLockEnabled) {
+ super.onReleased(inside);
+ } else {
+ pressed = !pressed;
+ }
+ }
+
+ /**
+ * Overriding this method so that we can reduce the target area for certain keys.
+ */
+ @Override
+ public boolean isInside(int x, int y) {
+ boolean result = LatinKeyboard.this.isInside(this, x, y);
+ return result;
+ }
+
+ boolean isInsideSuper(int x, int y) {
+ return super.isInside(x, y);
+ }
+ }
+
+ /**
+ * Animation to be displayed on the spacebar preview popup when switching
+ * 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 int mWidth;
+ private int mHeight;
+ private Drawable mBackground;
+ private int mDiff;
+ private TextPaint mTextPaint;
+ private int mMiddleX;
+ private int mAscent;
+ private Drawable mLeftDrawable;
+ private Drawable mRightDrawable;
+ private boolean mHitThreshold;
+ private int mThreshold;
+ private String mCurrentLanguage;
+ private String mNextLanguage;
+ private String mPrevLanguage;
+
+ public SlidingLocaleDrawable(Drawable background, int width, int height) {
+ mBackground = background;
+ mBackground.setBounds(0, 0,
+ mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
+ mWidth = width;
+ mHeight = height;
+ mTextPaint = new TextPaint();
+ int textSize = getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18);
+ mTextPaint.setTextSize(textSize);
+ mTextPaint.setColor(0);
+ mTextPaint.setTextAlign(Align.CENTER);
+ mTextPaint.setAlpha(255);
+ mTextPaint.setAntiAlias(true);
+ mAscent = (int) mTextPaint.ascent();
+ 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);
+ mLeftDrawable.setBounds(0, 0,
+ mLeftDrawable.getIntrinsicWidth(), mLeftDrawable.getIntrinsicHeight());
+ mRightDrawable.setBounds(mWidth - mRightDrawable.getIntrinsicWidth(), 0,
+ mWidth, mRightDrawable.getIntrinsicHeight());
+ mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ }
+
+ void setDiff(int diff) {
+ if (diff == Integer.MAX_VALUE) {
+ mHitThreshold = false;
+ mCurrentLanguage = null;
+ return;
+ }
+ mDiff = diff;
+ if (mDiff > mWidth) mDiff = mWidth;
+ if (mDiff < -mWidth) mDiff = -mWidth;
+ if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.save();
+ if (mHitThreshold) {
+ mTextPaint.setColor(0xFF000000);
+ canvas.clipRect(0, 0, mWidth, mHeight);
+ if (mCurrentLanguage == null) {
+ mCurrentLanguage = getInputLanguage(mWidth, mTextPaint);
+ mNextLanguage = getNextInputLanguage(mWidth, mTextPaint);
+ mPrevLanguage = getPrevInputLanguage(mWidth, mTextPaint);
+ }
+ canvas.drawText(mCurrentLanguage,
+ mWidth / 2 + mDiff, -mAscent + 4, mTextPaint);
+ canvas.drawText(mNextLanguage,
+ mDiff - mWidth / 2, -mAscent + 4, mTextPaint);
+ canvas.drawText(mPrevLanguage,
+ mDiff + mWidth + mWidth / 2, -mAscent + 4, mTextPaint);
+ mLeftDrawable.draw(canvas);
+ mRightDrawable.draw(canvas);
+ }
+ if (mBackground != null) {
+ canvas.translate(mMiddleX, 0);
+ mBackground.draw(canvas);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // Ignore
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // Ignore
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mHeight;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
new file mode 100644
index 000000000..2686a93c6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.List;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.KeyboardView;
+import android.inputmethodservice.Keyboard.Key;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.widget.PopupWindow;
+
+public class LatinKeyboardView extends KeyboardView {
+
+ static final int KEYCODE_OPTIONS = -100;
+ static final int KEYCODE_SHIFT_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;
+
+ private Keyboard mPhoneKeyboard;
+
+ private boolean mExtensionVisible;
+ private LatinKeyboardView mExtension;
+ private PopupWindow mExtensionPopup;
+ private boolean mFirstEvent;
+
+ public LatinKeyboardView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setPhoneKeyboard(Keyboard phoneKeyboard) {
+ mPhoneKeyboard = phoneKeyboard;
+ }
+
+ @Override
+ protected boolean onLongPress(Key key) {
+ if (key.codes[0] == Keyboard.KEYCODE_MODE_CHANGE) {
+ getOnKeyboardActionListener().onKey(KEYCODE_OPTIONS, null);
+ return true;
+ } else if (key.codes[0] == Keyboard.KEYCODE_SHIFT) {
+ getOnKeyboardActionListener().onKey(KEYCODE_SHIFT_LONGPRESS, null);
+ invalidateAllKeys();
+ return true;
+ } else if (key.codes[0] == '0' && getKeyboard() == mPhoneKeyboard) {
+ // Long pressing on 0 in phone number keypad gives you a '+'.
+ getOnKeyboardActionListener().onKey('+', null);
+ return true;
+ } else {
+ return super.onLongPress(key);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ LatinKeyboard keyboard = (LatinKeyboard) getKeyboard();
+ if (DEBUG_LINE) {
+ mLastX = (int) me.getX();
+ mLastY = (int) me.getY();
+ invalidate();
+ }
+ // Reset any bounding box controls in the keyboard
+ if (me.getAction() == MotionEvent.ACTION_DOWN) {
+ keyboard.keyReleased();
+ }
+
+ if (me.getAction() == MotionEvent.ACTION_UP) {
+ int languageDirection = keyboard.getLanguageChangeDirection();
+ if (languageDirection != 0) {
+ getOnKeyboardActionListener().onKey(
+ languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE,
+ null);
+ me.setAction(MotionEvent.ACTION_CANCEL);
+ keyboard.keyReleased();
+ return super.onTouchEvent(me);
+ }
+ }
+
+ // If we don't have an extension keyboard, don't go any further.
+ if (keyboard.getExtension() == 0) {
+ return super.onTouchEvent(me);
+ }
+ if (me.getY() < 0) {
+ if (mExtensionVisible) {
+ int action = me.getAction();
+ if (mFirstEvent) action = MotionEvent.ACTION_DOWN;
+ mFirstEvent = false;
+ MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
+ action,
+ me.getX(), me.getY() + mExtension.getHeight(), me.getMetaState());
+ boolean result = mExtension.onTouchEvent(translated);
+ translated.recycle();
+ if (me.getAction() == MotionEvent.ACTION_UP
+ || me.getAction() == MotionEvent.ACTION_CANCEL) {
+ closeExtension();
+ }
+ return result;
+ } else {
+ if (openExtension()) {
+ MotionEvent cancel = MotionEvent.obtain(me.getDownTime(), me.getEventTime(),
+ MotionEvent.ACTION_CANCEL, me.getX() - 100, me.getY() - 100, 0);
+ super.onTouchEvent(cancel);
+ cancel.recycle();
+ if (mExtension.getHeight() > 0) {
+ MotionEvent translated = MotionEvent.obtain(me.getEventTime(),
+ me.getEventTime(),
+ MotionEvent.ACTION_DOWN,
+ me.getX(), me.getY() + mExtension.getHeight(),
+ me.getMetaState());
+ mExtension.onTouchEvent(translated);
+ translated.recycle();
+ } else {
+ mFirstEvent = true;
+ }
+ }
+ return true;
+ }
+ } else if (mExtensionVisible) {
+ closeExtension();
+ // Send a down event into the main keyboard first
+ MotionEvent down = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
+ MotionEvent.ACTION_DOWN,
+ me.getX(), me.getY(), me.getMetaState());
+ super.onTouchEvent(down);
+ down.recycle();
+ // Send the actual event
+ return super.onTouchEvent(me);
+ } else {
+ return super.onTouchEvent(me);
+ }
+ }
+
+ private boolean openExtension() {
+ if (((LatinKeyboard) getKeyboard()).getExtension() == 0) return false;
+ makePopupWindow();
+ mExtensionVisible = true;
+ return true;
+ }
+
+ private void makePopupWindow() {
+ if (mExtensionPopup == null) {
+ int[] windowLocation = new int[2];
+ mExtensionPopup = new PopupWindow(getContext());
+ mExtensionPopup.setBackgroundDrawable(null);
+ LayoutInflater li = (LayoutInflater) getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mExtension = (LatinKeyboardView) li.inflate(R.layout.input_trans, null);
+ mExtension.setOnKeyboardActionListener((LatinIME) getContext());
+ mExtension.setPopupParent(this);
+ mExtension.setPopupOffset(0, -windowLocation[1]);
+ Keyboard keyboard;
+ mExtension.setKeyboard(keyboard = new LatinKeyboard(getContext(),
+ ((LatinKeyboard) getKeyboard()).getExtension()));
+ mExtensionPopup.setContentView(mExtension);
+ mExtensionPopup.setWidth(getWidth());
+ mExtensionPopup.setHeight(keyboard.getHeight());
+ getLocationInWindow(windowLocation);
+ // TODO: Fix the "- 30".
+ mExtension.setPopupOffset(0, -windowLocation[1] - 30);
+ mExtensionPopup.showAtLocation(this, 0, 0, -keyboard.getHeight()
+ + windowLocation[1]);
+ } else {
+ mExtension.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void closing() {
+ super.closing();
+ if (mExtensionPopup != null && mExtensionPopup.isShowing()) {
+ mExtensionPopup.dismiss();
+ mExtensionPopup = null;
+ }
+ }
+
+ private void closeExtension() {
+ mExtension.setVisibility(INVISIBLE);
+ mExtension.closing();
+ mExtensionVisible = false;
+ }
+
+ /**************************** INSTRUMENTATION *******************************/
+
+ static final boolean DEBUG_AUTO_PLAY = false;
+ static final boolean DEBUG_LINE = false;
+ private static final int MSG_TOUCH_DOWN = 1;
+ private static final int MSG_TOUCH_UP = 2;
+
+ Handler mHandler2;
+
+ private String mStringToPlay;
+ private int mStringIndex;
+ private boolean mDownDelivered;
+ private Key[] mAsciiKeys = new Key[256];
+ private boolean mPlaying;
+ private int mLastX;
+ private int mLastY;
+ private Paint mPaint;
+
+ @Override
+ public void setKeyboard(Keyboard k) {
+ super.setKeyboard(k);
+ if (DEBUG_AUTO_PLAY) {
+ findKeys();
+ if (mHandler2 == null) {
+ mHandler2 = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ removeMessages(MSG_TOUCH_DOWN);
+ removeMessages(MSG_TOUCH_UP);
+ if (mPlaying == false) return;
+
+ switch (msg.what) {
+ case MSG_TOUCH_DOWN:
+ if (mStringIndex >= mStringToPlay.length()) {
+ mPlaying = false;
+ return;
+ }
+ char c = mStringToPlay.charAt(mStringIndex);
+ while (c > 255 || mAsciiKeys[(int) c] == null) {
+ mStringIndex++;
+ if (mStringIndex >= mStringToPlay.length()) {
+ mPlaying = false;
+ return;
+ }
+ c = mStringToPlay.charAt(mStringIndex);
+ }
+ int x = mAsciiKeys[c].x + 10;
+ int y = mAsciiKeys[c].y + 26;
+ MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_DOWN, x, y, 0);
+ LatinKeyboardView.this.dispatchTouchEvent(me);
+ me.recycle();
+ sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else
+ // happens
+ mDownDelivered = true;
+ break;
+ case MSG_TOUCH_UP:
+ char cUp = mStringToPlay.charAt(mStringIndex);
+ int x2 = mAsciiKeys[cUp].x + 10;
+ int y2 = mAsciiKeys[cUp].y + 26;
+ mStringIndex++;
+
+ MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_UP, x2, y2, 0);
+ LatinKeyboardView.this.dispatchTouchEvent(me2);
+ me2.recycle();
+ sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else
+ // happens
+ mDownDelivered = false;
+ break;
+ }
+ }
+ };
+
+ }
+ }
+ }
+
+ private void findKeys() {
+ List<Key> keys = getKeyboard().getKeys();
+ // Get the keys on this keyboard
+ for (int i = 0; i < keys.size(); i++) {
+ int code = keys.get(i).codes[0];
+ if (code >= 0 && code <= 255) {
+ mAsciiKeys[code] = keys.get(i);
+ }
+ }
+ }
+
+ void startPlaying(String s) {
+ if (!DEBUG_AUTO_PLAY) return;
+ if (s == null) return;
+ mStringToPlay = s.toLowerCase();
+ mPlaying = true;
+ mDownDelivered = false;
+ mStringIndex = 0;
+ mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10);
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+ if (DEBUG_AUTO_PLAY && mPlaying) {
+ mHandler2.removeMessages(MSG_TOUCH_DOWN);
+ mHandler2.removeMessages(MSG_TOUCH_UP);
+ if (mDownDelivered) {
+ mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20);
+ } else {
+ mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20);
+ }
+ }
+ if (DEBUG_LINE) {
+ if (mPaint == null) {
+ mPaint = new Paint();
+ mPaint.setColor(0x80FFFFFF);
+ mPaint.setAntiAlias(false);
+ }
+ c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint);
+ c.drawLine(0, mLastY, getWidth(), mLastY, mPaint);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
new file mode 100755
index 000000000..712b9cf37
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2008-2009 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.content.Context;
+import android.text.AutoText;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.android.inputmethod.latin.WordComposer;
+
+/**
+ * This class loads a dictionary and provides a list of suggestions for a given sequence of
+ * characters. This includes corrections and completions.
+ * @hide pending API Council Approval
+ */
+public class Suggest implements Dictionary.WordCallback {
+
+ public static final int CORRECTION_NONE = 0;
+ public static final int CORRECTION_BASIC = 1;
+ public static final int CORRECTION_FULL = 2;
+
+ private static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
+
+ private BinaryDictionary mMainDict;
+
+ private Dictionary mUserDictionary;
+
+ private Dictionary mAutoDictionary;
+
+ private Dictionary mContactsDictionary;
+
+ private int mPrefMaxSuggestions = 12;
+
+ private boolean mAutoTextEnabled;
+
+ private int[] mPriorities = new int[mPrefMaxSuggestions];
+ // Handle predictive correction for only the first 1280 characters for performance reasons
+ // If we support scripts that need latin characters beyond that, we should probably use some
+ // kind of a sparse array or language specific list with a mapping lookup table.
+ // 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of
+ // latin characters.
+ private int[] mNextLettersFrequencies = new int[1280];
+ private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
+ private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
+ private boolean mHaveCorrection;
+ private CharSequence mOriginalWord;
+ private String mLowerOriginalWord;
+ private boolean mCapitalize;
+
+ private int mCorrectionMode = CORRECTION_BASIC;
+
+
+ public Suggest(Context context, int dictionaryResId) {
+ mMainDict = new BinaryDictionary(context, dictionaryResId);
+ for (int i = 0; i < mPrefMaxSuggestions; i++) {
+ StringBuilder sb = new StringBuilder(32);
+ mStringPool.add(sb);
+ }
+ }
+
+ public void setAutoTextEnabled(boolean enabled) {
+ mAutoTextEnabled = enabled;
+ }
+
+ public int getCorrectionMode() {
+ return mCorrectionMode;
+ }
+
+ public void setCorrectionMode(int mode) {
+ mCorrectionMode = mode;
+ }
+
+ public boolean hasMainDictionary() {
+ return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
+ }
+
+ /**
+ * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
+ * before the main dictionary, if set.
+ */
+ public void setUserDictionary(Dictionary userDictionary) {
+ mUserDictionary = userDictionary;
+ }
+
+ /**
+ * Sets an optional contacts dictionary resource to be loaded.
+ */
+ public void setContactsDictionary(Dictionary userDictionary) {
+ mContactsDictionary = userDictionary;
+ }
+
+ public void setAutoDictionary(Dictionary autoDictionary) {
+ mAutoDictionary = autoDictionary;
+ }
+
+ /**
+ * Number of suggestions to generate from the input key sequence. This has
+ * to be a number between 1 and 100 (inclusive).
+ * @param maxSuggestions
+ * @throws IllegalArgumentException if the number is out of range
+ */
+ public void setMaxSuggestions(int maxSuggestions) {
+ if (maxSuggestions < 1 || maxSuggestions > 100) {
+ throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
+ }
+ mPrefMaxSuggestions = maxSuggestions;
+ mPriorities = new int[mPrefMaxSuggestions];
+ collectGarbage();
+ while (mStringPool.size() < mPrefMaxSuggestions) {
+ StringBuilder sb = new StringBuilder(32);
+ mStringPool.add(sb);
+ }
+ }
+
+ private boolean haveSufficientCommonality(String original, CharSequence suggestion) {
+ final int originalLength = original.length();
+ final int suggestionLength = suggestion.length();
+ final int minLength = Math.min(originalLength, suggestionLength);
+ if (minLength <= 2) return true;
+ int matching = 0;
+ int lessMatching = 0; // Count matches if we skip one character
+ int i;
+ for (i = 0; i < minLength; i++) {
+ final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i));
+ if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) {
+ matching++;
+ lessMatching++;
+ } else if (i + 1 < suggestionLength
+ && origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) {
+ lessMatching++;
+ }
+ }
+ matching = Math.max(matching, lessMatching);
+
+ if (minLength <= 4) {
+ return matching >= 2;
+ } else {
+ return matching > minLength / 2;
+ }
+ }
+
+ /**
+ * Returns a list of words that match the list of character codes passed in.
+ * This list will be overwritten the next time this function is called.
+ * @param a view for retrieving the context for AutoText
+ * @param codes the list of codes. Each list item contains an array of character codes
+ * in order of probability where the character at index 0 in the array has the highest
+ * probability.
+ * @return list of suggestions.
+ */
+ public List<CharSequence> getSuggestions(View view, WordComposer wordComposer,
+ boolean includeTypedWordIfValid) {
+ mHaveCorrection = false;
+ mCapitalize = wordComposer.isCapitalized();
+ collectGarbage();
+ Arrays.fill(mPriorities, 0);
+ Arrays.fill(mNextLettersFrequencies, 0);
+
+ // Save a lowercase version of the original word
+ mOriginalWord = wordComposer.getTypedWord();
+ if (mOriginalWord != null) {
+ mOriginalWord = mOriginalWord.toString();
+ mLowerOriginalWord = mOriginalWord.toString().toLowerCase();
+ } else {
+ mLowerOriginalWord = "";
+ }
+ // Search the dictionary only if there are at least 2 characters
+ if (wordComposer.size() > 1) {
+ if (mUserDictionary != null || mContactsDictionary != null) {
+ if (mUserDictionary != null) {
+ mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
+ }
+ if (mContactsDictionary != null) {
+ mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
+ }
+
+ if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)
+ && mCorrectionMode == CORRECTION_FULL) {
+ mHaveCorrection = true;
+ }
+ }
+ mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
+ if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) {
+ mHaveCorrection = true;
+ }
+ }
+ if (mOriginalWord != null) {
+ mSuggestions.add(0, mOriginalWord.toString());
+ }
+
+ // Check if the first suggestion has a minimum number of characters in common
+ if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 1) {
+ if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) {
+ mHaveCorrection = false;
+ }
+ }
+
+ if (mAutoTextEnabled) {
+ int i = 0;
+ int max = 6;
+ // Don't autotext the suggestions from the dictionaries
+ if (mCorrectionMode == CORRECTION_BASIC) max = 1;
+ while (i < mSuggestions.size() && i < max) {
+ String suggestedWord = mSuggestions.get(i).toString().toLowerCase();
+ CharSequence autoText =
+ AutoText.get(suggestedWord, 0, suggestedWord.length(), view);
+ // Is there an AutoText correction?
+ boolean canAdd = autoText != null;
+ // 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?
+ if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) {
+ canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1));
+ }
+ if (canAdd) {
+ mHaveCorrection = true;
+ mSuggestions.add(i + 1, autoText);
+ i++;
+ }
+ i++;
+ }
+ }
+
+ removeDupes();
+ return mSuggestions;
+ }
+
+ public int[] getNextLettersFrequencies() {
+ return mNextLettersFrequencies;
+ }
+
+ private void removeDupes() {
+ final ArrayList<CharSequence> suggestions = mSuggestions;
+ if (suggestions.size() < 2) return;
+ int i = 1;
+ // Don't cache suggestions.size(), since we may be removing items
+ while (i < suggestions.size()) {
+ final CharSequence cur = suggestions.get(i);
+ // Compare each candidate with each previous candidate
+ for (int j = 0; j < i; j++) {
+ CharSequence previous = suggestions.get(j);
+ if (TextUtils.equals(cur, previous)) {
+ removeFromSuggestions(i);
+ i--;
+ break;
+ }
+ }
+ i++;
+ }
+ }
+
+ private void removeFromSuggestions(int index) {
+ CharSequence garbage = mSuggestions.remove(index);
+ if (garbage != null && garbage instanceof StringBuilder) {
+ mStringPool.add(garbage);
+ }
+ }
+
+ public boolean hasMinimalCorrection() {
+ return mHaveCorrection;
+ }
+
+ private boolean compareCaseInsensitive(final String mLowerOriginalWord,
+ final char[] word, final int offset, final int length) {
+ final int originalLength = mLowerOriginalWord.length();
+ if (originalLength == length && Character.isUpperCase(word[offset])) {
+ for (int i = 0; i < originalLength; i++) {
+ if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public boolean addWord(final char[] word, final int offset, final int length, final int freq) {
+ int pos = 0;
+ final int[] priorities = mPriorities;
+ final int prefMaxSuggestions = mPrefMaxSuggestions;
+ // Check if it's the same word, only caps are different
+ if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) {
+ pos = 0;
+ } else {
+ // Check the last one's priority and bail
+ if (priorities[prefMaxSuggestions - 1] >= freq) return true;
+ while (pos < prefMaxSuggestions) {
+ if (priorities[pos] < freq
+ || (priorities[pos] == freq && length < mSuggestions
+ .get(pos).length())) {
+ break;
+ }
+ pos++;
+ }
+ }
+
+ if (pos >= prefMaxSuggestions) {
+ return true;
+ }
+ 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)
+ : new StringBuilder(32);
+ sb.setLength(0);
+ if (mCapitalize) {
+ sb.append(Character.toUpperCase(word[offset]));
+ if (length > 1) {
+ sb.append(word, offset + 1, length - 1);
+ }
+ } else {
+ sb.append(word, offset, length);
+ }
+ mSuggestions.add(pos, sb);
+ if (mSuggestions.size() > prefMaxSuggestions) {
+ CharSequence garbage = mSuggestions.remove(prefMaxSuggestions);
+ if (garbage instanceof StringBuilder) {
+ mStringPool.add(garbage);
+ }
+ }
+ return true;
+ }
+
+ public boolean isValidWord(final CharSequence word) {
+ if (word == null || word.length() == 0) {
+ return false;
+ }
+ return (mCorrectionMode == CORRECTION_FULL && mMainDict.isValidWord(word))
+ || (mCorrectionMode > CORRECTION_NONE &&
+ ((mUserDictionary != null && mUserDictionary.isValidWord(word)))
+ || (mAutoDictionary != null && mAutoDictionary.isValidWord(word))
+ || (mContactsDictionary != null && mContactsDictionary.isValidWord(word)));
+ }
+
+ private void collectGarbage() {
+ int poolSize = mStringPool.size();
+ int garbageSize = mSuggestions.size();
+ while (poolSize < mPrefMaxSuggestions && garbageSize > 0) {
+ CharSequence garbage = mSuggestions.get(garbageSize - 1);
+ if (garbage != null && garbage instanceof StringBuilder) {
+ mStringPool.add(garbage);
+ poolSize++;
+ }
+ garbageSize--;
+ }
+ if (poolSize == mPrefMaxSuggestions + 1) {
+ Log.w("Suggest", "String pool got too big: " + poolSize);
+ }
+ mSuggestions.clear();
+ }
+
+ public void close() {
+ if (mMainDict != null) {
+ mMainDict.close();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java
new file mode 100644
index 000000000..c5e8ad9a1
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/TextEntryState.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2008-2009 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.content.Context;
+import android.text.format.DateFormat;
+import android.util.Log;
+
+import android.inputmethodservice.Keyboard.Key;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Calendar;
+
+public class TextEntryState {
+
+ private static boolean LOGGING = false;
+
+ private static int sBackspaceCount = 0;
+
+ private static int sAutoSuggestCount = 0;
+
+ private static int sAutoSuggestUndoneCount = 0;
+
+ private static int sManualSuggestCount = 0;
+
+ private static int sWordNotInDictionaryCount = 0;
+
+ private static int sSessionCount = 0;
+
+ private static int sTypedChars;
+
+ private static int sActualChars;
+
+ private static final String[] STATES = {
+ "Unknown",
+ "Start",
+ "In word",
+ "Accepted default",
+ "Picked suggestion",
+ "Punc. after word",
+ "Punc. after accepted",
+ "Space after accepted",
+ "Space after picked",
+ "Undo commit"
+ };
+
+ public static final int STATE_UNKNOWN = 0;
+ public static final int STATE_START = 1;
+ public static final int STATE_IN_WORD = 2;
+ public static final int STATE_ACCEPTED_DEFAULT = 3;
+ public static final int STATE_PICKED_SUGGESTION = 4;
+ public static final int STATE_PUNCTUATION_AFTER_WORD = 5;
+ public static final int STATE_PUNCTUATION_AFTER_ACCEPTED = 6;
+ public static final int STATE_SPACE_AFTER_ACCEPTED = 7;
+ public static final int STATE_SPACE_AFTER_PICKED = 8;
+ public static final int STATE_UNDO_COMMIT = 9;
+
+ private static int sState = STATE_UNKNOWN;
+
+ private static FileOutputStream sKeyLocationFile;
+ private static FileOutputStream sUserActionFile;
+
+ public static void newSession(Context context) {
+ sSessionCount++;
+ sAutoSuggestCount = 0;
+ sBackspaceCount = 0;
+ sAutoSuggestUndoneCount = 0;
+ sManualSuggestCount = 0;
+ sWordNotInDictionaryCount = 0;
+ sTypedChars = 0;
+ sActualChars = 0;
+ sState = STATE_START;
+
+ if (LOGGING) {
+ try {
+ sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND);
+ sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND);
+ } catch (IOException ioe) {
+ Log.e("TextEntryState", "Couldn't open file for output: " + ioe);
+ }
+ }
+ }
+
+ public static void endSession() {
+ if (sKeyLocationFile == null) {
+ return;
+ }
+ try {
+ sKeyLocationFile.close();
+ // Write to log file
+ // Write timestamp, settings,
+ String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime())
+ .toString()
+ + " BS: " + sBackspaceCount
+ + " auto: " + sAutoSuggestCount
+ + " manual: " + sManualSuggestCount
+ + " typed: " + sWordNotInDictionaryCount
+ + " undone: " + sAutoSuggestUndoneCount
+ + " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars)
+ + "\n";
+ sUserActionFile.write(out.getBytes());
+ sUserActionFile.close();
+ sKeyLocationFile = null;
+ sUserActionFile = null;
+ } catch (IOException ioe) {
+
+ }
+ }
+
+ public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) {
+ if (typedWord == null) return;
+ if (!typedWord.equals(actualWord)) {
+ sAutoSuggestCount++;
+ }
+ sTypedChars += typedWord.length();
+ sActualChars += actualWord.length();
+ sState = STATE_ACCEPTED_DEFAULT;
+ }
+
+ public static void acceptedTyped(CharSequence typedWord) {
+ sWordNotInDictionaryCount++;
+ sState = STATE_PICKED_SUGGESTION;
+ }
+
+ public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) {
+ sManualSuggestCount++;
+ if (typedWord.equals(actualWord)) {
+ acceptedTyped(typedWord);
+ }
+ sState = STATE_PICKED_SUGGESTION;
+ }
+
+ public static void typedCharacter(char c, boolean isSeparator) {
+ boolean isSpace = c == ' ';
+ switch (sState) {
+ case STATE_IN_WORD:
+ if (isSpace || isSeparator) {
+ sState = STATE_START;
+ } else {
+ // State hasn't changed.
+ }
+ break;
+ case STATE_ACCEPTED_DEFAULT:
+ case STATE_SPACE_AFTER_PICKED:
+ if (isSpace) {
+ sState = STATE_SPACE_AFTER_ACCEPTED;
+ } else if (isSeparator) {
+ sState = STATE_PUNCTUATION_AFTER_ACCEPTED;
+ } else {
+ sState = STATE_IN_WORD;
+ }
+ break;
+ case STATE_PICKED_SUGGESTION:
+ if (isSpace) {
+ sState = STATE_SPACE_AFTER_PICKED;
+ } else if (isSeparator) {
+ // Swap
+ sState = STATE_PUNCTUATION_AFTER_ACCEPTED;
+ } else {
+ sState = STATE_IN_WORD;
+ }
+ break;
+ case STATE_START:
+ case STATE_UNKNOWN:
+ case STATE_SPACE_AFTER_ACCEPTED:
+ case STATE_PUNCTUATION_AFTER_ACCEPTED:
+ case STATE_PUNCTUATION_AFTER_WORD:
+ if (!isSpace && !isSeparator) {
+ sState = STATE_IN_WORD;
+ } else {
+ sState = STATE_START;
+ }
+ break;
+ case STATE_UNDO_COMMIT:
+ if (isSpace || isSeparator) {
+ sState = STATE_ACCEPTED_DEFAULT;
+ } else {
+ sState = STATE_IN_WORD;
+ }
+ }
+ }
+
+ public static void backspace() {
+ if (sState == STATE_ACCEPTED_DEFAULT) {
+ sState = STATE_UNDO_COMMIT;
+ sAutoSuggestUndoneCount++;
+ } else if (sState == STATE_UNDO_COMMIT) {
+ sState = STATE_IN_WORD;
+ }
+ sBackspaceCount++;
+ }
+
+ public static void reset() {
+ sState = STATE_START;
+ }
+
+ public static int getState() {
+ return sState;
+ }
+
+ public static void keyPressedAt(Key key, int x, int y) {
+ if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) {
+ String out =
+ "KEY: " + (char) key.codes[0]
+ + " X: " + x
+ + " Y: " + y
+ + " MX: " + (key.x + key.width / 2)
+ + " MY: " + (key.y + key.height / 2)
+ + "\n";
+ try {
+ sKeyLocationFile.write(out.getBytes());
+ } catch (IOException ioe) {
+ // TODO: May run out of space
+ }
+ }
+ }
+}
+
diff --git a/java/src/com/android/inputmethod/latin/Tutorial.java b/java/src/com/android/inputmethod/latin/Tutorial.java
new file mode 100644
index 000000000..03d4858c4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Tutorial.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2008-2009 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.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Layout;
+import android.text.SpannableStringBuilder;
+import android.text.StaticLayout;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+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 long mStartTime;
+ private static final long MINIMUM_TIME = 6000;
+ private static final long MAXIMUM_TIME = 20000;
+ private View mInputView;
+ private LatinIME mIme;
+ private int[] mLocation = new int[2];
+ private int mBubblePointerOffset;
+
+ private static final int MSG_SHOW_BUBBLE = 0;
+
+ private int mBubbleIndex;
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SHOW_BUBBLE:
+ Bubble bubba = (Bubble) msg.obj;
+ bubba.show(mLocation[0], mLocation[1]);
+ break;
+ }
+ }
+ };
+
+ 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,
+ int backgroundResource, int bx, int by, int textResource1, int textResource2) {
+ bubbleBackground = context.getResources().getDrawable(backgroundResource);
+ x = bx;
+ y = by;
+ width = (int) (inputView.getWidth() * 0.9);
+ this.gravity = Gravity.TOP | Gravity.LEFT;
+ text = new SpannableStringBuilder()
+ .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);
+ LayoutInflater inflate =
+ (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ textView = (TextView) inflate.inflate(R.layout.bubble_text, null);
+ textView.setBackgroundDrawable(bubbleBackground);
+ textView.setText(text);
+ //textView.setText(textResource1);
+ window.setContentView(textView);
+ window.setFocusable(false);
+ window.setTouchable(true);
+ window.setOutsideTouchable(false);
+ }
+
+ private int chooseSize(PopupWindow pop, View parentView, CharSequence text, TextView tv) {
+ int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ /*
+ * Figure out how big the text would be if we laid it out to the
+ * full width of this view minus the border.
+ */
+ int cap = width - wid;
+
+ Layout l = new StaticLayout(text, tv.getPaint(), cap,
+ Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+ float max = 0;
+ for (int i = 0; i < l.getLineCount(); i++) {
+ max = Math.max(max, l.getLineWidth(i));
+ }
+
+ /*
+ * Now set the popup size to be big enough for the text plus the border.
+ */
+ pop.setWidth(width);
+ pop.setHeight(ht + l.getHeight());
+ return l.getHeight();
+ }
+
+ void show(int offx, int offy) {
+ int textHeight = chooseSize(window, inputView, text, textView);
+ offy -= textView.getPaddingTop() + textHeight;
+ if (inputView.getVisibility() == View.VISIBLE
+ && inputView.getWindowVisibility() == View.VISIBLE) {
+ try {
+ if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) offy -= window.getHeight();
+ if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) offx -= window.getWidth();
+ textView.setOnTouchListener(new View.OnTouchListener() {
+ public boolean onTouch(View view, MotionEvent me) {
+ Tutorial.this.next();
+ return true;
+ }
+ });
+ window.showAtLocation(inputView, Gravity.NO_GRAVITY, x + offx, y + offy);
+ } catch (Exception e) {
+ // Input view is not valid
+ }
+ }
+ }
+
+ void hide() {
+ if (window.isShowing()) {
+ textView.setOnTouchListener(null);
+ window.dismiss();
+ }
+ }
+
+ boolean isShowing() {
+ return window.isShowing();
+ }
+ }
+
+ public Tutorial(LatinIME ime, LatinKeyboardView inputView) {
+ Context context = inputView.getContext();
+ mIme = ime;
+ int inputWidth = inputView.getWidth();
+ final int x = inputWidth / 20; // Half of 1/10th
+ mBubblePointerOffset = inputView.getContext().getResources()
+ .getDimensionPixelOffset(R.dimen.bubble_pointer_offset);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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;
+ }
+
+ void start() {
+ mInputView.getLocationInWindow(mLocation);
+ mBubbleIndex = -1;
+ mInputView.setOnTouchListener(this);
+ next();
+ }
+
+ boolean next() {
+ if (mBubbleIndex >= 0) {
+ // If the bubble is not yet showing, don't move to the next.
+ if (!mBubbles.get(mBubbleIndex).isShowing()) {
+ return true;
+ }
+ // Hide all previous bubbles as well, as they may have had a delayed show
+ for (int i = 0; i <= mBubbleIndex; i++) {
+ mBubbles.get(i).hide();
+ }
+ }
+ mBubbleIndex++;
+ if (mBubbleIndex >= mBubbles.size()) {
+ mInputView.setOnTouchListener(null);
+ mIme.sendDownUpKeyEvents(-1); // Inform the setupwizard that tutorial is in last bubble
+ mIme.tutorialDone();
+ return false;
+ }
+ if (mBubbleIndex == 3 || mBubbleIndex == 4) {
+ mIme.mKeyboardSwitcher.toggleSymbols();
+ }
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(mBubbleIndex)), 500);
+ return true;
+ }
+
+ void hide() {
+ for (int i = 0; i < mBubbles.size(); i++) {
+ mBubbles.get(i).hide();
+ }
+ mInputView.setOnTouchListener(null);
+ }
+
+ boolean close() {
+ mHandler.removeMessages(MSG_SHOW_BUBBLE);
+ hide();
+ return true;
+ }
+
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ next();
+ }
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
new file mode 100644
index 000000000..4b98eacce
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserDictionary.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2008 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.UserDictionary.Words;
+
+public class UserDictionary extends ExpandableDictionary {
+
+ private static final String[] PROJECTION = {
+ Words._ID,
+ Words.WORD,
+ Words.FREQUENCY
+ };
+
+ private static final int INDEX_WORD = 1;
+ private static final int INDEX_FREQUENCY = 2;
+
+ private ContentObserver mObserver;
+
+ private boolean mRequiresReload;
+ private String mLocale;
+
+ public UserDictionary(Context context, String locale) {
+ super(context);
+ mLocale = locale;
+ // Perform a managed query. The Activity will handle closing and requerying the cursor
+ // when needed.
+ ContentResolver cres = context.getContentResolver();
+
+ cres.registerContentObserver(Words.CONTENT_URI, true, mObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean self) {
+ mRequiresReload = true;
+ }
+ });
+
+ loadDictionary();
+ }
+
+ public synchronized void close() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ private synchronized void loadDictionary() {
+ Cursor cursor = getContext().getContentResolver()
+ .query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)",
+ new String[] { mLocale }, null);
+ addWords(cursor);
+ mRequiresReload = false;
+ }
+
+ /**
+ * Adds a word to the dictionary and makes it persistent.
+ * @param word the word to add. If the word is capitalized, then the dictionary will
+ * recognize it as a capitalized word when searched.
+ * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
+ * the highest.
+ * @TODO use a higher or float range for frequency
+ */
+ @Override
+ public synchronized void addWord(String word, int frequency) {
+ if (mRequiresReload) loadDictionary();
+ // Safeguard against adding long words. Can cause stack overflow.
+ if (word.length() >= getMaxWordLength()) return;
+
+ super.addWord(word, frequency);
+
+ // Update the user dictionary provider
+ ContentValues values = new ContentValues(5);
+ values.put(Words.WORD, word);
+ values.put(Words.FREQUENCY, frequency);
+ values.put(Words.LOCALE, mLocale);
+ values.put(Words.APP_ID, 0);
+
+ getContext().getContentResolver().insert(Words.CONTENT_URI, values);
+ // In case the above does a synchronous callback of the change observer
+ mRequiresReload = false;
+ }
+
+ @Override
+ public synchronized void getWords(final WordComposer codes, final WordCallback callback,
+ int[] nextLettersFrequencies) {
+ if (mRequiresReload) loadDictionary();
+ super.getWords(codes, callback, nextLettersFrequencies);
+ }
+
+ @Override
+ public synchronized boolean isValidWord(CharSequence word) {
+ if (mRequiresReload) loadDictionary();
+ return super.isValidWord(word);
+ }
+
+ private void addWords(Cursor cursor) {
+ clearDictionary();
+
+ final int maxWordLength = getMaxWordLength();
+ if (cursor.moveToFirst()) {
+ while (!cursor.isAfterLast()) {
+ String word = cursor.getString(INDEX_WORD);
+ int frequency = cursor.getInt(INDEX_FREQUENCY);
+ // Safeguard against adding really long words. Stack may overflow due
+ // to recursion
+ if (word.length() < maxWordLength) {
+ super.addWord(word, frequency);
+ }
+ cursor.moveToNext();
+ }
+ }
+ cursor.close();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
new file mode 100644
index 000000000..e97cb24ba
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2008-2009 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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A place to store the currently composing word with information such as adjacent key codes as well
+ */
+public class WordComposer {
+ /**
+ * The list of unicode values for each keystroke (including surrounding keys)
+ */
+ private ArrayList<int[]> mCodes;
+
+ /**
+ * The word chosen from the candidate list, until it is committed.
+ */
+ private String mPreferredWord;
+
+ private StringBuilder mTypedWord;
+
+ private int mCapsCount;
+
+ private boolean mAutoCapitalized;
+
+ /**
+ * Whether the user chose to capitalize the word.
+ */
+ private boolean mIsCapitalized;
+
+ WordComposer() {
+ mCodes = new ArrayList<int[]>(12);
+ mTypedWord = new StringBuilder(20);
+ }
+
+ /**
+ * Clear out the keys registered so far.
+ */
+ public void reset() {
+ mCodes.clear();
+ mIsCapitalized = false;
+ mPreferredWord = null;
+ mTypedWord.setLength(0);
+ mCapsCount = 0;
+ }
+
+ /**
+ * Number of keystrokes in the composing word.
+ * @return the number of keystrokes
+ */
+ public int size() {
+ return mCodes.size();
+ }
+
+ /**
+ * Returns the codes at a particular position in the word.
+ * @param index the position in the word
+ * @return the unicode for the pressed and surrounding keys
+ */
+ public int[] getCodesAt(int index) {
+ return mCodes.get(index);
+ }
+
+ /**
+ * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
+ * the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
+ * @param codes the array of unicode values
+ */
+ public void add(int primaryCode, int[] codes) {
+ mTypedWord.append((char) primaryCode);
+ mCodes.add(codes);
+ if (Character.isUpperCase((char) primaryCode)) mCapsCount++;
+ }
+
+ /**
+ * Delete the last keystroke as a result of hitting backspace.
+ */
+ public void deleteLast() {
+ mCodes.remove(mCodes.size() - 1);
+ final int lastPos = mTypedWord.length() - 1;
+ char last = mTypedWord.charAt(lastPos);
+ mTypedWord.deleteCharAt(lastPos);
+ if (Character.isUpperCase(last)) mCapsCount--;
+ }
+
+ /**
+ * Returns the word as it was typed, without any correction applied.
+ * @return the word that was typed so far
+ */
+ public CharSequence getTypedWord() {
+ int wordSize = mCodes.size();
+ if (wordSize == 0) {
+ return null;
+ }
+// StringBuffer sb = new StringBuffer(wordSize);
+// for (int i = 0; i < wordSize; i++) {
+// char c = (char) mCodes.get(i)[0];
+// if (i == 0 && mIsCapitalized) {
+// c = Character.toUpperCase(c);
+// }
+// sb.append(c);
+// }
+// return sb;
+ return mTypedWord;
+ }
+
+ public void setCapitalized(boolean capitalized) {
+ mIsCapitalized = capitalized;
+ }
+
+ /**
+ * Whether or not the user typed a capital letter as the first letter in the word
+ * @return capitalization preference
+ */
+ public boolean isCapitalized() {
+ return mIsCapitalized;
+ }
+
+ /**
+ * Stores the user's selected word, before it is actually committed to the text field.
+ * @param preferred
+ */
+ public void setPreferredWord(String preferred) {
+ mPreferredWord = preferred;
+ }
+
+ /**
+ * Return the word chosen by the user, or the typed word if no other word was chosen.
+ * @return the preferred word
+ */
+ public CharSequence getPreferredWord() {
+ return mPreferredWord != null ? mPreferredWord : getTypedWord();
+ }
+
+ /**
+ * Returns true if more than one character is upper case, otherwise returns false.
+ */
+ public boolean isMostlyCaps() {
+ return mCapsCount > 1;
+ }
+
+ /**
+ * Saves the reason why the word is capitalized - whether it was automatic or
+ * due to the user hitting shift in the middle of a sentence.
+ * @param auto whether it was an automatic capitalization due to start of sentence
+ */
+ public void setAutoCapitalized(boolean auto) {
+ mAutoCapitalized = auto;
+ }
+
+ /**
+ * Returns whether the word was automatically capitalized.
+ * @return whether the word was automatically capitalized
+ */
+ public boolean isAutoCapitalized() {
+ return mAutoCapitalized;
+ }
+}