diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
36 files changed, 3455 insertions, 7559 deletions
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 4fbb5b012..307b81d43 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -16,10 +16,6 @@ package com.android.inputmethod.latin; -import java.util.HashMap; -import java.util.Set; -import java.util.Map.Entry; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -30,6 +26,10 @@ import android.os.AsyncTask; import android.provider.BaseColumns; import android.util.Log; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Set; + /** * Stores new words temporarily until they are promoted to the user dictionary * for longevity. Words in the auto dictionary are used to determine if it's ok @@ -98,7 +98,7 @@ public class AutoDictionary extends ExpandableDictionary { } @Override - public boolean isValidWord(CharSequence word) { + public synchronized boolean isValidWord(CharSequence word) { final int frequency = getWordFrequency(word); return frequency >= VALIDITY_THRESHOLD; } @@ -138,7 +138,8 @@ public class AutoDictionary extends ExpandableDictionary { } @Override - public void addWord(String word, int addFrequency) { + public void addWord(String newWord, int addFrequency) { + String word = newWord; final int length = word.length(); // Don't add very short or very long words. if (length < 2 || length > getMaxWordLength()) return; @@ -224,7 +225,7 @@ public class AutoDictionary extends ExpandableDictionary { private final DatabaseHelper mDbHelper; private final String mLocale; - public UpdateDbTask(Context context, DatabaseHelper openHelper, + public UpdateDbTask(@SuppressWarnings("unused") Context context, DatabaseHelper openHelper, HashMap<String, Integer> pendingWrites, String locale) { mMap = pendingWrites; mLocale = locale; diff --git a/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java b/java/src/com/android/inputmethod/latin/BackupAgent.java index a14a4751f..ee070af75 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMEBackupAgent.java +++ b/java/src/com/android/inputmethod/latin/BackupAgent.java @@ -22,7 +22,7 @@ import android.app.backup.SharedPreferencesBackupHelper; /** * Backs up the Latin IME shared preferences. */ -public class LatinIMEBackupAgent extends BackupAgentHelper { +public class BackupAgent extends BackupAgentHelper { @Override public void onCreate() { diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index d0e143dd0..813f7d32f 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2008 The Android Open Source Project - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -16,16 +16,12 @@ package com.android.inputmethod.latin; -import java.io.InputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.Channels; -import java.util.Arrays; - import android.content.Context; +import android.content.res.AssetFileDescriptor; import android.util.Log; +import java.util.Arrays; + /** * Implements a static, compacted, binary dictionary of standard words. */ @@ -45,115 +41,79 @@ public class BinaryDictionary extends Dictionary { private static final int MAX_BIGRAMS = 60; private static final int TYPED_LETTER_MULTIPLIER = 2; - private static final boolean ENABLE_MISSED_CHARACTERS = true; + private static final BinaryDictionary sInstance = new BinaryDictionary(); private int mDicTypeId; private int mNativeDict; - private int mDictLength; - private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; - private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; - private char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; - private int[] mFrequencies = new int[MAX_WORDS]; - private int[] mFrequencies_bigrams = new int[MAX_BIGRAMS]; - // Keep a reference to the native dict direct buffer in Java to avoid - // unexpected deallocation of the direct buffer. - private ByteBuffer mNativeDictDirectBuffer; + private long mDictLength; + private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; + private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; + private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; + private final int[] mFrequencies = new int[MAX_WORDS]; + private final int[] mFrequencies_bigrams = new int[MAX_BIGRAMS]; static { try { System.loadLibrary("jni_latinime"); } catch (UnsatisfiedLinkError ule) { - Log.e("BinaryDictionary", "Could not load native library jni_latinime"); + Log.e(TAG, "Could not load native library jni_latinime"); } } - /** - * Create a dictionary from a raw resource file - * @param context application context for reading resources - * @param resId the resource containing the raw binary dictionary - */ - public BinaryDictionary(Context context, int[] resId, int dicTypeId) { - if (resId != null && resId.length > 0 && resId[0] != 0) { - loadDictionary(context, resId); - } - mDicTypeId = dicTypeId; + private BinaryDictionary() { } /** - * Create a dictionary from a byte buffer. This is used for testing. + * Initialize a dictionary from a raw resource file * @param context application context for reading resources - * @param byteBuffer a ByteBuffer containing the binary dictionary + * @param resId the resource containing the raw binary dictionary + * @return initialized instance of BinaryDictionary */ - public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) { - if (byteBuffer != null) { - if (byteBuffer.isDirect()) { - mNativeDictDirectBuffer = byteBuffer; - } else { - mNativeDictDirectBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity()); - byteBuffer.rewind(); - mNativeDictDirectBuffer.put(byteBuffer); + public static BinaryDictionary initDictionary(Context context, int resId, int dicTypeId) { + synchronized (sInstance) { + sInstance.closeInternal(); + if (resId != 0) { + sInstance.loadDictionary(context, resId); + sInstance.mDicTypeId = dicTypeId; } - mDictLength = byteBuffer.capacity(); - mNativeDict = openNative(mNativeDictDirectBuffer, - TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); } - mDicTypeId = dicTypeId; + return sInstance; } - private native int openNative(ByteBuffer bb, int typedLetterMultiplier, - int fullWordMultiplier); + private native int openNative(String sourceDir, long dictOffset, long dictSize, + int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, + int maxWords, int maxAlternatives); private native void closeNative(int dict); private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); - private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize, - char[] outputChars, int[] frequencies, int maxWordLength, int maxWords, - int maxAlternatives, int skipPos, int[] nextLettersFrequencies, int nextLettersSize); + private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize, + char[] outputChars, int[] frequencies, + int[] nextLettersFrequencies, int nextLettersSize); private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies, int maxWordLength, int maxBigrams, int maxAlternatives); - private final void loadDictionary(Context context, int[] resId) { - InputStream[] is = null; + private final void loadDictionary(Context context, int resId) { try { - // merging separated dictionary into one if dictionary is separated - int total = 0; - is = new InputStream[resId.length]; - for (int i = 0; i < resId.length; i++) { - is[i] = context.getResources().openRawResource(resId[i]); - total += is[i].available(); - } - - mNativeDictDirectBuffer = - ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder()); - int got = 0; - for (int i = 0; i < resId.length; i++) { - got += Channels.newChannel(is[i]).read(mNativeDictDirectBuffer); - } - if (got != total) { - Log.e(TAG, "Read " + got + " bytes, expected " + total); - } else { - mNativeDict = openNative(mNativeDictDirectBuffer, - TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); - mDictLength = total; - } - } catch (IOException e) { - Log.w(TAG, "No available memory for binary dictionary"); - } finally { - try { - if (is != null) { - for (int i = 0; i < is.length; i++) { - is[i].close(); - } - } - } catch (IOException e) { - Log.w(TAG, "Failed to close input stream"); + final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId); + if (afd == null) { + Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); + return; } + mNativeDict = openNative(context.getApplicationInfo().sourceDir, + afd.getStartOffset(), afd.getLength(), + TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER, + MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES); + mDictLength = afd.getLength(); + } catch (android.content.res.Resources.NotFoundException e) { + Log.e(TAG, "Could not find the resource. resId=" + resId); + return; } } - @Override public void getBigrams(final WordComposer codes, final CharSequence previousWord, final WordCallback callback, int[] nextLettersFrequencies) { + if (mNativeDict == 0) return; char[] chars = previousWord.toString().toCharArray(); Arrays.fill(mOutputChars_bigrams, (char) 0); @@ -169,12 +129,12 @@ public class BinaryDictionary extends Dictionary { mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS, MAX_ALTERNATIVES); - for (int j = 0; j < count; j++) { + for (int j = 0; j < count; ++j) { if (mFrequencies_bigrams[j] < 1) break; - int start = j * MAX_WORD_LENGTH; + final int start = j * MAX_WORD_LENGTH; int len = 0; - while (mOutputChars_bigrams[start + len] != 0) { - len++; + while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) { + ++len; } if (len > 0) { callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j], @@ -186,10 +146,12 @@ public class BinaryDictionary extends Dictionary { @Override public void getWords(final WordComposer codes, final WordCallback callback, int[] nextLettersFrequencies) { + if (mNativeDict == 0) return; + final int codesSize = codes.size(); // Won't deal with really long words. if (codesSize > MAX_WORD_LENGTH - 1) return; - + Arrays.fill(mInputCodes, -1); for (int i = 0; i < codesSize; i++) { int[] alternatives = codes.getCodesAt(i); @@ -199,33 +161,16 @@ public class BinaryDictionary extends Dictionary { Arrays.fill(mOutputChars, (char) 0); Arrays.fill(mFrequencies, 0); - int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, - mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1, - nextLettersFrequencies, + int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, + mFrequencies, nextLettersFrequencies, nextLettersFrequencies != null ? nextLettersFrequencies.length : 0); - // If there aren't sufficient suggestions, search for words by allowing wild cards at - // the different character positions. This feature is not ready for prime-time as we need - // to figure out the best ranking for such words compared to proximity corrections and - // completions. - if (ENABLE_MISSED_CHARACTERS && count < 5) { - for (int skip = 0; skip < codesSize; skip++) { - int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, - mOutputChars, mFrequencies, - MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip, - null, 0); - count = Math.max(count, tempCount); - if (tempCount > 0) break; - } - } - - for (int j = 0; j < count; j++) { + for (int j = 0; j < count; ++j) { if (mFrequencies[j] < 1) break; - int start = j * MAX_WORD_LENGTH; + final int start = j * MAX_WORD_LENGTH; int len = 0; - while (mOutputChars[start + len] != 0) { - len++; + while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { + ++len; } if (len > 0) { callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId, @@ -241,21 +186,29 @@ public class BinaryDictionary extends Dictionary { return isValidWordNative(mNativeDict, chars, chars.length); } - public int getSize() { - return mDictLength; // This value is initialized on the call to openNative() + public long getSize() { + return mDictLength; // This value is initialized in loadDictionary() } @Override public synchronized void close() { + closeInternal(); + } + + private void closeInternal() { if (mNativeDict != 0) { closeNative(mNativeDict); mNativeDict = 0; + mDictLength = 0; } } @Override protected void finalize() throws Throwable { - close(); - super.finalize(); + try { + closeInternal(); + } finally { + super.finalize(); + } } } diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index 68f288925..fc45c7c75 100755..100644 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -1,12 +1,12 @@ /* - * Copyright (C) 2008 The Android Open Source Project - * + * 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 @@ -16,77 +16,106 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + import android.content.Context; import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.Rect; +import android.graphics.Color; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.CharacterStyle; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; import android.util.AttributeSet; -import android.view.GestureDetector; import android.view.Gravity; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup.LayoutParams; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.TextView; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -public class CandidateView extends View { +public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener { - private static final int OUT_OF_BOUNDS_WORD_INDEX = -1; - private static final int OUT_OF_BOUNDS_X_COORD = -1; + private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); + private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); + private static final int MAX_SUGGESTIONS = 16; - private LatinIME mService; - private final ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); - private boolean mShowingCompletions; - private CharSequence mSelectedString; - private int mSelectedIndex; - private int mTouchX = OUT_OF_BOUNDS_X_COORD; - private final Drawable mSelectionHighlight; - private boolean mTypedWordValid; - - private boolean mHaveMinimalSuggestion; - - private Rect mBgPadding; + private static boolean DBG = LatinImeLogger.sDBG; - private final TextView mPreviewText; - private final PopupWindow mPreviewPopup; - private int mCurrentWordIndex; - private Drawable mDivider; - - private static final int MAX_SUGGESTIONS = 32; - private static final int SCROLL_PIXELS = 20; - - private final int[] mWordWidth = new int[MAX_SUGGESTIONS]; - private final int[] mWordX = new int[MAX_SUGGESTIONS]; - private int mPopupPreviewX; - private int mPopupPreviewY; - - private static final int X_GAP = 10; - + private final ArrayList<View> mWords = new ArrayList<View>(); + private final boolean mConfigCandidateHighlightFontColorEnabled; + private final CharacterStyle mInvertedForegroundColorSpan; + private final CharacterStyle mInvertedBackgroundColorSpan; private final int mColorNormal; private final int mColorRecommended; private final int mColorOther; - private final Paint mPaint; - private final int mDescent; - private boolean mScrolled; + private final PopupWindow mPreviewPopup; + private final TextView mPreviewText; + + private LatinIME mService; + private SuggestedWords mSuggestions = SuggestedWords.EMPTY; + private boolean mShowingAutoCorrectionInverted; private boolean mShowingAddToDictionary; - private CharSequence mAddToDictionaryHint; - private int mTargetScrollX; + private final UiHandler mHandler = new UiHandler(); + + private class UiHandler extends Handler { + private static final int MSG_HIDE_PREVIEW = 0; + private static final int MSG_UPDATE_SUGGESTION = 1; + + private static final long DELAY_HIDE_PREVIEW = 1000; + private static final long DELAY_UPDATE_SUGGESTION = 300; + + @Override + public void dispatchMessage(Message msg) { + switch (msg.what) { + case MSG_HIDE_PREVIEW: + hidePreview(); + break; + case MSG_UPDATE_SUGGESTION: + updateSuggestions(); + break; + } + } + + public void postHidePreview() { + cancelHidePreview(); + sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW); + } + + public void cancelHidePreview() { + removeMessages(MSG_HIDE_PREVIEW); + } + + public void postUpdateSuggestions() { + cancelUpdateSuggestions(); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION), + DELAY_UPDATE_SUGGESTION); + } - private final int mMinTouchableWidth; + public void cancelUpdateSuggestions() { + removeMessages(MSG_UPDATE_SUGGESTION); + } - private int mTotalWidth; - - private final GestureDetector mGestureDetector; + public void cancelAllMessages() { + cancelHidePreview(); + cancelUpdateSuggestions(); + } + } /** * Construct a CandidateView for showing suggested words for completion. @@ -95,95 +124,38 @@ public class CandidateView extends View { */ public CandidateView(Context context, AttributeSet attrs) { super(context, attrs); - mSelectionHighlight = context.getResources().getDrawable( - R.drawable.list_selector_background_pressed); - LayoutInflater inflate = - (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); Resources res = context.getResources(); mPreviewPopup = new PopupWindow(context); - mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null); - mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + LayoutInflater inflater = LayoutInflater.from(context); + mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null); + mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation); + mConfigCandidateHighlightFontColorEnabled = + res.getBoolean(R.bool.config_candidate_highlight_font_color_enabled); mColorNormal = res.getColor(R.color.candidate_normal); mColorRecommended = res.getColor(R.color.candidate_recommended); mColorOther = res.getColor(R.color.candidate_other); - mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider); - mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary); - - mPaint = new Paint(); - mPaint.setColor(mColorNormal); - mPaint.setAntiAlias(true); - mPaint.setTextSize(mPreviewText.getTextSize()); - mPaint.setStrokeWidth(0); - mPaint.setTextAlign(Align.CENTER); - mDescent = (int) mPaint.descent(); - mMinTouchableWidth = (int)res.getDimension(R.dimen.candidate_min_touchable_width); - - mGestureDetector = new GestureDetector( - new CandidateStripGestureListener(mMinTouchableWidth)); - setWillNotDraw(false); - setHorizontalScrollBarEnabled(false); - setVerticalScrollBarEnabled(false); - scrollTo(0, getScrollY()); - } - - private class CandidateStripGestureListener extends GestureDetector.SimpleOnGestureListener { - private final int mTouchSlopSquare; - - public CandidateStripGestureListener(int touchSlop) { - // Slightly reluctant to scroll to be able to easily choose the suggestion - mTouchSlopSquare = touchSlop * touchSlop; + mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorNormal ^ 0x00ffffff); + mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorNormal); + + for (int i = 0; i < MAX_SUGGESTIONS; i++) { + View v = inflater.inflate(R.layout.candidate, null); + TextView tv = (TextView)v.findViewById(R.id.candidate_word); + tv.setTag(i); + tv.setOnClickListener(this); + if (i == 0) + tv.setOnLongClickListener(this); + ImageView divider = (ImageView)v.findViewById(R.id.candidate_divider); + // Do not display divider of first candidate. + divider.setVisibility(i == 0 ? GONE : VISIBLE); + mWords.add(v); } - @Override - public void onLongPress(MotionEvent me) { - if (mSuggestions.size() > 0) { - if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) { - longPressFirstWord(); - } - } - } - - @Override - public boolean onDown(MotionEvent e) { - mScrolled = false; - return false; - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, - float distanceX, float distanceY) { - if (!mScrolled) { - // This is applied only when we recognize that scrolling is starting. - final int deltaX = (int) (e2.getX() - e1.getX()); - final int deltaY = (int) (e2.getY() - e1.getY()); - final int distance = (deltaX * deltaX) + (deltaY * deltaY); - if (distance < mTouchSlopSquare) { - return true; - } - mScrolled = true; - } - - final int width = getWidth(); - mScrolled = true; - int scrollX = getScrollX(); - scrollX += (int) distanceX; - if (scrollX < 0) { - scrollX = 0; - } - if (distanceX > 0 && scrollX + width > mTotalWidth) { - scrollX -= (int) distanceX; - } - mTargetScrollX = scrollX; - scrollTo(scrollX, getScrollY()); - hidePreview(); - invalidate(); - return true; - } + scrollTo(0, getScrollY()); } /** @@ -193,158 +165,113 @@ public class CandidateView extends View { public void setService(LatinIME listener) { mService = listener; } - - @Override - public int computeHorizontalScrollRange() { - return mTotalWidth; - } - /** - * If the canvas is null, then only touch calculations are performed to pick the target - * candidate. - */ - @Override - protected void onDraw(Canvas canvas) { - if (canvas != null) { - super.onDraw(canvas); - } - mTotalWidth = 0; - - final int height = getHeight(); - if (mBgPadding == null) { - mBgPadding = new Rect(0, 0, 0, 0); - if (getBackground() != null) { - getBackground().getPadding(mBgPadding); - } - mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(), - mDivider.getIntrinsicHeight()); + public void setSuggestions(SuggestedWords suggestions) { + if (suggestions == null) + return; + mSuggestions = suggestions; + if (mShowingAutoCorrectionInverted) { + mHandler.postUpdateSuggestions(); + } else { + updateSuggestions(); } + } - final int count = mSuggestions.size(); - final Rect bgPadding = mBgPadding; - final Paint paint = mPaint; - final int touchX = mTouchX; - final int scrollX = getScrollX(); - final boolean scrolled = mScrolled; - final boolean typedWordValid = mTypedWordValid; - final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2; - - boolean existsAutoCompletion = false; - - int x = 0; + private void updateSuggestions() { + final SuggestedWords suggestions = mSuggestions; + clear(); + final int count = suggestions.size(); for (int i = 0; i < count; i++) { - CharSequence suggestion = mSuggestions.get(i); - if (suggestion == null) continue; - final int wordLength = suggestion.length(); - - paint.setColor(mColorNormal); - if (mHaveMinimalSuggestion - && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) { - paint.setTypeface(Typeface.DEFAULT_BOLD); - paint.setColor(mColorRecommended); - existsAutoCompletion = true; + CharSequence word = suggestions.getWord(i); + if (word == null) continue; + final int wordLength = word.length(); + final List<SuggestedWordInfo> suggestedWordInfoList = + suggestions.mSuggestedWordInfoList; + + final View v = mWords.get(i); + final TextView tv = (TextView)v.findViewById(R.id.candidate_word); + final TextView dv = (TextView)v.findViewById(R.id.candidate_debug_info); + tv.setTextColor(mColorNormal); + // TODO: Needs safety net? + if (suggestions.mHasMinimalSuggestion + && ((i == 1 && !suggestions.mTypedWordValid) + || (i == 0 && suggestions.mTypedWordValid))) { + final CharacterStyle style; + if (mConfigCandidateHighlightFontColorEnabled) { + style = BOLD_SPAN; + tv.setTextColor(mColorRecommended); + } else { + style = UNDERLINE_SPAN; + } + final Spannable spannedWord = new SpannableString(word); + spannedWord.setSpan(style, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + word = spannedWord; } else if (i != 0 || (wordLength == 1 && count > 1)) { - // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 and - // there are multiple suggestions, such as the default punctuation list. - paint.setColor(mColorOther); - } - int wordWidth; - if ((wordWidth = mWordWidth[i]) == 0) { - float textWidth = paint.measureText(suggestion, 0, wordLength); - wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2); - mWordWidth[i] = wordWidth; + // HACK: even if i == 0, we use mColorOther when this + // suggestion's length is 1 + // and there are multiple suggestions, such as the default + // punctuation list. + if (mConfigCandidateHighlightFontColorEnabled) + tv.setTextColor(mColorOther); } - - mWordX[i] = x; - - if (touchX != OUT_OF_BOUNDS_X_COORD && !scrolled - && touchX + scrollX >= x && touchX + scrollX < x + wordWidth) { - if (canvas != null && !mShowingAddToDictionary) { - canvas.translate(x, 0); - mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height); - mSelectionHighlight.draw(canvas); - canvas.translate(-x, 0); + tv.setText(word); + tv.setClickable(true); + + if (suggestedWordInfoList != null && suggestedWordInfoList.get(i) != null) { + final SuggestedWordInfo info = suggestedWordInfoList.get(i); + if (info.isPreviousSuggestedWord()) { + int color = tv.getCurrentTextColor(); + tv.setTextColor(Color.argb((int)(Color.alpha(color) * 0.5f), Color.red(color), + Color.green(color), Color.blue(color))); } - mSelectedString = suggestion; - mSelectedIndex = i; - } - - if (canvas != null) { - canvas.drawText(suggestion, 0, wordLength, x + wordWidth / 2, y, paint); - paint.setColor(mColorOther); - canvas.translate(x + wordWidth, 0); - // Draw a divider unless it's after the hint - if (!(mShowingAddToDictionary && i == 1)) { - mDivider.draw(canvas); + final String debugString = info.getDebugString(); + if (DBG) { + if (!TextUtils.isEmpty(debugString)) { + dv.setText(debugString); + dv.setVisibility(VISIBLE); + } } - canvas.translate(-x - wordWidth, 0); - } - paint.setTypeface(Typeface.DEFAULT); - x += wordWidth; - } - mService.onAutoCompletionStateChanged(existsAutoCompletion); - mTotalWidth = x; - if (mTargetScrollX != scrollX) { - scrollToTarget(); - } - } - - private void scrollToTarget() { - int scrollX = getScrollX(); - if (mTargetScrollX > scrollX) { - scrollX += SCROLL_PIXELS; - if (scrollX >= mTargetScrollX) { - scrollX = mTargetScrollX; - scrollTo(scrollX, getScrollY()); - requestLayout(); - } else { - scrollTo(scrollX, getScrollY()); - } - } else { - scrollX -= SCROLL_PIXELS; - if (scrollX <= mTargetScrollX) { - scrollX = mTargetScrollX; - scrollTo(scrollX, getScrollY()); - requestLayout(); } else { - scrollTo(scrollX, getScrollY()); + dv.setVisibility(GONE); } + addView(v); } - invalidate(); - } - - public void setSuggestions(List<CharSequence> suggestions, boolean completions, - boolean typedWordValid, boolean haveMinimalSuggestion) { - clear(); - if (suggestions != null) { - int insertCount = Math.min(suggestions.size(), MAX_SUGGESTIONS); - for (CharSequence suggestion : suggestions) { - mSuggestions.add(suggestion); - if (--insertCount == 0) - break; - } - } - mShowingCompletions = completions; - mTypedWordValid = typedWordValid; + scrollTo(0, getScrollY()); - mTargetScrollX = 0; - mHaveMinimalSuggestion = haveMinimalSuggestion; - // Compute the total width - onDraw(null); - invalidate(); requestLayout(); } + public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) { + // Displaying auto corrected word as inverted is enabled only when highlighting candidate + // with color is disabled. + if (mConfigCandidateHighlightFontColorEnabled) + return; + final TextView tv = (TextView)mWords.get(1).findViewById(R.id.candidate_word); + final Spannable word = new SpannableString(autoCorrectedWord); + final int wordLength = word.length(); + word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + word.setSpan(mInvertedForegroundColorSpan, 0, wordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + tv.setText(word); + mShowingAutoCorrectionInverted = true; + } + + public boolean isConfigCandidateHighlightFontColorEnabled() { + return mConfigCandidateHighlightFontColorEnabled; + } + public boolean isShowingAddToDictionaryHint() { return mShowingAddToDictionary; } public void showAddToDictionaryHint(CharSequence word) { - ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>(); - suggestions.add(word); - suggestions.add(mAddToDictionaryHint); - setSuggestions(suggestions, false, false, false); + SuggestedWords.Builder builder = new SuggestedWords.Builder() + .addWord(word) + .addWord(getContext().getText(R.string.hint_add_to_dictionary)); + setSuggestions(builder.build()); mShowingAddToDictionary = true; + // Disable R.string.hint_add_to_dictionary button + TextView tv = (TextView)getChildAt(1).findViewById(R.id.candidate_word); + tv.setClickable(false); } public boolean dismissAddToDictionaryHint() { @@ -353,135 +280,78 @@ public class CandidateView extends View { return true; } - /* package */ List<CharSequence> getSuggestions() { + public SuggestedWords getSuggestions() { return mSuggestions; } public void clear() { - // Don't call mSuggestions.clear() because it's being used for logging - // in LatinIME.pickSuggestionManually(). - mSuggestions.clear(); - mTouchX = OUT_OF_BOUNDS_X_COORD; - mSelectedString = null; - mSelectedIndex = -1; mShowingAddToDictionary = false; - invalidate(); - Arrays.fill(mWordWidth, 0); - Arrays.fill(mWordX, 0); + mShowingAutoCorrectionInverted = false; + removeAllViews(); } - - @Override - public boolean onTouchEvent(MotionEvent me) { - if (mGestureDetector.onTouchEvent(me)) { - return true; + private void hidePreview() { + mPreviewPopup.dismiss(); + } + + private void showPreview(int index, CharSequence word) { + if (TextUtils.isEmpty(word)) + return; + + final TextView previewText = mPreviewText; + previewText.setTextColor(mColorNormal); + previewText.setText(word); + previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + View v = getChildAt(index); + final int[] offsetInWindow = new int[2]; + v.getLocationInWindow(offsetInWindow); + final int posX = offsetInWindow[0]; + final int posY = offsetInWindow[1] - previewText.getMeasuredHeight(); + final PopupWindow previewPopup = mPreviewPopup; + if (previewPopup.isShowing()) { + previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight()); + } else { + previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY); } + previewText.setVisibility(VISIBLE); + mHandler.postHidePreview(); + } - int action = me.getAction(); - int x = (int) me.getX(); - int y = (int) me.getY(); - mTouchX = x; - - switch (action) { - case MotionEvent.ACTION_DOWN: - invalidate(); - break; - case MotionEvent.ACTION_MOVE: - 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); - } - mService.pickSuggestionManually(mSelectedIndex, mSelectedString); - mSelectedString = null; - mSelectedIndex = -1; - } - } - break; - case MotionEvent.ACTION_UP: - if (!mScrolled) { - if (mSelectedString != null) { - if (mShowingAddToDictionary) { - longPressFirstWord(); - clear(); - } else { - if (!mShowingCompletions) { - TextEntryState.acceptedSuggestion(mSuggestions.get(0), - mSelectedString); - } - mService.pickSuggestionManually(mSelectedIndex, mSelectedString); - } - } - } - mSelectedString = null; - mSelectedIndex = -1; - requestLayout(); - hidePreview(); - invalidate(); - break; + private void addToDictionary(CharSequence word) { + if (mService.addWordToDictionary(word.toString())) { + showPreview(0, getContext().getString(R.string.added_word, word)); } - return true; } - private void hidePreview() { - mTouchX = OUT_OF_BOUNDS_X_COORD; - mCurrentWordIndex = OUT_OF_BOUNDS_WORD_INDEX; - mPreviewPopup.dismiss(); + @Override + public boolean onLongClick(View view) { + int index = (Integer) view.getTag(); + CharSequence word = mSuggestions.getWord(index); + if (word.length() < 2) + return false; + addToDictionary(word); + return true; } - - private void showPreview(int wordIndex, String altText) { - int oldWordIndex = mCurrentWordIndex; - mCurrentWordIndex = wordIndex; - // If index changed or changing text - if (oldWordIndex != mCurrentWordIndex || altText != null) { - if (wordIndex == OUT_OF_BOUNDS_WORD_INDEX) { - hidePreview(); - } else { - CharSequence word = altText != null? altText : mSuggestions.get(wordIndex); - mPreviewText.setText(word); - mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2); - final int popupWidth = wordWidth - + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight(); - final int popupHeight = mPreviewText.getMeasuredHeight(); - //mPreviewText.setVisibility(INVISIBLE); - mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX() - + (mWordWidth[wordIndex] - wordWidth) / 2; - mPopupPreviewY = - popupHeight; - int [] offsetInWindow = new int[2]; - getLocationInWindow(offsetInWindow); - if (mPreviewPopup.isShowing()) { - mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1], - popupWidth, popupHeight); - } else { - mPreviewPopup.setWidth(popupWidth); - mPreviewPopup.setHeight(popupHeight); - mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX, - mPopupPreviewY + offsetInWindow[1]); - } - mPreviewText.setVisibility(VISIBLE); + + @Override + public void onClick(View view) { + int index = (Integer) view.getTag(); + CharSequence word = mSuggestions.getWord(index); + if (mShowingAddToDictionary && index == 0) { + addToDictionary(word); + } else { + if (!mSuggestions.mIsApplicationSpecifiedCompletions) { + TextEntryState.acceptedSuggestion(mSuggestions.getWord(0), word); } + mService.pickSuggestionManually(index, word); } } - private void longPressFirstWord() { - CharSequence word = mSuggestions.get(0); - if (word.length() < 2) return; - if (mService.addWordToDictionary(word.toString())) { - showPreview(0, getContext().getResources().getString(R.string.added_word, word)); - } - } - @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); + mHandler.cancelAllMessages(); hidePreview(); } } diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java index 95a3b5c7d..048f72dc5 100644 --- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -21,6 +21,7 @@ import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.os.SystemClock; +import android.provider.BaseColumns; import android.provider.ContactsContract.Contacts; import android.text.TextUtils; import android.util.Log; @@ -28,7 +29,7 @@ import android.util.Log; public class ContactsDictionary extends ExpandableDictionary { private static final String[] PROJECTION = { - Contacts._ID, + BaseColumns._ID, Contacts.DISPLAY_NAME, }; diff --git a/java/src/com/android/inputmethod/latin/LatinIMEDebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java index cba1a0af9..03211f36b 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMEDebugSettings.java +++ b/java/src/com/android/inputmethod/latin/DebugSettings.java @@ -24,10 +24,10 @@ import android.preference.CheckBoxPreference; import android.preference.PreferenceActivity; import android.util.Log; -public class LatinIMEDebugSettings extends PreferenceActivity +public class DebugSettings extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "LatinIMEDebugSettings"; + private static final String TAG = "DebugSettings"; private static final String DEBUG_MODE_KEY = "debug_mode"; private CheckBoxPreference mDebugMode; @@ -43,6 +43,7 @@ public class LatinIMEDebugSettings extends PreferenceActivity updateDebugMode(); } + @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key.equals(DEBUG_MODE_KEY)) { if (mDebugMode != null) { diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index d04bf57a7..74933595c 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -20,7 +20,7 @@ package com.android.inputmethod.latin; * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key * strokes. */ -abstract public class Dictionary { +public abstract class Dictionary { /** * Whether or not to replicate the typed word in the suggested list, even if it's valid. */ @@ -42,11 +42,11 @@ abstract public class Dictionary { public interface WordCallback { /** * Adds a word to a list of suggestions. The word is expected to be ordered based on - * the provided frequency. + * the provided frequency. * @param word the character array containing the word * @param wordOffset starting offset of the word in the character array * @param wordLength length of valid characters in the character array - * @param frequency the frequency of occurence. This is normalized between 1 and 255, but + * @param frequency the frequency of occurrence. 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 @@ -74,6 +74,7 @@ abstract public class Dictionary { * Searches for pairs in the bigram dictionary that matches the previous word and all the * possible words following are added through the callback object. * @param composer the key sequence to match + * @param previousWord the word before * @param callback the callback object to send possible word following previous word * @param nextLettersFrequencies array of frequencies of next letters that could follow the * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have @@ -116,5 +117,6 @@ abstract public class Dictionary { * Override to clean up any resources. */ public void close() { + // empty base implementation } } diff --git a/java/src/com/android/inputmethod/latin/EditingUtil.java b/java/src/com/android/inputmethod/latin/EditingUtils.java index 781d7fd4a..0ca06ddfc 100644 --- a/java/src/com/android/inputmethod/latin/EditingUtil.java +++ b/java/src/com/android/inputmethod/latin/EditingUtils.java @@ -28,7 +28,7 @@ import java.util.regex.Pattern; /** * Utility methods to deal with editing text through an InputConnection. */ -public class EditingUtil { +public class EditingUtils { /** * Number of characters we want to look back in order to identify the previous word */ @@ -39,7 +39,9 @@ public class EditingUtil { private static Method sMethodGetSelectedText; private static Method sMethodSetComposingRegion; - private EditingUtil() {}; + private EditingUtils() { + // Unintentional empty constructor for singleton. + } /** * Append newText to the text field represented by connection. @@ -54,14 +56,15 @@ public class EditingUtil { connection.finishComposingText(); // Add a space if the field already has text. + String text = newText; CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0); if (charBeforeCursor != null && !charBeforeCursor.equals(" ") && (charBeforeCursor.length() > 0)) { - newText = " " + newText; + text = " " + text; } - connection.setComposingText(newText, 1); + connection.setComposingText(text, 1); } private static int getCursorPosition(InputConnection connection) { @@ -76,33 +79,29 @@ public class EditingUtil { /** * @param connection connection to the current text field. * @param sep characters which may separate words - * @param range the range object to store the result into * @return the word that surrounds the cursor, including up to one trailing * separator. For example, if the field contains "he|llo world", where | * represents the cursor, then "hello " will be returned. */ - public static String getWordAtCursor( - InputConnection connection, String separators, Range range) { - Range r = getWordRangeAtCursor(connection, separators, range); - return (r == null) ? null : r.word; + public static String getWordAtCursor(InputConnection connection, String separators) { + Range r = getWordRangeAtCursor(connection, separators); + return (r == null) ? null : r.mWord; } /** * Removes the word surrounding the cursor. Parameters are identical to * getWordAtCursor. */ - public static void deleteWordAtCursor( - InputConnection connection, String separators) { - - Range range = getWordRangeAtCursor(connection, separators, null); + public static void deleteWordAtCursor(InputConnection connection, String separators) { + Range range = getWordRangeAtCursor(connection, separators); if (range == null) return; connection.finishComposingText(); // Move cursor to beginning of word, to avoid crash when cursor is outside // of valid range after deleting text. - int newCursor = getCursorPosition(connection) - range.charsBefore; + int newCursor = getCursorPosition(connection) - range.mCharsBefore; connection.setSelection(newCursor, newCursor); - connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter); + connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter); } /** @@ -110,31 +109,28 @@ public class EditingUtil { */ public static class Range { /** Characters before selection start */ - public int charsBefore; + public final int mCharsBefore; /** * Characters after selection start, including one trailing word * separator. */ - public int charsAfter; + public final int mCharsAfter; /** The actual characters that make up a word */ - public String word; - - public Range() {} + public final String mWord; public Range(int charsBefore, int charsAfter, String word) { if (charsBefore < 0 || charsAfter < 0) { throw new IndexOutOfBoundsException(); } - this.charsBefore = charsBefore; - this.charsAfter = charsAfter; - this.word = word; + this.mCharsBefore = charsBefore; + this.mCharsAfter = charsAfter; + this.mWord = word; } } - private static Range getWordRangeAtCursor( - InputConnection connection, String sep, Range range) { + private static Range getWordRangeAtCursor(InputConnection connection, String sep) { if (connection == null || sep == null) { return null; } @@ -150,18 +146,15 @@ public class EditingUtil { // Find last word separator after the cursor int end = -1; - while (++end < after.length() && !isWhitespace(after.charAt(end), sep)); + while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) { + // Nothing to do here. + } int cursor = getCursorPosition(connection); if (start >= 0 && cursor + end <= after.length() + before.length()) { String word = before.toString().substring(start, before.length()) + after.toString().substring(0, end); - - Range returnRange = range != null? range : new Range(); - returnRange.charsBefore = before.length() - start; - returnRange.charsAfter = end; - returnRange.word = word; - return returnRange; + return new Range(before.length() - start, end, word); } return null; @@ -193,9 +186,15 @@ public class EditingUtil { } public static class SelectedWord { - public int start; - public int end; - public CharSequence word; + public final int mStart; + public final int mEnd; + public final CharSequence mWord; + + public SelectedWord(int start, int end, CharSequence word) { + mStart = start; + mEnd = end; + mWord = word; + } } /** @@ -223,14 +222,10 @@ public class EditingUtil { int selStart, int selEnd, String wordSeparators) { if (selStart == selEnd) { // There is just a cursor, so get the word at the cursor - EditingUtil.Range range = new EditingUtil.Range(); - CharSequence touching = getWordAtCursor(ic, wordSeparators, range); - if (!TextUtils.isEmpty(touching)) { - SelectedWord selWord = new SelectedWord(); - selWord.word = touching; - selWord.start = selStart - range.charsBefore; - selWord.end = selEnd + range.charsAfter; - return selWord; + EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators); + if (range != null && !TextUtils.isEmpty(range.mWord)) { + return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter, + range.mWord); } } else { // Is the previous character empty or a word separator? If not, return null. @@ -256,11 +251,7 @@ public class EditingUtil { } } // Prepare the selected word - SelectedWord selWord = new SelectedWord(); - selWord.start = selStart; - selWord.end = selEnd; - selWord.word = touching; - return selWord; + return new SelectedWord(selStart, selEnd, touching); } return null; } @@ -324,7 +315,7 @@ public class EditingUtil { } if (sMethodSetComposingRegion != null) { try { - sMethodSetComposingRegion.invoke(ic, word.start, word.end); + sMethodSetComposingRegion.invoke(ic, word.mStart, word.mEnd); } catch (InvocationTargetException exc) { // Ignore } catch (IllegalArgumentException e) { diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index e954c0818..0fc86c335 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -16,11 +16,11 @@ package com.android.inputmethod.latin; -import java.util.LinkedList; - import android.content.Context; import android.os.AsyncTask; +import java.util.LinkedList; + /** * Base class for an in-memory dictionary that can grow dynamically and can * be searched for suggestions and valid words. @@ -49,53 +49,65 @@ public class ExpandableDictionary extends Dictionary { // Use this lock before touching mUpdatingDictionary & mRequiresDownload private Object mUpdatingLock = new Object(); - static class Node { - char code; - int frequency; - boolean terminal; - Node parent; - NodeArray children; - LinkedList<NextWord> ngrams; // Supports ngram + private static class Node { + char mCode; + int mFrequency; + boolean mTerminal; + Node mParent; + NodeArray mChildren; + LinkedList<NextWord> mNGrams; // Supports ngram } - static class NodeArray { - Node[] data; - int length = 0; + private static class NodeArray { + Node[] mData; + int mLength = 0; private static final int INCREMENT = 2; NodeArray() { - data = new Node[INCREMENT]; + mData = new Node[INCREMENT]; } void add(Node n) { - if (length + 1 > data.length) { - Node[] tempData = new Node[length + INCREMENT]; - if (length > 0) { - System.arraycopy(data, 0, tempData, 0, length); + if (mLength + 1 > mData.length) { + Node[] tempData = new Node[mLength + INCREMENT]; + if (mLength > 0) { + System.arraycopy(mData, 0, tempData, 0, mLength); } - data = tempData; + mData = tempData; } - data[length++] = n; + mData[mLength++] = n; } } - static class NextWord { - Node word; - NextWord nextWord; - int frequency; + private static class NextWord { + public final Node mWord; + private int mFrequency; - NextWord(Node word, int frequency) { - this.word = word; - this.frequency = frequency; + public NextWord(Node word, int frequency) { + mWord = word; + mFrequency = frequency; + } + + public int getFrequency() { + return mFrequency; + } + + public int setFrequency(int freq) { + mFrequency = freq; + return mFrequency; } - } + public int addFrequency(int add) { + mFrequency += add; + return mFrequency; + } + } private NodeArray mRoots; private int[][] mCodes; - ExpandableDictionary(Context context, int dicTypeId) { + public ExpandableDictionary(Context context, int dicTypeId) { mContext = context; clearDictionary(); mCodes = new int[MAX_WORD_LENGTH][]; @@ -128,13 +140,14 @@ public class ExpandableDictionary extends Dictionary { /** Override to load your dictionary here, on a background thread. */ public void loadDictionaryAsync() { + // empty base implementation } - Context getContext() { + public Context getContext() { return mContext; } - - int getMaxWordLength() { + + public int getMaxWordLength() { return MAX_WORD_LENGTH; } @@ -145,35 +158,36 @@ public class ExpandableDictionary extends Dictionary { private void addWordRec(NodeArray children, final String word, final int depth, final int frequency, Node parentNode) { final int wordLength = word.length(); + if (wordLength <= depth) return; final char c = word.charAt(depth); // Does children have the current character? - final int childrenLength = children.length; + final int childrenLength = children.mLength; Node childNode = null; boolean found = false; for (int i = 0; i < childrenLength; i++) { - childNode = children.data[i]; - if (childNode.code == c) { + childNode = children.mData[i]; + if (childNode.mCode == c) { found = true; break; } } if (!found) { childNode = new Node(); - childNode.code = c; - childNode.parent = parentNode; + childNode.mCode = c; + childNode.mParent = parentNode; children.add(childNode); } if (wordLength == depth + 1) { // Terminate this word - childNode.terminal = true; - childNode.frequency = Math.max(frequency, childNode.frequency); - if (childNode.frequency > 255) childNode.frequency = 255; + childNode.mTerminal = true; + childNode.mFrequency = Math.max(frequency, childNode.mFrequency); + if (childNode.mFrequency > 255) childNode.mFrequency = 255; return; } - if (childNode.children == null) { - childNode.children = new NodeArray(); + if (childNode.mChildren == null) { + childNode.mChildren = new NodeArray(); } - addWordRec(childNode.children, word, depth + 1, frequency, childNode); + addWordRec(childNode.mChildren, word, depth + 1, frequency, childNode); } @Override @@ -216,7 +230,7 @@ public class ExpandableDictionary extends Dictionary { */ public int getWordFrequency(CharSequence word) { Node node = searchNode(mRoots, word, 0, word.length()); - return (node == null) ? -1 : node.frequency; + return (node == null) ? -1 : node.mFrequency; } /** @@ -241,7 +255,7 @@ public class ExpandableDictionary extends Dictionary { protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word, final int depth, boolean completion, int snr, int inputIndex, int skipPos, WordCallback callback) { - final int count = roots.length; + final int count = roots.mLength; final int codeSize = mInputLength; // Optimization: Prune out words that are too long compared to how much was typed. if (depth > mMaxDepth) { @@ -255,12 +269,12 @@ public class ExpandableDictionary extends Dictionary { } for (int i = 0; i < count; i++) { - final Node node = roots.data[i]; - final char c = node.code; + final Node node = roots.mData[i]; + final char c = node.mCode; final char lowerC = toLowerCase(c); - final boolean terminal = node.terminal; - final NodeArray children = node.children; - final int freq = node.frequency; + final boolean terminal = node.mTerminal; + final NodeArray children = node.mChildren; + final int freq = node.mFrequency; if (completion) { word[depth] = c; if (terminal) { @@ -340,24 +354,22 @@ public class ExpandableDictionary extends Dictionary { private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) { Node firstWord = searchWord(mRoots, word1, 0, null); Node secondWord = searchWord(mRoots, word2, 0, null); - LinkedList<NextWord> bigram = firstWord.ngrams; + LinkedList<NextWord> bigram = firstWord.mNGrams; if (bigram == null || bigram.size() == 0) { - firstWord.ngrams = new LinkedList<NextWord>(); - bigram = firstWord.ngrams; + firstWord.mNGrams = new LinkedList<NextWord>(); + bigram = firstWord.mNGrams; } else { for (NextWord nw : bigram) { - if (nw.word == secondWord) { + if (nw.mWord == secondWord) { if (addFrequency) { - nw.frequency += frequency; + return nw.addFrequency(frequency); } else { - nw.frequency = frequency; + return nw.setFrequency(frequency); } - return nw.frequency; } } } - NextWord nw = new NextWord(secondWord, frequency); - firstWord.ngrams.add(nw); + firstWord.mNGrams.add(new NextWord(secondWord, frequency)); return frequency; } @@ -369,31 +381,31 @@ public class ExpandableDictionary extends Dictionary { final int wordLength = word.length(); final char c = word.charAt(depth); // Does children have the current character? - final int childrenLength = children.length; + final int childrenLength = children.mLength; Node childNode = null; boolean found = false; for (int i = 0; i < childrenLength; i++) { - childNode = children.data[i]; - if (childNode.code == c) { + childNode = children.mData[i]; + if (childNode.mCode == c) { found = true; break; } } if (!found) { childNode = new Node(); - childNode.code = c; - childNode.parent = parentNode; + childNode.mCode = c; + childNode.mParent = parentNode; children.add(childNode); } if (wordLength == depth + 1) { // Terminate this word - childNode.terminal = true; + childNode.mTerminal = true; return childNode; } - if (childNode.children == null) { - childNode.children = new NodeArray(); + if (childNode.mChildren == null) { + childNode.mChildren = new NodeArray(); } - return searchWord(childNode.children, word, depth + 1, childNode); + return searchWord(childNode.mChildren, word, depth + 1, childNode); } // @VisibleForTesting @@ -408,8 +420,8 @@ public class ExpandableDictionary extends Dictionary { private void runReverseLookUp(final CharSequence previousWord, final WordCallback callback) { Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length()); - if (prevWord != null && prevWord.ngrams != null) { - reverseLookUp(prevWord.ngrams, callback); + if (prevWord != null && prevWord.mNGrams != null) { + reverseLookUp(prevWord.mNGrams, callback); } } @@ -430,6 +442,7 @@ public class ExpandableDictionary extends Dictionary { try { Thread.sleep(100); } catch (InterruptedException e) { + // } } } @@ -444,14 +457,14 @@ public class ExpandableDictionary extends Dictionary { Node node; int freq; for (NextWord nextWord : terminalNodes) { - node = nextWord.word; - freq = nextWord.frequency; + node = nextWord.mWord; + freq = nextWord.getFrequency(); // TODO Not the best way to limit suggestion threshold if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) { sb.setLength(0); do { - sb.insert(0, node.code); - node = node.parent; + sb.insert(0, node.mCode); + node = node.mParent; } while(node != null); // TODO better way to feed char array? @@ -468,18 +481,18 @@ public class ExpandableDictionary extends Dictionary { private Node searchNode(final NodeArray children, final CharSequence word, final int offset, final int length) { // TODO Consider combining with addWordRec - final int count = children.length; + final int count = children.mLength; char currentChar = word.charAt(offset); for (int j = 0; j < count; j++) { - final Node node = children.data[j]; - if (node.code == currentChar) { + final Node node = children.mData[j]; + if (node.mCode == currentChar) { if (offset == length - 1) { - if (node.terminal) { + if (node.mTerminal) { return node; } } else { - if (node.children != null) { - Node returnNode = searchNode(node.children, word, offset + 1, length); + if (node.mChildren != null) { + Node returnNode = searchNode(node.mChildren, word, offset + 1, length); if (returnNode != null) return returnNode; } } @@ -504,15 +517,16 @@ public class ExpandableDictionary extends Dictionary { } static char toLowerCase(char c) { + char baseChar = c; if (c < BASE_CHARS.length) { - c = BASE_CHARS[c]; + baseChar = BASE_CHARS[c]; } - if (c >= 'A' && c <= 'Z') { - c = (char) (c | 32); - } else if (c > 127) { - c = Character.toLowerCase(c); + if (baseChar >= 'A' && baseChar <= 'Z') { + return (char)(baseChar | 32); + } else if (baseChar > 127) { + return Character.toLowerCase(baseChar); } - return c; + return baseChar; } /** diff --git a/java/src/com/android/inputmethod/latin/Hints.java b/java/src/com/android/inputmethod/latin/Hints.java deleted file mode 100644 index c467365e7..000000000 --- a/java/src/com/android/inputmethod/latin/Hints.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import com.android.inputmethod.voice.SettingsUtil; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.inputmethod.InputConnection; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.Map; - -/** - * Logic to determine when to display hints on usage to the user. - */ -public class Hints { - public interface Display { - public void showHint(int viewResource); - } - - private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN = - "voice_hint_num_unique_days_shown"; - private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN = - "voice_hint_last_time_shown"; - private static final String PREF_VOICE_INPUT_LAST_TIME_USED = - "voice_input_last_time_used"; - private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT = - "voice_punctuation_hint_view_count"; - private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7; - private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7; - - private Context mContext; - private Display mDisplay; - private boolean mVoiceResultContainedPunctuation; - private int mSwipeHintMaxDaysToShow; - private int mPunctuationHintMaxDisplays; - - // Only show punctuation hint if voice result did not contain punctuation. - static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION - = new HashMap<CharSequence, String>(); - static { - SPEAKABLE_PUNCTUATION.put(",", "comma"); - SPEAKABLE_PUNCTUATION.put(".", "period"); - SPEAKABLE_PUNCTUATION.put("?", "question mark"); - } - - public Hints(Context context, Display display) { - mContext = context; - mDisplay = display; - - ContentResolver cr = mContext.getContentResolver(); - mSwipeHintMaxDaysToShow = SettingsUtil.getSettingsInt( - cr, - SettingsUtil.LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS, - DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW); - mPunctuationHintMaxDisplays = SettingsUtil.getSettingsInt( - cr, - SettingsUtil.LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS, - DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS); - } - - public boolean showSwipeHintIfNecessary(boolean fieldRecommended) { - if (fieldRecommended && shouldShowSwipeHint()) { - showHint(R.layout.voice_swipe_hint); - return true; - } - - return false; - } - - public boolean showPunctuationHintIfNecessary(InputConnection ic) { - if (!mVoiceResultContainedPunctuation - && ic != null - && getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT) - < mPunctuationHintMaxDisplays) { - CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0); - if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) { - showHint(R.layout.voice_punctuation_hint); - return true; - } - } - - return false; - } - - public void registerVoiceResult(String text) { - // Update the current time as the last time voice input was used. - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(mContext).edit(); - editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis()); - SharedPreferencesCompat.apply(editor); - - mVoiceResultContainedPunctuation = false; - for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) { - if (text.indexOf(s.toString()) >= 0) { - mVoiceResultContainedPunctuation = true; - break; - } - } - } - - private boolean shouldShowSwipeHint() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); - - int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); - - // If we've already shown the hint for enough days, we'll return false. - if (numUniqueDaysShown < mSwipeHintMaxDaysToShow) { - - long lastTimeVoiceWasUsed = sp.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0); - - // If the user has used voice today, we'll return false. (We don't show the hint on - // any day that the user has already used voice.) - if (!isFromToday(lastTimeVoiceWasUsed)) { - return true; - } - } - - return false; - } - - /** - * Determines whether the provided time is from some time today (i.e., this day, month, - * and year). - */ - private boolean isFromToday(long timeInMillis) { - if (timeInMillis == 0) return false; - - Calendar today = Calendar.getInstance(); - today.setTimeInMillis(System.currentTimeMillis()); - - Calendar timestamp = Calendar.getInstance(); - timestamp.setTimeInMillis(timeInMillis); - - return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) && - today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) && - today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH)); - } - - private void showHint(int hintViewResource) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); - - int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0); - long lastTimeHintWasShown = sp.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0); - - // If this is the first time the hint is being shown today, increase the saved values - // to represent that. We don't need to increase the last time the hint was shown unless - // it is a different day from the current value. - if (!isFromToday(lastTimeHintWasShown)) { - SharedPreferences.Editor editor = sp.edit(); - editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1); - editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis()); - SharedPreferencesCompat.apply(editor); - } - - if (mDisplay != null) { - mDisplay.showHint(hintViewResource); - } - } - - private int getAndIncrementPref(String pref) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); - int value = sp.getInt(pref, 0); - SharedPreferences.Editor editor = sp.edit(); - editor.putInt(pref, value + 1); - SharedPreferencesCompat.apply(editor); - return value; - } -} diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index e811a2cdd..a9f2c2c22 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -16,11 +16,6 @@ package com.android.inputmethod.latin; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.Configuration; @@ -32,32 +27,43 @@ import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.text.TextUtils; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + public class InputLanguageSelection extends PreferenceActivity { + private SharedPreferences mPrefs; private String mSelectedLanguages; private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>(); private static final String[] BLACKLIST_LANGUAGES = { - "ko", "ja", "zh", "el" + "ko", "ja", "zh", "el", "zz" }; private static class Loc implements Comparable<Object> { - static Collator sCollator = Collator.getInstance(); + private static Collator sCollator = Collator.getInstance(); - String label; - Locale locale; + private String mLabel; + public final Locale mLocale; public Loc(String label, Locale locale) { - this.label = label; - this.locale = locale; + this.mLabel = label; + this.mLocale = locale; + } + + public void setLabel(String label) { + this.mLabel = label; } @Override public String toString() { - return this.label; + return this.mLabel; } + @Override public int compareTo(Object o) { - return sCollator.compare(this.label, ((Loc) o).label); + return sCollator.compare(this.mLabel, ((Loc) o).mLabel); } } @@ -66,15 +72,15 @@ public class InputLanguageSelection extends PreferenceActivity { super.onCreate(icicle); addPreferencesFromResource(R.xml.language_prefs); // Get the settings preferences - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mSelectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mSelectedLanguages = mPrefs.getString(Settings.PREF_SELECTED_LANGUAGES, ""); String[] languageList = mSelectedLanguages.split(","); mAvailableLanguages = getUniqueLocales(); PreferenceGroup parent = getPreferenceScreen(); for (int i = 0; i < mAvailableLanguages.size(); i++) { CheckBoxPreference pref = new CheckBoxPreference(this); - Locale locale = mAvailableLanguages.get(i).locale; - pref.setTitle(LanguageSwitcher.toTitleCase(locale.getDisplayName(locale))); + Locale locale = mAvailableLanguages.get(i).mLocale; + pref.setTitle(SubtypeSwitcher.getFullDisplayName(locale, true)); boolean checked = isLocaleIn(locale, languageList); pref.setChecked(checked); if (hasDictionary(locale)) { @@ -93,15 +99,15 @@ public class InputLanguageSelection extends PreferenceActivity { } private boolean hasDictionary(Locale locale) { - Resources res = getResources(); - Configuration conf = res.getConfiguration(); - Locale saveLocale = conf.locale; + final Resources res = getResources(); + final Configuration conf = res.getConfiguration(); + final Locale saveLocale = conf.locale; boolean haveDictionary = false; conf.locale = locale; res.updateConfiguration(conf, res.getDisplayMetrics()); - int[] dictionaries = LatinIME.getDictionary(res); - BinaryDictionary bd = new BinaryDictionary(this, dictionaries, Suggest.DIC_MAIN); + int mainDicResId = LatinIME.getMainDictionaryResourceId(res); + BinaryDictionary bd = BinaryDictionary.initDictionary(this, mainDicResId, 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. @@ -135,18 +141,17 @@ public class InputLanguageSelection extends PreferenceActivity { for (int i = 0; i < count; i++) { CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i); if (pref.isChecked()) { - Locale locale = mAvailableLanguages.get(i).locale; + Locale locale = mAvailableLanguages.get(i).mLocale; checkedLanguages += get5Code(locale) + ","; } } if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - Editor editor = sp.edit(); - editor.putString(LatinIME.PREF_SELECTED_LANGUAGES, checkedLanguages); + Editor editor = mPrefs.edit(); + editor.putString(Settings.PREF_SELECTED_LANGUAGES, checkedLanguages); SharedPreferencesCompat.apply(editor); } - ArrayList<Loc> getUniqueLocales() { + public ArrayList<Loc> getUniqueLocales() { String[] locales = getAssets().getLocales(); Arrays.sort(locales); ArrayList<Loc> uniqueLocales = new ArrayList<Loc>(); @@ -167,23 +172,24 @@ public class InputLanguageSelection extends PreferenceActivity { if (finalSize == 0) { preprocess[finalSize++] = - new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(l)), l); + new Loc(SubtypeSwitcher.getFullDisplayName(l, true), l); } else { // check previous entry: // same lang and a country -> upgrade to full name and // insert ours with full name // diff lang -> insert ours with lang-only name - if (preprocess[finalSize-1].locale.getLanguage().equals( + if (preprocess[finalSize-1].mLocale.getLanguage().equals( language)) { - preprocess[finalSize-1].label = LanguageSwitcher.toTitleCase( - preprocess[finalSize-1].locale.getDisplayName()); + preprocess[finalSize-1].setLabel(SubtypeSwitcher.getFullDisplayName( + preprocess[finalSize-1].mLocale, false)); preprocess[finalSize++] = - new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName()), l); + new Loc(SubtypeSwitcher.getFullDisplayName(l, false), l); } else { String displayName; if (s.equals("zz_ZZ")) { + // ignore this locale } else { - displayName = LanguageSwitcher.toTitleCase(l.getDisplayName(l)); + displayName = SubtypeSwitcher.getFullDisplayName(l, true); preprocess[finalSize++] = new Loc(displayName, l); } } diff --git a/java/src/com/android/inputmethod/latin/KeyDetector.java b/java/src/com/android/inputmethod/latin/KeyDetector.java deleted file mode 100644 index 76fe1200e..000000000 --- a/java/src/com/android/inputmethod/latin/KeyDetector.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; - -import java.util.Arrays; -import java.util.List; - -abstract class KeyDetector { - protected Keyboard mKeyboard; - - private Key[] mKeys; - - protected int mCorrectionX; - - protected int mCorrectionY; - - protected boolean mProximityCorrectOn; - - protected int mProximityThresholdSquare; - - public Key[] setKeyboard(Keyboard keyboard, float correctionX, float correctionY) { - if (keyboard == null) - throw new NullPointerException(); - mCorrectionX = (int)correctionX; - mCorrectionY = (int)correctionY; - mKeyboard = keyboard; - List<Key> keys = mKeyboard.getKeys(); - Key[] array = keys.toArray(new Key[keys.size()]); - mKeys = array; - return array; - } - - protected int getTouchX(int x) { - return x + mCorrectionX; - } - - protected int getTouchY(int y) { - return y + mCorrectionY; - } - - protected Key[] getKeys() { - if (mKeys == null) - throw new IllegalStateException("keyboard isn't set"); - // mKeyboard is guaranteed not to be null at setKeybaord() method if mKeys is not null - return mKeys; - } - - public void setProximityCorrectionEnabled(boolean enabled) { - mProximityCorrectOn = enabled; - } - - public boolean isProximityCorrectionEnabled() { - return mProximityCorrectOn; - } - - public void setProximityThreshold(int threshold) { - mProximityThresholdSquare = threshold * threshold; - } - - /** - * Allocates array that can hold all key indices returned by {@link #getKeyIndexAndNearbyCodes} - * method. The maximum size of the array should be computed by {@link #getMaxNearbyKeys}. - * - * @return Allocates and returns an array that can hold all key indices returned by - * {@link #getKeyIndexAndNearbyCodes} method. All elements in the returned array are - * initialized by {@link com.android.inputmethod.latin.LatinKeyboardView.NOT_A_KEY} - * value. - */ - public int[] newCodeArray() { - int[] codes = new int[getMaxNearbyKeys()]; - Arrays.fill(codes, LatinKeyboardBaseView.NOT_A_KEY); - return codes; - } - - /** - * Computes maximum size of the array that can contain all nearby key indices returned by - * {@link #getKeyIndexAndNearbyCodes}. - * - * @return Returns maximum size of the array that can contain all nearby key indices returned - * by {@link #getKeyIndexAndNearbyCodes}. - */ - abstract protected int getMaxNearbyKeys(); - - /** - * Finds all possible nearby key indices around a touch event point and returns the nearest key - * index. The algorithm to determine the nearby keys depends on the threshold set by - * {@link #setProximityThreshold(int)} and the mode set by - * {@link #setProximityCorrectionEnabled(boolean)}. - * - * @param x The x-coordinate of a touch point - * @param y The y-coordinate of a touch point - * @param allKeys All nearby key indices are returned in this array - * @return The nearest key index - */ - abstract public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys); -} diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java deleted file mode 100644 index 0db204ed8..000000000 --- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ /dev/null @@ -1,606 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.preference.PreferenceManager; -import android.view.InflateException; - -import java.lang.ref.SoftReference; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Locale; - -public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener { - - public static final int MODE_NONE = 0; - public static final int MODE_TEXT = 1; - public static final int MODE_SYMBOLS = 2; - public static final int MODE_PHONE = 3; - public static final int MODE_URL = 4; - public static final int MODE_EMAIL = 5; - public static final int MODE_IM = 6; - public static final int MODE_WEB = 7; - - // Main keyboard layouts without the settings key - public static final int KEYBOARDMODE_NORMAL = R.id.mode_normal; - public static final int KEYBOARDMODE_URL = R.id.mode_url; - public static final int KEYBOARDMODE_EMAIL = R.id.mode_email; - public static final int KEYBOARDMODE_IM = R.id.mode_im; - public static final int KEYBOARDMODE_WEB = R.id.mode_webentry; - // Main keyboard layouts with the settings key - public static final int KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY = - R.id.mode_normal_with_settings_key; - public static final int KEYBOARDMODE_URL_WITH_SETTINGS_KEY = - R.id.mode_url_with_settings_key; - public static final int KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY = - R.id.mode_email_with_settings_key; - public static final int KEYBOARDMODE_IM_WITH_SETTINGS_KEY = - R.id.mode_im_with_settings_key; - public static final int KEYBOARDMODE_WEB_WITH_SETTINGS_KEY = - R.id.mode_webentry_with_settings_key; - - // Symbols keyboard layout without the settings key - public static final int KEYBOARDMODE_SYMBOLS = R.id.mode_symbols; - // Symbols keyboard layout with the settings key - public static final int KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY = - R.id.mode_symbols_with_settings_key; - - public static final String DEFAULT_LAYOUT_ID = "4"; - public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902"; - private static final int[] THEMES = new int [] { - R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal, - R.layout.input_stone_bold, R.layout.input_gingerbread}; - - // Ids for each characters' color in the keyboard - private static final int CHAR_THEME_COLOR_WHITE = 0; - private static final int CHAR_THEME_COLOR_BLACK = 1; - - // Tables which contains resource ids for each character theme color - private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black}; - private static final int[] KBD_PHONE_SYMBOLS = new int[] { - R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black}; - private static final int[] KBD_SYMBOLS = new int[] { - R.xml.kbd_symbols, R.xml.kbd_symbols_black}; - private static final int[] KBD_SYMBOLS_SHIFT = new int[] { - R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black}; - private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black}; - - private LatinKeyboardView mInputView; - private static final int[] ALPHABET_MODES = { - KEYBOARDMODE_NORMAL, - KEYBOARDMODE_URL, - KEYBOARDMODE_EMAIL, - KEYBOARDMODE_IM, - KEYBOARDMODE_WEB, - KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY, - KEYBOARDMODE_URL_WITH_SETTINGS_KEY, - KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY, - KEYBOARDMODE_IM_WITH_SETTINGS_KEY, - KEYBOARDMODE_WEB_WITH_SETTINGS_KEY }; - - private LatinIME mInputMethodService; - - private KeyboardId mSymbolsId; - private KeyboardId mSymbolsShiftedId; - - private KeyboardId mCurrentId; - private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboards = - new HashMap<KeyboardId, SoftReference<LatinKeyboard>>(); - - private int mMode = MODE_NONE; /** One of the MODE_XXX values */ - private int mImeOptions; - private boolean mIsSymbols; - /** mIsAutoCompletionActive indicates that auto completed word will be input instead of - * what user actually typed. */ - private boolean mIsAutoCompletionActive; - private boolean mHasVoice; - private boolean mVoiceOnPrimary; - private boolean mPreferSymbols; - - private static final int AUTO_MODE_SWITCH_STATE_ALPHA = 0; - private static final int AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN = 1; - private static final int AUTO_MODE_SWITCH_STATE_SYMBOL = 2; - // The following states are used only on the distinct multi-touch panel devices. - private static final int AUTO_MODE_SWITCH_STATE_MOMENTARY = 3; - private static final int AUTO_MODE_SWITCH_STATE_CHORDING = 4; - private int mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA; - - // Indicates whether or not we have the settings key - private boolean mHasSettingsKey; - private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto; - private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW = R.string.settings_key_mode_always_show; - // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to - // in the source code now. - // Default is SETTINGS_KEY_MODE_AUTO. - private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO; - - private int mLastDisplayWidth; - private LanguageSwitcher mLanguageSwitcher; - private Locale mInputLocale; - - private int mLayoutId; - - private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); - - public static KeyboardSwitcher getInstance() { - return sInstance; - } - - private KeyboardSwitcher() { - // Intentional empty constructor for singleton. - } - - public static void init(LatinIME ims) { - sInstance.mInputMethodService = ims; - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims); - sInstance.mLayoutId = Integer.valueOf( - prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID)); - sInstance.updateSettingsKeyState(prefs); - prefs.registerOnSharedPreferenceChangeListener(sInstance); - - sInstance.mSymbolsId = sInstance.makeSymbolsId(false); - sInstance.mSymbolsShiftedId = sInstance.makeSymbolsShiftedId(false); - } - - /** - * Sets the input locale, when there are multiple locales for input. - * If no locale switching is required, then the locale should be set to null. - * @param locale the current input locale, or null for default locale with no locale - * button. - */ - public void setLanguageSwitcher(LanguageSwitcher languageSwitcher) { - mLanguageSwitcher = languageSwitcher; - mInputLocale = mLanguageSwitcher.getInputLocale(); - } - - private KeyboardId makeSymbolsId(boolean hasVoice) { - return new KeyboardId(KBD_SYMBOLS[getCharColorId()], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - } - - private KeyboardId makeSymbolsShiftedId(boolean hasVoice) { - return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - } - - public void makeKeyboards(boolean forceCreate) { - mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary); - mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary); - - if (forceCreate) mKeyboards.clear(); - // Configuration change is coming after the keyboard gets recreated. So don't rely on that. - // If keyboards have already been made, check if we have a screen width change and - // create the keyboard layouts again at the correct orientation - int displayWidth = mInputMethodService.getMaxWidth(); - if (displayWidth == mLastDisplayWidth) return; - mLastDisplayWidth = displayWidth; - if (!forceCreate) mKeyboards.clear(); - } - - /** - * Represents the parameters necessary to construct a new LatinKeyboard, - * which also serve as a unique identifier for each keyboard type. - */ - private static class KeyboardId { - // TODO: should have locale and portrait/landscape orientation? - public final int mXml; - public final int mKeyboardMode; /** A KEYBOARDMODE_XXX value */ - public final boolean mEnableShiftLock; - public final boolean mHasVoice; - - private final int mHashCode; - - public KeyboardId(int xml, int mode, boolean enableShiftLock, boolean hasVoice) { - this.mXml = xml; - this.mKeyboardMode = mode; - this.mEnableShiftLock = enableShiftLock; - this.mHasVoice = hasVoice; - - this.mHashCode = Arrays.hashCode(new Object[] { - xml, mode, enableShiftLock, hasVoice - }); - } - - public KeyboardId(int xml, boolean hasVoice) { - this(xml, 0, false, hasVoice); - } - - @Override - public boolean equals(Object other) { - return other instanceof KeyboardId && equals((KeyboardId) other); - } - - private boolean equals(KeyboardId other) { - return other.mXml == this.mXml - && other.mKeyboardMode == this.mKeyboardMode - && other.mEnableShiftLock == this.mEnableShiftLock - && other.mHasVoice == this.mHasVoice; - } - - @Override - public int hashCode() { - return mHashCode; - } - } - - public void setVoiceMode(boolean enableVoice, boolean voiceOnPrimary) { - if (enableVoice != mHasVoice || voiceOnPrimary != mVoiceOnPrimary) { - mKeyboards.clear(); - } - mHasVoice = enableVoice; - mVoiceOnPrimary = voiceOnPrimary; - setKeyboardMode(mMode, mImeOptions, mHasVoice, mIsSymbols); - } - - private boolean hasVoiceButton(boolean isSymbols) { - return mHasVoice && (isSymbols != mVoiceOnPrimary); - } - - public void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA; - mPreferSymbols = mode == MODE_SYMBOLS; - if (mode == MODE_SYMBOLS) { - mode = MODE_TEXT; - } - try { - setKeyboardMode(mode, imeOptions, enableVoice, mPreferSymbols); - } catch (RuntimeException e) { - LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e); - } - } - - private void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) { - if (mInputView == null) return; - mMode = mode; - mImeOptions = imeOptions; - if (enableVoice != mHasVoice) { - // TODO clean up this unnecessary recursive call. - setVoiceMode(enableVoice, mVoiceOnPrimary); - } - mIsSymbols = isSymbols; - - mInputView.setPreviewEnabled(mInputMethodService.getPopupOn()); - KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols); - LatinKeyboard keyboard = null; - keyboard = getKeyboard(id); - - if (mode == MODE_PHONE) { - mInputView.setPhoneKeyboard(keyboard); - } - - mCurrentId = id; - mInputView.setKeyboard(keyboard); - keyboard.setShifted(false); - keyboard.setShiftLocked(keyboard.isShiftLocked()); - keyboard.setImeOptions(mInputMethodService.getResources(), mMode, imeOptions); - keyboard.setColorOfSymbolIcons(mIsAutoCompletionActive, isBlackSym()); - // Update the settings key state because number of enabled IMEs could have been changed - updateSettingsKeyState(PreferenceManager.getDefaultSharedPreferences(mInputMethodService)); - } - - private LatinKeyboard getKeyboard(KeyboardId id) { - SoftReference<LatinKeyboard> ref = mKeyboards.get(id); - LatinKeyboard keyboard = (ref == null) ? null : ref.get(); - if (keyboard == null) { - Resources orig = mInputMethodService.getResources(); - Configuration conf = orig.getConfiguration(); - Locale saveLocale = conf.locale; - conf.locale = mInputLocale; - orig.updateConfiguration(conf, null); - keyboard = new LatinKeyboard(mInputMethodService, id.mXml, id.mKeyboardMode); - keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols - || id.mXml == R.xml.kbd_symbols_black), mHasVoice); - keyboard.setLanguageSwitcher(mLanguageSwitcher, mIsAutoCompletionActive, isBlackSym()); - - if (id.mEnableShiftLock) { - keyboard.enableShiftLock(); - } - mKeyboards.put(id, new SoftReference<LatinKeyboard>(keyboard)); - - conf.locale = saveLocale; - orig.updateConfiguration(conf, null); - } - return keyboard; - } - - private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { - boolean hasVoice = hasVoiceButton(isSymbols); - int charColorId = getCharColorId(); - // TODO: generalize for any KeyboardId - int keyboardRowsResId = KBD_QWERTY[charColorId]; - if (isSymbols) { - if (mode == MODE_PHONE) { - return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId], hasVoice); - } else { - return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - } - } - switch (mode) { - case MODE_NONE: - LatinImeLogger.logOnWarning( - "getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols); - /* fall through */ - case MODE_TEXT: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY : KEYBOARDMODE_NORMAL, - true, hasVoice); - case MODE_SYMBOLS: - return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ? - KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS, - false, hasVoice); - case MODE_PHONE: - return new KeyboardId(KBD_PHONE[charColorId], hasVoice); - case MODE_URL: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_URL_WITH_SETTINGS_KEY : KEYBOARDMODE_URL, true, hasVoice); - case MODE_EMAIL: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY : KEYBOARDMODE_EMAIL, true, hasVoice); - case MODE_IM: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_IM_WITH_SETTINGS_KEY : KEYBOARDMODE_IM, true, hasVoice); - case MODE_WEB: - return new KeyboardId(keyboardRowsResId, mHasSettingsKey ? - KEYBOARDMODE_WEB_WITH_SETTINGS_KEY : KEYBOARDMODE_WEB, true, hasVoice); - } - return null; - } - - public int getKeyboardMode() { - return mMode; - } - - public boolean isAlphabetMode() { - if (mCurrentId == null) { - return false; - } - int currentMode = mCurrentId.mKeyboardMode; - for (Integer mode : ALPHABET_MODES) { - if (currentMode == mode) { - return true; - } - } - return false; - } - - public void setShifted(boolean shifted) { - if (mInputView != null) { - mInputView.setShifted(shifted); - } - } - - public void setShiftLocked(boolean shiftLocked) { - if (mInputView != null) { - mInputView.setShiftLocked(shiftLocked); - } - } - - public void toggleShift() { - if (isAlphabetMode()) - return; - if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) { - LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId); - mCurrentId = mSymbolsShiftedId; - mInputView.setKeyboard(symbolsShiftedKeyboard); - // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To - // enable the indicator, we need to call enableShiftLock() and setShiftLocked(true). - // Thus we can keep the ALT key's Key.on value true while LatinKey.onRelease() is - // called. - symbolsShiftedKeyboard.enableShiftLock(); - symbolsShiftedKeyboard.setShiftLocked(true); - symbolsShiftedKeyboard.setImeOptions(mInputMethodService.getResources(), - mMode, mImeOptions); - } else { - LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId); - mCurrentId = mSymbolsId; - mInputView.setKeyboard(symbolsKeyboard); - // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the - // indicator, we need to call enableShiftLock() and setShiftLocked(false). - symbolsKeyboard.enableShiftLock(); - symbolsKeyboard.setShifted(false); - symbolsKeyboard.setImeOptions(mInputMethodService.getResources(), mMode, mImeOptions); - } - } - - public void onCancelInput() { - // Snap back to the previous keyboard mode if the user cancels sliding input. - if (mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY && getPointerCount() == 1) - mInputMethodService.changeKeyboardMode(); - } - - public void toggleSymbols() { - setKeyboardMode(mMode, mImeOptions, mHasVoice, !mIsSymbols); - if (mIsSymbols && !mPreferSymbols) { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN; - } else { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA; - } - } - - public boolean hasDistinctMultitouch() { - return mInputView != null && mInputView.hasDistinctMultitouch(); - } - - public void setAutoModeSwitchStateMomentary() { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_MOMENTARY; - } - - public boolean isInMomentaryAutoModeSwitchState() { - return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY; - } - - public boolean isInChordingAutoModeSwitchState() { - return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_CHORDING; - } - - public boolean isVibrateAndSoundFeedbackRequired() { - return mInputView != null && !mInputView.isInSlidingKeyInput(); - } - - private int getPointerCount() { - return mInputView == null ? 0 : mInputView.getPointerCount(); - } - - /** - * Updates state machine to figure out when to automatically snap back to the previous mode. - */ - public void onKey(int key) { - // Switch back to alpha mode if user types one or more non-space/enter characters - // followed by a space/enter - switch (mAutoModeSwitchState) { - case AUTO_MODE_SWITCH_STATE_MOMENTARY: - // Only distinct multi touch devices can be in this state. - // On non-distinct multi touch devices, mode change key is handled by {@link onKey}, - // not by {@link onPress} and {@link onRelease}. So, on such devices, - // {@link mAutoModeSwitchState} starts from {@link AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN}, - // or {@link AUTO_MODE_SWITCH_STATE_ALPHA}, not from - // {@link AUTO_MODE_SWITCH_STATE_MOMENTARY}. - if (key == LatinKeyboard.KEYCODE_MODE_CHANGE) { - // Detected only the mode change key has been pressed, and then released. - if (mIsSymbols) { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN; - } else { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA; - } - } else if (getPointerCount() == 1) { - // Snap back to the previous keyboard mode if the user pressed the mode change key - // and slid to other key, then released the finger. - // If the user cancels the sliding input, snapping back to the previous keyboard - // mode is handled by {@link #onCancelInput}. - mInputMethodService.changeKeyboardMode(); - } else { - // Chording input is being started. The keyboard mode will be snapped back to the - // previous mode in {@link onReleaseSymbol} when the mode change key is released. - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_CHORDING; - } - break; - case AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN: - if (key != LatinIME.KEYCODE_SPACE && key != LatinIME.KEYCODE_ENTER && key >= 0) { - mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL; - } - break; - case AUTO_MODE_SWITCH_STATE_SYMBOL: - // Snap back to alpha keyboard mode if user types one or more non-space/enter - // characters followed by a space/enter. - if (key == LatinIME.KEYCODE_ENTER || key == LatinIME.KEYCODE_SPACE) { - mInputMethodService.changeKeyboardMode(); - } - break; - } - } - - public LatinKeyboardView getInputView() { - return mInputView; - } - - public void recreateInputView() { - changeLatinKeyboardView(mLayoutId, true); - } - - private void changeLatinKeyboardView(int newLayout, boolean forceReset) { - if (mLayoutId != newLayout || mInputView == null || forceReset) { - if (mInputView != null) { - mInputView.closing(); - } - if (THEMES.length <= newLayout) { - newLayout = Integer.valueOf(DEFAULT_LAYOUT_ID); - } - - LatinIMEUtil.GCUtils.getInstance().reset(); - boolean tryGC = true; - for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { - try { - mInputView = (LatinKeyboardView) mInputMethodService.getLayoutInflater( - ).inflate(THEMES[newLayout], null); - tryGC = false; - } catch (OutOfMemoryError e) { - tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( - mLayoutId + "," + newLayout, e); - } catch (InflateException e) { - tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( - mLayoutId + "," + newLayout, e); - } - } - mInputView.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); - } else if (LatinIMESettings.PREF_SETTINGS_KEY.equals(key)) { - updateSettingsKeyState(sharedPreferences); - recreateInputView(); - } - } - - public boolean isBlackSym () { - if (mInputView != null && mInputView.getSymbolColorScheme() == 1) { - return true; - } - return false; - } - - private int getCharColorId () { - if (isBlackSym()) { - return CHAR_THEME_COLOR_BLACK; - } else { - return CHAR_THEME_COLOR_WHITE; - } - } - - public void onAutoCompletionStateChanged(boolean isAutoCompletion) { - if (isAutoCompletion != mIsAutoCompletionActive) { - LatinKeyboardView keyboardView = getInputView(); - mIsAutoCompletionActive = isAutoCompletion; - keyboardView.invalidateKey(((LatinKeyboard) keyboardView.getKeyboard()) - .onAutoCompletionStateChanged(isAutoCompletion)); - } - } - - private void updateSettingsKeyState(SharedPreferences prefs) { - Resources resources = mInputMethodService.getResources(); - final String settingsKeyMode = prefs.getString(LatinIMESettings.PREF_SETTINGS_KEY, - resources.getString(DEFAULT_SETTINGS_KEY_MODE)); - // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or - // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system - if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW)) - || (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO)) - && LatinIMEUtil.hasMultipleEnabledIMEs(mInputMethodService))) { - mHasSettingsKey = true; - } else { - mHasSettingsKey = false; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java index 7b5c30491..6faf7f95e 100644 --- a/java/src/com/android/inputmethod/latin/LanguageSwitcher.java +++ b/java/src/com/android/inputmethod/latin/LanguageSwitcher.java @@ -16,21 +16,21 @@ package com.android.inputmethod.latin; -import java.util.Locale; - import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; -import android.preference.PreferenceManager; import android.text.TextUtils; +import java.util.ArrayList; +import java.util.Locale; + /** * Keeps track of list of selected input languages and the current * input language that the user has selected. */ public class LanguageSwitcher { - private Locale[] mLocales; - private LatinIME mIme; + private final ArrayList<Locale> mLocales = new ArrayList<Locale>(); + private final LatinIME mIme; private String[] mSelectedLanguageArray; private String mSelectedLanguages; private int mCurrentIndex = 0; @@ -40,15 +40,10 @@ public class LanguageSwitcher { public LanguageSwitcher(LatinIME ime) { mIme = ime; - mLocales = new Locale[0]; - } - - public Locale[] getLocales() { - return mLocales; } public int getLocaleCount() { - return mLocales.length; + return mLocales.size(); } /** @@ -57,14 +52,14 @@ public class LanguageSwitcher { * @return whether there was any change */ public boolean loadLocales(SharedPreferences sp) { - String selectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, null); - String currentLanguage = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null); + String selectedLanguages = sp.getString(Settings.PREF_SELECTED_LANGUAGES, null); + String currentLanguage = sp.getString(Settings.PREF_INPUT_LANGUAGE, null); if (selectedLanguages == null || selectedLanguages.length() < 1) { loadDefaults(); - if (mLocales.length == 0) { + if (mLocales.size() == 0) { return false; } - mLocales = new Locale[0]; + mLocales.clear(); return true; } if (selectedLanguages.equals(mSelectedLanguages)) { @@ -77,7 +72,7 @@ public class LanguageSwitcher { if (currentLanguage != null) { // Find the index mCurrentIndex = 0; - for (int i = 0; i < mLocales.length; i++) { + for (int i = 0; i < mLocales.size(); i++) { if (mSelectedLanguageArray[i].equals(currentLanguage)) { mCurrentIndex = i; break; @@ -96,11 +91,11 @@ public class LanguageSwitcher { } private void constructLocales() { - mLocales = new Locale[mSelectedLanguageArray.length]; - for (int i = 0; i < mLocales.length; i++) { - final String lang = mSelectedLanguageArray[i]; - mLocales[i] = new Locale(lang.substring(0, 2), + mLocales.clear(); + for (final String lang : mSelectedLanguageArray) { + final Locale locale = new Locale(lang.substring(0, 2), lang.length() > 4 ? lang.substring(3, 5) : ""); + mLocales.add(locale); } } @@ -129,7 +124,17 @@ public class LanguageSwitcher { public Locale getInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - return mLocales[mCurrentIndex]; + return mLocales.get(mCurrentIndex); + } + + private int nextLocaleIndex() { + final int size = mLocales.size(); + return (mCurrentIndex + 1) % size; + } + + private int prevLocaleIndex() { + final int size = mLocales.size(); + return (mCurrentIndex - 1 + size) % size; } /** @@ -139,8 +144,7 @@ public class LanguageSwitcher { */ public Locale getNextInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - - return mLocales[(mCurrentIndex + 1) % mLocales.length]; + return mLocales.get(nextLocaleIndex()); } /** @@ -166,8 +170,7 @@ public class LanguageSwitcher { */ public Locale getPrevInputLocale() { if (getLocaleCount() == 0) return mDefaultInputLocale; - - return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length]; + return mLocales.get(prevLocaleIndex()); } public void reset() { @@ -175,27 +178,16 @@ public class LanguageSwitcher { } public void next() { - mCurrentIndex++; - if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around + mCurrentIndex = nextLocaleIndex(); } public void prev() { - mCurrentIndex--; - if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around + mCurrentIndex = prevLocaleIndex(); } - public void persist() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme); - Editor editor = sp.edit(); - editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage()); + public void persist(SharedPreferences prefs) { + Editor editor = prefs.edit(); + editor.putString(Settings.PREF_INPUT_LANGUAGE, getInputLanguage()); SharedPreferencesCompat.apply(editor); } - - static String toTitleCase(String s) { - if (s.length() == 0) { - return s; - } - - return Character.toUpperCase(s.charAt(0)) + s.substring(1); - } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index b1689f886..8166e0b4e 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -16,12 +16,15 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.LatinIMEUtil.RingCharBuffer; -import com.android.inputmethod.voice.FieldContext; -import com.android.inputmethod.voice.SettingsUtil; -import com.android.inputmethod.voice.VoiceInput; - -import org.xmlpull.v1.XmlPullParserException; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardActionListener; +import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.LatinKeyboard; +import com.android.inputmethod.keyboard.LatinKeyboardView; +import com.android.inputmethod.latin.Utils.RingCharBuffer; +import com.android.inputmethod.voice.VoiceIMEConnector; import android.app.AlertDialog; import android.content.BroadcastReceiver; @@ -32,23 +35,23 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.content.res.XmlResourceParser; import android.inputmethodservice.InputMethodService; -import android.inputmethodservice.Keyboard; import android.media.AudioManager; +import android.net.ConnectivityManager; import android.os.Debug; import android.os.Handler; import android.os.Message; import android.os.SystemClock; +import android.os.Vibrator; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; -import android.speech.SpeechRecognizer; -import android.text.ClipboardManager; +import android.text.InputType; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; +import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -58,206 +61,134 @@ import android.view.ViewParent; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import java.io.FileDescriptor; -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; +import java.util.Arrays; import java.util.Locale; -import java.util.Map; /** * Input method implementation for Qwerty'ish keyboard. */ -public class LatinIME extends InputMethodService - implements LatinKeyboardBaseView.OnKeyboardActionListener, - VoiceInput.UiListener, +public class LatinIME extends InputMethodService implements KeyboardActionListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "LatinIME"; private static final boolean PERF_DEBUG = false; - static final boolean DEBUG = false; - static final boolean TRACE = false; - static final boolean VOICE_INSTALLED = true; - static final boolean ENABLE_VOICE_BUTTON = true; - - private static final String PREF_VIBRATE_ON = "vibrate_on"; - private static final String PREF_SOUND_ON = "sound_on"; - private static final String PREF_POPUP_ON = "popup_on"; - private static final String PREF_AUTO_CAP = "auto_cap"; - private static final String PREF_QUICK_FIXES = "quick_fixes"; - private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; - private static final String PREF_AUTO_COMPLETE = "auto_complete"; - //private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; - private static final String PREF_VOICE_MODE = "voice_mode"; - - // Whether or not the user has used voice input before (and thus, whether to show the - // first-run warning dialog or not). - private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input"; - - // Whether or not the user has used voice input from an unsupported locale UI before. - // For example, the user has a Chinese UI but activates voice input. - private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE = - "has_used_voice_input_unsupported_locale"; - - // A list of locales which are supported by default for voice input, unless we get a - // different list from Gservices. - public static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = - "en " + - "en_US " + - "en_GB " + - "en_AU " + - "en_CA " + - "en_IE " + - "en_IN " + - "en_NZ " + - "en_SG " + - "en_ZA "; - - // The private IME option used to indicate that no microphone should be shown for a - // given text field. For instance this is specified by the search dialog when the - // dialog is already showing a voice search button. - private static final String IME_OPTION_NO_MICROPHONE = "nm"; - - public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; - public static final String PREF_INPUT_LANGUAGE = "input_language"; - private static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled"; - - private static final int MSG_UPDATE_SUGGESTIONS = 0; - private static final int MSG_START_TUTORIAL = 1; - private static final int MSG_UPDATE_SHIFT_STATE = 2; - private static final int MSG_VOICE_RESULTS = 3; - private static final int MSG_UPDATE_OLD_SUGGESTIONS = 4; + private static final boolean TRACE = false; + private static boolean DEBUG = LatinImeLogger.sDBG; + + private static final int DELAY_UPDATE_SUGGESTIONS = 180; + private static final int DELAY_UPDATE_OLD_SUGGESTIONS = 300; + private static final int DELAY_UPDATE_SHIFT_STATE = 300; + private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; // How many continuous deletes at which to start deleting at a higher speed. private static final int DELETE_ACCELERATE_AT = 20; // Key events coming any faster than this are long-presses. private static final int QUICK_PRESS = 200; - static final int KEYCODE_ENTER = '\n'; - static final int KEYCODE_SPACE = ' '; - static final int KEYCODE_PERIOD = '.'; - - // Contextual menu positions - private static final int POS_METHOD = 0; - private static final int POS_SETTINGS = 1; + private int mSuggestionVisibility; + private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE + = R.string.prefs_suggestion_visibility_show_value; + private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE + = R.string.prefs_suggestion_visibility_show_only_portrait_value; + private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE + = R.string.prefs_suggestion_visibility_hide_value; + + private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] { + SUGGESTION_VISIBILILTY_SHOW_VALUE, + SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE, + SUGGESTION_VISIBILILTY_HIDE_VALUE + }; - //private LatinKeyboardView mInputView; - private LinearLayout mCandidateViewContainer; + private View mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; - private CompletionInfo[] mCompletions; + private CompletionInfo[] mApplicationSpecifiedCompletions; private AlertDialog mOptionsDialog; - private AlertDialog mVoiceWarningDialog; - /* package */ KeyboardSwitcher mKeyboardSwitcher; + private InputMethodManager mImm; + private Resources mResources; + private SharedPreferences mPrefs; + private String mInputMethodId; + private KeyboardSwitcher mKeyboardSwitcher; + private SubtypeSwitcher mSubtypeSwitcher; + private VoiceIMEConnector mVoiceConnector; private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private ContactsDictionary mContactsDictionary; private AutoDictionary mAutoDictionary; - private Hints mHints; - - private Resources mResources; - - private String mInputLocale; - private String mSystemLocale; - private LanguageSwitcher mLanguageSwitcher; + // These variables are initialized according to the {@link EditorInfo#inputType}. + private boolean mAutoSpace; + private boolean mInputTypeNoAutoCorrect; + private boolean mIsSettingsSuggestionStripOn; + private boolean mApplicationSpecifiedCompletionOn; - private StringBuilder mComposing = new StringBuilder(); + private final StringBuilder mComposing = new StringBuilder(); private WordComposer mWord = new WordComposer(); - private int mCommittedLength; - private boolean mPredicting; - private boolean mRecognizing; - private boolean mAfterVoiceInput; - private boolean mImmediatelyAfterVoiceInput; - private boolean mShowingVoiceSuggestions; - private boolean mVoiceInputHighlighted; - private boolean mEnableVoiceButton; private CharSequence mBestWord; - private boolean mPredictionOn; - private boolean mCompletionOn; + private boolean mHasValidSuggestions; private boolean mHasDictionary; - private boolean mAutoSpace; private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; private boolean mReCorrectionEnabled; - // Bigram Suggestion is disabled in this version. - private final boolean mBigramSuggestionEnabled = false; + private boolean mBigramSuggestionEnabled; private boolean mAutoCorrectOn; - // TODO move this state variable outside LatinIME - private boolean mCapsLock; - private boolean mPasswordText; private boolean mVibrateOn; private boolean mSoundOn; private boolean mPopupOn; private boolean mAutoCap; private boolean mQuickFixes; - private boolean mHasUsedVoiceInput; - private boolean mHasUsedVoiceInputUnsupportedLocale; - private boolean mLocaleSupportedForVoiceInput; - private boolean mShowSuggestions; - private boolean mIsShowingHint; - private int mCorrectionMode; - private boolean mEnableVoice = true; - private boolean mVoiceOnPrimary; - private int mOrientation; - private List<CharSequence> mSuggestPuncList; - // Keep track of the last selection range to decide if we need to show word alternatives - private int mLastSelectionStart; - private int mLastSelectionEnd; + private boolean mConfigEnableShowSubtypeSettings; + private boolean mConfigSwipeDownDismissKeyboardEnabled; + private int mConfigDelayBeforeFadeoutLanguageOnSpacebar; + private int mConfigDurationOfFadeoutLanguageOnSpacebar; + private float mConfigFinalFadeoutFactorOfLanguageOnSpacebar; - // Input type is such that we should not auto-correct - private boolean mInputTypeNoAutoCorrect; + private int mCorrectionMode; + private int mCommittedLength; + private int mOrientation; + // Keep track of the last selection range to decide if we need to show word alternatives + private int mLastSelectionStart; + private int mLastSelectionEnd; + private SuggestedWords mSuggestPuncList; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; - private CharSequence mJustRevertedSeparator; + private boolean mJustReverted; private int mDeleteCount; private long mLastKeyTime; - // Modifier keys state - private ModifierKeyState mShiftKeyState = new ModifierKeyState(); - private ModifierKeyState mSymbolKeyState = new ModifierKeyState(); - - private Tutorial mTutorial; - private AudioManager mAudioManager; // Align sound effect volume on music volume - private final float FX_VOLUME = -1.0f; + private static final float FX_VOLUME = -1.0f; private boolean mSilentMode; /* package */ String mWordSeparators; private String mSentenceSeparators; private String mSuggestPuncs; - private VoiceInput mVoiceInput; - private VoiceResults mVoiceResults = new VoiceResults(); + // TODO: Move this flag to VoiceIMEConnector private boolean mConfigurationChanging; // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; private boolean mRefreshKeyboardRequired; - // For each word, a list of potential replacements, usually from voice. - private Map<String, List<CharSequence>> mWordToSuggestions = - new HashMap<String, List<CharSequence>>(); - - private ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); - - private class VoiceResults { - List<String> candidates; - Map<String, List<CharSequence>> alternatives; - } + private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>(); public abstract static class WordAlternatives { protected CharSequence mChosenWord; @@ -281,7 +212,7 @@ public class LatinIME extends InputMethodService return mChosenWord; } - public abstract List<CharSequence> getAlternatives(); + public abstract SuggestedWords.Builder getAlternatives(); } public class TypedWordAlternatives extends WordAlternatives { @@ -302,192 +233,224 @@ public class LatinIME extends InputMethodService } @Override - public List<CharSequence> getAlternatives() { + public SuggestedWords.Builder getAlternatives() { return getTypedSuggestions(word); } } - /* package */ Handler mHandler = new Handler() { + public final UIHandler mHandler = new UIHandler(); + + public class UIHandler extends Handler { + private static final int MSG_UPDATE_SUGGESTIONS = 0; + private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1; + private static final int MSG_UPDATE_SHIFT_STATE = 2; + private static final int MSG_VOICE_RESULTS = 3; + private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4; + private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5; + @Override public void handleMessage(Message msg) { + final KeyboardSwitcher switcher = mKeyboardSwitcher; + final LatinKeyboardView inputView = switcher.getInputView(); switch (msg.what) { - case MSG_UPDATE_SUGGESTIONS: - updateSuggestions(); - break; - case MSG_UPDATE_OLD_SUGGESTIONS: - setOldSuggestions(); - break; - case MSG_START_TUTORIAL: - if (mTutorial == null) { - if (mKeyboardSwitcher.getInputView().isShown()) { - mTutorial = new Tutorial( - LatinIME.this, mKeyboardSwitcher.getInputView()); - mTutorial.start(); - } else { - // Try again soon if the view is not yet showing - sendMessageDelayed(obtainMessage(MSG_START_TUTORIAL), 100); - } - } - break; - case MSG_UPDATE_SHIFT_STATE: - updateShiftKeyState(getCurrentInputEditorInfo()); - break; - case MSG_VOICE_RESULTS: - handleVoiceResults(); - break; + case MSG_UPDATE_SUGGESTIONS: + updateSuggestions(); + break; + case MSG_UPDATE_OLD_SUGGESTIONS: + setOldSuggestions(); + break; + case MSG_UPDATE_SHIFT_STATE: + switcher.updateShiftState(); + break; + case MSG_VOICE_RESULTS: + mVoiceConnector.handleVoiceResults(preferCapitalization() + || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked())); + break; + case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR: + if (inputView != null) + inputView.setSpacebarTextFadeFactor( + (1.0f + mConfigFinalFadeoutFactorOfLanguageOnSpacebar) / 2, + (LatinKeyboard)msg.obj); + sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj), + mConfigDurationOfFadeoutLanguageOnSpacebar); + break; + case MSG_DISMISS_LANGUAGE_ON_SPACEBAR: + if (inputView != null) + inputView.setSpacebarTextFadeFactor( + mConfigFinalFadeoutFactorOfLanguageOnSpacebar, (LatinKeyboard)msg.obj); + break; } } - }; + + public void postUpdateSuggestions() { + removeMessages(MSG_UPDATE_SUGGESTIONS); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), DELAY_UPDATE_SUGGESTIONS); + } + + public void cancelUpdateSuggestions() { + removeMessages(MSG_UPDATE_SUGGESTIONS); + } + + public boolean hasPendingUpdateSuggestions() { + return hasMessages(MSG_UPDATE_SUGGESTIONS); + } + + public void postUpdateOldSuggestions() { + removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), + DELAY_UPDATE_OLD_SUGGESTIONS); + } + + public void cancelUpdateOldSuggestions() { + removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + } + + public void postUpdateShiftKeyState() { + removeMessages(MSG_UPDATE_SHIFT_STATE); + sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), DELAY_UPDATE_SHIFT_STATE); + } + + public void cancelUpdateShiftState() { + removeMessages(MSG_UPDATE_SHIFT_STATE); + } + + public void updateVoiceResults() { + sendMessage(obtainMessage(MSG_VOICE_RESULTS)); + } + + public void startDisplayLanguageOnSpacebar(boolean localeChanged) { + removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR); + removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR); + final LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) { + final LatinKeyboard keyboard = inputView.getLatinKeyboard(); + // The language is never displayed when the delay is zero. + if (mConfigDelayBeforeFadeoutLanguageOnSpacebar != 0) + inputView.setSpacebarTextFadeFactor(localeChanged ? 1.0f + : mConfigFinalFadeoutFactorOfLanguageOnSpacebar, keyboard); + // The language is always displayed when the delay is negative. + if (localeChanged && mConfigDelayBeforeFadeoutLanguageOnSpacebar > 0) { + sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard), + mConfigDelayBeforeFadeoutLanguageOnSpacebar); + } + } + } + } @Override public void onCreate() { - LatinImeLogger.init(this); - KeyboardSwitcher.init(this); - super.onCreate(); - //setStatusIcon(R.drawable.ime_qwerty); - mResources = getResources(); - final Configuration conf = mResources.getConfiguration(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - mLanguageSwitcher = new LanguageSwitcher(this); - mLanguageSwitcher.loadLocales(prefs); + mPrefs = prefs; + LatinImeLogger.init(this, prefs); + SubtypeSwitcher.init(this, prefs); + KeyboardSwitcher.init(this, prefs); + + super.onCreate(); + + mImm = ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)); + mInputMethodId = Utils.getInputMethodId(mImm, getApplicationInfo().packageName); + mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); - mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - mSystemLocale = conf.locale.toString(); - mLanguageSwitcher.setSystemLocale(conf.locale); - String inputLanguage = mLanguageSwitcher.getInputLanguage(); - if (inputLanguage == null) { - inputLanguage = conf.locale.toString(); + + final Resources res = getResources(); + mResources = res; + + // If the option should not be shown, do not read the recorrection preference + // but always use the default setting defined in the resources. + if (res.getBoolean(R.bool.config_enable_show_recorrection_option)) { + mReCorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED, + res.getBoolean(R.bool.default_recorrection_enabled)); + } else { + mReCorrectionEnabled = res.getBoolean(R.bool.default_recorrection_enabled); } - mReCorrectionEnabled = prefs.getBoolean(PREF_RECORRECTION_ENABLED, - getResources().getBoolean(R.bool.default_recorrection_enabled)); - LatinIMEUtil.GCUtils.getInstance().reset(); + mConfigEnableShowSubtypeSettings = res.getBoolean( + R.bool.config_enable_show_subtype_settings); + mConfigSwipeDownDismissKeyboardEnabled = res.getBoolean( + R.bool.config_swipe_down_dismiss_keyboard_enabled); + mConfigDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger( + R.integer.config_delay_before_fadeout_language_on_spacebar); + mConfigDurationOfFadeoutLanguageOnSpacebar = res.getInteger( + R.integer.config_duration_of_fadeout_language_on_spacebar); + mConfigFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger( + R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; + + Utils.GCUtils.getInstance().reset(); boolean tryGC = true; - for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { try { - initSuggest(inputLanguage); + initSuggest(); tryGC = false; } catch (OutOfMemoryError e) { - tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e); + tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e); } } - mOrientation = conf.orientation; + mOrientation = res.getConfiguration().orientation; initSuggestPuncList(); - // register to receive ringer mode changes for silent mode - IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); + // register to receive ringer mode change and network state change. + final IntentFilter filter = new IntentFilter(); + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); registerReceiver(mReceiver, filter); - if (VOICE_INSTALLED) { - mVoiceInput = new VoiceInput(this, this); - mHints = new Hints(this, new Hints.Display() { - public void showHint(int viewResource) { - LayoutInflater inflater = (LayoutInflater) getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - View view = inflater.inflate(viewResource, null); - setCandidatesView(view); - setCandidatesViewShown(true); - mIsShowingHint = true; - } - }); - } + mVoiceConnector = VoiceIMEConnector.init(this, prefs, mHandler); prefs.registerOnSharedPreferenceChangeListener(this); } /** - * Loads a dictionary or multiple separated dictionary - * @return returns array of dictionary resource ids + * Returns a main dictionary resource id + * @return main dictionary resource id */ - /* package */ static int[] getDictionary(Resources res) { + public static int getMainDictionaryResourceId(Resources res) { + final String MAIN_DIC_NAME = "main"; String packageName = LatinIME.class.getPackage().getName(); - XmlResourceParser xrp = res.getXml(R.xml.dictionary); - ArrayList<Integer> dictionaries = new ArrayList<Integer>(); - - try { - int current = xrp.getEventType(); - while (current != XmlResourceParser.END_DOCUMENT) { - if (current == XmlResourceParser.START_TAG) { - String tag = xrp.getName(); - if (tag != null) { - if (tag.equals("part")) { - String dictFileName = xrp.getAttributeValue(null, "name"); - dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName)); - } - } - } - xrp.next(); - current = xrp.getEventType(); - } - } catch (XmlPullParserException e) { - Log.e(TAG, "Dictionary XML parsing failure"); - } catch (IOException e) { - Log.e(TAG, "Dictionary XML IOException"); - } - - int count = dictionaries.size(); - int[] dict = new int[count]; - for (int i = 0; i < count; i++) { - dict[i] = dictionaries.get(i); - } - - return dict; + return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName); } - private void initSuggest(String locale) { - mInputLocale = locale; + private void initSuggest() { + updateAutoTextEnabled(); + String locale = mSubtypeSwitcher.getInputLocaleStr(); - Resources orig = getResources(); - Configuration conf = orig.getConfiguration(); - Locale saveLocale = conf.locale; - conf.locale = new Locale(locale); - orig.updateConfiguration(conf, orig.getDisplayMetrics()); + Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale)); if (mSuggest != null) { mSuggest.close(); } - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); + final SharedPreferences prefs = mPrefs; + mQuickFixes = isQuickFixesEnabled(prefs); - int[] dictionaries = getDictionary(orig); - mSuggest = new Suggest(this, dictionaries); - updateAutoTextEnabled(saveLocale); - if (mUserDictionary != null) mUserDictionary.close(); - mUserDictionary = new UserDictionary(this, mInputLocale); - if (mContactsDictionary == null) { - mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); - } - if (mAutoDictionary != null) { - mAutoDictionary.close(); - } - mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO); - if (mUserBigramDictionary != null) { - mUserBigramDictionary.close(); - } - mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale, - Suggest.DIC_USER); - mSuggest.setUserBigramDictionary(mUserBigramDictionary); + final Resources res = mResources; + int mainDicResId = getMainDictionaryResourceId(res); + mSuggest = new Suggest(this, mainDicResId); + loadAndSetAutoCorrectionThreshold(prefs); + + mUserDictionary = new UserDictionary(this, locale); mSuggest.setUserDictionary(mUserDictionary); + + mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); mSuggest.setContactsDictionary(mContactsDictionary); + + mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO); mSuggest.setAutoDictionary(mAutoDictionary); + + mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER); + mSuggest.setUserBigramDictionary(mUserBigramDictionary); + updateCorrectionMode(); - mWordSeparators = mResources.getString(R.string.word_separators); - mSentenceSeparators = mResources.getString(R.string.sentence_separators); + mWordSeparators = res.getString(R.string.word_separators); + mSentenceSeparators = res.getString(R.string.sentence_separators); - conf.locale = saveLocale; - orig.updateConfiguration(conf, orig.getDisplayMetrics()); + mSubtypeSwitcher.changeSystemLocale(savedLocale); } @Override public void onDestroy() { - if (mUserDictionary != null) { - mUserDictionary.close(); - } - if (mContactsDictionary != null) { - mContactsDictionary.close(); + if (mSuggest != null) { + mSuggest.close(); + mSuggest = null; } unregisterReceiver(mReceiver); - if (VOICE_INSTALLED && mVoiceInput != null) { - mVoiceInput.destroy(); - } + mVoiceConnector.destroy(); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); super.onDestroy(); @@ -495,203 +458,205 @@ public class LatinIME extends InputMethodService @Override public void onConfigurationChanged(Configuration conf) { - // If the system locale changes and is different from the saved - // locale (mSystemLocale), then reload the input locale list from the - // latin ime settings (shared prefs) and reset the input locale - // to the first one. - final String systemLocale = conf.locale.toString(); - if (!TextUtils.equals(systemLocale, mSystemLocale)) { - mSystemLocale = systemLocale; - if (mLanguageSwitcher != null) { - mLanguageSwitcher.loadLocales( - PreferenceManager.getDefaultSharedPreferences(this)); - mLanguageSwitcher.setSystemLocale(conf.locale); - toggleLanguage(true, true); - } else { - reloadKeyboards(); - } - } + mSubtypeSwitcher.onConfigurationChanged(conf); // If orientation changed while predicting, commit the change if (conf.orientation != mOrientation) { InputConnection ic = getCurrentInputConnection(); commitTyped(ic); if (ic != null) ic.finishComposingText(); // For voice input mOrientation = conf.orientation; - reloadKeyboards(); + if (isShowingOptionDialog()) + mOptionsDialog.dismiss(); } + mConfigurationChanging = true; super.onConfigurationChanged(conf); - if (mRecognizing) { - switchToRecognitionStatusView(); - } + mVoiceConnector.onConfigurationChanged(conf); mConfigurationChanging = false; } @Override public View onCreateInputView() { - mKeyboardSwitcher.recreateInputView(); - mKeyboardSwitcher.makeKeyboards(true); - mKeyboardSwitcher.setKeyboardMode( - KeyboardSwitcher.MODE_TEXT, 0, - shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); - return mKeyboardSwitcher.getInputView(); + return mKeyboardSwitcher.onCreateInputView(); } @Override public View onCreateCandidatesView() { - mKeyboardSwitcher.makeKeyboards(true); - mCandidateViewContainer = (LinearLayout) getLayoutInflater().inflate( - R.layout.candidates, null); - mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates); + LayoutInflater inflater = getLayoutInflater(); + LinearLayout container = (LinearLayout)inflater.inflate(R.layout.candidates, null); + mCandidateViewContainer = container; + if (container.getPaddingRight() != 0) { + HorizontalScrollView scrollView = + (HorizontalScrollView) container.findViewById(R.id.candidates_scroll_view); + scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); + container.setGravity(Gravity.CENTER_HORIZONTAL); + } + mCandidateView = (CandidateView) container.findViewById(R.id.candidates); mCandidateView.setService(this); setCandidatesViewShown(true); - return mCandidateViewContainer; + return container; + } + + private static boolean isPasswordVariation(int variation) { + return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD + || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; + } + + private static boolean isEmailVariation(int variation) { + return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; } @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + final KeyboardSwitcher switcher = mKeyboardSwitcher; + LatinKeyboardView inputView = switcher.getInputView(); + + if(DEBUG) { + Log.d(TAG, "onStartInputView: " + inputView); + } // In landscape mode, this method gets called without the input view being created. if (inputView == null) { return; } + mSubtypeSwitcher.updateParametersOnStartInputView(); + if (mRefreshKeyboardRequired) { mRefreshKeyboardRequired = false; - toggleLanguage(true, true); + onRefreshKeyboard(); } - mKeyboardSwitcher.makeKeyboards(false); - TextEntryState.newSession(this); - // Most such things we decide below in the switch statement, but we need to know - // now whether this is a password text field, because we need to know now (before - // the switch statement) whether we want to enable the voice button. - mPasswordText = false; - int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; - if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || - variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) { - mPasswordText = true; - } + // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to + // know now whether this is a password text field, because we need to know now whether we + // want to enable the voice button. + mVoiceConnector.resetVoiceStates(isPasswordVariation( + attribute.inputType & InputType.TYPE_MASK_VARIATION)); - mEnableVoiceButton = shouldShowVoiceButton(makeFieldContext(), attribute); - final boolean enableVoiceButton = mEnableVoiceButton && mEnableVoice; + final int mode = initializeInputAttributesAndGetMode(attribute.inputType); - mAfterVoiceInput = false; - mImmediatelyAfterVoiceInput = false; - mShowingVoiceSuggestions = false; - mVoiceInputHighlighted = false; - mInputTypeNoAutoCorrect = false; - mPredictionOn = false; - mCompletionOn = false; - mCompletions = null; - mCapsLock = false; + inputView.closing(); mEnteredText = null; + mComposing.setLength(0); + mHasValidSuggestions = false; + mDeleteCount = 0; + mJustAddedAutoSpace = false; + + loadSettings(attribute); + if (mSubtypeSwitcher.isKeyboardMode()) { + switcher.loadKeyboard(mode, attribute.imeOptions, + mVoiceConnector.isVoiceButtonEnabled(), + mVoiceConnector.isVoiceButtonOnPrimary()); + switcher.updateShiftState(); + } + + setCandidatesViewShownInternal(isCandidateStripVisible(), + false /* needsInputViewShown */ ); + // Delay updating suggestions because keyboard input view may not be shown at this point. + mHandler.postUpdateSuggestions(); + + updateCorrectionMode(); + + inputView.setPreviewEnabled(mPopupOn); + inputView.setProximityCorrectionEnabled(true); + // If we just entered a text field, maybe it has some old text that requires correction + checkReCorrectionOnStart(); + inputView.setForeground(true); - switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { - case EditorInfo.TYPE_CLASS_NUMBER: - case EditorInfo.TYPE_CLASS_DATETIME: - // fall through - // NOTE: For now, we use the phone keyboard for NUMBER and DATETIME until we get - // a dedicated number entry keypad. - // TODO: Use a dedicated number entry keypad here when we get one. - case EditorInfo.TYPE_CLASS_PHONE: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, - attribute.imeOptions, enableVoiceButton); + mVoiceConnector.onStartInputView(inputView.getWindowToken()); + + if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); + } + + private int initializeInputAttributesAndGetMode(int inputType) { + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + mAutoSpace = false; + mInputTypeNoAutoCorrect = false; + mIsSettingsSuggestionStripOn = false; + mApplicationSpecifiedCompletionOn = false; + mApplicationSpecifiedCompletions = null; + + final int mode; + switch (inputType & InputType.TYPE_MASK_CLASS) { + case InputType.TYPE_CLASS_NUMBER: + case InputType.TYPE_CLASS_DATETIME: + mode = KeyboardId.MODE_NUMBER; + break; + case InputType.TYPE_CLASS_PHONE: + mode = KeyboardId.MODE_PHONE; break; - case EditorInfo.TYPE_CLASS_TEXT: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions, enableVoiceButton); - //startPrediction(); - mPredictionOn = true; + case InputType.TYPE_CLASS_TEXT: + mIsSettingsSuggestionStripOn = true; // Make sure that passwords are not displayed in candidate view - if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || - variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { - mPredictionOn = false; + if (isPasswordVariation(variation)) { + mIsSettingsSuggestionStripOn = false; } - if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { + if (isEmailVariation(variation) + || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { mAutoSpace = false; } else { mAutoSpace = true; } - if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { - mPredictionOn = false; - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, - attribute.imeOptions, enableVoiceButton); - } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { - mPredictionOn = false; - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, - attribute.imeOptions, enableVoiceButton); - } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, - attribute.imeOptions, enableVoiceButton); - } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { - mPredictionOn = false; - } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_WEB, - attribute.imeOptions, enableVoiceButton); + if (isEmailVariation(variation)) { + mIsSettingsSuggestionStripOn = false; + mode = KeyboardId.MODE_EMAIL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { + mIsSettingsSuggestionStripOn = false; + mode = KeyboardId.MODE_URL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + mode = KeyboardId.MODE_IM; + } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + mIsSettingsSuggestionStripOn = false; + mode = KeyboardId.MODE_TEXT; + } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) { + mode = KeyboardId.MODE_WEB; // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. - if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { + if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) { mInputTypeNoAutoCorrect = true; } + } else { + mode = KeyboardId.MODE_TEXT; } // If NO_SUGGESTIONS is set, don't do prediction. - if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { - mPredictionOn = false; + if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { + mIsSettingsSuggestionStripOn = false; mInputTypeNoAutoCorrect = true; } // If it's not multiline and the autoCorrect flag is not set, then don't correct - if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && - (attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { + if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0 && + (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) { mInputTypeNoAutoCorrect = true; } - if ((attribute.inputType & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { - mPredictionOn = false; - mCompletionOn = isFullscreenMode(); + if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { + mIsSettingsSuggestionStripOn = false; + mApplicationSpecifiedCompletionOn = isFullscreenMode(); } break; default: - mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, - attribute.imeOptions, enableVoiceButton); + mode = KeyboardId.MODE_TEXT; + break; } - inputView.closing(); - mComposing.setLength(0); - mPredicting = false; - mDeleteCount = 0; - mJustAddedAutoSpace = false; - loadSettings(); - updateShiftKeyState(attribute); - - setCandidatesViewShownInternal(isCandidateStripVisible() || mCompletionOn, - false /* needsInputViewShown */ ); - updateSuggestions(); - - // If the dictionary is not big enough, don't auto correct - mHasDictionary = mSuggest.hasMainDictionary(); - - updateCorrectionMode(); - - inputView.setPreviewEnabled(mPopupOn); - inputView.setProximityCorrectionEnabled(true); - mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions); - // If we just entered a text field, maybe it has some old text that requires correction - checkReCorrectionOnStart(); - checkTutorial(attribute.privateImeOptions); - if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); + return mode; } private void checkReCorrectionOnStart() { - if (mReCorrectionEnabled && isPredictionOn()) { + if (!mReCorrectionEnabled) return; + + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + // There could be a pending composing span. Clean it up first. + ic.finishComposingText(); + + if (isShowingSuggestionsStrip() && isSuggestionsRequested()) { // First get the cursor position. This is required by setOldSuggestions(), so that // it can pass the correct range to setComposingRegion(). At this point, we don't - // have valid values for mLastSelectionStart/Stop because onUpdateSelection() has + // have valid values for mLastSelectionStart/End because onUpdateSelection() has // not been called yet. - InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; ExtractedTextRequest etr = new ExtractedTextRequest(); etr.token = 0; // anything is fine here ExtractedText et = ic.getExtractedText(etr, 0); @@ -702,7 +667,7 @@ public class LatinIME extends InputMethodService // Then look for possible corrections in a delayed fashion if (!TextUtils.isEmpty(et.text) && isCursorTouchingWord()) { - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } } } @@ -712,19 +677,12 @@ public class LatinIME extends InputMethodService super.onFinishInput(); LatinImeLogger.commit(); - onAutoCompletionStateChanged(false); + mKeyboardSwitcher.onAutoCorrectionStateChanged(false); - if (VOICE_INSTALLED && !mConfigurationChanging) { - if (mAfterVoiceInput) { - mVoiceInput.flushAllTextModificationCounters(); - mVoiceInput.logInputEnded(); - } - mVoiceInput.flushLogs(); - mVoiceInput.cancel(); - } - if (mKeyboardSwitcher.getInputView() != null) { - mKeyboardSwitcher.getInputView().closing(); - } + mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging); + + KeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) inputView.closing(); if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); } @@ -732,21 +690,17 @@ public class LatinIME extends InputMethodService @Override public void onFinishInputView(boolean finishingInput) { super.onFinishInputView(finishingInput); - // Remove penging messages related to update suggestions - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); - mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + KeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) inputView.setForeground(false); + // Remove pending messages related to update suggestions + mHandler.cancelUpdateSuggestions(); + mHandler.cancelUpdateOldSuggestions(); } @Override public void onUpdateExtractedText(int token, ExtractedText text) { super.onUpdateExtractedText(token, text); - InputConnection ic = getCurrentInputConnection(); - if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) { - if (mHints.showPunctuationHintIfNecessary(ic)) { - mVoiceInput.logPunctuationHintDisplayed(); - } - } - mImmediatelyAfterVoiceInput = false; + mVoiceConnector.showPunctuationHintIfNecessary(); } @Override @@ -765,64 +719,58 @@ public class LatinIME extends InputMethodService + ", ce=" + candidatesEnd); } - if (mAfterVoiceInput) { - mVoiceInput.setCursorPos(newSelEnd); - mVoiceInput.setSelectionSpan(newSelEnd - newSelStart); - } + mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart); // If the current selection in the text view changes, we should // clear whatever candidate text we have. - if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted) - && (newSelStart != candidatesEnd - || newSelEnd != candidatesEnd) - && mLastSelectionStart != newSelStart)) { + if ((((mComposing.length() > 0 && mHasValidSuggestions) + || mVoiceConnector.isVoiceInputHighlighted()) && (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart)) { mComposing.setLength(0); - mPredicting = false; - postUpdateSuggestions(); + mHasValidSuggestions = false; + mHandler.postUpdateSuggestions(); TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.finishComposingText(); } - mVoiceInputHighlighted = false; - } else if (!mPredicting && !mJustAccepted) { + mVoiceConnector.setVoiceInputHighlighted(false); + } else if (!mHasValidSuggestions && !mJustAccepted) { switch (TextEntryState.getState()) { - case ACCEPTED_DEFAULT: - TextEntryState.reset(); - // fall through - case SPACE_AFTER_PICKED: - mJustAddedAutoSpace = false; // The user moved the cursor. - break; + case ACCEPTED_DEFAULT: + TextEntryState.reset(); + // $FALL-THROUGH$ + case SPACE_AFTER_PICKED: + mJustAddedAutoSpace = false; // The user moved the cursor. + break; + default: + break; } } mJustAccepted = false; - postUpdateShiftKeyState(); + mHandler.postUpdateShiftKeyState(); // Make a note of the cursor position mLastSelectionStart = newSelStart; mLastSelectionEnd = newSelEnd; - if (mReCorrectionEnabled) { + if (mReCorrectionEnabled && isShowingSuggestionsStrip()) { // Don't look for corrections if the keyboard is not visible - if (mKeyboardSwitcher != null && mKeyboardSwitcher.getInputView() != null - && mKeyboardSwitcher.getInputView().isShown()) { + if (mKeyboardSwitcher.isInputViewShown()) { // Check if we should go in or out of correction mode. - if (isPredictionOn() - && mJustRevertedSeparator == null + if (isSuggestionsRequested() && !mJustReverted && (candidatesStart == candidatesEnd || newSelStart != oldSelStart || TextEntryState.isCorrecting()) - && (newSelStart < newSelEnd - 1 || (!mPredicting)) - && !mVoiceInputHighlighted) { + && (newSelStart < newSelEnd - 1 || !mHasValidSuggestions)) { if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } else { abortCorrection(false); // Show the punctuation suggestions list if the current one is not // and if not showing "Touch again to save". - if (mCandidateView != null - && !mSuggestPuncList.equals(mCandidateView.getSuggestions()) - && !mCandidateView.isShowingAddToDictionaryHint()) { - setNextSuggestions(); + if (mCandidateView != null && !isShowingPunctuationList() + && !mCandidateView.isShowingAddToDictionaryHint()) { + setPunctuationSuggestions(); } } } @@ -840,7 +788,7 @@ public class LatinIME extends InputMethodService */ @Override public void onExtractedTextClicked() { - if (mReCorrectionEnabled && isPredictionOn()) return; + if (mReCorrectionEnabled && isSuggestionsRequested()) return; super.onExtractedTextClicked(); } @@ -856,7 +804,7 @@ public class LatinIME extends InputMethodService */ @Override public void onExtractedCursorMovement(int dx, int dy) { - if (mReCorrectionEnabled && isPredictionOn()) return; + if (mReCorrectionEnabled && isSuggestionsRequested()) return; super.onExtractedCursorMovement(dx, dy); } @@ -864,52 +812,42 @@ public class LatinIME extends InputMethodService @Override public void hideWindow() { LatinImeLogger.commit(); - onAutoCompletionStateChanged(false); + mKeyboardSwitcher.onAutoCorrectionStateChanged(false); if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } - if (!mConfigurationChanging) { - if (mAfterVoiceInput) mVoiceInput.logInputEnded(); - if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) { - mVoiceInput.logKeyboardWarningDialogDismissed(); - mVoiceWarningDialog.dismiss(); - mVoiceWarningDialog = null; - } - if (VOICE_INSTALLED & mRecognizing) { - mVoiceInput.cancel(); - } - } - mWordToSuggestions.clear(); + mVoiceConnector.hideVoiceWindow(mConfigurationChanging); mWordHistory.clear(); super.hideWindow(); TextEntryState.endSession(); } @Override - public void onDisplayCompletions(CompletionInfo[] completions) { + public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) { if (DEBUG) { - Log.i("foo", "Received completions:"); - for (int i=0; i<(completions != null ? completions.length : 0); i++) { - Log.i("foo", " #" + i + ": " + completions[i]); + Log.i(TAG, "Received completions:"); + final int count = (applicationSpecifiedCompletions != null) + ? applicationSpecifiedCompletions.length : 0; + for (int i = 0; i < count; i++) { + Log.i(TAG, " #" + i + ": " + applicationSpecifiedCompletions[i]); } } - if (mCompletionOn) { - mCompletions = completions; - if (completions == null) { + if (mApplicationSpecifiedCompletionOn) { + mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; + if (applicationSpecifiedCompletions == null) { clearSuggestions(); return; } - List<CharSequence> stringList = new ArrayList<CharSequence>(); - for (int i=0; i<(completions != null ? completions.length : 0); i++) { - CompletionInfo ci = completions[i]; - if (ci != null) stringList.add(ci.getText()); - } + SuggestedWords.Builder builder = new SuggestedWords.Builder() + .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) + .setTypedWordValid(true) + .setHasMinimalSuggestion(true); // When in fullscreen mode, show completions generated by the application - setSuggestions(stringList, true, true, true); + setSuggestions(builder.build()); mBestWord = null; setCandidatesViewShown(true); } @@ -918,8 +856,8 @@ public class LatinIME extends InputMethodService private void setCandidatesViewShownInternal(boolean shown, boolean needsInputViewShown) { // TODO: Remove this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { - super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null - && (needsInputViewShown ? mKeyboardSwitcher.getInputView().isShown() : true)); + super.setCandidatesViewShown(shown + && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true)); } } @@ -934,14 +872,43 @@ public class LatinIME extends InputMethodService if (!isFullscreenMode()) { outInsets.contentTopInsets = outInsets.visibleTopInsets; } + KeyboardView inputView = mKeyboardSwitcher.getInputView(); + // Need to set touchable region only if input view is being shown + if (inputView != null && mKeyboardSwitcher.isInputViewShown()) { + final int x = 0; + int y = 0; + final int width = inputView.getWidth(); + int height = inputView.getHeight() + EXTENDED_TOUCHABLE_REGION_HEIGHT; + if (mCandidateViewContainer != null) { + ViewParent candidateParent = mCandidateViewContainer.getParent(); + if (candidateParent instanceof FrameLayout) { + FrameLayout fl = (FrameLayout) candidateParent; + if (fl != null) { + // Check frame layout's visibility + if (fl.getVisibility() == View.INVISIBLE) { + y = fl.getHeight(); + height += y; + } else if (fl.getVisibility() == View.VISIBLE) { + height += fl.getHeight(); + } + } + } + } + if (DEBUG) { + Log.d(TAG, "Touchable region " + x + ", " + y + ", " + width + ", " + height); + } + outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; + outInsets.touchableRegion.set(x, y, width, height); + } } @Override public boolean onEvaluateFullscreenMode() { - DisplayMetrics dm = getResources().getDisplayMetrics(); + final Resources res = mResources; + DisplayMetrics dm = res.getDisplayMetrics(); float displayHeight = dm.heightPixels; // If the display is more than X inches high, don't go to fullscreen mode - float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen); + float dimen = res.getDimension(R.dimen.max_height_for_fullscreen); if (displayHeight > dimen) { return false; } else { @@ -952,25 +919,13 @@ public class LatinIME extends InputMethodService @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { - if (mKeyboardSwitcher.getInputView().handleBack()) { - return true; - } else if (mTutorial != null) { - mTutorial.close(); - mTutorial = null; - } - } - break; - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - // If tutorial is visible, don't allow dpad to work - if (mTutorial != null) { + case KeyEvent.KEYCODE_BACK: + if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { + if (mKeyboardSwitcher.getInputView().handleBack()) { return true; } - break; + } + break; } return super.onKeyDown(keyCode, event); } @@ -978,57 +933,30 @@ public class LatinIME extends InputMethodService @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - // If tutorial is visible, don't allow dpad to work - if (mTutorial != null) { - return true; - } - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - // Enable shift key and DPAD to do selections - if (inputView != null && inputView.isShown() - && inputView.isShifted()) { - event = new KeyEvent(event.getDownTime(), event.getEventTime(), - event.getAction(), event.getKeyCode(), event.getRepeatCount(), - event.getDeviceId(), event.getScanCode(), - KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); - InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.sendKeyEvent(event); - return true; - } - break; + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + // Enable shift key and DPAD to do selections + if (mKeyboardSwitcher.isInputViewShown() + && mKeyboardSwitcher.isShiftedOrShiftLocked()) { + KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(), + event.getAction(), event.getKeyCode(), event.getRepeatCount(), + event.getDeviceId(), event.getScanCode(), + KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) + ic.sendKeyEvent(newEvent); + return true; + } + break; } return super.onKeyUp(keyCode, event); } - private void revertVoiceInput() { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.commitText("", 1); - updateSuggestions(); - mVoiceInputHighlighted = false; - } - - private void commitVoiceInput() { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) ic.finishComposingText(); - updateSuggestions(); - mVoiceInputHighlighted = false; - } - - private void reloadKeyboards() { - mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - if (mKeyboardSwitcher.getInputView() != null - && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) { - mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary); - } - mKeyboardSwitcher.makeKeyboards(true); - } - - private void commitTyped(InputConnection inputConnection) { - if (mPredicting) { - mPredicting = false; + public void commitTyped(InputConnection inputConnection) { + if (mHasValidSuggestions) { + mHasValidSuggestions = false; if (mComposing.length() > 0) { if (inputConnection != null) { inputConnection.commitText(mComposing, 1); @@ -1041,27 +969,13 @@ public class LatinIME extends InputMethodService } } - private void postUpdateShiftKeyState() { - mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - // TODO: Should remove this 300ms delay? - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SHIFT_STATE), 300); - } - - public void updateShiftKeyState(EditorInfo attr) { + public boolean getCurrentAutoCapsState() { InputConnection ic = getCurrentInputConnection(); - if (ic != null && attr != null && mKeyboardSwitcher.isAlphabetMode()) { - mKeyboardSwitcher.setShifted(mShiftKeyState.isMomentary() || mCapsLock - || getCursorCapsMode(ic, attr) != 0); - } - } - - private int getCursorCapsMode(InputConnection ic, EditorInfo attr) { - int caps = 0; EditorInfo ei = getCurrentInputEditorInfo(); - if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { - caps = ic.getCursorCapsMode(attr.inputType); + if (mAutoCap && ic != null && ei != null && ei.inputType != InputType.TYPE_NULL) { + return ic.getCursorCapsMode(ei.inputType) != 0; } - return caps; + return false; } private void swapPunctuationAndSpace() { @@ -1069,12 +983,13 @@ public class LatinIME extends InputMethodService if (ic == null) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); if (lastTwo != null && lastTwo.length() == 2 - && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) { + && lastTwo.charAt(0) == Keyboard.CODE_SPACE + && isSentenceSeparator(lastTwo.charAt(1))) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); mJustAddedAutoSpace = true; } } @@ -1084,14 +999,14 @@ public class LatinIME extends InputMethodService if (ic == null) return; CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 - && lastThree.charAt(0) == KEYCODE_PERIOD - && lastThree.charAt(1) == KEYCODE_SPACE - && lastThree.charAt(2) == KEYCODE_PERIOD) { + && lastThree.charAt(0) == Keyboard.CODE_PERIOD + && lastThree.charAt(1) == Keyboard.CODE_SPACE + && lastThree.charAt(2) == Keyboard.CODE_PERIOD) { ic.beginBatchEdit(); ic.deleteSurroundingText(3, 0); ic.commitText(" ..", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); } } @@ -1103,12 +1018,13 @@ public class LatinIME extends InputMethodService CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && Character.isLetterOrDigit(lastThree.charAt(0)) - && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) { + && lastThree.charAt(1) == Keyboard.CODE_SPACE + && lastThree.charAt(2) == Keyboard.CODE_SPACE) { ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); mJustAddedAutoSpace = true; } } @@ -1121,8 +1037,8 @@ public class LatinIME extends InputMethodService // if there is one. CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 - && lastOne.charAt(0) == KEYCODE_PERIOD - && text.charAt(0) == KEYCODE_PERIOD) { + && lastOne.charAt(0) == Keyboard.CODE_PERIOD + && text.charAt(0) == Keyboard.CODE_PERIOD) { ic.deleteSurroundingText(1, 0); } } @@ -1133,7 +1049,7 @@ public class LatinIME extends InputMethodService CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 - && lastOne.charAt(0) == KEYCODE_SPACE) { + && lastOne.charAt(0) == Keyboard.CODE_SPACE) { ic.deleteSurroundingText(1, 0); } } @@ -1142,7 +1058,7 @@ public class LatinIME extends InputMethodService mUserDictionary.addWord(word, 128); // Suggestion strip should be updated after the operation of adding word to the // user dictionary - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); return true; } @@ -1154,14 +1070,11 @@ public class LatinIME extends InputMethodService } } - private void showInputMethodPicker() { - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) - .showInputMethodPicker(); - } - - private void onOptionKeyPressed() { + private void onSettingsKeyPressed() { if (!isShowingOptionDialog()) { - if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) { + if (!mConfigEnableShowSubtypeSettings) { + showSubtypeSelectorAndSettings(); + } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { showOptionsMenu(); } else { launchSettings(); @@ -1169,10 +1082,10 @@ public class LatinIME extends InputMethodService } } - private void onOptionKeyLongPressed() { + private void onSettingsKeyLongPressed() { if (!isShowingOptionDialog()) { - if (LatinIMEUtil.hasMultipleEnabledIMEs(this)) { - showInputMethodPicker(); + if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) { + mImm.showInputMethodPicker(); } else { launchSettings(); } @@ -1183,150 +1096,137 @@ public class LatinIME extends InputMethodService return mOptionsDialog != null && mOptionsDialog.isShowing(); } - // Implementation of KeyboardViewListener - - public void onKey(int primaryCode, int[] keyCodes, int x, int y) { + // Implementation of {@link KeyboardActionListener}. + @Override + public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { long when = SystemClock.uptimeMillis(); - if (primaryCode != Keyboard.KEYCODE_DELETE || - when > mLastKeyTime + QUICK_PRESS) { + if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { mDeleteCount = 0; } mLastKeyTime = when; - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); + KeyboardSwitcher switcher = mKeyboardSwitcher; + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); switch (primaryCode) { - case Keyboard.KEYCODE_DELETE: - handleBackspace(); - mDeleteCount++; - LatinImeLogger.logOnDelete(); - break; - case Keyboard.KEYCODE_SHIFT: - // Shift key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) - handleShift(); - break; - case Keyboard.KEYCODE_MODE_CHANGE: - // Symbol key is handled in onPress() when device has distinct multi-touch panel. - if (!distinctMultiTouch) - changeKeyboardMode(); - break; - case Keyboard.KEYCODE_CANCEL: - if (!isShowingOptionDialog()) { - handleClose(); - } - break; - case LatinKeyboardView.KEYCODE_OPTIONS: - onOptionKeyPressed(); - break; - case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS: - onOptionKeyLongPressed(); - break; - case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE: - toggleLanguage(false, true); - break; - case LatinKeyboardView.KEYCODE_PREV_LANGUAGE: - toggleLanguage(false, false); - break; - case LatinKeyboardView.KEYCODE_VOICE: - if (VOICE_INSTALLED) { - startListening(false /* was a button press, was not a swipe */); - } - break; - case 9 /*Tab*/: - sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); - break; - default: - if (primaryCode != KEYCODE_ENTER) { - mJustAddedAutoSpace = false; - } - RingCharBuffer.getInstance().push((char)primaryCode, x, y); - LatinImeLogger.logOnInputChar(); - if (isWordSeparator(primaryCode)) { - handleSeparator(primaryCode); - } else { - handleCharacter(primaryCode, keyCodes); - } - // Cancel the just reverted state - mJustRevertedSeparator = null; + case Keyboard.CODE_DELETE: + handleBackspace(); + mDeleteCount++; + LatinImeLogger.logOnDelete(); + break; + case Keyboard.CODE_SHIFT: + // Shift key is handled in onPress() when device has distinct multi-touch panel. + if (!distinctMultiTouch) + switcher.toggleShift(); + break; + case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: + // Symbol key is handled in onPress() when device has distinct multi-touch panel. + if (!distinctMultiTouch) + switcher.changeKeyboardMode(); + break; + case Keyboard.CODE_CANCEL: + if (!isShowingOptionDialog()) { + handleClose(); + } + break; + case Keyboard.CODE_SETTINGS: + onSettingsKeyPressed(); + break; + case Keyboard.CODE_SETTINGS_LONGPRESS: + onSettingsKeyLongPressed(); + break; + case Keyboard.CODE_NEXT_LANGUAGE: + toggleLanguage(false, true); + break; + case Keyboard.CODE_PREV_LANGUAGE: + toggleLanguage(false, false); + break; + case Keyboard.CODE_CAPSLOCK: + switcher.toggleCapsLock(); + break; + case Keyboard.CODE_VOICE: + mSubtypeSwitcher.switchToShortcutIME(); + break; + case Keyboard.CODE_TAB: + handleTab(); + break; + default: + if (primaryCode != Keyboard.CODE_ENTER) { + mJustAddedAutoSpace = false; + } + RingCharBuffer.getInstance().push((char)primaryCode, x, y); + LatinImeLogger.logOnInputChar(); + if (isWordSeparator(primaryCode)) { + handleSeparator(primaryCode); + } else { + handleCharacter(primaryCode, keyCodes); + } + // Cancel the just reverted state + mJustReverted = false; } - mKeyboardSwitcher.onKey(primaryCode); + switcher.onKey(primaryCode); // Reset after any single keystroke mEnteredText = null; } - public void onText(CharSequence text) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } + @Override + public void onTextInput(CharSequence text) { + mVoiceConnector.commitVoiceInput(); InputConnection ic = getCurrentInputConnection(); if (ic == null) return; abortCorrection(false); ic.beginBatchEdit(); - if (mPredicting) { - commitTyped(ic); - } + commitTyped(ic); maybeRemovePreviousPeriod(text); ic.commitText(text, 1); ic.endBatchEdit(); - updateShiftKeyState(getCurrentInputEditorInfo()); - mKeyboardSwitcher.onKey(0); // dummy key code. - mJustRevertedSeparator = null; + mKeyboardSwitcher.updateShiftState(); + mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); + mJustReverted = false; mJustAddedAutoSpace = false; mEnteredText = text; } - public void onCancel() { + @Override + public void onCancelInput() { // User released a finger outside any key mKeyboardSwitcher.onCancelInput(); } private void handleBackspace() { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - mVoiceInput.incrementTextModificationDeleteCount( - mVoiceResults.candidates.get(0).toString().length()); - revertVoiceInput(); - return; - } - boolean deleteChar = false; - InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; + if (mVoiceConnector.logAndRevertVoiceInput()) return; + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; ic.beginBatchEdit(); - if (mAfterVoiceInput) { - // Don't log delete if the user is pressing delete at - // the beginning of the text box (hence not deleting anything) - if (mVoiceInput.getCursorPos() > 0) { - // If anything was selected before the delete was pressed, increment the - // delete count by the length of the selection - int deleteLen = mVoiceInput.getSelectionSpan() > 0 ? - mVoiceInput.getSelectionSpan() : 1; - mVoiceInput.incrementTextModificationDeleteCount(deleteLen); - } - } + mVoiceConnector.handleBackspace(); - if (mPredicting) { + boolean deleteChar = false; + if (mHasValidSuggestions) { final int length = mComposing.length(); if (length > 0) { mComposing.delete(length - 1, length); mWord.deleteLast(); ic.setComposingText(mComposing, 1); if (mComposing.length() == 0) { - mPredicting = false; + mHasValidSuggestions = false; } - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } else { ic.deleteSurroundingText(1, 0); } } else { deleteChar = true; } - postUpdateShiftKeyState(); + mHandler.postUpdateShiftKeyState(); + TextEntryState.backspace(); if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { revertLastWord(deleteChar); ic.endBatchEdit(); return; - } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { + } + + if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { ic.deleteSurroundingText(mEnteredText.length(), 0); } else if (deleteChar) { if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { @@ -1345,153 +1245,134 @@ public class LatinIME extends InputMethodService } } } - mJustRevertedSeparator = null; + mJustReverted = false; ic.endBatchEdit(); } - private void resetShift() { - handleShiftInternal(true); - } + private void handleTab() { + final int imeOptions = getCurrentInputEditorInfo().imeOptions; + final int navigationFlags = + EditorInfo.IME_FLAG_NAVIGATE_NEXT | EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; + if ((imeOptions & navigationFlags) == 0) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB); + return; + } - private void handleShift() { - handleShiftInternal(false); - } + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) + return; - private void handleShiftInternal(boolean forceNormal) { - mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - KeyboardSwitcher switcher = mKeyboardSwitcher; - LatinKeyboardView inputView = switcher.getInputView(); - if (switcher.isAlphabetMode()) { - if (mCapsLock || forceNormal) { - mCapsLock = false; - switcher.setShifted(false); - } else if (inputView != null) { - if (inputView.isShifted()) { - mCapsLock = true; - switcher.setShiftLocked(true); - } else { - switcher.setShifted(true); - } - } - } else { - switcher.toggleShift(); + // True if keyboard is in either chording shift or manual temporary upper case mode. + final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase(); + if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 + && !isManualTemporaryUpperCase) { + ic.performEditorAction(EditorInfo.IME_ACTION_NEXT); + } else if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 + && isManualTemporaryUpperCase) { + ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); } } private void abortCorrection(boolean force) { if (force || TextEntryState.isCorrecting()) { + TextEntryState.onAbortCorrection(); + setCandidatesViewShown(isCandidateStripVisible()); getCurrentInputConnection().finishComposingText(); clearSuggestions(); } } private void handleCharacter(int primaryCode, int[] keyCodes) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } + mVoiceConnector.handleCharacter(); - if (mAfterVoiceInput) { - // Assume input length is 1. This assumption fails for smiley face insertions. - mVoiceInput.incrementTextModificationInsertCount(1); - } if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) { abortCorrection(false); } - if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { - if (!mPredicting) { - mPredicting = true; + int code = primaryCode; + if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) { + if (!mHasValidSuggestions) { + mHasValidSuggestions = true; mComposing.setLength(0); saveWordInHistory(mBestWord); mWord.reset(); } } - if (mKeyboardSwitcher.getInputView().isShifted()) { + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (switcher.isShiftedOrShiftLocked()) { if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT || keyCodes[0] > Character.MAX_CODE_POINT) { return; } - primaryCode = keyCodes[0]; - if (mKeyboardSwitcher.isAlphabetMode() && Character.isLowerCase(primaryCode)) { - int upperCaseCode = Character.toUpperCase(primaryCode); - if (upperCaseCode != primaryCode) { - primaryCode = upperCaseCode; + code = keyCodes[0]; + if (switcher.isAlphabetMode() && Character.isLowerCase(code)) { + int upperCaseCode = Character.toUpperCase(code); + if (upperCaseCode != code) { + code = upperCaseCode; } else { // Some keys, such as [eszett], have upper case as multi-characters. - String upperCase = new String(new int[] {primaryCode}, 0, 1).toUpperCase(); - onText(upperCase); + String upperCase = new String(new int[] {code}, 0, 1).toUpperCase(); + onTextInput(upperCase); return; } } } - if (mPredicting) { - if (mKeyboardSwitcher.getInputView().isShifted() - && mKeyboardSwitcher.isAlphabetMode() - && mComposing.length() == 0) { + if (mHasValidSuggestions) { + if (mComposing.length() == 0 && switcher.isAlphabetMode() + && switcher.isShiftedOrShiftLocked()) { mWord.setFirstCharCapitalized(true); } - mComposing.append((char) primaryCode); - mWord.add(primaryCode, keyCodes); + mComposing.append((char) code); + mWord.add(code, keyCodes); InputConnection ic = getCurrentInputConnection(); if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWord.size() == 1) { - mWord.setAutoCapitalized( - getCursorCapsMode(ic, getCurrentInputEditorInfo()) != 0); + mWord.setAutoCapitalized(getCurrentAutoCapsState()); } ic.setComposingText(mComposing, 1); } - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } else { - sendKeyChar((char)primaryCode); + sendKeyChar((char)code); } - updateShiftKeyState(getCurrentInputEditorInfo()); + switcher.updateShiftState(); if (LatinIME.PERF_DEBUG) measureCps(); - TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); + TextEntryState.typedCharacter((char) code, isWordSeparator(code)); } private void handleSeparator(int primaryCode) { - if (VOICE_INSTALLED && mVoiceInputHighlighted) { - commitVoiceInput(); - } - - if (mAfterVoiceInput){ - // Assume input length is 1. This assumption fails for smiley face insertions. - mVoiceInput.incrementTextModificationInsertPunctuationCount(1); - } + mVoiceConnector.handleSeparator(); // Should dismiss the "Touch again to save" message when handling separator if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) { - postUpdateSuggestions(); + mHandler.postUpdateSuggestions(); } boolean pickedDefault = false; // Handle separator - InputConnection ic = getCurrentInputConnection(); + final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); abortCorrection(false); } - if (mPredicting) { + if (mHasValidSuggestions) { // In certain languages where single quote is a separator, it's better // not to auto correct, but accept the typed word. For instance, // in Italian dov' should not be expanded to dove' because the elision // requires the last vowel to be removed. - if (mAutoCorrectOn && primaryCode != '\'' && - (mJustRevertedSeparator == null - || mJustRevertedSeparator.length() == 0 - || mJustRevertedSeparator.charAt(0) != primaryCode)) { + if (mAutoCorrectOn && primaryCode != '\'' && !mJustReverted) { pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". - if (primaryCode == KEYCODE_SPACE) { + if (primaryCode == Keyboard.CODE_SPACE) { mJustAddedAutoSpace = true; } } else { commitTyped(ic); } } - if (mJustAddedAutoSpace && primaryCode == KEYCODE_ENTER) { + if (mJustAddedAutoSpace && primaryCode == Keyboard.CODE_ENTER) { removeTrailingSpace(); mJustAddedAutoSpace = false; } @@ -1500,21 +1381,32 @@ public class LatinIME extends InputMethodService // Handle the case of ". ." -> " .." with auto-space if necessary // before changing the TextEntryState. if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED - && primaryCode == KEYCODE_PERIOD) { + && primaryCode == Keyboard.CODE_PERIOD) { reswapPeriodAndSpace(); } TextEntryState.typedCharacter((char) primaryCode, true); if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED - && primaryCode != KEYCODE_ENTER) { + && primaryCode != Keyboard.CODE_ENTER) { swapPunctuationAndSpace(); - } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) { + } else if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { doubleSpace(); } if (pickedDefault) { - TextEntryState.backToAcceptedDefault(mWord.getTypedWord()); + CharSequence typedWord = mWord.getTypedWord(); + TextEntryState.backToAcceptedDefault(typedWord); + if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) { + if (ic != null) { + CorrectionInfo correctionInfo = new CorrectionInfo( + mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); + ic.commitCorrection(correctionInfo); + } + if (mCandidateView != null) + mCandidateView.onAutoCorrectionInverted(mBestWord); + } + setPunctuationSuggestions(); } - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); if (ic != null) { ic.endBatchEdit(); } @@ -1522,16 +1414,11 @@ public class LatinIME extends InputMethodService private void handleClose() { commitTyped(getCurrentInputConnection()); - if (VOICE_INSTALLED & mRecognizing) { - mVoiceInput.cancel(); - } + mVoiceConnector.handleClose(); requestHideSelf(0); - if (mKeyboardSwitcher != null) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - if (inputView != null) { - inputView.closing(); - } - } + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) + inputView.closing(); TextEntryState.endSession(); } @@ -1552,272 +1439,116 @@ public class LatinIME extends InputMethodService mWordHistory.add(entry); } - private void postUpdateSuggestions() { - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); + private boolean isSuggestionsRequested() { + return mIsSettingsSuggestionStripOn + && (mCorrectionMode > 0 || isShowingSuggestionsStrip()); } - private void postUpdateOldSuggestions() { - mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300); + private boolean isShowingPunctuationList() { + return mSuggestPuncList == mCandidateView.getSuggestions(); } - private boolean isPredictionOn() { - return mPredictionOn; + private boolean isShowingSuggestionsStrip() { + return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE) + || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE + && mOrientation == Configuration.ORIENTATION_PORTRAIT); } private boolean isCandidateStripVisible() { - return isPredictionOn() && mShowSuggestions; - } - - public void onCancelVoice() { - if (mRecognizing) { - switchToKeyboardView(); - } - } - - private void switchToKeyboardView() { - mHandler.post(new Runnable() { - public void run() { - mRecognizing = false; - if (mKeyboardSwitcher.getInputView() != null) { - setInputView(mKeyboardSwitcher.getInputView()); - } - setCandidatesViewShown(true); - updateInputViewShown(); - postUpdateSuggestions(); - }}); + if (mCandidateView == null) + return false; + if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isCorrecting()) + return true; + if (!isShowingSuggestionsStrip()) + return false; + if (mApplicationSpecifiedCompletionOn) + return true; + return isSuggestionsRequested(); } - private void switchToRecognitionStatusView() { - final boolean configChanged = mConfigurationChanging; + public void switchToKeyboardView() { mHandler.post(new Runnable() { + @Override public void run() { - setCandidatesViewShown(false); - mRecognizing = true; - View v = mVoiceInput.getView(); - ViewParent p = v.getParent(); - if (p != null && p instanceof ViewGroup) { - ((ViewGroup)v.getParent()).removeView(v); + if (DEBUG) { + Log.d(TAG, "Switch to keyboard view."); } - setInputView(v); - updateInputViewShown(); - if (configChanged) { - mVoiceInput.onConfigurationChanged(); + View v = mKeyboardSwitcher.getInputView(); + if (v != null) { + // Confirms that the keyboard view doesn't have parent view. + ViewParent p = v.getParent(); + if (p != null && p instanceof ViewGroup) { + ((ViewGroup) p).removeView(v); + } + setInputView(v); } - }}); - } - - private void startListening(boolean swipe) { - if (!mHasUsedVoiceInput || - (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) { - // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel. - showVoiceWarningDialog(swipe); - } else { - reallyStartListening(swipe); - } - } - - private void reallyStartListening(boolean swipe) { - if (!mHasUsedVoiceInput) { - // The user has started a voice input, so remember that in the - // future (so we don't show the warning dialog after the first run). - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true); - SharedPreferencesCompat.apply(editor); - mHasUsedVoiceInput = true; - } - - if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) { - // The user has started a voice input from an unsupported locale, so remember that - // in the future (so we don't show the warning dialog the next time they do this). - SharedPreferences.Editor editor = - PreferenceManager.getDefaultSharedPreferences(this).edit(); - editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true); - SharedPreferencesCompat.apply(editor); - mHasUsedVoiceInputUnsupportedLocale = true; - } - - // Clear N-best suggestions - clearSuggestions(); - - FieldContext context = new FieldContext( - getCurrentInputConnection(), - getCurrentInputEditorInfo(), - mLanguageSwitcher.getInputLanguage(), - mLanguageSwitcher.getEnabledLanguages()); - mVoiceInput.startListening(context, swipe); - switchToRecognitionStatusView(); - } - - private void showVoiceWarningDialog(final boolean swipe) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setCancelable(true); - builder.setIcon(R.drawable.ic_mic_dialog); - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - mVoiceInput.logKeyboardWarningDialogOk(); - reallyStartListening(swipe); - } - }); - builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - mVoiceInput.logKeyboardWarningDialogCancel(); + setCandidatesViewShown(isCandidateStripVisible()); + updateInputViewShown(); + mHandler.postUpdateSuggestions(); } }); - - if (mLocaleSupportedForVoiceInput) { - String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_warning_how_to_turn_off); - builder.setMessage(message); - } else { - String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" + - getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_warning_how_to_turn_off); - builder.setMessage(message); - } - - builder.setTitle(R.string.voice_warning_title); - mVoiceWarningDialog = builder.create(); - - Window window = mVoiceWarningDialog.getWindow(); - WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); - lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; - window.setAttributes(lp); - window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); - mVoiceInput.logKeyboardWarningDialogShown(); - mVoiceWarningDialog.show(); } - public void onVoiceResults(List<String> candidates, - Map<String, List<CharSequence>> alternatives) { - if (!mRecognizing) { - return; - } - mVoiceResults.candidates = candidates; - mVoiceResults.alternatives = alternatives; - mHandler.sendMessage(mHandler.obtainMessage(MSG_VOICE_RESULTS)); + public void clearSuggestions() { + setSuggestions(SuggestedWords.EMPTY); } - private void handleVoiceResults() { - mAfterVoiceInput = true; - mImmediatelyAfterVoiceInput = true; - - InputConnection ic = getCurrentInputConnection(); - if (!isFullscreenMode()) { - // Start listening for updates to the text from typing, etc. - if (ic != null) { - ExtractedTextRequest req = new ExtractedTextRequest(); - ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR); - } - } - - vibrate(); - switchToKeyboardView(); - - final List<CharSequence> nBest = new ArrayList<CharSequence>(); - boolean capitalizeFirstWord = preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() - && mKeyboardSwitcher.getInputView().isShifted()); - for (String c : mVoiceResults.candidates) { - if (capitalizeFirstWord) { - c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); - } - nBest.add(c); - } - - if (nBest.size() == 0) { - return; - } - - String bestResult = nBest.get(0).toString(); - - mVoiceInput.logVoiceInputDelivered(bestResult.length()); - - mHints.registerVoiceResult(bestResult); - - if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text - - commitTyped(ic); - EditingUtil.appendText(ic, bestResult); - - if (ic != null) ic.endBatchEdit(); - - mVoiceInputHighlighted = true; - mWordToSuggestions.putAll(mVoiceResults.alternatives); - } - - private void clearSuggestions() { - setSuggestions(null, false, false, false); - } - - private void setSuggestions( - List<CharSequence> suggestions, - boolean completions, - boolean typedWordValid, - boolean haveMinimalSuggestion) { - - if (mIsShowingHint) { + public void setSuggestions(SuggestedWords words) { + if (mVoiceConnector.getAndResetIsShowingHint()) { setCandidatesView(mCandidateViewContainer); - mIsShowingHint = false; } if (mCandidateView != null) { - mCandidateView.setSuggestions( - suggestions, completions, typedWordValid, haveMinimalSuggestion); + mCandidateView.setSuggestions(words); + if (mCandidateView.isConfigCandidateHighlightFontColorEnabled()) { + mKeyboardSwitcher.onAutoCorrectionStateChanged( + words.hasWordAboveAutoCorrectionScoreThreshold()); + } } } - private void updateSuggestions() { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); + public void updateSuggestions() { + mKeyboardSwitcher.setPreferredLetters(null); // Check if we have a suggestion engine attached. - if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { + if ((mSuggest == null || !isSuggestionsRequested()) + && !mVoiceConnector.isVoiceInputHighlighted()) { return; } - if (!mPredicting) { - setNextSuggestions(); + if (!mHasValidSuggestions) { + setPunctuationSuggestions(); return; } showSuggestions(mWord); } - private List<CharSequence> getTypedSuggestions(WordComposer word) { - List<CharSequence> stringList = mSuggest.getSuggestions( - mKeyboardSwitcher.getInputView(), word, false, null); - return stringList; + private SuggestedWords.Builder getTypedSuggestions(WordComposer word) { + return mSuggest.getSuggestedWordBuilder(mKeyboardSwitcher.getInputView(), word, null); } private void showCorrections(WordAlternatives alternatives) { - List<CharSequence> stringList = alternatives.getAlternatives(); - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null); - showSuggestions(stringList, alternatives.getOriginalWord(), false, false); + mKeyboardSwitcher.setPreferredLetters(null); + SuggestedWords.Builder builder = alternatives.getAlternatives(); + builder.setTypedWordValid(false).setHasMinimalSuggestion(false); + showSuggestions(builder.build(), alternatives.getOriginalWord()); } private void showSuggestions(WordComposer word) { - // long startTime = System.currentTimeMillis(); // TIME MEASUREMENT! // TODO Maybe need better way of retrieving previous word - CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), + CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), mWordSeparators); - List<CharSequence> stringList = mSuggest.getSuggestions( - mKeyboardSwitcher.getInputView(), word, false, prevWord); - // long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT! - // Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime)); + SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder( + mKeyboardSwitcher.getInputView(), word, prevWord); int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); + mKeyboardSwitcher.setPreferredLetters(nextLettersFrequencies); - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters( - nextLettersFrequencies); - - boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection(); - //|| mCorrectionMode == mSuggest.CORRECTION_FULL; - CharSequence typedWord = word.getTypedWord(); + boolean correctionAvailable = !mInputTypeNoAutoCorrect && !mJustReverted + && mSuggest.hasAutoCorrection(); + final CharSequence typedWord = word.getTypedWord(); // If we're in basic correct - boolean typedWordValid = mSuggest.isValidWord(typedWord) || + final boolean typedWordValid = mSuggest.isValidWord(typedWord) || (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase())); if (mCorrectionMode == Suggest.CORRECTION_FULL @@ -1828,34 +1559,50 @@ public class LatinIME extends InputMethodService correctionAvailable &= !word.isMostlyCaps(); correctionAvailable &= !TextEntryState.isCorrecting(); - showSuggestions(stringList, typedWord, typedWordValid, correctionAvailable); + // Basically, we update the suggestion strip only when suggestion count > 1. However, + // there is an exception: We update the suggestion strip whenever typed word's length + // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, + // in most cases, suggestion count is 1 when typed word's length is 1, but we do always + // need to clear the previous state when the user starts typing a word (i.e. typed word's + // length == 1). + if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid + || mCandidateView.isShowingAddToDictionaryHint()) { + builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(correctionAvailable); + } else { + final SuggestedWords previousSuggestions = mCandidateView.getSuggestions(); + if (previousSuggestions == mSuggestPuncList) + return; + builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); + } + showSuggestions(builder.build(), typedWord); } - private void showSuggestions(List<CharSequence> stringList, CharSequence typedWord, - boolean typedWordValid, boolean correctionAvailable) { - setSuggestions(stringList, false, typedWordValid, correctionAvailable); - if (stringList.size() > 0) { - if (correctionAvailable && !typedWordValid && stringList.size() > 1) { - mBestWord = stringList.get(1); + private void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) { + setSuggestions(suggestedWords); + if (suggestedWords.size() > 0) { + if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords, mSuggest)) { + mBestWord = typedWord; + } else if (suggestedWords.hasAutoCorrectionWord()) { + mBestWord = suggestedWords.getWord(1); } else { mBestWord = typedWord; } } else { mBestWord = null; } - setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); + setCandidatesViewShown(isCandidateStripVisible()); } private boolean pickDefaultSuggestion() { // Complete any pending candidate query first - if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { - mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); + if (mHandler.hasPendingUpdateSuggestions()) { + mHandler.cancelUpdateSuggestions(); updateSuggestions(); } if (mBestWord != null && mBestWord.length() > 0) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); mJustAccepted = true; - pickSuggestion(mBestWord, false); + pickSuggestion(mBestWord); // Add the word to the auto dictionary if it's not a known word addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); return true; @@ -1865,24 +1612,17 @@ public class LatinIME extends InputMethodService } public void pickSuggestionManually(int index, CharSequence suggestion) { - List<CharSequence> suggestions = mCandidateView.getSuggestions(); - - if (mAfterVoiceInput && mShowingVoiceSuggestions) { - mVoiceInput.flushAllTextModificationCounters(); - // send this intent AFTER logging any prior aggregated edits. - mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index, - mWordSeparators, - getCurrentInputConnection()); - } + SuggestedWords suggestions = mCandidateView.getSuggestions(); + mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators); final boolean correcting = TextEntryState.isCorrecting(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); } - if (mCompletionOn && mCompletions != null && index >= 0 - && index < mCompletions.length) { - CompletionInfo ci = mCompletions[index]; + if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null + && index >= 0 && index < mApplicationSpecifiedCompletions.length) { + CompletionInfo ci = mApplicationSpecifiedCompletions[index]; if (ic != null) { ic.commitCompletion(ci); } @@ -1890,7 +1630,7 @@ public class LatinIME extends InputMethodService if (mCandidateView != null) { mCandidateView.clear(); } - updateShiftKeyState(getCurrentInputEditorInfo()); + mKeyboardSwitcher.updateShiftState(); if (ic != null) { ic.endBatchEdit(); } @@ -1903,17 +1643,18 @@ public class LatinIME extends InputMethodService // Word separators are suggested before the user inputs something. // So, LatinImeLogger logs "" as a user's input. LatinImeLogger.logOnManualSuggestion( - "", suggestion.toString(), index, suggestions); + "", suggestion.toString(), index, suggestions.mWords); final char primaryCode = suggestion.charAt(0); - onKey(primaryCode, new int[]{primaryCode}, LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE); + onCodeInput(primaryCode, new int[] { primaryCode }, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE); if (ic != null) { ic.endBatchEdit(); } return; } mJustAccepted = true; - pickSuggestion(suggestion, correcting); + pickSuggestion(suggestion); // Add the word to the auto dictionary if it's not a known word if (index == 0) { addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); @@ -1921,7 +1662,7 @@ public class LatinIME extends InputMethodService addToBigramDictionary(suggestion, 1); } LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), - index, suggestions); + index, suggestions.mWords); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space if (mAutoSpace && !correcting) { @@ -1937,13 +1678,13 @@ public class LatinIME extends InputMethodService // Fool the state watcher so that a subsequent backspace will not do a revert, unless // we just did a correction, in which case we need to stay in // TextEntryState.State.PICKED_SUGGESTION state. - TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); - setNextSuggestions(); + TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true); + setPunctuationSuggestions(); } else if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. clearSuggestions(); - postUpdateOldSuggestions(); + mHandler.postUpdateOldSuggestions(); } if (showingAddToDictionaryHint) { mCandidateView.showAddToDictionaryHint(suggestion); @@ -1953,91 +1694,25 @@ public class LatinIME extends InputMethodService } } - private void rememberReplacedWord(CharSequence suggestion) { - if (mShowingVoiceSuggestions) { - // Retain the replaced word in the alternatives array. - EditingUtil.Range range = new EditingUtil.Range(); - String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(), - mWordSeparators, range); - if (!mWordToSuggestions.containsKey(wordToBeReplaced)) { - wordToBeReplaced = wordToBeReplaced.toLowerCase(); - } - if (mWordToSuggestions.containsKey(wordToBeReplaced)) { - List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced); - if (suggestions.contains(suggestion)) { - suggestions.remove(suggestion); - } - suggestions.add(wordToBeReplaced); - mWordToSuggestions.remove(wordToBeReplaced); - mWordToSuggestions.put(suggestion.toString(), suggestions); - } - } - } - /** * Commits the chosen word to the text field and saves it for later * retrieval. * @param suggestion the suggestion picked by the user to be committed to * the text field - * @param correcting whether this is due to a correction of an existing - * word. */ - private void pickSuggestion(CharSequence suggestion, boolean correcting) { - LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); - if (mCapsLock) { - suggestion = suggestion.toString().toUpperCase(); - } else if (preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() - && inputView.isShifted())) { - suggestion = suggestion.toString().toUpperCase().charAt(0) - + suggestion.subSequence(1, suggestion.length()).toString(); - } + private void pickSuggestion(CharSequence suggestion) { + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (!switcher.isKeyboardAvailable()) + return; InputConnection ic = getCurrentInputConnection(); if (ic != null) { - rememberReplacedWord(suggestion); + mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators); ic.commitText(suggestion, 1); } saveWordInHistory(suggestion); - mPredicting = false; + mHasValidSuggestions = false; mCommittedLength = suggestion.length(); - ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); - // If we just corrected a word, then don't show punctuations - if (!correcting) { - setNextSuggestions(); - } - updateShiftKeyState(getCurrentInputEditorInfo()); - } - - /** - * Tries to apply any voice alternatives for the word if this was a spoken word and - * there are voice alternatives. - * @param touching The word that the cursor is touching, with position information - * @return true if an alternative was found, false otherwise. - */ - private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) { - // Search for result in spoken word alternatives - String selectedWord = touching.word.toString().trim(); - if (!mWordToSuggestions.containsKey(selectedWord)) { - selectedWord = selectedWord.toLowerCase(); - } - if (mWordToSuggestions.containsKey(selectedWord)) { - mShowingVoiceSuggestions = true; - List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord); - // If the first letter of touching is capitalized, make all the suggestions - // start with a capital letter. - if (Character.isUpperCase(touching.word.charAt(0))) { - for (int i = 0; i < suggestions.size(); i++) { - String origSugg = (String) suggestions.get(i); - String capsSugg = origSugg.toUpperCase().charAt(0) - + origSugg.subSequence(1, origSugg.length()).toString(); - suggestions.set(i, capsSugg); - } - } - setSuggestions(suggestions, false, true, true); - setCandidatesViewShown(true); - return true; - } - return false; + switcher.setPreferredLetters(null); } /** @@ -2046,12 +1721,12 @@ public class LatinIME extends InputMethodService * @param touching The word that the cursor is touching, with position information * @return true if an alternative was found, false otherwise. */ - private boolean applyTypedAlternatives(EditingUtil.SelectedWord touching) { + private boolean applyTypedAlternatives(EditingUtils.SelectedWord touching) { // If we didn't find a match, search for result in typed word history WordComposer foundWord = null; WordAlternatives alternatives = null; for (WordAlternatives entry : mWordHistory) { - if (TextUtils.equals(entry.getChosenWord(), touching.word)) { + if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) { if (entry instanceof TypedWordAlternatives) { foundWord = ((TypedWordAlternatives) entry).word; } @@ -2059,22 +1734,22 @@ public class LatinIME extends InputMethodService break; } } - // If we didn't find a match, at least suggest completions + // If we didn't find a match, at least suggest corrections. if (foundWord == null - && (mSuggest.isValidWord(touching.word) - || mSuggest.isValidWord(touching.word.toString().toLowerCase()))) { + && (mSuggest.isValidWord(touching.mWord) + || mSuggest.isValidWord(touching.mWord.toString().toLowerCase()))) { foundWord = new WordComposer(); - for (int i = 0; i < touching.word.length(); i++) { - foundWord.add(touching.word.charAt(i), new int[] { - touching.word.charAt(i) + for (int i = 0; i < touching.mWord.length(); i++) { + foundWord.add(touching.mWord.charAt(i), new int[] { + touching.mWord.charAt(i) }); } - foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.word.charAt(0))); + foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0))); } // Found a match, show suggestions if (foundWord != null || alternatives != null) { if (alternatives == null) { - alternatives = new TypedWordAlternatives(touching.word, foundWord); + alternatives = new TypedWordAlternatives(touching.mWord, foundWord); } showCorrections(alternatives); if (foundWord != null) { @@ -2088,39 +1763,41 @@ public class LatinIME extends InputMethodService } private void setOldSuggestions() { - mShowingVoiceSuggestions = false; + mVoiceConnector.setShowingVoiceSuggestions(false); if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) { return; } InputConnection ic = getCurrentInputConnection(); if (ic == null) return; - if (!mPredicting) { + if (!mHasValidSuggestions) { // Extract the selected or touching text - EditingUtil.SelectedWord touching = EditingUtil.getWordAtCursorOrSelection(ic, + EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic, mLastSelectionStart, mLastSelectionEnd, mWordSeparators); - if (touching != null && touching.word.length() > 1) { + if (touching != null && touching.mWord.length() > 1) { ic.beginBatchEdit(); - if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) { + if (!mVoiceConnector.applyVoiceAlternatives(touching) + && !applyTypedAlternatives(touching)) { abortCorrection(true); } else { TextEntryState.selectedForCorrection(); - EditingUtil.underlineWord(ic, touching); + EditingUtils.underlineWord(ic, touching); } ic.endBatchEdit(); } else { abortCorrection(true); - setNextSuggestions(); // Show the punctuation suggestions list + setPunctuationSuggestions(); // Show the punctuation suggestions list } } else { abortCorrection(true); } } - private void setNextSuggestions() { - setSuggestions(mSuggestPuncList, false, false, false); + private void setPunctuationSuggestions() { + setSuggestions(mSuggestPuncList); + setCandidatesViewShown(isCandidateStripVisible()); } private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { @@ -2145,19 +1822,17 @@ public class LatinIME extends InputMethodService || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { return; } - if (suggestion != null) { - if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) - || (!mSuggest.isValidWord(suggestion.toString()) - && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { - mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); - } + if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) + || (!mSuggest.isValidWord(suggestion.toString()) + && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { + mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); + } - if (mUserBigramDictionary != null) { - CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), - mSentenceSeparators); - if (!TextUtils.isEmpty(prevWord)) { - mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); - } + if (mUserBigramDictionary != null) { + CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(), + mSentenceSeparators); + if (!TextUtils.isEmpty(prevWord)) { + mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); } } } @@ -2187,24 +1862,37 @@ public class LatinIME extends InputMethodService public void revertLastWord(boolean deleteChar) { final int length = mComposing.length(); - if (!mPredicting && length > 0) { + if (!mHasValidSuggestions && length > 0) { final InputConnection ic = getCurrentInputConnection(); - mPredicting = true; - mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0); + mJustReverted = true; + final CharSequence punctuation = ic.getTextBeforeCursor(1, 0); if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; - CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); - if (toTheLeft != null && toTheLeft.length() > 0 - && isWordSeparator(toTheLeft.charAt(0))) { + final CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); + if (!TextUtils.isEmpty(toTheLeft) && isWordSeparator(toTheLeft.charAt(0))) { toDelete--; } ic.deleteSurroundingText(toDelete, 0); - ic.setComposingText(mComposing, 1); - TextEntryState.backspace(); - postUpdateSuggestions(); + // Re-insert punctuation only when the deleted character was word separator and the + // composing text wasn't equal to the auto-corrected text. + if (deleteChar + && !TextUtils.isEmpty(punctuation) && isWordSeparator(punctuation.charAt(0)) + && !TextUtils.equals(mComposing, toTheLeft)) { + ic.commitText(mComposing, 1); + TextEntryState.acceptedTyped(mComposing); + ic.commitText(punctuation, 1); + TextEntryState.typedCharacter(punctuation.charAt(0), true); + // Clear composing text + mComposing.setLength(0); + } else { + mHasValidSuggestions = true; + ic.setComposingText(mComposing, 1); + TextEntryState.backspace(); + } + mHandler.postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); - mJustRevertedSeparator = null; + mJustReverted = false; } } @@ -2222,130 +1910,97 @@ public class LatinIME extends InputMethodService } private void sendSpace() { - sendKeyChar((char)KEYCODE_SPACE); - updateShiftKeyState(getCurrentInputEditorInfo()); - //onKey(KEY_SPACE[0], KEY_SPACE); + sendKeyChar((char)Keyboard.CODE_SPACE); + mKeyboardSwitcher.updateShiftState(); } public boolean preferCapitalization() { return mWord.isFirstCharCapitalized(); } + // Notify that language or mode have been changed and toggleLanguage will update KeyboaredID + // according to new language or mode. + public void onRefreshKeyboard() { + toggleLanguage(true, true); + } + + // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER. private void toggleLanguage(boolean reset, boolean next) { - if (reset) { - mLanguageSwitcher.reset(); - } else { - if (next) { - mLanguageSwitcher.next(); - } else { - mLanguageSwitcher.prev(); - } + if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) { + mSubtypeSwitcher.toggleLanguage(reset, next); } - int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode(); - reloadKeyboards(); - mKeyboardSwitcher.makeKeyboards(true); - mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0, - mEnableVoiceButton && mEnableVoice); - initSuggest(mLanguageSwitcher.getInputLanguage()); - mLanguageSwitcher.persist(); - updateShiftKeyState(getCurrentInputEditorInfo()); + // Reload keyboard because the current language has been changed. + KeyboardSwitcher switcher = mKeyboardSwitcher; + final EditorInfo attribute = getCurrentInputEditorInfo(); + final int mode = initializeInputAttributesAndGetMode((attribute != null) + ? attribute.inputType : 0); + final int imeOptions = (attribute != null) ? attribute.imeOptions : 0; + switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(), + mVoiceConnector.isVoiceButtonOnPrimary()); + initSuggest(); + switcher.updateShiftState(); } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (PREF_SELECTED_LANGUAGES.equals(key)) { - mLanguageSwitcher.loadLocales(sharedPreferences); + mSubtypeSwitcher.onSharedPreferenceChanged(sharedPreferences, key); + if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { mRefreshKeyboardRequired = true; - } else if (PREF_RECORRECTION_ENABLED.equals(key)) { - mReCorrectionEnabled = sharedPreferences.getBoolean(PREF_RECORRECTION_ENABLED, - getResources().getBoolean(R.bool.default_recorrection_enabled)); + } else if (Settings.PREF_RECORRECTION_ENABLED.equals(key)) { + mReCorrectionEnabled = sharedPreferences.getBoolean( + Settings.PREF_RECORRECTION_ENABLED, + mResources.getBoolean(R.bool.default_recorrection_enabled)); } } - public void swipeRight() { - if (LatinKeyboardView.DEBUG_AUTO_PLAY) { - ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); - CharSequence text = cm.getText(); - if (!TextUtils.isEmpty(text)) { - mKeyboardSwitcher.getInputView().startPlaying(text.toString()); - } - } - } - - public void swipeLeft() { - } - - public void swipeDown() { - handleClose(); - } - - public void swipeUp() { - //launchSettings(); + @Override + public void onSwipeDown() { + if (mConfigSwipeDownDismissKeyboardEnabled) + handleClose(); } + @Override public void onPress(int primaryCode) { if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) { vibrate(); playKeyClick(primaryCode); } - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); - if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) { - mShiftKeyState.onPress(); - handleShift(); - } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) { - changeKeyboardMode(); - mSymbolKeyState.onPress(); - mKeyboardSwitcher.setAutoModeSwitchStateMomentary(); + KeyboardSwitcher switcher = mKeyboardSwitcher; + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); + if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { + switcher.onPressShift(); + } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { + switcher.onPressSymbol(); } else { - mShiftKeyState.onOtherKeyPressed(); - mSymbolKeyState.onOtherKeyPressed(); + switcher.onOtherKeyPressed(); } } + @Override public void onRelease(int primaryCode) { + KeyboardSwitcher switcher = mKeyboardSwitcher; // Reset any drag flags in the keyboard - ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased(); - //vibrate(); - final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch(); - if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_SHIFT) { - if (mShiftKeyState.isMomentary()) - resetShift(); - mShiftKeyState.onRelease(); - } else if (distinctMultiTouch && primaryCode == Keyboard.KEYCODE_MODE_CHANGE) { - // Snap back to the previous keyboard mode if the user chords the mode change key and - // other key, then released the mode change key. - if (mKeyboardSwitcher.isInChordingAutoModeSwitchState()) - changeKeyboardMode(); - mSymbolKeyState.onRelease(); + switcher.keyReleased(); + final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); + if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { + switcher.onReleaseShift(); + } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { + switcher.onReleaseSymbol(); } } - private FieldContext makeFieldContext() { - return new FieldContext( - getCurrentInputConnection(), - getCurrentInputEditorInfo(), - mLanguageSwitcher.getInputLanguage(), - mLanguageSwitcher.getEnabledLanguages()); - } - - private boolean fieldCanDoVoice(FieldContext fieldContext) { - return !mPasswordText - && mVoiceInput != null - && !mVoiceInput.isBlacklistedField(fieldContext); - } - - private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) { - return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) - && !(attribute != null - && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions)) - && SpeechRecognizer.isRecognitionAvailable(this); - } - // receive ringer mode changes to detect silent mode + // receive ringer mode change and network state change. private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - updateRingerMode(); + final String action = intent.getAction(); + if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) { + updateRingerMode(); + } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + mSubtypeSwitcher.onNetworkStateChanged(intent); + } } }; @@ -2372,13 +2027,13 @@ public class LatinIME extends InputMethodService // FIXME: These should be triggered after auto-repeat logic int sound = AudioManager.FX_KEYPRESS_STANDARD; switch (primaryCode) { - case Keyboard.KEYCODE_DELETE: + case Keyboard.CODE_DELETE: sound = AudioManager.FX_KEYPRESS_DELETE; break; - case KEYCODE_ENTER: + case Keyboard.CODE_ENTER: sound = AudioManager.FX_KEYPRESS_RETURN; break; - case KEYCODE_SPACE: + case Keyboard.CODE_SPACE: sound = AudioManager.FX_KEYPRESS_SPACEBAR; break; } @@ -2386,48 +2041,28 @@ public class LatinIME extends InputMethodService } } - private void vibrate() { + public void vibrate() { if (!mVibrateOn) { return; } - if (mKeyboardSwitcher.getInputView() != null) { - mKeyboardSwitcher.getInputView().performHapticFeedback( + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + if (inputView != null) { + inputView.performHapticFeedback( HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } - private void checkTutorial(String privateImeOptions) { - if (privateImeOptions == null) return; - if (privateImeOptions.equals("com.android.setupwizard:ShowTutorial")) { - if (mTutorial == null) startTutorial(); - } else if (privateImeOptions.equals("com.android.setupwizard:HideTutorial")) { - if (mTutorial != null) { - if (mTutorial.close()) { - mTutorial = null; - } - } - } - } - - private void startTutorial() { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_TUTORIAL), 500); - } - - /* package */ void tutorialDone() { - mTutorial = null; - } - - /* package */ void promoteToUserDictionary(String word, int frequency) { + public void promoteToUserDictionary(String word, int frequency) { if (mUserDictionary.isValidWord(word)) return; mUserDictionary.addWord(word, frequency); } - /* package */ WordComposer getCurrentWord() { + public WordComposer getCurrentWord() { return mWord; } - /* package */ boolean getPopupOn() { + public boolean getPopupOn() { return mPopupOn; } @@ -2445,22 +2080,34 @@ public class LatinIME extends InputMethodService } } - private void updateAutoTextEnabled(Locale systemLocale) { + private void updateAutoTextEnabled() { if (mSuggest == null) return; - boolean different = - !systemLocale.getLanguage().equalsIgnoreCase(mInputLocale.substring(0, 2)); - mSuggest.setAutoTextEnabled(!different && mQuickFixes); + mSuggest.setAutoTextEnabled(mQuickFixes + && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage()); + } + + private void updateSuggestionVisibility(SharedPreferences prefs) { + final Resources res = mResources; + final String suggestionVisiblityStr = prefs.getString( + Settings.PREF_SHOW_SUGGESTIONS_SETTING, + res.getString(R.string.prefs_suggestion_visibility_default_value)); + for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) { + if (suggestionVisiblityStr.equals(res.getString(visibility))) { + mSuggestionVisibility = visibility; + break; + } + } } protected void launchSettings() { - launchSettings(LatinIMESettings.class); + launchSettings(Settings.class); } public void launchDebugSettings() { - launchSettings(LatinIMEDebugSettings.class); + launchSettings(DebugSettings.class); } - protected void launchSettings (Class<? extends PreferenceActivity> settingsClass) { + protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); @@ -2468,97 +2115,176 @@ public class LatinIME extends InputMethodService startActivity(intent); } - private void loadSettings() { + private void loadSettings(EditorInfo attribute) { // Get the settings preferences - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false); - mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); - mPopupOn = sp.getBoolean(PREF_POPUP_ON, - mResources.getBoolean(R.bool.default_popup_preview)); - mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); - mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); - mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); - mHasUsedVoiceInputUnsupportedLocale = - sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false); - - // Get the current list of supported locales and check the current locale against that - // list. We cache this value so as not to check it every time the user starts a voice - // input. Because this method is called by onStartInputView, this should mean that as - // long as the locale doesn't change while the user is keeping the IME open, the - // value should never be stale. - String supportedLocalesString = SettingsUtil.getSettingsString( - getContentResolver(), - SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, - DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); - ArrayList<String> voiceInputSupportedLocales = - newArrayList(supportedLocalesString.split("\\s+")); - - mLocaleSupportedForVoiceInput = voiceInputSupportedLocales.contains(mInputLocale); - - mShowSuggestions = sp.getBoolean(PREF_SHOW_SUGGESTIONS, true); - - if (VOICE_INSTALLED) { - final String voiceMode = sp.getString(PREF_VOICE_MODE, - getString(R.string.voice_mode_main)); - boolean enableVoice = !voiceMode.equals(getString(R.string.voice_mode_off)) - && mEnableVoiceButton; - boolean voiceOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main)); - if (mKeyboardSwitcher != null && - (enableVoice != mEnableVoice || voiceOnPrimary != mVoiceOnPrimary)) { - mKeyboardSwitcher.setVoiceMode(enableVoice, voiceOnPrimary); + final SharedPreferences prefs = mPrefs; + Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); + mVibrateOn = vibrator != null && vibrator.hasVibrator() + && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false); + mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, false); + mPopupOn = prefs.getBoolean(Settings.PREF_POPUP_ON, + mResources.getBoolean(R.bool.config_default_popup_preview)); + mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true); + mQuickFixes = isQuickFixesEnabled(prefs); + + mAutoCorrectEnabled = isAutoCorrectEnabled(prefs); + mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(prefs); + loadAndSetAutoCorrectionThreshold(prefs); + + mVoiceConnector.loadSettings(attribute, prefs); + + updateCorrectionMode(); + updateAutoTextEnabled(); + updateSuggestionVisibility(prefs); + SubtypeSwitcher.getInstance().loadSettings(); + } + + /** + * Load Auto correction threshold from SharedPreferences, and modify mSuggest's threshold. + */ + private void loadAndSetAutoCorrectionThreshold(SharedPreferences sp) { + // When mSuggest is not initialized, cannnot modify mSuggest's threshold. + if (mSuggest == null) return; + // When auto correction setting is turned off, the threshold is ignored. + if (!isAutoCorrectEnabled(sp)) return; + + final String currentAutoCorrectionSetting = sp.getString( + Settings.PREF_AUTO_CORRECTION_THRESHOLD, + mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); + final String[] autoCorrectionThresholdValues = mResources.getStringArray( + R.array.auto_correction_threshold_values); + // When autoCrrectionThreshold is greater than 1.0, auto correction is virtually turned off. + double autoCorrectionThreshold = Double.MAX_VALUE; + try { + final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); + if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { + autoCorrectionThreshold = Double.parseDouble( + autoCorrectionThresholdValues[arrayIndex]); } - mEnableVoice = enableVoice; - mVoiceOnPrimary = voiceOnPrimary; + } catch (NumberFormatException e) { + // Whenever the threshold settings are correct, never come here. + autoCorrectionThreshold = Double.MAX_VALUE; + Log.w(TAG, "Cannot load auto correction threshold setting." + + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting + + ", autoCorrectionThresholdValues: " + + Arrays.toString(autoCorrectionThresholdValues)); } - mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, - mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; - //mBigramSuggestionEnabled = sp.getBoolean( - // PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions; - updateCorrectionMode(); - updateAutoTextEnabled(mResources.getConfiguration().locale); - mLanguageSwitcher.loadLocales(sp); + // TODO: This should be refactored : + // setAutoCorrectionThreshold should be called outside of this method. + mSuggest.setAutoCorrectionThreshold(autoCorrectionThreshold); + } + + private boolean isQuickFixesEnabled(SharedPreferences sp) { + final boolean showQuickFixesOption = mResources.getBoolean( + R.bool.config_enable_quick_fixes_option); + if (!showQuickFixesOption) { + return isAutoCorrectEnabled(sp); + } + return sp.getBoolean(Settings.PREF_QUICK_FIXES, mResources.getBoolean( + R.bool.config_default_quick_fixes)); + } + + private boolean isAutoCorrectEnabled(SharedPreferences sp) { + final String currentAutoCorrectionSetting = sp.getString( + Settings.PREF_AUTO_CORRECTION_THRESHOLD, + mResources.getString(R.string.auto_correction_threshold_mode_index_modest)); + final String autoCorrectionOff = mResources.getString( + R.string.auto_correction_threshold_mode_index_off); + return !currentAutoCorrectionSetting.equals(autoCorrectionOff); + } + + private boolean isBigramSuggestionEnabled(SharedPreferences sp) { + final boolean showBigramSuggestionsOption = mResources.getBoolean( + R.bool.config_enable_bigram_suggestions_option); + if (!showBigramSuggestionsOption) { + return isAutoCorrectEnabled(sp); + } + return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, mResources.getBoolean( + R.bool.config_default_bigram_suggestions)); } private void initSuggestPuncList() { - mSuggestPuncList = new ArrayList<CharSequence>(); - 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)); + if (mSuggestPuncs != null || mSuggestPuncList != null) + return; + SuggestedWords.Builder builder = new SuggestedWords.Builder(); + String puncs = mResources.getString(R.string.suggested_punctuations); + if (puncs != null) { + for (int i = 0; i < puncs.length(); i++) { + builder.addWord(puncs.subSequence(i, i + 1)); } } + mSuggestPuncList = builder.build(); + mSuggestPuncs = puncs; } 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); - builder.setIcon(R.drawable.ic_dialog_keyboard); - builder.setNegativeButton(android.R.string.cancel, null); - CharSequence itemSettings = getString(R.string.english_ime_settings); - CharSequence itemInputMethod = getString(R.string.selectInputMethod); - builder.setItems(new CharSequence[] { - itemInputMethod, itemSettings}, - new DialogInterface.OnClickListener() { + private void showSubtypeSelectorAndSettings() { + final CharSequence title = getString(R.string.english_ime_input_options); + final CharSequence[] items = new CharSequence[] { + // TODO: Should use new string "Select active input modes". + getString(R.string.language_selection_title), + getString(R.string.english_ime_settings), + }; + final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface di, int position) { + di.dismiss(); + switch (position) { + case 0: + Intent intent = new Intent( + android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(android.provider.Settings.EXTRA_INPUT_METHOD_ID, + mInputMethodId); + startActivity(intent); + break; + case 1: + launchSettings(); + break; + } + } + }; + showOptionsMenuInternal(title, items, listener); + } + private void showOptionsMenu() { + final CharSequence title = getString(R.string.english_ime_input_options); + final CharSequence[] items = new CharSequence[] { + getString(R.string.selectInputMethod), + getString(R.string.english_ime_settings), + }; + final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface di, int position) { di.dismiss(); switch (position) { - case POS_SETTINGS: - launchSettings(); - break; - case POS_METHOD: - ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) - .showInputMethodPicker(); - break; + case 0: + launchSettings(); + break; + case 1: + mImm.showInputMethodPicker(); + break; } } - }); - builder.setTitle(mResources.getString(R.string.english_ime_input_options)); + }; + showOptionsMenuInternal(title, items, listener); + } + + private void showOptionsMenuInternal(CharSequence title, CharSequence[] items, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + builder.setIcon(R.drawable.ic_dialog_keyboard); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setItems(items, listener); + builder.setTitle(title); mOptionsDialog = builder.create(); + mOptionsDialog.setCanceledOnTouchOutside(true); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); @@ -2568,22 +2294,6 @@ public class LatinIME extends InputMethodService mOptionsDialog.show(); } - public void changeKeyboardMode() { - mKeyboardSwitcher.toggleSymbols(); - if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { - mKeyboardSwitcher.setShiftLocked(mCapsLock); - } - - updateShiftKeyState(getCurrentInputEditorInfo()); - } - - public static <E> ArrayList<E> newArrayList(E... elements) { - int capacity = (elements.length * 110) / 100 + 5; - ArrayList<E> list = new ArrayList<E>(capacity); - Collections.addAll(list, elements); - return list; - } - @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { super.dump(fd, fout, args); @@ -2591,14 +2301,13 @@ public class LatinIME extends InputMethodService final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); - p.println(" mCapsLock=" + mCapsLock); p.println(" mComposing=" + mComposing.toString()); - p.println(" mPredictionOn=" + mPredictionOn); + p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn); p.println(" mCorrectionMode=" + mCorrectionMode); - p.println(" mPredicting=" + mPredicting); + p.println(" mHasValidSuggestions=" + mHasValidSuggestions); p.println(" mAutoCorrectOn=" + mAutoCorrectOn); p.println(" mAutoSpace=" + mAutoSpace); - p.println(" mCompletionOn=" + mCompletionOn); + p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn); p.println(" TextEntryState.state=" + TextEntryState.getState()); p.println(" mSoundOn=" + mSoundOn); p.println(" mVibrateOn=" + mVibrateOn); @@ -2623,7 +2332,8 @@ public class LatinIME extends InputMethodService System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); } - public void onAutoCompletionStateChanged(boolean isAutoCompletion) { - mKeyboardSwitcher.onAutoCompletionStateChanged(isAutoCompletion); + @Override + public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) { + SubtypeSwitcher.getInstance().updateSubtype(subtype); } } diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java deleted file mode 100644 index ffff33da2..000000000 --- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import java.util.ArrayList; -import java.util.Locale; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.backup.BackupManager; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceGroup; -import android.speech.SpeechRecognizer; -import android.text.AutoText; -import android.util.Log; - -import com.android.inputmethod.voice.SettingsUtil; -import com.android.inputmethod.voice.VoiceInputLogger; - -public class LatinIMESettings extends PreferenceActivity - implements SharedPreferences.OnSharedPreferenceChangeListener, - DialogInterface.OnDismissListener { - - private static final String QUICK_FIXES_KEY = "quick_fixes"; - private static final String PREDICTION_SETTINGS_KEY = "prediction_settings"; - private static final String VOICE_SETTINGS_KEY = "voice_mode"; - /* package */ static final String PREF_SETTINGS_KEY = "settings_key"; - - private static final String TAG = "LatinIMESettings"; - - // Dialog ids - private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; - - private CheckBoxPreference mQuickFixes; - private ListPreference mVoicePreference; - private ListPreference mSettingsKeyPreference; - private boolean mVoiceOn; - - private VoiceInputLogger mLogger; - - private boolean mOkClicked = false; - private String mVoiceModeOff; - - @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.prefs); - mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY); - mVoicePreference = (ListPreference) findPreference(VOICE_SETTINGS_KEY); - mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY); - SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - prefs.registerOnSharedPreferenceChangeListener(this); - - mVoiceModeOff = getString(R.string.voice_mode_off); - mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); - mLogger = VoiceInputLogger.getLogger(this); - } - - @Override - protected void onResume() { - super.onResume(); - int autoTextSize = AutoText.getSize(getListView()); - if (autoTextSize < 1) { - ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY)) - .removePreference(mQuickFixes); - } - if (!LatinIME.VOICE_INSTALLED - || !SpeechRecognizer.isRecognitionAvailable(this)) { - getPreferenceScreen().removePreference(mVoicePreference); - } else { - updateVoiceModeSummary(); - } - updateSettingsKeySummary(); - } - - @Override - protected void onDestroy() { - getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( - this); - super.onDestroy(); - } - - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - (new BackupManager(this)).dataChanged(); - // If turning on voice input, show dialog - if (key.equals(VOICE_SETTINGS_KEY) && !mVoiceOn) { - if (!prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff) - .equals(mVoiceModeOff)) { - showVoiceConfirmation(); - } - } - mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); - updateVoiceModeSummary(); - updateSettingsKeySummary(); - } - - private void updateSettingsKeySummary() { - mSettingsKeyPreference.setSummary( - getResources().getStringArray(R.array.settings_key_modes) - [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]); - } - - private void showVoiceConfirmation() { - mOkClicked = false; - showDialog(VOICE_INPUT_CONFIRM_DIALOG); - } - - private void updateVoiceModeSummary() { - mVoicePreference.setSummary( - getResources().getStringArray(R.array.voice_input_modes_summary) - [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]); - } - - @Override - protected Dialog onCreateDialog(int id) { - switch (id) { - case VOICE_INPUT_CONFIRM_DIALOG: - DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - if (whichButton == DialogInterface.BUTTON_NEGATIVE) { - mVoicePreference.setValue(mVoiceModeOff); - mLogger.settingsWarningDialogCancel(); - } else if (whichButton == DialogInterface.BUTTON_POSITIVE) { - mOkClicked = true; - mLogger.settingsWarningDialogOk(); - } - updateVoicePreference(); - } - }; - AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setTitle(R.string.voice_warning_title) - .setPositiveButton(android.R.string.ok, listener) - .setNegativeButton(android.R.string.cancel, listener); - - // Get the current list of supported locales and check the current locale against - // that list, to decide whether to put a warning that voice input will not work in - // the current language as part of the pop-up confirmation dialog. - String supportedLocalesString = SettingsUtil.getSettingsString( - getContentResolver(), - SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, - LatinIME.DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); - ArrayList<String> voiceInputSupportedLocales = - LatinIME.newArrayList(supportedLocalesString.split("\\s+")); - boolean localeSupported = voiceInputSupportedLocales.contains( - Locale.getDefault().toString()); - - if (localeSupported) { - String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_hint_dialog_message); - builder.setMessage(message); - } else { - String message = getString(R.string.voice_warning_locale_not_supported) + - "\n\n" + getString(R.string.voice_warning_may_not_understand) + "\n\n" + - getString(R.string.voice_hint_dialog_message); - builder.setMessage(message); - } - - AlertDialog dialog = builder.create(); - dialog.setOnDismissListener(this); - mLogger.settingsWarningDialogShown(); - return dialog; - default: - Log.e(TAG, "unknown dialog " + id); - return null; - } - } - - public void onDismiss(DialogInterface dialog) { - mLogger.settingsWarningDialogDismissed(); - if (!mOkClicked) { - // This assumes that onPreferenceClick gets called first, and this if the user - // agreed after the warning, we set the mOkClicked value to true. - mVoicePreference.setValue(mVoiceModeOff); - } - } - - private void updateVoicePreference() { - boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff); - if (isChecked) { - mLogger.voiceInputSettingEnabled(); - } else { - mLogger.voiceInputSettingDisabled(); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java deleted file mode 100644 index 85ecaee50..000000000 --- a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.view.inputmethod.InputMethodManager; - -import android.content.Context; -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 (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; - } - } - } - } - - public static boolean hasMultipleEnabledIMEs(Context context) { - return ((InputMethodManager) context.getSystemService( - Context.INPUT_METHOD_SERVICE)).getEnabledInputMethodList().size() > 1; - } - - /* package */ static class RingCharBuffer { - private static RingCharBuffer sRingCharBuffer = new RingCharBuffer(); - private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; - private static final int INVALID_COORDINATE = -2; - /* package */ static final int BUFSIZE = 20; - private Context mContext; - private boolean mEnabled = false; - private int mEnd = 0; - /* package */ int mLength = 0; - private char[] mCharBuf = new char[BUFSIZE]; - private int[] mXBuf = new int[BUFSIZE]; - private int[] mYBuf = new int[BUFSIZE]; - - private RingCharBuffer() { - } - public static RingCharBuffer getInstance() { - return sRingCharBuffer; - } - public static RingCharBuffer init(Context context, boolean enabled) { - sRingCharBuffer.mContext = context; - sRingCharBuffer.mEnabled = enabled; - return sRingCharBuffer; - } - private int normalize(int in) { - int ret = in % BUFSIZE; - return ret < 0 ? ret + BUFSIZE : ret; - } - public void push(char c, int x, int y) { - if (!mEnabled) return; - mCharBuf[mEnd] = c; - mXBuf[mEnd] = x; - mYBuf[mEnd] = y; - mEnd = normalize(mEnd + 1); - if (mLength < BUFSIZE) { - ++mLength; - } - } - public char pop() { - if (mLength < 1) { - return PLACEHOLDER_DELIMITER_CHAR; - } else { - mEnd = normalize(mEnd - 1); - --mLength; - return mCharBuf[mEnd]; - } - } - public char getLastChar() { - if (mLength < 1) { - return PLACEHOLDER_DELIMITER_CHAR; - } else { - return mCharBuf[normalize(mEnd - 1)]; - } - } - public int getPreviousX(char c, int back) { - int index = normalize(mEnd - 2 - back); - if (mLength <= back - || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { - return INVALID_COORDINATE; - } else { - return mXBuf[index]; - } - } - public int getPreviousY(char c, int back) { - int index = normalize(mEnd - 2 - back); - if (mLength <= back - || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { - return INVALID_COORDINATE; - } else { - return mYBuf[index]; - } - } - public String getLastString() { - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < mLength; ++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() { - mLength = 0; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index a8ab9cc98..aaecfffdd 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -16,19 +16,23 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.Dictionary.DataType; import android.content.Context; import android.content.SharedPreferences; -import android.inputmethodservice.Keyboard; + import java.util.List; public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener { + public static boolean sDBG = false; + + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { } - public static void init(Context context) { + public static void init(Context context, SharedPreferences prefs) { } public static void commit() { @@ -68,4 +72,6 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang public static void onSetKeyboard(Keyboard kb) { } + public static void onPrintAllUsabilityStudyLogs() { + } } diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java deleted file mode 100644 index fbba55bad..000000000 --- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java +++ /dev/null @@ -1,1023 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.inputmethodservice.Keyboard; -import android.text.TextPaint; -import android.util.Log; -import android.view.ViewConfiguration; -import android.view.inputmethod.EditorInfo; - -import java.util.List; -import java.util.Locale; - -public class LatinKeyboard extends Keyboard { - - private static final boolean DEBUG_PREFERRED_LETTER = false; - private static final String TAG = "LatinKeyboard"; - private static final int OPACITY_FULLY_OPAQUE = 255; - private static final int SPACE_LED_LENGTH_PERCENT = 80; - - private Drawable mShiftLockIcon; - private Drawable mShiftLockPreviewIcon; - private Drawable mOldShiftIcon; - private Drawable mSpaceIcon; - private Drawable mSpaceAutoCompletionIndicator; - private Drawable mSpacePreviewIcon; - private Drawable mMicIcon; - private Drawable mMicPreviewIcon; - private Drawable m123MicIcon; - private Drawable m123MicPreviewIcon; - private final Drawable mButtonArrowLeftIcon; - private final Drawable mButtonArrowRightIcon; - private Key mShiftKey; - private Key mEnterKey; - private Key mF1Key; - private final Drawable mHintIcon; - private Key mSpaceKey; - private Key m123Key; - private final int NUMBER_HINT_COUNT = 10; - private Key[] mNumberHintKeys; - private Drawable[] mNumberHintIcons = new Drawable[NUMBER_HINT_COUNT]; - private final int[] mSpaceKeyIndexArray; - private int mSpaceDragStartX; - private int mSpaceDragLastDiff; - private Locale mLocale; - private LanguageSwitcher mLanguageSwitcher; - private final Resources mRes; - private final Context mContext; - private int mMode; - // Whether this keyboard has voice icon on it - private boolean mHasVoiceButton; - // Whether voice icon is enabled at all - private boolean mVoiceEnabled; - private final boolean mIsAlphaKeyboard; - private CharSequence m123Label; - private boolean mCurrentlyInSpace; - private SlidingLocaleDrawable mSlidingLocaleIcon; - private int[] mPrefLetterFrequencies; - private int mPrefLetter; - private int mPrefLetterX; - private int mPrefLetterY; - private int mPrefDistance; - - // TODO: generalize for any keyboardId - private boolean mIsBlackSym; - - // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes - // non-private. - private final int mVerticalGap; - - private static final int SHIFT_OFF = 0; - private static final int SHIFT_ON = 1; - private static final int SHIFT_LOCKED = 2; - - private int mShiftState = SHIFT_OFF; - - private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; - private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; - private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f; - // Minimum width of space key preview (proportional to keyboard width) - private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f; - // Height in space key the language name will be drawn. (proportional to space key height) - private static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f; - // If the full language name needs to be smaller than this value to be drawn on space key, - // its short language name will be used instead. - private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f; - - private static int sSpacebarVerticalCorrection; - - public LatinKeyboard(Context context, int xmlLayoutResId) { - this(context, xmlLayoutResId, 0); - } - - public LatinKeyboard(Context context, int xmlLayoutResId, int mode) { - super(context, xmlLayoutResId, mode); - final Resources res = context.getResources(); - mContext = context; - mMode = mode; - mRes = res; - mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); - mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); - setDefaultBounds(mShiftLockPreviewIcon); - mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); - mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led); - mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space); - mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic); - mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic); - setDefaultBounds(mMicPreviewIcon); - mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); - mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); - m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic); - m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic); - mHintIcon = res.getDrawable(R.drawable.hint_popup); - setDefaultBounds(m123MicPreviewIcon); - sSpacebarVerticalCorrection = res.getDimensionPixelOffset( - R.dimen.spacebar_vertical_correction); - mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty - || xmlLayoutResId == R.xml.kbd_qwerty_black; - // The index of space key is available only after Keyboard constructor has finished. - mSpaceKeyIndexArray = new int[] { indexOf(LatinIME.KEYCODE_SPACE) }; - initializeNumberHintResources(context); - // TODO remove this initialization after cleanup - mVerticalGap = super.getVerticalGap(); - } - - private void initializeNumberHintResources(Context context) { - final Resources res = context.getResources(); - mNumberHintIcons[0] = res.getDrawable(R.drawable.keyboard_hint_0); - mNumberHintIcons[1] = res.getDrawable(R.drawable.keyboard_hint_1); - mNumberHintIcons[2] = res.getDrawable(R.drawable.keyboard_hint_2); - mNumberHintIcons[3] = res.getDrawable(R.drawable.keyboard_hint_3); - mNumberHintIcons[4] = res.getDrawable(R.drawable.keyboard_hint_4); - mNumberHintIcons[5] = res.getDrawable(R.drawable.keyboard_hint_5); - mNumberHintIcons[6] = res.getDrawable(R.drawable.keyboard_hint_6); - mNumberHintIcons[7] = res.getDrawable(R.drawable.keyboard_hint_7); - mNumberHintIcons[8] = res.getDrawable(R.drawable.keyboard_hint_8); - mNumberHintIcons[9] = res.getDrawable(R.drawable.keyboard_hint_9); - } - - @Override - protected Key createKeyFromXml(Resources res, Row parent, int x, int y, - XmlResourceParser parser) { - Key key = new LatinKey(res, parent, x, y, parser); - switch (key.codes[0]) { - case LatinIME.KEYCODE_ENTER: - mEnterKey = key; - break; - case LatinKeyboardView.KEYCODE_F1: - mF1Key = key; - break; - case LatinIME.KEYCODE_SPACE: - mSpaceKey = key; - break; - case KEYCODE_MODE_CHANGE: - m123Key = key; - m123Label = key.label; - break; - } - - // For number hints on the upper-right corner of key - if (mNumberHintKeys == null) { - // NOTE: This protected method is being called from the base class constructor before - // mNumberHintKeys gets initialized. - mNumberHintKeys = new Key[NUMBER_HINT_COUNT]; - } - int hintNumber = -1; - if (LatinKeyboardBaseView.isNumberAtLeftmostPopupChar(key)) { - hintNumber = key.popupCharacters.charAt(0) - '0'; - } else if (LatinKeyboardBaseView.isNumberAtRightmostPopupChar(key)) { - hintNumber = key.popupCharacters.charAt(key.popupCharacters.length() - 1) - '0'; - } - if (hintNumber >= 0 && hintNumber <= 9) { - mNumberHintKeys[hintNumber] = key; - } - - return key; - } - - void setImeOptions(Resources res, int mode, int options) { - mMode = mode; - // TODO should clean up this method - if (mEnterKey != null) { - // Reset some of the rarely used attributes. - mEnterKey.popupCharacters = null; - mEnterKey.popupResId = 0; - mEnterKey.text = null; - switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { - case EditorInfo.IME_ACTION_GO: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_go_key); - break; - case EditorInfo.IME_ACTION_NEXT: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_next_key); - break; - case EditorInfo.IME_ACTION_DONE: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_done_key); - break; - case EditorInfo.IME_ACTION_SEARCH: - mEnterKey.iconPreview = res.getDrawable( - R.drawable.sym_keyboard_feedback_search); - mEnterKey.icon = res.getDrawable(mIsBlackSym ? - R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search); - mEnterKey.label = null; - break; - case EditorInfo.IME_ACTION_SEND: - mEnterKey.iconPreview = null; - mEnterKey.icon = null; - mEnterKey.label = res.getText(R.string.label_send_key); - break; - default: - if (mode == KeyboardSwitcher.MODE_IM) { - mEnterKey.icon = mHintIcon; - mEnterKey.iconPreview = null; - mEnterKey.label = ":-)"; - mEnterKey.text = ":-) "; - mEnterKey.popupResId = R.xml.popup_smileys; - } else { - mEnterKey.iconPreview = res.getDrawable( - R.drawable.sym_keyboard_feedback_return); - mEnterKey.icon = res.getDrawable(mIsBlackSym ? - R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return); - mEnterKey.label = null; - } - break; - } - // Set the initial size of the preview icon - if (mEnterKey.iconPreview != null) { - setDefaultBounds(mEnterKey.iconPreview); - } - } - } - - void enableShiftLock() { - int index = getShiftKeyIndex(); - if (index >= 0) { - mShiftKey = getKeys().get(index); - if (mShiftKey instanceof LatinKey) { - ((LatinKey)mShiftKey).enableShiftLock(); - } - mOldShiftIcon = mShiftKey.icon; - } - } - - void setShiftLocked(boolean shiftLocked) { - if (mShiftKey != null) { - if (shiftLocked) { - mShiftKey.on = true; - mShiftKey.icon = mShiftLockIcon; - mShiftState = SHIFT_LOCKED; - } else { - mShiftKey.on = false; - mShiftKey.icon = mShiftLockIcon; - mShiftState = SHIFT_ON; - } - } - } - - boolean isShiftLocked() { - return mShiftState == SHIFT_LOCKED; - } - - @Override - public boolean setShifted(boolean shiftState) { - boolean shiftChanged = false; - if (mShiftKey != null) { - if (shiftState == false) { - shiftChanged = mShiftState != SHIFT_OFF; - mShiftState = SHIFT_OFF; - mShiftKey.on = false; - mShiftKey.icon = mOldShiftIcon; - } else { - if (mShiftState == SHIFT_OFF) { - shiftChanged = mShiftState == SHIFT_OFF; - mShiftState = SHIFT_ON; - mShiftKey.icon = mShiftLockIcon; - } - } - } else { - return super.setShifted(shiftState); - } - return shiftChanged; - } - - @Override - public boolean isShifted() { - if (mShiftKey != null) { - return mShiftState != SHIFT_OFF; - } else { - return super.isShifted(); - } - } - - /* package */ boolean isAlphaKeyboard() { - return mIsAlphaKeyboard; - } - - public void setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack) { - mIsBlackSym = isBlack; - if (isBlack) { - mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked); - mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space); - mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic); - m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic); - } else { - mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked); - mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space); - mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic); - m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic); - } - updateDynamicKeys(); - if (mSpaceKey != null) { - updateSpaceBarForLocale(isAutoCompletion, isBlack); - } - updateNumberHintKeys(); - } - - private void setDefaultBounds(Drawable drawable) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - - public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) { - mHasVoiceButton = hasVoiceButton; - mVoiceEnabled = hasVoice; - updateDynamicKeys(); - } - - private void updateDynamicKeys() { - update123Key(); - updateF1Key(); - } - - private void update123Key() { - // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode. - if (m123Key != null && mIsAlphaKeyboard) { - if (mVoiceEnabled && !mHasVoiceButton) { - m123Key.icon = m123MicIcon; - m123Key.iconPreview = m123MicPreviewIcon; - m123Key.label = null; - } else { - m123Key.icon = null; - m123Key.iconPreview = null; - m123Key.label = m123Label; - } - } - } - - private void updateF1Key() { - // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key. - if (mF1Key == null) - return; - - if (mIsAlphaKeyboard) { - if (mMode == KeyboardSwitcher.MODE_URL) { - setNonMicF1Key(mF1Key, "/", R.xml.popup_slash); - } else if (mMode == KeyboardSwitcher.MODE_EMAIL) { - setNonMicF1Key(mF1Key, "@", R.xml.popup_at); - } else { - if (mVoiceEnabled && mHasVoiceButton) { - setMicF1Key(mF1Key); - } else { - setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); - } - } - } else { // Symbols keyboard - if (mVoiceEnabled && mHasVoiceButton) { - setMicF1Key(mF1Key); - } else { - setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); - } - } - } - - private void setMicF1Key(Key key) { - // HACK: draw mMicIcon and mHintIcon at the same time - final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes, - drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon)); - - key.label = null; - key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE }; - key.popupResId = R.xml.popup_mic; - key.icon = micWithSettingsHintDrawable; - key.iconPreview = mMicPreviewIcon; - } - - private void setNonMicF1Key(Key key, String label, int popupResId) { - key.label = label; - key.codes = new int[] { label.charAt(0) }; - key.popupResId = popupResId; - key.icon = mHintIcon; - key.iconPreview = null; - } - - public boolean isF1Key(Key key) { - return key == mF1Key; - } - - public static boolean hasPuncOrSmileysPopup(Key key) { - return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys; - } - - /** - * @return a key which should be invalidated. - */ - public Key onAutoCompletionStateChanged(boolean isAutoCompletion) { - updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym); - return mSpaceKey; - } - - private void updateNumberHintKeys() { - for (int i = 0; i < mNumberHintKeys.length; ++i) { - if (mNumberHintKeys[i] != null) { - mNumberHintKeys[i].icon = mNumberHintIcons[i]; - } - } - } - - public boolean isLanguageSwitchEnabled() { - return mLocale != null; - } - - private void updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack) { - // If application locales are explicitly selected. - if (mLocale != null) { - mSpaceKey.icon = new BitmapDrawable(mRes, - drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); - } else { - // sym_keyboard_space_led can be shared with Black and White symbol themes. - if (isAutoCompletion) { - mSpaceKey.icon = new BitmapDrawable(mRes, - drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); - } else { - mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space) - : mRes.getDrawable(R.drawable.sym_keyboard_space); - } - } - } - - // Compute width of text with specified text size using paint. - private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) { - paint.setTextSize(textSize); - paint.getTextBounds(text, 0, text.length(), bounds); - return bounds.width(); - } - - // Overlay two images: mainIcon and hintIcon. - private Bitmap drawSynthesizedSettingsHintImage( - int width, int height, Drawable mainIcon, Drawable hintIcon) { - if (mainIcon == null || hintIcon == null) - return null; - Rect hintIconPadding = new Rect(0, 0, 0, 0); - hintIcon.getPadding(hintIconPadding); - final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(buffer); - canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); - - // Draw main icon at the center of the key visual - // Assuming the hintIcon shares the same padding with the key's background drawable - final int drawableX = (width + hintIconPadding.left - hintIconPadding.right - - mainIcon.getIntrinsicWidth()) / 2; - final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom - - mainIcon.getIntrinsicHeight()) / 2; - setDefaultBounds(mainIcon); - canvas.translate(drawableX, drawableY); - mainIcon.draw(canvas); - canvas.translate(-drawableX, -drawableY); - - // Draw hint icon fully in the key - hintIcon.setBounds(0, 0, width, height); - hintIcon.draw(canvas); - return buffer; - } - - // Layout local language name and left and right arrow on space bar. - private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow, - Drawable rArrow, int width, int height, float origTextSize, - boolean allowVariableTextSize) { - final float arrowWidth = lArrow.getIntrinsicWidth(); - final float arrowHeight = lArrow.getIntrinsicHeight(); - final float maxTextWidth = width - (arrowWidth + arrowWidth); - final Rect bounds = new Rect(); - - // Estimate appropriate language name text size to fit in maxTextWidth. - String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); - int textWidth = getTextWidth(paint, language, origTextSize, bounds); - // Assuming text width and text size are proportional to each other. - float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); - - final boolean useShortName; - if (allowVariableTextSize) { - textWidth = getTextWidth(paint, language, textSize, bounds); - // If text size goes too small or text does not fit, use short name - useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME - || textWidth > maxTextWidth; - } else { - useShortName = textWidth > maxTextWidth; - textSize = origTextSize; - } - if (useShortName) { - language = LanguageSwitcher.toTitleCase(locale.getLanguage()); - textWidth = getTextWidth(paint, language, origTextSize, bounds); - textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); - } - paint.setTextSize(textSize); - - // Place left and right arrow just before and after language text. - final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; - final int top = (int)(baseline - arrowHeight); - final float remains = (width - textWidth) / 2; - lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline); - rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth), - (int)baseline); - - return language; - } - - private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack) { - final int width = mSpaceKey.width; - final int height = mSpaceIcon.getIntrinsicHeight(); - final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(buffer); - canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); - - // If application locales are explicitly selected. - if (mLocale != null) { - final Paint paint = new Paint(); - paint.setAlpha(opacity); - paint.setAntiAlias(true); - paint.setTextAlign(Align.CENTER); - - final boolean allowVariableTextSize = true; - final String language = layoutSpaceBar(paint, mLanguageSwitcher.getInputLocale(), - mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height, - getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14), - allowVariableTextSize); - - // Draw language text with shadow - final int shadowColor = mRes.getColor(isBlack - ? R.color.latinkeyboard_bar_language_shadow_black - : R.color.latinkeyboard_bar_language_shadow_white); - final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; - final float descent = paint.descent(); - paint.setColor(shadowColor); - canvas.drawText(language, width / 2, baseline - descent - 1, paint); - paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text)); - canvas.drawText(language, width / 2, baseline - descent, paint); - - // Put arrows that are already layed out on either side of the text - if (mLanguageSwitcher.getLocaleCount() > 1) { - mButtonArrowLeftIcon.draw(canvas); - mButtonArrowRightIcon.draw(canvas); - } - } - - // Draw the spacebar icon at the bottom - if (isAutoCompletion) { - final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; - final int iconHeight = mSpaceAutoCompletionIndicator.getIntrinsicHeight(); - int x = (width - iconWidth) / 2; - int y = height - iconHeight; - mSpaceAutoCompletionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight); - mSpaceAutoCompletionIndicator.draw(canvas); - } else { - final int iconWidth = mSpaceIcon.getIntrinsicWidth(); - final int iconHeight = mSpaceIcon.getIntrinsicHeight(); - int x = (width - iconWidth) / 2; - int y = height - iconHeight; - mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight); - mSpaceIcon.draw(canvas); - } - return buffer; - } - - private void updateLocaleDrag(int diff) { - if (mSlidingLocaleIcon == null) { - final int width = Math.max(mSpaceKey.width, - (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO)); - final int height = mSpacePreviewIcon.getIntrinsicHeight(); - mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, width, height); - mSlidingLocaleIcon.setBounds(0, 0, width, height); - mSpaceKey.iconPreview = mSlidingLocaleIcon; - } - mSlidingLocaleIcon.setDiff(diff); - if (Math.abs(diff) == Integer.MAX_VALUE) { - mSpaceKey.iconPreview = mSpacePreviewIcon; - } else { - mSpaceKey.iconPreview = mSlidingLocaleIcon; - } - mSpaceKey.iconPreview.invalidateSelf(); - } - - public int getLanguageChangeDirection() { - if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2 - || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) { - return 0; // No change - } - return mSpaceDragLastDiff > 0 ? 1 : -1; - } - - public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion, - boolean isBlackSym) { - mLanguageSwitcher = switcher; - Locale locale = mLanguageSwitcher.getLocaleCount() > 0 - ? mLanguageSwitcher.getInputLocale() - : null; - // If the language count is 1 and is the same as the system language, don't show it. - if (locale != null - && mLanguageSwitcher.getLocaleCount() == 1 - && mLanguageSwitcher.getSystemLocale().getLanguage() - .equalsIgnoreCase(locale.getLanguage())) { - locale = null; - } - mLocale = locale; - setColorOfSymbolIcons(isAutoCompletion, isBlackSym); - } - - boolean isCurrentlyInSpace() { - return mCurrentlyInSpace; - } - - void setPreferredLetters(int[] frequencies) { - mPrefLetterFrequencies = frequencies; - mPrefLetter = 0; - } - - void keyReleased() { - mCurrentlyInSpace = false; - mSpaceDragLastDiff = 0; - mPrefLetter = 0; - mPrefLetterX = 0; - mPrefLetterY = 0; - mPrefDistance = Integer.MAX_VALUE; - if (mSpaceKey != null) { - updateLocaleDrag(Integer.MAX_VALUE); - } - } - - /** - * Does the magic of locking the touch gesture into the spacebar when - * switching input languages. - */ - boolean isInside(LatinKey key, int x, int y) { - final int code = key.codes[0]; - if (code == KEYCODE_SHIFT || - code == KEYCODE_DELETE) { - y -= key.height / 10; - if (code == KEYCODE_SHIFT) x += key.width / 6; - if (code == KEYCODE_DELETE) x -= key.width / 6; - } else if (code == LatinIME.KEYCODE_SPACE) { - y += LatinKeyboard.sSpacebarVerticalCorrection; - if (mLanguageSwitcher.getLocaleCount() > 1) { - if (mCurrentlyInSpace) { - int diff = x - mSpaceDragStartX; - if (Math.abs(diff - mSpaceDragLastDiff) > 0) { - updateLocaleDrag(diff); - } - mSpaceDragLastDiff = diff; - return true; - } else { - boolean insideSpace = key.isInsideSuper(x, y); - if (insideSpace) { - mCurrentlyInSpace = true; - mSpaceDragStartX = x; - updateLocaleDrag(0); - } - return insideSpace; - } - } - } else if (mPrefLetterFrequencies != null) { - // New coordinate? Reset - if (mPrefLetterX != x || mPrefLetterY != y) { - mPrefLetter = 0; - mPrefDistance = Integer.MAX_VALUE; - } - // Handle preferred next letter - final int[] pref = mPrefLetterFrequencies; - if (mPrefLetter > 0) { - if (DEBUG_PREFERRED_LETTER) { - if (mPrefLetter == code && !key.isInsideSuper(x, y)) { - Log.d(TAG, "CORRECTED !!!!!!"); - } - } - return mPrefLetter == code; - } else { - final boolean inside = key.isInsideSuper(x, y); - int[] nearby = getNearestKeys(x, y); - List<Key> nearbyKeys = getKeys(); - if (inside) { - // If it's a preferred letter - if (inPrefList(code, pref)) { - // Check if its frequency is much lower than a nearby key - mPrefLetter = code; - mPrefLetterX = x; - mPrefLetterY = y; - for (int i = 0; i < nearby.length; i++) { - Key k = nearbyKeys.get(nearby[i]); - if (k != key && inPrefList(k.codes[0], pref)) { - final int dist = distanceFrom(k, x, y); - if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) && - (pref[k.codes[0]] > pref[mPrefLetter] * 3)) { - mPrefLetter = k.codes[0]; - mPrefDistance = dist; - if (DEBUG_PREFERRED_LETTER) { - Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!"); - } - break; - } - } - } - - return mPrefLetter == code; - } - } - - // Get the surrounding keys and intersect with the preferred list - // For all in the intersection - // if distance from touch point is within a reasonable distance - // make this the pref letter - // If no pref letter - // return inside; - // else return thiskey == prefletter; - - for (int i = 0; i < nearby.length; i++) { - Key k = nearbyKeys.get(nearby[i]); - if (inPrefList(k.codes[0], pref)) { - final int dist = distanceFrom(k, x, y); - if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB) - && dist < mPrefDistance) { - mPrefLetter = k.codes[0]; - mPrefLetterX = x; - mPrefLetterY = y; - mPrefDistance = dist; - } - } - } - // Didn't find any - if (mPrefLetter == 0) { - return inside; - } else { - return mPrefLetter == code; - } - } - } - - // Lock into the spacebar - if (mCurrentlyInSpace) return false; - - return key.isInsideSuper(x, y); - } - - private boolean inPrefList(int code, int[] pref) { - if (code < pref.length && code >= 0) return pref[code] > 0; - return false; - } - - private int distanceFrom(Key k, int x, int y) { - if (y > k.y && y < k.y + k.height) { - return Math.abs(k.x + k.width / 2 - x); - } else { - return Integer.MAX_VALUE; - } - } - - @Override - public int[] getNearestKeys(int x, int y) { - if (mCurrentlyInSpace) { - return mSpaceKeyIndexArray; - } else { - // Avoid dead pixels at edges of the keyboard - return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)), - Math.max(0, Math.min(y, getHeight() - 1))); - } - } - - private int indexOf(int code) { - List<Key> keys = getKeys(); - int count = keys.size(); - for (int i = 0; i < count; i++) { - if (keys.get(i).codes[0] == code) return i; - } - return -1; - } - - private int getTextSizeFromTheme(int style, int defValue) { - TypedArray array = mContext.getTheme().obtainStyledAttributes( - style, new int[] { android.R.attr.textSize }); - int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue); - return textSize; - } - - // TODO LatinKey could be static class - class LatinKey extends Keyboard.Key { - - // functional normal state (with properties) - private final int[] KEY_STATE_FUNCTIONAL_NORMAL = { - android.R.attr.state_single - }; - - // functional pressed state (with properties) - private final int[] KEY_STATE_FUNCTIONAL_PRESSED = { - android.R.attr.state_single, - android.R.attr.state_pressed - }; - - private boolean mShiftLockEnabled; - - public LatinKey(Resources res, Keyboard.Row parent, int x, int y, - XmlResourceParser parser) { - super(res, parent, x, y, parser); - if (popupCharacters != null && popupCharacters.length() == 0) { - // If there is a keyboard with no keys specified in popupCharacters - popupResId = 0; - } - } - - private void enableShiftLock() { - mShiftLockEnabled = true; - } - - // sticky is used for shift key. If a key is not sticky and is modifier, - // the key will be treated as functional. - private boolean isFunctionalKey() { - return !sticky && modifier; - } - - @Override - public void onReleased(boolean inside) { - if (!mShiftLockEnabled) { - super.onReleased(inside); - } else { - pressed = !pressed; - } - } - - /** - * Overriding this method so that we can reduce the target area for certain keys. - */ - @Override - public boolean isInside(int x, int y) { - // TODO This should be done by parent.isInside(this, x, y) - // if Key.parent were protected. - boolean result = LatinKeyboard.this.isInside(this, x, y); - return result; - } - - boolean isInsideSuper(int x, int y) { - return super.isInside(x, y); - } - - @Override - public int[] getCurrentDrawableState() { - if (isFunctionalKey()) { - if (pressed) { - return KEY_STATE_FUNCTIONAL_PRESSED; - } else { - return KEY_STATE_FUNCTIONAL_NORMAL; - } - } - return super.getCurrentDrawableState(); - } - - @Override - public int squaredDistanceFrom(int x, int y) { - // We should count vertical gap between rows to calculate the center of this Key. - final int verticalGap = LatinKeyboard.this.mVerticalGap; - final int xDist = this.x + width / 2 - x; - final int yDist = this.y + (height + verticalGap) / 2 - y; - return xDist * xDist + yDist * yDist; - } - } - - /** - * Animation to be displayed on the spacebar preview popup when switching - * languages by swiping the spacebar. It draws the current, previous and - * next languages and moves them by the delta of touch movement on the spacebar. - */ - class SlidingLocaleDrawable extends Drawable { - - private final int mWidth; - private final int mHeight; - private final Drawable mBackground; - private final TextPaint mTextPaint; - private final int mMiddleX; - private final Drawable mLeftDrawable; - private final Drawable mRightDrawable; - private final int mThreshold; - private int mDiff; - private boolean mHitThreshold; - private String mCurrentLanguage; - private String mNextLanguage; - private String mPrevLanguage; - - public SlidingLocaleDrawable(Drawable background, int width, int height) { - mBackground = background; - setDefaultBounds(mBackground); - mWidth = width; - mHeight = height; - mTextPaint = new TextPaint(); - mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18)); - mTextPaint.setColor(R.color.latinkeyboard_transparent); - mTextPaint.setTextAlign(Align.CENTER); - mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE); - mTextPaint.setAntiAlias(true); - mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2; - mLeftDrawable = - mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left); - mRightDrawable = - mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right); - mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop(); - } - - private void setDiff(int diff) { - if (diff == Integer.MAX_VALUE) { - mHitThreshold = false; - mCurrentLanguage = null; - return; - } - mDiff = diff; - if (mDiff > mWidth) mDiff = mWidth; - if (mDiff < -mWidth) mDiff = -mWidth; - if (Math.abs(mDiff) > mThreshold) mHitThreshold = true; - invalidateSelf(); - } - - private String getLanguageName(Locale locale) { - return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); - } - - @Override - public void draw(Canvas canvas) { - canvas.save(); - if (mHitThreshold) { - Paint paint = mTextPaint; - final int width = mWidth; - final int height = mHeight; - final int diff = mDiff; - final Drawable lArrow = mLeftDrawable; - final Drawable rArrow = mRightDrawable; - canvas.clipRect(0, 0, width, height); - if (mCurrentLanguage == null) { - final LanguageSwitcher languageSwitcher = mLanguageSwitcher; - mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale()); - mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale()); - mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale()); - } - // Draw language text with shadow - final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent(); - paint.setColor(mRes.getColor(R.color.latinkeyboard_feedback_language_text)); - canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint); - canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint); - canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint); - - setDefaultBounds(lArrow); - rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width, - rArrow.getIntrinsicHeight()); - lArrow.draw(canvas); - rArrow.draw(canvas); - } - if (mBackground != null) { - canvas.translate(mMiddleX, 0); - mBackground.draw(canvas); - } - canvas.restore(); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void setAlpha(int alpha) { - // Ignore - } - - @Override - public void setColorFilter(ColorFilter cf) { - // Ignore - } - - @Override - public int getIntrinsicWidth() { - return mWidth; - } - - @Override - public int getIntrinsicHeight() { - return mHeight; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java deleted file mode 100644 index 008d37202..000000000 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java +++ /dev/null @@ -1,1508 +0,0 @@ -/* - * 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.pm.PackageManager; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.Region.Op; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.util.Log; -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.ViewGroup.LayoutParams; -import android.widget.PopupWindow; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.WeakHashMap; - -/** - * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and - * detecting key presses and touch movements. - * - * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class. - * - * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground - * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout - * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset - * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize - * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize - * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor - * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection - * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout - */ -public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy { - private static final String TAG = "LatinKeyboardBaseView"; - private static final boolean DEBUG = false; - - public static final int NOT_A_TOUCH_COORDINATE = -1; - - 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. - * @param x - * x-coordinate pixel of touched event. If onKey is not called by onTouchEvent, - * the value should be NOT_A_TOUCH_COORDINATE. - * @param y - * y-coordinate pixel of touched event. If onKey is not called by onTouchEvent, - * the value should be NOT_A_TOUCH_COORDINATE. - */ - void onKey(int primaryCode, int[] keyCodes, int x, int y); - - /** - * Sends a sequence of characters to the listener. - * - * @param text - * the sequence of characters to be displayed. - */ - void onText(CharSequence text); - - /** - * Called when user released a finger outside any key. - */ - void onCancel(); - - /** - * 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(); - } - - // Timing constants - private final int mKeyRepeatInterval; - - // Miscellaneous constants - /* package */ static final int NOT_A_KEY = -1; - private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; - private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1; - - // XML attribute - private int mKeyTextSize; - private int mKeyTextColor; - private Typeface mKeyTextStyle = Typeface.DEFAULT; - private int mLabelTextSize; - private int mSymbolColorScheme = 0; - private int mShadowColor; - private float mShadowRadius; - private Drawable mKeyBackground; - private float mBackgroundDimAmount; - private float mKeyHysteresisDistance; - private float mVerticalCorrection; - private int mPreviewOffset; - private int mPreviewHeight; - private int mPopupLayout; - - // Main keyboard - private Keyboard mKeyboard; - private Key[] mKeys; - // TODO this attribute should be gotten from Keyboard. - private int mKeyboardVerticalGap; - - // Key preview popup - private TextView mPreviewText; - private PopupWindow mPreviewPopup; - private int mPreviewTextSizeLarge; - private int[] mOffsetInWindow; - private int mOldPreviewKeyIndex = NOT_A_KEY; - private boolean mShowPreview = true; - private boolean mShowTouchPoints = true; - private int mPopupPreviewOffsetX; - private int mPopupPreviewOffsetY; - private int mWindowY; - private int mPopupPreviewDisplayedY; - private final int mDelayBeforePreview; - private final int mDelayAfterPreview; - - // Popup mini keyboard - private PopupWindow mMiniKeyboardPopup; - private LatinKeyboardBaseView mMiniKeyboard; - private View mMiniKeyboardParent; - private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>(); - private int mMiniKeyboardOriginX; - private int mMiniKeyboardOriginY; - private long mMiniKeyboardPopupTime; - private int[] mWindowOffset; - private final float mMiniKeyboardSlideAllowance; - private int mMiniKeyboardTrackerId; - - /** Listener for {@link OnKeyboardActionListener}. */ - private OnKeyboardActionListener mKeyboardActionListener; - - private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); - - // TODO: Let the PointerTracker class manage this pointer queue - private final PointerQueue mPointerQueue = new PointerQueue(); - - private final boolean mHasDistinctMultitouch; - private int mOldPointerCount = 1; - - protected KeyDetector mKeyDetector = new ProximityKeyDetector(); - - // Swipe gesture detector - private GestureDetector mGestureDetector; - private final SwipeTracker mSwipeTracker = new SwipeTracker(); - private final int mSwipeThreshold; - private final boolean mDisambiguateSwipe; - - // Drawing - /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ - private boolean mDrawPending; - /** The dirty region in the keyboard bitmap */ - private final 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; - private Key mInvalidatedKey; - /** The canvas for the above mutable keyboard bitmap */ - private Canvas mCanvas; - private final Paint mPaint; - private final Rect mPadding; - private final Rect mClipRegion = new Rect(0, 0, 0, 0); - // This map caches key label text height in pixel as value and key label text size as map key. - private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>(); - // Distance from horizontal center of the key, proportional to key label text height. - private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f; - private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H"; - - private final UIHandler mHandler = new UIHandler(); - - class UIHandler extends Handler { - private static final int MSG_POPUP_PREVIEW = 1; - private static final int MSG_DISMISS_PREVIEW = 2; - private static final int MSG_REPEAT_KEY = 3; - private static final int MSG_LONGPRESS_KEY = 4; - - private boolean mInKeyRepeat; - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_POPUP_PREVIEW: - showKey(msg.arg1, (PointerTracker)msg.obj); - break; - case MSG_DISMISS_PREVIEW: - mPreviewPopup.dismiss(); - break; - case MSG_REPEAT_KEY: { - final PointerTracker tracker = (PointerTracker)msg.obj; - tracker.repeatKey(msg.arg1); - startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker); - break; - } - case MSG_LONGPRESS_KEY: { - final PointerTracker tracker = (PointerTracker)msg.obj; - openPopupIfRequired(msg.arg1, tracker); - break; - } - } - } - - public void popupPreview(long delay, int keyIndex, PointerTracker tracker) { - removeMessages(MSG_POPUP_PREVIEW); - if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { - // Show right away, if it's already visible and finger is moving around - showKey(keyIndex, tracker); - } else { - sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker), - delay); - } - } - - public void cancelPopupPreview() { - removeMessages(MSG_POPUP_PREVIEW); - } - - public void dismissPreview(long delay) { - if (mPreviewPopup.isShowing()) { - sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay); - } - } - - public void cancelDismissPreview() { - removeMessages(MSG_DISMISS_PREVIEW); - } - - public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { - mInKeyRepeat = true; - sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); - } - - public void cancelKeyRepeatTimer() { - mInKeyRepeat = false; - removeMessages(MSG_REPEAT_KEY); - } - - public boolean isInKeyRepeat() { - return mInKeyRepeat; - } - - public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { - removeMessages(MSG_LONGPRESS_KEY); - sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); - } - - public void cancelLongPressTimer() { - removeMessages(MSG_LONGPRESS_KEY); - } - - public void cancelKeyTimers() { - cancelKeyRepeatTimer(); - cancelLongPressTimer(); - } - - public void cancelAllMessages() { - cancelKeyTimers(); - cancelPopupPreview(); - cancelDismissPreview(); - } - } - - static class PointerQueue { - private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>(); - - public void add(PointerTracker tracker) { - mQueue.add(tracker); - } - - public int lastIndexOf(PointerTracker tracker) { - LinkedList<PointerTracker> queue = mQueue; - for (int index = queue.size() - 1; index >= 0; index--) { - PointerTracker t = queue.get(index); - if (t == tracker) - return index; - } - return -1; - } - - public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) { - LinkedList<PointerTracker> queue = mQueue; - int oldestPos = 0; - for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) { - if (t.isModifier()) { - oldestPos++; - } else { - t.onUpEvent(t.getLastX(), t.getLastY(), eventTime); - t.setAlreadyProcessed(); - queue.remove(oldestPos); - } - } - } - - public void releaseAllPointersExcept(PointerTracker tracker, long eventTime) { - for (PointerTracker t : mQueue) { - if (t == tracker) - continue; - t.onUpEvent(t.getLastX(), t.getLastY(), eventTime); - t.setAlreadyProcessed(); - } - mQueue.clear(); - if (tracker != null) - mQueue.add(tracker); - } - - public void remove(PointerTracker tracker) { - mQueue.remove(tracker); - } - - public boolean isInSlidingKeyInput() { - for (final PointerTracker tracker : mQueue) { - if (tracker.isInSlidingKeyInput()) - return true; - } - return false; - } - } - - 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_keyHysteresisDistance: - mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0); - 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; - } - } - - final Resources res = getResources(); - - mPreviewPopup = new PopupWindow(context); - if (previewLayout != 0) { - mPreviewText = (TextView) inflate.inflate(previewLayout, null); - mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large); - mPreviewPopup.setContentView(mPreviewText); - mPreviewPopup.setBackgroundDrawable(null); - } else { - mShowPreview = false; - } - mPreviewPopup.setTouchable(false); - mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation); - mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); - mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); - - mMiniKeyboardParent = this; - mMiniKeyboardPopup = new PopupWindow(context); - mMiniKeyboardPopup.setBackgroundDrawable(null); - mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation); - - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setTextSize(keyTextSize); - mPaint.setTextAlign(Align.CENTER); - mPaint.setAlpha(255); - - mPadding = new Rect(0, 0, 0, 0); - mKeyBackground.getPadding(mPadding); - - mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); - // TODO: Refer frameworks/base/core/res/res/values/config.xml - mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); - mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance); - - GestureDetector.SimpleOnGestureListener listener = - new GestureDetector.SimpleOnGestureListener() { - @Override - public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, - float velocityY) { - 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(); - if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { - if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) { - swipeRight(); - return true; - } - } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { - if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) { - swipeLeft(); - return true; - } - } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { - if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) { - swipeUp(); - return true; - } - } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { - if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { - swipeDown(); - return true; - } - } - return false; - } - }; - - final boolean ignoreMultitouch = true; - mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); - mGestureDetector.setIsLongpressEnabled(false); - - mHasDistinctMultitouch = context.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); - mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); - } - - public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { - mKeyboardActionListener = listener; - for (PointerTracker tracker : mPointerTrackers) { - tracker.setOnKeyboardActionListener(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) { - dismissKeyPreview(); - } - // Remove any pending messages, except dismissing preview - mHandler.cancelKeyTimers(); - mHandler.cancelPopupPreview(); - mKeyboard = keyboard; - LatinImeLogger.onSetKeyboard(keyboard); - mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), - -getPaddingTop() + mVerticalCorrection); - mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap); - for (PointerTracker tracker : mPointerTrackers) { - tracker.setKeyboard(mKeys, mKeyHysteresisDistance); - } - requestLayout(); - // Hint to reallocate the buffer if the size changed - mKeyboardChanged = true; - invalidateAllKeys(); - computeProximityThreshold(keyboard); - mMiniKeyboardCache.clear(); - } - - /** - * Returns the current keyboard being displayed by this view. - * @return the currently attached keyboard - * @see #setKeyboard(Keyboard) - */ - public Keyboard getKeyboard() { - return mKeyboard; - } - - /** - * Return whether the device has distinct multi-touch panel. - * @return true if the device has distinct multi-touch panel. - */ - public boolean hasDistinctMultitouch() { - return mHasDistinctMultitouch; - } - - /** - * 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 getSymbolColorScheme() { - return mSymbolColorScheme; - } - - public void setPopupParent(View v) { - mMiniKeyboardParent = v; - } - - public void setPopupOffset(int x, int y) { - mPopupPreviewOffsetX = x; - mPopupPreviewOffsetY = y; - 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) { - mKeyDetector.setProximityCorrectionEnabled(enabled); - } - - /** - * Returns true if proximity correction is enabled. - */ - public boolean isProximityCorrectionEnabled() { - return mKeyDetector.isProximityCorrectionEnabled(); - } - - 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 + mKeyboardVerticalGap) + key.gap; - } - if (dimensionSum < 0 || length == 0) return; - mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length)); - } - - @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)) { - // TODO we should use Rect.inset and Rect.contains here. - // 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); - - boolean shouldDrawIcon = true; - if (label != null) { - // For characters, use large font. For labels like "Done", use small font. - final int labelSize; - if (label.length() > 1 && key.codes.length < 2) { - labelSize = mLabelTextSize; - paint.setTypeface(Typeface.DEFAULT_BOLD); - } else { - labelSize = mKeyTextSize; - paint.setTypeface(mKeyTextStyle); - } - paint.setTextSize(labelSize); - - Integer labelHeightValue = mTextHeightCache.get(labelSize); - final int labelHeight; - if (labelHeightValue != null) { - labelHeight = labelHeightValue; - } else { - Rect textBounds = new Rect(); - paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds); - labelHeight = textBounds.height(); - mTextHeightCache.put(labelSize, labelHeight); - } - - // Draw a drop shadow for the text - paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); - final int centerX = (key.width + padding.left - padding.right) / 2; - final int centerY = (key.height + padding.top - padding.bottom) / 2; - final float baseline = centerY - + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR; - canvas.drawText(label, centerX, baseline, paint); - // Turn off drop shadow - paint.setShadowLayer(0, 0, 0, 0); - - // Usually don't draw icon if label is not null, but we draw icon for the number - // hint and popup hint. - shouldDrawIcon = shouldDrawLabelAndIcon(key); - } - if (key.icon != null && shouldDrawIcon) { - // Special handing for the upper-right number hint icons - final int drawableWidth; - final int drawableHeight; - final int drawableX; - final int drawableY; - if (shouldDrawIconFully(key)) { - drawableWidth = key.width; - drawableHeight = key.height; - drawableX = 0; - drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL; - } else { - drawableWidth = key.icon.getIntrinsicWidth(); - drawableHeight = key.icon.getIntrinsicHeight(); - drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2; - drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2; - } - canvas.translate(drawableX, drawableY); - key.icon.setBounds(0, 0, drawableWidth, drawableHeight); - 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 (mMiniKeyboard != null) { - paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); - canvas.drawRect(0, 0, getWidth(), getHeight(), paint); - } - - if (DEBUG) { - if (mShowTouchPoints) { - for (PointerTracker tracker : mPointerTrackers) { - int startX = tracker.getStartX(); - int startY = tracker.getStartY(); - int lastX = tracker.getLastX(); - int lastY = tracker.getLastY(); - paint.setAlpha(128); - paint.setColor(0xFFFF0000); - canvas.drawCircle(startX, startY, 3, paint); - canvas.drawLine(startX, startY, lastX, lastY, paint); - paint.setColor(0xFF0000FF); - canvas.drawCircle(lastX, lastY, 3, paint); - paint.setColor(0xFF00FF00); - canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint); - } - } - } - - mDrawPending = false; - mDirtyRect.setEmpty(); - } - - // TODO: clean up this method. - private void dismissKeyPreview() { - for (PointerTracker tracker : mPointerTrackers) - tracker.updateKey(NOT_A_KEY); - showPreview(NOT_A_KEY, null); - } - - public void showPreview(int keyIndex, PointerTracker tracker) { - int oldKeyIndex = mOldPreviewKeyIndex; - mOldPreviewKeyIndex = keyIndex; - final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard) - && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled(); - // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show - // the space key preview and 3) pointer moves off the space key to other letter key, we - // should hide the preview of the previous key. - final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null) - || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex); - // If key changed and preview is on or the key is space (language switch is enabled) - if (oldKeyIndex != keyIndex - && (mShowPreview - || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) { - if (keyIndex == NOT_A_KEY) { - mHandler.cancelPopupPreview(); - mHandler.dismissPreview(mDelayAfterPreview); - } else if (tracker != null) { - mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker); - } - } - } - - private void showKey(final int keyIndex, PointerTracker tracker) { - Key key = tracker.getKey(keyIndex); - if (key == null) - return; - // Should not draw hint icon in key preview - if (key.icon != null && !shouldDrawLabelAndIcon(key)) { - mPreviewText.setCompoundDrawables(null, null, null, - key.iconPreview != null ? key.iconPreview : key.icon); - mPreviewText.setText(null); - } else { - mPreviewText.setCompoundDrawables(null, null, null, null); - mPreviewText.setText(adjustCase(tracker.getPreviewText(key))); - if (key.label.length() > 1 && key.codes.length < 2) { - mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); - mPreviewText.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; - } - - int popupPreviewX = key.x - (popupWidth - key.width) / 2; - int popupPreviewY = key.y - popupHeight + mPreviewOffset; - - mHandler.cancelDismissPreview(); - if (mOffsetInWindow == null) { - mOffsetInWindow = new int[2]; - getLocationInWindow(mOffsetInWindow); - mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero - mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero - int[] windowLocation = new int[2]; - getLocationOnScreen(windowLocation); - mWindowY = windowLocation[1]; - } - // Set the preview background state - mPreviewText.getBackground().setState( - key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - popupPreviewX += mOffsetInWindow[0]; - popupPreviewY += mOffsetInWindow[1]; - - // If the popup cannot be shown above the key, put it on the side - if (popupPreviewY + 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) { - popupPreviewX += (int) (key.width * 2.5); - } else { - popupPreviewX -= (int) (key.width * 2.5); - } - popupPreviewY += popupHeight; - } - - if (mPreviewPopup.isShowing()) { - mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight); - } else { - mPreviewPopup.setWidth(popupWidth); - mPreviewPopup.setHeight(popupHeight); - mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY, - popupPreviewX, popupPreviewY); - } - // Record popup preview position to display mini-keyboard later at the same positon - mPopupPreviewDisplayedY = popupPreviewY; - 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(Key) - */ - 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 key key in the attached {@link Keyboard}. - * @see #invalidateAllKeys - */ - public void invalidateKey(Key key) { - if (key == null) - return; - mInvalidatedKey = key; - // TODO we should clean up this and record key's region to use in onBufferDraw. - 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(int keyIndex, PointerTracker tracker) { - // Check if we have a popup layout specified first. - if (mPopupLayout == 0) { - return false; - } - - Key popupKey = tracker.getKey(keyIndex); - if (popupKey == null) - return false; - boolean result = onLongPress(popupKey); - if (result) { - dismissKeyPreview(); - mMiniKeyboardTrackerId = tracker.mPointerId; - // Mark this tracker "already processed" and remove it from the pointer queue - tracker.setAlreadyProcessed(); - mPointerQueue.remove(tracker); - } - return result; - } - - private View inflateMiniKeyboardContainer(Key popupKey) { - int popupKeyboardId = popupKey.popupResId; - LayoutInflater inflater = (LayoutInflater)getContext().getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - View container = inflater.inflate(mPopupLayout, null); - if (container == null) - throw new NullPointerException(); - - LatinKeyboardBaseView miniKeyboard = - (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); - miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { - public void onKey(int primaryCode, int[] keyCodes, int x, int y) { - mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y); - dismissPopupKeyboard(); - } - - public void onText(CharSequence text) { - mKeyboardActionListener.onText(text); - dismissPopupKeyboard(); - } - - public void onCancel() { - mKeyboardActionListener.onCancel(); - 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); - } - }); - // Override default ProximityKeyDetector. - miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance); - // Remove gesture detector on mini-keyboard - miniKeyboard.mGestureDetector = null; - - Keyboard keyboard; - if (popupKey.popupCharacters != null) { - keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters, - -1, getPaddingLeft() + getPaddingRight()); - } else { - keyboard = new Keyboard(getContext(), popupKeyboardId); - } - miniKeyboard.setKeyboard(keyboard); - miniKeyboard.setPopupParent(this); - - container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); - - return container; - } - - private static boolean isOneRowKeys(List<Key> keys) { - if (keys.size() == 0) return false; - final int edgeFlags = keys.get(0).edgeFlags; - // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows, - // does not have both top and bottom edge flags on at the same time. On the other hand, - // the first key of mini keyboard that was created with popupCharacters must have both top - // and bottom edge flags on. - // When you want to use one row mini-keyboard from xml file, make sure that the row has - // both top and bottom edge flags set. - return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0; - } - - /** - * 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) { - // TODO if popupKey.popupCharacters has only one letter, send it as key without opening - // mini keyboard. - - if (popupKey.popupResId == 0) - return false; - - View container = mMiniKeyboardCache.get(popupKey); - if (container == null) { - container = inflateMiniKeyboardContainer(popupKey); - mMiniKeyboardCache.put(popupKey, container); - } - mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView); - if (mWindowOffset == null) { - mWindowOffset = new int[2]; - getLocationInWindow(mWindowOffset); - } - - // Get width of a key in the mini popup keyboard = "miniKeyWidth". - // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard. - // We adjust the position of mini popup keyboard with the edge key in it: - // a) When we have the leftmost key in popup keyboard directly above the pressed key - // Right edges of both keys should be aligned for consistent default selection - // b) When we have the rightmost key in popup keyboard directly above the pressed key - // Left edges of both keys should be aligned for consistent default selection - final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys(); - final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).width : 0; - - // HACK: Have the leftmost number in the popup characters right above the key - boolean isNumberAtLeftmost = - hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey); - int popupX = popupKey.x + mWindowOffset[0]; - popupX += getPaddingLeft(); - if (isNumberAtLeftmost) { - popupX += popupKey.width - miniKeyWidth; // adjustment for a) described above - popupX -= container.getPaddingLeft(); - } else { - popupX += miniKeyWidth; // adjustment for b) described above - popupX -= container.getMeasuredWidth(); - popupX += container.getPaddingRight(); - } - int popupY = popupKey.y + mWindowOffset[1]; - popupY += getPaddingTop(); - popupY -= container.getMeasuredHeight(); - popupY += container.getPaddingBottom(); - final int x = popupX; - final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY; - - int adjustedX = x; - if (x < 0) { - adjustedX = 0; - } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) { - adjustedX = getMeasuredWidth() - container.getMeasuredWidth(); - } - mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0]; - mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1]; - mMiniKeyboard.setPopupOffset(adjustedX, y); - mMiniKeyboard.setShifted(isShifted()); - // Mini keyboard needs no pop-up key preview displayed. - mMiniKeyboard.setPreviewEnabled(false); - mMiniKeyboardPopup.setContentView(container); - mMiniKeyboardPopup.setWidth(container.getMeasuredWidth()); - mMiniKeyboardPopup.setHeight(container.getMeasuredHeight()); - mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y); - - // Inject down event on the key to mini keyboard. - long eventTime = SystemClock.uptimeMillis(); - mMiniKeyboardPopupTime = eventTime; - MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.x - + popupKey.width / 2, popupKey.y + popupKey.height / 2, eventTime); - mMiniKeyboard.onTouchEvent(downEvent); - downEvent.recycle(); - - invalidateAllKeys(); - return true; - } - - private static boolean hasMultiplePopupChars(Key key) { - if (key.popupCharacters != null && key.popupCharacters.length() > 1) { - return true; - } - return false; - } - - private boolean shouldDrawIconFully(Key key) { - return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key) - || LatinKeyboard.hasPuncOrSmileysPopup(key); - } - - private boolean shouldDrawLabelAndIcon(Key key) { - return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key) - || LatinKeyboard.hasPuncOrSmileysPopup(key); - } - - private boolean isLatinF1Key(Key key) { - return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key); - } - - private boolean isNonMicLatinF1Key(Key key) { - return isLatinF1Key(key) && key.label != null; - } - - private static boolean isNumberAtEdgeOfPopupChars(Key key) { - return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key); - } - - /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) { - if (key.popupCharacters != null && key.popupCharacters.length() > 0 - && isAsciiDigit(key.popupCharacters.charAt(0))) { - return true; - } - return false; - } - - /* package */ static boolean isNumberAtRightmostPopupChar(Key key) { - if (key.popupCharacters != null && key.popupCharacters.length() > 0 - && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) { - return true; - } - return false; - } - - private static boolean isAsciiDigit(char c) { - return (c < 0x80) && Character.isDigit(c); - } - - private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) { - return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action, - x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0); - } - - private PointerTracker getPointerTracker(final int id) { - final ArrayList<PointerTracker> pointers = mPointerTrackers; - final Key[] keys = mKeys; - final OnKeyboardActionListener listener = mKeyboardActionListener; - - // Create pointer trackers until we can get 'id+1'-th tracker, if needed. - for (int i = pointers.size(); i <= id; i++) { - final PointerTracker tracker = - new PointerTracker(i, mHandler, mKeyDetector, this, getResources()); - if (keys != null) - tracker.setKeyboard(keys, mKeyHysteresisDistance); - if (listener != null) - tracker.setOnKeyboardActionListener(listener); - pointers.add(tracker); - } - - return pointers.get(id); - } - - public boolean isInSlidingKeyInput() { - if (mMiniKeyboard != null) { - return mMiniKeyboard.isInSlidingKeyInput(); - } else { - return mPointerQueue.isInSlidingKeyInput(); - } - } - - public int getPointerCount() { - return mOldPointerCount; - } - - @Override - public boolean onTouchEvent(MotionEvent me) { - final int action = me.getActionMasked(); - final int pointerCount = me.getPointerCount(); - final int oldPointerCount = mOldPointerCount; - mOldPointerCount = pointerCount; - - // TODO: cleanup this code into a multi-touch to single-touch event converter class? - // If the device does not have distinct multi-touch support panel, ignore all multi-touch - // events except a transition from/to single-touch. - if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { - return true; - } - - // Track the last few movements to look for spurious swipes. - mSwipeTracker.addMovement(me); - - // Gesture detector must be enabled only when mini-keyboard is not on the screen. - if (mMiniKeyboard == null - && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) { - dismissKeyPreview(); - mHandler.cancelKeyTimers(); - return true; - } - - final long eventTime = me.getEventTime(); - final int index = me.getActionIndex(); - final int id = me.getPointerId(index); - final int x = (int)me.getX(index); - final int y = (int)me.getY(index); - - // Needs to be called after the gesture detector gets a turn, as it may have - // displayed the mini keyboard - if (mMiniKeyboard != null) { - final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId); - if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) { - final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex); - final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex); - MotionEvent translated = generateMiniKeyboardMotionEvent(action, - miniKeyboardX, miniKeyboardY, eventTime); - mMiniKeyboard.onTouchEvent(translated); - translated.recycle(); - } - return true; - } - - if (mHandler.isInKeyRepeat()) { - // It will keep being in the key repeating mode while the key is being pressed. - if (action == MotionEvent.ACTION_MOVE) { - return true; - } - final PointerTracker tracker = getPointerTracker(id); - // Key repeating timer will be canceled if 2 or more keys are in action, and current - // event (UP or DOWN) is non-modifier key. - if (pointerCount > 1 && !tracker.isModifier()) { - mHandler.cancelKeyRepeatTimer(); - } - // Up event will pass through. - } - - // TODO: cleanup this code into a multi-touch to single-touch event converter class? - // Translate mutli-touch event to single-touch events on the device that has no distinct - // multi-touch panel. - if (!mHasDistinctMultitouch) { - // Use only main (id=0) pointer tracker. - PointerTracker tracker = getPointerTracker(0); - if (pointerCount == 1 && oldPointerCount == 2) { - // Multi-touch to single touch transition. - // Send a down event for the latest pointer. - tracker.onDownEvent(x, y, eventTime); - } else if (pointerCount == 2 && oldPointerCount == 1) { - // Single-touch to multi-touch transition. - // Send an up event for the last pointer. - tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime); - } else if (pointerCount == 1 && oldPointerCount == 1) { - tracker.onTouchEvent(action, x, y, eventTime); - } else { - Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount - + " (old " + oldPointerCount + ")"); - } - return true; - } - - if (action == MotionEvent.ACTION_MOVE) { - for (int i = 0; i < pointerCount; i++) { - PointerTracker tracker = getPointerTracker(me.getPointerId(i)); - tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime); - } - } else { - PointerTracker tracker = getPointerTracker(id); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - onDownEvent(tracker, x, y, eventTime); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - onUpEvent(tracker, x, y, eventTime); - break; - case MotionEvent.ACTION_CANCEL: - onCancelEvent(tracker, x, y, eventTime); - break; - } - } - - return true; - } - - private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) { - if (tracker.isOnModifierKey(x, y)) { - // Before processing a down event of modifier key, all pointers already being tracked - // should be released. - mPointerQueue.releaseAllPointersExcept(null, eventTime); - } - tracker.onDownEvent(x, y, eventTime); - mPointerQueue.add(tracker); - } - - private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) { - if (tracker.isModifier()) { - // Before processing an up event of modifier key, all pointers already being tracked - // should be released. - mPointerQueue.releaseAllPointersExcept(tracker, eventTime); - } else { - int index = mPointerQueue.lastIndexOf(tracker); - if (index >= 0) { - mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime); - } else { - Log.w(TAG, "onUpEvent: corresponding down event not found for pointer " - + tracker.mPointerId); - } - } - tracker.onUpEvent(x, y, eventTime); - mPointerQueue.remove(tracker); - } - - private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) { - tracker.onCancelEvent(x, y, eventTime); - mPointerQueue.remove(tracker); - } - - protected void swipeRight() { - mKeyboardActionListener.swipeRight(); - } - - protected void swipeLeft() { - mKeyboardActionListener.swipeLeft(); - } - - protected void swipeUp() { - mKeyboardActionListener.swipeUp(); - } - - protected void swipeDown() { - mKeyboardActionListener.swipeDown(); - } - - public void closing() { - mPreviewPopup.dismiss(); - mHandler.cancelAllMessages(); - - dismissPopupKeyboard(); - mBuffer = null; - mCanvas = null; - mMiniKeyboardCache.clear(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - closing(); - } - - private void dismissPopupKeyboard() { - if (mMiniKeyboardPopup.isShowing()) { - mMiniKeyboardPopup.dismiss(); - mMiniKeyboard = null; - mMiniKeyboardOriginX = 0; - mMiniKeyboardOriginY = 0; - invalidateAllKeys(); - } - } - - public boolean handleBack() { - if (mMiniKeyboardPopup.isShowing()) { - dismissPopupKeyboard(); - return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java deleted file mode 100644 index a5476e457..000000000 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import java.util.List; - -public class LatinKeyboardView extends LatinKeyboardBaseView { - - static final int KEYCODE_OPTIONS = -100; - static final int KEYCODE_OPTIONS_LONGPRESS = -101; - static final int KEYCODE_VOICE = -102; - static final int KEYCODE_F1 = -103; - static final int KEYCODE_NEXT_LANGUAGE = -104; - static final int KEYCODE_PREV_LANGUAGE = -105; - - private Keyboard mPhoneKeyboard; - - /** Whether we've started dropping move events because we found a big jump */ - private boolean mDroppingEvents; - /** - * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has - * occured - */ - private boolean mDisableDisambiguation; - /** The distance threshold at which we start treating the touch session as a multi-touch */ - private int mJumpThresholdSquare = Integer.MAX_VALUE; - /** The y coordinate of the last row */ - private int mLastRowY; - - public LatinKeyboardView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public void setPhoneKeyboard(Keyboard phoneKeyboard) { - mPhoneKeyboard = phoneKeyboard; - } - - @Override - public void setPreviewEnabled(boolean previewEnabled) { - if (getKeyboard() == mPhoneKeyboard) { - // Phone keyboard never shows popup preview (except language switch). - super.setPreviewEnabled(false); - } else { - super.setPreviewEnabled(previewEnabled); - } - } - - @Override - public void setKeyboard(Keyboard newKeyboard) { - final Keyboard oldKeyboard = getKeyboard(); - if (oldKeyboard instanceof LatinKeyboard) { - // Reset old keyboard state before switching to new keyboard. - ((LatinKeyboard)oldKeyboard).keyReleased(); - } - super.setKeyboard(newKeyboard); - // One-seventh of the keyboard width seems like a reasonable threshold - mJumpThresholdSquare = newKeyboard.getMinWidth() / 7; - mJumpThresholdSquare *= mJumpThresholdSquare; - // Assuming there are 4 rows, this is the coordinate of the last row - mLastRowY = (newKeyboard.getHeight() * 3) / 4; - setKeyboardLocal(newKeyboard); - } - - @Override - protected boolean onLongPress(Key key) { - int primaryCode = key.codes[0]; - if (primaryCode == KEYCODE_OPTIONS) { - return invokeOnKey(KEYCODE_OPTIONS_LONGPRESS); - } else if (primaryCode == '0' && getKeyboard() == mPhoneKeyboard) { - // Long pressing on 0 in phone number keypad gives you a '+'. - return invokeOnKey('+'); - } else { - return super.onLongPress(key); - } - } - - private boolean invokeOnKey(int primaryCode) { - getOnKeyboardActionListener().onKey(primaryCode, null, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE, - LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE); - return true; - } - - @Override - protected CharSequence adjustCase(CharSequence label) { - Keyboard keyboard = getKeyboard(); - if (keyboard.isShifted() - && keyboard instanceof LatinKeyboard - && ((LatinKeyboard) keyboard).isAlphaKeyboard() - && !TextUtils.isEmpty(label) && label.length() < 3 - && Character.isLowerCase(label.charAt(0))) { - label = label.toString().toUpperCase(); - } - return label; - } - - public boolean setShiftLocked(boolean shiftLocked) { - Keyboard keyboard = getKeyboard(); - if (keyboard instanceof LatinKeyboard) { - ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked); - invalidateAllKeys(); - return true; - } - return false; - } - - /** - * This function checks to see if we need to handle any sudden jumps in the pointer location - * that could be due to a multi-touch being treated as a move by the firmware or hardware. - * Once a sudden jump is detected, all subsequent move events are discarded - * until an UP is received.<P> - * When a sudden jump is detected, an UP event is simulated at the last position and when - * the sudden moves subside, a DOWN event is simulated for the second key. - * @param me the motion event - * @return true if the event was consumed, so that it doesn't continue to be handled by - * KeyboardView. - */ - private boolean handleSuddenJump(MotionEvent me) { - final int action = me.getAction(); - final int x = (int) me.getX(); - final int y = (int) me.getY(); - boolean result = false; - - // Real multi-touch event? Stop looking for sudden jumps - if (me.getPointerCount() > 1) { - mDisableDisambiguation = true; - } - if (mDisableDisambiguation) { - // If UP, reset the multi-touch flag - if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false; - return false; - } - - switch (action) { - case MotionEvent.ACTION_DOWN: - // Reset the "session" - mDroppingEvents = false; - mDisableDisambiguation = false; - break; - case MotionEvent.ACTION_MOVE: - // Is this a big jump? - final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y); - // Check the distance and also if the move is not entirely within the bottom row - // If it's only in the bottom row, it might be an intentional slide gesture - // for language switching - if (distanceSquare > mJumpThresholdSquare - && (mLastY < mLastRowY || y < mLastRowY)) { - // If we're not yet dropping events, start dropping and send an UP event - if (!mDroppingEvents) { - mDroppingEvents = true; - // Send an up event - MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), - MotionEvent.ACTION_UP, - mLastX, mLastY, me.getMetaState()); - super.onTouchEvent(translated); - translated.recycle(); - } - result = true; - } else if (mDroppingEvents) { - // If moves are small and we're already dropping events, continue dropping - result = true; - } - break; - case MotionEvent.ACTION_UP: - if (mDroppingEvents) { - // Send a down event first, as we dropped a bunch of sudden jumps and assume that - // the user is releasing the touch on the second key. - MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(), - MotionEvent.ACTION_DOWN, - x, y, me.getMetaState()); - super.onTouchEvent(translated); - translated.recycle(); - mDroppingEvents = false; - // Let the up event get processed as well, result = false - } - break; - } - // Track the previous coordinate - mLastX = x; - mLastY = y; - return result; - } - - @Override - public boolean onTouchEvent(MotionEvent me) { - LatinKeyboard keyboard = (LatinKeyboard) getKeyboard(); - if (DEBUG_LINE) { - mLastX = (int) me.getX(); - mLastY = (int) me.getY(); - invalidate(); - } - - // If there was a sudden jump, return without processing the actual motion event. - if (handleSuddenJump(me)) - return true; - - // Reset any bounding box controls in the keyboard - if (me.getAction() == MotionEvent.ACTION_DOWN) { - keyboard.keyReleased(); - } - - if (me.getAction() == MotionEvent.ACTION_UP) { - int languageDirection = keyboard.getLanguageChangeDirection(); - if (languageDirection != 0) { - getOnKeyboardActionListener().onKey( - languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE, - null, mLastX, mLastY); - me.setAction(MotionEvent.ACTION_CANCEL); - keyboard.keyReleased(); - return super.onTouchEvent(me); - } - } - - return super.onTouchEvent(me); - } - - /**************************** INSTRUMENTATION *******************************/ - - static final boolean DEBUG_AUTO_PLAY = false; - static final boolean DEBUG_LINE = false; - private static final int MSG_TOUCH_DOWN = 1; - private static final int MSG_TOUCH_UP = 2; - - Handler mHandler2; - - private String mStringToPlay; - private int mStringIndex; - private boolean mDownDelivered; - private Key[] mAsciiKeys = new Key[256]; - private boolean mPlaying; - private int mLastX; - private int mLastY; - private Paint mPaint; - - private void setKeyboardLocal(Keyboard k) { - if (DEBUG_AUTO_PLAY) { - findKeys(); - if (mHandler2 == null) { - mHandler2 = new Handler() { - @Override - public void handleMessage(Message msg) { - removeMessages(MSG_TOUCH_DOWN); - removeMessages(MSG_TOUCH_UP); - if (mPlaying == false) return; - - switch (msg.what) { - case MSG_TOUCH_DOWN: - if (mStringIndex >= mStringToPlay.length()) { - mPlaying = false; - return; - } - char c = mStringToPlay.charAt(mStringIndex); - while (c > 255 || mAsciiKeys[c] == null) { - mStringIndex++; - if (mStringIndex >= mStringToPlay.length()) { - mPlaying = false; - return; - } - c = mStringToPlay.charAt(mStringIndex); - } - int x = mAsciiKeys[c].x + 10; - int y = mAsciiKeys[c].y + 26; - MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(), - SystemClock.uptimeMillis(), - MotionEvent.ACTION_DOWN, x, y, 0); - LatinKeyboardView.this.dispatchTouchEvent(me); - me.recycle(); - sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else - // happens - mDownDelivered = true; - break; - case MSG_TOUCH_UP: - char cUp = mStringToPlay.charAt(mStringIndex); - int x2 = mAsciiKeys[cUp].x + 10; - int y2 = mAsciiKeys[cUp].y + 26; - mStringIndex++; - - MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(), - SystemClock.uptimeMillis(), - MotionEvent.ACTION_UP, x2, y2, 0); - LatinKeyboardView.this.dispatchTouchEvent(me2); - me2.recycle(); - sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else - // happens - mDownDelivered = false; - break; - } - } - }; - - } - } - } - - private void findKeys() { - List<Key> keys = getKeyboard().getKeys(); - // Get the keys on this keyboard - for (int i = 0; i < keys.size(); i++) { - int code = keys.get(i).codes[0]; - if (code >= 0 && code <= 255) { - mAsciiKeys[code] = keys.get(i); - } - } - } - - public void startPlaying(String s) { - if (DEBUG_AUTO_PLAY) { - if (s == null) return; - mStringToPlay = s.toLowerCase(); - mPlaying = true; - mDownDelivered = false; - mStringIndex = 0; - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); - } - } - - @Override - public void draw(Canvas c) { - LatinIMEUtil.GCUtils.getInstance().reset(); - boolean tryGC = true; - for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { - try { - super.draw(c); - tryGC = false; - } catch (OutOfMemoryError e) { - tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e); - } - } - if (DEBUG_AUTO_PLAY) { - if (mPlaying) { - mHandler2.removeMessages(MSG_TOUCH_DOWN); - mHandler2.removeMessages(MSG_TOUCH_UP); - if (mDownDelivered) { - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); - } else { - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); - } - } - } - if (DEBUG_LINE) { - if (mPaint == null) { - mPaint = new Paint(); - mPaint.setColor(0x80FFFFFF); - mPaint.setAntiAlias(false); - } - c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint); - c.drawLine(0, mLastY, getWidth(), mLastY, mPaint); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java b/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java deleted file mode 100644 index 356e62d48..000000000 --- a/java/src/com/android/inputmethod/latin/MiniKeyboardKeyDetector.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.inputmethodservice.Keyboard.Key; - -class MiniKeyboardKeyDetector extends KeyDetector { - private static final int MAX_NEARBY_KEYS = 1; - - private final int mSlideAllowanceSquare; - private final int mSlideAllowanceSquareTop; - - public MiniKeyboardKeyDetector(float slideAllowance) { - super(); - mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance); - // Top slide allowance is slightly longer (sqrt(2) times) than other edges. - mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2; - } - - @Override - protected int getMaxNearbyKeys() { - return MAX_NEARBY_KEYS; - } - - @Override - public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) { - final Key[] keys = getKeys(); - final int touchX = getTouchX(x); - final int touchY = getTouchY(y); - int closestKeyIndex = LatinKeyboardBaseView.NOT_A_KEY; - int closestKeyDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare; - final int keyCount = keys.length; - for (int i = 0; i < keyCount; i++) { - final Key key = keys[i]; - int dist = key.squaredDistanceFrom(touchX, touchY); - if (dist < closestKeyDist) { - closestKeyIndex = i; - closestKeyDist = dist; - } - } - if (allKeys != null && closestKeyIndex != LatinKeyboardBaseView.NOT_A_KEY) - allKeys[0] = keys[closestKeyIndex].codes[0]; - return closestKeyIndex; - } -} diff --git a/java/src/com/android/inputmethod/latin/ModifierKeyState.java b/java/src/com/android/inputmethod/latin/ModifierKeyState.java deleted file mode 100644 index 097e87abe..000000000 --- a/java/src/com/android/inputmethod/latin/ModifierKeyState.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -class ModifierKeyState { - private static final int RELEASING = 0; - private static final int PRESSING = 1; - private static final int MOMENTARY = 2; - - private int mState = RELEASING; - - public void onPress() { - mState = PRESSING; - } - - public void onRelease() { - mState = RELEASING; - } - - public void onOtherKeyPressed() { - if (mState == PRESSING) - mState = MOMENTARY; - } - - public boolean isMomentary() { - return mState == MOMENTARY; - } -} diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java deleted file mode 100644 index b23b4d7b8..000000000 --- a/java/src/com/android/inputmethod/latin/PointerTracker.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener; -import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler; - -import android.content.res.Resources; -import android.inputmethodservice.Keyboard; -import android.inputmethodservice.Keyboard.Key; -import android.util.Log; -import android.view.MotionEvent; - -public class PointerTracker { - private static final String TAG = "PointerTracker"; - private static final boolean DEBUG = false; - private static final boolean DEBUG_MOVE = false; - - public interface UIProxy { - public void invalidateKey(Key key); - public void showPreview(int keyIndex, PointerTracker tracker); - public boolean hasDistinctMultitouch(); - } - - public final int mPointerId; - - // Timing constants - private final int mDelayBeforeKeyRepeatStart; - private final int mLongPressKeyTimeout; - private final int mMultiTapKeyTimeout; - - // Miscellaneous constants - private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY; - private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; - - private final UIProxy mProxy; - private final UIHandler mHandler; - private final KeyDetector mKeyDetector; - private OnKeyboardActionListener mListener; - private final KeyboardSwitcher mKeyboardSwitcher; - private final boolean mHasDistinctMultitouch; - - private Key[] mKeys; - private int mKeyHysteresisDistanceSquared = -1; - - private final KeyState mKeyState; - - // true if keyboard layout has been changed. - private boolean mKeyboardLayoutHasBeenChanged; - - // true if event is already translated to a key action (long press or mini-keyboard) - private boolean mKeyAlreadyProcessed; - - // true if this pointer is repeatable key - private boolean mIsRepeatableKey; - - // true if this pointer is in sliding key input - private boolean mIsInSlidingKeyInput; - - // For multi-tap - private int mLastSentIndex; - private int mTapCount; - private long mLastTapTime; - private boolean mInMultiTap; - private final StringBuilder mPreviewLabel = new StringBuilder(1); - - // pressed key - private int mPreviousKey = NOT_A_KEY; - - // This class keeps track of a key index and a position where this pointer is. - private static class KeyState { - private final KeyDetector mKeyDetector; - - // The position and time at which first down event occurred. - private int mStartX; - private int mStartY; - private long mDownTime; - - // The current key index where this pointer is. - private int mKeyIndex = NOT_A_KEY; - // The position where mKeyIndex was recognized for the first time. - private int mKeyX; - private int mKeyY; - - // Last pointer position. - private int mLastX; - private int mLastY; - - public KeyState(KeyDetector keyDetecor) { - mKeyDetector = keyDetecor; - } - - public int getKeyIndex() { - return mKeyIndex; - } - - public int getKeyX() { - return mKeyX; - } - - public int getKeyY() { - return mKeyY; - } - - public int getStartX() { - return mStartX; - } - - public int getStartY() { - return mStartY; - } - - public long getDownTime() { - return mDownTime; - } - - public int getLastX() { - return mLastX; - } - - public int getLastY() { - return mLastY; - } - - public int onDownKey(int x, int y, long eventTime) { - mStartX = x; - mStartY = y; - mDownTime = eventTime; - - return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); - } - - private int onMoveKeyInternal(int x, int y) { - mLastX = x; - mLastY = y; - return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); - } - - public int onMoveKey(int x, int y) { - return onMoveKeyInternal(x, y); - } - - public int onMoveToNewKey(int keyIndex, int x, int y) { - mKeyIndex = keyIndex; - mKeyX = x; - mKeyY = y; - return keyIndex; - } - - public int onUpKey(int x, int y) { - return onMoveKeyInternal(x, y); - } - } - - public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy, - Resources res) { - if (proxy == null || handler == null || keyDetector == null) - throw new NullPointerException(); - mPointerId = id; - mProxy = proxy; - mHandler = handler; - mKeyDetector = keyDetector; - mKeyboardSwitcher = KeyboardSwitcher.getInstance(); - mKeyState = new KeyState(keyDetector); - mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); - mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); - mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout); - mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout); - resetMultiTap(); - } - - public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { - mListener = listener; - } - - public void setKeyboard(Key[] keys, float keyHysteresisDistance) { - if (keys == null || keyHysteresisDistance < 0) - throw new IllegalArgumentException(); - mKeys = keys; - mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); - // Mark that keyboard layout has been changed. - mKeyboardLayoutHasBeenChanged = true; - } - - public boolean isInSlidingKeyInput() { - return mIsInSlidingKeyInput; - } - - private boolean isValidKeyIndex(int keyIndex) { - return keyIndex >= 0 && keyIndex < mKeys.length; - } - - public Key getKey(int keyIndex) { - return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null; - } - - private boolean isModifierInternal(int keyIndex) { - Key key = getKey(keyIndex); - if (key == null) - return false; - int primaryCode = key.codes[0]; - return primaryCode == Keyboard.KEYCODE_SHIFT - || primaryCode == Keyboard.KEYCODE_MODE_CHANGE; - } - - public boolean isModifier() { - return isModifierInternal(mKeyState.getKeyIndex()); - } - - public boolean isOnModifierKey(int x, int y) { - return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null)); - } - - public boolean isSpaceKey(int keyIndex) { - Key key = getKey(keyIndex); - return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE; - } - - public void updateKey(int keyIndex) { - if (mKeyAlreadyProcessed) - return; - int oldKeyIndex = mPreviousKey; - mPreviousKey = keyIndex; - if (keyIndex != oldKeyIndex) { - if (isValidKeyIndex(oldKeyIndex)) { - // if new key index is not a key, old key was just released inside of the key. - final boolean inside = (keyIndex == NOT_A_KEY); - mKeys[oldKeyIndex].onReleased(inside); - mProxy.invalidateKey(mKeys[oldKeyIndex]); - } - if (isValidKeyIndex(keyIndex)) { - mKeys[keyIndex].onPressed(); - mProxy.invalidateKey(mKeys[keyIndex]); - } - } - } - - public void setAlreadyProcessed() { - mKeyAlreadyProcessed = true; - } - - public void onTouchEvent(int action, int x, int y, long eventTime) { - switch (action) { - case MotionEvent.ACTION_MOVE: - onMoveEvent(x, y, eventTime); - break; - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - onDownEvent(x, y, eventTime); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - onUpEvent(x, y, eventTime); - break; - case MotionEvent.ACTION_CANCEL: - onCancelEvent(x, y, eventTime); - break; - } - } - - public void onDownEvent(int x, int y, long eventTime) { - if (DEBUG) - debugLog("onDownEvent:", x, y); - int keyIndex = mKeyState.onDownKey(x, y, eventTime); - mKeyboardLayoutHasBeenChanged = false; - mKeyAlreadyProcessed = false; - mIsRepeatableKey = false; - mIsInSlidingKeyInput = false; - checkMultiTap(eventTime, keyIndex); - if (mListener != null) { - if (isValidKeyIndex(keyIndex)) { - mListener.onPress(mKeys[keyIndex].codes[0]); - // This onPress call may have changed keyboard layout. Those cases are detected at - // {@link #setKeyboard}. In those cases, we should update keyIndex according to the - // new keyboard layout. - if (mKeyboardLayoutHasBeenChanged) { - mKeyboardLayoutHasBeenChanged = false; - keyIndex = mKeyState.onDownKey(x, y, eventTime); - } - } - } - if (isValidKeyIndex(keyIndex)) { - if (mKeys[keyIndex].repeatable) { - repeatKey(keyIndex); - mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this); - mIsRepeatableKey = true; - } - startLongPressTimer(keyIndex); - } - showKeyPreviewAndUpdateKey(keyIndex); - } - - public void onMoveEvent(int x, int y, long eventTime) { - if (DEBUG_MOVE) - debugLog("onMoveEvent:", x, y); - if (mKeyAlreadyProcessed) - return; - final KeyState keyState = mKeyState; - int keyIndex = keyState.onMoveKey(x, y); - final Key oldKey = getKey(keyState.getKeyIndex()); - if (isValidKeyIndex(keyIndex)) { - if (oldKey == null) { - // The pointer has been slid in to the new key, but the finger was not on any keys. - // In this case, we must call onPress() to notify that the new key is being pressed. - if (mListener != null) { - mListener.onPress(getKey(keyIndex).codes[0]); - // This onPress call may have changed keyboard layout. Those cases are detected - // at {@link #setKeyboard}. In those cases, we should update keyIndex according - // to the new keyboard layout. - if (mKeyboardLayoutHasBeenChanged) { - mKeyboardLayoutHasBeenChanged = false; - keyIndex = keyState.onMoveKey(x, y); - } - } - keyState.onMoveToNewKey(keyIndex, x, y); - startLongPressTimer(keyIndex); - } else if (!isMinorMoveBounce(x, y, keyIndex)) { - // The pointer has been slid in to the new key from the previous key, we must call - // onRelease() first to notify that the previous key has been released, then call - // onPress() to notify that the new key is being pressed. - mIsInSlidingKeyInput = true; - if (mListener != null) - mListener.onRelease(oldKey.codes[0]); - resetMultiTap(); - if (mListener != null) { - mListener.onPress(getKey(keyIndex).codes[0]); - // This onPress call may have changed keyboard layout. Those cases are detected - // at {@link #setKeyboard}. In those cases, we should update keyIndex according - // to the new keyboard layout. - if (mKeyboardLayoutHasBeenChanged) { - mKeyboardLayoutHasBeenChanged = false; - keyIndex = keyState.onMoveKey(x, y); - } - } - keyState.onMoveToNewKey(keyIndex, x, y); - startLongPressTimer(keyIndex); - } - } else { - if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) { - // The pointer has been slid out from the previous key, we must call onRelease() to - // notify that the previous key has been released. - mIsInSlidingKeyInput = true; - if (mListener != null) - mListener.onRelease(oldKey.codes[0]); - resetMultiTap(); - keyState.onMoveToNewKey(keyIndex, x ,y); - mHandler.cancelLongPressTimer(); - } - } - showKeyPreviewAndUpdateKey(keyState.getKeyIndex()); - } - - public void onUpEvent(int x, int y, long eventTime) { - if (DEBUG) - debugLog("onUpEvent :", x, y); - mHandler.cancelKeyTimers(); - mHandler.cancelPopupPreview(); - showKeyPreviewAndUpdateKey(NOT_A_KEY); - mIsInSlidingKeyInput = false; - if (mKeyAlreadyProcessed) - return; - int keyIndex = mKeyState.onUpKey(x, y); - if (isMinorMoveBounce(x, y, keyIndex)) { - // Use previous fixed key index and coordinates. - keyIndex = mKeyState.getKeyIndex(); - x = mKeyState.getKeyX(); - y = mKeyState.getKeyY(); - } - if (!mIsRepeatableKey) { - detectAndSendKey(keyIndex, x, y, eventTime); - } - - if (isValidKeyIndex(keyIndex)) - mProxy.invalidateKey(mKeys[keyIndex]); - } - - public void onCancelEvent(int x, int y, long eventTime) { - if (DEBUG) - debugLog("onCancelEvt:", x, y); - mHandler.cancelKeyTimers(); - mHandler.cancelPopupPreview(); - showKeyPreviewAndUpdateKey(NOT_A_KEY); - mIsInSlidingKeyInput = false; - int keyIndex = mKeyState.getKeyIndex(); - if (isValidKeyIndex(keyIndex)) - mProxy.invalidateKey(mKeys[keyIndex]); - } - - public void repeatKey(int keyIndex) { - Key key = getKey(keyIndex); - if (key != null) { - // While key is repeating, because there is no need to handle multi-tap key, we can - // pass -1 as eventTime argument. - detectAndSendKey(keyIndex, key.x, key.y, -1); - } - } - - public int getLastX() { - return mKeyState.getLastX(); - } - - public int getLastY() { - return mKeyState.getLastY(); - } - - public long getDownTime() { - return mKeyState.getDownTime(); - } - - // These package scope methods are only for debugging purpose. - /* package */ int getStartX() { - return mKeyState.getStartX(); - } - - /* package */ int getStartY() { - return mKeyState.getStartY(); - } - - private boolean isMinorMoveBounce(int x, int y, int newKey) { - if (mKeys == null || mKeyHysteresisDistanceSquared < 0) - throw new IllegalStateException("keyboard and/or hysteresis not set"); - int curKey = mKeyState.getKeyIndex(); - if (newKey == curKey) { - return true; - } else if (isValidKeyIndex(curKey)) { - return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared; - } else { - return false; - } - } - - private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { - final int left = key.x; - final int right = key.x + key.width; - final int top = key.y; - final int bottom = key.y + key.height; - final int edgeX = x < left ? left : (x > right ? right : x); - final int edgeY = y < top ? top : (y > bottom ? bottom : y); - final int dx = x - edgeX; - final int dy = y - edgeY; - return dx * dx + dy * dy; - } - - private void showKeyPreviewAndUpdateKey(int keyIndex) { - updateKey(keyIndex); - // The modifier key, such as shift key, should not be shown as preview when multi-touch is - // supported. On the other hand, if multi-touch is not supported, the modifier key should - // be shown as preview. - if (mHasDistinctMultitouch && isModifier()) { - mProxy.showPreview(NOT_A_KEY, this); - } else { - mProxy.showPreview(keyIndex, this); - } - } - - private void startLongPressTimer(int keyIndex) { - if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) { - // We use longer timeout for sliding finger input started from the symbols mode key. - mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this); - } else { - mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this); - } - } - - private void detectAndSendKey(int index, int x, int y, long eventTime) { - final OnKeyboardActionListener listener = mListener; - final Key key = getKey(index); - - if (key == null) { - if (listener != null) - listener.onCancel(); - } else { - if (key.text != null) { - if (listener != null) { - listener.onText(key.text); - listener.onRelease(0); // dummy key code - } - } else { - int code = key.codes[0]; - int[] codes = mKeyDetector.newCodeArray(); - mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes); - // Multi-tap - if (mInMultiTap) { - if (mTapCount != -1) { - mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y); - } else { - mTapCount = 0; - } - code = key.codes[mTapCount]; - } - /* - * Swap the first and second values in the codes array if the primary code is not - * the first value but the second value in the array. This happens when key - * debouncing is in effect. - */ - if (codes.length >= 2 && codes[0] != code && codes[1] == code) { - codes[1] = codes[0]; - codes[0] = code; - } - if (listener != null) { - listener.onKey(code, codes, x, y); - listener.onRelease(code); - } - } - mLastSentIndex = index; - mLastTapTime = eventTime; - } - } - - /** - * Handle multi-tap keys by producing the key label for the current multi-tap state. - */ - public CharSequence getPreviewText(Key key) { - if (mInMultiTap) { - // Multi-tap - mPreviewLabel.setLength(0); - mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); - return mPreviewLabel; - } else { - return key.label; - } - } - - private void resetMultiTap() { - mLastSentIndex = NOT_A_KEY; - mTapCount = 0; - mLastTapTime = -1; - mInMultiTap = false; - } - - private void checkMultiTap(long eventTime, int keyIndex) { - Key key = getKey(keyIndex); - if (key == null) - return; - - final boolean isMultiTap = - (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex); - if (key.codes.length > 1) { - mInMultiTap = true; - if (isMultiTap) { - mTapCount = (mTapCount + 1) % key.codes.length; - return; - } else { - mTapCount = -1; - return; - } - } - if (!isMultiTap) { - resetMultiTap(); - } - } - - private void debugLog(String title, int x, int y) { - int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); - Key key = getKey(keyIndex); - final String code; - if (key == null) { - code = "----"; - } else { - int primaryCode = key.codes[0]; - code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode); - } - Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title, - (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code, - (isModifier() ? "modifier" : ""))); - } -} diff --git a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java deleted file mode 100644 index 325ce674c..000000000 --- a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.inputmethodservice.Keyboard.Key; - -import java.util.Arrays; - -class ProximityKeyDetector extends KeyDetector { - private static final int MAX_NEARBY_KEYS = 12; - - // working area - private int[] mDistances = new int[MAX_NEARBY_KEYS]; - - @Override - protected int getMaxNearbyKeys() { - return MAX_NEARBY_KEYS; - } - - @Override - public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) { - final Key[] keys = getKeys(); - final int touchX = getTouchX(x); - final int touchY = getTouchY(y); - int primaryIndex = LatinKeyboardBaseView.NOT_A_KEY; - int closestKey = LatinKeyboardBaseView.NOT_A_KEY; - int closestKeyDist = mProximityThresholdSquare + 1; - int[] distances = mDistances; - Arrays.fill(distances, Integer.MAX_VALUE); - int [] nearestKeyIndices = mKeyboard.getNearestKeys(touchX, touchY); - final int keyCount = nearestKeyIndices.length; - for (int i = 0; i < keyCount; i++) { - final Key key = keys[nearestKeyIndices[i]]; - int dist = 0; - boolean isInside = key.isInside(touchX, touchY); - if (isInside) { - primaryIndex = nearestKeyIndices[i]; - } - - if (((mProximityCorrectOn - && (dist = key.squaredDistanceFrom(touchX, touchY)) < mProximityThresholdSquare) - || isInside) - && key.codes[0] > 32) { - // Find insertion point - final int nCodes = key.codes.length; - if (dist < closestKeyDist) { - closestKeyDist = dist; - closestKey = nearestKeyIndices[i]; - } - - if (allKeys == null) continue; - - for (int j = 0; j < distances.length; j++) { - if (distances[j] > dist) { - // Make space for nCodes codes - System.arraycopy(distances, j, distances, j + nCodes, - distances.length - j - nCodes); - System.arraycopy(allKeys, j, allKeys, j + nCodes, - allKeys.length - j - nCodes); - System.arraycopy(key.codes, 0, allKeys, j, nCodes); - Arrays.fill(distances, j, j + nCodes, dist); - break; - } - } - } - } - if (primaryIndex == LatinKeyboardBaseView.NOT_A_KEY) { - primaryIndex = closestKey; - } - return primaryIndex; - } -} diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java new file mode 100644 index 000000000..12338ce61 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.voice.VoiceIMEConnector; +import com.android.inputmethod.voice.VoiceInputLogger; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.backup.BackupManager; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Vibrator; +import android.preference.CheckBoxPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.speech.SpeechRecognizer; +import android.text.AutoText; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.widget.TextView; + +import java.util.Locale; + +public class Settings extends PreferenceActivity + implements SharedPreferences.OnSharedPreferenceChangeListener, + DialogInterface.OnDismissListener, OnPreferenceClickListener { + private static final String TAG = "Settings"; + + public static final String PREF_GENERAL_SETTINGS_KEY = "general_settings"; + public static final String PREF_VIBRATE_ON = "vibrate_on"; + public static final String PREF_SOUND_ON = "sound_on"; + public static final String PREF_POPUP_ON = "popup_on"; + public static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled"; + public static final String PREF_AUTO_CAP = "auto_cap"; + public static final String PREF_SETTINGS_KEY = "settings_key"; + public static final String PREF_VOICE_SETTINGS_KEY = "voice_mode"; + public static final String PREF_INPUT_LANGUAGE = "input_language"; + public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; + public static final String PREF_SUBTYPES = "subtype_settings"; + + public static final String PREF_PREDICTION_SETTINGS_KEY = "prediction_settings"; + public static final String PREF_QUICK_FIXES = "quick_fixes"; + public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; + public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; + public static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; + + public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; + + // Dialog ids + private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; + + private PreferenceScreen mInputLanguageSelection; + private CheckBoxPreference mQuickFixes; + private ListPreference mVoicePreference; + private ListPreference mSettingsKeyPreference; + private ListPreference mShowCorrectionSuggestionsPreference; + private ListPreference mAutoCorrectionThreshold; + private CheckBoxPreference mBigramSuggestion; + private boolean mVoiceOn; + + private AlertDialog mDialog; + + private VoiceInputLogger mLogger; + + private boolean mOkClicked = false; + private String mVoiceModeOff; + + private void ensureConsistencyOfAutoCorrectionSettings() { + final String autoCorrectionOff = getResources().getString( + R.string.auto_correction_threshold_mode_index_off); + final String currentSetting = mAutoCorrectionThreshold.getValue(); + mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff)); + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs); + mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES); + mInputLanguageSelection.setOnPreferenceClickListener(this); + mQuickFixes = (CheckBoxPreference) findPreference(PREF_QUICK_FIXES); + mVoicePreference = (ListPreference) findPreference(PREF_VOICE_SETTINGS_KEY); + mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY); + mShowCorrectionSuggestionsPreference = + (ListPreference) findPreference(PREF_SHOW_SUGGESTIONS_SETTING); + SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + prefs.registerOnSharedPreferenceChangeListener(this); + + mVoiceModeOff = getString(R.string.voice_mode_off); + mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + .equals(mVoiceModeOff)); + mLogger = VoiceInputLogger.getLogger(this); + + mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD); + mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS); + ensureConsistencyOfAutoCorrectionSettings(); + + final PreferenceGroup generalSettings = + (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS_KEY); + final PreferenceGroup textCorrectionGroup = + (PreferenceGroup) findPreference(PREF_PREDICTION_SETTINGS_KEY); + + final boolean showSettingsKeyOption = getResources().getBoolean( + R.bool.config_enable_show_settings_key_option); + if (!showSettingsKeyOption) { + generalSettings.removePreference(mSettingsKeyPreference); + } + + final boolean showVoiceKeyOption = getResources().getBoolean( + R.bool.config_enable_show_voice_key_option); + if (!showVoiceKeyOption) { + generalSettings.removePreference(mVoicePreference); + } + + Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); + if (vibrator == null || !vibrator.hasVibrator()) { + generalSettings.removePreference(findPreference(PREF_VIBRATE_ON)); + } + + final boolean showSubtypeSettings = getResources().getBoolean( + R.bool.config_enable_show_subtype_settings); + if (!showSubtypeSettings) { + generalSettings.removePreference(findPreference(PREF_SUBTYPES)); + } + + final boolean showPopupOption = getResources().getBoolean( + R.bool.config_enable_show_popup_on_keypress_option); + if (!showPopupOption) { + generalSettings.removePreference(findPreference(PREF_POPUP_ON)); + } + + final boolean showRecorrectionOption = getResources().getBoolean( + R.bool.config_enable_show_recorrection_option); + if (!showRecorrectionOption) { + generalSettings.removePreference(findPreference(PREF_RECORRECTION_ENABLED)); + } + + final boolean showQuickFixesOption = getResources().getBoolean( + R.bool.config_enable_quick_fixes_option); + if (!showQuickFixesOption) { + textCorrectionGroup.removePreference(findPreference(PREF_QUICK_FIXES)); + } + + final boolean showBigramSuggestionsOption = getResources().getBoolean( + R.bool.config_enable_bigram_suggestions_option); + if (!showBigramSuggestionsOption) { + textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_SUGGESTIONS)); + } + + final boolean showUsabilityModeStudyOption = getResources().getBoolean( + R.bool.config_enable_usability_study_mode_option); + if (!showUsabilityModeStudyOption) { + getPreferenceScreen().removePreference(findPreference(PREF_USABILITY_STUDY_MODE)); + } + } + + @Override + protected void onResume() { + super.onResume(); + int autoTextSize = AutoText.getSize(getListView()); + if (autoTextSize < 1) { + ((PreferenceGroup) findPreference(PREF_PREDICTION_SETTINGS_KEY)) + .removePreference(mQuickFixes); + } + if (!VoiceIMEConnector.VOICE_INSTALLED + || !SpeechRecognizer.isRecognitionAvailable(this)) { + getPreferenceScreen().removePreference(mVoicePreference); + } else { + updateVoiceModeSummary(); + } + updateSettingsKeySummary(); + updateShowCorrectionSuggestionsSummary(); + } + + @Override + protected void onDestroy() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( + this); + super.onDestroy(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + (new BackupManager(this)).dataChanged(); + // If turning on voice input, show dialog + if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) { + if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + .equals(mVoiceModeOff)) { + showVoiceConfirmation(); + } + } + ensureConsistencyOfAutoCorrectionSettings(); + mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) + .equals(mVoiceModeOff)); + updateVoiceModeSummary(); + updateSettingsKeySummary(); + updateShowCorrectionSuggestionsSummary(); + } + + @Override + public boolean onPreferenceClick(Preference pref) { + if (pref == mInputLanguageSelection) { + final String action; + if (android.os.Build.VERSION.SDK_INT + >= /* android.os.Build.VERSION_CODES.HONEYCOMB */ 11) { + action = "android.settings.INPUT_METHOD_AND_SUBTYPE_ENABLER"; + } else { + action = "com.android.inputmethod.latin.INPUT_LANGUAGE_SELECTION"; + } + startActivity(new Intent(action)); + return true; + } + return false; + } + + private void updateShowCorrectionSuggestionsSummary() { + mShowCorrectionSuggestionsPreference.setSummary( + getResources().getStringArray(R.array.prefs_suggestion_visibilities) + [mShowCorrectionSuggestionsPreference.findIndexOfValue( + mShowCorrectionSuggestionsPreference.getValue())]); + } + + private void updateSettingsKeySummary() { + mSettingsKeyPreference.setSummary( + getResources().getStringArray(R.array.settings_key_modes) + [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]); + } + + private void showVoiceConfirmation() { + mOkClicked = false; + showDialog(VOICE_INPUT_CONFIRM_DIALOG); + // Make URL in the dialog message clickable + if (mDialog != null) { + TextView textView = (TextView) mDialog.findViewById(android.R.id.message); + if (textView != null) { + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } + } + } + + private void updateVoiceModeSummary() { + mVoicePreference.setSummary( + getResources().getStringArray(R.array.voice_input_modes_summary) + [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]); + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case VOICE_INPUT_CONFIRM_DIALOG: + DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + if (whichButton == DialogInterface.BUTTON_NEGATIVE) { + mVoicePreference.setValue(mVoiceModeOff); + mLogger.settingsWarningDialogCancel(); + } else if (whichButton == DialogInterface.BUTTON_POSITIVE) { + mOkClicked = true; + mLogger.settingsWarningDialogOk(); + } + updateVoicePreference(); + } + }; + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(R.string.voice_warning_title) + .setPositiveButton(android.R.string.ok, listener) + .setNegativeButton(android.R.string.cancel, listener); + + // Get the current list of supported locales and check the current locale against + // that list, to decide whether to put a warning that voice input will not work in + // the current language as part of the pop-up confirmation dialog. + boolean localeSupported = SubtypeSwitcher.getInstance().isVoiceSupported( + Locale.getDefault().toString()); + + final CharSequence message; + if (localeSupported) { + message = TextUtils.concat( + getText(R.string.voice_warning_may_not_understand), "\n\n", + getText(R.string.voice_hint_dialog_message)); + } else { + message = TextUtils.concat( + getText(R.string.voice_warning_locale_not_supported), "\n\n", + getText(R.string.voice_warning_may_not_understand), "\n\n", + getText(R.string.voice_hint_dialog_message)); + } + builder.setMessage(message); + AlertDialog dialog = builder.create(); + mDialog = dialog; + dialog.setOnDismissListener(this); + mLogger.settingsWarningDialogShown(); + return dialog; + default: + Log.e(TAG, "unknown dialog " + id); + return null; + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + mLogger.settingsWarningDialogDismissed(); + if (!mOkClicked) { + // This assumes that onPreferenceClick gets called first, and this if the user + // agreed after the warning, we set the mOkClicked value to true. + mVoicePreference.setValue(mVoiceModeOff); + } + } + + private void updateVoicePreference() { + boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff); + if (isChecked) { + mLogger.voiceInputSettingEnabled(); + } else { + mLogger.voiceInputSettingDisabled(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java new file mode 100644 index 000000000..917521c40 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 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.Resources; + +import java.util.Locale; + +public class SubtypeLocale { + private static String[] sExceptionKeys; + private static String[] sExceptionValues; + + private SubtypeLocale() { + // Intentional empty constructor for utility class. + } + + public static void init(Context context) { + final Resources res = context.getResources(); + sExceptionKeys = res.getStringArray(R.array.subtype_locale_exception_keys); + sExceptionValues = res.getStringArray(R.array.subtype_locale_exception_values); + } + + public static String getFullDisplayName(Locale locale) { + String localeCode = locale.toString(); + for (int index = 0; index < sExceptionKeys.length; index++) { + if (sExceptionKeys[index].equals(localeCode)) + return sExceptionValues[index]; + } + return locale.getDisplayName(locale); + } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java new file mode 100644 index 000000000..f4262cc99 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.LatinKeyboard; +import com.android.inputmethod.keyboard.LatinKeyboardView; +import com.android.inputmethod.voice.SettingsUtil; +import com.android.inputmethod.voice.VoiceIMEConnector; +import com.android.inputmethod.voice.VoiceInput; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.IBinder; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class SubtypeSwitcher { + private static boolean DBG = LatinImeLogger.sDBG; + private static final String TAG = "SubtypeSwitcher"; + + private static final char LOCALE_SEPARATER = '_'; + private static final String KEYBOARD_MODE = "keyboard"; + private static final String VOICE_MODE = "voice"; + private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY = + "requireNetworkConnectivity"; + private final TextUtils.SimpleStringSplitter mLocaleSplitter = + new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); + + private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); + private /* final */ LatinIME mService; + private /* final */ SharedPreferences mPrefs; + private /* final */ InputMethodManager mImm; + private /* final */ Resources mResources; + private /* final */ ConnectivityManager mConnectivityManager; + private /* final */ boolean mConfigUseSpacebarLanguageSwitcher; + private final ArrayList<InputMethodSubtype> mEnabledKeyboardSubtypesOfCurrentInputMethod = + new ArrayList<InputMethodSubtype>(); + private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); + + /*-----------------------------------------------------------*/ + // Variants which should be changed only by reload functions. + private boolean mNeedsToDisplayLanguage; + private boolean mIsSystemLanguageSameAsInputLanguage; + private InputMethodInfo mShortcutInputMethodInfo; + private InputMethodSubtype mShortcutSubtype; + private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod; + private Locale mSystemLocale; + private Locale mInputLocale; + private String mInputLocaleStr; + private String mMode; + private VoiceInput mVoiceInput; + /*-----------------------------------------------------------*/ + + private boolean mIsNetworkConnected; + + public static SubtypeSwitcher getInstance() { + return sInstance; + } + + public static void init(LatinIME service, SharedPreferences prefs) { + sInstance.initialize(service, prefs); + sInstance.updateAllParameters(); + + SubtypeLocale.init(service); + } + + private SubtypeSwitcher() { + // Intentional empty constructor for singleton. + } + + private void initialize(LatinIME service, SharedPreferences prefs) { + mService = service; + mPrefs = prefs; + mResources = service.getResources(); + mImm = (InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE); + mConnectivityManager = (ConnectivityManager) service.getSystemService( + Context.CONNECTIVITY_SERVICE); + mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); + mEnabledLanguagesOfCurrentInputMethod.clear(); + mSystemLocale = null; + mInputLocale = null; + mInputLocaleStr = null; + // Mode is initialized to KEYBOARD_MODE, in case that LatinIME can't obtain currentSubtype + mMode = KEYBOARD_MODE; + mAllEnabledSubtypesOfCurrentInputMethod = null; + // TODO: Voice input should be created here + mVoiceInput = null; + mConfigUseSpacebarLanguageSwitcher = mResources.getBoolean( + R.bool.config_use_spacebar_language_switcher); + if (mConfigUseSpacebarLanguageSwitcher) + initLanguageSwitcher(service); + + final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + mIsNetworkConnected = (info != null && info.isConnected()); + } + + // Update all parameters stored in SubtypeSwitcher. + // Only configuration changed event is allowed to call this because this is heavy. + private void updateAllParameters() { + mSystemLocale = mResources.getConfiguration().locale; + updateSubtype(mImm.getCurrentInputMethodSubtype()); + updateParametersOnStartInputView(); + } + + // Update parameters which are changed outside LatinIME. This parameters affect UI so they + // should be updated every time onStartInputview. + public void updateParametersOnStartInputView() { + if (mConfigUseSpacebarLanguageSwitcher) { + updateForSpacebarLanguageSwitch(); + } else { + updateEnabledSubtypes(); + } + updateShortcutIME(); + } + + // Reload enabledSubtypes from the framework. + private void updateEnabledSubtypes() { + boolean foundCurrentSubtypeBecameDisabled = true; + mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList( + null, true); + mEnabledLanguagesOfCurrentInputMethod.clear(); + mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); + for (InputMethodSubtype ims: mAllEnabledSubtypesOfCurrentInputMethod) { + final String locale = ims.getLocale(); + final String mode = ims.getMode(); + mLocaleSplitter.setString(locale); + if (mLocaleSplitter.hasNext()) { + mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); + } + if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) { + foundCurrentSubtypeBecameDisabled = false; + } + if (KEYBOARD_MODE.equals(ims.getMode())) { + mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims); + } + } + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && mIsSystemLanguageSameAsInputLanguage); + if (foundCurrentSubtypeBecameDisabled) { + if (DBG) { + Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + mMode); + Log.w(TAG, "Last subtype was disabled. Update to the current one."); + } + updateSubtype(mImm.getCurrentInputMethodSubtype()); + } + } + + private void updateShortcutIME() { + if (DBG) { + Log.d(TAG, "Update shortcut IME from : " + + (mShortcutInputMethodInfo == null + ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale() + + ", " + mShortcutSubtype.getMode()))); + } + // TODO: Update an icon for shortcut IME + Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts = + mImm.getShortcutInputMethodsAndSubtypes(); + for (InputMethodInfo imi: shortcuts.keySet()) { + List<InputMethodSubtype> subtypes = shortcuts.get(imi); + // TODO: Returns the first found IMI for now. Should handle all shortcuts as + // appropriate. + mShortcutInputMethodInfo = imi; + // TODO: Pick up the first found subtype for now. Should handle all subtypes + // as appropriate. + mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; + break; + } + if (DBG) { + Log.d(TAG, "Update shortcut IME to : " + + (mShortcutInputMethodInfo == null + ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale() + + ", " + mShortcutSubtype.getMode()))); + } + } + + // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. + public void updateSubtype(InputMethodSubtype newSubtype) { + final String newLocale; + final String newMode; + if (newSubtype == null) { + // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, + // fallback to the default locale and mode. + Log.w(TAG, "Couldn't get the current subtype."); + newLocale = "en_US"; + newMode = KEYBOARD_MODE; + } else { + newLocale = newSubtype.getLocale(); + newMode = newSubtype.getMode(); + } + if (DBG) { + Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode + + ", from: " + mInputLocaleStr + ", " + mMode); + } + boolean languageChanged = false; + if (!newLocale.equals(mInputLocaleStr)) { + if (mInputLocaleStr != null) { + languageChanged = true; + } + updateInputLocale(newLocale); + } + boolean modeChanged = false; + String oldMode = mMode; + if (!newMode.equals(mMode)) { + if (mMode != null) { + modeChanged = true; + } + mMode = newMode; + } + + // If the old mode is voice input, we need to reset or cancel its status. + // We cancel its status when we change mode, while we reset otherwise. + if (isKeyboardMode()) { + if (modeChanged) { + if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { + mVoiceInput.cancel(); + } + } + if (modeChanged || languageChanged) { + updateShortcutIME(); + mService.onRefreshKeyboard(); + } + } else if (isVoiceMode() && mVoiceInput != null) { + if (VOICE_MODE.equals(oldMode)) { + mVoiceInput.reset(); + } + // If needsToShowWarningDialog is true, voice input need to show warning before + // show recognition view. + if (languageChanged || modeChanged + || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) { + triggerVoiceIME(); + } + } else { + Log.w(TAG, "Unknown subtype mode: " + mMode); + if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { + // We need to reset the voice input to release the resources and to reset its status + // as it is not the current input mode. + mVoiceInput.reset(); + } + } + } + + // Update the current input locale from Locale string. + private void updateInputLocale(String inputLocaleStr) { + // example: inputLocaleStr = "en_US" "en" "" + // "en_US" --> language: en & country: US + // "en" --> language: en + // "" --> the system locale + mLocaleSplitter.setString(inputLocaleStr); + if (mLocaleSplitter.hasNext()) { + String language = mLocaleSplitter.next(); + if (mLocaleSplitter.hasNext()) { + mInputLocale = new Locale(language, mLocaleSplitter.next()); + } else { + mInputLocale = new Locale(language); + } + mInputLocaleStr = inputLocaleStr; + } else { + mInputLocale = mSystemLocale; + String country = mSystemLocale.getCountry(); + mInputLocaleStr = mSystemLocale.getLanguage() + + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage()); + } + mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocale().getLanguage()); + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && mIsSystemLanguageSameAsInputLanguage); + } + + //////////////////////////// + // Shortcut IME functions // + //////////////////////////// + + public void switchToShortcutIME() { + final IBinder token = mService.getWindow().getWindow().getAttributes().token; + if (token == null || mShortcutInputMethodInfo == null) { + return; + } + final String imiId = mShortcutInputMethodInfo.getId(); + final InputMethodSubtype subtype = mShortcutSubtype; + new Thread("SwitchToShortcutIME") { + @Override + public void run() { + mImm.setInputMethodAndSubtype(token, imiId, subtype); + } + }.start(); + } + + public Drawable getShortcutIcon() { + return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype); + } + + private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) { + final PackageManager pm = mService.getPackageManager(); + if (imi != null) { + final String imiPackageName = imi.getPackageName(); + if (DBG) { + Log.d(TAG, "Update icons of IME: " + imiPackageName + "," + + subtype.getLocale() + "," + subtype.getMode()); + } + if (subtype != null) { + return pm.getDrawable(imiPackageName, subtype.getIconResId(), + imi.getServiceInfo().applicationInfo); + } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) { + return pm.getDrawable(imiPackageName, + imi.getSubtypeAt(0).getIconResId(), + imi.getServiceInfo().applicationInfo); + } else { + try { + return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "IME can't be found: " + imiPackageName); + } + } + } + return null; + } + + private static boolean contains(String[] hay, String needle) { + for (String element : hay) { + if (element.equals(needle)) + return true; + } + return false; + } + + public boolean isShortcutAvailable() { + if (mShortcutInputMethodInfo == null) + return false; + if (mShortcutSubtype != null && contains(mShortcutSubtype.getExtraValue().split(","), + SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) { + return mIsNetworkConnected; + } + return true; + } + + public void onNetworkStateChanged(Intent intent) { + final boolean noConnection = intent.getBooleanExtra( + ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + mIsNetworkConnected = !noConnection; + + final LatinKeyboardView inputView = KeyboardSwitcher.getInstance().getInputView(); + if (inputView != null) { + final LatinKeyboard keyboard = inputView.getLatinKeyboard(); + if (keyboard != null) { + keyboard.updateShortcutKey(isShortcutAvailable(), inputView); + } + } + } + + ////////////////////////////////// + // Language Switching functions // + ////////////////////////////////// + + public int getEnabledKeyboardLocaleCount() { + if (mConfigUseSpacebarLanguageSwitcher) { + return mLanguageSwitcher.getLocaleCount(); + } else { + return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); + } + } + + public boolean useSpacebarLanguageSwitcher() { + return mConfigUseSpacebarLanguageSwitcher; + } + + public boolean needsToDisplayLanguage() { + return mNeedsToDisplayLanguage; + } + + public Locale getInputLocale() { + if (mConfigUseSpacebarLanguageSwitcher) { + return mLanguageSwitcher.getInputLocale(); + } else { + return mInputLocale; + } + } + + public String getInputLocaleStr() { + if (mConfigUseSpacebarLanguageSwitcher) { + String inputLanguage = null; + inputLanguage = mLanguageSwitcher.getInputLanguage(); + // Should return system locale if there is no Language available. + if (inputLanguage == null) { + inputLanguage = getSystemLocale().getLanguage(); + } + return inputLanguage; + } else { + return mInputLocaleStr; + } + } + + public String[] getEnabledLanguages() { + if (mConfigUseSpacebarLanguageSwitcher) { + return mLanguageSwitcher.getEnabledLanguages(); + } else { + return mEnabledLanguagesOfCurrentInputMethod.toArray( + new String[mEnabledLanguagesOfCurrentInputMethod.size()]); + } + } + + public Locale getSystemLocale() { + if (mConfigUseSpacebarLanguageSwitcher) { + return mLanguageSwitcher.getSystemLocale(); + } else { + return mSystemLocale; + } + } + + public boolean isSystemLanguageSameAsInputLanguage() { + if (mConfigUseSpacebarLanguageSwitcher) { + return getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocaleStr().substring(0, 2)); + } else { + return mIsSystemLanguageSameAsInputLanguage; + } + } + + public void onConfigurationChanged(Configuration conf) { + final Locale systemLocale = conf.locale; + // If system configuration was changed, update all parameters. + if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { + if (mConfigUseSpacebarLanguageSwitcher) { + // If the system locale changes and is different from the saved + // locale (mSystemLocale), then reload the input locale list from the + // latin ime settings (shared prefs) and reset the input locale + // to the first one. + mLanguageSwitcher.loadLocales(mPrefs); + mLanguageSwitcher.setSystemLocale(systemLocale); + } else { + updateAllParameters(); + } + } + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (mConfigUseSpacebarLanguageSwitcher) { + if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { + mLanguageSwitcher.loadLocales(sharedPreferences); + } + } + } + + /** + * Change system locale for this application + * @param newLocale + * @return oldLocale + */ + public Locale changeSystemLocale(Locale newLocale) { + Configuration conf = mResources.getConfiguration(); + Locale oldLocale = conf.locale; + conf.locale = newLocale; + mResources.updateConfiguration(conf, mResources.getDisplayMetrics()); + return oldLocale; + } + + public boolean isKeyboardMode() { + return KEYBOARD_MODE.equals(mMode); + } + + + /////////////////////////// + // Voice Input functions // + /////////////////////////// + + public boolean setVoiceInput(VoiceInput vi) { + if (mVoiceInput == null && vi != null) { + mVoiceInput = vi; + if (isVoiceMode()) { + if (DBG) { + Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr()); + } + triggerVoiceIME(); + return true; + } + } + return false; + } + + public boolean isVoiceMode() { + return VOICE_MODE.equals(mMode); + } + + private void triggerVoiceIME() { + if (!mService.isInputViewShown()) return; + VoiceIMEConnector.getInstance().startListening(false, + KeyboardSwitcher.getInstance().getInputView().getWindowToken()); + } + + ////////////////////////////////////// + // Spacebar Language Switch support // + ////////////////////////////////////// + + private LanguageSwitcher mLanguageSwitcher; + + public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) { + if (returnsNameInThisLocale) { + return toTitleCase(SubtypeLocale.getFullDisplayName(locale)); + } else { + return toTitleCase(locale.getDisplayName()); + } + } + + public static String getDisplayLanguage(Locale locale) { + return toTitleCase(locale.getDisplayLanguage(locale)); + } + + public static String getShortDisplayLanguage(Locale locale) { + return toTitleCase(locale.getLanguage()); + } + + private static String toTitleCase(String s) { + if (s.length() == 0) { + return s; + } + return Character.toUpperCase(s.charAt(0)) + s.substring(1); + } + + private void updateForSpacebarLanguageSwitch() { + // We need to update mNeedsToDisplayLanguage in onStartInputView because + // getEnabledKeyboardLocaleCount could have been changed. + mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 + && getSystemLocale().getLanguage().equalsIgnoreCase( + getInputLocale().getLanguage())); + } + + public String getInputLanguageName() { + return getDisplayLanguage(getInputLocale()); + } + + public String getNextInputLanguageName() { + if (mConfigUseSpacebarLanguageSwitcher) { + return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale()); + } else { + return ""; + } + } + + public String getPreviousInputLanguageName() { + if (mConfigUseSpacebarLanguageSwitcher) { + return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale()); + } else { + return ""; + } + } + + // A list of locales which are supported by default for voice input, unless we get a + // different list from Gservices. + private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = + "en " + + "en_US " + + "en_GB " + + "en_AU " + + "en_CA " + + "en_IE " + + "en_IN " + + "en_NZ " + + "en_SG " + + "en_ZA "; + + public boolean isVoiceSupported(String locale) { + // Get the current list of supported locales and check the current locale against that + // list. We cache this value so as not to check it every time the user starts a voice + // input. Because this method is called by onStartInputView, this should mean that as + // long as the locale doesn't change while the user is keeping the IME open, the + // value should never be stale. + String supportedLocalesString = SettingsUtil.getSettingsString( + mService.getContentResolver(), + SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, + DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); + List<String> voiceInputSupportedLocales = Arrays.asList( + supportedLocalesString.split("\\s+")); + return voiceInputSupportedLocales.contains(locale); + } + + public void loadSettings() { + if (mConfigUseSpacebarLanguageSwitcher) { + mLanguageSwitcher.loadLocales(mPrefs); + } + } + + public void toggleLanguage(boolean reset, boolean next) { + if (mConfigUseSpacebarLanguageSwitcher) { + if (reset) { + mLanguageSwitcher.reset(); + } else { + if (next) { + mLanguageSwitcher.next(); + } else { + mLanguageSwitcher.prev(); + } + } + mLanguageSwitcher.persist(mPrefs); + } + } + + private void initLanguageSwitcher(LatinIME service) { + final Configuration conf = service.getResources().getConfiguration(); + mLanguageSwitcher = new LanguageSwitcher(service); + mLanguageSwitcher.loadLocales(mPrefs); + mLanguageSwitcher.setSystemLocale(conf.locale); + } +} diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 3b898941f..ced355bb2 100755..100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2008 The Android Open Source Project - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -16,24 +16,23 @@ package com.android.inputmethod.latin; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import android.content.Context; import android.text.AutoText; import android.text.TextUtils; import android.util.Log; import android.view.View; +import java.util.ArrayList; +import java.util.Arrays; + /** - * This class loads a dictionary and provides a list of suggestions for a given sequence of + * This class loads a dictionary and provides a list of suggestions for a given sequence of * characters. This includes corrections and completions. - * @hide pending API Council Approval */ public class Suggest implements Dictionary.WordCallback { + public static final String TAG = Suggest.class.getSimpleName(); + public static final int APPROX_MAX_WORD_LENGTH = 32; public static final int CORRECTION_NONE = 0; @@ -65,6 +64,8 @@ public class Suggest implements Dictionary.WordCallback { static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; + private static boolean DBG = LatinImeLogger.sDBG; + private BinaryDictionary mMainDict; private Dictionary mUserDictionary; @@ -81,6 +82,7 @@ public class Suggest implements Dictionary.WordCallback { private boolean mAutoTextEnabled; + private double mAutoCorrectionThreshold; private int[] mPriorities = new int[mPrefMaxSuggestions]; private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; @@ -93,8 +95,7 @@ public class Suggest implements Dictionary.WordCallback { private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>(); ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>(); private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>(); - private boolean mHaveCorrection; - private CharSequence mOriginalWord; + private boolean mHasAutoCorrection; private String mLowerOriginalWord; // TODO: Remove these member variables by passing more context to addWord() callback method @@ -103,13 +104,8 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; - public Suggest(Context context, int[] dictionaryResId) { - mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN); - initPool(); - } - - public Suggest(Context context, ByteBuffer byteBuffer) { - mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN); + public Suggest(Context context, int dictionaryResId) { + mMainDict = BinaryDictionary.initDictionary(context, dictionaryResId, DIC_MAIN); initPool(); } @@ -133,7 +129,7 @@ public class Suggest implements Dictionary.WordCallback { } public boolean hasMainDictionary() { - return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; + return mMainDict != null && mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; } public int getApproxMaxWordLength() { @@ -154,7 +150,7 @@ public class Suggest implements Dictionary.WordCallback { public void setContactsDictionary(Dictionary userDictionary) { mContactsDictionary = userDictionary; } - + public void setAutoDictionary(Dictionary autoDictionary) { mAutoDictionary = autoDictionary; } @@ -163,6 +159,14 @@ public class Suggest implements Dictionary.WordCallback { mUserBigramDictionary = userBigramDictionary; } + public void setAutoCorrectionThreshold(double threshold) { + mAutoCorrectionThreshold = threshold; + } + + public boolean isAggressiveAutoCorrectionMode() { + return (mAutoCorrectionThreshold == 0); + } + /** * Number of suggestions to generate from the input key sequence. This has * to be a number between 1 and 100 (inclusive). @@ -183,45 +187,24 @@ public class Suggest implements Dictionary.WordCallback { } } - private boolean haveSufficientCommonality(String original, CharSequence suggestion) { - final int originalLength = original.length(); - final int suggestionLength = suggestion.length(); - final int minLength = Math.min(originalLength, suggestionLength); - if (minLength <= 2) return true; - int matching = 0; - int lessMatching = 0; // Count matches if we skip one character - int i; - for (i = 0; i < minLength; i++) { - final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i)); - if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) { - matching++; - lessMatching++; - } else if (i + 1 < suggestionLength - && origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) { - lessMatching++; - } - } - matching = Math.max(matching, lessMatching); - - if (minLength <= 4) { - return matching >= 2; - } else { - return matching > minLength / 2; - } - } - /** - * Returns a list of words that match the list of character codes passed in. - * This list will be overwritten the next time this function is called. + * Returns a object which represents suggested words that match the list of character codes + * passed in. This object contents will be overwritten the next time this function is called. * @param view a view for retrieving the context for AutoText * @param wordComposer contains what is currently being typed * @param prevWordForBigram previous word (used only for bigram) - * @return list of suggestions. + * @return suggested words object. */ - public List<CharSequence> getSuggestions(View view, WordComposer wordComposer, - boolean includeTypedWordIfValid, CharSequence prevWordForBigram) { + public SuggestedWords getSuggestions(View view, WordComposer wordComposer, + CharSequence prevWordForBigram) { + return getSuggestedWordBuilder(view, wordComposer, prevWordForBigram).build(); + } + + // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder + public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer, + CharSequence prevWordForBigram) { LatinImeLogger.onStartSuggestion(prevWordForBigram); - mHaveCorrection = false; + mHasAutoCorrection = false; mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); collectGarbage(mSuggestions, mPrefMaxSuggestions); @@ -229,13 +212,13 @@ public class Suggest implements Dictionary.WordCallback { Arrays.fill(mNextLettersFrequencies, 0); // Save a lowercase version of the original word - mOriginalWord = wordComposer.getTypedWord(); - if (mOriginalWord != null) { - final String mOriginalWordString = mOriginalWord.toString(); - mOriginalWord = mOriginalWordString; - mLowerOriginalWord = mOriginalWordString.toLowerCase(); + CharSequence typedWord = wordComposer.getTypedWord(); + if (typedWord != null) { + final String typedWordString = typedWord.toString(); + typedWord = typedWordString; + mLowerOriginalWord = typedWordString.toLowerCase(); // Treating USER_TYPED as UNIGRAM suggestion for logging now. - LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED, + LatinImeLogger.onAddSuggestedWord(typedWordString, Suggest.DIC_USER_TYPED, Dictionary.DataType.UNIGRAM); } else { mLowerOriginalWord = ""; @@ -249,7 +232,7 @@ public class Suggest implements Dictionary.WordCallback { if (!TextUtils.isEmpty(prevWordForBigram)) { CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase(); - if (mMainDict.isValidWord(lowerPrevWord)) { + if (mMainDict != null && mMainDict.isValidWord(lowerPrevWord)) { prevWordForBigram = lowerPrevWord; } if (mUserBigramDictionary != null) { @@ -293,29 +276,37 @@ public class Suggest implements Dictionary.WordCallback { mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies); } - if (mSuggestions.size() > 0 && isValidWord(mOriginalWord) + if (mSuggestions.size() > 0 && isValidWord(typedWord) && (mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { - mHaveCorrection = true; + if (DBG) { + Log.d(TAG, "Auto corrected by CORRECTION_FULL."); + } + mHasAutoCorrection = true; } } - mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); + if (mMainDict != null) mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM) - && mSuggestions.size() > 0) { - mHaveCorrection = true; + && mSuggestions.size() > 0 && mPriorities.length > 0) { + // TODO: when the normalized score of the first suggestion is nearly equals to + // the normalized score of the second suggestion, behave less aggressive. + final double normalizedScore = Utils.calcNormalizedScore( + typedWord, mSuggestions.get(0), mPriorities[0]); + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Normalized " + typedWord + "," + mSuggestions.get(0) + "," + + mPriorities[0] + ", " + normalizedScore + + "(" + mAutoCorrectionThreshold + ")"); + } + if (normalizedScore >= mAutoCorrectionThreshold) { + if (DBG) { + Log.d(TAG, "Auto corrected by S-threthhold."); + } + mHasAutoCorrection = true; + } } } - if (mOriginalWord != null) { - mSuggestions.add(0, mOriginalWord.toString()); - } - - // Check if the first suggestion has a minimum number of characters in common - if (wordComposer.size() > 1 && mSuggestions.size() > 1 - && (mCorrectionMode == CORRECTION_FULL - || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { - if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { - mHaveCorrection = false; - } + if (typedWord != null) { + mSuggestions.add(0, typedWord.toString()); } if (mAutoTextEnabled) { int i = 0; @@ -326,8 +317,25 @@ public class Suggest implements Dictionary.WordCallback { String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); CharSequence autoText = AutoText.get(suggestedWord, 0, suggestedWord.length(), view); - // Is there an AutoText correction? + // Is there an AutoText (also known as Quick Fixes) correction? boolean canAdd = autoText != null; + // Capitalize as needed + final int autoTextLength = autoText != null ? autoText.length() : 0; + if (autoTextLength > 0 && (mIsAllUpperCase || mIsFirstCharCapitalized)) { + int poolSize = mStringPool.size(); + StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove( + poolSize - 1) : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + if (mIsAllUpperCase) { + sb.append(autoText.toString().toUpperCase()); + } else if (mIsFirstCharCapitalized) { + sb.append(Character.toUpperCase(autoText.charAt(0))); + if (autoTextLength > 1) { + sb.append(autoText.subSequence(1, autoTextLength)); + } + } + autoText = sb.toString(); + } // Is that correction already the current prediction (or original word)? canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i)); // Is that correction already the next predicted word? @@ -335,7 +343,10 @@ public class Suggest implements Dictionary.WordCallback { canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1)); } if (canAdd) { - mHaveCorrection = true; + if (DBG) { + Log.d(TAG, "Auto corrected by AUTOTEXT."); + } + mHasAutoCorrection = true; mSuggestions.add(i + 1, autoText); i++; } @@ -343,7 +354,7 @@ public class Suggest implements Dictionary.WordCallback { } } removeDupes(); - return mSuggestions; + return new SuggestedWords.Builder().addWords(mSuggestions, null); } public int[] getNextLettersFrequencies() { @@ -377,11 +388,11 @@ public class Suggest implements Dictionary.WordCallback { } } - public boolean hasMinimalCorrection() { - return mHaveCorrection; + public boolean hasAutoCorrection() { + return mHasAutoCorrection; } - private boolean compareCaseInsensitive(final String mLowerOriginalWord, + private boolean compareCaseInsensitive(final String mLowerOriginalWord, final char[] word, final int offset, final int length) { final int originalLength = mLowerOriginalWord.length(); if (originalLength == length && Character.isUpperCase(word[offset])) { @@ -395,6 +406,7 @@ public class Suggest implements Dictionary.WordCallback { return false; } + @Override public boolean addWord(final char[] word, final int offset, final int length, int freq, final int dicTypeId, final Dictionary.DataType dataType) { Dictionary.DataType dataTypeForLog = dataType; @@ -450,11 +462,10 @@ public class Suggest implements Dictionary.WordCallback { return true; } - System.arraycopy(priorities, pos, priorities, pos + 1, - prefMaxSuggestions - pos - 1); + System.arraycopy(priorities, pos, priorities, pos + 1, prefMaxSuggestions - pos - 1); priorities[pos] = freq; int poolSize = mStringPool.size(); - StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) + StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) : new StringBuilder(getApproxMaxWordLength()); sb.setLength(0); if (mIsAllUpperCase) { @@ -500,7 +511,7 @@ public class Suggest implements Dictionary.WordCallback { } public boolean isValidWord(final CharSequence word) { - if (word == null || word.length() == 0) { + if (word == null || word.length() == 0 || mMainDict == null) { return false; } return mMainDict.isValidWord(word) @@ -508,7 +519,7 @@ public class Suggest implements Dictionary.WordCallback { || (mAutoDictionary != null && mAutoDictionary.isValidWord(word)) || (mContactsDictionary != null && mContactsDictionary.isValidWord(word)); } - + private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) { int poolSize = mStringPool.size(); int garbageSize = suggestions.size(); @@ -529,6 +540,23 @@ public class Suggest implements Dictionary.WordCallback { public void close() { if (mMainDict != null) { mMainDict.close(); + mMainDict = null; + } + if (mUserDictionary != null) { + mUserDictionary.close(); + mUserDictionary = null; + } + if (mUserBigramDictionary != null) { + mUserBigramDictionary.close(); + mUserBigramDictionary = null; + } + if (mContactsDictionary != null) { + mContactsDictionary.close(); + mContactsDictionary = null; + } + if (mAutoDictionary != null) { + mAutoDictionary.close(); + mAutoDictionary = null; } } } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java new file mode 100644 index 000000000..f774ce3a5 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -0,0 +1,190 @@ +/* + * 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.view.inputmethod.CompletionInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +public class SuggestedWords { + public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, false, null); + + public final List<CharSequence> mWords; + public final boolean mIsApplicationSpecifiedCompletions; + public final boolean mTypedWordValid; + public final boolean mHasMinimalSuggestion; + public final List<SuggestedWordInfo> mSuggestedWordInfoList; + + private SuggestedWords(List<CharSequence> words, boolean isApplicationSpecifiedCompletions, + boolean typedWordValid, boolean hasMinamlSuggestion, + List<SuggestedWordInfo> suggestedWordInfoList) { + if (words != null) { + mWords = words; + } else { + mWords = Collections.emptyList(); + } + mIsApplicationSpecifiedCompletions = isApplicationSpecifiedCompletions; + mTypedWordValid = typedWordValid; + mHasMinimalSuggestion = hasMinamlSuggestion; + mSuggestedWordInfoList = suggestedWordInfoList; + } + + public int size() { + return mWords.size(); + } + + public CharSequence getWord(int pos) { + return mWords.get(pos); + } + + public boolean hasAutoCorrectionWord() { + return mHasMinimalSuggestion && size() > 1 && !mTypedWordValid; + } + + public boolean hasWordAboveAutoCorrectionScoreThreshold() { + return mHasMinimalSuggestion && ((size() > 1 && !mTypedWordValid) || mTypedWordValid); + } + + public static class Builder { + private List<CharSequence> mWords = new ArrayList<CharSequence>(); + private boolean mIsCompletions; + private boolean mTypedWordValid; + private boolean mHasMinimalSuggestion; + private List<SuggestedWordInfo> mSuggestedWordInfoList = + new ArrayList<SuggestedWordInfo>(); + + public Builder() { + // Nothing to do here. + } + + public Builder addWords(List<CharSequence> words, + List<SuggestedWordInfo> suggestedWordInfoList) { + final int N = words.size(); + for (int i = 0; i < N; ++i) { + SuggestedWordInfo suggestedWordInfo = null; + if (suggestedWordInfoList != null) { + suggestedWordInfo = suggestedWordInfoList.get(i); + } + if (suggestedWordInfo == null) { + suggestedWordInfo = new SuggestedWordInfo(); + } + addWord(words.get(i), suggestedWordInfo); + } + return this; + } + + public Builder addWord(CharSequence word) { + return addWord(word, null, false); + } + + public Builder addWord(CharSequence word, CharSequence debugString, + boolean isPreviousSuggestedWord) { + SuggestedWordInfo info = new SuggestedWordInfo(debugString, isPreviousSuggestedWord); + return addWord(word, info); + } + + private Builder addWord(CharSequence word, SuggestedWordInfo suggestedWordInfo) { + mWords.add(word); + mSuggestedWordInfoList.add(suggestedWordInfo); + return this; + } + + public Builder setApplicationSpecifiedCompletions(CompletionInfo[] infos) { + for (CompletionInfo info : infos) + addWord(info.getText()); + mIsCompletions = true; + return this; + } + + public Builder setTypedWordValid(boolean typedWordValid) { + mTypedWordValid = typedWordValid; + return this; + } + + public Builder setHasMinimalSuggestion(boolean hasMinamlSuggestion) { + mHasMinimalSuggestion = hasMinamlSuggestion; + return this; + } + + // Should get rid of the first one (what the user typed previously) from suggestions + // and replace it with what the user currently typed. + public Builder addTypedWordAndPreviousSuggestions(CharSequence typedWord, + SuggestedWords previousSuggestions) { + mWords.clear(); + mSuggestedWordInfoList.clear(); + final HashSet<String> alreadySeen = new HashSet<String>(); + addWord(typedWord, null, false); + alreadySeen.add(typedWord.toString()); + final int previousSize = previousSuggestions.size(); + for (int pos = 1; pos < previousSize; pos++) { + final String prevWord = previousSuggestions.getWord(pos).toString(); + // Filter out duplicate suggestion. + if (!alreadySeen.contains(prevWord)) { + addWord(prevWord, null, true); + alreadySeen.add(prevWord); + } + } + mIsCompletions = false; + mTypedWordValid = false; + mHasMinimalSuggestion = false; + return this; + } + + public SuggestedWords build() { + return new SuggestedWords(mWords, mIsCompletions, mTypedWordValid, + mHasMinimalSuggestion, mSuggestedWordInfoList); + } + + public int size() { + return mWords.size(); + } + + public CharSequence getWord(int pos) { + return mWords.get(pos); + } + } + + public static class SuggestedWordInfo { + private final CharSequence mDebugString; + private final boolean mPreviousSuggestedWord; + + public SuggestedWordInfo() { + mDebugString = ""; + mPreviousSuggestedWord = false; + } + + public SuggestedWordInfo(CharSequence debugString, boolean previousSuggestedWord) { + mDebugString = debugString; + mPreviousSuggestedWord = previousSuggestedWord; + } + + public String getDebugString() { + if (mDebugString == null) { + return ""; + } else { + return mDebugString.toString(); + } + } + + public boolean isPreviousSuggestedWord () { + return mPreviousSuggestedWord; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/SwipeTracker.java b/java/src/com/android/inputmethod/latin/SwipeTracker.java deleted file mode 100644 index 970e91965..000000000 --- a/java/src/com/android/inputmethod/latin/SwipeTracker.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.view.MotionEvent; - -class SwipeTracker { - private static final int NUM_PAST = 4; - private static final int LONGEST_PAST_TIME = 200; - - final EventRingBuffer mBuffer = new EventRingBuffer(NUM_PAST); - - private float mYVelocity; - private float mXVelocity; - - public void addMovement(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mBuffer.clear(); - return; - } - long time = ev.getEventTime(); - final int count = ev.getHistorySize(); - for (int i = 0; i < count; 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) { - final EventRingBuffer buffer = mBuffer; - while (buffer.size() > 0) { - long lastT = buffer.getTime(0); - if (lastT >= time - LONGEST_PAST_TIME) - break; - buffer.dropOldest(); - } - buffer.add(x, y, time); - } - - public void computeCurrentVelocity(int units) { - computeCurrentVelocity(units, Float.MAX_VALUE); - } - - public void computeCurrentVelocity(int units, float maxVelocity) { - final EventRingBuffer buffer = mBuffer; - final float oldestX = buffer.getX(0); - final float oldestY = buffer.getY(0); - final long oldestTime = buffer.getTime(0); - - float accumX = 0; - float accumY = 0; - final int count = buffer.size(); - for (int pos = 1; pos < count; pos++) { - final int dur = (int)(buffer.getTime(pos) - oldestTime); - if (dur == 0) continue; - float dist = buffer.getX(pos) - oldestX; - float vel = (dist / dur) * units; // pixels/frame. - if (accumX == 0) accumX = vel; - else accumX = (accumX + vel) * .5f; - - dist = buffer.getY(pos) - 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; - } - - static class EventRingBuffer { - private final int bufSize; - private final float xBuf[]; - private final float yBuf[]; - private final long timeBuf[]; - private int top; // points new event - private int end; // points oldest event - private int count; // the number of valid data - - public EventRingBuffer(int max) { - this.bufSize = max; - xBuf = new float[max]; - yBuf = new float[max]; - timeBuf = new long[max]; - clear(); - } - - public void clear() { - top = end = count = 0; - } - - public int size() { - return count; - } - - // Position 0 points oldest event - private int index(int pos) { - return (end + pos) % bufSize; - } - - private int advance(int index) { - return (index + 1) % bufSize; - } - - public void add(float x, float y, long time) { - xBuf[top] = x; - yBuf[top] = y; - timeBuf[top] = time; - top = advance(top); - if (count < bufSize) { - count++; - } else { - end = advance(end); - } - } - - public float getX(int pos) { - return xBuf[index(pos)]; - } - - public float getY(int pos) { - return yBuf[index(pos)]; - } - - public long getTime(int pos) { - return timeBuf[index(pos)]; - } - - public void dropOldest() { - count--; - end = advance(end); - } - } -}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index 9011191f1..f571f26d5 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -16,8 +16,9 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.keyboard.Key; + import android.content.Context; -import android.inputmethodservice.Keyboard.Key; import android.text.format.DateFormat; import android.util.Log; @@ -61,7 +62,7 @@ public class TextEntryState { SPACE_AFTER_PICKED, UNDO_COMMIT, CORRECTING, - PICKED_CORRECTION; + PICKED_CORRECTION, } private static State sState = State.UNKNOWN; @@ -96,7 +97,7 @@ public class TextEntryState { } try { sKeyLocationFile.close(); - // Write to log file + // Write to log file // Write timestamp, settings, String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime()) .toString() @@ -112,7 +113,7 @@ public class TextEntryState { sKeyLocationFile = null; sUserActionFile = null; } catch (IOException ioe) { - + // ignore } } @@ -134,16 +135,18 @@ public class TextEntryState { public static void backToAcceptedDefault(CharSequence typedWord) { if (typedWord == null) return; switch (sState) { - case SPACE_AFTER_ACCEPTED: - case PUNCTUATION_AFTER_ACCEPTED: - case IN_WORD: - sState = State.ACCEPTED_DEFAULT; - break; + case SPACE_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_ACCEPTED: + case IN_WORD: + sState = State.ACCEPTED_DEFAULT; + break; + default: + break; } displayState(); } - public static void acceptedTyped(CharSequence typedWord) { + public static void acceptedTyped(@SuppressWarnings("unused") CharSequence typedWord) { sWordNotInDictionaryCount++; sState = State.PICKED_SUGGESTION; displayState(); @@ -168,6 +171,13 @@ public class TextEntryState { displayState(); } + public static void onAbortCorrection() { + if (isCorrecting()) { + sState = State.START; + } + displayState(); + } + public static void typedCharacter(char c, boolean isSeparator) { boolean isSpace = c == ' '; switch (sState) { @@ -253,13 +263,13 @@ public class TextEntryState { } public static void keyPressedAt(Key key, int x, int y) { - if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) { - String out = - "KEY: " + (char) key.codes[0] - + " X: " + x + if (LOGGING && sKeyLocationFile != null && key.mCode >= 32) { + String out = + "KEY: " + (char) key.mCode + + " X: " + x + " Y: " + y - + " MX: " + (key.x + key.width / 2) - + " MY: " + (key.y + key.height / 2) + + " MX: " + (key.mX + key.mWidth / 2) + + " MY: " + (key.mY + key.mHeight / 2) + "\n"; try { sKeyLocationFile.write(out.getBytes()); diff --git a/java/src/com/android/inputmethod/latin/Tutorial.java b/java/src/com/android/inputmethod/latin/Tutorial.java deleted file mode 100644 index d3eaf30c6..000000000 --- a/java/src/com/android/inputmethod/latin/Tutorial.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Message; -import android.text.Layout; -import android.text.SpannableStringBuilder; -import android.text.StaticLayout; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.widget.PopupWindow; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -public class Tutorial implements OnTouchListener { - - private List<Bubble> mBubbles = new ArrayList<Bubble>(); - private View mInputView; - private LatinIME mIme; - private int[] mLocation = new int[2]; - - private static final int MSG_SHOW_BUBBLE = 0; - - private int mBubbleIndex; - - Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_SHOW_BUBBLE: - Bubble bubba = (Bubble) msg.obj; - bubba.show(mLocation[0], mLocation[1]); - break; - } - } - }; - - class Bubble { - Drawable bubbleBackground; - int x; - int y; - int width; - int gravity; - CharSequence text; - boolean dismissOnTouch; - boolean dismissOnClose; - PopupWindow window; - TextView textView; - View inputView; - - Bubble(Context context, View inputView, - int backgroundResource, int bx, int by, int textResource1, int textResource2) { - bubbleBackground = context.getResources().getDrawable(backgroundResource); - x = bx; - y = by; - width = (int) (inputView.getWidth() * 0.9); - this.gravity = Gravity.TOP | Gravity.LEFT; - text = new SpannableStringBuilder() - .append(context.getResources().getText(textResource1)) - .append("\n") - .append(context.getResources().getText(textResource2)); - this.dismissOnTouch = true; - this.dismissOnClose = false; - this.inputView = inputView; - window = new PopupWindow(context); - window.setBackgroundDrawable(null); - LayoutInflater inflate = - (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - textView = (TextView) inflate.inflate(R.layout.bubble_text, null); - textView.setBackgroundDrawable(bubbleBackground); - textView.setText(text); - //textView.setText(textResource1); - window.setContentView(textView); - window.setFocusable(false); - window.setTouchable(true); - window.setOutsideTouchable(false); - } - - private int chooseSize(PopupWindow pop, View parentView, CharSequence text, TextView tv) { - int wid = tv.getPaddingLeft() + tv.getPaddingRight(); - int ht = tv.getPaddingTop() + tv.getPaddingBottom(); - - /* - * Figure out how big the text would be if we laid it out to the - * full width of this view minus the border. - */ - int cap = width - wid; - - Layout l = new StaticLayout(text, tv.getPaint(), cap, - Layout.Alignment.ALIGN_NORMAL, 1, 0, true); - float max = 0; - for (int i = 0; i < l.getLineCount(); i++) { - max = Math.max(max, l.getLineWidth(i)); - } - - /* - * Now set the popup size to be big enough for the text plus the border. - */ - pop.setWidth(width); - pop.setHeight(ht + l.getHeight()); - return l.getHeight(); - } - - void show(int offx, int offy) { - int textHeight = chooseSize(window, inputView, text, textView); - offy -= textView.getPaddingTop() + textHeight; - if (inputView.getVisibility() == View.VISIBLE - && inputView.getWindowVisibility() == View.VISIBLE) { - try { - if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) offy -= window.getHeight(); - if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) offx -= window.getWidth(); - textView.setOnTouchListener(new View.OnTouchListener() { - public boolean onTouch(View view, MotionEvent me) { - Tutorial.this.next(); - return true; - } - }); - window.showAtLocation(inputView, Gravity.NO_GRAVITY, x + offx, y + offy); - } catch (Exception e) { - // Input view is not valid - } - } - } - - void hide() { - if (window.isShowing()) { - textView.setOnTouchListener(null); - window.dismiss(); - } - } - - boolean isShowing() { - return window.isShowing(); - } - } - - public Tutorial(LatinIME ime, LatinKeyboardView inputView) { - Context context = inputView.getContext(); - mIme = ime; - int inputWidth = inputView.getWidth(); - final int x = inputWidth / 20; // Half of 1/10th - Bubble bWelcome = new Bubble(context, inputView, - R.drawable.dialog_bubble_step02, x, 0, - R.string.tip_to_open_keyboard, R.string.touch_to_continue); - mBubbles.add(bWelcome); - Bubble bAccents = new Bubble(context, inputView, - R.drawable.dialog_bubble_step02, x, 0, - R.string.tip_to_view_accents, R.string.touch_to_continue); - mBubbles.add(bAccents); - Bubble b123 = new Bubble(context, inputView, - R.drawable.dialog_bubble_step07, x, 0, - R.string.tip_to_open_symbols, R.string.touch_to_continue); - mBubbles.add(b123); - Bubble bABC = new Bubble(context, inputView, - R.drawable.dialog_bubble_step07, x, 0, - R.string.tip_to_close_symbols, R.string.touch_to_continue); - mBubbles.add(bABC); - Bubble bSettings = new Bubble(context, inputView, - R.drawable.dialog_bubble_step07, x, 0, - R.string.tip_to_launch_settings, R.string.touch_to_continue); - mBubbles.add(bSettings); - Bubble bDone = new Bubble(context, inputView, - R.drawable.dialog_bubble_step02, x, 0, - R.string.tip_to_start_typing, R.string.touch_to_finish); - mBubbles.add(bDone); - mInputView = inputView; - } - - void start() { - mInputView.getLocationInWindow(mLocation); - mBubbleIndex = -1; - mInputView.setOnTouchListener(this); - next(); - } - - boolean next() { - if (mBubbleIndex >= 0) { - // If the bubble is not yet showing, don't move to the next. - if (!mBubbles.get(mBubbleIndex).isShowing()) { - return true; - } - // Hide all previous bubbles as well, as they may have had a delayed show - for (int i = 0; i <= mBubbleIndex; i++) { - mBubbles.get(i).hide(); - } - } - mBubbleIndex++; - if (mBubbleIndex >= mBubbles.size()) { - mInputView.setOnTouchListener(null); - mIme.sendDownUpKeyEvents(-1); // Inform the setupwizard that tutorial is in last bubble - mIme.tutorialDone(); - return false; - } - if (mBubbleIndex == 3 || mBubbleIndex == 4) { - mIme.mKeyboardSwitcher.toggleSymbols(); - } - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(mBubbleIndex)), 500); - return true; - } - - void hide() { - for (int i = 0; i < mBubbles.size(); i++) { - mBubbles.get(i).hide(); - } - mInputView.setOnTouchListener(null); - } - - boolean close() { - mHandler.removeMessages(MSG_SHOW_BUBBLE); - hide(); - return true; - } - - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - next(); - } - return true; - } -} diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java index 67d9c0bcf..4750fb991 100644 --- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -16,10 +16,6 @@ package com.android.inputmethod.latin; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -30,6 +26,10 @@ import android.os.AsyncTask; import android.provider.BaseColumns; import android.util.Log; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + /** * Stores all the pairs user types in databases. Prune the database if the size * gets too big. Unlike AutoDictionary, it even stores the pairs that are already @@ -108,25 +108,25 @@ public class UserBigramDictionary extends ExpandableDictionary { private static DatabaseHelper sOpenHelper = null; private static class Bigram { - String word1; - String word2; - int frequency; + public final String mWord1; + public final String mWord2; + public final int frequency; Bigram(String word1, String word2, int frequency) { - this.word1 = word1; - this.word2 = word2; + this.mWord1 = word1; + this.mWord2 = word2; this.frequency = frequency; } @Override public boolean equals(Object bigram) { Bigram bigram2 = (Bigram) bigram; - return (word1.equals(bigram2.word1) && word2.equals(bigram2.word2)); + return (mWord1.equals(bigram2.mWord1) && mWord2.equals(bigram2.mWord2)); } @Override public int hashCode() { - return (word1 + " " + word2).hashCode(); + return (mWord1 + " " + mWord2).hashCode(); } } @@ -357,7 +357,7 @@ public class UserBigramDictionary extends ExpandableDictionary { Cursor c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID }, MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND " + MAIN_COLUMN_LOCALE + "=?", - new String[] { bi.word1, bi.word2, mLocale }, null, null, null); + new String[] { bi.mWord1, bi.mWord2, mLocale }, null, null, null); int pairId; if (c.moveToFirst()) { @@ -368,7 +368,7 @@ public class UserBigramDictionary extends ExpandableDictionary { } else { // new pair Long pairIdLong = db.insert(MAIN_TABLE_NAME, null, - getContentValues(bi.word1, bi.word2, mLocale)); + getContentValues(bi.mWord1, bi.mWord2, mLocale)); pairId = pairIdLong.intValue(); } c.close(); diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java index 49b95e9aa..56ee5b9e7 100644 --- a/java/src/com/android/inputmethod/latin/UserDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserDictionary.java @@ -21,18 +21,21 @@ import android.content.ContentValues; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; +import android.net.Uri; import android.provider.UserDictionary.Words; public class UserDictionary extends ExpandableDictionary { - private static final String[] PROJECTION = { - Words._ID, + private static final String[] PROJECTION_QUERY = { Words.WORD, - Words.FREQUENCY + Words.FREQUENCY, }; - private static final int INDEX_WORD = 1; - private static final int INDEX_FREQUENCY = 2; + private static final String[] PROJECTION_ADD = { + Words._ID, + Words.FREQUENCY, + Words.LOCALE, + }; private ContentObserver mObserver; private String mLocale; @@ -66,7 +69,7 @@ public class UserDictionary extends ExpandableDictionary { @Override public void loadDictionaryAsync() { Cursor cursor = getContext().getContentResolver() - .query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)", + .query(Words.CONTENT_URI, PROJECTION_QUERY, "(locale IS NULL) or (locale=?)", new String[] { mLocale }, null); addWords(cursor); } @@ -80,7 +83,7 @@ public class UserDictionary extends ExpandableDictionary { * @TODO use a higher or float range for frequency */ @Override - public synchronized void addWord(String word, int frequency) { + public synchronized void addWord(final String word, final int frequency) { // Force load the dictionary here synchronously if (getRequiresReload()) loadDictionaryAsync(); // Safeguard against adding long words. Can cause stack overflow. @@ -97,8 +100,24 @@ public class UserDictionary extends ExpandableDictionary { final ContentResolver contentResolver = getContext().getContentResolver(); new Thread("addWord") { + @Override public void run() { - contentResolver.insert(Words.CONTENT_URI, values); + Cursor cursor = contentResolver.query(Words.CONTENT_URI, PROJECTION_ADD, + "word=? and ((locale IS NULL) or (locale=?))", + new String[] { word, mLocale }, null); + if (cursor != null && cursor.moveToFirst()) { + String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE)); + // If locale is null, we will not override the entry. + if (locale != null && locale.equals(mLocale.toString())) { + long id = cursor.getLong(cursor.getColumnIndex(Words._ID)); + Uri uri = Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id)); + // Update the entry with new frequency value. + contentResolver.update(uri, values, null, null); + } + } else { + // Insert new entry. + contentResolver.insert(Words.CONTENT_URI, values); + } } }.start(); @@ -122,9 +141,11 @@ public class UserDictionary extends ExpandableDictionary { final int maxWordLength = getMaxWordLength(); if (cursor.moveToFirst()) { + final int indexWord = cursor.getColumnIndex(Words.WORD); + final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY); while (!cursor.isAfterLast()) { - String word = cursor.getString(INDEX_WORD); - int frequency = cursor.getInt(INDEX_FREQUENCY); + String word = cursor.getString(indexWord); + int frequency = cursor.getInt(indexFrequency); // Safeguard against adding really long words. Stack may overflow due // to recursion if (word.length() < maxWordLength) { diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java new file mode 100644 index 000000000..e980d3a30 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -0,0 +1,442 @@ +/* + * 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.inputmethodservice.InputMethodService; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Utils { + private static final String TAG = Utils.class.getSimpleName(); + private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4; + private static boolean DBG = LatinImeLogger.sDBG; + + /** + * 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 (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; + } + } + } + } + + public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm) { + return imm.getEnabledInputMethodList().size() > 1 + // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled + // input method subtype (The current IME should be LatinIME.) + || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; + } + + public static String getInputMethodId(InputMethodManager imm, String packageName) { + for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) { + if (imi.getPackageName().equals(packageName)) + return imi.getId(); + } + throw new RuntimeException("Can not find input method id for " + packageName); + } + + public static boolean shouldBlockedBySafetyNetForAutoCorrection(SuggestedWords suggestions, + Suggest suggest) { + // Safety net for auto correction. + // Actually if we hit this safety net, it's actually a bug. + if (suggestions.size() <= 1 || suggestions.mTypedWordValid) return false; + // If user selected aggressive auto correction mode, there is no need to use the safety + // net. + if (suggest.isAggressiveAutoCorrectionMode()) return false; + CharSequence typedWord = suggestions.getWord(0); + // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH, + // we should not use net because relatively edit distance can be big. + if (typedWord.length() < MINIMUM_SAFETY_NET_CHAR_LENGTH) return false; + CharSequence candidateWord = suggestions.getWord(1); + final int typedWordLength = typedWord.length(); + final int maxEditDistanceOfNativeDictionary = typedWordLength < 5 ? 2 : typedWordLength / 2; + final int distance = Utils.editDistance(typedWord, candidateWord); + if (DBG) { + Log.d(TAG, "Autocorrected edit distance = " + distance + + ", " + maxEditDistanceOfNativeDictionary); + } + if (distance > maxEditDistanceOfNativeDictionary) { + if (DBG) { + Log.d(TAG, "Safety net: before = " + typedWord + ", after = " + candidateWord); + Log.w(TAG, "(Error) The edit distance of this correction exceeds limit. " + + "Turning off auto-correction."); + } + return true; + } else { + return false; + } + } + + /* package */ static class RingCharBuffer { + private static RingCharBuffer sRingCharBuffer = new RingCharBuffer(); + private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; + private static final int INVALID_COORDINATE = -2; + /* package */ static final int BUFSIZE = 20; + private InputMethodService mContext; + private boolean mEnabled = false; + private boolean mUsabilityStudy = false; + private int mEnd = 0; + /* package */ int mLength = 0; + private char[] mCharBuf = new char[BUFSIZE]; + private int[] mXBuf = new int[BUFSIZE]; + private int[] mYBuf = new int[BUFSIZE]; + + private RingCharBuffer() { + // Intentional empty constructor for singleton. + } + public static RingCharBuffer getInstance() { + return sRingCharBuffer; + } + public static RingCharBuffer init(InputMethodService context, boolean enabled, + boolean usabilityStudy) { + sRingCharBuffer.mContext = context; + sRingCharBuffer.mEnabled = enabled || usabilityStudy; + sRingCharBuffer.mUsabilityStudy = usabilityStudy; + UsabilityStudyLogUtils.getInstance().init(context); + return sRingCharBuffer; + } + private int normalize(int in) { + int ret = in % BUFSIZE; + return ret < 0 ? ret + BUFSIZE : ret; + } + public void push(char c, int x, int y) { + if (!mEnabled) return; + if (mUsabilityStudy) { + UsabilityStudyLogUtils.getInstance().writeChar(c, x, y); + } + mCharBuf[mEnd] = c; + mXBuf[mEnd] = x; + mYBuf[mEnd] = y; + mEnd = normalize(mEnd + 1); + if (mLength < BUFSIZE) { + ++mLength; + } + } + public char pop() { + if (mLength < 1) { + return PLACEHOLDER_DELIMITER_CHAR; + } else { + mEnd = normalize(mEnd - 1); + --mLength; + return mCharBuf[mEnd]; + } + } + public char getLastChar() { + if (mLength < 1) { + return PLACEHOLDER_DELIMITER_CHAR; + } else { + return mCharBuf[normalize(mEnd - 1)]; + } + } + public int getPreviousX(char c, int back) { + int index = normalize(mEnd - 2 - back); + if (mLength <= back + || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { + return INVALID_COORDINATE; + } else { + return mXBuf[index]; + } + } + public int getPreviousY(char c, int back) { + int index = normalize(mEnd - 2 - back); + if (mLength <= back + || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { + return INVALID_COORDINATE; + } else { + return mYBuf[index]; + } + } + public String getLastString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mLength; ++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() { + mLength = 0; + } + } + + public static int editDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("editDistance: Arguments should not be null."); + } + final int sl = s.length(); + final int tl = t.length(); + int[][] dp = new int [sl + 1][tl + 1]; + for (int i = 0; i <= sl; i++) { + dp[i][0] = i; + } + for (int j = 0; j <= tl; j++) { + dp[0][j] = j; + } + for (int i = 0; i < sl; ++i) { + for (int j = 0; j < tl; ++j) { + if (Character.toLowerCase(s.charAt(i)) == Character.toLowerCase(t.charAt(j))) { + dp[i + 1][j + 1] = dp[i][j]; + } else { + dp[i + 1][j + 1] = 1 + Math.min(dp[i][j], + Math.min(dp[i + 1][j], dp[i][j + 1])); + } + } + } + return dp[sl][tl]; + } + + // In dictionary.cpp, getSuggestion() method, + // suggestion scores are computed using the below formula. + // original score (called 'frequency') + // := pow(mTypedLetterMultiplier (this is defined 2), + // (the number of matched characters between typed word and suggested word)) + // * (individual word's score which defined in the unigram dictionary, + // and this score is defined in range [0, 255].) + // * (when before.length() == after.length(), + // mFullWordMultiplier (this is defined 2)) + // So, maximum original score is pow(2, before.length()) * 255 * 2 + // So, we can normalize original score by dividing this value. + private static final int MAX_INITIAL_SCORE = 255; + private static final int TYPED_LETTER_MULTIPLIER = 2; + private static final int FULL_WORD_MULTIPLYER = 2; + public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) { + final int beforeLength = before.length(); + final int afterLength = after.length(); + if (beforeLength == 0 || afterLength == 0) return 0; + final int distance = editDistance(before, after); + // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character + // correction. + final double maximumScore = MAX_INITIAL_SCORE + * Math.pow(TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength)) + * FULL_WORD_MULTIPLYER; + // add a weight based on edit distance. + // distance <= max(afterLength, beforeLength) == afterLength, + // so, 0 <= distance / afterLength <= 1 + final double weight = 1.0 - (double) distance / afterLength; + return (score / maximumScore) * weight; + } + + public static class UsabilityStudyLogUtils { + private static final String TAG = "UsabilityStudyLogUtils"; + private static final String FILENAME = "log.txt"; + private static final UsabilityStudyLogUtils sInstance = + new UsabilityStudyLogUtils(); + private final Handler mLoggingHandler; + private File mFile; + private File mDirectory; + private InputMethodService mIms; + private PrintWriter mWriter; + private final Date mDate; + private final SimpleDateFormat mDateFormat; + + private UsabilityStudyLogUtils() { + mDate = new Date(); + mDateFormat = new SimpleDateFormat("dd MMM HH:mm:ss.SSS"); + + HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task", + Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + mLoggingHandler = new Handler(handlerThread.getLooper()); + } + + public static UsabilityStudyLogUtils getInstance() { + return sInstance; + } + + public void init(InputMethodService ims) { + mIms = ims; + mDirectory = ims.getFilesDir(); + } + + private void createLogFileIfNotExist() { + if ((mFile == null || !mFile.exists()) + && (mDirectory != null && mDirectory.exists())) { + try { + mWriter = getPrintWriter(mDirectory, FILENAME, false); + } catch (IOException e) { + Log.e(TAG, "Can't create log file."); + } + } + } + + public void writeBackSpace() { + UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0"); + } + + public void writeChar(char c, int x, int y) { + String inputChar = String.valueOf(c); + switch (c) { + case '\n': + inputChar = "<enter>"; + break; + case '\t': + inputChar = "<tab>"; + break; + case ' ': + inputChar = "<space>"; + break; + } + UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y); + LatinImeLogger.onPrintAllUsabilityStudyLogs(); + } + + public void write(final String log) { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + createLogFileIfNotExist(); + final long currentTime = System.currentTimeMillis(); + mDate.setTime(currentTime); + + final String printString = String.format("%s\t%d\t%s\n", + mDateFormat.format(mDate), currentTime, log); + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Write: " + log); + } + mWriter.print(printString); + } + }); + } + + public void printAll() { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + mWriter.flush(); + StringBuilder sb = new StringBuilder(); + BufferedReader br = getBufferedReader(); + String line; + try { + while ((line = br.readLine()) != null) { + sb.append('\n'); + sb.append(line); + } + } catch (IOException e) { + Log.e(TAG, "Can't read log file."); + } finally { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "output all logs\n" + sb.toString()); + } + mIms.getCurrentInputConnection().commitText(sb.toString(), 0); + try { + br.close(); + } catch (IOException e) { + // ignore. + } + } + } + }); + } + + public void clearAll() { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + if (mFile != null && mFile.exists()) { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Delete log file."); + } + mFile.delete(); + mWriter.close(); + } + } + }); + } + + private BufferedReader getBufferedReader() { + createLogFileIfNotExist(); + try { + return new BufferedReader(new FileReader(mFile)); + } catch (FileNotFoundException e) { + return null; + } + } + + private PrintWriter getPrintWriter( + File dir, String filename, boolean renew) throws IOException { + mFile = new File(dir, filename); + if (mFile.exists()) { + if (renew) { + mFile.delete(); + } + } + return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */); + } + } +} |