diff options
Diffstat (limited to 'java/src')
18 files changed, 2800 insertions, 178 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 93f1985ca..94331d3f2 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -85,8 +85,8 @@ public class AutoDictionary extends ExpandableDictionary { private static DatabaseHelper mOpenHelper = 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) { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 8d2363012..fad56c5d9 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -24,7 +24,6 @@ import java.nio.channels.Channels; import java.util.Arrays; import android.content.Context; -import android.content.res.AssetManager; import android.util.Log; /** @@ -40,6 +39,7 @@ public class BinaryDictionary extends Dictionary { 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; private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; @@ -53,9 +53,9 @@ public class BinaryDictionary extends Dictionary { static { try { - System.loadLibrary("jni_latinime"); + System.loadLibrary("jni_latinime2"); } catch (UnsatisfiedLinkError ule) { - Log.e("BinaryDictionary", "Could not load native library jni_latinime"); + Log.e("BinaryDictionary", "Could not load native library jni_latinime2"); } } @@ -64,10 +64,11 @@ 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) { + public BinaryDictionary(Context context, int resId, int dicTypeId) { if (resId != 0) { loadDictionary(context, resId); } + mDicTypeId = dicTypeId; } /** @@ -75,7 +76,7 @@ public class BinaryDictionary extends Dictionary { * @param context application context for reading resources * @param byteBuffer a ByteBuffer containing the binary dictionary */ - public BinaryDictionary(Context context, ByteBuffer byteBuffer) { + public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) { if (byteBuffer != null) { if (byteBuffer.isDirect()) { mNativeDictDirectBuffer = byteBuffer; @@ -88,6 +89,7 @@ public class BinaryDictionary extends Dictionary { mNativeDict = openNative(mNativeDictDirectBuffer, TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); } + mDicTypeId = dicTypeId; } private native int openNative(ByteBuffer bb, int typedLetterMultiplier, int fullWordMultiplier); @@ -194,7 +196,7 @@ public class BinaryDictionary extends Dictionary { len++; } if (len > 0) { - callback.addWord(mOutputChars, start, len, mFrequencies[j], DataType.UNIGRAM); + 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 ae45001b8..e0de96543 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; @@ -167,7 +166,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; @@ -220,7 +219,6 @@ public class CandidateView extends View { } int x = 0; final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); - final int width = getWidth(); final Rect bgPadding = mBgPadding; final Paint paint = mPaint; final int touchX = mTouchX; @@ -325,7 +323,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); @@ -376,8 +373,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; @@ -412,7 +415,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); } diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java index 15edb706a..f5ff865c4 100644 --- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -20,7 +20,6 @@ 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; @@ -37,21 +36,23 @@ public class ContactsDictionary extends ExpandableDictionary { 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); diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 54317c861..a02edeee5 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -51,10 +51,11 @@ 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); } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 6f4d925ee..d8a9547c1 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -16,11 +16,8 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.Dictionary.WordCallback; - 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 @@ -29,6 +26,7 @@ import android.os.SystemClock; public class ExpandableDictionary extends Dictionary { private Context mContext; private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; + private int mDicTypeId; private int mMaxDepth; private int mInputLength; private int[] mNextLettersFrequencies; @@ -75,10 +73,11 @@ public class ExpandableDictionary extends Dictionary { 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() { @@ -267,7 +266,8 @@ public class ExpandableDictionary extends Dictionary { if (completion) { word[depth] = c; if (terminal) { - if (!callback.addWord(word, 0, depth + 1, freq * snr, DataType.UNIGRAM)) { + if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId, + DataType.UNIGRAM)) { return; } // Add to frequency of next letters for predictive correction @@ -305,7 +305,7 @@ 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); } } diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index 5e835e543..718fda18d 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -99,7 +99,7 @@ public class InputLanguageSelection extends PreferenceActivity { boolean haveDictionary = false; conf.locale = locale; res.updateConfiguration(conf, res.getDisplayMetrics()); - BinaryDictionary bd = new BinaryDictionary(this, R.raw.main); + BinaryDictionary bd = new BinaryDictionary(this, R.raw.main, 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..bbde23221 100644 --- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -21,12 +21,15 @@ 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.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 +48,12 @@ 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[] LAYOUTS = new int [] { + R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal, + R.layout.input_stone_bold}; + 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 +66,8 @@ public class KeyboardSwitcher { KEYBOARDMODE_IM, KEYBOARDMODE_WEB}; - //LatinIME mContext; Context mContext; - InputMethodService mInputMethodService; + LatinIME mInputMethodService; private KeyboardId mSymbolsId; private KeyboardId mSymbolsShiftedId; @@ -67,7 +75,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 +87,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,13 +112,22 @@ 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( + isBlackSym() ? R.xml.kbd_symbols_black : R.xml.kbd_symbols, hasVoice); + } + + private KeyboardId makeSymbolsShiftedId(boolean hasVoice) { + return new KeyboardId( + isBlackSym() ? R.xml.kbd_symbols_shift_black : R.xml.kbd_symbols_shift, hasVoice); + } + void makeKeyboards(boolean forceCreate) { if (forceCreate) mKeyboards.clear(); // Configuration change is coming after the keyboard gets recreated. So don't rely on that. @@ -114,9 +137,8 @@ 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); + mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary); + mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary); } /** @@ -140,6 +162,7 @@ public class KeyboardSwitcher { this(xml, 0, false, hasVoice); } + @Override public boolean equals(Object other) { return other instanceof KeyboardId && equals((KeyboardId) other); } @@ -150,6 +173,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 +197,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) { @@ -188,8 +218,8 @@ public class KeyboardSwitcher { mInputView.setPreviewEnabled(true); 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 +231,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 +243,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 +269,35 @@ public class KeyboardSwitcher { private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { boolean hasVoice = hasVoiceButton(isSymbols); + // TODO: generalize for any KeyboardId + int keyboardRowsResId = isBlackSym() ? R.xml.kbd_qwerty_black : R.xml.kbd_qwerty; if (isSymbols) { return (mode == MODE_PHONE) - ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice) - : new KeyboardId(R.xml.kbd_symbols, hasVoice); + ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice) : makeSymbolsId(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) { + if (mTextMode == MODE_TEXT_ALPHA) { return new KeyboardId(R.xml.kbd_alpha, 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 makeSymbolsId(hasVoice); case MODE_PHONE: return new KeyboardId(R.xml.kbd_phone, hasVoice); case MODE_URL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true, hasVoice); + 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 +310,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; } @@ -348,4 +372,63 @@ 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 (LAYOUTS.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(LAYOUTS[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(LAYOUTS[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; + } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 51fb9d876..2527c81fa 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -31,7 +31,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.Keyboard; -import android.inputmethodservice.KeyboardView; import android.media.AudioManager; import android.os.Debug; import android.os.Handler; @@ -73,7 +72,7 @@ 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"; @@ -147,7 +146,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; @@ -227,8 +226,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; @@ -309,8 +309,9 @@ public class LatinIME extends InputMethodService 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 @@ -333,6 +334,7 @@ public class LatinIME extends InputMethodService }; @Override public void onCreate() { + LatinImeLogger.init(this); super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); mResources = getResources(); @@ -348,7 +350,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(); @@ -389,12 +402,12 @@ public class LatinIME extends InputMethodService 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); mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); mSuggest.setAutoDictionary(mAutoDictionary); @@ -408,12 +421,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(); } @@ -453,15 +472,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 @@ -478,8 +494,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; } @@ -587,7 +604,7 @@ public class LatinIME extends InputMethodService attribute.imeOptions, enableVoiceButton); updateShiftKeyState(attribute); } - mInputView.closing(); + inputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; @@ -603,7 +620,7 @@ public class LatinIME extends InputMethodService updateCorrectionMode(); - mInputView.setProximityCorrectionEnabled(true); + inputView.setProximityCorrectionEnabled(true); mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions); checkTutorial(attribute.privateImeOptions); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); @@ -613,6 +630,8 @@ public class LatinIME extends InputMethodService public void onFinishInput() { super.onFinishInput(); + LatinImeLogger.commit(); + if (VOICE_INSTALLED && !mConfigurationChanging) { if (mAfterVoiceInput) { mVoiceInput.flushAllTextModificationCounters(); @@ -621,8 +640,8 @@ 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(); } @@ -694,8 +713,9 @@ public class LatinIME extends InputMethodService mLastSelectionEnd = newSelEnd; + // TODO: Uncomment this block when we enable re-editing feature // If a word is selected - if (isPredictionOn() && mJustRevertedSeparator == null + /*if (isPredictionOn() && mJustRevertedSeparator == null && (candidatesStart == candidatesEnd || newSelStart != oldSelStart) && (newSelStart < newSelEnd - 1 || (!mPredicting)) && !mVoiceInputHighlighted) { @@ -704,11 +724,13 @@ public class LatinIME extends InputMethodService } else { abortCorrection(false); } - } + }*/ } @Override public void hideWindow() { + LatinImeLogger.commit(); + if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); @@ -751,6 +773,7 @@ public class LatinIME extends InputMethodService CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } + // When in fullscreen mode, show completions generated by the application setSuggestions(stringList, true, true, true); mBestWord = null; setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); @@ -762,7 +785,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()); } } @@ -791,8 +815,8 @@ public class LatinIME extends InputMethodService 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(); @@ -824,8 +848,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(), @@ -858,7 +884,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); @@ -886,9 +913,10 @@ 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.getInputView() != null + && mKeyboardSwitcher.isAlphabetMode() && ic != null) { + mKeyboardSwitcher.getInputView().setShifted( + mCapsLock || getCursorCapsMode(ic, attr) != 0); } } @@ -1001,6 +1029,7 @@ public class LatinIME extends InputMethodService case Keyboard.KEYCODE_DELETE: handleBackspace(); mDeleteCount++; + LatinImeLogger.logOnDelete(); break; case Keyboard.KEYCODE_SHIFT: handleShift(); @@ -1041,6 +1070,7 @@ public class LatinIME extends InputMethodService if (primaryCode != KEYCODE_ENTER) { mJustAddedAutoSpace = false; } + LatinImeLogger.logOnInputChar((char)primaryCode); if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { @@ -1140,7 +1170,8 @@ public class LatinIME extends InputMethodService if (mKeyboardSwitcher.isAlphabetMode()) { // Alphabet keyboard checkToggleCapsLock(); - mInputView.setShifted(mCapsLock || !mInputView.isShifted()); + mKeyboardSwitcher.getInputView().setShifted(mCapsLock + || !mKeyboardSwitcher.getInputView().isShifted()); } else { mKeyboardSwitcher.toggleShift(); } @@ -1172,16 +1203,21 @@ public class LatinIME extends InputMethodService 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); @@ -1230,8 +1266,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) { @@ -1261,8 +1296,8 @@ public class LatinIME extends InputMethodService } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) { doubleSpace(); } - if (pickedDefault && mBestWord != null) { - TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); + if (pickedDefault) { + TextEntryState.backToAcceptedDefault(mWord.getTypedWord()); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { @@ -1276,7 +1311,7 @@ public class LatinIME extends InputMethodService mVoiceInput.cancel(); } requestHideSelf(0); - mInputView.closing(); + mKeyboardSwitcher.getInputView().closing(); TextEntryState.endSession(); } @@ -1293,7 +1328,7 @@ public class LatinIME extends InputMethodService } private void checkToggleCapsLock() { - if (mInputView.getKeyboard().isShifted()) { + if (mKeyboardSwitcher.getInputView().getKeyboard().isShifted()) { toggleCapsLock(); } } @@ -1301,7 +1336,8 @@ public class LatinIME extends InputMethodService private void toggleCapsLock() { mCapsLock = !mCapsLock; if (mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setShiftLocked( + mCapsLock); } } @@ -1334,8 +1370,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(); }}); @@ -1434,7 +1470,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); @@ -1470,7 +1506,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()); @@ -1525,7 +1562,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) { @@ -1540,13 +1578,13 @@ public class LatinIME extends InputMethodService } private List<CharSequence> getTypedSuggestions(WordComposer word) { - List<CharSequence> stringList = mSuggest.getSuggestions(mInputView, word, false, null); + List<CharSequence> stringList = mSuggest.getSuggestions(mKeyboardSwitcher.getInputView(), word, false, null); return stringList; } private void showCorrections(WordAlternatives alternatives) { List<CharSequence> stringList = alternatives.getAlternatives(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null); showSuggestions(stringList, alternatives.getOriginalWord(), false, false); } @@ -1554,21 +1592,22 @@ public class LatinIME extends InputMethodService //long startTime = System.currentTimeMillis(); // TIME MEASUREMENT! // TODO Maybe need better way of retrieving previous word CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection()); - List<CharSequence> stringList = mSuggest.getSuggestions(mInputView, word, false, + List<CharSequence> stringList = mSuggest.getSuggestions(mKeyboardSwitcher.getInputView(), word, false, prevWord); //long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT! //Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime)); 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 = word.getTypedWord(); // If we're in basic correct boolean typedWordValid = mSuggest.isValidWord(typedWord) || - (preferCapitalization() + (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase())); if (mCorrectionMode == Suggest.CORRECTION_FULL) { correctionAvailable |= typedWordValid; @@ -1595,7 +1634,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); @@ -1607,11 +1646,14 @@ public class LatinIME extends InputMethodService pickSuggestion(mBestWord); // Add the word to the auto dictionary if it's not a known word checkAddToDictionary(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(); @@ -1642,7 +1684,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(); @@ -1655,6 +1702,8 @@ public class LatinIME extends InputMethodService if (index == 0) { checkAddToDictionary(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); } + LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), + index, suggestions); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space if (mAutoSpace && !correcting) { @@ -1697,10 +1746,12 @@ public class LatinIME extends InputMethodService } private void pickSuggestion(CharSequence suggestion) { + 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(); } @@ -1714,7 +1765,7 @@ public class LatinIME extends InputMethodService saveWordInHistory(suggestion); mPredicting = false; mCommittedLength = suggestion.length(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); + ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); setNextSuggestions(); updateShiftKeyState(getCurrentInputEditorInfo()); } @@ -1900,7 +1951,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)); } @@ -1924,7 +1975,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()); } } } @@ -1975,7 +2026,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(); } @@ -2027,7 +2078,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(); } } @@ -2054,8 +2105,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); } } @@ -2173,14 +2225,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); @@ -2209,7 +2265,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); @@ -2219,7 +2275,8 @@ public class LatinIME extends InputMethodService private void changeKeyboardMode() { mKeyboardSwitcher.toggleSymbols(); if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setShiftLocked( + mCapsLock); } updateShiftKeyState(getCurrentInputEditorInfo()); 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..e8899504e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -0,0 +1,770 @@ +/* + * 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.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 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 sLOGPRINT = 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 = true; + + 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; + + 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 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; + private static int sLastAutoSuggestDicTypeId; + private static HashMap<String, Integer> sSuggestDicMap = new HashMap<String, Integer>(); + 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 (sLOGPRINT) { + 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, ""); + sLOGPRINT = prefs.getBoolean(PREF_DEBUG_MODE, sLOGPRINT); + sDBG = sLOGPRINT; + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + 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 (sLOGPRINT) { + Log.d(TAG, "Log Exception. (1)"); + } + mLogBuffer.add(new LogEntry(time, ID_EXCEPTION, data)); + } + + private void flushPrivacyLogSafely() { + if (sLOGPRINT) { + 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: + 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 (sLOGPRINT) { + 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)) { + sLOGPRINT = sharedPreferences.getBoolean(PREF_DEBUG_MODE, sLOGPRINT); + sDBG = sLOGPRINT; + } 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, 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); + sLatinImeLogger.mManualSuggestCountPerDic[dicTypeId]++; + if (dicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnManualSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + } + // TODO: Don't send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + String[] strings = new String[3 + suggestions.size()]; + strings[0] = before; + strings[1] = after; + strings[2] = String.valueOf(position); + for (int i = 0; i < suggestions.size(); ++i) { + String s = suggestions.get(i).toString(); + strings[i + 3] = sSuggestDicMap.containsKey(s) ? s : ""; + } + sLatinImeLogger.sendLogToDropBox(ID_MANUALSUGGESTION, 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); + sLatinImeLogger.mAutoSuggestCountPerDic[sLastAutoSuggestDicTypeId]++; + if (sLastAutoSuggestDicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnAutoSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + } + // TODO: Not to send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + String[] strings = new String[] {before, after, separator}; + 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, ""}); + } + } + + public static void onStartSuggestion() { + if (sLogEnabled) { + sSuggestDicMap.clear(); + } + } + + public static void onAddSuggestedWord(String word, int typeId) { + if (sLogEnabled) { + sSuggestDicMap.put(word, typeId); + } + } + + 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 ea6b74e1b..db4d167d4 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -81,8 +81,10 @@ public class LatinKeyboard extends Keyboard { 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; @@ -121,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) ' '); } @@ -177,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: @@ -196,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; @@ -271,6 +274,10 @@ public class LatinKeyboard extends Keyboard { } } + /* package */ boolean isAlphaKeyboard() { + return mIsAlphaKeyboard; + } + public void setExtension(int resId) { mExtensionResId = resId; } @@ -279,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()); } @@ -316,22 +343,23 @@ 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) { + 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); @@ -341,12 +369,14 @@ public class LatinKeyboard extends Keyboard { paint.setTextAlign(Align.CENTER); final String language = getInputLanguage(mSpaceKey.width, paint); final int ascent = (int) -paint.ascent(); - paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_shadow)); - canvas.drawText(language, - width / 2, ascent - 1, 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); + canvas.drawText(language, width / 2, ascent, paint); // Put arrows on either side of the text if (mLanguageSwitcher.getLocaleCount() > 1) { Rect bounds = new Rect(); @@ -431,7 +461,7 @@ public class LatinKeyboard extends Keyboard { } if (mLocale != null && mLocale.equals(locale)) return; mLocale = locale; - updateSpaceBarForLocale(); + updateSpaceBarForLocale(mIsBlackSym); } boolean isCurrentlyInSpace() { @@ -677,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); 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..4205aadcf --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java @@ -0,0 +1,1511 @@ +/* + * 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 MSG_SHOW_PREVIEW = 1; + private static final int MSG_REMOVE_PREVIEW = 2; + private static final int MSG_REPEAT = 3; + private static final int MSG_LONGPRESS = 4; + + 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 int mKeyDebounceThreshold; + private static final int KEY_DEBOUNCE_FACTOR = 6; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = true; + private int mPopupPreviewX; + private int mPopupPreviewY; + private int mWindowY; + + private int mLastX; + private int mLastY; + private int mStartX; + private int mStartY; + + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private long mDownTime; + private long mLastMoveTime; + private int mLastKey; + private int mLastCodeX; + private int mLastCodeY; + private int mCurrentKey = NOT_A_KEY; + private int mDownKey = NOT_A_KEY; + private long mLastKeyTime; + private long mCurrentKeyTime; + private int[] mKeyIndices = new int[12]; + 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; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SHOW_PREVIEW: + showKey(msg.arg1); + break; + case MSG_REMOVE_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT: + if (repeatKey()) { + Message repeat = Message.obtain(this, MSG_REPEAT); + sendMessageDelayed(repeat, REPEAT_INTERVAL); + } + break; + case MSG_LONGPRESS: + openPopupIfRequired((MotionEvent) msg.obj); + break; + } + } + }; + + 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 + removeMessages(); + 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 + + // 1/KEY_DEBOUNCE_FACTOR of distance between adjacent keys + mKeyDebounceThreshold = mProximityThreshold / KEY_DEBOUNCE_FACTOR; + } + + @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) { + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(mLastX, mLastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint); + } + } + + mDrawPending = false; + mDirtyRect.setEmpty(); + } + + private int getKeyIndices(int x, int y, int[] allKeys) { + final Key[] keys = mKeys; + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + java.util.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); + for (int c = 0; c < nCodes; c++) { + allKeys[j + c] = key.codes[c]; + mDistances[j + c] = 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); + getKeyIndices(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + 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) { + mHandler.removeMessages(MSG_SHOW_PREVIEW); + if (previewPopup.isShowing()) { + if (keyIndex == NOT_A_KEY) { + mHandler.sendMessageDelayed(mHandler + .obtainMessage(MSG_REMOVE_PREVIEW), + DELAY_AFTER_PREVIEW); + } + } + if (keyIndex != NOT_A_KEY) { + if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { + // Show right away, if it's already visible and finger is moving around + showKey(keyIndex); + } else { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0), + 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.removeMessages(MSG_REMOVE_PREVIEW); + 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 isMinorMoveForKeyDebounce(int x, int y) { + // TODO: Check the coordinate against each key border. The current + // logic is pretty simple. + return ((x - mLastCodeX) * (x - mLastCodeX) + + (y - mLastCodeY) * (y - mLastCodeY)) < mKeyDebounceThreshold; + } + + 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 = getKeyIndices(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.removeMessages(MSG_REPEAT); + mHandler.removeMessages(MSG_LONGPRESS); + 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; + mStartX = touchX; + mStartY = touchY; + mLastCodeX = touchX; + mLastCodeY = touchY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastKey = NOT_A_KEY; + mCurrentKey = keyIndex; + mDownKey = keyIndex; + mDownTime = me.getEventTime(); + mLastMoveTime = mDownTime; + checkMultiTap(eventTime, keyIndex); + mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? + mKeys[keyIndex].codes[0] : 0); + if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { + mRepeatKeyIndex = mCurrentKey; + Message msg = mHandler.obtainMessage(MSG_REPEAT); + mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); + repeatKey(); + // Delivering the key could have caused an abort + if (mAbortKey) { + mRepeatKeyIndex = NOT_A_KEY; + break; + } + } + if (mCurrentKey != NOT_A_KEY) { + Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); + mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_MOVE: + boolean continueLongPress = false; + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mCurrentKeyTime = eventTime - mDownTime; + } else { + if (keyIndex == mCurrentKey + || isMinorMoveForKeyDebounce(touchX, touchY)) { + mCurrentKeyTime += eventTime - mLastMoveTime; + continueLongPress = true; + } else if (mRepeatKeyIndex == NOT_A_KEY) { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastCodeX = mLastX; + mLastCodeY = mLastY; + mLastKeyTime = + mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + } + } + if (!continueLongPress) { + // Cancel old longpress + mHandler.removeMessages(MSG_LONGPRESS); + // Start new longpress if key has changed + if (keyIndex != NOT_A_KEY) { + Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); + mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT); + } + } + showPreview(mCurrentKey); + mLastMoveTime = eventTime; + break; + + case MotionEvent.ACTION_UP: + removeMessages(); + if (keyIndex == mCurrentKey) { + mCurrentKeyTime += eventTime - mLastMoveTime; + } else { + resetMultiTap(); + mLastKey = mCurrentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKey = keyIndex; + mCurrentKeyTime = 0; + } + if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME + && mLastKey != NOT_A_KEY) { + mCurrentKey = mLastKey; + touchX = mLastCodeX; + touchY = mLastCodeY; + } + showPreview(NOT_A_KEY); + Arrays.fill(mKeyIndices, 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: + removeMessages(); + dismissPopupKeyboard(); + mAbortKey = true; + showPreview(NOT_A_KEY); + invalidateKey(mCurrentKey); + break; + } + mLastX = touchX; + mLastY = 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(); + } + removeMessages(); + + dismissPopupKeyboard(); + mBuffer = null; + mCanvas = null; + mMiniKeyboardCache.clear(); + } + + private void removeMessages() { + mHandler.removeMessages(MSG_REPEAT); + mHandler.removeMessages(MSG_LONGPRESS); + mHandler.removeMessages(MSG_SHOW_PREVIEW); + } + + @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 323f4bf6b..bce2cde25 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -22,7 +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.Keyboard.Key; import android.os.Handler; import android.os.Message; @@ -32,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; @@ -65,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); } @@ -77,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); @@ -106,6 +111,19 @@ 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; + } + /** * 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. @@ -294,7 +312,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())); @@ -465,7 +484,16 @@ public class LatinKeyboardView extends KeyboardView { @Override public void draw(Canvas c) { - super.draw(c); + 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); diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 3e6090c72..6705e9a36 100755 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -53,6 +53,14 @@ public class Suggest implements Dictionary.WordCallback { */ 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; private BinaryDictionary mMainDict; @@ -88,12 +96,12 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; public Suggest(Context context, int dictionaryResId) { - mMainDict = new BinaryDictionary(context, dictionaryResId); + mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN); initPool(); } public Suggest(Context context, ByteBuffer byteBuffer) { - mMainDict = new BinaryDictionary(context, byteBuffer); + mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN); initPool(); } @@ -196,6 +204,7 @@ public class Suggest implements Dictionary.WordCallback { */ public List<CharSequence> getSuggestions(View view, WordComposer wordComposer, boolean includeTypedWordIfValid, CharSequence prevWordForBigram) { + LatinImeLogger.onStartSuggestion(); mHaveCorrection = false; mCapitalize = wordComposer.isCapitalized(); collectGarbage(mSuggestions, mPrefMaxSuggestions); @@ -207,10 +216,12 @@ public class Suggest implements Dictionary.WordCallback { if (mOriginalWord != null) { mOriginalWord = mOriginalWord.toString(); mLowerOriginalWord = mOriginalWord.toString().toLowerCase(); + LatinImeLogger.onAddSuggestedWord(mOriginalWord.toString(), Suggest.DIC_USER_TYPED); } else { mLowerOriginalWord = ""; } + // Search the dictionary only if there are at least 2 characters if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM || mCorrectionMode == CORRECTION_BASIC)) { // At first character, just get the bigrams @@ -356,7 +367,7 @@ public class Suggest implements Dictionary.WordCallback { } public boolean addWord(final char[] word, final int offset, final int length, int freq, - final Dictionary.DataType dataType) { + final int dicTypeId, final Dictionary.DataType dataType) { ArrayList<CharSequence> suggestions; int[] priorities; int prefMaxSuggestions; @@ -404,7 +415,7 @@ public class Suggest implements Dictionary.WordCallback { pos++; } } - + if (pos >= prefMaxSuggestions) { return true; } @@ -430,6 +441,8 @@ public class Suggest implements Dictionary.WordCallback { if (garbage instanceof StringBuilder) { mStringPool.add(garbage); } + } else { + LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId); } return true; } diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index 8fd9b7129..224423c23 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -132,8 +132,23 @@ public class TextEntryState { sTypedChars += typedWord.length(); sActualChars += actualWord.length(); sState = STATE_ACCEPTED_DEFAULT; + LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString()); } - + + // STATE_ACCEPTED_DEFAULT will be changed to other sub-states + // (see "case STATE_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 STATE_SPACE_AFTER_ACCEPTED: + case STATE_PUNCTUATION_AFTER_ACCEPTED: + case STATE_IN_WORD: + sState = STATE_ACCEPTED_DEFAULT; + break; + } + } + public static void acceptedTyped(CharSequence typedWord) { sWordNotInDictionaryCount++; sState = STATE_PICKED_SUGGESTION; @@ -211,6 +226,7 @@ public class TextEntryState { if (sState == STATE_ACCEPTED_DEFAULT) { sState = STATE_UNDO_COMMIT; sAutoSuggestUndoneCount++; + LatinImeLogger.logOnAutoSuggestionCanceled(); } else if (sState == STATE_UNDO_COMMIT) { sState = STATE_IN_WORD; } 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); |