diff options
Diffstat (limited to 'java/src')
26 files changed, 4758 insertions, 615 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 93f1985ca..4fbb5b012 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -83,14 +83,14 @@ public class AutoDictionary extends ExpandableDictionary { sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE); } - private static DatabaseHelper mOpenHelper = null; + private static DatabaseHelper sOpenHelper = null; - public AutoDictionary(Context context, LatinIME ime, String locale) { - super(context); + public AutoDictionary(Context context, LatinIME ime, String locale, int dicTypeId) { + super(context, dicTypeId); mIme = ime; mLocale = locale; - if (mOpenHelper == null) { - mOpenHelper = new DatabaseHelper(getContext()); + if (sOpenHelper == null) { + sOpenHelper = new DatabaseHelper(getContext()); } if (mLocale != null && mLocale.length() > 1) { loadDictionary(); @@ -169,7 +169,7 @@ public class AutoDictionary extends ExpandableDictionary { // Nothing pending? Return if (mPendingWrites.isEmpty()) return; // Create a background thread to write the pending entries - new UpdateDbTask(getContext(), mOpenHelper, mPendingWrites, mLocale).execute(); + new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute(); // Create a new map for writing new entries into while the old one is written to db mPendingWrites = new HashMap<String, Integer>(); } @@ -209,7 +209,7 @@ public class AutoDictionary extends ExpandableDictionary { qb.setProjectionMap(sDictProjectionMap); // Get the database and run the query - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + SQLiteDatabase db = sOpenHelper.getReadableDatabase(); Cursor c = qb.query(db, null, selection, selectionArgs, null, null, DEFAULT_SORT_ORDER); return c; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 87de94b76..69c2b94f2 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -16,10 +16,15 @@ package com.android.inputmethod.latin; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.Channels; import java.util.Arrays; import android.content.Context; -import android.content.res.AssetManager; import android.util.Log; /** @@ -27,18 +32,33 @@ import android.util.Log; */ public class BinaryDictionary extends Dictionary { - public static final int MAX_WORD_LENGTH = 48; + /** + * There is difference between what java and native code can handle. + * This value should only be used in BinaryDictionary.java + * It is necessary to keep it at this value because some languages e.g. German have + * really long words. + */ + protected static final int MAX_WORD_LENGTH = 48; + + private static final String TAG = "BinaryDictionary"; private static final int MAX_ALTERNATIVES = 16; - private static final int MAX_WORDS = 16; + private static final int MAX_WORDS = 18; + private static final int MAX_BIGRAMS = 60; private static final int TYPED_LETTER_MULTIPLIER = 2; private static final boolean ENABLE_MISSED_CHARACTERS = true; + private int mDicTypeId; private int mNativeDict; - private int mDictLength; // This value is set from native code, don't change the name!!!! + private int mDictLength; private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; + private char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; private int[] mFrequencies = new int[MAX_WORDS]; + private int[] mFrequencies_bigrams = new int[MAX_BIGRAMS]; + // Keep a reference to the native dict direct buffer in Java to avoid + // unexpected deallocation of the direct buffer. + private ByteBuffer mNativeDictDirectBuffer; static { try { @@ -53,32 +73,122 @@ public class BinaryDictionary extends Dictionary { * @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) { + public BinaryDictionary(Context context, int[] resId, int dicTypeId) { + if (resId != null && resId.length > 0 && resId[0] != 0) { loadDictionary(context, resId); } + mDicTypeId = dicTypeId; + } + + /** + * Create a dictionary from a byte buffer. This is used for testing. + * @param context application context for reading resources + * @param byteBuffer a ByteBuffer containing the binary dictionary + */ + public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) { + if (byteBuffer != null) { + if (byteBuffer.isDirect()) { + mNativeDictDirectBuffer = byteBuffer; + } else { + mNativeDictDirectBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity()); + byteBuffer.rewind(); + mNativeDictDirectBuffer.put(byteBuffer); + } + mDictLength = byteBuffer.capacity(); + mNativeDict = openNative(mNativeDictDirectBuffer, + TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + } + mDicTypeId = dicTypeId; } - private native int openNative(AssetManager am, String resourcePath, int typedLetterMultiplier, + private native int openNative(ByteBuffer bb, 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); + char[] outputChars, int[] frequencies, int maxWordLength, int maxWords, + int maxAlternatives, int skipPos, int[] nextLettersFrequencies, int nextLettersSize); + private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, + int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies, + int maxWordLength, int maxBigrams, int maxAlternatives); + + private final void loadDictionary(Context context, int[] resId) { + InputStream[] is = null; + try { + // merging separated dictionary into one if dictionary is separated + int total = 0; + is = new InputStream[resId.length]; + for (int i = 0; i < resId.length; i++) { + is[i] = context.getResources().openRawResource(resId[i]); + total += is[i].available(); + } + + mNativeDictDirectBuffer = + ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder()); + int got = 0; + for (int i = 0; i < resId.length; i++) { + got += Channels.newChannel(is[i]).read(mNativeDictDirectBuffer); + } + if (got != total) { + Log.e(TAG, "Read " + got + " bytes, expected " + total); + } else { + mNativeDict = openNative(mNativeDictDirectBuffer, + TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + mDictLength = total; + } + } catch (IOException e) { + Log.w(TAG, "No available memory for binary dictionary"); + } finally { + try { + if (is != null) { + for (int i = 0; i < is.length; i++) { + is[i].close(); + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to close input stream"); + } + } + } + + + @Override + public void getBigrams(final WordComposer codes, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + + char[] chars = previousWord.toString().toCharArray(); + Arrays.fill(mOutputChars_bigrams, (char) 0); + Arrays.fill(mFrequencies_bigrams, 0); + + int codesSize = codes.size(); + Arrays.fill(mInputCodes, -1); + int[] alternatives = codes.getCodesAt(0); + System.arraycopy(alternatives, 0, mInputCodes, 0, + Math.min(alternatives.length, MAX_ALTERNATIVES)); + + int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize, + mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS, + MAX_ALTERNATIVES); + + for (int j = 0; j < count; j++) { + if (mFrequencies_bigrams[j] < 1) break; + int start = j * MAX_WORD_LENGTH; + int len = 0; + while (mOutputChars_bigrams[start + len] != 0) { + len++; + } + if (len > 0) { + callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j], + mDicTypeId, DataType.BIGRAM); + } + } } @Override public void getWords(final WordComposer codes, final WordCallback callback, int[] nextLettersFrequencies) { final int codesSize = codes.size(); - // Wont deal with really long words. + // Won't deal with really long words. if (codesSize > MAX_WORD_LENGTH - 1) return; Arrays.fill(mInputCodes, -1); @@ -119,7 +229,8 @@ public class BinaryDictionary extends Dictionary { len++; } if (len > 0) { - callback.addWord(mOutputChars, start, len, mFrequencies[j]); + callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId, + DataType.UNIGRAM); } } } diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index 4dc61d4a4..7fcc3d532 100755 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -83,7 +83,6 @@ public class CandidateView extends View { private int mDescent; private boolean mScrolled; private boolean mShowingAddToDictionary; - private CharSequence mWordToAddToDictionary; private CharSequence mAddToDictionaryHint; private int mTargetScrollX; @@ -144,9 +143,13 @@ public class CandidateView extends View { mPaint.setStrokeWidth(0); mPaint.setTextAlign(Align.CENTER); mDescent = (int) mPaint.descent(); - // 80 pixels for a 160dpi device would mean half an inch + // 50 pixels for a 160dpi device would mean about 0.3 inch mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50); + // Slightly reluctant to scroll to be able to easily choose the suggestion + // 50 pixels for a 160dpi device would mean about 0.3 inch + final int touchSlop = (int) (getResources().getDisplayMetrics().density * 50); + final int touchSlopSquare = touchSlop * touchSlop; mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent me) { @@ -160,6 +163,13 @@ public class CandidateView extends View { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + final int deltaX = (int) (e2.getX() - e1.getX()); + final int deltaY = (int) (e2.getY() - e1.getY()); + final int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance < touchSlopSquare) { + return false; + } + final int width = getWidth(); mScrolled = true; int scrollX = getScrollX(); @@ -167,7 +177,7 @@ public class CandidateView extends View { if (scrollX < 0) { scrollX = 0; } - if (distanceX > 0 && scrollX + width > mTotalWidth) { + if (distanceX > 0 && scrollX + width > mTotalWidth) { scrollX -= (int) distanceX; } mTargetScrollX = scrollX; @@ -219,8 +229,7 @@ public class CandidateView extends View { mDivider.getIntrinsicHeight()); } int x = 0; - final int count = mSuggestions.size(); - final int width = getWidth(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); final Rect bgPadding = mBgPadding; final Paint paint = mPaint; final int touchX = mTouchX; @@ -325,7 +334,6 @@ public class CandidateView extends View { } public void showAddToDictionaryHint(CharSequence word) { - mWordToAddToDictionary = word; ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>(); suggestions.add(word); suggestions.add(mAddToDictionaryHint); @@ -341,7 +349,7 @@ public class CandidateView extends View { public void scrollPrev() { int i = 0; - final int count = mSuggestions.size(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int firstItem = 0; // Actually just before the first item, if at the boundary while (i < count) { if (mWordX[i] < getScrollX() @@ -360,7 +368,7 @@ public class CandidateView extends View { int i = 0; int scrollX = getScrollX(); int targetX = scrollX; - final int count = mSuggestions.size(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int rightEdge = scrollX + getWidth(); while (i < count) { if (mWordX[i] <= rightEdge && @@ -382,8 +390,14 @@ public class CandidateView extends View { mScrolled = true; } } - + + /* package */ List<CharSequence> getSuggestions() { + return mSuggestions; + } + public void clear() { + // Don't call mSuggestions.clear() because it's being used for logging + // in LatinIME.pickSuggestionManually(). mSuggestions = EMPTY_LIST; mTouchX = OUT_OF_BOUNDS; mSelectedString = null; @@ -418,7 +432,11 @@ public class CandidateView extends View { if (y <= 0) { // Fling up!? if (mSelectedString != null) { + // If there are completions from the application, we don't change the state to + // STATE_PICKED_SUGGESTION if (!mShowingCompletions) { + // This "acceptedSuggestion" will not be counted as a word because + // it will be counted in pickSuggestion instead. TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); } @@ -453,25 +471,6 @@ public class CandidateView extends View { } 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; diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java index 15edb706a..ab75868cf 100644 --- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -20,9 +20,10 @@ 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; +import android.text.TextUtils; +import android.util.Log; public class ContactsDictionary extends ExpandableDictionary { @@ -31,27 +32,35 @@ public class ContactsDictionary extends ExpandableDictionary { Contacts.DISPLAY_NAME, }; + /** + * Frequency for contacts information into the dictionary + */ + private static final int FREQUENCY_FOR_CONTACTS = 128; + private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90; + private static final int INDEX_NAME = 1; private ContentObserver mObserver; private long mLastLoadedContacts; - public ContactsDictionary(Context context) { - super(context); + public ContactsDictionary(Context context, int dicTypeId) { + super(context, dicTypeId); // 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) { - setRequiresReload(true); - } - }); + cres.registerContentObserver( + Contacts.CONTENT_URI, true,mObserver = new ContentObserver(null) { + @Override + public void onChange(boolean self) { + setRequiresReload(true); + } + }); loadDictionary(); } + @Override public synchronized void close() { if (mObserver != null) { getContext().getContentResolver().unregisterContentObserver(mObserver); @@ -89,6 +98,7 @@ public class ContactsDictionary extends ExpandableDictionary { if (name != null) { int len = name.length(); + String prevWord = null; // TODO: Better tokenization for non-Latin writing systems for (int i = 0; i < len; i++) { @@ -112,7 +122,13 @@ public class ContactsDictionary extends ExpandableDictionary { // capitalization of i. final int wordLen = word.length(); if (wordLen < maxWordLength && wordLen > 1) { - super.addWord(word, 128); + super.addWord(word, FREQUENCY_FOR_CONTACTS); + if (!TextUtils.isEmpty(prevWord)) { + // TODO Do not add email address + // Not so critical + super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM); + } + prevWord = word; } } } diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index e7b526663..d04bf57a7 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -21,7 +21,6 @@ package com.android.inputmethod.latin; * strokes. */ abstract public class Dictionary { - /** * Whether or not to replicate the typed word in the suggested list, even if it's valid. */ @@ -31,7 +30,11 @@ abstract public class Dictionary { * 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; - + + public static enum DataType { + UNIGRAM, BIGRAM + } + /** * Interface to be implemented by classes requesting words to be fetched from the dictionary. * @see #getWords(WordComposer, WordCallback) @@ -45,9 +48,12 @@ abstract public class Dictionary { * @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 + * @param dicTypeId of the dictionary where word was from + * @param dataType tells type of this data * @return true if the word was added, false if no more words are required */ - boolean addWord(char[] word, int wordOffset, int wordLength, int frequency); + boolean addWord(char[] word, int wordOffset, int wordLength, int frequency, int dicTypeId, + DataType dataType); } /** @@ -65,6 +71,21 @@ abstract public class Dictionary { int[] nextLettersFrequencies); /** + * Searches for pairs in the bigram dictionary that matches the previous word and all the + * possible words following are added through the callback object. + * @param composer the key sequence to match + * @param callback the callback object to send possible word following previous word + * @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. + */ + public void getBigrams(final WordComposer composer, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + // empty base implementation + } + + /** * 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 diff --git a/java/src/com/android/inputmethod/voice/EditingUtil.java b/java/src/com/android/inputmethod/latin/EditingUtil.java index 6316d8ccf..be31cb787 100644 --- a/java/src/com/android/inputmethod/voice/EditingUtil.java +++ b/java/src/com/android/inputmethod/latin/EditingUtil.java @@ -14,16 +14,23 @@ * the License. */ -package com.android.inputmethod.voice; +package com.android.inputmethod.latin; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; +import java.util.regex.Pattern; + /** * Utility methods to deal with editing text through an InputConnection. */ public class EditingUtil { + /** + * Number of characters we want to look back in order to identify the previous word + */ + private static final int LOOKBACK_CHARACTER_NUM = 15; + private EditingUtil() {}; /** @@ -75,9 +82,21 @@ public class EditingUtil { * represents the cursor, then "hello " will be returned. */ public static String getWordAtCursor( - InputConnection connection, String separators) { - Range range = getWordRangeAtCursor(connection, separators); - return (range == null) ? null : range.word; + InputConnection connection, String separators) { + return getWordAtCursor(connection, separators, null); + } + + /** + * @param connection connection to the current text field. + * @param sep characters which may separate words + * @return the word that surrounds the cursor, including up to one trailing + * separator. For example, if the field contains "he|llo world", where | + * represents the cursor, then "hello " will be returned. + */ + public static String getWordAtCursor( + InputConnection connection, String separators, Range range) { + Range r = getWordRangeAtCursor(connection, separators, range); + return (r == null) ? null : r.word; } /** @@ -87,7 +106,7 @@ public class EditingUtil { public static void deleteWordAtCursor( InputConnection connection, String separators) { - Range range = getWordRangeAtCursor(connection, separators); + Range range = getWordRangeAtCursor(connection, separators, null); if (range == null) return; connection.finishComposingText(); @@ -101,18 +120,20 @@ public class EditingUtil { /** * Represents a range of text, relative to the current cursor position. */ - private static class Range { + public static class Range { /** Characters before selection start */ - int charsBefore; + public int charsBefore; /** * Characters after selection start, including one trailing word * separator. */ - int charsAfter; + public int charsAfter; /** The actual characters that make up a word */ - String word; + public String word; + + public Range() {} public Range(int charsBefore, int charsAfter, String word) { if (charsBefore < 0 || charsAfter < 0) { @@ -125,7 +146,7 @@ public class EditingUtil { } private static Range getWordRangeAtCursor( - InputConnection connection, String sep) { + InputConnection connection, String sep, Range range) { if (connection == null || sep == null) { return null; } @@ -137,20 +158,22 @@ public class EditingUtil { // Find first word separator before the cursor int start = before.length(); - while (--start > 0 && !isWhitespace(before.charAt(start - 1), sep)); + while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--; // Find last word separator after the cursor int end = -1; while (++end < after.length() && !isWhitespace(after.charAt(end), sep)); - if (end < after.length() - 1) { - end++; // Include trailing space, if it exists, in word - } int cursor = getCursorPosition(connection); if (start >= 0 && cursor + end <= after.length() + before.length()) { String word = before.toString().substring(start, before.length()) - + after.toString().substring(0, end); - return new Range(before.length() - start, end, word); + + after.toString().substring(0, end); + + Range returnRange = range != null? range : new Range(); + returnRange.charsBefore = before.length() - start; + returnRange.charsAfter = end; + returnRange.word = word; + return returnRange; } return null; @@ -159,4 +182,48 @@ public class EditingUtil { private static boolean isWhitespace(int code, String whitespace) { return whitespace.contains(String.valueOf((char) code)); } + + private static final Pattern spaceRegex = Pattern.compile("\\s+"); + + public static CharSequence getPreviousWord(InputConnection connection, + String sentenceSeperators) { + //TODO: Should fix this. This could be slow! + CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + if (prev == null) { + return null; + } + String[] w = spaceRegex.split(prev); + if (w.length >= 2 && w[w.length-2].length() > 0) { + char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1); + if (sentenceSeperators.contains(String.valueOf(lastChar))) { + return null; + } + return w[w.length-2]; + } else { + return null; + } + } + + /** + * Checks if the cursor is touching/inside a word or the selection is for a whole + * word and no more and no less. + * @param range the Range object that contains the bounds of the word around the cursor + * @param start the start of the selection + * @param end the end of the selection, which could be the same as the start, if text is not + * in selection mode + * @return false if the selection is a partial word or straddling multiple words, true if + * the selection is a full word or there is no selection. + */ + public static boolean isFullWordOrInside(Range range, int start, int end) { + // Is the cursor inside or touching a word? + if (start == end) return true; + + // Is it a selection? Then is the start of the selection the start of the word and + // the size of the selection the size of the word? Then return true + if (start < end + && (range.charsBefore == 0 && range.charsAfter == end - start)) { + return true; + } + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 46bc41c42..e954c0818 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -16,24 +16,30 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.Dictionary.WordCallback; +import java.util.LinkedList; import android.content.Context; import android.os.AsyncTask; -import android.os.SystemClock; /** * 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 { + /** + * There is difference between what java and native code can handle. + * It uses 32 because Java stack overflows when greater value is used. + */ + protected static final int MAX_WORD_LENGTH = 32; + private Context mContext; private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; + private int mDicTypeId; private int mMaxDepth; private int mInputLength; private int[] mNextLettersFrequencies; + private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH); - public static final int MAX_WORD_LENGTH = 32; private static final char QUOTE = '\''; private boolean mRequiresReload; @@ -47,7 +53,9 @@ public class ExpandableDictionary extends Dictionary { char code; int frequency; boolean terminal; + Node parent; NodeArray children; + LinkedList<NextWord> ngrams; // Supports ngram } static class NodeArray { @@ -71,14 +79,27 @@ public class ExpandableDictionary extends Dictionary { } } + static class NextWord { + Node word; + NextWord nextWord; + int frequency; + + NextWord(Node word, int frequency) { + this.word = word; + this.frequency = frequency; + } + } + + private NodeArray mRoots; private int[][] mCodes; - ExpandableDictionary(Context context) { + ExpandableDictionary(Context context, int dicTypeId) { mContext = context; clearDictionary(); mCodes = new int[MAX_WORD_LENGTH][]; + mDicTypeId = dicTypeId; } public void loadDictionary() { @@ -118,12 +139,11 @@ public class ExpandableDictionary extends Dictionary { } public void addWord(String word, int frequency) { - addWordRec(mRoots, word, 0, frequency); + addWordRec(mRoots, word, 0, frequency, null); } - private void addWordRec(NodeArray children, final String word, - final int depth, final int frequency) { - + private void addWordRec(NodeArray children, final String word, final int depth, + final int frequency, Node parentNode) { final int wordLength = word.length(); final char c = word.charAt(depth); // Does children have the current character? @@ -140,6 +160,7 @@ public class ExpandableDictionary extends Dictionary { if (!found) { childNode = new Node(); childNode.code = c; + childNode.parent = parentNode; children.add(childNode); } if (wordLength == depth + 1) { @@ -152,7 +173,7 @@ public class ExpandableDictionary extends Dictionary { if (childNode.children == null) { childNode.children = new NodeArray(); } - addWordRec(childNode.children, word, depth + 1, frequency); + addWordRec(childNode.children, word, depth + 1, frequency, childNode); } @Override @@ -186,7 +207,7 @@ public class ExpandableDictionary extends Dictionary { if (mRequiresReload) startDictionaryLoadingTaskLocked(); if (mUpdatingDictionary) return false; } - final int freq = getWordFrequencyRec(mRoots, word, 0, word.length()); + final int freq = getWordFrequency(word); return freq > -1; } @@ -194,32 +215,8 @@ public class ExpandableDictionary extends Dictionary { * 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; + Node node = searchNode(mRoots, word, 0, word.length()); + return (node == null) ? -1 : node.frequency; } /** @@ -267,7 +264,8 @@ public class ExpandableDictionary extends Dictionary { if (completion) { word[depth] = c; if (terminal) { - if (!callback.addWord(word, 0, depth + 1, freq * snr)) { + if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId, + DataType.UNIGRAM)) { return; } // Add to frequency of next letters for predictive correction @@ -305,7 +303,8 @@ public class ExpandableDictionary extends Dictionary { || !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); + callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, + DataType.UNIGRAM); } } if (children != null) { @@ -324,6 +323,171 @@ public class ExpandableDictionary extends Dictionary { } } + protected int setBigram(String word1, String word2, int frequency) { + return addOrSetBigram(word1, word2, frequency, false); + } + + protected int addBigram(String word1, String word2, int frequency) { + return addOrSetBigram(word1, word2, frequency, true); + } + + /** + * Adds bigrams to the in-memory trie structure that is being used to retrieve any word + * @param frequency frequency for this bigrams + * @param addFrequency if true, it adds to current frequency + * @return returns the final frequency + */ + private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) { + Node firstWord = searchWord(mRoots, word1, 0, null); + Node secondWord = searchWord(mRoots, word2, 0, null); + LinkedList<NextWord> bigram = firstWord.ngrams; + if (bigram == null || bigram.size() == 0) { + firstWord.ngrams = new LinkedList<NextWord>(); + bigram = firstWord.ngrams; + } else { + for (NextWord nw : bigram) { + if (nw.word == secondWord) { + if (addFrequency) { + nw.frequency += frequency; + } else { + nw.frequency = frequency; + } + return nw.frequency; + } + } + } + NextWord nw = new NextWord(secondWord, frequency); + firstWord.ngrams.add(nw); + return frequency; + } + + /** + * Searches for the word and add the word if it does not exist. + * @return Returns the terminal node of the word we are searching for. + */ + private Node searchWord(NodeArray children, String word, int depth, Node parentNode) { + 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; + childNode.parent = parentNode; + children.add(childNode); + } + if (wordLength == depth + 1) { + // Terminate this word + childNode.terminal = true; + return childNode; + } + if (childNode.children == null) { + childNode.children = new NodeArray(); + } + return searchWord(childNode.children, word, depth + 1, childNode); + } + + // @VisibleForTesting + boolean reloadDictionaryIfRequired() { + synchronized (mUpdatingLock) { + // If we need to update, start off a background task + if (mRequiresReload) startDictionaryLoadingTaskLocked(); + // Currently updating contacts, don't return any results. + return mUpdatingDictionary; + } + } + + private void runReverseLookUp(final CharSequence previousWord, final WordCallback callback) { + Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length()); + if (prevWord != null && prevWord.ngrams != null) { + reverseLookUp(prevWord.ngrams, callback); + } + } + + @Override + public void getBigrams(final WordComposer codes, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + if (!reloadDictionaryIfRequired()) { + runReverseLookUp(previousWord, callback); + } + } + + /** + * Used only for testing purposes + * This function will wait for loading from database to be done + */ + void waitForDictionaryLoading() { + while (mUpdatingDictionary) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + } + + /** + * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words + * through callback. + * @param terminalNodes list of terminal nodes we want to add + */ + private void reverseLookUp(LinkedList<NextWord> terminalNodes, + final WordCallback callback) { + Node node; + int freq; + for (NextWord nextWord : terminalNodes) { + node = nextWord.word; + freq = nextWord.frequency; + // TODO Not the best way to limit suggestion threshold + if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) { + sb.setLength(0); + do { + sb.insert(0, node.code); + node = node.parent; + } while(node != null); + + // TODO better way to feed char array? + callback.addWord(sb.toString().toCharArray(), 0, sb.length(), freq, mDicTypeId, + DataType.BIGRAM); + } + } + } + + /** + * Search for the terminal node of the word + * @return Returns the terminal node of the word if the word exists + */ + private Node searchNode(final NodeArray children, final CharSequence word, final int offset, + final int length) { + // TODO Consider combining with addWordRec + 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; + } + } else { + if (node.children != null) { + Node returnNode = searchNode(node.children, word, offset + 1, length); + if (returnNode != null) return returnNode; + } + } + } + } + return null; + } + protected void clearDictionary() { mRoots = new NodeArray(); } @@ -332,18 +496,11 @@ public class ExpandableDictionary extends Dictionary { @Override protected Void doInBackground(Void... v) { loadDictionaryAsync(); - return null; - } - - @Override - protected void onPostExecute(Void result) { - // TODO Auto-generated method stub synchronized (mUpdatingLock) { mUpdatingDictionary = false; } - super.onPostExecute(result); + return null; } - } static char toLowerCase(char c) { diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index 5e835e543..4f672271a 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -99,7 +99,10 @@ public class InputLanguageSelection extends PreferenceActivity { boolean haveDictionary = false; conf.locale = locale; res.updateConfiguration(conf, res.getDisplayMetrics()); - BinaryDictionary bd = new BinaryDictionary(this, R.raw.main); + + int[] dictionaries = LatinIME.getDictionary(res); + BinaryDictionary bd = new BinaryDictionary(this, dictionaries, Suggest.DIC_MAIN); + // Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of // 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words. if (bd.getSize() > Suggest.LARGE_DICTIONARY_THRESHOLD / 4) { diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java index 1a196448f..d04930303 100644 --- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -21,12 +21,16 @@ import java.util.Locale; import java.util.Map; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.inputmethodservice.InputMethodService; +import android.inputmethodservice.Keyboard; +import android.preference.PreferenceManager; +import android.view.InflateException; -public class KeyboardSwitcher { +public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener { + public static final int MODE_NONE = 0; public static final int MODE_TEXT = 1; public static final int MODE_SYMBOLS = 2; public static final int MODE_PHONE = 3; @@ -45,6 +49,27 @@ public class KeyboardSwitcher { public static final int KEYBOARDMODE_IM = R.id.mode_im; public static final int KEYBOARDMODE_WEB = R.id.mode_webentry; + public static final String DEFAULT_LAYOUT_ID = "3"; + public static final String PREF_KEYBOARD_LAYOUT = "keyboard_layout"; + private static final int[] THEMES = new int [] { + R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal, + R.layout.input_stone_bold}; + + // Ids for each characters' color in the keyboard + private static final int CHAR_THEME_COLOR_WHITE = 0; + private static final int CHAR_THEME_COLOR_BLACK = 1; + + // Tables which contains resource ids for each character theme color + private static final int[] KBD_ALPHA = new int[] {R.xml.kbd_alpha, R.xml.kbd_alpha_black}; + private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black}; + private static final int[] KBD_PHONE_SYMBOLS = new int[] { + R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black}; + private static final int[] KBD_SYMBOLS = new int[] { + R.xml.kbd_symbols, R.xml.kbd_symbols_black}; + private static final int[] KBD_SYMBOLS_SHIFT = new int[] { + R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black}; + private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black}; + 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; @@ -57,9 +82,8 @@ public class KeyboardSwitcher { KEYBOARDMODE_IM, KEYBOARDMODE_WEB}; - //LatinIME mContext; Context mContext; - InputMethodService mInputMethodService; + LatinIME mInputMethodService; private KeyboardId mSymbolsId; private KeyboardId mSymbolsShiftedId; @@ -67,7 +91,7 @@ public class KeyboardSwitcher { private KeyboardId mCurrentId; private Map<KeyboardId, LatinKeyboard> mKeyboards; - private int mMode; /** One of the MODE_XXX values */ + private int mMode = MODE_NONE; /** One of the MODE_XXX values */ private int mImeOptions; private int mTextMode = MODE_TEXT_QWERTY; private boolean mIsSymbols; @@ -79,13 +103,19 @@ public class KeyboardSwitcher { private int mLastDisplayWidth; private LanguageSwitcher mLanguageSwitcher; private Locale mInputLocale; - private boolean mEnableMultipleLanguages; - KeyboardSwitcher(Context context, InputMethodService ims) { + private int mLayoutId; + + KeyboardSwitcher(Context context, LatinIME ims) { mContext = context; + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims); + mLayoutId = Integer.valueOf(prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID)); + prefs.registerOnSharedPreferenceChangeListener(this); + mKeyboards = new HashMap<KeyboardId, LatinKeyboard>(); - mSymbolsId = new KeyboardId(R.xml.kbd_symbols, false); - mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, false); + mSymbolsId = makeSymbolsId(false); + mSymbolsShiftedId = makeSymbolsShiftedId(false); mInputMethodService = ims; } @@ -98,14 +128,24 @@ public class KeyboardSwitcher { void setLanguageSwitcher(LanguageSwitcher languageSwitcher) { mLanguageSwitcher = languageSwitcher; mInputLocale = mLanguageSwitcher.getInputLocale(); - mEnableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 1; } void setInputView(LatinKeyboardView inputView) { mInputView = inputView; } - + + private KeyboardId makeSymbolsId(boolean hasVoice) { + return new KeyboardId(KBD_SYMBOLS[getCharColorId()], hasVoice); + } + + private KeyboardId makeSymbolsShiftedId(boolean hasVoice) { + return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], hasVoice); + } + void makeKeyboards(boolean forceCreate) { + mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary); + mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary); + if (forceCreate) mKeyboards.clear(); // Configuration change is coming after the keyboard gets recreated. So don't rely on that. // If keyboards have already been made, check if we have a screen width change and @@ -114,9 +154,6 @@ public class KeyboardSwitcher { 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); } /** @@ -140,6 +177,7 @@ public class KeyboardSwitcher { this(xml, 0, false, hasVoice); } + @Override public boolean equals(Object other) { return other instanceof KeyboardId && equals((KeyboardId) other); } @@ -150,6 +188,7 @@ public class KeyboardSwitcher { && other.mEnableShiftLock == this.mEnableShiftLock; } + @Override public int hashCode() { return (mXml + 1) * (mKeyboardMode + 1) * (mEnableShiftLock ? 2 : 1) * (mHasVoice ? 4 : 8); @@ -173,8 +212,14 @@ public class KeyboardSwitcher { 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); + if (mode == MODE_SYMBOLS) { + mode = MODE_TEXT; + } + try { + setKeyboardMode(mode, imeOptions, enableVoice, mPreferSymbols); + } catch (RuntimeException e) { + LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e); + } } void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) { @@ -186,10 +231,10 @@ public class KeyboardSwitcher { } mIsSymbols = isSymbols; - mInputView.setPreviewEnabled(true); + mInputView.setPreviewEnabled(mInputMethodService.getPopupOn()); KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols); - - LatinKeyboard keyboard = getKeyboard(id); + LatinKeyboard keyboard = null; + keyboard = getKeyboard(id); if (mode == MODE_PHONE) { mInputView.setPhoneKeyboard(keyboard); @@ -201,6 +246,7 @@ public class KeyboardSwitcher { keyboard.setShifted(false); keyboard.setShiftLocked(keyboard.isShiftLocked()); keyboard.setImeOptions(mContext.getResources(), mMode, imeOptions); + keyboard.setBlackFlag(isBlackSym()); } private LatinKeyboard getKeyboard(KeyboardId id) { @@ -212,8 +258,10 @@ public class KeyboardSwitcher { orig.updateConfiguration(conf, null); LatinKeyboard keyboard = new LatinKeyboard( mContext, id.mXml, id.mKeyboardMode); - keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols), mHasVoice); + keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols + || id.mXml == R.xml.kbd_symbols_black), mHasVoice); keyboard.setLanguageSwitcher(mLanguageSwitcher); + keyboard.setBlackFlag(isBlackSym()); if (id.mKeyboardMode == KEYBOARDMODE_NORMAL || id.mKeyboardMode == KEYBOARDMODE_URL || id.mKeyboardMode == KEYBOARDMODE_IM @@ -236,31 +284,40 @@ public class KeyboardSwitcher { private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { boolean hasVoice = hasVoiceButton(isSymbols); + int charColorId = getCharColorId(); + // TODO: generalize for any KeyboardId + int keyboardRowsResId = KBD_QWERTY[charColorId]; if (isSymbols) { - return (mode == MODE_PHONE) - ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice) - : new KeyboardId(R.xml.kbd_symbols, hasVoice); + if (mode == MODE_PHONE) { + return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId], hasVoice); + } else { + return new KeyboardId(KBD_SYMBOLS[charColorId], hasVoice); + } } switch (mode) { + case MODE_NONE: + LatinImeLogger.logOnWarning( + "getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols); + /* fall through */ 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); + if (mTextMode == MODE_TEXT_ALPHA) { + return new KeyboardId( + KBD_ALPHA[charColorId], KEYBOARDMODE_NORMAL, true, hasVoice); } - break; + // Normally mTextMode should be MODE_TEXT_QWERTY. + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_NORMAL, true, hasVoice); case MODE_SYMBOLS: - return new KeyboardId(R.xml.kbd_symbols, hasVoice); + return new KeyboardId(KBD_SYMBOLS[charColorId], hasVoice); case MODE_PHONE: - return new KeyboardId(R.xml.kbd_phone, hasVoice); + return new KeyboardId(KBD_PHONE[charColorId], hasVoice); case MODE_URL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_URL, true, hasVoice); case MODE_EMAIL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_EMAIL, true, hasVoice); case MODE_IM: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_IM, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_IM, true, hasVoice); case MODE_WEB: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_WEB, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_WEB, true, hasVoice); } return null; } @@ -273,19 +330,6 @@ public class KeyboardSwitcher { 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; } @@ -300,6 +344,18 @@ public class KeyboardSwitcher { return false; } + void setShifted(boolean shifted) { + if (mInputView != null) { + mInputView.setShifted(shifted); + } + } + + void setShiftLocked(boolean shiftLocked) { + if (mInputView != null) { + mInputView.setShiftLocked(shiftLocked); + } + } + void toggleShift() { if (mCurrentId.equals(mSymbolsId)) { LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId); @@ -314,7 +370,7 @@ public class KeyboardSwitcher { LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId); symbolsShiftedKeyboard.setShifted(false); mCurrentId = mSymbolsId; - mInputView.setKeyboard(getKeyboard(mSymbolsId)); + mInputView.setKeyboard(symbolsKeyboard); symbolsKeyboard.setShifted(false); symbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); } @@ -348,4 +404,72 @@ public class KeyboardSwitcher { } return false; } + + public LatinKeyboardView getInputView() { + return mInputView; + } + + public void recreateInputView() { + changeLatinKeyboardView(mLayoutId, true); + } + + private void changeLatinKeyboardView(int newLayout, boolean forceReset) { + if (mLayoutId != newLayout || mInputView == null || forceReset) { + if (mInputView != null) { + mInputView.closing(); + } + if (THEMES.length <= newLayout) { + newLayout = Integer.valueOf(DEFAULT_LAYOUT_ID); + } + + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + mInputView = (LatinKeyboardView) mInputMethodService.getLayoutInflater( + ).inflate(THEMES[newLayout], null); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( + mLayoutId + "," + newLayout, e); + } catch (InflateException e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( + mLayoutId + "," + newLayout, e); + } + } + mInputView.setExtentionLayoutResId(THEMES[newLayout]); + mInputView.setOnKeyboardActionListener(mInputMethodService); + mLayoutId = newLayout; + } + mInputMethodService.mHandler.post(new Runnable() { + public void run() { + if (mInputView != null) { + mInputMethodService.setInputView(mInputView); + } + mInputMethodService.updateInputViewShown(); + }}); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PREF_KEYBOARD_LAYOUT.equals(key)) { + changeLatinKeyboardView( + Integer.valueOf(sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)), false); + } + } + + public boolean isBlackSym () { + if (mInputView != null && mInputView.getSymbolColorSheme() == 1) { + return true; + } + return false; + } + + private int getCharColorId () { + if (isBlackSym()) { + return CHAR_THEME_COLOR_BLACK; + } else { + return CHAR_THEME_COLOR_WHITE; + } + } + } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 15b537f94..9bd16adb2 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -16,11 +16,12 @@ 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 org.xmlpull.v1.XmlPullParserException; + import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,9 +31,9 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.Keyboard; -import android.inputmethodservice.KeyboardView; import android.media.AudioManager; import android.os.Debug; import android.os.Handler; @@ -40,9 +41,9 @@ import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.speech.SpeechRecognizer; -import android.text.AutoText; import android.text.ClipboardManager; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -50,8 +51,8 @@ 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.ViewParent; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; @@ -62,6 +63,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -74,21 +76,25 @@ import java.util.Map; * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService - implements KeyboardView.OnKeyboardActionListener, + implements LatinKeyboardBaseView.OnKeyboardActionListener, VoiceInput.UiListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "LatinIME"; + private static final boolean PERF_DEBUG = false; static final boolean DEBUG = false; static final boolean TRACE = false; static final boolean VOICE_INSTALLED = true; static final boolean ENABLE_VOICE_BUTTON = true; + private static final boolean MODIFY_TEXT_FOR_CORRECTION = false; private static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String PREF_SOUND_ON = "sound_on"; + private static final String PREF_POPUP_ON = "popup_on"; private static final String PREF_AUTO_CAP = "auto_cap"; private static final String PREF_QUICK_FIXES = "quick_fixes"; private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; private static final String PREF_AUTO_COMPLETE = "auto_complete"; + private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; private static final String PREF_VOICE_MODE = "voice_mode"; // Whether or not the user has used voice input before (and thus, whether to show the @@ -127,6 +133,7 @@ public class LatinIME extends InputMethodService 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; + private static final int MSG_UPDATE_OLD_SUGGESTIONS = 5; // 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. @@ -145,7 +152,7 @@ public class LatinIME extends InputMethodService private static final int POS_SETTINGS = 0; private static final int POS_METHOD = 1; - private LatinKeyboardView mInputView; + //private LatinKeyboardView mInputView; private CandidateViewContainer mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; @@ -157,6 +164,7 @@ public class LatinIME extends InputMethodService KeyboardSwitcher mKeyboardSwitcher; private UserDictionary mUserDictionary; + private UserBigramDictionary mUserBigramDictionary; private ContactsDictionary mContactsDictionary; private AutoDictionary mAutoDictionary; @@ -176,7 +184,6 @@ public class LatinIME extends InputMethodService private boolean mAfterVoiceInput; private boolean mImmediatelyAfterVoiceInput; private boolean mShowingVoiceSuggestions; - private boolean mImmediatelyAfterVoiceSuggestions; private boolean mVoiceInputHighlighted; private boolean mEnableVoiceButton; private CharSequence mBestWord; @@ -186,25 +193,32 @@ public class LatinIME extends InputMethodService private boolean mAutoSpace; private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; + private boolean mBigramSuggestionEnabled; private boolean mAutoCorrectOn; + // TODO move this state variable outside LatinIME private boolean mCapsLock; private boolean mPasswordText; - private boolean mEmailText; private boolean mVibrateOn; private boolean mSoundOn; + private boolean mPopupOn; private boolean mAutoCap; private boolean mQuickFixes; private boolean mHasUsedVoiceInput; private boolean mHasUsedVoiceInputUnsupportedLocale; private boolean mLocaleSupportedForVoiceInput; private boolean mShowSuggestions; - private boolean mSuggestionShouldReplaceCurrentWord; private boolean mIsShowingHint; private int mCorrectionMode; private boolean mEnableVoice = true; private boolean mVoiceOnPrimary; private int mOrientation; private List<CharSequence> mSuggestPuncList; + // Keep track of the last selection range to decide if we need to show word alternatives + private int mLastSelectionStart; + private int mLastSelectionEnd; + + // Input type is such that we should not auto-correct + private boolean mInputTypeNoAutoCorrect; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; @@ -219,8 +233,9 @@ public class LatinIME extends InputMethodService private final float FX_VOLUME = -1.0f; private boolean mSilentMode; - private String mWordSeparators; + /* package */ String mWordSeparators; private String mSentenceSeparators; + private String mSuggestPuncs; private VoiceInput mVoiceInput; private VoiceResults mVoiceResults = new VoiceResults(); private long mSwipeTriggerTimeMillis; @@ -228,17 +243,66 @@ public class LatinIME extends InputMethodService // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; + private boolean mRefreshKeyboardRequired; // For each word, a list of potential replacements, usually from voice. private Map<String, List<CharSequence>> mWordToSuggestions = new HashMap<String, List<CharSequence>>(); + private ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); + private class VoiceResults { List<String> candidates; Map<String, List<CharSequence>> alternatives; } + + public abstract static class WordAlternatives { + protected CharSequence mChosenWord; - private boolean mRefreshKeyboardRequired; + public WordAlternatives() { + // Nothing + } + + public WordAlternatives(CharSequence chosenWord) { + mChosenWord = chosenWord; + } + + @Override + public int hashCode() { + return mChosenWord.hashCode(); + } + + public abstract CharSequence getOriginalWord(); + + public CharSequence getChosenWord() { + return mChosenWord; + } + + public abstract List<CharSequence> getAlternatives(); + } + + public class TypedWordAlternatives extends WordAlternatives { + private WordComposer word; + + public TypedWordAlternatives() { + // Nothing + } + + public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) { + super(chosenWord); + word = wordComposer; + } + + @Override + public CharSequence getOriginalWord() { + return word.getTypedWord(); + } + + @Override + public List<CharSequence> getAlternatives() { + return getTypedSuggestions(word); + } + } Handler mHandler = new Handler() { @Override @@ -247,10 +311,14 @@ public class LatinIME extends InputMethodService case MSG_UPDATE_SUGGESTIONS: updateSuggestions(); break; + case MSG_UPDATE_OLD_SUGGESTIONS: + setOldSuggestions(); + break; case MSG_START_TUTORIAL: if (mTutorial == null) { - if (mInputView.isShown()) { - mTutorial = new Tutorial(LatinIME.this, mInputView); + if (mKeyboardSwitcher.getInputView().isShown()) { + mTutorial = new Tutorial( + LatinIME.this, mKeyboardSwitcher.getInputView()); mTutorial.start(); } else { // Try again soon if the view is not yet showing @@ -273,6 +341,7 @@ public class LatinIME extends InputMethodService }; @Override public void onCreate() { + LatinImeLogger.init(this); super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); mResources = getResources(); @@ -288,7 +357,18 @@ public class LatinIME extends InputMethodService if (inputLanguage == null) { inputLanguage = conf.locale.toString(); } - initSuggest(inputLanguage); + + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + initSuggest(inputLanguage); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e); + } + } + mOrientation = conf.orientation; initSuggestPuncList(); @@ -311,6 +391,46 @@ public class LatinIME extends InputMethodService prefs.registerOnSharedPreferenceChangeListener(this); } + /** + * Loads a dictionary or multiple separated dictionary + * @return returns array of dictionary resource ids + */ + static int[] getDictionary(Resources res) { + String packageName = LatinIME.class.getPackage().getName(); + XmlResourceParser xrp = res.getXml(R.xml.dictionary); + int dictionaryCount = 0; + ArrayList<Integer> dictionaries = new ArrayList<Integer>(); + + try { + int current = xrp.getEventType(); + while (current != XmlResourceParser.END_DOCUMENT) { + if (current == XmlResourceParser.START_TAG) { + String tag = xrp.getName(); + if (tag != null) { + if (tag.equals("part")) { + String dictFileName = xrp.getAttributeValue(null, "name"); + dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName)); + } + } + } + xrp.next(); + current = xrp.getEventType(); + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Dictionary XML parsing failure"); + } catch (IOException e) { + Log.e(TAG, "Dictionary XML IOException"); + } + + int count = dictionaries.size(); + int[] dict = new int[count]; + for (int i = 0; i < count; i++) { + dict[i] = dictionaries.get(i); + } + + return dict; + } + private void initSuggest(String locale) { mInputLocale = locale; @@ -324,17 +444,25 @@ public class LatinIME extends InputMethodService } SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); - mSuggest = new Suggest(this, R.raw.main); + + int[] dictionaries = getDictionary(orig); + mSuggest = new Suggest(this, dictionaries); updateAutoTextEnabled(saveLocale); if (mUserDictionary != null) mUserDictionary.close(); mUserDictionary = new UserDictionary(this, mInputLocale); if (mContactsDictionary == null) { - mContactsDictionary = new ContactsDictionary(this); + mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); } if (mAutoDictionary != null) { mAutoDictionary.close(); } - mAutoDictionary = new AutoDictionary(this, this, mInputLocale); + mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO); + if (mUserBigramDictionary != null) { + mUserBigramDictionary.close(); + } + mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale, + Suggest.DIC_USER); + mSuggest.setUserBigramDictionary(mUserBigramDictionary); mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); mSuggest.setAutoDictionary(mAutoDictionary); @@ -348,12 +476,18 @@ public class LatinIME extends InputMethodService @Override public void onDestroy() { - mUserDictionary.close(); - mContactsDictionary.close(); + if (mUserDictionary != null) { + mUserDictionary.close(); + } + if (mContactsDictionary != null) { + mContactsDictionary.close(); + } unregisterReceiver(mReceiver); - if (VOICE_INSTALLED) { + if (VOICE_INSTALLED && mVoiceInput != null) { mVoiceInput.destroy(); } + LatinImeLogger.commit(); + LatinImeLogger.onDestroy(); super.onDestroy(); } @@ -393,15 +527,12 @@ public class LatinIME extends InputMethodService @Override public View onCreateInputView() { - mInputView = (LatinKeyboardView) getLayoutInflater().inflate( - R.layout.input, null); - mKeyboardSwitcher.setInputView(mInputView); + mKeyboardSwitcher.recreateInputView(); mKeyboardSwitcher.makeKeyboards(true); - mInputView.setOnKeyboardActionListener(this); mKeyboardSwitcher.setKeyboardMode( KeyboardSwitcher.MODE_TEXT, 0, shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); - return mInputView; + return mKeyboardSwitcher.getInputView(); } @Override @@ -418,8 +549,9 @@ public class LatinIME extends InputMethodService @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); // In landscape mode, this method gets called without the input view being created. - if (mInputView == null) { + if (inputView == null) { return; } @@ -448,15 +580,12 @@ public class LatinIME extends InputMethodService mAfterVoiceInput = false; mImmediatelyAfterVoiceInput = false; mShowingVoiceSuggestions = false; - mImmediatelyAfterVoiceSuggestions = false; mVoiceInputHighlighted = false; - mWordToSuggestions.clear(); mInputTypeNoAutoCorrect = false; mPredictionOn = false; mCompletionOn = false; mCompletions = null; mCapsLock = false; - mEmailText = false; mEnteredText = null; switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { @@ -479,9 +608,6 @@ public class LatinIME extends InputMethodService 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; @@ -532,7 +658,7 @@ public class LatinIME extends InputMethodService attribute.imeOptions, enableVoiceButton); updateShiftKeyState(attribute); } - mInputView.closing(); + inputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; @@ -548,7 +674,8 @@ public class LatinIME extends InputMethodService updateCorrectionMode(); - mInputView.setProximityCorrectionEnabled(true); + inputView.setPreviewEnabled(mPopupOn); + inputView.setProximityCorrectionEnabled(true); mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions); checkTutorial(attribute.privateImeOptions); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); @@ -558,6 +685,8 @@ public class LatinIME extends InputMethodService public void onFinishInput() { super.onFinishInput(); + LatinImeLogger.commit(); + if (VOICE_INSTALLED && !mConfigurationChanging) { if (mAfterVoiceInput) { mVoiceInput.flushAllTextModificationCounters(); @@ -566,10 +695,11 @@ public class LatinIME extends InputMethodService mVoiceInput.flushLogs(); mVoiceInput.cancel(); } - if (mInputView != null) { - mInputView.closing(); + if (mKeyboardSwitcher.getInputView() != null) { + mKeyboardSwitcher.getInputView().closing(); } if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); + if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); } @Override @@ -605,15 +735,15 @@ public class LatinIME extends InputMethodService mVoiceInput.setSelectionSpan(newSelEnd - newSelStart); } - 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))) { + || newSelEnd != candidatesEnd) + && mLastSelectionStart != newSelStart)) { mComposing.setLength(0); mPredicting = false; - updateSuggestions(); + postUpdateSuggestions(); TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { @@ -622,10 +752,10 @@ public class LatinIME extends InputMethodService mVoiceInputHighlighted = false; } else if (!mPredicting && !mJustAccepted) { switch (TextEntryState.getState()) { - case TextEntryState.STATE_ACCEPTED_DEFAULT: + case ACCEPTED_DEFAULT: TextEntryState.reset(); // fall through - case TextEntryState.STATE_SPACE_AFTER_PICKED: + case SPACE_AFTER_PICKED: mJustAddedAutoSpace = false; // The user moved the cursor. break; } @@ -633,32 +763,29 @@ public class LatinIME extends InputMethodService 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()); + // Make a note of the cursor position + mLastSelectionStart = newSelStart; + mLastSelectionEnd = newSelEnd; - setSuggestions(suggestions, false, true, true); - setCandidatesViewShown(true); - } + + // Check if we should go in or out of correction mode. + if (isPredictionOn() && mJustRevertedSeparator == null + && (candidatesStart == candidatesEnd || newSelStart != oldSelStart + || TextEntryState.isCorrecting()) + && (newSelStart < newSelEnd - 1 || (!mPredicting)) + && !mVoiceInputHighlighted) { + if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { + postUpdateOldSuggestions(); + } else { + abortCorrection(false); } } } @Override public void hideWindow() { + LatinImeLogger.commit(); + if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); @@ -675,13 +802,15 @@ public class LatinIME extends InputMethodService mVoiceInput.cancel(); } } + mWordToSuggestions.clear(); + mWordHistory.clear(); super.hideWindow(); TextEntryState.endSession(); } @Override public void onDisplayCompletions(CompletionInfo[] completions) { - if (false) { + if (DEBUG) { Log.i("foo", "Received completions:"); for (int i=0; i<(completions != null ? completions.length : 0); i++) { Log.i("foo", " #" + i + ": " + completions[i]); @@ -699,7 +828,7 @@ public class LatinIME extends InputMethodService CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } - //CharSequence typedWord = mWord.getTypedWord(); + // When in fullscreen mode, show completions generated by the application setSuggestions(stringList, true, true, true); mBestWord = null; setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); @@ -711,7 +840,8 @@ public class LatinIME extends InputMethodService // TODO: Remove this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { // Show the candidates view only if input view is showing - super.setCandidatesViewShown(shown && mInputView != null && mInputView.isShown()); + super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null + && mKeyboardSwitcher.getInputView().isShown()); } } @@ -724,11 +854,24 @@ public class LatinIME extends InputMethodService } @Override + public boolean onEvaluateFullscreenMode() { + DisplayMetrics dm = getResources().getDisplayMetrics(); + float displayHeight = dm.heightPixels; + // If the display is more than X inches high, don't go to fullscreen mode + float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen); + if (displayHeight > dimen) { + return false; + } else { + return super.onEvaluateFullscreenMode(); + } + } + + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: - if (event.getRepeatCount() == 0 && mInputView != null) { - if (mInputView.handleBack()) { + if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { + if (mKeyboardSwitcher.getInputView().handleBack()) { return true; } else if (mTutorial != null) { mTutorial.close(); @@ -760,8 +903,10 @@ public class LatinIME extends InputMethodService if (mTutorial != null) { return true; } + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); // Enable shift key and DPAD to do selections - if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { + if (inputView != null && inputView.isShown() + && inputView.isShifted()) { event = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), @@ -794,7 +939,8 @@ public class LatinIME extends InputMethodService mKeyboardSwitcher = new KeyboardSwitcher(this, this); } mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - if (mInputView != null) { + if (mKeyboardSwitcher.getInputView() != null + && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) { mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary); } mKeyboardSwitcher.makeKeyboards(true); @@ -809,7 +955,7 @@ public class LatinIME extends InputMethodService } mCommittedLength = mComposing.length(); TextEntryState.acceptedTyped(mComposing); - checkAddToDictionary(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); + addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); } updateSuggestions(); } @@ -822,9 +968,8 @@ public class LatinIME extends InputMethodService public void updateShiftKeyState(EditorInfo attr) { InputConnection ic = getCurrentInputConnection(); - if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode() - && ic != null) { - mInputView.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); + if (attr != null && mKeyboardSwitcher.isAlphabetMode() && ic != null) { + mKeyboardSwitcher.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); } } @@ -940,6 +1085,7 @@ public class LatinIME extends InputMethodService case Keyboard.KEYCODE_DELETE: handleBackspace(); mDeleteCount++; + LatinImeLogger.logOnDelete(); break; case Keyboard.KEYCODE_SHIFT: handleShift(); @@ -959,11 +1105,7 @@ public class LatinIME extends InputMethodService toggleLanguage(false, false); break; case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS: - if (mCapsLock) { - handleShift(); - } else { - toggleCapsLock(); - } + handleCapsLock(); break; case Keyboard.KEYCODE_MODE_CHANGE: changeKeyboardMode(); @@ -980,6 +1122,7 @@ public class LatinIME extends InputMethodService if (primaryCode != KEYCODE_ENTER) { mJustAddedAutoSpace = false; } + LatinImeLogger.logOnInputChar((char)primaryCode); if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { @@ -1001,6 +1144,7 @@ public class LatinIME extends InputMethodService } InputConnection ic = getCurrentInputConnection(); if (ic == null) return; + abortCorrection(false); ic.beginBatchEdit(); if (mPredicting) { commitTyped(ic); @@ -1025,6 +1169,8 @@ public class LatinIME extends InputMethodService InputConnection ic = getCurrentInputConnection(); if (ic == null) return; + ic.beginBatchEdit(); + if (mAfterVoiceInput) { // Don't log delete if the user is pressing delete at // the beginning of the text box (hence not deleting anything) @@ -1055,8 +1201,9 @@ public class LatinIME extends InputMethodService } postUpdateShiftKeyState(); TextEntryState.backspace(); - if (TextEntryState.getState() == TextEntryState.STATE_UNDO_COMMIT) { + if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { revertLastWord(deleteChar); + ic.endBatchEdit(); return; } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { ic.deleteSurroundingText(mEnteredText.length(), 0); @@ -1078,16 +1225,47 @@ public class LatinIME extends InputMethodService } } mJustRevertedSeparator = null; + ic.endBatchEdit(); } private void handleShift() { mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - if (mKeyboardSwitcher.isAlphabetMode()) { - // Alphabet keyboard - checkToggleCapsLock(); - mInputView.setShifted(mCapsLock || !mInputView.isShifted()); + KeyboardSwitcher switcher = mKeyboardSwitcher; + LatinKeyboardView inputView = switcher.getInputView(); + if (switcher.isAlphabetMode()) { + if (mCapsLock) { + mCapsLock = false; + switcher.setShifted(false); + } else if (inputView != null) { + if (inputView.isShifted()) { + mCapsLock = true; + switcher.setShiftLocked(true); + } else { + switcher.setShifted(true); + } + } } else { - mKeyboardSwitcher.toggleShift(); + switcher.toggleShift(); + } + } + + private void handleCapsLock() { + mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (switcher.isAlphabetMode()) { + mCapsLock = !mCapsLock; + if (mCapsLock) { + switcher.setShiftLocked(true); + } else { + switcher.setShifted(false); + } + } + } + + private void abortCorrection(boolean force) { + if (force || TextEntryState.isCorrecting()) { + getCurrentInputConnection().finishComposingText(); + setSuggestions(null, false, false, false); } } @@ -1100,24 +1278,31 @@ public class LatinIME extends InputMethodService // Assume input length is 1. This assumption fails for smiley face insertions. mVoiceInput.incrementTextModificationInsertCount(1); } + abortCorrection(false); if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { if (!mPredicting) { mPredicting = true; mComposing.setLength(0); + saveWordInHistory(mBestWord); mWord.reset(); } } - if (mInputView.isShifted()) { - // TODO: This doesn't work with ß, need to fix it in the next release. + if (mKeyboardSwitcher.getInputView().isShifted()) { + // TODO: This doesn't work with [beta], 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); + primaryCode = keyCodes[0]; + if (mKeyboardSwitcher.isAlphabetMode()) { + primaryCode = Character.toUpperCase(primaryCode); + } } if (mPredicting) { - if (mInputView.isShifted() && mComposing.length() == 0) { + if (mKeyboardSwitcher.getInputView().isShifted() + && mKeyboardSwitcher.isAlphabetMode() + && mComposing.length() == 0) { mWord.setCapitalized(true); } mComposing.append((char) primaryCode); @@ -1136,7 +1321,7 @@ public class LatinIME extends InputMethodService sendKeyChar((char)primaryCode); } updateShiftKeyState(getCurrentInputEditorInfo()); - measureCps(); + if (LatinIME.PERF_DEBUG) measureCps(); TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); } @@ -1160,6 +1345,7 @@ public class LatinIME extends InputMethodService InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); + abortCorrection(false); } if (mPredicting) { // In certain languages where single quote is a separator, it's better @@ -1170,8 +1356,7 @@ public class LatinIME extends InputMethodService (mJustRevertedSeparator == null || mJustRevertedSeparator.length() == 0 || mJustRevertedSeparator.charAt(0) != primaryCode)) { - pickDefaultSuggestion(); - pickedDefault = true; + pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". if (primaryCode == KEYCODE_SPACE) { @@ -1189,21 +1374,20 @@ public class LatinIME extends InputMethodService // Handle the case of ". ." -> " .." with auto-space if necessary // before changing the TextEntryState. - if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED + 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 + 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); + if (pickedDefault) { + TextEntryState.backToAcceptedDefault(mWord.getTypedWord()); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { @@ -1217,21 +1401,25 @@ public class LatinIME extends InputMethodService mVoiceInput.cancel(); } requestHideSelf(0); - mInputView.closing(); + mKeyboardSwitcher.getInputView().closing(); TextEntryState.endSession(); } - private void checkToggleCapsLock() { - if (mInputView.getKeyboard().isShifted()) { - toggleCapsLock(); + private void saveWordInHistory(CharSequence result) { + if (mWord.size() <= 1) { + mWord.reset(); + return; } - } - - private void toggleCapsLock() { - mCapsLock = !mCapsLock; - if (mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + // Skip if result is null. It happens in some edge case. + if (TextUtils.isEmpty(result)) { + return; } + + // Make a copy of the CharSequence, since it is/could be a mutable CharSequence + final String resultCopy = result.toString(); + TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy, + new WordComposer(mWord)); + mWordHistory.add(entry); } private void postUpdateSuggestions() { @@ -1239,6 +1427,11 @@ public class LatinIME extends InputMethodService mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); } + private void postUpdateOldSuggestions() { + mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300); + } + private boolean isPredictionOn() { boolean predictionOn = mPredictionOn; return predictionOn; @@ -1258,8 +1451,8 @@ public class LatinIME extends InputMethodService mHandler.post(new Runnable() { public void run() { mRecognizing = false; - if (mInputView != null) { - setInputView(mInputView); + if (mKeyboardSwitcher.getInputView() != null) { + setInputView(mKeyboardSwitcher.getInputView()); } updateInputViewShown(); }}); @@ -1358,7 +1551,7 @@ public class LatinIME extends InputMethodService Window window = mVoiceWarningDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mInputView.getWindowToken(); + lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); @@ -1394,7 +1587,8 @@ public class LatinIME extends InputMethodService final List<CharSequence> nBest = new ArrayList<CharSequence>(); boolean capitalizeFirstWord = preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted()); + || (mKeyboardSwitcher.isAlphabetMode() + && mKeyboardSwitcher.getInputView().isShifted()); for (String c : mVoiceResults.candidates) { if (capitalizeFirstWord) { c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); @@ -1419,13 +1613,6 @@ public class LatinIME extends InputMethodService 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); @@ -1450,9 +1637,8 @@ public class LatinIME extends InputMethodService } private void updateSuggestions() { - mSuggestionShouldReplaceCurrentWord = false; - - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); // Check if we have a suggestion engine attached. if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { @@ -1463,24 +1649,56 @@ public class LatinIME extends InputMethodService setNextSuggestions(); return; } + showSuggestions(mWord); + } + + private List<CharSequence> getTypedSuggestions(WordComposer word) { + List<CharSequence> stringList = mSuggest.getSuggestions( + mKeyboardSwitcher.getInputView(), word, false, null); + return stringList; + } + + private void showCorrections(WordAlternatives alternatives) { + List<CharSequence> stringList = alternatives.getAlternatives(); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null); + showSuggestions(stringList, alternatives.getOriginalWord(), false, false); + } + + private void showSuggestions(WordComposer word) { + //long startTime = System.currentTimeMillis(); // TIME MEASUREMENT! + // TODO Maybe need better way of retrieving previous word + CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), + mWordSeparators); + List<CharSequence> stringList = mSuggest.getSuggestions( + mKeyboardSwitcher.getInputView(), word, false, prevWord); + //long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT! + //Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime)); - List<CharSequence> stringList = mSuggest.getSuggestions(mInputView, mWord, false); int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters( + nextLettersFrequencies); boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection(); //|| mCorrectionMode == mSuggest.CORRECTION_FULL; - CharSequence typedWord = mWord.getTypedWord(); + CharSequence typedWord = word.getTypedWord(); // If we're in basic correct boolean typedWordValid = mSuggest.isValidWord(typedWord) || - (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase())); - if (mCorrectionMode == Suggest.CORRECTION_FULL) { + (preferCapitalization() + && mSuggest.isValidWord(typedWord.toString().toLowerCase())); + if (mCorrectionMode == Suggest.CORRECTION_FULL + || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { correctionAvailable |= typedWordValid; } // Don't auto-correct words with multiple capital letter - correctionAvailable &= !mWord.isMostlyCaps(); + correctionAvailable &= !word.isMostlyCaps(); + correctionAvailable &= !TextEntryState.isCorrecting(); + + showSuggestions(stringList, typedWord, typedWordValid, correctionAvailable); + } + private void showSuggestions(List<CharSequence> stringList, CharSequence typedWord, + boolean typedWordValid, boolean correctionAvailable) { setSuggestions(stringList, false, typedWordValid, correctionAvailable); if (stringList.size() > 0) { if (correctionAvailable && !typedWordValid && stringList.size() > 1) { @@ -1494,7 +1712,7 @@ public class LatinIME extends InputMethodService setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); } - private void pickDefaultSuggestion() { + private boolean pickDefaultSuggestion() { // Complete any pending candidate query first if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); @@ -1503,14 +1721,18 @@ public class LatinIME extends InputMethodService if (mBestWord != null && mBestWord.length() > 0) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); mJustAccepted = true; - pickSuggestion(mBestWord); + pickSuggestion(mBestWord, false); // Add the word to the auto dictionary if it's not a known word - checkAddToDictionary(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); + addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); + return true; + } + return false; } public void pickSuggestionManually(int index, CharSequence suggestion) { if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index); + List<CharSequence> suggestions = mCandidateView.getSuggestions(); if (mAfterVoiceInput && !mShowingVoiceSuggestions) { mVoiceInput.flushAllTextModificationCounters(); @@ -1518,6 +1740,7 @@ public class LatinIME extends InputMethodService mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.length()); } + final boolean correcting = TextEntryState.isCorrecting(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); @@ -1540,7 +1763,12 @@ public class LatinIME extends InputMethodService } // If this is a punctuation, apply it through the normal key press - if (suggestion.length() == 1 && isWordSeparator(suggestion.charAt(0))) { + if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0)) + || isSuggestedPunctuation(suggestion.charAt(0)))) { + // Word separators are suggested before the user inputs something. + // So, LatinImeLogger logs "" as a user's input. + LatinImeLogger.logOnManualSuggestion( + "", suggestion.toString(), index, suggestions); onKey(suggestion.charAt(0), null); if (ic != null) { ic.endBatchEdit(); @@ -1548,20 +1776,34 @@ public class LatinIME extends InputMethodService return; } mJustAccepted = true; - pickSuggestion(suggestion); + pickSuggestion(suggestion, correcting); // Add the word to the auto dictionary if it's not a known word if (index == 0) { - checkAddToDictionary(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); + addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); + } else { + addToBigramDictionary(suggestion, 1); } + LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), + index, suggestions); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space - if (mAutoSpace) { + if (mAutoSpace && !correcting) { 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)) { + + // Fool the state watcher so that a subsequent backspace will not do a revert, unless + // we just did a correction, in which case we need to stay in + // TextEntryState.State.PICKED_SUGGESTION state. + if (!correcting) { + TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); + setNextSuggestions(); + } else { + // In case the cursor position doesn't change, make sure we show the suggestions again. + postUpdateOldSuggestions(); + } + if (index == 0 && mCorrectionMode > 0 && !mSuggest.isValidWord(suggestion) + && !mSuggest.isValidWord(suggestion.toString().toLowerCase())) { mCandidateView.showAddToDictionaryHint(suggestion); } if (ic != null) { @@ -1569,43 +1811,226 @@ public class LatinIME extends InputMethodService } } - private void pickSuggestion(CharSequence suggestion) { + private void rememberReplacedWord(CharSequence suggestion) { + if (mShowingVoiceSuggestions) { + // Retain the replaced word in the alternatives array. + EditingUtil.Range range = new EditingUtil.Range(); + String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(), + mWordSeparators, range); + if (!mWordToSuggestions.containsKey(wordToBeReplaced)) { + wordToBeReplaced = wordToBeReplaced.toLowerCase(); + } + if (mWordToSuggestions.containsKey(wordToBeReplaced)) { + List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced); + if (suggestions.contains(suggestion)) { + suggestions.remove(suggestion); + } + suggestions.add(wordToBeReplaced); + mWordToSuggestions.remove(wordToBeReplaced); + mWordToSuggestions.put(suggestion.toString(), suggestions); + } + } + } + + /** + * Commits the chosen word to the text field and saves it for later + * retrieval. + * @param suggestion the suggestion picked by the user to be committed to + * the text field + * @param correcting whether this is due to a correction of an existing + * word. + */ + private void pickSuggestion(CharSequence suggestion, boolean correcting) { + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); if (mCapsLock) { suggestion = suggestion.toString().toUpperCase(); } else if (preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) { + || (mKeyboardSwitcher.isAlphabetMode() + && inputView.isShifted())) { suggestion = suggestion.toString().toUpperCase().charAt(0) + suggestion.subSequence(1, suggestion.length()).toString(); } InputConnection ic = getCurrentInputConnection(); if (ic != null) { - if (mSuggestionShouldReplaceCurrentWord) { + rememberReplacedWord(suggestion); + // If text is in correction mode and we're not using composing + // text to underline, then the word at the cursor position needs + // to be removed before committing the correction + if (correcting && !MODIFY_TEXT_FOR_CORRECTION) { + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } EditingUtil.deleteWordAtCursor(ic, getWordSeparators()); } - if (!VoiceInput.DELETE_SYMBOL.equals(suggestion)) { - ic.commitText(suggestion, 1); - } + + ic.commitText(suggestion, 1); } + saveWordInHistory(suggestion); mPredicting = false; mCommittedLength = suggestion.length(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); - setNextSuggestions(); + ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); + // If we just corrected a word, then don't show punctuations + if (!correcting) { + setNextSuggestions(); + } updateShiftKeyState(getCurrentInputEditorInfo()); } + private void setOldSuggestions() { + // TODO: Inefficient to check if touching word and then get the touching word. Do it + // in one go. + mShowingVoiceSuggestions = false; + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + ic.beginBatchEdit(); + // If there is a selection, then undo the selection first. Unfortunately this causes + // a flicker. TODO: Add getSelectionText() to InputConnection API. + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } + if (!mPredicting && isCursorTouchingWord()) { + EditingUtil.Range range = new EditingUtil.Range(); + CharSequence touching = EditingUtil.getWordAtCursor(getCurrentInputConnection(), + mWordSeparators, range); + // If it's a selection, check if it's an entire word and no more, no less. + boolean fullword = EditingUtil.isFullWordOrInside(range, mLastSelectionStart, + mLastSelectionEnd); + if (fullword && touching != null && touching.length() > 1) { + // Strip out any trailing word separator + if (mWordSeparators.indexOf(touching.charAt(touching.length() - 1)) > 0) { + touching = touching.toString().substring(0, touching.length() - 1); + } + + // Search for result in spoken word alternatives + String selectedWord = touching.toString().trim(); + if (!mWordToSuggestions.containsKey(selectedWord)){ + selectedWord = selectedWord.toLowerCase(); + } + if (mWordToSuggestions.containsKey(selectedWord)){ + mShowingVoiceSuggestions = true; + underlineWord(touching, range.charsBefore, range.charsAfter); + List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord); + // If the first letter of touching is capitalized, make all the suggestions + // start with a capital letter. + if (Character.isUpperCase((char) touching.charAt(0))) { + for (int i=0; i< suggestions.size(); i++) { + String origSugg = (String) suggestions.get(i); + String capsSugg = origSugg.toUpperCase().charAt(0) + + origSugg.subSequence(1, origSugg.length()).toString(); + suggestions.set(i,capsSugg); + } + } + setSuggestions(suggestions, false, true, true); + setCandidatesViewShown(true); + TextEntryState.selectedForCorrection(); + ic.endBatchEdit(); + return; + } + + // If we didn't find a match, search for result in typed word history + WordComposer foundWord = null; + WordAlternatives alternatives = null; + for (WordAlternatives entry : mWordHistory) { + if (TextUtils.equals(entry.getChosenWord(), touching)) { + if (entry instanceof TypedWordAlternatives) { + foundWord = ((TypedWordAlternatives)entry).word; + } + alternatives = entry; + break; + } + } + // If we didn't find a match, at least suggest completions + if (foundWord == null && mSuggest.isValidWord(touching)) { + foundWord = new WordComposer(); + for (int i = 0; i < touching.length(); i++) { + foundWord.add(touching.charAt(i), new int[] { touching.charAt(i) }); + } + } + // Found a match, show suggestions + if (foundWord != null || alternatives != null) { + underlineWord(touching, range.charsBefore, range.charsAfter); + TextEntryState.selectedForCorrection(); + if (alternatives == null) alternatives = new TypedWordAlternatives(touching, + foundWord); + showCorrections(alternatives); + if (foundWord != null) { + mWord = new WordComposer(foundWord); + } else { + mWord.reset(); + } + // Revert the selection + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + ic.endBatchEdit(); + return; + } + abortCorrection(true); + } else { + abortCorrection(true); + setNextSuggestions(); + } + } else { + abortCorrection(true); + } + // Revert the selection + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + ic.endBatchEdit(); + } + private void setNextSuggestions() { setSuggestions(mSuggestPuncList, false, false, false); } - private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta) { + private void underlineWord(CharSequence word, int left, int right) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + if (MODIFY_TEXT_FOR_CORRECTION) { + ic.finishComposingText(); + ic.deleteSurroundingText(left, right); + ic.setComposingText(word, 1); + } + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } + + private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { + checkAddToDictionary(suggestion, frequencyDelta, false); + } + + private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) { + checkAddToDictionary(suggestion, frequencyDelta, true); + } + + /** + * Adds to the UserBigramDictionary and/or AutoDictionary + * @param addToBigramDictionary true if it should be added to bigram dictionary if possible + */ + private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, + boolean addToBigramDictionary) { + if (suggestion == null || suggestion.length() < 1) return; // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be // adding words in situations where the user or application really didn't // want corrections enabled or learned. - if (!(mCorrectionMode == Suggest.CORRECTION_FULL)) return; - if (mAutoDictionary.isValidWord(suggestion) - || (!mSuggest.isValidWord(suggestion.toString()) + if (!(mCorrectionMode == Suggest.CORRECTION_FULL + || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { + return; + } + if (suggestion != null) { + if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) + || (!mSuggest.isValidWord(suggestion.toString()) && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { - mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); + mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); + } + + if (mUserBigramDictionary != null) { + CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), + mSentenceSeparators); + if (!TextUtils.isEmpty(prevWord)) { + mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); + } + } } } @@ -1635,7 +2060,6 @@ public class LatinIME extends InputMethodService 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; @@ -1647,7 +2071,6 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(toDelete, 0); ic.setComposingText(mComposing, 1); TextEntryState.backspace(); - ic.endBatchEdit(); postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); @@ -1664,7 +2087,7 @@ public class LatinIME extends InputMethodService return separators.contains(String.valueOf((char)code)); } - public boolean isSentenceSeparator(int code) { + private boolean isSentenceSeparator(int code) { return mSentenceSeparators.contains(String.valueOf((char)code)); } @@ -1688,7 +2111,7 @@ public class LatinIME extends InputMethodService ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); CharSequence text = cm.getText(); if (!TextUtils.isEmpty(text)) { - mInputView.startPlaying(text.toString()); + mKeyboardSwitcher.getInputView().startPlaying(text.toString()); } } } @@ -1739,7 +2162,7 @@ public class LatinIME extends InputMethodService public void onRelease(int primaryCode) { // Reset any drag flags in the keyboard - ((LatinKeyboard) mInputView.getKeyboard()).keyReleased(); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased(); //vibrate(); } @@ -1791,7 +2214,7 @@ public class LatinIME extends InputMethodService // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode if (mAudioManager == null) { - if (mInputView != null) { + if (mKeyboardSwitcher.getInputView() != null) { updateRingerMode(); } } @@ -1818,8 +2241,9 @@ public class LatinIME extends InputMethodService if (!mVibrateOn) { return; } - if (mInputView != null) { - mInputView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, + if (mKeyboardSwitcher.getInputView() != null) { + mKeyboardSwitcher.getInputView().performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } @@ -1854,6 +2278,10 @@ public class LatinIME extends InputMethodService return mWord; } + boolean getPopupOn() { + return mPopupOn; + } + private void updateCorrectionMode() { mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) @@ -1861,6 +2289,8 @@ public class LatinIME extends InputMethodService mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled) ? Suggest.CORRECTION_FULL : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); + mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled) + ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; if (mSuggest != null) { mSuggest.setCorrectionMode(mCorrectionMode); } @@ -1877,7 +2307,7 @@ public class LatinIME extends InputMethodService launchSettings(LatinIMESettings.class); } - protected void launchSettings(Class settingsClass) { + protected void launchSettings(Class<LatinIMESettings> settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); @@ -1890,6 +2320,8 @@ public class LatinIME extends InputMethodService SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false); mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); + mPopupOn = sp.getBoolean(PREF_POPUP_ON, + mResources.getBoolean(R.bool.default_popup_preview)); mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); @@ -1927,6 +2359,7 @@ public class LatinIME extends InputMethodService } mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; + mBigramSuggestionEnabled = sp.getBoolean(PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions; updateCorrectionMode(); updateAutoTextEnabled(mResources.getConfiguration().locale); mLanguageSwitcher.loadLocales(sp); @@ -1934,14 +2367,18 @@ public class LatinIME extends InputMethodService 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)); + mSuggestPuncs = mResources.getString(R.string.suggested_punctuations); + if (mSuggestPuncs != null) { + for (int i = 0; i < mSuggestPuncs.length(); i++) { + mSuggestPuncList.add(mSuggestPuncs.subSequence(i, i + 1)); } } } + private boolean isSuggestedPunctuation(int code) { + return mSuggestPuncs.contains(String.valueOf((char)code)); + } + private void showOptionsMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); @@ -1970,7 +2407,7 @@ public class LatinIME extends InputMethodService mOptionsDialog = builder.create(); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mInputView.getWindowToken(); + lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); @@ -1980,7 +2417,7 @@ public class LatinIME extends InputMethodService private void changeKeyboardMode() { mKeyboardSwitcher.toggleSymbols(); if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + mKeyboardSwitcher.setShiftLocked(mCapsLock); } updateShiftKeyState(getCurrentInputEditorInfo()); @@ -2010,19 +2447,17 @@ public class LatinIME extends InputMethodService p.println(" TextEntryState.state=" + TextEntryState.getState()); p.println(" mSoundOn=" + mSoundOn); p.println(" mVibrateOn=" + mVibrateOn); + p.println(" mPopupOn=" + mPopupOn); } // 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; diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java index 21b967420..806ef00af 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java +++ b/java/src/com/android/inputmethod/latin/LatinIMESettings.java @@ -24,13 +24,13 @@ import android.app.Dialog; import android.app.backup.BackupManager; import android.content.DialogInterface; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; 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.SpeechRecognizer; import android.text.AutoText; import android.util.Log; @@ -43,11 +43,9 @@ public class LatinIMESettings extends PreferenceActivity 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 DEBUG_MODE_KEY = "debug_mode"; private static final String TAG = "LatinIMESettings"; @@ -55,7 +53,7 @@ public class LatinIMESettings extends PreferenceActivity private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; private CheckBoxPreference mQuickFixes; - private CheckBoxPreference mShowSuggestions; + private CheckBoxPreference mDebugMode; private ListPreference mVoicePreference; private boolean mVoiceOn; @@ -69,7 +67,6 @@ public class LatinIMESettings extends PreferenceActivity 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); @@ -77,6 +74,9 @@ public class LatinIMESettings extends PreferenceActivity mVoiceModeOff = getString(R.string.voice_mode_off); mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); mLogger = VoiceInputLogger.getLogger(this); + + mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY); + updateDebugMode(mDebugMode.isChecked()); } @Override @@ -110,11 +110,35 @@ public class LatinIMESettings extends PreferenceActivity .equals(mVoiceModeOff)) { showVoiceConfirmation(); } + } else if (key.equals(DEBUG_MODE_KEY)) { + updateDebugMode(prefs.getBoolean(DEBUG_MODE_KEY, false)); } mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); updateVoiceModeSummary(); } + private void updateDebugMode(boolean isDebugMode) { + if (mDebugMode == null) { + return; + } + String version = ""; + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + version = "Version " + info.versionName; + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not find version info."); + } + if (!isDebugMode) { + mDebugMode.setEnabled(false); + mDebugMode.setTitle(version); + mDebugMode.setSummary(""); + } else { + mDebugMode.setEnabled(true); + mDebugMode.setTitle(getResources().getString(R.string.prefs_debug_mode)); + mDebugMode.setSummary(version); + } + } + private void showVoiceConfirmation() { mOkClicked = false; showDialog(VOICE_INPUT_CONFIRM_DIALOG); diff --git a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java new file mode 100644 index 000000000..838b4fe10 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.os.AsyncTask; +import android.text.format.DateUtils; +import android.util.Log; + +public class LatinIMEUtil { + + /** + * Cancel an {@link AsyncTask}. + * + * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + */ + public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) { + if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { + task.cancel(mayInterruptIfRunning); + } + } + + public static class GCUtils { + private static final String TAG = "GCUtils"; + public static final int GC_TRY_COUNT = 2; + // GC_TRY_LOOP_MAX is used for the hard limit of GC wait, + // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT. + public static final int GC_TRY_LOOP_MAX = 5; + private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS; + private static GCUtils sInstance = new GCUtils(); + private int mGCTryCount = 0; + + public static GCUtils getInstance() { + return sInstance; + } + + public void reset() { + mGCTryCount = 0; + } + + public boolean tryGCOrWait(String metaData, Throwable t) { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Encountered Exception or Error. Try GC."); + } + if (mGCTryCount == 0) { + System.gc(); + } + if (++mGCTryCount > GC_TRY_COUNT) { + LatinImeLogger.logOnException(metaData, t); + return false; + } else { + try { + Thread.sleep(GC_INTERVAL); + return true; + } catch (InterruptedException e) { + Log.e(TAG, "Sleep was interrupted."); + LatinImeLogger.logOnException(metaData, t); + return false; + } + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java new file mode 100644 index 000000000..19eead0a0 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -0,0 +1,847 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.Dictionary.DataType; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.AsyncTask; +import android.os.DropBoxManager; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Pair; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "LatinIMELogs"; + public static boolean sDBG = false; + private static boolean sPRINTLOGGING = false; + // SUPPRESS_EXCEPTION should be true when released to public. + private static final boolean SUPPRESS_EXCEPTION = true; + // DEFAULT_LOG_ENABLED should be false when released to public. + private static final boolean DEFAULT_LOG_ENABLED = false; + + private static final long MINIMUMSENDINTERVAL = 300 * DateUtils.SECOND_IN_MILLIS; // 300 sec + private static final long MINIMUMCOUNTINTERVAL = 20 * DateUtils.SECOND_IN_MILLIS; // 20 sec + private static final long MINIMUMSENDSIZE = 40; + private static final char SEPARATER = ';'; + private static final char NULL_CHAR = '\uFFFC'; + private static final int EXCEPTION_MAX_LENGTH = 400; + + // ID_MANUALSUGGESTION has been replaced by ID_MANUALSUGGESTION_WITH_DATATYPE + // private static final int ID_MANUALSUGGESTION = 0; + private static final int ID_AUTOSUGGESTIONCANCELLED = 1; + private static final int ID_AUTOSUGGESTION = 2; + private static final int ID_INPUT_COUNT = 3; + private static final int ID_DELETE_COUNT = 4; + private static final int ID_WORD_COUNT = 5; + private static final int ID_ACTUAL_CHAR_COUNT = 6; + private static final int ID_THEME_ID = 7; + private static final int ID_SETTING_AUTO_COMPLETE = 8; + private static final int ID_VERSION = 9; + private static final int ID_EXCEPTION = 10; + private static final int ID_MANUALSUGGESTIONCOUNT = 11; + private static final int ID_AUTOSUGGESTIONCANCELLEDCOUNT = 12; + private static final int ID_AUTOSUGGESTIONCOUNT = 13; + private static final int ID_LANGUAGES = 14; + private static final int ID_MANUALSUGGESTION_WITH_DATATYPE = 15; + + private static final String PREF_ENABLE_LOG = "enable_logging"; + private static final String PREF_DEBUG_MODE = "debug_mode"; + private static final String PREF_AUTO_COMPLETE = "auto_complete"; + + public static boolean sLogEnabled = true; + /* package */ static LatinImeLogger sLatinImeLogger = new LatinImeLogger(); + // Store the last auto suggested word. + // This is required for a cancellation log of auto suggestion of that word. + /* package */ static String sLastAutoSuggestBefore; + /* package */ static String sLastAutoSuggestAfter; + /* package */ static String sLastAutoSuggestSeparator; + // This value holds MAIN, USER, AUTO, etc... + private static int sLastAutoSuggestDicTypeId; + // This value holds 0 (= unigram), 1 (= bigram) etc... + private static int sLastAutoSuggestDataType; + private static HashMap<String, Pair<Integer, Integer>> sSuggestDicMap + = new HashMap<String, Pair<Integer, Integer>>(); + private static String[] sPreviousWords; + private static DebugKeyEnabler sDebugKeyEnabler = new DebugKeyEnabler(); + + private ArrayList<LogEntry> mLogBuffer = null; + private ArrayList<LogEntry> mPrivacyLogBuffer = null; + /* package */ RingCharBuffer mRingCharBuffer = null; + + private Context mContext = null; + private DropBoxManager mDropBox = null; + private AddTextToDropBoxTask mAddTextToDropBoxTask; + private long mLastTimeActive; + private long mLastTimeSend; + private long mLastTimeCountEntry; + + private String mThemeId; + private String mSelectedLanguages; + private String mCurrentLanguage; + private int mDeleteCount; + private int mInputCount; + private int mWordCount; + private int[] mAutoSuggestCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int[] mManualSuggestCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int[] mAutoCancelledCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int mActualCharCount; + + private static class LogEntry implements Comparable<LogEntry> { + public final int mTag; + public final String[] mData; + public long mTime; + + public LogEntry (long time, int tag, String[] data) { + mTag = tag; + mTime = time; + mData = data; + } + + public int compareTo(LogEntry log2) { + if (mData.length == 0 && log2.mData.length == 0) { + return 0; + } else if (mData.length == 0) { + return 1; + } else if (log2.mData.length == 0) { + return -1; + } + return log2.mData[0].compareTo(mData[0]); + } + } + + private class AddTextToDropBoxTask extends AsyncTask<Void, Void, Void> { + private final DropBoxManager mDropBox; + private final long mTime; + private final String mData; + public AddTextToDropBoxTask(DropBoxManager db, long time, String data) { + mDropBox = db; + mTime = time; + mData = data; + } + @Override + protected Void doInBackground(Void... params) { + if (sPRINTLOGGING) { + Log.d(TAG, "Commit log: " + mData); + } + mDropBox.addText(TAG, mData); + return null; + } + @Override + protected void onPostExecute(Void v) { + mLastTimeSend = mTime; + } + } + + private void initInternal(Context context) { + mContext = context; + mDropBox = (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE); + mLastTimeSend = System.currentTimeMillis(); + mLastTimeActive = mLastTimeSend; + mLastTimeCountEntry = mLastTimeSend; + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + mLogBuffer = new ArrayList<LogEntry>(); + mPrivacyLogBuffer = new ArrayList<LogEntry>(); + mRingCharBuffer = new RingCharBuffer(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + sLogEnabled = prefs.getBoolean(PREF_ENABLE_LOG, DEFAULT_LOG_ENABLED); + mThemeId = prefs.getString(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT, + KeyboardSwitcher.DEFAULT_LAYOUT_ID); + mSelectedLanguages = prefs.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + mCurrentLanguage = prefs.getString(LatinIME.PREF_INPUT_LANGUAGE, ""); + sPRINTLOGGING = prefs.getBoolean(PREF_DEBUG_MODE, sPRINTLOGGING); + sDBG = sPRINTLOGGING; + prefs.registerOnSharedPreferenceChangeListener(this); + } + + /** + * Clear all logged data + */ + private void reset() { + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + mLogBuffer.clear(); + mPrivacyLogBuffer.clear(); + mRingCharBuffer.reset(); + } + + public void destroy() { + LatinIMEUtil.cancelTask(mAddTextToDropBoxTask, false); + } + + /** + * Check if the input string is safe as an entry or not. + */ + private static boolean checkStringDataSafe(String s) { + if (sDBG) { + Log.d(TAG, "Check String safety: " + s); + } + for (int i = 0; i < s.length(); ++i) { + if (Character.isDigit(s.charAt(i))) { + return false; + } + } + return true; + } + + private void addCountEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log counts. (4)"); + } + mLogBuffer.add(new LogEntry (time, ID_DELETE_COUNT, + new String[] {String.valueOf(mDeleteCount)})); + mLogBuffer.add(new LogEntry (time, ID_INPUT_COUNT, + new String[] {String.valueOf(mInputCount)})); + mLogBuffer.add(new LogEntry (time, ID_WORD_COUNT, + new String[] {String.valueOf(mWordCount)})); + mLogBuffer.add(new LogEntry (time, ID_ACTUAL_CHAR_COUNT, + new String[] {String.valueOf(mActualCharCount)})); + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + mLastTimeCountEntry = time; + } + + private void addSuggestionCountEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "log suggest counts. (1)"); + } + String[] s = new String[mAutoSuggestCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mAutoSuggestCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_AUTOSUGGESTIONCOUNT, s)); + + s = new String[mAutoCancelledCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mAutoCancelledCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_AUTOSUGGESTIONCANCELLEDCOUNT, s)); + + s = new String[mManualSuggestCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mManualSuggestCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_MANUALSUGGESTIONCOUNT, s)); + + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + } + + private void addThemeIdEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log theme Id. (1)"); + } + // TODO: Not to convert theme ID here. Currently "2" is treated as "6" in a log server. + if (mThemeId.equals("2")) { + mThemeId = "6"; + } else if (mThemeId.equals("3")) { + mThemeId = "7"; + } + mLogBuffer.add(new LogEntry (time, ID_THEME_ID, + new String[] {mThemeId})); + } + + private void addLanguagesEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log language settings. (1)"); + } + // CurrentLanguage and SelectedLanguages will be blank if user doesn't use multi-language + // switching. + if (TextUtils.isEmpty(mCurrentLanguage)) { + mCurrentLanguage = mContext.getResources().getConfiguration().locale.toString(); + } + mLogBuffer.add(new LogEntry (time, ID_LANGUAGES, + new String[] {mCurrentLanguage , mSelectedLanguages})); + } + + private void addSettingsEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log settings. (1)"); + } + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + mLogBuffer.add(new LogEntry (time, ID_SETTING_AUTO_COMPLETE, + new String[] {String.valueOf(prefs.getBoolean(PREF_AUTO_COMPLETE, + mContext.getResources().getBoolean(R.bool.enable_autocorrect)))})); + } + + private void addVersionNameEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log Version. (1)"); + } + try { + PackageInfo info = mContext.getPackageManager().getPackageInfo( + mContext.getPackageName(), 0); + mLogBuffer.add(new LogEntry (time, ID_VERSION, + new String[] {String.valueOf(info.versionCode), info.versionName})); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not find version name."); + } + } + + private void addExceptionEntry(long time, String[] data) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log Exception. (1)"); + } + mLogBuffer.add(new LogEntry(time, ID_EXCEPTION, data)); + } + + private void flushPrivacyLogSafely() { + if (sPRINTLOGGING) { + Log.d(TAG, "Log obfuscated data. (" + mPrivacyLogBuffer.size() + ")"); + } + long now = System.currentTimeMillis(); + Collections.sort(mPrivacyLogBuffer); + for (LogEntry l: mPrivacyLogBuffer) { + l.mTime = now; + mLogBuffer.add(l); + } + mPrivacyLogBuffer.clear(); + } + + /** + * Add an entry + * @param tag + * @param data + */ + private void addData(int tag, Object data) { + switch (tag) { + case ID_DELETE_COUNT: + if (((mLastTimeActive - mLastTimeCountEntry) > MINIMUMCOUNTINTERVAL) + || (mDeleteCount == 0 && mInputCount == 0)) { + addCountEntry(mLastTimeActive); + } + mDeleteCount += (Integer)data; + break; + case ID_INPUT_COUNT: + if (((mLastTimeActive - mLastTimeCountEntry) > MINIMUMCOUNTINTERVAL) + || (mDeleteCount == 0 && mInputCount == 0)) { + addCountEntry(mLastTimeActive); + } + mInputCount += (Integer)data; + break; + case ID_MANUALSUGGESTION_WITH_DATATYPE: + case ID_AUTOSUGGESTION: + ++mWordCount; + String[] dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + mActualCharCount += dataStrings[1].length(); + if (checkStringDataSafe(dataStrings[0]) && checkStringDataSafe(dataStrings[1])) { + mPrivacyLogBuffer.add( + new LogEntry (System.currentTimeMillis(), tag, dataStrings)); + } else { + if (sDBG) { + Log.d(TAG, "Skipped to add an entry because data is unsafe."); + } + } + break; + case ID_AUTOSUGGESTIONCANCELLED: + --mWordCount; + dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + mActualCharCount -= dataStrings[1].length(); + if (checkStringDataSafe(dataStrings[0]) && checkStringDataSafe(dataStrings[1])) { + mPrivacyLogBuffer.add( + new LogEntry (System.currentTimeMillis(), tag, dataStrings)); + } else { + if (sDBG) { + Log.d(TAG, "Skipped to add an entry because data is unsafe."); + } + } + break; + case ID_EXCEPTION: + dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + addExceptionEntry(System.currentTimeMillis(), dataStrings); + break; + default: + if (sDBG) { + Log.e(TAG, "Log Tag is not entried."); + } + break; + } + } + + private void commitInternal() { + // if there is no log entry in mLogBuffer, will not send logs to DropBox. + if (!mLogBuffer.isEmpty() && (mAddTextToDropBoxTask == null + || mAddTextToDropBoxTask.getStatus() == AsyncTask.Status.FINISHED)) { + if (sPRINTLOGGING) { + Log.d(TAG, "Commit (" + mLogBuffer.size() + ")"); + } + flushPrivacyLogSafely(); + long now = System.currentTimeMillis(); + addCountEntry(now); + addThemeIdEntry(now); + addLanguagesEntry(now); + addSettingsEntry(now); + addVersionNameEntry(now); + addSuggestionCountEntry(now); + String s = LogSerializer.createStringFromEntries(mLogBuffer); + reset(); + mAddTextToDropBoxTask = (AddTextToDropBoxTask) new AddTextToDropBoxTask( + mDropBox, now, s).execute(); + } + } + + private void commitInternalAndStopSelf() { + if (sDBG) { + Log.e(TAG, "Exception was thrown and let's die."); + } + commitInternal(); + LatinIME ime = ((LatinIME) mContext); + ime.hideWindow(); + ime.stopSelf(); + } + + private synchronized void sendLogToDropBox(int tag, Object s) { + long now = System.currentTimeMillis(); + if (sDBG) { + String out = ""; + if (s instanceof String[]) { + for (String str: ((String[]) s)) { + out += str + ","; + } + } else if (s instanceof Integer) { + out += (Integer) s; + } + Log.d(TAG, "SendLog: " + tag + ";" + out + ", will be sent after " + + (- (now - mLastTimeSend - MINIMUMSENDINTERVAL) / 1000) + " sec."); + } + if (now - mLastTimeActive > MINIMUMSENDINTERVAL) { + // Send a log before adding an log entry if the last data is too old. + commitInternal(); + addData(tag, s); + } else if (now - mLastTimeSend > MINIMUMSENDINTERVAL) { + // Send a log after adding an log entry. + addData(tag, s); + commitInternal(); + } else { + addData(tag, s); + } + mLastTimeActive = now; + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PREF_ENABLE_LOG.equals(key)) { + if (sharedPreferences.getBoolean(key, DEFAULT_LOG_ENABLED)) { + sLogEnabled = (mContext != null); + } else { + sLogEnabled = false; + } + if (sDebugKeyEnabler.check()) { + sharedPreferences.edit().putBoolean(PREF_DEBUG_MODE, true).commit(); + } + } else if (KeyboardSwitcher.PREF_KEYBOARD_LAYOUT.equals(key)) { + mThemeId = sharedPreferences.getString(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT, + KeyboardSwitcher.DEFAULT_LAYOUT_ID); + addThemeIdEntry(mLastTimeActive); + } else if (PREF_DEBUG_MODE.equals(key)) { + sPRINTLOGGING = sharedPreferences.getBoolean(PREF_DEBUG_MODE, sPRINTLOGGING); + sDBG = sPRINTLOGGING; + } else if (LatinIME.PREF_INPUT_LANGUAGE.equals(key)) { + mCurrentLanguage = sharedPreferences.getString(LatinIME.PREF_INPUT_LANGUAGE, ""); + addLanguagesEntry(mLastTimeActive); + } else if (LatinIME.PREF_INPUT_LANGUAGE.equals(key)) { + mSelectedLanguages = sharedPreferences.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + } + } + + public static void init(Context context) { + sLatinImeLogger.initInternal(context); + } + + public static void commit() { + if (sLogEnabled) { + if (System.currentTimeMillis() - sLatinImeLogger.mLastTimeActive > MINIMUMCOUNTINTERVAL + || (sLatinImeLogger.mLogBuffer.size() + + sLatinImeLogger.mPrivacyLogBuffer.size() > MINIMUMSENDSIZE)) { + sLatinImeLogger.commitInternal(); + } + } + } + + public static void onDestroy() { + sLatinImeLogger.commitInternal(); + sLatinImeLogger.destroy(); + } + + // TODO: Handle CharSequence instead of String + public static void logOnManualSuggestion(String before, String after, int position + , List<CharSequence> suggestions) { + if (sLogEnabled) { + // log punctuation + if (before.length() == 0 && after.length() == 1) { + sLatinImeLogger.sendLogToDropBox(ID_MANUALSUGGESTION_WITH_DATATYPE, new String[] { + before, after, String.valueOf(position), ""}); + } else if (!sSuggestDicMap.containsKey(after)) { + if (sDBG) { + Log.e(TAG, "logOnManualSuggestion was cancelled: from unknown dic."); + } + } else { + int dicTypeId = sSuggestDicMap.get(after).first; + sLatinImeLogger.mManualSuggestCountPerDic[dicTypeId]++; + if (dicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnManualSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + sPreviousWords = null; + } + // TODO: Don't send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + + /* Example: + * When user typed "Illegal imm" and picked "immigrants", + * the suggestion list has "immigrants, immediate, immigrant". + * At this time, the log strings will be something like below: + * strings[0 = COLUMN_BEFORE_ID] = imm + * strings[1 = COLUMN_AFTER_ID] = immigrants + * strings[2 = COLUMN_PICKED_POSITION_ID] = 0 + * strings[3 = COLUMN_SUGGESTION_LENGTH_ID] = 3 + * strings[4 = COLUMN_PREVIOUS_WORDS_COUNT_ID] = 1 + * strings[5] = immigrants + * strings[6] = immediate + * strings[7] = immigrant + * strings[8] = 1 (= bigram) + * strings[9] = 0 (= unigram) + * strings[10] = 1 (= bigram) + * strings[11] = Illegal + */ + + // 0 for unigram, 1 for bigram, 2 for trigram... + int previousWordsLength = (sPreviousWords == null) ? 0 : sPreviousWords.length; + int suggestionLength = suggestions.size(); + + final int COLUMN_BEFORE_ID = 0; + final int COLUMN_AFTER_ID = 1; + final int COLUMN_PICKED_POSITION_ID = 2; + final int COLUMN_SUGGESTION_LENGTH_ID = 3; + final int COLUMN_PREVIOUS_WORDS_COUNT_ID = 4; + final int BASE_COLUMN_SIZE = 5; + + String[] strings = + new String[BASE_COLUMN_SIZE + suggestionLength * 2 + previousWordsLength]; + strings[COLUMN_BEFORE_ID] = before; + strings[COLUMN_AFTER_ID] = after; + strings[COLUMN_PICKED_POSITION_ID] = String.valueOf(position); + strings[COLUMN_SUGGESTION_LENGTH_ID] = String.valueOf(suggestionLength); + strings[COLUMN_PREVIOUS_WORDS_COUNT_ID] = String.valueOf(previousWordsLength); + + for (int i = 0; i < suggestionLength; ++i) { + String s = suggestions.get(i).toString(); + if (sSuggestDicMap.containsKey(s)) { + strings[BASE_COLUMN_SIZE + i] = s; + strings[BASE_COLUMN_SIZE + suggestionLength + i] + = sSuggestDicMap.get(s).second.toString(); + } else { + strings[BASE_COLUMN_SIZE + i] = ""; + strings[BASE_COLUMN_SIZE + suggestionLength + i] = ""; + } + } + + for (int i = 0; i < previousWordsLength; ++i) { + strings[BASE_COLUMN_SIZE + suggestionLength * 2 + i] = sPreviousWords[i]; + } + + sLatinImeLogger.sendLogToDropBox(ID_MANUALSUGGESTION_WITH_DATATYPE, strings); + } + } + sSuggestDicMap.clear(); + } + } + + public static void logOnAutoSuggestion(String before, String after) { + if (sLogEnabled) { + if (!sSuggestDicMap.containsKey(after)) { + if (sDBG) { + Log.e(TAG, "logOnAutoSuggestion was cancelled: from unknown dic."); + } + } else { + String separator = String.valueOf(sLatinImeLogger.mRingCharBuffer.getLastChar()); + sLastAutoSuggestDicTypeId = sSuggestDicMap.get(after).first; + sLastAutoSuggestDataType = sSuggestDicMap.get(after).second; + sLatinImeLogger.mAutoSuggestCountPerDic[sLastAutoSuggestDicTypeId]++; + if (sLastAutoSuggestDicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnAutoSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + sPreviousWords = null; + } + // TODO: Not to send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + int previousWordsLength = (sPreviousWords == null) ? 0 : sPreviousWords.length; + + final int COLUMN_BEFORE_ID = 0; + final int COLUMN_AFTER_ID = 1; + final int COLUMN_SEPARATOR_ID = 2; + final int COLUMN_DATA_TYPE_ID = 3; + final int BASE_COLUMN_SIZE = 4; + + String[] strings = new String[4 + previousWordsLength]; + strings[COLUMN_BEFORE_ID] = before; + strings[COLUMN_AFTER_ID] = after; + strings[COLUMN_SEPARATOR_ID] = separator; + strings[COLUMN_DATA_TYPE_ID] = String.valueOf(sLastAutoSuggestDataType); + for (int i = 0; i < previousWordsLength; ++i) { + strings[BASE_COLUMN_SIZE + i] = sPreviousWords[i]; + } + sLatinImeLogger.sendLogToDropBox(ID_AUTOSUGGESTION, strings); + } + synchronized (LatinImeLogger.class) { + sLastAutoSuggestBefore = before; + sLastAutoSuggestAfter = after; + sLastAutoSuggestSeparator = separator; + } + } + sSuggestDicMap.clear(); + } + } + + public static void logOnAutoSuggestionCanceled() { + if (sLogEnabled) { + sLatinImeLogger.mAutoCancelledCountPerDic[sLastAutoSuggestDicTypeId]++; + if (sLastAutoSuggestBefore != null && sLastAutoSuggestAfter != null) { + String[] strings = new String[] { + sLastAutoSuggestBefore, sLastAutoSuggestAfter, sLastAutoSuggestSeparator}; + sLatinImeLogger.sendLogToDropBox(ID_AUTOSUGGESTIONCANCELLED, strings); + } + synchronized (LatinImeLogger.class) { + sLastAutoSuggestBefore = ""; + sLastAutoSuggestAfter = ""; + sLastAutoSuggestSeparator = ""; + } + } + } + + public static void logOnDelete() { + if (sLogEnabled) { + String mLastWord = sLatinImeLogger.mRingCharBuffer.getLastString(); + if (!TextUtils.isEmpty(mLastWord) + && mLastWord.equalsIgnoreCase(sLastAutoSuggestBefore)) { + logOnAutoSuggestionCanceled(); + } + sLatinImeLogger.mRingCharBuffer.pop(); + sLatinImeLogger.sendLogToDropBox(ID_DELETE_COUNT, 1); + } + } + + public static void logOnInputChar(char c) { + if (sLogEnabled) { + sLatinImeLogger.mRingCharBuffer.push(c); + sLatinImeLogger.sendLogToDropBox(ID_INPUT_COUNT, 1); + } + } + + public static void logOnException(String metaData, Throwable e) { + if (sLogEnabled) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + e.printStackTrace(ps); + String exceptionString = URLEncoder.encode(new String(baos.toByteArray(), 0, + Math.min(EXCEPTION_MAX_LENGTH, baos.size()))); + sLatinImeLogger.sendLogToDropBox( + ID_EXCEPTION, new String[] {metaData, exceptionString}); + if (sDBG) { + Log.e(TAG, "Exception: " + new String(baos.toByteArray())+ ":" + exceptionString); + } + if (SUPPRESS_EXCEPTION) { + sLatinImeLogger.commitInternalAndStopSelf(); + } else { + sLatinImeLogger.commitInternal(); + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else if (e instanceof Error) { + throw (Error) e; + } + } + } + } + + public static void logOnWarning(String warning) { + if (sLogEnabled) { + sLatinImeLogger.sendLogToDropBox( + ID_EXCEPTION, new String[] {warning, ""}); + } + } + + // TODO: This code supports only Bigram. + public static void onStartSuggestion(CharSequence previousWords) { + if (sLogEnabled) { + sSuggestDicMap.clear(); + sPreviousWords = new String[] { + (previousWords == null) ? "" : previousWords.toString()}; + } + } + + public static void onAddSuggestedWord(String word, int typeId, DataType dataType) { + if (sLogEnabled) { + sSuggestDicMap.put(word, new Pair<Integer, Integer>(typeId, dataType.ordinal())); + } + } + + private static class LogSerializer { + private static void appendWithLength(StringBuffer sb, String data) { + sb.append(data.length()); + sb.append(SEPARATER); + sb.append(data); + sb.append(SEPARATER); + } + + private static void appendLogEntry(StringBuffer sb, String time, String tag, + String[] data) { + if (data.length > 0) { + appendWithLength(sb, String.valueOf(data.length + 2)); + appendWithLength(sb, time); + appendWithLength(sb, tag); + for (String s: data) { + appendWithLength(sb, s); + } + } + } + + public static String createStringFromEntries(ArrayList<LogEntry> logs) { + StringBuffer sb = new StringBuffer(); + for (LogEntry log: logs) { + appendLogEntry(sb, String.valueOf(log.mTime), String.valueOf(log.mTag), log.mData); + } + return sb.toString(); + } + } + + /* package */ static class RingCharBuffer { + final int BUFSIZE = 20; + private Context mContext; + private int mEnd = 0; + /* package */ int length = 0; + private char[] mCharBuf = new char[BUFSIZE]; + + public RingCharBuffer(Context context) { + mContext = context; + } + + private int normalize(int in) { + int ret = in % BUFSIZE; + return ret < 0 ? ret + BUFSIZE : ret; + } + public void push(char c) { + mCharBuf[mEnd] = c; + mEnd = normalize(mEnd + 1); + if (length < BUFSIZE) { + ++length; + } + } + public char pop() { + if (length < 1) { + return NULL_CHAR; + } else { + mEnd = normalize(mEnd - 1); + --length; + return mCharBuf[mEnd]; + } + } + public char getLastChar() { + if (length < 1) { + return NULL_CHAR; + } else { + return mCharBuf[normalize(mEnd - 1)]; + } + } + public String getLastString() { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; ++i) { + char c = mCharBuf[normalize(mEnd - 1 - i)]; + if (!((LatinIME)mContext).isWordSeparator(c)) { + sb.append(c); + } else { + break; + } + } + return sb.reverse().toString(); + } + public void reset() { + length = 0; + } + } + + private static class DebugKeyEnabler { + private int mCounter = 0; + private long mLastTime = 0; + public boolean check() { + if (System.currentTimeMillis() - mLastTime > 10 * 1000) { + mCounter = 0; + mLastTime = System.currentTimeMillis(); + } else if (++mCounter >= 10) { + return true; + } + return false; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java index 6aea5d13a..db4d167d4 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -47,7 +47,6 @@ public class LatinKeyboard extends Keyboard { private Drawable mShiftLockIcon; private Drawable mShiftLockPreviewIcon; private Drawable mOldShiftIcon; - private Drawable mOldShiftPreviewIcon; private Drawable mSpaceIcon; private Drawable mSpacePreviewIcon; private Drawable mMicIcon; @@ -68,7 +67,6 @@ public class LatinKeyboard extends Keyboard { 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 @@ -77,16 +75,16 @@ public class LatinKeyboard extends Keyboard { 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 int mExtensionResId; + // TODO: generalize for any keyboardId + private boolean mIsBlackSym; + private static final int SHIFT_OFF = 0; private static final int SHIFT_ON = 1; private static final int SHIFT_LOCKED = 2; @@ -107,7 +105,6 @@ public class LatinKeyboard extends Keyboard { 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); @@ -126,7 +123,8 @@ public class LatinKeyboard extends Keyboard { setDefaultBounds(m123MicPreviewIcon); sSpacebarVerticalCorrection = res.getDimensionPixelOffset( R.dimen.spacebar_vertical_correction); - mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty; + mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty + || xmlLayoutResId == R.xml.kbd_qwerty_black; mSpaceKeyIndex = indexOf((int) ' '); } @@ -182,8 +180,8 @@ public class LatinKeyboard extends Keyboard { 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.icon = res.getDrawable(mIsBlackSym ? + R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search); mEnterKey.label = null; break; case EditorInfo.IME_ACTION_SEND: @@ -201,8 +199,8 @@ public class LatinKeyboard extends Keyboard { } else { mEnterKey.iconPreview = res.getDrawable( R.drawable.sym_keyboard_feedback_return); - mEnterKey.icon = res.getDrawable( - R.drawable.sym_keyboard_return); + mEnterKey.icon = res.getDrawable(mIsBlackSym ? + R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return); mEnterKey.label = null; } break; @@ -224,7 +222,6 @@ public class LatinKeyboard extends Keyboard { ((LatinKey)mShiftKey).enableShiftLock(); } mOldShiftIcon = mShiftKey.icon; - mOldShiftPreviewIcon = mShiftKey.iconPreview; } } @@ -277,6 +274,10 @@ public class LatinKeyboard extends Keyboard { } } + /* package */ boolean isAlphaKeyboard() { + return mIsAlphaKeyboard; + } + public void setExtension(int resId) { mExtensionResId = resId; } @@ -285,6 +286,26 @@ public class LatinKeyboard extends Keyboard { return mExtensionResId; } + public void setBlackFlag(boolean f) { + mIsBlackSym = f; + if (f) { + mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked); + mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space); + mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic); + m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic); + } else { + mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked); + mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space); + mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic); + m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic); + } + updateF1Key(); + if (mSpaceKey != null) { + mSpaceKey.icon = mSpaceIcon; + updateSpaceBarForLocale(f); + } + } + private void setDefaultBounds(Drawable drawable) { drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } @@ -322,39 +343,40 @@ public class LatinKeyboard extends Keyboard { } } - private void updateSpaceBarForLocale() { + private void updateSpaceBarForLocale(boolean isBlack) { 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); + drawSpaceBar(canvas, buffer.getWidth(), buffer.getHeight(), 255, isBlack); mSpaceKey.icon = new BitmapDrawable(mRes, buffer); mSpaceKey.repeatable = mLanguageSwitcher.getLocaleCount() < 2; } else { - mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space); + mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space) + : 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); + private void drawSpaceBar(Canvas canvas, int width, int height, int opacity, boolean isBlack) { + canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), 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); + + int shadowColor = isBlack ? mRes.getColor(R.color.latinkeyboard_bar_language_shadow_black) + : mRes.getColor(R.color.latinkeyboard_bar_language_shadow_white); + + paint.setColor(shadowColor); + canvas.drawText(language, width / 2, ascent - 1, paint); + paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text)); + canvas.drawText(language, width / 2, ascent, paint); // Put arrows on either side of the text if (mLanguageSwitcher.getLocaleCount() > 1) { Rect bounds = new Rect(); @@ -439,7 +461,7 @@ public class LatinKeyboard extends Keyboard { } if (mLocale != null && mLocale.equals(locale)) return; mLocale = locale; - updateSpaceBarForLocale(); + updateSpaceBarForLocale(mIsBlackSym); } boolean isCurrentlyInSpace() { @@ -503,9 +525,10 @@ public class LatinKeyboard extends Keyboard { // 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 !!!!!!"); + if (DEBUG_PREFERRED_LETTER) { + if (mPrefLetter == code && !key.isInsideSuper(x, y)) { + Log.d(TAG, "CORRECTED !!!!!!"); + } } return mPrefLetter == code; } else { @@ -684,7 +707,7 @@ public class LatinKeyboard extends Keyboard { mTextPaint = new TextPaint(); int textSize = getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18); mTextPaint.setTextSize(textSize); - mTextPaint.setColor(0); + mTextPaint.setColor(R.color.latinkeyboard_transparent); mTextPaint.setTextAlign(Align.CENTER); mTextPaint.setAlpha(255); mTextPaint.setAntiAlias(true); @@ -718,7 +741,7 @@ public class LatinKeyboard extends Keyboard { public void draw(Canvas canvas) { canvas.save(); if (mHitThreshold) { - mTextPaint.setColor(0xFF000000); + mTextPaint.setColor(mRes.getColor(R.color.latinkeyboard_text_color)); canvas.clipRect(0, 0, mWidth, mHeight); if (mCurrentLanguage == null) { mCurrentLanguage = getInputLanguage(mWidth, mTextPaint); diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java new file mode 100644 index 000000000..665c641c2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java @@ -0,0 +1,1633 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.Paint.Align; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup.LayoutParams; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and + * detecting key presses and touch movements. + * + * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground + * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout + * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset + * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize + * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize + * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor + * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection + * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout + */ +public class LatinKeyboardBaseView extends View implements View.OnClickListener { + + public interface OnKeyboardActionListener { + + /** + * Called when the user presses a key. This is sent before the + * {@link #onKey} is called. For keys that repeat, this is only + * called once. + * + * @param primaryCode + * the unicode of the key being pressed. If the touch is + * not on a valid key, the value will be zero. + */ + void onPress(int primaryCode); + + /** + * Called when the user releases a key. This is sent after the + * {@link #onKey} is called. For keys that repeat, this is only + * called once. + * + * @param primaryCode + * the code of the key that was released + */ + void onRelease(int primaryCode); + + /** + * Send a key press to the listener. + * + * @param primaryCode + * this is the key that was pressed + * @param keyCodes + * the codes for all the possible alternative keys with + * the primary code being the first. If the primary key + * code is a single character such as an alphabet or + * number or symbol, the alternatives will include other + * characters that may be on the same key or adjacent + * keys. These codes are useful to correct for + * accidental presses of a key adjacent to the intended + * key. + */ + void onKey(int primaryCode, int[] keyCodes); + + /** + * Sends a sequence of characters to the listener. + * + * @param text + * the sequence of characters to be displayed. + */ + void onText(CharSequence text); + + /** + * Called when the user quickly moves the finger from right to + * left. + */ + void swipeLeft(); + + /** + * Called when the user quickly moves the finger from left to + * right. + */ + void swipeRight(); + + /** + * Called when the user quickly moves the finger from up to down. + */ + void swipeDown(); + + /** + * Called when the user quickly moves the finger from down to up. + */ + void swipeUp(); + } + + private static final boolean DEBUG = false; + private static final int NOT_A_KEY = -1; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; + + private Keyboard mKeyboard; + private int mCurrentKeyIndex = NOT_A_KEY; + private int mLabelTextSize; + private int mKeyTextSize; + private int mKeyTextColor; + private float mShadowRadius; + private int mShadowColor; + private float mBackgroundDimAmount; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mPreviewTextSizeLarge; + private int mPreviewOffset; + private int mPreviewHeight; + private int[] mOffsetInWindow; + + private PopupWindow mPopupKeyboard; + private View mMiniKeyboardContainer; + private LatinKeyboardBaseView mMiniKeyboard; + private boolean mMiniKeyboardOnScreen; + private View mPopupParent; + private int mMiniKeyboardOffsetX; + private int mMiniKeyboardOffsetY; + private Map<Key,View> mMiniKeyboardCache; + private int[] mWindowOffset; + private Key[] mKeys; + private Typeface mKeyTextStyle = Typeface.DEFAULT; + private int mSymbolColorScheme = 0; + + /** Listener for {@link OnKeyboardActionListener}. */ + private OnKeyboardActionListener mKeyboardActionListener; + + private static final int DELAY_BEFORE_PREVIEW = 0; + private static final int DELAY_AFTER_PREVIEW = 70; + private static final int DEBOUNCE_TIME = 70; + + private int mVerticalCorrection; + private int mProximityThreshold; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = true; + private int mPopupPreviewX; + private int mPopupPreviewY; + private int mWindowY; + + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private int mCurrentKey = NOT_A_KEY; + private int mDownKey = NOT_A_KEY; + private int mStartX; + private int mStartY; + + private KeyDebouncer mDebouncer; + + private GestureDetector mGestureDetector; + private int mPopupX; + private int mPopupY; + private int mRepeatKeyIndex = NOT_A_KEY; + private int mPopupLayout; + private boolean mAbortKey; + private Key mInvalidatedKey; + private Rect mClipRegion = new Rect(0, 0, 0, 0); + private boolean mPossiblePoly; + private SwipeTracker mSwipeTracker = new SwipeTracker(); + private int mSwipeThreshold; + private boolean mDisambiguateSwipe; + + // Variables for dealing with multiple pointers + private int mOldPointerCount = 1; + private float mOldPointerX; + private float mOldPointerY; + + private Drawable mKeyBackground; + + private static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int REPEAT_START_DELAY = 400; + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + + private static int MAX_NEARBY_KEYS = 12; + private int[] mDistances = new int[MAX_NEARBY_KEYS]; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private StringBuilder mPreviewLabel = new StringBuilder(1); + + /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ + private boolean mDrawPending; + /** The dirty region in the keyboard bitmap */ + private Rect mDirtyRect = new Rect(); + /** The keyboard bitmap for faster updates */ + private Bitmap mBuffer; + /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ + private boolean mKeyboardChanged; + /** The canvas for the above mutable keyboard bitmap */ + private Canvas mCanvas; + + UIHandler mHandler = new UIHandler(); + + class UIHandler extends Handler { + private static final int MSG_POPUP_PREVIEW = 1; + private static final int MSG_DISMISS_PREVIEW = 2; + private static final int MSG_REPEAT_KEY = 3; + private static final int MSG_LOGPRESS_KEY = 4; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_POPUP_PREVIEW: + showKey(msg.arg1); + break; + case MSG_DISMISS_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT_KEY: + if (repeatKey()) { + startKeyRepeatTimer(REPEAT_INTERVAL); + } + break; + case MSG_LOGPRESS_KEY: + openPopupIfRequired((MotionEvent) msg.obj); + break; + } + } + + public void popupPreview(int keyIndex, long delay) { + removeMessages(MSG_POPUP_PREVIEW); + sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0), delay); + } + + public void cancelPopupPreview() { + removeMessages(MSG_POPUP_PREVIEW); + } + + public void dismissPreview(long delay) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay); + } + + public void cancelDismissPreview() { + removeMessages(MSG_DISMISS_PREVIEW); + } + + public void startKeyRepeatTimer(long delay) { + sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY), delay); + } + + public void startLongPressTimer(MotionEvent me, long delay) { + sendMessageDelayed(obtainMessage(MSG_LOGPRESS_KEY, me), delay); + } + + public void cancelLongPressTimer() { + removeMessages(MSG_LOGPRESS_KEY); + } + + public void cancelKeyTimers() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + } + + public void cancelKeyTimersAndPopupPreview() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + removeMessages(MSG_POPUP_PREVIEW); + } + + public void cancelAllMessages() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + removeMessages(MSG_POPUP_PREVIEW); + removeMessages(MSG_DISMISS_PREVIEW); + } + }; + + static class KeyDebouncer { + private final Key[] mKeys; + private final int mKeyDebounceThresholdSquared; + + // for move de-bouncing + private int mLastCodeX; + private int mLastCodeY; + private int mLastX; + private int mLastY; + + // for time de-bouncing + private int mLastKey; + private long mLastKeyTime; + private long mLastMoveTime; + private long mCurrentKeyTime; + + KeyDebouncer(Key[] keys, float hysteresisPixel) { + if (keys == null || hysteresisPixel < 1.0f) + throw new IllegalArgumentException(); + mKeys = keys; + mKeyDebounceThresholdSquared = (int)(hysteresisPixel * hysteresisPixel); + } + + public int getLastCodeX() { + return mLastCodeX; + } + + public int getLastCodeY() { + return mLastCodeY; + } + + public int getLastX() { + return mLastX; + } + + public int getLastY() { + return mLastY; + } + + public int getLastKey() { + return mLastKey; + } + + public void startMoveDebouncing(int x, int y) { + mLastCodeX = x; + mLastCodeY = y; + } + + public void updateMoveDebouncing(int x, int y) { + mLastX = x; + mLastY = y; + } + + public void resetMoveDebouncing() { + mLastCodeX = mLastX; + mLastCodeY = mLastY; + } + + public boolean isMinorMoveBounce(int x, int y, int newKey, int curKey) { + if (newKey == curKey) { + return true; + } else if (curKey >= 0 && curKey < mKeys.length) { + return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) + < mKeyDebounceThresholdSquared; + } else { + return false; + } + } + + private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { + final int left = key.x; + final int right = key.x + key.width; + final int top = key.y; + final int bottom = key.y + key.height; + final int edgeX = x < left ? left : (x > right ? right : x); + final int edgeY = y < top ? top : (y > bottom ? bottom : y); + final int dx = x - edgeX; + final int dy = y - edgeY; + return dx * dx + dy * dy; + } + + public void startTimeDebouncing(long eventTime) { + mLastKey = NOT_A_KEY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + public void updateTimeDebouncing(long eventTime) { + mCurrentKeyTime += eventTime - mLastMoveTime; + mLastMoveTime = eventTime; + } + + public void resetTimeDebouncing(long eventTime, int currentKey) { + mLastKey = currentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + public boolean isMinorTimeBounce() { + return mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME + && mLastKey != NOT_A_KEY; + } + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.keyboardViewStyle); + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView); + LayoutInflater inflate = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int previewLayout = 0; + int keyTextSize = 0; + + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case R.styleable.LatinKeyboardBaseView_keyBackground: + mKeyBackground = a.getDrawable(attr); + break; + case R.styleable.LatinKeyboardBaseView_verticalCorrection: + mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewLayout: + previewLayout = a.getResourceId(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewOffset: + mPreviewOffset = a.getDimensionPixelOffset(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewHeight: + mPreviewHeight = a.getDimensionPixelSize(attr, 80); + break; + case R.styleable.LatinKeyboardBaseView_keyTextSize: + mKeyTextSize = a.getDimensionPixelSize(attr, 18); + break; + case R.styleable.LatinKeyboardBaseView_keyTextColor: + mKeyTextColor = a.getColor(attr, 0xFF000000); + break; + case R.styleable.LatinKeyboardBaseView_labelTextSize: + mLabelTextSize = a.getDimensionPixelSize(attr, 14); + break; + case R.styleable.LatinKeyboardBaseView_popupLayout: + mPopupLayout = a.getResourceId(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_shadowColor: + mShadowColor = a.getColor(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_shadowRadius: + mShadowRadius = a.getFloat(attr, 0f); + break; + // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) + case R.styleable.LatinKeyboardBaseView_backgroundDimAmount: + mBackgroundDimAmount = a.getFloat(attr, 0.5f); + break; + //case android.R.styleable. + case R.styleable.LatinKeyboardBaseView_keyTextStyle: + int textStyle = a.getInt(attr, 0); + switch (textStyle) { + case 0: + mKeyTextStyle = Typeface.DEFAULT; + break; + case 1: + mKeyTextStyle = Typeface.DEFAULT_BOLD; + break; + default: + mKeyTextStyle = Typeface.defaultFromStyle(textStyle); + break; + } + break; + case R.styleable.LatinKeyboardBaseView_symbolColorScheme: + mSymbolColorScheme = a.getInt(attr, 0); + break; + } + } + + mPreviewPopup = new PopupWindow(context); + if (previewLayout != 0) { + mPreviewText = (TextView) inflate.inflate(previewLayout, null); + mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + } else { + mShowPreview = false; + } + + mPreviewPopup.setTouchable(false); + + mPopupKeyboard = new PopupWindow(context); + mPopupKeyboard.setBackgroundDrawable(null); + //mPopupKeyboard.setClippingEnabled(false); + + mPopupParent = this; + //mPredicting = true; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(keyTextSize); + mPaint.setTextAlign(Align.CENTER); + mPaint.setAlpha(255); + + mPadding = new Rect(0, 0, 0, 0); + mMiniKeyboardCache = new HashMap<Key,View>(); + mKeyBackground.getPadding(mPadding); + + mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); + // TODO: Refer frameworks/base/core/res/res/values/config.xml + mDisambiguateSwipe = getResources().getBoolean(R.bool.config_swipeDisambiguation); + resetMultiTap(); + initGestureDetector(); + } + + private void initGestureDetector() { + mGestureDetector = new GestureDetector( + getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + if (mPossiblePoly) return false; + final float absX = Math.abs(velocityX); + final float absY = Math.abs(velocityY); + float deltaX = me2.getX() - me1.getX(); + float deltaY = me2.getY() - me1.getY(); + int travelX = getWidth() / 2; // Half the keyboard width + int travelY = getHeight() / 2; // Half the keyboard height + mSwipeTracker.computeCurrentVelocity(1000); + final float endingVelocityX = mSwipeTracker.getXVelocity(); + final float endingVelocityY = mSwipeTracker.getYVelocity(); + boolean sendDownKey = false; + if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { + if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { + sendDownKey = true; + } else { + swipeRight(); + return true; + } + } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { + if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { + sendDownKey = true; + } else { + swipeLeft(); + return true; + } + } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { + if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { + sendDownKey = true; + } else { + swipeUp(); + return true; + } + } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { + if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { + sendDownKey = true; + } else { + swipeDown(); + return true; + } + } + + if (sendDownKey) { + detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); + } + return false; + } + }); + + mGestureDetector.setIsLongpressEnabled(false); + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + /** + * Returns the {@link OnKeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected OnKeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * Attaches a keyboard to this view. The keyboard can be switched at any time and the + * view will re-layout itself to accommodate the keyboard. + * @see Keyboard + * @see #getKeyboard() + * @param keyboard the keyboard to display in this view + */ + public void setKeyboard(Keyboard keyboard) { + if (mKeyboard != null) { + showPreview(NOT_A_KEY); + } + // Remove any pending messages, except dismissing preview + mHandler.cancelKeyTimersAndPopupPreview(); + mKeyboard = keyboard; + List<Key> keys = mKeyboard.getKeys(); + mKeys = keys.toArray(new Key[keys.size()]); + requestLayout(); + // Hint to reallocate the buffer if the size changed + mKeyboardChanged = true; + invalidateAllKeys(); + computeProximityThreshold(keyboard); + mMiniKeyboardCache.clear(); + // Not really necessary to do every time, but will free up views + // Switching to a different keyboard should abort any pending keys so that the key up + // doesn't get delivered to the old or new keyboard + mAbortKey = true; // Until the next ACTION_DOWN + } + + /** + * Returns the current keyboard being displayed by this view. + * @return the currently attached keyboard + * @see #setKeyboard(Keyboard) + */ + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Sets the state of the shift key of the keyboard, if any. + * @param shifted whether or not to enable the state of the shift key + * @return true if the shift key state changed, false if there was no change + */ + public boolean setShifted(boolean shifted) { + if (mKeyboard != null) { + if (mKeyboard.setShifted(shifted)) { + // The whole keyboard probably needs to be redrawn + invalidateAllKeys(); + return true; + } + } + return false; + } + + /** + * Returns the state of the shift key of the keyboard, if any. + * @return true if the shift is in a pressed state, false otherwise. If there is + * no shift key on the keyboard or there is no keyboard attached, it returns false. + */ + public boolean isShifted() { + if (mKeyboard != null) { + return mKeyboard.isShifted(); + } + return false; + } + + /** + * Enables or disables the key feedback popup. This is a popup that shows a magnified + * version of the depressed key. By default the preview is enabled. + * @param previewEnabled whether or not to enable the key feedback popup + * @see #isPreviewEnabled() + */ + public void setPreviewEnabled(boolean previewEnabled) { + mShowPreview = previewEnabled; + } + + /** + * Returns the enabled state of the key feedback popup. + * @return whether or not the key feedback popup is enabled + * @see #setPreviewEnabled(boolean) + */ + public boolean isPreviewEnabled() { + return mShowPreview; + } + + public int getSymbolColorSheme() { + return mSymbolColorScheme; + } + + public void setVerticalCorrection(int verticalOffset) { + } + + public void setPopupParent(View v) { + mPopupParent = v; + } + + public void setPopupOffset(int x, int y) { + mMiniKeyboardOffsetX = x; + mMiniKeyboardOffsetY = y; + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + /** + * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key + * codes for adjacent keys. When disabled, only the primary key code will be + * reported. + * @param enabled whether or not the proximity correction is enabled + */ + public void setProximityCorrectionEnabled(boolean enabled) { + mProximityCorrectOn = enabled; + } + + /** + * Returns true if proximity correction is enabled. + */ + public boolean isProximityCorrectionEnabled() { + return mProximityCorrectOn; + } + + /** + * Popup keyboard close button clicked. + * @hide + */ + public void onClick(View v) { + dismissPopupKeyboard(); + } + + protected CharSequence adjustCase(CharSequence label) { + if (mKeyboard.isShifted() && label != null && label.length() < 3 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Round up a little + if (mKeyboard == null) { + setMeasuredDimension( + getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); + } else { + int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); + if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { + width = MeasureSpec.getSize(widthMeasureSpec); + } + setMeasuredDimension( + width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); + } + } + + /** + * Compute the average distance between adjacent keys (horizontally and vertically) + * and square it to get the proximity threshold. We use a square here and in computing + * the touch distance from a key's center to avoid taking a square root. + * @param keyboard + */ + private void computeProximityThreshold(Keyboard keyboard) { + if (keyboard == null) return; + final Key[] keys = mKeys; + if (keys == null) return; + int length = keys.length; + int dimensionSum = 0; + for (int i = 0; i < length; i++) { + Key key = keys[i]; + dimensionSum += Math.min(key.width, key.height) + key.gap; + } + if (dimensionSum < 0 || length == 0) return; + mProximityThreshold = (int) (dimensionSum * 1.4f / length); + mProximityThreshold *= mProximityThreshold; // Square it + + final float hysteresisPixel = getContext().getResources() + .getDimension(R.dimen.key_debounce_hysteresis_distance); + mDebouncer = new KeyDebouncer(keys, hysteresisPixel); + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Release the buffer, if any and it will be reallocated on the next draw + mBuffer = null; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mDrawPending || mBuffer == null || mKeyboardChanged) { + onBufferDraw(); + } + canvas.drawBitmap(mBuffer, 0, 0, null); + } + + private void onBufferDraw() { + if (mBuffer == null || mKeyboardChanged) { + if (mBuffer == null || mKeyboardChanged && + (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { + // Make sure our bitmap is at least 1x1 + final int width = Math.max(1, getWidth()); + final int height = Math.max(1, getHeight()); + mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBuffer); + } + invalidateAllKeys(); + mKeyboardChanged = false; + } + final Canvas canvas = mCanvas; + canvas.clipRect(mDirtyRect, Op.REPLACE); + + if (mKeyboard == null) return; + + final Paint paint = mPaint; + final Drawable keyBackground = mKeyBackground; + final Rect clipRegion = mClipRegion; + final Rect padding = mPadding; + final int kbdPaddingLeft = getPaddingLeft(); + final int kbdPaddingTop = getPaddingTop(); + final Key[] keys = mKeys; + final Key invalidKey = mInvalidatedKey; + + paint.setColor(mKeyTextColor); + boolean drawSingleKey = false; + if (invalidKey != null && canvas.getClipBounds(clipRegion)) { + // Is clipRegion completely contained within the invalidated key? + if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && + invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && + invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && + invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { + drawSingleKey = true; + } + } + canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + final int keyCount = keys.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[i]; + if (drawSingleKey && invalidKey != key) { + continue; + } + int[] drawableState = key.getCurrentDrawableState(); + keyBackground.setState(drawableState); + + // Switch the character to uppercase if shift is pressed + String label = key.label == null? null : adjustCase(key.label).toString(); + + final Rect bounds = keyBackground.getBounds(); + if (key.width != bounds.right || + key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height); + } + canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); + keyBackground.draw(canvas); + + if (label != null) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length() > 1 && key.codes.length < 2) { + paint.setTextSize(mLabelTextSize); + paint.setTypeface(Typeface.DEFAULT_BOLD); + } else { + paint.setTextSize(mKeyTextSize); + paint.setTypeface(mKeyTextStyle); + } + // Draw a drop shadow for the text + paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); + // Draw the text + canvas.drawText(label, + (key.width - padding.left - padding.right) / 2 + + padding.left, + (key.height - padding.top - padding.bottom) / 2 + + (paint.getTextSize() - paint.descent()) / 2 + padding.top, + paint); + // Turn off drop shadow + paint.setShadowLayer(0, 0, 0, 0); + } else if (key.icon != null) { + final int drawableX = (key.width - padding.left - padding.right + - key.icon.getIntrinsicWidth()) / 2 + padding.left; + final int drawableY = (key.height - padding.top - padding.bottom + - key.icon.getIntrinsicHeight()) / 2 + padding.top; + canvas.translate(drawableX, drawableY); + key.icon.setBounds(0, 0, + key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); + key.icon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + } + canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); + } + mInvalidatedKey = null; + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); + canvas.drawRect(0, 0, getWidth(), getHeight(), paint); + } + + if (DEBUG) { + if (mShowTouchPoints) { + int lastX = mDebouncer.getLastX(); + int lastY = mDebouncer.getLastY(); + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, lastX, lastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(lastX, lastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + lastX) / 2, (mStartY + lastY) / 2, 2, paint); + } + } + + mDrawPending = false; + mDirtyRect.setEmpty(); + } + + private int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) { + final Key[] keys = mKeys; + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + Arrays.fill(mDistances, Integer.MAX_VALUE); + int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); + final int keyCount = nearestKeyIndices.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[nearestKeyIndices[i]]; + int dist = 0; + boolean isInside = key.isInside(x,y); + if (isInside) { + primaryIndex = nearestKeyIndices[i]; + } + + if (((mProximityCorrectOn + && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) + || isInside) + && key.codes[0] > 32) { + // Find insertion point + final int nCodes = key.codes.length; + if (dist < closestKeyDist) { + closestKeyDist = dist; + closestKey = nearestKeyIndices[i]; + } + + if (allKeys == null) continue; + + for (int j = 0; j < mDistances.length; j++) { + if (mDistances[j] > dist) { + // Make space for nCodes codes + System.arraycopy(mDistances, j, mDistances, j + nCodes, + mDistances.length - j - nCodes); + System.arraycopy(allKeys, j, allKeys, j + nCodes, + allKeys.length - j - nCodes); + System.arraycopy(key.codes, 0, allKeys, j, nCodes); + Arrays.fill(mDistances, j, j + nCodes, dist); + break; + } + } + } + } + if (primaryIndex == NOT_A_KEY) { + primaryIndex = closestKey; + } + return primaryIndex; + } + + private void detectAndSendKey(int index, int x, int y, long eventTime) { + if (index != NOT_A_KEY && index < mKeys.length) { + final Key key = mKeys[index]; + if (key.text != null) { + mKeyboardActionListener.onText(key.text); + mKeyboardActionListener.onRelease(NOT_A_KEY); + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = new int[MAX_NEARBY_KEYS]; + Arrays.fill(codes, NOT_A_KEY); + getKeyIndexAndNearbyCodes(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + /* + * Swap the first and second values in the codes array if the primary code is not + * the first value but the second value in the array. This happens when key + * debouncing is in effect. + */ + if (codes.length >= 2 && codes[0] != code && codes[1] == code) { + codes[1] = codes[0]; + codes[0] = code; + } + mKeyboardActionListener.onKey(code, codes); + mKeyboardActionListener.onRelease(code); + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + private CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return adjustCase(mPreviewLabel); + } else { + return adjustCase(key.label); + } + } + + private void showPreview(int keyIndex) { + int oldKeyIndex = mCurrentKeyIndex; + final PopupWindow previewPopup = mPreviewPopup; + + mCurrentKeyIndex = keyIndex; + // Release the old key and press the new key + final Key[] keys = mKeys; + if (oldKeyIndex != mCurrentKeyIndex) { + if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { + keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY); + invalidateKey(oldKeyIndex); + } + if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { + keys[mCurrentKeyIndex].onPressed(); + invalidateKey(mCurrentKeyIndex); + } + } + // If key changed and preview is on ... + if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { + if (keyIndex == NOT_A_KEY) { + mHandler.cancelPopupPreview(); + if (previewPopup.isShowing()) { + mHandler.dismissPreview(DELAY_AFTER_PREVIEW); + } + } else { + if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { + // Show right away, if it's already visible and finger is moving around + showKey(keyIndex); + } else { + mHandler.popupPreview(keyIndex, DELAY_BEFORE_PREVIEW); + } + } + } + } + + private void showKey(final int keyIndex) { + final PopupWindow previewPopup = mPreviewPopup; + final Key[] keys = mKeys; + if (keyIndex < 0 || keyIndex >= mKeys.length) return; + Key key = keys[keyIndex]; + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); + mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); + } else { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); + mPreviewText.setTypeface(mKeyTextStyle); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + getPaddingLeft(); + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.cancelDismissPreview(); + if (mOffsetInWindow == null) { + mOffsetInWindow = new int[2]; + getLocationInWindow(mOffsetInWindow); + mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero + mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero + int[] mWindowLocation = new int[2]; + getLocationOnScreen(mWindowLocation); + mWindowY = mWindowLocation[1]; + } + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + mPopupPreviewX += mOffsetInWindow[0]; + mPopupPreviewY += mOffsetInWindow[1]; + + // If the popup cannot be shown above the key, put it on the side + if (mPopupPreviewY + mWindowY < 0) { + // If the key you're pressing is on the left side of the keyboard, show the popup on + // the right, offset by enough to see at least one key to the left/right. + if (key.x + key.width <= getWidth() / 2) { + mPopupPreviewX += (int) (key.width * 2.5); + } else { + mPopupPreviewX -= (int) (key.width * 2.5); + } + mPopupPreviewY += popupHeight; + } + + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX, mPopupPreviewY, + popupWidth, popupHeight); + } else { + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX, mPopupPreviewY); + } + mPreviewText.setVisibility(VISIBLE); + } + + /** + * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient + * because the keyboard renders the keys to an off-screen buffer and an invalidate() only + * draws the cached buffer. + * @see #invalidateKey(int) + */ + public void invalidateAllKeys() { + mDirtyRect.union(0, 0, getWidth(), getHeight()); + mDrawPending = true; + invalidate(); + } + + /** + * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only + * one key is changing it's content. Any changes that affect the position or size of the key + * may not be honored. + * @param keyIndex the index of the key in the attached {@link Keyboard}. + * @see #invalidateAllKeys + */ + public void invalidateKey(int keyIndex) { + if (mKeys == null) return; + if (keyIndex < 0 || keyIndex >= mKeys.length) { + return; + } + final Key key = mKeys[keyIndex]; + mInvalidatedKey = key; + mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(), + key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); + onBufferDraw(); + invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(), + key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); + } + + private boolean openPopupIfRequired(MotionEvent me) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { + return false; + } + + Key popupKey = mKeys[mCurrentKey]; + boolean result = onLongPress(popupKey); + if (result) { + mAbortKey = true; + showPreview(NOT_A_KEY); + } + return result; + } + + /** + * Called when a key is long pressed. By default this will open any popup keyboard associated + * with this key through the attributes popupLayout and popupCharacters. + * @param popupKey the key that was long pressed + * @return true if the long press is handled, false otherwise. Subclasses should call the + * method on the base class if the subclass doesn't wish to handle the call. + */ + protected boolean onLongPress(Key popupKey) { + int popupKeyboardId = popupKey.popupResId; + + if (popupKeyboardId != 0) { + mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); + if (mMiniKeyboardContainer == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); + mMiniKeyboard = (LatinKeyboardBaseView) mMiniKeyboardContainer.findViewById( + R.id.LatinKeyboardBaseView); + View closeButton = mMiniKeyboardContainer.findViewById( + R.id.closeButton); + if (closeButton != null) closeButton.setOnClickListener(this); + mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { + public void onKey(int primaryCode, int[] keyCodes) { + mKeyboardActionListener.onKey(primaryCode, keyCodes); + dismissPopupKeyboard(); + } + + public void onText(CharSequence text) { + mKeyboardActionListener.onText(text); + dismissPopupKeyboard(); + } + + public void swipeLeft() { } + public void swipeRight() { } + public void swipeUp() { } + public void swipeDown() { } + public void onPress(int primaryCode) { + mKeyboardActionListener.onPress(primaryCode); + } + public void onRelease(int primaryCode) { + mKeyboardActionListener.onRelease(primaryCode); + } + }); + //mInputView.setSuggest(mSuggest); + Keyboard keyboard; + if (popupKey.popupCharacters != null) { + keyboard = new Keyboard(getContext(), popupKeyboardId, + popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); + } else { + keyboard = new Keyboard(getContext(), popupKeyboardId); + } + mMiniKeyboard.setKeyboard(keyboard); + mMiniKeyboard.setPopupParent(this); + mMiniKeyboardContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); + } else { + mMiniKeyboard = (LatinKeyboardBaseView) mMiniKeyboardContainer.findViewById( + R.id.LatinKeyboardBaseView); + } + if (mWindowOffset == null) { + mWindowOffset = new int[2]; + getLocationInWindow(mWindowOffset); + } + mPopupX = popupKey.x + getPaddingLeft(); + mPopupY = popupKey.y + getPaddingTop(); + mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); + mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); + mMiniKeyboard.setShifted(isShifted()); + mPopupKeyboard.setContentView(mMiniKeyboardContainer); + mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); + mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); + mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + mMiniKeyboardOnScreen = true; + //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); + invalidateAllKeys(); + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + // Convert multi-pointer up/down events to single up/down events to + // deal with the typical multi-pointer behavior of two-thumb typing + final int pointerCount = me.getPointerCount(); + final int action = me.getAction(); + boolean result = false; + final long now = me.getEventTime(); + + if (pointerCount != mOldPointerCount) { + if (pointerCount == 1) { + // Send a down event for the latest pointer + MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + me.getX(), me.getY(), me.getMetaState()); + result = onModifiedTouchEvent(down, false); + down.recycle(); + // If it's an up action, then deliver the up as well. + if (action == MotionEvent.ACTION_UP) { + result = onModifiedTouchEvent(me, true); + } + } else { + // Send an up event for the last pointer + MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, + mOldPointerX, mOldPointerY, me.getMetaState()); + result = onModifiedTouchEvent(up, true); + up.recycle(); + } + } else { + if (pointerCount == 1) { + result = onModifiedTouchEvent(me, false); + mOldPointerX = me.getX(); + mOldPointerY = me.getY(); + } else { + // Don't do anything when 2 pointers are down and moving. + result = true; + } + } + mOldPointerCount = pointerCount; + + return result; + } + + private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { + int touchX = (int) me.getX() - getPaddingLeft(); + int touchY = (int) me.getY() + mVerticalCorrection - getPaddingTop(); + final int action = me.getAction(); + final long eventTime = me.getEventTime(); + int keyIndex = getKeyIndexAndNearbyCodes(touchX, touchY, null); + mPossiblePoly = possiblePoly; + + // Track the last few movements to look for spurious swipes. + if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); + mSwipeTracker.addMovement(me); + + // Ignore all motion events until a DOWN. + if (mAbortKey + && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + if (mGestureDetector.onTouchEvent(me)) { + showPreview(NOT_A_KEY); + mHandler.cancelKeyTimers(); + return true; + } + + // Needs to be called after the gesture detector gets a turn, as it may have + // displayed the mini keyboard + if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mAbortKey = false; + mCurrentKey = keyIndex; + mDownKey = keyIndex; + mStartX = touchX; + mStartY = touchY; + mDebouncer.startMoveDebouncing(touchX, touchY); + mDebouncer.startTimeDebouncing(eventTime); + checkMultiTap(eventTime, keyIndex); + mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? + mKeys[keyIndex].codes[0] : 0); + if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { + mRepeatKeyIndex = mCurrentKey; + mHandler.startKeyRepeatTimer(REPEAT_START_DELAY); + repeatKey(); + // Delivering the key could have caused an abort + if (mAbortKey) { + mRepeatKeyIndex = NOT_A_KEY; + break; + } + } + if (mCurrentKey != NOT_A_KEY) { + mHandler.startLongPressTimer(me, LONGPRESS_TIMEOUT); + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_MOVE: + boolean continueLongPress = false; + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mDebouncer.updateTimeDebouncing(eventTime); + } else if (mDebouncer.isMinorMoveBounce(touchX, touchY, keyIndex, + mCurrentKey)) { + mDebouncer.updateTimeDebouncing(eventTime); + continueLongPress = true; + } else if (mRepeatKeyIndex == NOT_A_KEY) { + resetMultiTap(); + mDebouncer.resetTimeDebouncing(eventTime, mCurrentKey); + mDebouncer.resetMoveDebouncing(); + mCurrentKey = keyIndex; + } + } + if (!continueLongPress) { + // Cancel old longpress + mHandler.cancelLongPressTimer(); + // Start new longpress if key has changed + if (keyIndex != NOT_A_KEY) { + mHandler.startLongPressTimer(me, LONGPRESS_TIMEOUT); + } + } + /* + * While time debouncing is in effect, mCurrentKey holds the new key and mDebouncer + * holds the last key. At ACTION_UP event if time debouncing will be in effect + * eventually, the last key should be sent as the result. In such case mCurrentKey + * should not be showed as popup preview. + */ + showPreview(mDebouncer.isMinorTimeBounce() ? mDebouncer.getLastKey() : mCurrentKey); + break; + + case MotionEvent.ACTION_UP: + mHandler.cancelKeyTimersAndPopupPreview(); + if (mDebouncer.isMinorMoveBounce(touchX, touchY, keyIndex, mCurrentKey)) { + mDebouncer.updateTimeDebouncing(eventTime); + } else { + resetMultiTap(); + mDebouncer.resetTimeDebouncing(eventTime, mCurrentKey); + mCurrentKey = keyIndex; + } + if (mDebouncer.isMinorTimeBounce()) { + mCurrentKey = mDebouncer.getLastKey(); + touchX = mDebouncer.getLastCodeX(); + touchY = mDebouncer.getLastCodeY(); + } + showPreview(NOT_A_KEY); + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); + } + invalidateKey(keyIndex); + mRepeatKeyIndex = NOT_A_KEY; + break; + + case MotionEvent.ACTION_CANCEL: + mHandler.cancelKeyTimersAndPopupPreview(); + dismissPopupKeyboard(); + mAbortKey = true; + showPreview(NOT_A_KEY); + invalidateKey(mCurrentKey); + break; + } + mDebouncer.updateMoveDebouncing(touchX, touchY); + return true; + } + + private boolean repeatKey() { + Key key = mKeys[mRepeatKeyIndex]; + detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); + return true; + } + + protected void swipeRight() { + mKeyboardActionListener.swipeRight(); + } + + protected void swipeLeft() { + mKeyboardActionListener.swipeLeft(); + } + + protected void swipeUp() { + mKeyboardActionListener.swipeUp(); + } + + protected void swipeDown() { + mKeyboardActionListener.swipeDown(); + } + + public void closing() { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + mHandler.cancelAllMessages(); + + dismissPopupKeyboard(); + mBuffer = null; + mCanvas = null; + mMiniKeyboardCache.clear(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + closing(); + } + + private void dismissPopupKeyboard() { + if (mPopupKeyboard.isShowing()) { + mPopupKeyboard.dismiss(); + mMiniKeyboardOnScreen = false; + invalidateAllKeys(); + } + } + + public boolean handleBack() { + if (mPopupKeyboard.isShowing()) { + dismissPopupKeyboard(); + return true; + } + return false; + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + if (keyIndex == NOT_A_KEY) return; + Key key = mKeys[keyIndex]; + if (key.codes.length > 1) { + mInMultiTap = true; + if (eventTime < mLastTapTime + MULTITAP_INTERVAL + && keyIndex == mLastSentIndex) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { + resetMultiTap(); + } + } + + private static class SwipeTracker { + + static final int NUM_PAST = 4; + static final int LONGEST_PAST_TIME = 200; + + final float mPastX[] = new float[NUM_PAST]; + final float mPastY[] = new float[NUM_PAST]; + final long mPastTime[] = new long[NUM_PAST]; + + float mYVelocity; + float mXVelocity; + + public void clear() { + mPastTime[0] = 0; + } + + public void addMovement(MotionEvent ev) { + long time = ev.getEventTime(); + final int N = ev.getHistorySize(); + for (int i=0; i<N; i++) { + addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), + ev.getHistoricalEventTime(i)); + } + addPoint(ev.getX(), ev.getY(), time); + } + + private void addPoint(float x, float y, long time) { + int drop = -1; + int i; + final long[] pastTime = mPastTime; + for (i=0; i<NUM_PAST; i++) { + if (pastTime[i] == 0) { + break; + } else if (pastTime[i] < time-LONGEST_PAST_TIME) { + drop = i; + } + } + if (i == NUM_PAST && drop < 0) { + drop = 0; + } + if (drop == i) drop--; + final float[] pastX = mPastX; + final float[] pastY = mPastY; + if (drop >= 0) { + final int start = drop+1; + final int count = NUM_PAST-drop-1; + System.arraycopy(pastX, start, pastX, 0, count); + System.arraycopy(pastY, start, pastY, 0, count); + System.arraycopy(pastTime, start, pastTime, 0, count); + i -= (drop+1); + } + pastX[i] = x; + pastY[i] = y; + pastTime[i] = time; + i++; + if (i < NUM_PAST) { + pastTime[i] = 0; + } + } + + public void computeCurrentVelocity(int units) { + computeCurrentVelocity(units, Float.MAX_VALUE); + } + + public void computeCurrentVelocity(int units, float maxVelocity) { + final float[] pastX = mPastX; + final float[] pastY = mPastY; + final long[] pastTime = mPastTime; + + final float oldestX = pastX[0]; + final float oldestY = pastY[0]; + final long oldestTime = pastTime[0]; + float accumX = 0; + float accumY = 0; + int N=0; + while (N < NUM_PAST) { + if (pastTime[N] == 0) { + break; + } + N++; + } + + for (int i=1; i < N; i++) { + final int dur = (int)(pastTime[i] - oldestTime); + if (dur == 0) continue; + float dist = pastX[i] - oldestX; + float vel = (dist/dur) * units; // pixels/frame. + if (accumX == 0) accumX = vel; + else accumX = (accumX + vel) * .5f; + + dist = pastY[i] - oldestY; + vel = (dist/dur) * units; // pixels/frame. + if (accumY == 0) accumY = vel; + else accumY = (accumY + vel) * .5f; + } + mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) + : Math.min(accumX, maxVelocity); + mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) + : Math.min(accumY, maxVelocity); + } + + public float getXVelocity() { + return mXVelocity; + } + + public float getYVelocity() { + return mYVelocity; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java index 74fc475e6..38d9cefb1 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -22,8 +22,6 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.inputmethodservice.Keyboard; -import android.inputmethodservice.KeyboardView; -import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; import android.inputmethodservice.Keyboard.Key; import android.os.Handler; import android.os.Message; @@ -33,7 +31,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.widget.PopupWindow; -public class LatinKeyboardView extends KeyboardView { +public class LatinKeyboardView extends LatinKeyboardBaseView { static final int KEYCODE_OPTIONS = -100; static final int KEYCODE_SHIFT_LONGPRESS = -101; @@ -66,6 +64,8 @@ public class LatinKeyboardView extends KeyboardView { /** The y coordinate of the last row */ private int mLastRowY; + private int mExtensionLayoutResId = 0; + public LatinKeyboardView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -78,6 +78,10 @@ public class LatinKeyboardView extends KeyboardView { mPhoneKeyboard = phoneKeyboard; } + public void setExtentionLayoutResId (int id) { + mExtensionLayoutResId = id; + } + @Override public void setKeyboard(Keyboard k) { super.setKeyboard(k); @@ -107,6 +111,29 @@ public class LatinKeyboardView extends KeyboardView { } } + @Override + protected CharSequence adjustCase(CharSequence label) { + Keyboard keyboard = getKeyboard(); + if (keyboard.isShifted() + && keyboard instanceof LatinKeyboard + && ((LatinKeyboard) keyboard).isAlphaKeyboard() + && label != null && label.length() < 3 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + public boolean setShiftLocked(boolean shiftLocked) { + Keyboard keyboard = getKeyboard(); + if (keyboard != null && keyboard instanceof LatinKeyboard) { + ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked); + invalidateAllKeys(); + return true; + } + return false; + } + /** * This function checks to see if we need to handle any sudden jumps in the pointer location * that could be due to a multi-touch being treated as a move by the firmware or hardware. @@ -295,7 +322,8 @@ public class LatinKeyboardView extends KeyboardView { mExtensionPopup.setBackgroundDrawable(null); LayoutInflater li = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); - mExtension = (LatinKeyboardView) li.inflate(R.layout.input_trans, null); + mExtension = (LatinKeyboardView) li.inflate(mExtensionLayoutResId == 0 ? + R.layout.input_trans : mExtensionLayoutResId, null); mExtension.setExtensionType(true); mExtension.setOnKeyboardActionListener( new ExtensionKeyboardListener(getOnKeyboardActionListener())); @@ -452,27 +480,39 @@ public class LatinKeyboardView extends KeyboardView { } } } - - 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); + + public void startPlaying(String s) { + if (DEBUG_AUTO_PLAY) { + 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); + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + super.draw(c); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e); + } + } + if (DEBUG_AUTO_PLAY) { + if (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) { diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index a70bea003..a96737f5c 100755 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -16,18 +16,17 @@ package com.android.inputmethod.latin; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import android.content.Context; import android.text.AutoText; import android.text.TextUtils; import android.util.Log; import android.view.View; -import java.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. @@ -35,9 +34,36 @@ import com.android.inputmethod.latin.WordComposer; */ public class Suggest implements Dictionary.WordCallback { + private static final String TAG = "Suggest"; + + public static final int APPROX_MAX_WORD_LENGTH = 32; + public static final int CORRECTION_NONE = 0; public static final int CORRECTION_BASIC = 1; public static final int CORRECTION_FULL = 2; + public static final int CORRECTION_FULL_BIGRAM = 3; + + /** + * Words that appear in both bigram and unigram data gets multiplier ranging from + * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the frequency score from + * bigram data. + */ + public static final double BIGRAM_MULTIPLIER_MIN = 1.2; + public static final double BIGRAM_MULTIPLIER_MAX = 1.5; + + /** + * Maximum possible bigram frequency. Will depend on how many bits are being used in data + * structure. Maximum bigram freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier. + */ + public static final int MAXIMUM_BIGRAM_FREQUENCY = 127; + + public static final int DIC_USER_TYPED = 0; + public static final int DIC_MAIN = 1; + public static final int DIC_USER = 2; + public static final int DIC_AUTO = 3; + public static final int DIC_CONTACTS = 4; + // If you add a type of dictionary, increment DIC_TYPE_LAST_ID + public static final int DIC_TYPE_LAST_ID = 4; static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; @@ -49,11 +75,17 @@ public class Suggest implements Dictionary.WordCallback { private Dictionary mContactsDictionary; + private Dictionary mUserBigramDictionary; + private int mPrefMaxSuggestions = 12; + private static final int PREF_MAX_BIGRAMS = 60; + private boolean mAutoTextEnabled; private int[] mPriorities = new int[mPrefMaxSuggestions]; + private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + // 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. @@ -61,6 +93,7 @@ public class Suggest implements Dictionary.WordCallback { // latin characters. private int[] mNextLettersFrequencies = new int[1280]; private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); + ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>(); private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); private boolean mHaveCorrection; private CharSequence mOriginalWord; @@ -69,11 +102,19 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; + public Suggest(Context context, int[] dictionaryResId) { + mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN); + initPool(); + } + + public Suggest(Context context, ByteBuffer byteBuffer) { + mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN); + initPool(); + } - public Suggest(Context context, int dictionaryResId) { - mMainDict = new BinaryDictionary(context, dictionaryResId); + private void initPool() { for (int i = 0; i < mPrefMaxSuggestions; i++) { - StringBuilder sb = new StringBuilder(32); + StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); mStringPool.add(sb); } } @@ -94,6 +135,10 @@ public class Suggest implements Dictionary.WordCallback { return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; } + public int getApproxMaxWordLength() { + return APPROX_MAX_WORD_LENGTH; + } + /** * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted * before the main dictionary, if set. @@ -113,6 +158,10 @@ public class Suggest implements Dictionary.WordCallback { mAutoDictionary = autoDictionary; } + public void setUserBigramDictionary(Dictionary userBigramDictionary) { + mUserBigramDictionary = userBigramDictionary; + } + /** * Number of suggestions to generate from the input key sequence. This has * to be a number between 1 and 100 (inclusive). @@ -125,9 +174,10 @@ public class Suggest implements Dictionary.WordCallback { } mPrefMaxSuggestions = maxSuggestions; mPriorities = new int[mPrefMaxSuggestions]; - collectGarbage(); + mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + collectGarbage(mSuggestions, mPrefMaxSuggestions); while (mStringPool.size() < mPrefMaxSuggestions) { - StringBuilder sb = new StringBuilder(32); + StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); mStringPool.add(sb); } } @@ -162,30 +212,77 @@ public class Suggest implements Dictionary.WordCallback { /** * 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. + * @param view a view for retrieving the context for AutoText + * @param wordComposer contains what is currently being typed + * @param prevWordForBigram previous word (used only for bigram) * @return list of suggestions. */ public List<CharSequence> getSuggestions(View view, WordComposer wordComposer, - boolean includeTypedWordIfValid) { + boolean includeTypedWordIfValid, CharSequence prevWordForBigram) { + LatinImeLogger.onStartSuggestion(prevWordForBigram); mHaveCorrection = false; mCapitalize = wordComposer.isCapitalized(); - collectGarbage(); + collectGarbage(mSuggestions, mPrefMaxSuggestions); 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(); + final String mOriginalWordString = mOriginalWord.toString(); + mOriginalWord = mOriginalWordString; + mLowerOriginalWord = mOriginalWordString.toLowerCase(); + // Treating USER_TYPED as UNIGRAM suggestion for logging now. + LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED, + Dictionary.DataType.UNIGRAM); } else { mLowerOriginalWord = ""; } - // Search the dictionary only if there are at least 2 characters - if (wordComposer.size() > 1) { + + if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM + || mCorrectionMode == CORRECTION_BASIC)) { + // At first character typed, search only the bigrams + Arrays.fill(mBigramPriorities, 0); + collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS); + + if (!TextUtils.isEmpty(prevWordForBigram)) { + CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase(); + if (mMainDict.isValidWord(lowerPrevWord)) { + prevWordForBigram = lowerPrevWord; + } + if (mUserBigramDictionary != null) { + mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + if (mContactsDictionary != null) { + mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + if (mMainDict != null) { + mMainDict.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + char currentChar = wordComposer.getTypedWord().charAt(0); + char currentCharUpper = Character.toUpperCase(currentChar); + int count = 0; + int bigramSuggestionSize = mBigramSuggestions.size(); + for (int i = 0; i < bigramSuggestionSize; i++) { + if (mBigramSuggestions.get(i).charAt(0) == currentChar + || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) { + int poolSize = mStringPool.size(); + StringBuilder sb = poolSize > 0 ? + (StringBuilder) mStringPool.remove(poolSize - 1) + : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + sb.append(mBigramSuggestions.get(i)); + mSuggestions.add(count++, sb); + if (count > mPrefMaxSuggestions) break; + } + } + } + + } else if (wordComposer.size() > 1) { + // At second character typed, search the unigrams (scores being affected by bigrams) if (mUserDictionary != null || mContactsDictionary != null) { if (mUserDictionary != null) { mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies); @@ -195,26 +292,29 @@ public class Suggest implements Dictionary.WordCallback { } if (mSuggestions.size() > 0 && isValidWord(mOriginalWord) - && mCorrectionMode == CORRECTION_FULL) { + && (mCorrectionMode == CORRECTION_FULL + || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { mHaveCorrection = true; } } mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); - if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) { + if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM) + && 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 (wordComposer.size() > 1 && mSuggestions.size() > 1 + && (mCorrectionMode == CORRECTION_FULL + || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { mHaveCorrection = false; } } - if (mAutoTextEnabled) { int i = 0; int max = 6; @@ -240,7 +340,6 @@ public class Suggest implements Dictionary.WordCallback { i++; } } - removeDupes(); return mSuggestions; } @@ -294,35 +393,67 @@ public class Suggest implements Dictionary.WordCallback { return false; } - public boolean addWord(final char[] word, final int offset, final int length, final int freq) { + public boolean addWord(final char[] word, final int offset, final int length, int freq, + final int dicTypeId, final Dictionary.DataType dataType) { + Dictionary.DataType dataTypeForLog = dataType; + ArrayList<CharSequence> suggestions; + int[] priorities; + int prefMaxSuggestions; + if(dataType == Dictionary.DataType.BIGRAM) { + suggestions = mBigramSuggestions; + priorities = mBigramPriorities; + prefMaxSuggestions = PREF_MAX_BIGRAMS; + } else { + suggestions = mSuggestions; + priorities = mPriorities; + prefMaxSuggestions = mPrefMaxSuggestions; + } + 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 { + if (dataType == Dictionary.DataType.UNIGRAM) { + // Check if the word was already added before (by bigram data) + int bigramSuggestion = searchBigramSuggestion(word,offset,length); + if(bigramSuggestion >= 0) { + dataTypeForLog = Dictionary.DataType.BIGRAM; + // turn freq from bigram into multiplier specified above + double multiplier = (((double) mBigramPriorities[bigramSuggestion]) + / MAXIMUM_BIGRAM_FREQUENCY) + * (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN) + + BIGRAM_MULTIPLIER_MIN; + /* Log.d(TAG,"bigram num: " + bigramSuggestion + + " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString() + + " currentPriority: " + freq + " bigramPriority: " + + mBigramPriorities[bigramSuggestion] + + " multiplier: " + multiplier); */ + freq = (int)Math.round((freq * multiplier)); + } + } + // 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())) { + || (priorities[pos] == freq && length < suggestions.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); + : new StringBuilder(getApproxMaxWordLength()); sb.setLength(0); if (mCapitalize) { sb.append(Character.toUpperCase(word[offset])); @@ -332,16 +463,38 @@ public class Suggest implements Dictionary.WordCallback { } else { sb.append(word, offset, length); } - mSuggestions.add(pos, sb); - if (mSuggestions.size() > prefMaxSuggestions) { - CharSequence garbage = mSuggestions.remove(prefMaxSuggestions); + suggestions.add(pos, sb); + if (suggestions.size() > prefMaxSuggestions) { + CharSequence garbage = suggestions.remove(prefMaxSuggestions); if (garbage instanceof StringBuilder) { mStringPool.add(garbage); } + } else { + LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog); } return true; } + private int searchBigramSuggestion(final char[] word, final int offset, final int length) { + // TODO This is almost O(n^2). Might need fix. + // search whether the word appeared in bigram data + int bigramSuggestSize = mBigramSuggestions.size(); + for(int i = 0; i < bigramSuggestSize; i++) { + if(mBigramSuggestions.get(i).length() == length) { + boolean chk = true; + for(int j = 0; j < length; j++) { + if(mBigramSuggestions.get(i).charAt(j) != word[offset+j]) { + chk = false; + break; + } + } + if(chk) return i; + } + } + + return -1; + } + public boolean isValidWord(final CharSequence word) { if (word == null || word.length() == 0) { return false; @@ -352,21 +505,21 @@ public class Suggest implements Dictionary.WordCallback { || (mContactsDictionary != null && mContactsDictionary.isValidWord(word)); } - private void collectGarbage() { + private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) { int poolSize = mStringPool.size(); - int garbageSize = mSuggestions.size(); - while (poolSize < mPrefMaxSuggestions && garbageSize > 0) { - CharSequence garbage = mSuggestions.get(garbageSize - 1); + int garbageSize = suggestions.size(); + while (poolSize < prefMaxSuggestions && garbageSize > 0) { + CharSequence garbage = suggestions.get(garbageSize - 1); if (garbage != null && garbage instanceof StringBuilder) { mStringPool.add(garbage); poolSize++; } garbageSize--; } - if (poolSize == mPrefMaxSuggestions + 1) { + if (poolSize == prefMaxSuggestions + 1) { Log.w("Suggest", "String pool got too big: " + poolSize); } - mSuggestions.clear(); + suggestions.clear(); } public void close() { diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index d056ceb16..9011191f1 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -17,19 +17,22 @@ package com.android.inputmethod.latin; import android.content.Context; +import android.inputmethodservice.Keyboard.Key; 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 final boolean DBG = false; + + private static final String TAG = "TextEntryState"; + private static boolean LOGGING = false; - + private static int sBackspaceCount = 0; private static int sAutoSuggestCount = 0; @@ -43,35 +46,26 @@ public class TextEntryState { 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; - + + public enum State { + UNKNOWN, + START, + IN_WORD, + ACCEPTED_DEFAULT, + PICKED_SUGGESTION, + PUNCTUATION_AFTER_WORD, + PUNCTUATION_AFTER_ACCEPTED, + SPACE_AFTER_ACCEPTED, + SPACE_AFTER_PICKED, + UNDO_COMMIT, + CORRECTING, + PICKED_CORRECTION; + } + + private static State sState = State.UNKNOWN; + private static FileOutputStream sKeyLocationFile; private static FileOutputStream sUserActionFile; @@ -84,7 +78,7 @@ public class TextEntryState { sWordNotInDictionaryCount = 0; sTypedChars = 0; sActualChars = 0; - sState = STATE_START; + sState = State.START; if (LOGGING) { try { @@ -129,90 +123,135 @@ public class TextEntryState { } sTypedChars += typedWord.length(); sActualChars += actualWord.length(); - sState = STATE_ACCEPTED_DEFAULT; + sState = State.ACCEPTED_DEFAULT; + LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString()); + displayState(); } - + + // State.ACCEPTED_DEFAULT will be changed to other sub-states + // (see "case ACCEPTED_DEFAULT" in typedCharacter() below), + // and should be restored back to State.ACCEPTED_DEFAULT after processing for each sub-state. + public static void backToAcceptedDefault(CharSequence typedWord) { + if (typedWord == null) return; + switch (sState) { + case SPACE_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_ACCEPTED: + case IN_WORD: + sState = State.ACCEPTED_DEFAULT; + break; + } + displayState(); + } + public static void acceptedTyped(CharSequence typedWord) { sWordNotInDictionaryCount++; - sState = STATE_PICKED_SUGGESTION; + sState = State.PICKED_SUGGESTION; + displayState(); } public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) { sManualSuggestCount++; + State oldState = sState; if (typedWord.equals(actualWord)) { acceptedTyped(typedWord); } - sState = STATE_PICKED_SUGGESTION; + if (oldState == State.CORRECTING || oldState == State.PICKED_CORRECTION) { + sState = State.PICKED_CORRECTION; + } else { + sState = State.PICKED_SUGGESTION; + } + displayState(); } - + + public static void selectedForCorrection() { + sState = State.CORRECTING; + displayState(); + } + public static void typedCharacter(char c, boolean isSeparator) { boolean isSpace = c == ' '; switch (sState) { - case STATE_IN_WORD: + case IN_WORD: if (isSpace || isSeparator) { - sState = STATE_START; + sState = State.START; } else { // State hasn't changed. } break; - case STATE_ACCEPTED_DEFAULT: - case STATE_SPACE_AFTER_PICKED: + case ACCEPTED_DEFAULT: + case SPACE_AFTER_PICKED: if (isSpace) { - sState = STATE_SPACE_AFTER_ACCEPTED; + sState = State.SPACE_AFTER_ACCEPTED; } else if (isSeparator) { - sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + sState = State.PUNCTUATION_AFTER_ACCEPTED; } else { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } break; - case STATE_PICKED_SUGGESTION: + case PICKED_SUGGESTION: + case PICKED_CORRECTION: if (isSpace) { - sState = STATE_SPACE_AFTER_PICKED; + sState = State.SPACE_AFTER_PICKED; } else if (isSeparator) { // Swap - sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + sState = State.PUNCTUATION_AFTER_ACCEPTED; } else { - sState = STATE_IN_WORD; + 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: + case START: + case UNKNOWN: + case SPACE_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_WORD: if (!isSpace && !isSeparator) { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } else { - sState = STATE_START; + sState = State.START; } break; - case STATE_UNDO_COMMIT: + case UNDO_COMMIT: if (isSpace || isSeparator) { - sState = STATE_ACCEPTED_DEFAULT; + sState = State.ACCEPTED_DEFAULT; } else { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } + break; + case CORRECTING: + sState = State.START; + break; } + displayState(); } public static void backspace() { - if (sState == STATE_ACCEPTED_DEFAULT) { - sState = STATE_UNDO_COMMIT; + if (sState == State.ACCEPTED_DEFAULT) { + sState = State.UNDO_COMMIT; sAutoSuggestUndoneCount++; - } else if (sState == STATE_UNDO_COMMIT) { - sState = STATE_IN_WORD; + LatinImeLogger.logOnAutoSuggestionCanceled(); + } else if (sState == State.UNDO_COMMIT) { + sState = State.IN_WORD; } sBackspaceCount++; + displayState(); } - + public static void reset() { - sState = STATE_START; + sState = State.START; + displayState(); } - - public static int getState() { + + public static State getState() { + if (DBG) { + Log.d(TAG, "Returning state = " + sState); + } return sState; } - + + public static boolean isCorrecting() { + return sState == State.CORRECTING || sState == State.PICKED_CORRECTION; + } + public static void keyPressedAt(Key key, int x, int y) { if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) { String out = @@ -229,5 +268,11 @@ public class TextEntryState { } } } + + private static void displayState() { + if (DBG) { + Log.d(TAG, "State = " + sState); + } + } } diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java new file mode 100644 index 000000000..c3eab94f1 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -0,0 +1,402 @@ +/* + * 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 java.util.HashSet; +import java.util.Iterator; + +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.os.AsyncTask; +import android.provider.BaseColumns; +import android.util.Log; + +/** + * Stores all the pairs user types in databases. Prune the database if the size + * gets too big. Unlike AutoDictionary, it even stores the pairs that are already + * in the dictionary. + */ +public class UserBigramDictionary extends ExpandableDictionary { + private static final String TAG = "UserBigramDictionary"; + + /** Any pair being typed or picked */ + private static final int FREQUENCY_FOR_TYPED = 2; + + /** Maximum frequency for all pairs */ + private static final int FREQUENCY_MAX = 127; + + /** + * If this pair is typed 6 times, it would be suggested. + * Should be smaller than ContactsDictionary.FREQUENCY_FOR_CONTACTS_BIGRAM + */ + protected static final int SUGGEST_THRESHOLD = 6 * FREQUENCY_FOR_TYPED; + + /** Maximum number of pairs. Pruning will start when databases goes above this number. */ + private static int sMaxUserBigrams = 10000; + + /** + * When it hits maximum bigram pair, it will delete until you are left with + * only (sMaxUserBigrams - sDeleteUserBigrams) pairs. + * Do not keep this number small to avoid deleting too often. + */ + private static int sDeleteUserBigrams = 1000; + + /** + * Database version should increase if the database structure changes + */ + private static final int DATABASE_VERSION = 1; + + private static final String DATABASE_NAME = "userbigram_dict.db"; + + /** Name of the words table in the database */ + private static final String MAIN_TABLE_NAME = "main"; + // TODO: Consume less space by using a unique id for locale instead of the whole + // 2-5 character string. (Same TODO from AutoDictionary) + private static final String MAIN_COLUMN_ID = BaseColumns._ID; + private static final String MAIN_COLUMN_WORD1 = "word1"; + private static final String MAIN_COLUMN_WORD2 = "word2"; + private static final String MAIN_COLUMN_LOCALE = "locale"; + + /** Name of the frequency table in the database */ + private static final String FREQ_TABLE_NAME = "frequency"; + private static final String FREQ_COLUMN_ID = BaseColumns._ID; + private static final String FREQ_COLUMN_PAIR_ID = "pair_id"; + private static final String FREQ_COLUMN_FREQUENCY = "freq"; + + private final LatinIME mIme; + + /** Locale for which this auto dictionary is storing words */ + private String mLocale; + + private HashSet<Bigram> mPendingWrites = new HashSet<Bigram>(); + private final Object mPendingWritesLock = new Object(); + private static volatile boolean sUpdatingDB = false; + + private final static HashMap<String, String> sDictProjectionMap; + + static { + sDictProjectionMap = new HashMap<String, String>(); + sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID); + sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1); + sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2); + sDictProjectionMap.put(MAIN_COLUMN_LOCALE, MAIN_COLUMN_LOCALE); + + sDictProjectionMap.put(FREQ_COLUMN_ID, FREQ_COLUMN_ID); + sDictProjectionMap.put(FREQ_COLUMN_PAIR_ID, FREQ_COLUMN_PAIR_ID); + sDictProjectionMap.put(FREQ_COLUMN_FREQUENCY, FREQ_COLUMN_FREQUENCY); + } + + private static DatabaseHelper sOpenHelper = null; + + private static class Bigram { + String word1; + String word2; + int frequency; + + Bigram(String word1, String word2, int frequency) { + this.word1 = word1; + this.word2 = word2; + this.frequency = frequency; + } + + @Override + public boolean equals(Object bigram) { + Bigram bigram2 = (Bigram) bigram; + return (word1.equals(bigram2.word1) && word2.equals(bigram2.word2)); + } + + @Override + public int hashCode() { + return (word1 + " " + word2).hashCode(); + } + } + + public void setDatabaseMax(int maxUserBigram) { + sMaxUserBigrams = maxUserBigram; + } + + public void setDatabaseDelete(int deleteUserBigram) { + sDeleteUserBigrams = deleteUserBigram; + } + + public UserBigramDictionary(Context context, LatinIME ime, String locale, int dicTypeId) { + super(context, dicTypeId); + mIme = ime; + mLocale = locale; + if (sOpenHelper == null) { + sOpenHelper = new DatabaseHelper(getContext()); + } + if (mLocale != null && mLocale.length() > 1) { + loadDictionary(); + } + } + + @Override + public void close() { + flushPendingWrites(); + // Don't close the database as locale changes will require it to be reopened anyway + // Also, the database is written to somewhat frequently, so it needs to be kept alive + // throughout the life of the process. + // mOpenHelper.close(); + super.close(); + } + + /** + * Pair will be added to the userbigram database. + */ + public int addBigrams(String word1, String word2) { + // remove caps + if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) { + word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1); + } + + int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED); + if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX; + synchronized (mPendingWritesLock) { + if (freq == FREQUENCY_FOR_TYPED || mPendingWrites.isEmpty()) { + mPendingWrites.add(new Bigram(word1, word2, freq)); + } else { + Bigram bi = new Bigram(word1, word2, freq); + mPendingWrites.remove(bi); + mPendingWrites.add(bi); + } + } + + return freq; + } + + /** + * Schedules a background thread to write any pending words to the database. + */ + public void flushPendingWrites() { + synchronized (mPendingWritesLock) { + // Nothing pending? Return + if (mPendingWrites.isEmpty()) return; + // Create a background thread to write the pending entries + new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute(); + // Create a new map for writing new entries into while the old one is written to db + mPendingWrites = new HashSet<Bigram>(); + } + } + + /** Used for testing purpose **/ + void waitUntilUpdateDBDone() { + synchronized (mPendingWritesLock) { + while (sUpdatingDB) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + return; + } + } + + @Override + public void loadDictionaryAsync() { + // Load the words that correspond to the current input locale + Cursor cursor = query(MAIN_COLUMN_LOCALE + "=?", new String[] { mLocale }); + try { + if (cursor.moveToFirst()) { + int word1Index = cursor.getColumnIndex(MAIN_COLUMN_WORD1); + int word2Index = cursor.getColumnIndex(MAIN_COLUMN_WORD2); + int frequencyIndex = cursor.getColumnIndex(FREQ_COLUMN_FREQUENCY); + while (!cursor.isAfterLast()) { + String word1 = cursor.getString(word1Index); + String word2 = cursor.getString(word2Index); + int frequency = cursor.getInt(frequencyIndex); + // Safeguard against adding really long words. Stack may overflow due + // to recursive lookup + if (word1.length() < MAX_WORD_LENGTH && word2.length() < MAX_WORD_LENGTH) { + super.setBigram(word1, word2, frequency); + } + cursor.moveToNext(); + } + } + } finally { + cursor.close(); + } + } + + /** + * Query the database + */ + private Cursor query(String selection, String[] selectionArgs) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + + // main INNER JOIN frequency ON (main._id=freq.pair_id) + qb.setTables(MAIN_TABLE_NAME + " INNER JOIN " + FREQ_TABLE_NAME + " ON (" + + MAIN_TABLE_NAME + "." + MAIN_COLUMN_ID + "=" + FREQ_TABLE_NAME + "." + + FREQ_COLUMN_PAIR_ID +")"); + + qb.setProjectionMap(sDictProjectionMap); + + // Get the database and run the query + SQLiteDatabase db = sOpenHelper.getReadableDatabase(); + Cursor c = qb.query(db, + new String[] { MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD2, FREQ_COLUMN_FREQUENCY }, + selection, selectionArgs, null, null, null); + return c; + } + + /** + * 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("PRAGMA foreign_keys = ON;"); + db.execSQL("CREATE TABLE " + MAIN_TABLE_NAME + " (" + + MAIN_COLUMN_ID + " INTEGER PRIMARY KEY," + + MAIN_COLUMN_WORD1 + " TEXT," + + MAIN_COLUMN_WORD2 + " TEXT," + + MAIN_COLUMN_LOCALE + " TEXT" + + ");"); + db.execSQL("CREATE TABLE " + FREQ_TABLE_NAME + " (" + + FREQ_COLUMN_ID + " INTEGER PRIMARY KEY," + + FREQ_COLUMN_PAIR_ID + " INTEGER," + + FREQ_COLUMN_FREQUENCY + " INTEGER," + + "FOREIGN KEY(" + FREQ_COLUMN_PAIR_ID + ") REFERENCES " + MAIN_TABLE_NAME + + "(" + MAIN_COLUMN_ID + ")" + " ON DELETE CASCADE" + + ");"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS " + MAIN_TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + FREQ_TABLE_NAME); + onCreate(db); + } + } + + /** + * Async task to write pending words to the database so that it stays in sync with + * the in-memory trie. + */ + private static class UpdateDbTask extends AsyncTask<Void, Void, Void> { + private final HashSet<Bigram> mMap; + private final DatabaseHelper mDbHelper; + private final String mLocale; + + public UpdateDbTask(Context context, DatabaseHelper openHelper, + HashSet<Bigram> pendingWrites, String locale) { + mMap = pendingWrites; + mLocale = locale; + mDbHelper = openHelper; + } + + /** Prune any old data if the database is getting too big. */ + private void checkPruneData(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys = ON;"); + Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID }, + null, null, null, null, null); + try { + int totalRowCount = c.getCount(); + // prune out old data if we have too much data + if (totalRowCount > sMaxUserBigrams) { + int numDeleteRows = (totalRowCount - sMaxUserBigrams) + sDeleteUserBigrams; + int pairIdColumnId = c.getColumnIndex(FREQ_COLUMN_PAIR_ID); + c.moveToFirst(); + int count = 0; + while (count < numDeleteRows && !c.isAfterLast()) { + String pairId = c.getString(pairIdColumnId); + // Deleting from MAIN table will delete the frequencies + // due to FOREIGN KEY .. ON DELETE CASCADE + db.delete(MAIN_TABLE_NAME, MAIN_COLUMN_ID + "=?", + new String[] { pairId }); + c.moveToNext(); + count++; + } + } + } finally { + c.close(); + } + } + + @Override + protected void onPreExecute() { + sUpdatingDB = true; + } + + @Override + protected Void doInBackground(Void... v) { + SQLiteDatabase db = mDbHelper.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys = ON;"); + // Write all the entries to the db + Iterator<Bigram> iterator = mMap.iterator(); + while (iterator.hasNext()) { + Bigram bi = iterator.next(); + + // find pair id + Cursor c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID }, + MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND " + + MAIN_COLUMN_LOCALE + "=?", + new String[] { bi.word1, bi.word2, mLocale }, null, null, null); + + int pairId; + if (c.moveToFirst()) { + // existing pair + pairId = c.getInt(c.getColumnIndex(MAIN_COLUMN_ID)); + db.delete(FREQ_TABLE_NAME, FREQ_COLUMN_PAIR_ID + "=?", + new String[] { Integer.toString(pairId) }); + } else { + // new pair + Long pairIdLong = db.insert(MAIN_TABLE_NAME, null, + getContentValues(bi.word1, bi.word2, mLocale)); + pairId = pairIdLong.intValue(); + } + c.close(); + + // insert new frequency + long s = db.insert(FREQ_TABLE_NAME, null, + getFrequencyContentValues(pairId, bi.frequency)); + } + checkPruneData(db); + sUpdatingDB = false; + + return null; + } + + private ContentValues getContentValues(String word1, String word2, String locale) { + ContentValues values = new ContentValues(3); + values.put(MAIN_COLUMN_WORD1, word1); + values.put(MAIN_COLUMN_WORD2, word2); + values.put(MAIN_COLUMN_LOCALE, locale); + return values; + } + + private ContentValues getFrequencyContentValues(int pairId, int frequency) { + ContentValues values = new ContentValues(2); + values.put(FREQ_COLUMN_PAIR_ID, pairId); + values.put(FREQ_COLUMN_FREQUENCY, frequency); + return values; + } + } + +} diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java index e8ca33af3..3315cf6c9 100644 --- a/java/src/com/android/inputmethod/latin/UserDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserDictionary.java @@ -38,7 +38,7 @@ public class UserDictionary extends ExpandableDictionary { private String mLocale; public UserDictionary(Context context, String locale) { - super(context); + super(context, Suggest.DIC_USER); mLocale = locale; // Perform a managed query. The Activity will handle closing and requerying the cursor // when needed. @@ -54,6 +54,7 @@ public class UserDictionary extends ExpandableDictionary { loadDictionary(); } + @Override public synchronized void close() { if (mObserver != null) { getContext().getContentResolver().unregisterContentObserver(mObserver); diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 19f714ae7..1ea74847a 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -44,11 +44,20 @@ public class WordComposer { */ private boolean mIsCapitalized; - WordComposer() { + public WordComposer() { mCodes = new ArrayList<int[]>(12); mTypedWord = new StringBuilder(20); } + WordComposer(WordComposer copy) { + mCodes = (ArrayList<int[]>) copy.mCodes.clone(); + mPreferredWord = copy.mPreferredWord; + mTypedWord = new StringBuilder(copy.mTypedWord); + mCapsCount = copy.mCapsCount; + mAutoCapitalized = copy.mAutoCapitalized; + mIsCapitalized = copy.mIsCapitalized; + } + /** * Clear out the keys registered so far. */ diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java deleted file mode 100644 index ccbf5b6bc..000000000 --- a/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.voice; - -import android.content.Intent; - -import com.android.inputmethod.latin.LatinIME; - -public class LatinIMEWithVoice extends LatinIME { - @Override - protected void launchSettings() { - launchSettings(LatinIMEWithVoiceSettings.class); - } -} diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java deleted file mode 100644 index 13a58e14d..000000000 --- a/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.voice; - -import com.android.inputmethod.latin.LatinIMESettings; - -public class LatinIMEWithVoiceSettings extends LatinIMESettings {} diff --git a/java/src/com/android/inputmethod/voice/VoiceInput.java b/java/src/com/android/inputmethod/voice/VoiceInput.java index ac06ab50d..f24c180d0 100644 --- a/java/src/com/android/inputmethod/voice/VoiceInput.java +++ b/java/src/com/android/inputmethod/voice/VoiceInput.java @@ -25,6 +25,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.Parcelable; import android.speech.RecognitionListener; import android.speech.SpeechRecognizer; import android.speech.RecognizerIntent; @@ -52,6 +53,8 @@ public class VoiceInput implements OnClickListener { private static final String EXTRA_RECOGNITION_CONTEXT = "android.speech.extras.RECOGNITION_CONTEXT"; private static final String EXTRA_CALLING_PACKAGE = "calling_package"; + private static final String EXTRA_ALTERNATES = "android.speech.extra.ALTERNATES"; + private static final int MAX_ALT_LIST_LENGTH = 6; private static final String DEFAULT_RECOMMENDED_PACKAGES = "com.android.mms " + @@ -63,7 +66,7 @@ public class VoiceInput implements OnClickListener { // WARNING! Before enabling this, fix the problem with calling getExtractedText() in // landscape view. It causes Extracted text updates to be rejected due to a token mismatch - public static boolean ENABLE_WORD_CORRECTIONS = false; + public static boolean ENABLE_WORD_CORRECTIONS = true; // Dummy word suggestion which means "delete current word" public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol @@ -73,6 +76,25 @@ public class VoiceInput implements OnClickListener { private VoiceInputLogger mLogger; + // Names of a few extras defined in VoiceSearch's RecognitionController + // Note, the version of voicesearch that shipped in Froyo returns the raw + // RecognitionClientAlternates protocol buffer under the key "alternates", + // so a VS market update must be installed on Froyo devices in order to see + // alternatives. + private static final String ALTERNATES_BUNDLE = "alternates_bundle"; + + // This is copied from the VoiceSearch app. + private static final class AlternatesBundleKeys { + public static final String ALTERNATES = "alternates"; + public static final String CONFIDENCE = "confidence"; + public static final String LENGTH = "length"; + public static final String MAX_SPAN_LENGTH = "max_span_length"; + public static final String SPANS = "spans"; + public static final String SPAN_KEY_DELIMITER = ":"; + public static final String START = "start"; + public static final String TEXT = "text"; + } + // Names of a few intent extras defined in VoiceSearch's RecognitionService. // These let us tweak the endpointer parameters. private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS = @@ -304,12 +326,12 @@ public class VoiceInput implements OnClickListener { intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, ""); intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle()); intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME"); + intent.putExtra(EXTRA_ALTERNATES, true); intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, SettingsUtil.getSettingsInt( mContext.getContentResolver(), SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS, 1)); - // Get endpointer params from Gservices. // TODO: Consider caching these values for improved performance on slower devices. final ContentResolver cr = mContext.getContentResolver(); @@ -563,26 +585,42 @@ public class VoiceInput implements OnClickListener { public void onResults(Bundle resultsBundle) { List<String> results = resultsBundle .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + // VS Market update is needed for IME froyo clients to access the alternatesBundle + // TODO: verify this. + Bundle alternatesBundle = resultsBundle.getBundle(ALTERNATES_BUNDLE); mState = DEFAULT; final Map<String, List<CharSequence>> alternatives = - new HashMap<String, List<CharSequence>>(); - if (results.size() >= 2 && ENABLE_WORD_CORRECTIONS) { - final String[][] words = new String[results.size()][]; - for (int i = 0; i < words.length; i++) { - words[i] = results.get(i).split(" "); - } - - for (int key = 0; key < words[0].length; key++) { - alternatives.put(words[0][key], new ArrayList<CharSequence>()); - for (int alt = 1; alt < words.length; alt++) { - int keyBegin = key * words[alt].length / words[0].length; - int keyEnd = (key + 1) * words[alt].length / words[0].length; - - for (int i = keyBegin; i < Math.min(words[alt].length, keyEnd); i++) { - List<CharSequence> altList = alternatives.get(words[0][key]); - if (!altList.contains(words[alt][i]) && altList.size() < 6) { - altList.add(words[alt][i]); + new HashMap<String, List<CharSequence>>(); + + if (ENABLE_WORD_CORRECTIONS && alternatesBundle != null && results.size() > 0) { + // Use the top recognition result to map each alternative's start:length to a word. + String[] words = results.get(0).split(" "); + Bundle spansBundle = alternatesBundle.getBundle(AlternatesBundleKeys.SPANS); + for (String key : spansBundle.keySet()) { + // Get the word for which these alternates correspond to. + Bundle spanBundle = spansBundle.getBundle(key); + int start = spanBundle.getInt(AlternatesBundleKeys.START); + int length = spanBundle.getInt(AlternatesBundleKeys.LENGTH); + // Only keep single-word based alternatives. + if (length == 1 && start < words.length) { + // Get the alternatives associated with the span. + // If a word appears twice in a recognition result, + // concatenate the alternatives for the word. + List<CharSequence> altList = alternatives.get(words[start]); + if (altList == null) { + altList = new ArrayList<CharSequence>(); + alternatives.put(words[start], altList); + } + Parcelable[] alternatesArr = spanBundle + .getParcelableArray(AlternatesBundleKeys.ALTERNATES); + for (int j = 0; j < alternatesArr.length && + altList.size() < MAX_ALT_LIST_LENGTH; j++) { + Bundle alternateBundle = (Bundle) alternatesArr[j]; + String alternate = alternateBundle.getString(AlternatesBundleKeys.TEXT); + // Don't allow duplicates in the alternates list. + if (!altList.contains(alternate)) { + altList.add(alternate); } } } diff --git a/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java b/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java deleted file mode 100644 index 8a339d14a..000000000 --- a/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * - * 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.google.android.voicesearch; - -import android.content.Intent; - -import com.android.inputmethod.latin.LatinIME; - -public class LatinIMEWithVoice extends LatinIME { - @Override - protected void launchSettings() { - launchSettings(LatinIMEWithVoiceSettings.class); - } -} diff --git a/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java b/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java deleted file mode 100644 index a53cebfd9..000000000 --- a/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.google.android.voicesearch; - -import com.android.inputmethod.latin.LatinIMESettings; - -public class LatinIMEWithVoiceSettings extends LatinIMESettings {} |